Linux--应用层自定义协议与序列化(例子:网络计算器)

ops/2024/9/23 7:37:45/

目录

0.上篇文章

1.应用层

再谈一谈协议

网络版计算器

序列化 和 反序列化

 2.重新理解 read、 write、 recv、 send 和 tcp 为什么支持全双工

3.网络计算器(代码实现) 

3.1序列化&反序列化的接口

3.2 项目逻辑

3.3 代码 

3.3.1辅助库

3.3.2 基于TCP的Socket封装

3.3.3 会话层(网络

 3.3.4 表示层

 3.3.5 应用层(业务)&&计算器方法

Protocol.hpp:协议框架

NetCal.hpp:计算器方法

3.3.6服务器 和 客户端 的启动

3.4 代码测试


0.上篇文章

Linux--Socket 编程 UDP(简单的回显服务器和客户端代码)-CSDN博客

Linux--Socket 编程 TCP(Echo Server)_linux socket tcp server程序-CSDN博客

1.应用层

        我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用
层。

再谈一谈协议

        协议是一种 "约定". socket api 的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接
收的. 如果我们要传输一些 "结构化的数据" 怎么办呢?(发的是结构体变量,接收的也是结构体变量)

其实, 协议就是双方约定好的结构化的数据(下面计算计算器的例子)


网络版计算器

例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过
去, 然后由服务器进行计算, 最后再把结果返回给客户端.
约定方案一:
• 客户端发送一个形如"1+1"的字符串;
• 这个字符串中有两个操作数, 都是整形;
• 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
• 数字和运算符之间没有空格;
• ...
约定方案二:
• 定义结构体来表示我们需要交互的信息;
• 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按
照相同的规则把字符串转化回结构体;
• 这个过程叫做 "序列化" 和 "反序列化"


序列化 和 反序列化

        发的消息其实就是一个结构体,不同的编程语言、编译器或硬件平台可能会以不同的方式在内存中组织结构体。例如,结构体中的字段可能会根据字段类型、对齐要求和编译器优化策略进行填充或重排。因此,直接传输结构体可能会导致接收方无法正确解释数据。所以我们需要序列化和反序列化。

        因此我们在传消息的时候要按照规则序列化(一变多)后再进行网络传输,到另一端后再按照规则反序列化由多变一,线程结构化数据,方便上层处理。因此有了序列化和反序列化,这一层软件层,对于上层(应用层)来说方便数据的处理,对于下层(传输层)来说方便网络的传输

无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据,
在另一端能够正确的进行解析, 就是 ok 的. 这种约定, 就是 应用层协议
但是, 为了深刻理解协议, 我打算自定义实现一下协议的过程。
• 我们采用方案 2, 我们也要体现协议定制的细节
• 我们要引入序列化和反序列化, 只不过我直接采用现成的方案 -- jsoncpp库

•我们要对 socket 进行字节流的读取处理
 


 2.重新理解 read、 write、 recv、 send 和 tcp 为什么支持全双工

1.发送和接收缓冲区:在传输层创建tcp套接字,每个TCP套接字在内核中都有一个发送缓冲区和一个接收缓冲区。发送缓冲区用于暂存待发送的数据,接收缓冲区用于暂存已接收但尚未被应用程序读取的数据。tcp支持全双工通信的本质原因:这两个缓冲区是独立的,允许数据在发送和接收方向上同时流动。read、 write、 recv、 send,本质也是拷贝到缓冲区/从缓冲区拷贝数据;发送数据的本质:是从发送缓冲区把数据通过协议栈和网络拷贝给接收方
的接收缓冲区;

2.滑动窗口机制TCP通过滑动窗口机制来实现流量控制和拥塞控制,这也是TCP协议叫做传输控制协议的原因。发送方会维护一个接收窗口,该窗口的大小表示接收方当前能够接收的数据量。发送方根据接收窗口的大小来发送数据,确保不会因为发送过快而导致接收方缓冲区溢出。这种机制保证了数据的可靠传输,同时也支持了全双工通信。

3.非阻塞和异步I/O:虽然readwriterecvsend等函数在默认情况下是阻塞的,但Linux提供了多种机制(如select、poll、epoll、非阻塞套接字等)来实现非阻塞和异步I/O。这些机制允许应用程序在等待I/O操作完成时继续执行其他任务,从而提高了程序的效率和响应性。在非阻塞或异步模式下,TCP连接仍然支持全双工通信,只是应用程序需要处理更多的I/O事件和状态变化。有人从缓冲区中拿数据,有人从缓冲区中填数据,这就是一个生产者消费者模型!IO函数要进行阻塞,就是为例维护消费端和生产端的同步关系。

所以:
• 在任何一台主机上, TCP 连接既有发送缓冲区, 又有接受缓冲区, 所以, 在内核
中, 可以在发消息的同时, 也可以收消息, 即全双工
• 这就是为什么一个 tcp sockfd 读写都是它的原因
• 实际数据什么时候发, 发多少, 出错了怎么办, 由 TCP 控制, 所以 TCP 叫做传
输控制协议(关于面向字节流:客户端发的,不一定全部是服务器收到)


3.网络计算器(代码实现) 


3.1序列化&反序列化的接口

Jsoncpp
Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。 它提供了将 JSON 数据序列化为字
符串以及从字符串反序列化为 C++ 数据结构的功能。 Jsoncpp 是开源的, 广泛用于各
种需要处理 JSON 数据的 C++ 项目中。
特性:
1. 简单易用: Jsoncpp 提供了直观的 API, 使得处理 JSON 数据变得简单。
2. 高性能: Jsoncpp 的性能经过优化, 能够高效地处理大量 JSON 数据。
3. 全面支持: 支持 JSON 标准中的所有数据类型, 包括对象、 数组、 字符串、 数
字、 布尔值和 null。
4. 错误处理: 在解析 JSON 数据时, Jsoncpp 提供了详细的错误信息和位置, 方便
开发者调试。
当使用 Jsoncpp 库进行 JSON 的序列化和反序列化时, 确实存在不同的做法和工具类
可供选择。 以下是对 Jsoncpp 中序列化和反序列化操作的详细介绍:
安装:
 

C++
ubuntu: sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

序列化
序列化指的是将数据结构或对象转换为一种格式, 以便在网络上传输或存储到文件
中。 Jsoncpp 提供了多种方式进行序列化:
eg

        1.使用 Json::Value 的 toStyledString 方法:

        优点: 将 Json::Value 对象直接转换为格式化的 JSON 字符串

C++
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{Json::Value root;root["name"] = "joe";root["sex"] = "男";std::string s = root.toStyledString();std::cout << s << std::endl;return 0;
} $
. / test.exe
{"name" : "joe","sex" : "男"
}

        2. 使用 Json::StreamWriter:

        优点: 提供了更多的定制选项, 如缩进、 换行符等。
 

#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{Json::Value root;root["name"] = "joe";root["sex"] = "男";Json::StreamWriterBuilder wbuilder; // StreamWriter 的工厂std::unique_ptr<Json::StreamWriter>writer(wbuilder.newStreamWriter());std::stringstream ss;writer->write(root, &ss);std::cout << ss.str() << std::endl;return 0;
} $
. / test.exe
{"name" : "joe","sex" : "男"
}

        3.使用 Json::FastWriter

        优点: 比 StyledWriter 更快, 因为它不添加额外的空格和换行符。

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{Json::Value root;root["name"] = "joe";root["sex"] = "男";Json::FastWriter writer;std::string s = writer.write(root);std::cout << s << std::endl;return 0;
}test.exe
{"name":"joe", "sex" : "男"}#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>int main(){Json::Value root;root["name"] = "joe";root["sex"] = "男";// Json::FastWriter writer;Json::StyledWriter writer;std::string s = writer.write(root);std::cout << s << std::endl;return 0;} $. / test.exe{"name" : "joe","sex" : "男"}

