目录
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:虽然
read
、write
、recv
、send
等函数在默认情况下是阻塞的,但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 2 const 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) { // 加过滤逻辑 --- TODO LockGuard 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 addr if (::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
代码逻辑:
- 命名空间和类定义:
- 定义了一个命名空间
socket_ns
,用于封装Socket相关的类和函数。- 定义了一个基类
Socket
,它是一个抽象类,提供了Socket操作的基本接口,如创建、绑定、监听、接收连接、发送和接收数据等。- 定义了一个派生类
TcpSocket
,它继承自Socket
类,并实现了所有虚函数,提供了TCP Socket的具体实现。- Socket基类:
- 定义了多个纯虚函数,包括创建Socket、绑定、监听、接受连接、连接服务器、获取文件描述符、关闭Socket、接收和发送数据等。
- 提供了一个构建监听Socket的成员函数
BuildListenSocket
,它依次调用创建Socket、绑定和监听函数来初始化监听Socket。- 提供了一个构建客户端Socket的成员函数
BuildClientSocket
,它调用创建Socket和连接服务器函数来初始化客户端Socket。- TcpSocket类:
- 实现了
Socket
类中的所有纯虚函数,提供了TCP Socket的具体实现。- 在构造函数中,可以初始化一个已存在的文件描述符,或者通过调用
CreateSocketOrDie
函数创建一个新的Socket文件描述符。CreateSocketOrDie
函数用于创建一个新的Socket文件描述符。CreateBindOrDie
函数用于将Socket绑定到一个指定的端口上。CreateListenOrDie
函数用于将Socket设置为监听模式,以便接受连接。Accepter
函数用于接受一个新的连接,并返回一个表示该连接的TcpSocket
对象。Conntecor
函数用于连接到一个指定的服务器。Sockfd
函数用于获取Socket的文件描述符。Close
函数用于关闭Socket。Recv
函数用于从Socket接收数据。Send
函数用于向Socket发送数据。- 日志和错误处理:
- 使用了自定义的日志系统(
log_ns
命名空间中的LOG
宏)来记录日志和错误信息。- 在发生错误时,使用
exit
函数终止程序,并传递一个错误码。- 内存管理:
- 使用了智能指针(
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问题 };
代码逻辑:
- 初始化与监听:
- 构造函数中,服务器通过调用
_listensock->BuildListenSocket(_port);
创建并监听指定端口上的 TCP 套接字。_listensock
是一个智能指针,指向TcpSocket
对象,它负责管理监听套接字。- 事件循环:
Loop()
方法是服务器的主循环,它持续运行直到_isrunning
标志被设置为false
。- 在循环中,服务器使用
_listensock
的Accepter
方法等待并接受客户端的连接请求。- 一旦接受到新的连接,服务器会打印客户端的信息和套接字文件描述符,然后为每个新连接创建一个新线程来处理。
- 线程处理:
- 对于每个新的连接,服务器创建一个
ThreadData
对象,其中包含套接字、服务器实例指针和客户端地址信息。- 使用
pthread_create
创建一个新线程,线程执行Execute
静态成员函数。Execute
函数中,线程首先分离自身,然后调用_service
回调函数来处理客户端的连接,传入套接字和客户端地址作为参数。- 处理完成后,关闭套接字并删除
ThreadData
对象。- 回调函数:
_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
类,它负责处理客户端的连接请求,并对接收到的数据进行处理后再发送回客户端。以下是代码的主要逻辑:
构造函数:
IOService
类接收一个process_t
类型的函数对象作为参数,这个函数对象负责将接收到的请求(Request
)转换为响应(Response
)。IOExcute方法:这是类的主要方法,它接收一个套接字(
SockSPtr
)和客户端地址(InetAddr
)作为参数。方法内部实现了一个循环,用于不断读取客户端发送的数据,并进行处理。
读取数据:使用套接字对象的
Recv
方法从客户端接收数据,并将其存储在packagestreamqueue
字符串中。报文解析:通过
Decode
函数对接收到的数据进行解析,提取出一个完整的报文。如果未能提取出完整报文,则继续等待更多数据。反序列化:使用
Factory::BuildRequestDefault
方法创建一个请求对象,并通过Deserialize
方法将提取出的报文反序列化为结构化的请求数据。业务处理:将请求对象传递给构造函数中接收的函数对象
_process
进行处理,得到一个响应对象。序列化应答:使用响应对象的
Serialize
方法将响应数据序列化为字符串。添加报头:通过
Encode
函数为序列化后的响应数据添加长度报头。发送响应:使用套接字对象的
Send
方法将处理后的响应数据发送回客户端。析构函数:
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\n int 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), protobuf Json::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), protobuf Json::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
类来方便地创建Request
和Response
对象的实例。通信协议的设计是基于文本的,使用特定的格式来传输数据,包括长度字段和实际的JSON字符串。以下是代码的主要逻辑概述:
- 协议设计:
- 报文格式设计为“长度\r\nJSON字符串\r\n”,其中长度字段表示JSON字符串的字节长度。
- 使用
\r\n
作为字段和报文之间的分隔符。- 编码和解码:
Encode
函数接受一个JSON字符串,将其长度和字符串本身按照协议格式组合成一个完整的报文。Decode
函数接受一个包含完整报文的字符串,解析出JSON字符串并返回,同时从输入字符串中移除已解析的报文部分。- 请求和应答数据结构:
Request
类表示客户端发送到服务器的请求,包含两个整数字段_x
和_y
,以及一个字符字段_oper
表示操作类型。Response
类表示服务器发送给客户端的应答,包含一个整数结果字段_result
,一个整数状态码字段_code
,以及一个字符串描述字段_desc
。- 序列化和反序列化:
Request
和Response
类都提供了Serialize
和Deserialize
方法,用于将对象转换为JSON字符串,以及从JSON字符串中恢复对象。- 工厂类:
Factory
类提供了静态方法BuildRequestDefault
和BuildResponseDefault
,用于创建Request
和Response
对象的默认实例。整体而言,这段代码展示了如何在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; } };
代码逻辑:
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; }
- 参数检查:
- 程序首先检查命令行参数的数量。如果不是两个参数(程序名和端口号),则打印用法信息并退出。
- 端口号解析:
- 从命令行参数中解析出端口号,并将其转换为
uint16_t
类型。- 创建对象:
- 创建一个
NetCal
对象cal
,它实现了计算器的逻辑。- 创建一个
IOService
对象service
,并将NetCal::Calculator
方法绑定到service
上。这样,service
对象就可以处理计算请求了。- 创建 TCP 服务器:
- 使用
std::make_unique
创建一个TcpServer
的唯一指针tsvr
。在创建过程中,将IOService::IOExcute
方法绑定到 TCP 服务器的处理逻辑上,并传入端口号。- 启动服务器循环:
- 调用
tsvr->Loop()
方法来启动 TCP 服务器的循环,等待并处理客户端的连接和请求。- 程序结束:
- 当 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]); //构建tcpsocket SockSPtr 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. 读取应答,response ssize_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 客户端,它不断向服务器发送计算请求,并接收并处理服务器的应答。客户端使用随机数生成操作数和操作符,并将请求序列化为字符串发送。接收到服务器的应答后,客户端会将其反序列化并打印结果。
- 创建 TCP 套接字:
- 创建一个
TcpSocket
的共享指针sock
,并尝试将其连接到服务器 IP 地址和端口号。如果连接失败,则打印错误信息并退出。- 初始化随机数生成器:
- 使用当前时间和进程 ID 作为种子,初始化随机数生成器。
- 构建并发送请求:
- 进入一个无限循环,不断构建并发送请求到服务器。
- 每次循环中,随机生成两个操作数
x
和y
,以及一个操作符oper
。- 使用
Factory::BuildRequestDefault()
创建一个请求对象,并设置其值。- 将请求对象序列化为字符串,并添加长度报头字段。
- 打印请求字符串。
- 发送请求字符串到服务器。
- 接收并处理应答:
- 进入一个内部循环,尝试从服务器接收应答。
- 使用
sock->Recv()
方法接收数据,并将其存储在packagestreamqueue
字符串中。- 如果接收到的数据长度小于等于 0,则跳出内部循环。
- 尝试从
packagestreamqueue
中提取一个完整的报文。- 如果提取到的报文不为空,则打印报文内容。
- 反序列化报文,创建一个应答对象,并打印结果。
- 跳出内部循环,等待下一次请求。
3.4 代码测试
- 加法运算符(+)的ASCII码是43。
- 减法运算符(-)的ASCII码是45。
- 乘法运算符(*)的ASCII码是42。
- 除法运算符(/)的ASCII码是47。
- 取模运算符(%)的ASCII码是37。
第一次计算:2+0=2,结果正确
第二次计算:9-9=0, 结果正确