反序列化
反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。 Jsoncpp 提供
了以下方法进行反序列化:
        使用 Json::Reader:

        优点: 提供详细的错误信息和位置, 方便调试。

C++
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main() {// JSON 字符串std::string json_string = "{\"name\":\"张三\",\"age\":30, \"city\":\"北京\"}";// 解析 JSON 字符串Json::Reader reader;Json::Value root;// 从字符串中读取 JSON 数据bool parsingSuccessful = reader.parse(json_string,root);if (!parsingSuccessful) {// 解析失败, 输出错误信息std::cout << "Failed to parse JSON: " <<reader.getFormattedErrorMessages() << std::endl;return 1;} // 访问 JSON 数据std::string name = root["name"].asString();int age = root["age"].asInt();std::string city = root["city"].asString();// 输出结果std::cout << "Name: " << name << std::endl;std::cout << "Age: " << age << std::endl;std::cout << "City: " << city << std::endl;return 0;
} $
. / test.exe
Name : 张三
Age : 30
City : 北京

类型检查

• bool isNull(): 检查值是否为 null。
• bool isBool(): 检查值是否为布尔类型。
• bool isInt(): 检查值是否为整数类型。
• bool isInt64(): 检查值是否为 64 位整数类型。
• bool isUInt(): 检查值是否为无符号整数类型。
• bool isUInt64(): 检查值是否为 64 位无符号整数类型。
• bool isIntegral(): 检查值是否为整数或可转换为整数的浮点数。
• bool isDouble(): 检查值是否为双精度浮点数。
• bool isNumeric(): 检查值是否为数字(整数或浮点数) 。
• bool isString(): 检查值是否为字符串。
• bool isArray(): 检查值是否为数组。
• bool isObject(): 检查值是否为对象(即键值对的集合) 。

3.2 项目逻辑

        tcp服务器(Tcpserver)处理请求,会做IO,那么就调用IO服务(Service),IO服务处理做序列化和反序列化,还要处理业务,这时就会调用业务处理服务(NetCal网络计算器)。网络->IO->业务,三层分别对应(会话层,表示层,应用层)

        无论是服务端还是客户端,都要遵循协议(Protocol),根据协议所规定的结构化数据进行通信。

3.3 代码 


3.3.1辅助库

用于封装和处理 IP 地址及其端口号:InetAddr.hpp

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>class InetAddr
{
private:void ToHost(const struct sockaddr_in &addr){_port = ntohs(addr.sin_port);// _ip = inet_ntoa(addr.sin_addr);char ip_buf[32];// inet_p to n// p: process// n: net// inet_pton(int af, const char *src, void *dst);// inet_pton(AF_INET, ip.c_str(), &addr.sin_addr.s_addr);::inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf));_ip = ip_buf;}public:InetAddr(const struct sockaddr_in &addr):_addr(addr){ToHost(addr);}InetAddr(){}bool operator == (const InetAddr &addr){return (this->_ip == addr._ip && this->_port == addr._port);}std::string Ip(){return _ip;}uint16_t Port(){return _port;}struct sockaddr_in Addr(){return _addr;}std::string AddrStr(){return _ip + ":" + std::to_string(_port);}~InetAddr(){}private:std::string _ip;uint16_t _port;struct sockaddr_in _addr;
};

日志库:Log.hpp

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <ctime>
#include <cstdarg>
#include <fstream>
#include <cstring>
#include <pthread.h>
#include "LockGuard.hpp"namespace log_ns
{enum{DEBUG = 1,INFO,WARNING,ERROR,FATAL};std::string LevelToString(int level){switch (level){case DEBUG:return "DEBUG";case INFO:return "INFO";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return "UNKNOWN";}}std::string GetCurrTime(){time_t now = time(nullptr);struct tm *curr_time = localtime(&now);char buffer[128];snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",curr_time->tm_year + 1900,curr_time->tm_mon + 1,curr_time->tm_mday,curr_time->tm_hour,curr_time->tm_min,curr_time->tm_sec);return buffer;}class logmessage{public:std::string _level;pid_t _id;std::string _filename;int _filenumber;std::string _curr_time;std::string _message_info;};#define SCREEN_TYPE 1
#define FILE_TYPE 2const std::string glogfile = "./log.txt";pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;// log.logMessage("", 12, INFO, "this is a %d message ,%f, %s hellwrodl", x, , , );class Log{public:Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE){}void Enable(int type){_type = type;}void FlushLogToScreen(const logmessage &lg){printf("[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curr_time.c_str(),lg._message_info.c_str());}void FlushLogToFile(const logmessage &lg){std::ofstream out(_logfile, std::ios::app);if (!out.is_open())return;char logtxt[2048];snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curr_time.c_str(),lg._message_info.c_str());out.write(logtxt, strlen(logtxt));out.close();}void FlushLog(const logmessage &lg){// 加过滤逻辑 --- TODOLockGuard lockguard(&glock);switch (_type){case SCREEN_TYPE:FlushLogToScreen(lg);break;case FILE_TYPE:FlushLogToFile(lg);break;}}void logMessage(std::string filename, int filenumber, int level, const char *format, ...){logmessage lg;lg._level = LevelToString(level);lg._id = getpid();lg._filename = filename;lg._filenumber = filenumber;lg._curr_time = GetCurrTime();va_list ap;va_start(ap, format);char log_info[1024];vsnprintf(log_info, sizeof(log_info), format, ap);va_end(ap);lg._message_info = log_info;// 打印出来日志FlushLog(lg);}~Log(){}private:int _type;std::string _logfile;};Log lg;#define LOG(Level, Format, ...)                                        \do                                                                 \{                                                                  \lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \} while (0)
#define EnableScreen()          \do                          \{                           \lg.Enable(SCREEN_TYPE); \} while (0)
#define EnableFILE()          \do                        \{                         \lg.Enable(FILE_TYPE); \} while (0)
};

给日志库上锁,保证线程安全:LockGuard.hpp

#include <pthread.h>class LockGuard
{
public:LockGuard(pthread_mutex_t *mutex):_mutex(mutex){pthread_mutex_lock(_mutex);}~LockGuard(){pthread_mutex_unlock(_mutex);}
private:pthread_mutex_t *_mutex;
};


3.3.2 基于TCP的Socket封装

使得Socket的使用更加面向对象。 

#include <iostream>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <pthread.h>
#include <memory>#include "Log.hpp"
#include "InetAddr.hpp"
//以下是对socket的封装,方便面向对象式的使用socket
namespace socket_ns
{using namespace log_ns;class Socket;using SockSPtr = std::shared_ptr<Socket>;//Socket是虚基类,实际上是拿TcpSocket//定义的对象enum//创建失败的常量{SOCKET_ERROR = 1,BIND_ERROR,LISTEN_ERR};const static int gblcklog = 8;//监听队列默认大小。// 模版方法模式class Socket{public:virtual void CreateSocketOrDie() = 0;virtual void CreateBindOrDie(uint16_t port) = 0;virtual void CreateListenOrDie(int backlog = gblcklog) = 0;virtual SockSPtr Accepter(InetAddr *cliaddr) = 0;virtual bool Conntecor(const std::string &peerip, uint16_t peerport) = 0;virtual int Sockfd() = 0;virtual void Close() = 0;virtual ssize_t Recv(std::string *out) = 0;//进行读取virtual ssize_t Send(const std::string &in) = 0;//进行发送public:void BuildListenSocket(uint16_t port)//创建监听套接字{CreateSocketOrDie();CreateBindOrDie(port);CreateListenOrDie();}//创建客户端套接字bool BuildClientSocket(const std::string &peerip, uint16_t peerport){CreateSocketOrDie();return Conntecor(peerip, peerport);}// void BuildUdpSocket()// {}};class TcpSocket : public Socket{public:TcpSocket(){}//监听套接字初始化/构造函数式的初始化TcpSocket(int sockfd) : _sockfd(sockfd){}~TcpSocket(){}void CreateSocketOrDie() override{// 1. 创建socket_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){LOG(FATAL, "socket create error\n");exit(SOCKET_ERROR);}LOG(INFO, "socket create success, sockfd: %d\n", _sockfd); // 3}void CreateBindOrDie(uint16_t port) override//bind{struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;// 2. bind sockfd 和 Socket addrif (::bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0){LOG(FATAL, "bind error\n");exit(BIND_ERROR);}LOG(INFO, "bind success, sockfd: %d\n", _sockfd); // 3}//监听void CreateListenOrDie(int backlog) override{// 3. 因为tcp是面向连接的,tcp需要未来不断地能够做到获取连接if (::listen(_sockfd, gblcklog) < 0){LOG(FATAL, "listen error\n");exit(LISTEN_ERR);}LOG(INFO, "listen success\n");}//方便获取客户端地址,accept获取一个新的文件描述符//而该文件描述符本质就是ip+端口号//之前我们使用文件描述符都是面向过程,都是作为函数参数进行传递的//我们需要面向对象的使用套接字,我们将得到的IO文件描述符设置进套接字里面//返回该套接字//using SockSPtr = std::shared_ptr<Socket>;//Socket是虚基类,实际上是拿TcpSocket//定义的对象SockSPtr Accepter(InetAddr *cliaddr) override{struct sockaddr_in client;socklen_t len = sizeof(client);// 4. 获取新连接:得到一个新的文件描述符,得到新的客户端int sockfd = ::accept(_sockfd, (struct sockaddr *)&client, &len);if (sockfd < 0){LOG(WARNING, "accept error\n");return nullptr;}*cliaddr = InetAddr(client);LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", cliaddr->AddrStr().c_str(), sockfd);return std::make_shared<TcpSocket>(sockfd); // C++14}//连接目标服务器(是否成功)//客户端ip和端口号bool Conntecor(const std::string &peerip, uint16_t peerport) override{struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(peerport);//将IPv4地址的字符串形式转换为网络字节顺序的二进制形式,//并将其存储在server.sin_addr中::inet_pton(AF_INET, peerip.c_str(), &server.sin_addr);int n = ::connect(_sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){ return false;}return true;}int Sockfd()//文件描述符{return _sockfd;}void Close(){if (_sockfd > 0){::close(_sockfd);}}ssize_t Recv(std::string *out) override//读到的消息{char inbuffer[4096];//从sockfd中读ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);if (n > 0){inbuffer[n] = 0;//这里不能是=,不可以覆盖式的读取,因为每一次可能并不是读取到一条完整的报文// "len"\r\n// "len"\r\n"{json}"\r\n//向上面的情况如果覆盖的读取将读取不到完整的报文了//所以要用+=*out += inbuffer;}return n;}ssize_t Send(const std::string &in) override{return ::send(_sockfd, in.c_str(), in.size(), 0);}private:int _sockfd; // 可以是listensock,普通socketfd};// class UdpSocket : public Socket// {};
} // namespace socket_n

代码逻辑:

  1. 命名空间和类定义
    • 定义了一个命名空间socket_ns,用于封装Socket相关的类和函数。
    • 定义了一个基类Socket,它是一个抽象类,提供了Socket操作的基本接口,如创建、绑定、监听、接收连接、发送和接收数据等。
    • 定义了一个派生类TcpSocket,它继承自Socket类,并实现了所有虚函数,提供了TCP Socket的具体实现。
  2. Socket基类
    • 定义了多个纯虚函数,包括创建Socket、绑定、监听、接受连接、连接服务器、获取文件描述符、关闭Socket、接收和发送数据等。
    • 提供了一个构建监听Socket的成员函数BuildListenSocket,它依次调用创建Socket、绑定和监听函数来初始化监听Socket。
    • 提供了一个构建客户端Socket的成员函数BuildClientSocket,它调用创建Socket和连接服务器函数来初始化客户端Socket。
  3. TcpSocket类
    • 实现了Socket类中的所有纯虚函数,提供了TCP Socket的具体实现。
    • 在构造函数中,可以初始化一个已存在的文件描述符,或者通过调用CreateSocketOrDie函数创建一个新的Socket文件描述符。
    • CreateSocketOrDie函数用于创建一个新的Socket文件描述符。
    • CreateBindOrDie函数用于将Socket绑定到一个指定的端口上。
    • CreateListenOrDie函数用于将Socket设置为监听模式,以便接受连接。
    • Accepter函数用于接受一个新的连接,并返回一个表示该连接的TcpSocket对象。
    • Conntecor函数用于连接到一个指定的服务器
    • Sockfd函数用于获取Socket的文件描述符。
    • Close函数用于关闭Socket。
    • Recv函数用于从Socket接收数据。
    • Send函数用于向Socket发送数据。
  4. 日志和错误处理
    • 使用了自定义的日志系统(log_ns命名空间中的LOG宏)来记录日志和错误信息。
    • 在发生错误时,使用exit函数终止程序,并传递一个错误码。
  5. 内存管理
    • 使用了智能指针(std::shared_ptr)来管理TcpSocket对象的内存,以避免内存泄漏。

3.3.3 会话层(网络

TcpServer.hpp:

#include <functional>
#include "Socket.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"//处理连接获取问题
using namespace socket_ns;static const int gport = 8888;
//套接字和客户端地址信息
using service_io_t = std::function<void(SockSPtr, InetAddr &)>;//解决IO的问题class TcpServer
{
public:TcpServer(service_io_t service, int port = gport): _port(port),_listensock(std::make_shared<TcpSocket>()),//基类指向子类,这里就有了一个TcpSocket对象_isrunning(false),_service(service){//模板模式_listensock->BuildListenSocket(_port);//创建监听套接字,直接启动了服务器套接字}class ThreadData{public:SockSPtr _sockfd;//封装过的套接字,方便获取文件描述符,智能指针自动管理了这个对象的内存//当智能指针的最后一个实例被销毁时,它所管理的TcpSocket对象也会被自动删除,从而避免了内存泄漏。TcpServer *_self;InetAddr _addr;public:ThreadData(SockSPtr sockfd, TcpServer *self, const InetAddr &addr):_sockfd(sockfd), _self(self), _addr(addr){}};void Loop(){// signal(SIGCHLD, SIG_IGN);_isrunning = true;while (_isrunning){InetAddr client;//这里就体现面向对象的好处了,模板式的获取对应的参数//调用Accepter,获取到客户端的地址,返回一个底层的套接字SockSPtr newsock = _listensock->Accepter(&client);if(newsock == nullptr)continue;//打印客户端信息和文件描述符LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", client.AddrStr().c_str(), newsock->Sockfd());//多线程版本 --- 不能关闭fd了,也不需要了pthread_t tid;ThreadData *td = new ThreadData(newsock, this, client);pthread_create(&tid, nullptr, Execute, td); // 新线程进行分离}_isrunning = false;}static void *Execute(void *args)//任务执行,回调service方法{pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);td->_self->_service(td->_sockfd, td->_addr);//展开回调,交给外部处理td->_sockfd->Close();//任务执行完后,关闭文件描述符delete td;return nullptr;}~TcpServer() {}private:uint16_t _port;SockSPtr _listensock;//套接字对象bool _isrunning;service_io_t _service;//解决IO问题
};

代码逻辑:

  1. 初始化与监听
    • 构造函数中,服务器通过调用 _listensock->BuildListenSocket(_port); 创建并监听指定端口上的 TCP 套接字。
    • _listensock 是一个智能指针,指向 TcpSocket 对象,它负责管理监听套接字。
  2. 事件循环
    • Loop() 方法是服务器的主循环,它持续运行直到 _isrunning 标志被设置为 false
    • 在循环中,服务器使用 _listensock 的 Accepter 方法等待并接受客户端的连接请求。
    • 一旦接受到新的连接,服务器会打印客户端的信息和套接字文件描述符,然后为每个新连接创建一个新线程来处理。
  3. 线程处理
    • 对于每个新的连接,服务器创建一个 ThreadData 对象,其中包含套接字、服务器实例指针和客户端地址信息。
    • 使用 pthread_create 创建一个新线程,线程执行 Execute 静态成员函数。
    • Execute 函数中,线程首先分离自身,然后调用 _service 回调函数来处理客户端的连接,传入套接字和客户端地址作为参数。
    • 处理完成后,关闭套接字并删除 ThreadData 对象。
  4. 回调函数
    • _service 是一个函数对象,其类型定义为 std::function<void(SockSPtr, InetAddr &)>,表示它接受一个套接字和一个客户端地址作为参数,并返回 void
    • 这个回调函数由外部提供,服务器在接收到新的连接时调用它来处理客户端的请求。

 3.3.4 表示层

Service.hpp:

#include <iostream>
#include <functional>
#include "InetAddr.hpp"
#include "Socket.hpp"
#include "Log.hpp"
#include "Protocol.hpp"
//IO服务
using namespace socket_ns;
using namespace log_ns;
//专门做任务处理的,把结构化的请求变为结构化的响应
using process_t = std::function<std::shared_ptr<Response>(std::shared_ptr<Request>)>;class IOService
{
public:IOService(process_t process):_process(process){}//文件描述符和客户端地址void IOExcute(SockSPtr sock, InetAddr &addr)//执行接口{std::string packagestreamqueue;//字节流队列while (true){// 1. 负责读取ssize_t n = sock->Recv(&packagestreamqueue);//读取报文 if (n <= 0){LOG(INFO, "client %s quit or recv error\n", addr.AddrStr().c_str());break;}std::cout << "--------------------------------------------" << std::endl;std::cout << "packagestreamqueue: \n" << packagestreamqueue << std::endl;// 我们能保证我们读到的是一个完整的报文吗?不能!// 2. 报文解析,提取报头和有效载荷    std::string package = Decode(packagestreamqueue);if(package.empty()) continue;//如果为空,那就证明没有读到一个完整的报文// 我们能保证我们读到的是一个完整的报文吗?能!!auto req = Factory::BuildRequestDefault();std::cout << "package: \n" << package << std::endl;// 3. 反序列化req->Deserialize(package);//这里就得到一个结构化的数据// 4. 业务处理auto resp = _process(req); // 通过请求,得到应答// 5. 序列化应答std::string respjson;resp->Serialize(&respjson);std::cout << "respjson: \n" << respjson << std::endl;// 6. 添加len长度报头respjson = Encode(respjson);std::cout << "respjson add header done: \n" << respjson << std::endl;// 7. 发送回去sock->Send(respjson);}}~IOService()  {}
private:process_t _process;
};

代码逻辑:

这段代码实现了一个IOService类,它负责处理客户端的连接请求,并对接收到的数据进行处理后再发送回客户端。以下是代码的主要逻辑:

  1. 构造函数IOService类接收一个process_t类型的函数对象作为参数,这个函数对象负责将接收到的请求(Request)转换为响应(Response)。

  2. IOExcute方法:这是类的主要方法,它接收一个套接字(SockSPtr)和客户端地址(InetAddr)作为参数。方法内部实现了一个循环,用于不断读取客户端发送的数据,并进行处理。

    • 读取数据:使用套接字对象的Recv方法从客户端接收数据,并将其存储在packagestreamqueue字符串中。

    • 报文解析:通过Decode函数对接收到的数据进行解析,提取出一个完整的报文。如果未能提取出完整报文,则继续等待更多数据。

    • 反序列化:使用Factory::BuildRequestDefault方法创建一个请求对象,并通过Deserialize方法将提取出的报文反序列化为结构化的请求数据。

    • 业务处理:将请求对象传递给构造函数中接收的函数对象_process进行处理,得到一个响应对象。

    • 序列化应答:使用响应对象的Serialize方法将响应数据序列化为字符串。

    • 添加报头:通过Encode函数为序列化后的响应数据添加长度报头。

    • 发送响应:使用套接字对象的Send方法将处理后的响应数据发送回客户端。

  3. 析构函数IOService类的析构函数为空,表示在销毁对象时不需要执行任何特殊操作。


 3.3.5 应用层(业务)&&计算器方法

Protocol.hpp:协议框架
#include <iostream>
#include <memory>
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>//协议
static const std::string sep = "\r\n";//分割符// 设计一下协议的报头和报文的完整格式
// "len"\r\n"{json}"\r\n --- 完整的报文, len 有效载荷的长度!
// \r\n: 区分len 和 json 串 的边界
// \r\n: 暂是没有其他用,打印方便,debug,第一行是长度,第二行是json串// 添加报头
std::string Encode(const std::string &jsonstr)
{int len = jsonstr.size();std::string lenstr = std::to_string(len);return lenstr + sep + jsonstr + sep; 
}
// 不能带const,读取的报文应该是完整的而不是残缺的
// "le
// "len"
// "len"\r\n
// "len"\r\n"{json}"\r\n (]
// "len"\r\n"{j
// "len"\r\n"{json}"\r\n"len"\r\n"{
// "len"\r\n"{json}"\r\n
// "len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"len"\r\n"{json}"\r
//读出报文
std::string Decode(std::string &packagestream)//接收一个json串
{// 分析,找的到分割符吗auto pos = packagestream.find(sep);if (pos == std::string::npos)return std::string();//如果找到分割符了,那就截取0到pos的字符串std::string lenstr = packagestream.substr(0, pos);int len = std::stoi(lenstr);// 计算一个完整的报文应该是多长??//"len"+{json}串的长度+2个分割符的长度:"len"\r\n"{json}"\r\nint total = lenstr.size() + len + 2 * sep.size();if (packagestream.size() < total)return std::string();//小于这个长度就不进行提取了,return返回// 符合要求进行提取json串std::string jsonstr = packagestream.substr(pos + sep.size(), len);//处理完了就删除整个报文结构:"len"\r\n"{json}"\r\n,接着处理下一条报文packagestream.erase(0, total);return jsonstr;
}class Request//请求
{
public:Request(){}Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper){}bool Serialize(std::string *out)//序列化{// 1. 使用现成的库, xml, json(jsoncpp), protobufJson::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::FastWriter writer;// Json::StyledWriter writer;std::string s = writer.write(root);*out = s;return true;}bool Deserialize(const std::string &in)//反序列化{Json::Value root;Json::Reader reader;bool res = reader.parse(in, root);_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;}void Print(){std::cout << _x << std::endl;std::cout << _y << std::endl;std::cout << _oper << std::endl;}~Request(){}int X(){return _x;}int Y(){return _y;}char Oper(){return _oper;}//客户端给void SetValue(int x, int y, char oper){_x = x;_y = y;_oper = oper;}private:int _x;int _y;char _oper; // + - * / % // x oper y
};// struct request resp={30, 0};
class Response//服务器给客户端的应答
{
public:Response() : _result(0), _code(0), _desc("success"){}bool Serialize(std::string *out){// 1. 使用现成的库, xml, json(jsoncpp), protobufJson::Value root;root["result"] = _result;root["code"] = _code;root["desc"] = _desc;Json::FastWriter writer;// Json::StyledWriter writer;std::string s = writer.write(root);*out = s;return true;}bool Deserialize(const std::string &in){Json::Value root;Json::Reader reader;bool res = reader.parse(in, root);if (!res)return false;_result = root["result"].asInt();_code = root["code"].asInt();_desc = root["desc"].asString();return true;}void PrintResult(){std::cout << "result: " << _result << ", code: " << _code << ", desc: " << _desc << std::endl; }~Response(){}public: int _result;int _code; // 0: success, 1: div zero 2. 非法操作std::string _desc;//结果描述
};class Factory//方便对象的建立
{
public:static std::shared_ptr<Request> BuildRequestDefault(){return std::make_shared<Request>();}static std::shared_ptr<Response> BuildResponseDefault(){return std::make_shared<Response>();}
};

代码逻辑:

这段代码实现了一个简单的客户端-服务器通信协议的框架,包括请求(Request)和应答(Response)的数据结构定义,以及用于序列化和反序列化这些数据结构的方法。此外,还定义了一个Factory类来方便地创建RequestResponse对象的实例。通信协议的设计是基于文本的,使用特定的格式来传输数据,包括长度字段和实际的JSON字符串。以下是代码的主要逻辑概述:

  1. 协议设计
    • 报文格式设计为“长度\r\nJSON字符串\r\n”,其中长度字段表示JSON字符串的字节长度。
    • 使用\r\n作为字段和报文之间的分隔符。
  2. 编码和解码
    • Encode函数接受一个JSON字符串,将其长度和字符串本身按照协议格式组合成一个完整的报文。
    • Decode函数接受一个包含完整报文的字符串,解析出JSON字符串并返回,同时从输入字符串中移除已解析的报文部分。
  3. 请求和应答数据结构
    • Request类表示客户端发送到服务器的请求,包含两个整数字段_x_y,以及一个字符字段_oper表示操作类型。
    • Response类表示服务器发送给客户端的应答,包含一个整数结果字段_result,一个整数状态码字段_code,以及一个字符串描述字段_desc
  4. 序列化和反序列化
    • RequestResponse类都提供了SerializeDeserialize方法,用于将对象转换为JSON字符串,以及从JSON字符串中恢复对象。
  5. 工厂类
    • Factory类提供了静态方法BuildRequestDefaultBuildResponseDefault,用于创建RequestResponse对象的默认实例。

整体而言,这段代码展示了如何在C++中使用JSON进行简单的序列化和反序列化操作,以及如何设计一个简单的文本基通信协议来传输结构化数据。


NetCal.hpp:计算器方法
#include "Protocol.hpp"
#include <memory>class NetCal
{
public:NetCal(){}~NetCal(){}std::shared_ptr<Response> Calculator(std::shared_ptr<Request> req){auto resp = Factory::BuildResponseDefault();//应答switch (req->Oper()){case '+':resp->_result = req->X() + req->Y();break;case '-':resp->_result = req->X() - req->Y();break;case '*':resp->_result = req->X() * req->Y();break;case '/':{if (req->Y() == 0){resp->_code = 1;resp->_desc = "div zero";}else{resp->_result = req->X() / req->Y();}}break;case '%':{if (req->Y() == 0){resp->_code = 2;resp->_desc = "mod zero";}else{resp->_result = req->X() % req->Y();}}break;default:{resp->_code = 3;resp->_desc = "illegal operation";}break;}return resp;}
};

代码逻辑:

  1. Calculator 方法
    • 参数:接受一个类型为 std::shared_ptr<Request> 的智能指针 req,指向请求对象。
    • 返回值:返回一个类型为 std::shared_ptr<Response> 的智能指针,指向应答对象。
    • 逻辑:
      • 首先,通过 Factory::BuildResponseDefault() 创建一个默认的应答对象 resp
      • 然后,根据请求对象 req 中的操作符(通过 req->Oper() 获取),执行相应的算术运算:
        • 如果是 '+',则执行加法运算。
        • 如果是 '-',则执行减法运算。
        • 如果是 '*',则执行乘法运算。
        • 如果是 '/',则执行除法运算。如果除数为 0,则设置应答对象的错误码和描述,否则执行除法。
        • 如果是 '%',则执行取模运算。如果除数为 0,则设置应答对象的错误码和描述,否则执行取模。
        • 如果操作符不是上述任何一个,则设置应答对象的错误码和描述为“illegal operation”。
      • 最后,返回应答对象 resp

3.3.6服务器 和 客户端 的启动

ServerMain.cc:服务器启动

#include "TcpServer.hpp"
#include "Service.hpp"
#include "NetCal.hpp"// ./tcpserver 8888
int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;exit(0);}uint16_t port = std::stoi(argv[1]);// 我们的软件代码,我们手动的划分了三层//网络对象NetCal cal;//网络服务IOService service(std::bind(&NetCal::Calculator, &cal, std::placeholders::_1));std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::bind(&IOService::IOExcute, &service, std::placeholders::_1, std::placeholders::_2), port);tsvr->Loop();return 0;
}
  1. 参数检查
    • 程序首先检查命令行参数的数量。如果不是两个参数(程序名和端口号),则打印用法信息并退出。
  2. 端口号解析
    • 从命令行参数中解析出端口号,并将其转换为 uint16_t 类型。
  3. 创建对象
    • 创建一个 NetCal 对象 cal,它实现了计算器的逻辑。
    • 创建一个 IOService 对象 service,并将 NetCal::Calculator 方法绑定到 service 上。这样,service 对象就可以处理计算请求了。
  4. 创建 TCP 服务器
    • 使用 std::make_unique 创建一个 TcpServer 的唯一指针 tsvr。在创建过程中,将 IOService::IOExcute 方法绑定到 TCP 服务器的处理逻辑上,并传入端口号。
  5. 启动服务器循环
    • 调用 tsvr->Loop() 方法来启动 TCP 服务器的循环,等待并处理客户端的连接和请求。
  6. 程序结束
    • 当 TCP 服务器退出循环时,程序结束。

总的来说,这段代码通过组合 NetCal(计算器逻辑)、IOService网络服务逻辑)和 TcpServer(TCP 服务器逻辑)三个组件,实现了一个简单的 TCP 计算器服务器。客户端可以通过 TCP 连接发送计算请求,服务器会处理这些请求并返回结果。


ClientMain.cc:客户端

#include <iostream>
#include <ctime>
#include <unistd.h>
#include "Socket.hpp"
#include "Protocol.hpp"using namespace socket_ns;int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);//构建tcpsocketSockSPtr sock = std::make_shared<TcpSocket>();if (!sock->BuildClientSocket(serverip, serverport)){std::cerr << "connect error" << std::endl;exit(1);}srand(time(nullptr) ^ getpid());const std::string opers = "+-*/%&^!";int cnt = 3;std::string packagestreamqueue;while (true){// 构建数据int x = rand() % 10;usleep(x * 1000);int y = rand() % 10;usleep(x * y * 100);char oper = opers[y % opers.size()];// 构建请求auto req = Factory::BuildRequestDefault();req->SetValue(x, y, oper);// 1. 序列化std::string reqstr;req->Serialize(&reqstr);// 2. 添加长度报头字段reqstr = Encode(reqstr);std::cout << "####################################" << std::endl;std::cout << "request string: \n" <<  reqstr << std::endl;// 3. 发送数据sock->Send(reqstr);while (true){// 4. 读取应答,responsessize_t n = sock->Recv(&packagestreamqueue);if (n <= 0){break;}// 我们能保证我们读到的是一个完整的报文吗?不能!// 5. 报文解析,提取报头和有效载荷std::string package = Decode(packagestreamqueue);if (package.empty())continue;std::cout << "package: \n" << package << std::endl;// 6. 反序列化auto resp = Factory::BuildResponseDefault();resp->Deserialize(package);// 7. 打印结果resp->PrintResult();break;}sleep(1);// break;}sock->Close();return 0;
}

代码逻辑:

        这段代码实现了一个 TCP 客户端,它不断向服务器发送计算请求,并接收并处理服务器的应答。客户端使用随机数生成操作数和操作符,并将请求序列化为字符串发送。接收到服务器的应答后,客户端会将其反序列化并打印结果。

  1. 创建 TCP 套接字
    • 创建一个 TcpSocket 的共享指针 sock,并尝试将其连接到服务器 IP 地址和端口号。如果连接失败,则打印错误信息并退出。
  2. 初始化随机数生成器
    • 使用当前时间和进程 ID 作为种子,初始化随机数生成器。
  3. 构建并发送请求
    • 进入一个无限循环,不断构建并发送请求到服务器
    • 每次循环中,随机生成两个操作数 x 和 y,以及一个操作符 oper
    • 使用 Factory::BuildRequestDefault() 创建一个请求对象,并设置其值。
    • 将请求对象序列化为字符串,并添加长度报头字段。
    • 打印请求字符串。
    • 发送请求字符串到服务器
  4. 接收并处理应答
    • 进入一个内部循环,尝试从服务器接收应答。
    • 使用 sock->Recv() 方法接收数据,并将其存储在 packagestreamqueue 字符串中。
    • 如果接收到的数据长度小于等于 0,则跳出内部循环。
    • 尝试从 packagestreamqueue 中提取一个完整的报文。
    • 如果提取到的报文不为空,则打印报文内容。
    • 反序列化报文,创建一个应答对象,并打印结果。
    • 跳出内部循环,等待下一次请求。

3.4 代码测试

  • 加法运算符(+)的ASCII码是43。
  • 减法运算符(-)的ASCII码是45。
  • 乘法运算符(*)的ASCII码是42。
  • 除法运算符(/)的ASCII码是47。
  • 取模运算符(%)的ASCII码是37。

第一次计算:2+0=2,结果正确

第二次计算:9-9=0, 结果正确

 

 


http://www.ppmy.cn/ops/93501.html

相关文章

深入理解看门狗机制及其在Java中的实现

深入理解看门狗机制及其在Java中的实现 什么是看门狗&#xff1f; 看门狗&#xff08;Watchdog&#xff09;是一种广泛应用于系统监控的机制&#xff0c;其主要作用是确保系统、设备或软件程序的正常运行。当看门狗检测到系统出现异常&#xff08;如无响应或任务超时&#xf…

mmdebstrap:创建 Debian 系统 chroot 环境的利器 ️

文章目录 mmdebstrap 的一般性参数说明 &#x1f4dc;mmdebstrap 的常见用法示例 &#x1f308;使用 mmdebstrap 的注意事项 ⚠️ &#x1f308;你好呀&#xff01;我是 山顶风景独好 &#x1f388;欢迎踏入我的博客世界&#xff0c;能与您在此邂逅&#xff0c;真是缘分使然&am…

JSON与Jsoncpp库:数据交换的灵活选择

目录 引言 一.JSON简介 二. Jsoncpp库概述 三. Jsoncpp核心类介绍 3.1 Json::Value类 3.2 序列化与反序列化类 四. 实现序列化 五. 实现反序列化 结语 引言 在现代软件开发中&#xff0c;数据交换格式扮演着至关重要的角色。JSON&#xff08;JavaScript Object Notati…

LeetCode Hot100 排序链表

给你链表的头结点 head &#xff0c;请将其按 升序 排列并返回 排序后的链表 。 示例 1&#xff1a; 输入&#xff1a;head [4,2,1,3] 输出&#xff1a;[1,2,3,4]示例 2&#xff1a; 输入&#xff1a;head [-1,5,3,4,0] 输出&#xff1a;[-1,0,3,4,5]示例 3&#xff1a; 输…

常用的数据结构有哪些?

常用的数据结构是计算机科学中用于组织、存储和高效处理数据的基本结构。这些结构的选择取决于具体的应用场景和需要解决的问题。以下是一些最常用的数据结构&#xff1a; 数组&#xff08;Array&#xff09;&#xff1a; 数组是一种基础的数据结构&#xff0c;用于在计算机内存…

JAVA:设计模式的详细指南

请关注微信公众号&#xff1a;拾荒的小海螺 博客地址&#xff1a;http://lsk-ww.cn/ 1、简述 设计模式&#xff08;Design Patterns&#xff09;是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它们可以帮助开发者以一种更优雅和高效的方式解决常见的…

【微信小程序】网络数据请求

1. 小程序中网络数据请求的限制 2. 配置 request 合法域名 3. 发起 GET 请求 调用微信小程序提供的 wx.request() 方法,可以发起 GET 数据请求,示例代码如下: 4. 发起 POST 请求 调用微信小程序提供的 wx.request() 方法,可以发起 POST 数据请求,示例代码如下: 5. …

8.13网络编程

笔记 多点通信 一、套接字属性 套接字属性的获取和设置 #include <sys/types.h> /* See NOTES */#include <sys/socket.h>int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen);int setsockopt(int sockfd, int level…