当前位置: 首页 > news >正文

下载建设银行官方网站下载免费发布信息

下载建设银行官方网站下载,免费发布信息,无锡网页推广,网站需求建设书总言 主要内容#xff1a;演示socke套接字编程#xff08;TCP模式#xff09;#xff0c;介绍序列化和反序列化#xff0c;并进行演示#xff08;json版本达成协议编写、守护进程介绍#xff09;。 文章目录 总言4、基于套接字的TCP网络程序4.0、log.hpp4.1、version1.…总言 主要内容演示socke套接字编程TCP模式介绍序列化和反序列化并进行演示json版本达成协议编写、守护进程介绍。 文章目录 总言4、基于套接字的TCP网络程序4.0、log.hpp4.1、version1.0echo服务器单进程单线程模式4.1.1、成员变量与构造、析构4.1.2、初始化服务器InitServer()4.1.2.1、socket、bind4.1.2.2、listen 4.1.3、启动服务器Start()4.1.3.1、accept 4.1.4、该部分整体框架4.1.4.1、tcp_server.hpp4.1.4.2、tcp_server.cc4.1.4.3、telnet 指令 4.2、version2.0 version2.1 多进程版4.2.1、version2.0采用信号捕捉达成非阻塞等待4.2.1.1、tcp_server.hpp4.2.1.2、tcp_client.ccconnect函数介绍 4.2.2、version2.1采用孤儿进程达成非阻塞等待4.2.2.1、tcp_server.hpp 4.4、version3.0多线程版4.4.1、tcp_server.hpp 4.5、version4.0线程池版4.5.1、tcp_server.hpp 4.6、TCP协议通讯流程 5、序列化和反序列化应用层·一5.1、基本情况介绍5.2、网络版本的计算器NetCal编写(version1.0自定义版协议)5.2.4、Sock.hpp TcpServer.hpp5.2.4.1、Sock.hpp5.2.4.2、TcpServer.hpp 5.2.5、CalServer.cc服务端5.2.3、Protocol.hpp定制的协议5.2.6、CalClient.cc客户端 5.3、网络版本的计算器NetCal编写(version2.0json版协议)5.3.1、守护进程5.3.1.1、问题引入5.3.1.2、如何做到setsid、daemon5.3.1.3、Daemon.hpp5.3.1.4、log.hpp 5.3.2、使用json完成序列化5.3.2.1、基本使用介绍 5.3.3、改动NetCal5.3.3.1、主要部分5.3.3.2、整体Protocol.hpp 4、基于套接字的TCP网络程序 4.0、log.hpp #pragma once#include iostream #include cstdio #include cstdarg #include ctime #include string// 日志级别 #define DEBUG 0 #define NORMAL 1 #define WARNING 2 #define ERROR 3 #define FATAL 4const char *gLevelMap[] {DEBUG,NORMAL,WARNING,ERROR,FATAL };// 完整的日志功能至少有 日志等级 时间 支持用户自定义(日志内容, 文件行文件名) void logMessage(int level, const char *format, ...)//const char *format, ... 可变参数 { // #ifndef DEBUG_SHOW // if(level DEBUG) return; // #endif//标准部分固定输出的内容char stdBuffer[1024]; time_t timestamp time(nullptr);snprintf(stdBuffer, sizeof stdBuffer, [%s] [%ld] , gLevelMap[level], timestamp);//自定义部分允许用户根据自己的需求设置char logBuffer[1024]; va_list args; //定义一个va_list对象va_start(args, format); vsnprintf(logBuffer, sizeof logBuffer, format, args);va_end(args); //相当于 args nullptrprintf(%s%s\n, stdBuffer, logBuffer); } 4.1、version1.0echo服务器单进程单线程模式 先来完善tcp_server.hpp的整体逻辑 4.1.1、成员变量与构造、析构 有了UDP的基础此处的框架搭建也相同。 class TcpServer { public:TcpServer(uint16_t port, const std::string ip):_port(port),_ip(ip), _linstensock(-1){ }~TcpServer(){ }// 初始化服务器bool InitServer(){}// 启动服务器void Start(){}private:uint16_t _port;//端口号std::string _ip;//IPint _listensock;//套接字 };4.1.2、初始化服务器InitServer() 4.1.2.1、socket、bind 说明初始化服务器这里用法和UDP类似区别在于socket时第二参数类型填入的是SOCK_STREAM。      socket NAMEsocket - create an endpoint for communicationSYNOPSIS#include sys/types.h /* See NOTES */#include sys/socket.hint socket(int domain, int type, int protocol); The socket has the indicated type, which specifies the communication semantics. Currently definedtypes are:SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be supported.SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length). bind: NAMEbind - bind a name to a socketSYNOPSIS#include sys/types.h /* See NOTES */#include sys/socket.hint bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); 相关演示如下 // 1、创建套接字_listensock socket(AF_INET, SOCK_STREAM, 0); // 这里填入SOCK_STREAMif (_listensock 0){logMessage(ERROR, socket, 创建套接字失败: %s-%d , errno, strerror(errno));exit(2);}logMessage(DEBUG, socket, 创建套接字成功,sock: %d, _listensock);// 2、绑定struct sockaddr_in localaddr;bzero(localaddr, 0);localaddr.sin_family AF_INET;localaddr.sin_addr.s_addr _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 字节序转换localaddr.sin_port htons(_port);if (bind(_listensock, (struct sockaddr *)localaddr, sizeof localaddr) 0){logMessage(ERROR, bind, 绑定失败%d:%s, errno, strerror(errno));exit(3);}logMessage(DEBUG, bind, 绑定成功. );4.1.2.2、listen 1、相关函数介绍   说明 因为TCP是面向连接的当我们正式通信的时候需要先建立连接。而作为服务器为了确保客户端能够随时享有通讯需求服务器需要保持在等待被连接的状态。 listen该函数可将套接字sockfd状态设置为监听状态并且最多允许有backlog个客户端处于连接等待状态 如果接收到更多的连接请求就忽略这里设置不会太大。 NAMElisten - listen for connections on a socketSYNOPSIS#include sys/types.h /* See NOTES */#include sys/socket.hint listen(int sockfd, int backlog);DESCRIPTIONlisten() marks the socket referred to by sockfd as a passive socket, that is, as asocket that will be used to accept incoming connection requests using accept(2).The sockfd argument is a file descriptor that refers to a socket of typeSOCK_STREAM or SOCK_SEQPACKET.The backlog argument defines the maximum length to which the queue of pending con‐nections for sockfd may grow. If a connection request arrives when the queue isfull, the client may receive an error with an indication of ECONNREFUSED or, ifthe underlying protocol supports retransmission, the request may be ignored sothat a later reattempt at connection succeeds. 相关参数   socket即创建套接字时的返回值   backlog关于该参数在后续TCP协议时详细解释这里我们只需要先使用即可。通常设置如下 const static int gbacklog 20; //不能太大、也不能太小返回值 RETURN VALUEOn success, zero is returned. On error, -1 is returned, and errno is set appropriately.2、建立链接   使用演示这里我们对监听的结果做了一下判断。 //3、监听if (listen(_listensock, gbacklog) 0){logMessage(ERROR, listen, 监听失败%d:%s, errno, strerror(errno));exit(4);}logMessage(DEBUG, linsten, 监听成功, 初始化套接字完成。);相关演示结果 4.1.3、启动服务器Start() 4.1.3.1、accept 说明三次握手完成后服务器调用accept()接受连接如果此时时还没有客户端的连接请求就阻塞等待直到有客户端连接上来。 NAMEaccept, accept4 - accept a connection on a socketSYNOPSIS#include sys/types.h /* See NOTES */#include sys/socket.hint accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);#define _GNU_SOURCE /* See feature_test_macros(7) */#include sys/socket.hint accept4(int sockfd, struct sockaddr *addr,socklen_t *addrlen, int flags);DESCRIPTIONThe accept() system call is used with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET). It extracts thefirst connection request on the queue of pending connections for the listening socket, sockfd, creates a new connectedsocket, and returns a new file descriptor referring to that socket. The newly created socket is not in the listeningstate. The original socket sockfd is unaffected by this call. 演示如下accept之后就是正常的通讯流程。 // 1、获取连接// 1.1、准备工作用于后续从网络中读取客户端的IP、端口号struct sockaddr_in clientaddr;memset(clientaddr, 0, sizeof(clientaddr));socklen_t len sizeof(clientaddr);// 1.2、连接int servicesock accept(_listensock, (struct sockaddr *)clientaddr, len);if(servicesock 0){logMessage(ERROR,accept, 获取链接失败, servicesock: %d ,servicesock);continue;//本次失败了结束此次循环即可可下一次重新获取连接}4.1.4、该部分整体框架 4.1.4.1、tcp_server.hpp #ifndef _TCP_SERVER_HPP #define _TCP_SERVER_HPP#includeiostream #includestring #includecstring #includememory #includeunistd.h #includesys/types.h #includesys/socket.h #includearpa/inet.h #includenetinet/in.h #includelog.hpp#define SIZE 1024// 业务函数服务器用于处理业务可根据需求自定义 // 【version1: echo版服务器】服务端打印从客户端读取到的数据并将其原封不动返回给客户端 static void service(int servicesock, const std::string clientip, const uint16_t clientport) {char server_buffer[SIZE];while (true){// a、读取客户端发来的数据ssize_t s read(servicesock, server_buffer, sizeof(server_buffer) - 1);if (s 0){server_buffer[s] \0; //\0的ASCII码是0std::cout clientip : clientport # server_buffer std::endl;}else if (s 0){logMessage(NORMAL, read, %s:%d 退出。, clientip.c_str(), clientport);break;}else{logMessage(ERROR, read, 读取失败, %d:%s, errno, strerror(errno));break;}// b、将读取到的结果返回write(servicesock, server_buffer, strlen(server_buffer));} }class TcpServer { private:const static int gbacklog 20;//listen中的参数设置public:TcpServer(uint16_t port, const std::string ip):_port(port),_ip(ip),_listensock(-1){ }~TcpServer(){ }// 初始化服务器bool InitServer(){// 1、创建套接字_listensock socket(AF_INET, SOCK_STREAM, 0); // 这里填入SOCK_STREAMif (_listensock 0){logMessage(ERROR, socket, 创建套接字失败: %s-%d , errno, strerror(errno));exit(2);}logMessage(DEBUG, socket, 创建套接字成功,sock: %d, _listensock);// 2、绑定struct sockaddr_in localaddr;bzero(localaddr, 0);localaddr.sin_family AF_INET;localaddr.sin_addr.s_addr _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 字节序转换localaddr.sin_port htons(_port);if (bind(_listensock, (struct sockaddr *)localaddr, sizeof localaddr) 0){logMessage(ERROR, bind, 绑定失败%d:%s, errno, strerror(errno));exit(3);}logMessage(DEBUG, bind, 绑定成功. );//3、监听if (listen(_listensock, gbacklog) 0){logMessage(ERROR, listen, 监听失败%d:%s, errno, strerror(errno));exit(4);}logMessage(DEBUG, linsten, 监听成功, 初始化套接字完成。);return true;}// 启动服务器void Start(){// 网络通讯角度作为一款网络服务器永远不退出的// OS角度服务器启动- 进程 - 常驻进程 - 永远在内存中存在除非挂了while (true){// 1、获取连接// 1.1、准备工作用于后续从网络中读取客户端的IP、端口号struct sockaddr_in clientaddr;memset(clientaddr, 0, sizeof(clientaddr));socklen_t len sizeof(clientaddr);// 1.2、连接int servicesock accept(_listensock, (struct sockaddr *)clientaddr, len);if(servicesock 0){logMessage(ERROR,accept, 获取链接失败, servicesock: %d ,servicesock);continue;//本次失败了结束此次循环即可可下一次重新获取连接}// 2、开始进行通讯服务// 2.1、获取客户端端口号、IPuint16_t client_port ntohs(clientaddr.sin_port); // uint16_t htons(uint16_t hostshort);std::string client_ip inet_ntoa(clientaddr.sin_addr); // char *inet_ntoa(struct in_addr in);logMessage(DEBUG, accept, 成功获取连接, servicesock: %d, client: [%s:%d] ., servicesock, client_ip.c_str(), client_port);// 2.2、根据需求处理客户端数据服务端的业务处理// version1: echo版服务器service(servicesock, client_ip, client_port);// 2.3、通讯结束关闭套接字。close(servicesock);}}private:uint16_t _port;//端口号std::string _ip;//IPint _listensock;//套接字 };#endif 4.1.4.2、tcp_server.cc #includetcp_server.hpp//使用手册 void Usage(std:: string proc) {std::cout \nUsage: proc port\n std::endl; }//服务端启动 ./tcp_server port int main(int argc, char*argv[]) {//1、检测命令行参数是否正确if(argc ! 2){Usage(argv[0]);exit(1);}uint16_t port atoi(argv[1]);//说明命令行参数为字符串port端口号需要整型//2、使用智能指针管理服务器std::unique_ptrTcpServer server(new TcpServer(port));//3、初始化和启动服务器server-InitServer();server-Start();return 0; } 4.1.4.3、telnet 指令 演示结果一先测试看看我们的代码能否成功通过。 演示结果二接下来验证以下这种单进程模式的缺陷服务器始终只能为一个客服端提供服务当有多个客服端同时连接时后者处于阻塞状态。   现象观察   原因解释 基于上述分析我们提出了以下方案用以解决服务端只一次能够处理一个客户端的问题。             4.2、version2.0 version2.1 多进程版 说明 采用多进程的方式解决问题。让父进程接收连接让子进程处理业务其中需要解决父进程等待子进程的问题。       4.2.1、version2.0采用信号捕捉达成非阻塞等待 4.2.1.1、tcp_server.hpp 说明仍旧是演示echo版服务器service函数不变这里只是使用了多进程用以让服务端能够同时服务多个客服端。   改动的部分在start函数中。这里使用了信号捕捉的方式让父进程达成非阻塞式等待子进程signal(SIGCHLD, SIG_IGN); 这种处理在Linux下的含义有在 信号章节 讲述过。 // 启动服务器void Start(){signal(SIGCHLD, SIG_IGN); // 对SIGCHLD主动忽略SIGCHLD信号子进程退出的时候会自动释放自己的僵尸状态// 网络通讯角度作为一款网络服务器永远不退出的// OS角度服务器启动- 进程 - 常驻进程 - 永远在内存中存在除非挂了while (true){// 1、获取连接// 1.1、准备工作用于后续从网络中读取客户端的IP、端口号struct sockaddr_in clientaddr;memset(clientaddr, 0, sizeof(clientaddr));socklen_t len sizeof(clientaddr);// 1.2、连接int servicesock accept(_listensock, (struct sockaddr *)clientaddr, len);if (servicesock 0){logMessage(ERROR, accept, 获取链接失败, servicesock: %d , servicesock);continue; // 本次失败了结束此次循环即可可下一次重新获取连接}// 2、开始进行通讯服务// 2.1、获取客户端端口号、IPuint16_t client_port ntohs(clientaddr.sin_port); // uint16_t htons(uint16_t hostshort);std::string client_ip inet_ntoa(clientaddr.sin_addr); // char *inet_ntoa(struct in_addr in);logMessage(DEBUG, accept, 成功获取连接, servicesock: %d, client: [%s:%d] ., servicesock, client_ip.c_str(), client_port);// 2.2、根据需求处理客户端数据服务端的业务处理// ——————————【version2:多进程版本】———————————//pid_t pd fork();assert(pd 0);if (pd 0) // 对子进程处理业务{close(_listensock); // 子进程用不到监听套接字可以关掉子进程能够继承父进程打开的文件及其fdservice(servicesock, client_ip, client_port);exit(0); // 正常退出}// 对父进程继续循环接收客户端的连接请求close(servicesock); // 父进程用不到accept提供的套接字可以关掉(对子进程无影响/文件描述符是有限资源有上限)// ————————————————————————————————————————————//}}使用talnet验证当前版本的tcp_server.hpp 4.2.1.2、tcp_client.ccconnect函数介绍 1、相关函数介绍    connect客户端不需要像服务端一样手动bind同时也不需要accept接收连接但客户端需要有链接别人的能力可以通过此函数达成。 NAMEconnect - initiate a connection on a socketSYNOPSIS#include sys/types.h /* See NOTES */#include sys/socket.hint connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);DESCRIPTIONThe connect() system call connects the socket referred to by the file descriptor sockfd to the address specified by addr.The addrlen argument specifies the size of addr. The format of the address in addr is determined by the address space ofthe socket sockfd; see socket(2) for further details.If the socket sockfd is of type SOCK_DGRAM then addr is the address to which datagrams are sent by default, and the onlyaddress from which datagrams are received. If the socket is of type SOCK_STREAM or SOCK_SEQPACKET, this call attempts tomake a connection to the socket that is bound to the address specified by addr.Generally, connection-based protocol sockets may successfully connect() only once; connectionless protocol sockets may useconnect() multiple times to change their association. Connectionless sockets may dissolve the association by connecting toan address with the sa_family member of sockaddr set to AF_UNSPEC (supported on Linux since kernel 2.2).RETURN VALUEIf the connection or binding succeeds, zero is returned. On error, -1 is returned, and errno is set appropriately. 2、代码演示   相关代码 #include iostream #include string #include cstring #include sys/socket.h #include sys/types.h #include arpa/inet.h #include netinet/in.h#define SIZE 1024// 使用手册 void Usage(std::string proc) {std::cout \nUsage: proc serverip serverport\n std::endl; }// 启动方式udp_client server_ip server_port int main(int argc, char *argv[]) {// 1、获取命令行传入的端口号、IP注意将其转换为对应的类型// a、检测命令行参数是否正确if (argc ! 3){Usage(argv[0]);exit(1);}// b、获取服务端端口号、IPstd::string server_ip argv[1];uint16_t server_port atoi(argv[2]);// 2、创建套接字int sock socket(AF_INET, SOCK_STREAM, 0);if(sock 0){std::cerr client: 创建套接字失败。 socket: sock std::endl;exit(2);}// 3、客户端需要有连接服务端的能力PS客户端不需要bind一般由OS自动分配端口号// a、准备工作struct sockaddr_in serveraddr;bzero(serveraddr, 0);serveraddr.sin_family AF_INET;serveraddr.sin_port htons(server_port);//端口号需要进行网络字节序的转换serveraddr.sin_addr.s_addr inet_addr(server_ip.c_str());//IP// b、建立连接if(connect(sock, (struct sockaddr*)serveraddr, sizeof serveraddr) 0){std::cerr client: connect建立连接失败。 std::endl;exit(3);}std::cout client: connect建立连接成功。 std::endl;// 4、链接成功即可通讯while(true){//a、客户端向服务端发送数据std::string message;std::cout client-请输入# ;std::getline(std::cin,message);send(sock, message.c_str(), message.size(),0);//b、客户端接收服务端传回的数据char client_buff[SIZE];ssize_t s recv(sock,client_buff,sizeof(client_buff)-1,0);if(s 0)//成功接收到数据{client_buff[s] 0;std::cout client-服务端响应 client_buff std::endl;}}return 0; }演示结果如下                4.2.2、version2.1采用孤儿进程达成非阻塞等待 4.2.2.1、tcp_server.hpp 说明 仍旧是演示echo版本服务器、多进程模式。只是这次不采用信用捕捉而是使用孤儿进程的方式达成非阻塞等待。   写法如下 在子进程中再创建子进程由于子进程关闭子子进程会成为孤儿进程被1号进程领养。孤儿进程退出的时候由OS自动回收孤儿进程 // 启动服务器void Start(){// 网络通讯角度作为一款网络服务器永远不退出的// OS角度服务器启动- 进程 - 常驻进程 - 永远在内存中存在除非挂了while (true){// 1、获取连接// 1.1、准备工作用于后续从网络中读取客户端的IP、端口号struct sockaddr_in clientaddr;memset(clientaddr, 0, sizeof(clientaddr));socklen_t len sizeof(clientaddr);// 1.2、连接int servicesock accept(_listensock, (struct sockaddr *)clientaddr, len);if (servicesock 0){logMessage(ERROR, accept, 获取链接失败, servicesock: %d , servicesock);continue; // 本次失败了结束此次循环即可可下一次重新获取连接}// 2、开始进行通讯服务// 2.1、获取客户端端口号、IPuint16_t client_port ntohs(clientaddr.sin_port); // uint16_t htons(uint16_t hostshort);std::string client_ip inet_ntoa(clientaddr.sin_addr); // char *inet_ntoa(struct in_addr in);logMessage(DEBUG, accept, 成功获取连接, servicesock: %d, client: [%s:%d] ., servicesock, client_ip.c_str(), client_port);// 2.2、根据需求处理客户端数据服务端的业务处理// ——————————【version2.1:多进程版本】———————————//pid_t pd fork();assert(pd ! -1);if( pd 0)//子进程{close(_listensock);//子进程关闭不必要的套接字if(fork() 0)// 在子进程中再fork子进程得到子子进程孙子进程{service(servicesock, client_ip, client_port);exit(0);// 孙子进程由于子进程关闭其成为孤儿进程OS领养 OS在孤儿进程退出的时候由OS自动回收孤儿进程}exit(0);//关闭子进程会导致子子进程变成孤儿进程}//对父进程waitpid(pd, nullptr, 0);close(servicesock);//父进程关闭不必要的套接字// —————————————————————————————————————————————//}} 这里也可以使用if(fork() 0)来判断写法无区别。 //version2.1 -- 多进程版pid_t id fork();if(id 0){// 子进程close(_listensock);if(fork() 0)//子进程本身exit(0); //子进程本身立即退出// 孙子进程称为孤儿进程OS领养OS在孤儿进程退出的时候由OS自动回收孤儿进程service(servicesock, client_ip, client_port);exit(0);}// 父进程waitpid(id, nullptr, 0); //不会阻塞close(servicesock);演示结果如下 4.4、version3.0多线程版 4.4.1、tcp_server.hpp 1、准备工作   1、为了让新线程执行业务处理需要设置回调函数。这里我们将其设置在了TcpServer类中并将其置为静态成员函数。注意线程结束时要关闭文件描述符和释放空间   PS此部分涉及的函数在多线程中有学习。 class TcpServer { private://【version3:多线程版本】: 为新线程提供的回调函数设置为静态成员函数是因为非静态成员函数有默认参数this不符合回调函数的格式要求static void* threadRoutine(void* args){线程分离结束服务若不捕捉捕捉属于阻塞式的我们要求服务端不能阻塞等待//则需要对线程进行分离pthread_detach(pthread_self());以避免系统层面的内存泄漏pthread_detach(pthread_self());threadData* data static_castthreadData *(args);//C11中的类型转换service(data-sock, data-ip, data-port);// 线程结束时需要关闭文件描述符、并释放new出来的空间close(data-sock);delete data;return nullptr;}//…… }2、void*args参数设置使用类可提供更多选择性。 //【version3:多线程版本】要在新线程中调用server要将其需要函数参数设置进void*args中。这里使用类来完成。 struct threadData {int sock;std::string ip;uint16_t port; };TcpServer类中需要改动的仍旧是start()部分代码如下 // 启动服务器void Start(){//signal(SIGCHLD, SIG_IGN); // 【version2.1:多进程版本】对SIGCHLD主动忽略SIGCHLD信号子进程退出的时候会自动释放自己的僵尸状态// 网络通讯角度作为一款网络服务器永远不退出的// OS角度服务器启动- 进程 - 常驻进程 - 永远在内存中存在除非挂了while (true){// 1、获取连接// 1.1、准备工作用于后续从网络中读取客户端的IP、端口号struct sockaddr_in clientaddr;memset(clientaddr, 0, sizeof(clientaddr));socklen_t len sizeof(clientaddr);// 1.2、连接int servicesock accept(_listensock, (struct sockaddr *)clientaddr, len);if (servicesock 0){logMessage(ERROR, accept, 获取链接失败, servicesock: %d , servicesock);continue; // 本次失败了结束此次循环即可可下一次重新获取连接}// 2、开始进行通讯服务// 2.1、获取客户端端口号、IPuint16_t client_port ntohs(clientaddr.sin_port); // uint16_t htons(uint16_t hostshort);std::string client_ip inet_ntoa(clientaddr.sin_addr); // char *inet_ntoa(struct in_addr in);logMessage(DEBUG, accept, 成功获取连接, servicesock: %d, client: [%s:%d] ., servicesock, client_ip.c_str(), client_port);// 2.2、根据需求处理客户端数据服务端的业务处理// ——————————【version3:多线程版本】———————————//// a、准备工作threadData* td new threadData;//在堆上td-sock servicesock;td-ip client_ip;td-port client_port;pthread_t pid;// b、创建线程pthread_create(pid, nullptr, threadRoutine, (void*)td);//PS注意这里不需要close(servicesock);因为主线程和新线程共享资源// —————————————————————————————————————————————//} }演示结果 4.5、version4.0线程池版 这里我们借用了之前写过的线程池库详细见多线程章节。 4.5.1、tcp_server.hpp 该线程库大体逻辑不变只是需要改动任务对象将其换成我们所需要的部分即task.hpp中封装的_func实际传入的是回调函数task.hpp中需要的成员变量是为了满足该_func函数。 #pragma once#include iostream #include string #include functional #include log.hpp// typedef std::functionvoid (int , const std::string , const uint16_t ) func_t; //写法一 using func_t std::functionvoid (int , const std::string , const uint16_t , const std::string ); //写法二class Task { public:Task(){}//无参构造Task(int sock, const std::string ip, uint16_t port, func_t func)//构造: _sock(sock), _ip(ip), _port(port), _func(func){}void operator ()(const std::string name){_func(_sock, _ip, _port, name);} public:int _sock;std::string _ip;uint16_t _port;func_t _func; };需要改动部分service业务处理函数新增了参数用以辅佐观察实则该函数可根据需求调节内容 // 【version4echo版服务器】和上面的相同只是为了显示是哪个新线程执行的业务处理新增了参数 static void service(int servicesock, const std::string clientip, const uint16_t clientport, const std::string threadname) {char server_buffer[SIZE];while (true){// a、读取客户端发来的数据ssize_t s read(servicesock, server_buffer, sizeof(server_buffer) - 1);if (s 0){server_buffer[s] \0; //\0的ASCII码是0std::cout threadname | clientip : clientport # server_buffer std::endl;}else if (s 0){logMessage(NORMAL, read, %s:%d 退出。, clientip.c_str(), clientport);break;}else{logMessage(ERROR, read, 读取失败, %d:%s, errno, strerror(errno));break;}// b、将读取到的结果返回write(servicesock, server_buffer, strlen(server_buffer));}close(servicesock);// c、结束需要将文件描述符关闭 }新增了成员变量std::unique_ptrThreadPoolTask _pthreadpool; 所以构造函数中的初始化列表也需要改动。其它改动部分仍旧在Start()中。 class TcpServer { private:const static int gbacklog 20; // listen中的参数设置public:TcpServer(uint16_t port, const std::string ip ) //在构造时初始化其它变量时也要初始化线程库: _port(port), _ip(ip), _listensock(-1),_pthreadpool(ThreadPoolTask::getThreadPool()){}~TcpServer(){close(_listensock);}// 初始化服务器bool InitServer(){// 1、创建套接字_listensock socket(AF_INET, SOCK_STREAM, 0); // 这里填入SOCK_STREAMif (_listensock 0){logMessage(ERROR, socket, 创建套接字失败: %s-%d , errno, strerror(errno));exit(2);}logMessage(DEBUG, socket, 创建套接字成功,sock: %d, _listensock);// 2、绑定struct sockaddr_in localaddr;bzero(localaddr, 0);localaddr.sin_family AF_INET;localaddr.sin_addr.s_addr _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 字节序转换localaddr.sin_port htons(_port);if (bind(_listensock, (struct sockaddr *)localaddr, sizeof localaddr) 0){logMessage(ERROR, bind, 绑定失败%d:%s, errno, strerror(errno));exit(3);}logMessage(DEBUG, bind, 绑定成功. );// 3、监听if (listen(_listensock, gbacklog) 0){logMessage(ERROR, listen, 监听失败%d:%s, errno, strerror(errno));exit(4);}logMessage(DEBUG, linsten, 监听成功, 初始化套接字完成。);return true;}// 启动服务器void Start(){_pthreadpool-run();//【version4】:在启动服务器时一并将线程池中的线程启动创建// 网络通讯角度作为一款网络服务器永远不退出的// OS角度服务器启动- 进程 - 常驻进程 - 永远在内存中存在除非挂了while (true){// 1、获取连接// 1.1、准备工作用于后续从网络中读取客户端的IP、端口号struct sockaddr_in clientaddr;memset(clientaddr, 0, sizeof(clientaddr));socklen_t len sizeof(clientaddr);// 1.2、连接int servicesock accept(_listensock, (struct sockaddr *)clientaddr, len);if (servicesock 0){logMessage(ERROR, accept, 获取链接失败, servicesock: %d , servicesock);continue; // 本次失败了结束此次循环即可可下一次重新获取连接}// 2、开始进行通讯服务// 2.1、获取客户端端口号、IPuint16_t client_port ntohs(clientaddr.sin_port); // uint16_t htons(uint16_t hostshort);std::string client_ip inet_ntoa(clientaddr.sin_addr); // char *inet_ntoa(struct in_addr in);logMessage(DEBUG, accept, 成功获取连接, servicesock: %d, client: [%s:%d] ., servicesock, client_ip.c_str(), client_port);// 2.2、根据需求处理客户端数据服务端的业务处理// ——————————【version4:线程池版本】———————————//// a、主线程派发任务对象将客服端当作一个任务对象创建后放入线程池中后续由线程池中的新线程来执行。Task t(servicesock, client_ip, client_port, service);_pthreadpool-pushTask(t);// ———————————————————————————————————————————//}}private:uint16_t _port; // 端口号std::string _ip; // IPint _listensock; // 套接字std::unique_ptrThreadPoolTask _pthreadpool; // 【version4】指向线程池的指针 }; 演示结果如下                            4.6、TCP协议通讯流程 5、序列化和反序列化应用层·一 5.1、基本情况介绍 1、应用层与应用层协议   应用层说明 我们之前写的一个个解决实际问题、满足日常需求的网络程序都是在应用层进行的。在之前几个小节的socket套接字编都是属于应用层的开发我们只是使用了传输层包装出来的接口而已且这些套接字只是演示了数据收发过程并非实际涉及协议。      应用层协议 协议是一种“约定”。通常只要保证一端发送时构造的数据, 在另一端能够正确的进行解析, 这种“约定”就是应用层协议。例如socket api的接口在读写数据时, 就是按 “字符串” 的方式来发送、接收数据的。         2、序列化和反序列化   基本介绍数据类型可以是字节流数据也可以是结构化数据。通常前者应用于网络传输后者应用于上层业务。 序列化把对象转换为字节序列的过程称为对象的序列化。 反序列化把字节序列恢复为对象的过程称为对象的反序列化。思考问题为什么要做这种序列化和反序列化的处理          5.2、网络版本的计算器NetCal编写(version1.0自定义版协议) 目的说明   ①从编码角度介绍什么是序列化、什么是反序列化。   ②手动定制协议→成熟的协议使用这里只是针对当前场景自定义协议这种完全自己写的协议会极大概率存在各种问题缺陷且若要对Cal中的协议做扩展需要改动很大 而使用别人提供的成熟方案相对而言还是能简单很多。因此此部分内容学习主要是为了理解协议定制的整体流程。       5.2.4、Sock.hpp TcpServer.hpp 5.2.4.1、Sock.hpp 说明该文件只是对sock函数接口进行了一次封装以便在客户端、服务端两个.cc文件中调用。      相关代码如下 #pragma once#include iostream #include string #include cstring #include cerrno #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include unistd.h #include Log.hppclass Sock { private:const static int gbacklog 20; // listen中参数设置详细将在后续介绍public:// 构造Sock(){}; // 无参构造// 析构~Sock(){};// 创建套接字int socket(int domain, int type, int protocol);int Socket(){int listensock socket(AF_INET, SOCK_STREAM, 0);if (listensock 0){logMessage(FATAL, socket:创建套接字失败。%d:%s, errno, strerror(errno));exit(2); // 退出}logMessage(NORMAL, socket:创建套接字成功, listensock:%d, listensock);return listensock; // 将套接字返回给TcpServer中的成员函数_listensock}// 绑定int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);void Bind(int sock, uint16_t port, const std::string ip 0.0.0.0){// 准备工作:sockaddr结构体struct sockaddr_in local;bzero(local, sizeof(local));local.sin_family AF_INET;local.sin_port htons(port); // 对端口号需要转换为网络字节序inet_pton(AF_INET, ip.c_str(), local.sin_addr); // 对ip:点分十进制风格--网络字节序四字节// 绑定if (bind(sock, (struct sockaddr *)local, sizeof(local)) 0){logMessage(FATAL, bind:绑定失败。%d:%s, errno, strerror(errno));exit(3); // 退出}logMessage(NORMAL, bind: 绑定成功。);}// 监听int listen(int sockfd, int backlog);void Listen(int sock){if (listen(sock, gbacklog) 0){logMessage(FATAL, listen:监听失败。%d:%s, errno, strerror(errno));exit(4); // 退出}logMessage(NORMAL, listen:监听成功。);}// 获取连接int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);int Accept(int listensock, std::string *ip, uint16_t *port) // 后两个*为输出型参数这里的作用是将accept接收到的ip、port返回给TcpServer。{// 准备工作用于接收源IP、源端口号struct sockaddr_in src;memset(src, 0, sizeof(src));src.sin_family AF_INET;socklen_t len sizeof(src);// 获取连接int servicesock accept(listensock, (struct sockaddr *)src, len);if (servicesock 0){logMessage(FATAL, accept:接收失败。%d:%s, errno, strerror(errno));exit(5);}logMessage(NORMAL, accept:接收成功。servicesock:%d, servicesock);if (ip) // 判空获取源IP*ip inet_ntoa(src.sin_addr); // 四字节网络字节序---主机字节序点分十进制if (port) // 判空获取源端口号*port ntohs(src.sin_port); // 网络字节序---主机字节序return servicesock;}// int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);bool Connect(int sock, const uint16_t port, const std::string ip){// 准备工作struct sockaddr_in aim;bzero(aim, sizeof(aim));aim.sin_family AF_INET;aim.sin_port htons(port); // 主机字节序---网络字节序aim.sin_addr.s_addr inet_addr(ip.c_str()); // 主机字节序点分十进制风格---网络字节序四字节// 连接if (connect(sock, (struct sockaddr *)aim, sizeof(aim)) 0){logMessage(FATAL, connect:连接失败。%d:%s, errno, strerror(errno));return false;}logMessage(NORMAL, connect:连接成功。);return true;} };5.2.4.2、TcpServer.hpp 说明 这里我们使用的仍旧是TCP网络通信相关内容的编写在之前章节演示过只是在其基础上封装了接口。 代码如下 使用的是线程版本的服务端。   1、这里新增了两个函数BindService是用于绑定业务处理函数可在CalServer.cc中根据需求实现并在启动服务器前将其绑定。   2、Excute是提供给新线程的用于执行业务函数。服务端能够处理的业务不止一种因此可以使用vectorfunc_t存储。如需要还可以使用unordered_mapstd::string, func_t为每个函数附带名称形成键值对。 #pragma once#include Sock.hpp #include vector #include functional #include pthread.hnamespace ns_tcpserver // 命名空间 {class TcpServer; // 声明struct ThreadData{// 构造ThreadData(int sock, TcpServer *server): sock_(sock), server_(server){ }// 析构~ThreadData(){ }int sock_;TcpServer *server_; // 为了直接传递this指针方便回调函数调用整个类成员及函数};using func_t std::functionvoid(int);// PS这里使用的是多线程版本的服务端class TcpServer{private:static void *ThreadRoutine(void *args) // 类中成员函数为了满足线程回调函数的参数需求设置为静态成员函数{// 线程分离:为了服务端不阻塞式等待需要对线程分离pthread_detach(pthread_self());// 解析args参数ThreadData *pdata static_castThreadData *(args);// 调用任务处理函数pdata-server_-Excute(pdata-sock_);// 线程处理完任务后需要关闭套接字释放申请出来的空间logMessage(NORMAL,新线程执行完毕关闭套接字%d,pdata-sock_);close(pdata-sock_);delete pdata;return nullptr;}public:// 构造初始化服务器TcpServer(const uint16_t port, const std::string ip 0.0.0.0){// 创建套接字listensock_ sock_.Socket();// 绑定套接字sock_.Bind(listensock_, port, ip);// 监听套接字sock_.Listen(listensock_);}// 析构~TcpServer(){if (listensock_ 0)close(listensock_); // 关闭套接字}// 绑定服务:提供给TcpServer.cc将服务器的业务处理函数插入到vector数组中void BindService(func_t func){func_.push_back(func);}// 新线程执行业务处理函数void Excute(int sock){for(auto f : func_){f(sock);}}// 启动服务器void Start(){// while死循环服务器启动后主线程一直在运行不断接收客户端请求并派发线程对其进行处理。while (true){// 连接uint16_t client_port; // 客服端端口号std::string client_ip; // 客户端IPint servicesock sock_.Accept(listensock_, client_ip, client_port); // 输出型参数调用accept函数获取客户端的端口号和IPif (servicesock -1) // 连接失败重新连接continue;logMessage(NORMAL, 连接成功, 可以开始业务处理。servicsock:%d, servicesock);// 派发新线程进行业务处理pthread_t tid;ThreadData *pdata new ThreadData(servicesock, this);pthread_create(tid, nullptr, ThreadRoutine, (void *)pdata);}}private:int listensock_; // 套接字Sock sock_; // 将套接字封装用于调用相关socket函数std::vectorfunc_t func_; // 服务端用于业务处理的函数这里使用了vector将函数存储表示服务器提供的处理业务可能有多种}; }5.2.5、CalServer.cc服务端 相关代码 #include TcpServer.hpp #include Protocol.hpp #include Log.hpp #include Daemon.hpp #include memory #include signal.hstatic void Usage(const std::string process) {std::cout \nUsage: process port\n std::endl; }ns_protocol::Response calculatorHelper(const ns_protocol::Request req) {// 根据op选项进行计算ns_protocol::Response resp(0, 0);switch (req.op_){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;elseresp.result_ req.x_ / req.y_;break;case %:if (req.y_ 0) // 模零错误需要设置状态码resp.code_ 2;elseresp.result_ req.x_ % req.y_;break;default: // 输入错误需要设置状态码resp.code_ -1;break;}return resp; // 返回结果响应结构体对象 }void calculator(int sock) // 服务端用于提供业务的函数 {std::string buffer;//临时缓冲区用于存储recv读取上来的请求while (true){// Recv读取请求客户端经过网络传输发来的数据属于字节流数据即客户端在发送前会做序列化处理bool ret ns_protocol::Recv(sock, buffer);if(!ret) //读取失败直接结束break;// 解析协议判断读取到的是否是完整的报文。std::string package ns_protocol::Decode(buffer);if(package.empty())//报文不完整continue;//继续重新读取直到读取到完整的报文logMessage(NORMAL,本次请求: %s,package.c_str());// 当读取到完整的报文后将请求反序列化字节流→结构化服务端要进行上层业务处理使用的是结构化的数据所以要进行反序列化ns_protocol::Request req;req.Deserialized(package);// 进行业务处理获取结构化存储的结果结构化数据ns_protocol::Response resp calculatorHelper(req);// 将结果序列化结构化→字节流服务端要将处理后的结果通过网络传输返回给客户端需要进行序列化处理std::string result_string resp.Serialize();// 对序列化后的结果添加长度信息形成完整的报文result_string ns_protocol::Encode(result_string);logMessage(NORMAL,本次响应: %s,result_string.c_str());// 返回客户端响应ns_protocol::Send(sock, result_string);} }// 启动./CalServer port int main(int argc, char *argv[]) {// 检查命令行参数,获取命令行端口号if (argc ! 2){Usage(argv[0]);exit(1);}//signal(SIGPIPE,SIG_IGN);//此句在自定义daemon中写过//让进程守护进程化MyDaemon();// 创建服务器std::unique_ptrns_tcpserver::TcpServer server(new ns_tcpserver::TcpServer(atoi(argv[1])));// 绑定业务处理函数这里是网络计算器server-BindService(calculator);// 启动服务器server-Start();return 0; } 5.2.3、Protocol.hpp定制的协议 以下为协议定制时的相关测试 //测试用于测试序列化、反序列化·自定义写法是否正确// 1.1、测试序列化ns_protocol::Request req(1234, 5678, );std::string str req.Serialize();std::cout Request-Serialize: str std::endl;std::cout std::endl;// 1.2、测试反序列化ns_protocol::Request req2;req2.Deserialized(str);std::cout Request-Deserialize: std::endl;std::cout x: req2.x_ y: req2.y_ op: req2.op_ std::endl;std::cout std::endl;// 2.1、测试序列化ns_protocol::Response resp(6012,0);std::string str2 resp.Serialize();std::cout Response-Serialize: str2 std::endl;std::cout std::endl;// 2.2、测试反序列化ns_protocol::Response resp2;resp2.Deserialized(str2);std::cout Response-Deserialize: std::endl;std::cout code: resp2.code_ result: resp2.result_ std::endl;std::cout std::endl;这里我们使用条件编译来完成先使用自定义方案理解之后会调整为成熟方案。如下 #pragma once #include iostream #include cstring #include string #include sys/types.h #include sys/socket.h #include jsoncpp/json/json.h// 这里的协议是用于服务网络版计算器的 namespace ns_protocol {// 控制条件编译自定义方案 现成方案#define MYSELF 1// 处理分隔符这里使用的是空格定义成宏方便根据需求修改 #define SPACE #define SPACE_LINE strlen(SPACE) // 加入数据长度并使用特殊字符\r\n区分各段 #define SEP \r\n #define SEP_LINE strlen(SEP)/// 请求结构体对象 ///class Request{public:// 构造Request(){};Request(int x, int y, char op): x_(x), y_(y), op_(op){}// 对请求进行序列化结构化数据→字节流数据std::string Serialize() // 将x_、y_、op_{ #ifdef MYSELF// version1 x_[空格] op_[空格] y_std::string str;str std::to_string(x_); // 先将对应的运算数转换为字符类型例如32--32。这里注意与ASCII中值为32的字符区别str SPACE; // 中间以我们设置的间隔符分割为了反序列化时能够提取每部分str op_; // op_本身就是char类型str SPACE;str std::to_string(y_);return str; #elsestd::cout TODO std::endl; #endif}// 对请求进行反序列化字节流数据→结构化数据bool Deserialized(const std::string str) // 获取x_、y_、op_{ #ifdef MYSELF//----------------------------------// version1 x_[空格] op_[空格] y_ 根据分隔符提取有效数放入结构化对象中// 例如1234[空格][空格]5678// a、找左运算数std::size_t left_oper str.find(SPACE);if (left_oper std::string::npos) // 没找到return false;// b、找右运算数std::size_t right_oper str.rfind(SPACE);if (right_oper std::string::npos) // 没找到return false;// c、提取运算数赋值给结构化对象成员x_ atoi((str.substr(0, left_oper)).c_str()); // string substr (size_t pos 0, size_t len npos) const;y_ atoi((str.substr(right_oper SPACE_LINE).c_str())); // 注意这里右运算符需要将[空格]跳过if (left_oper SPACE_LINE str.size())return false;elseop_ str[left_oper SPACE_LINE]; // 提取运算符时也要注意跳过分隔符[空格]return true;//---------------------------------- #elsestd::cout TODO std::endl; #endif}public:int x_; // 左运算数int y_; // 右运算数char op_; // 运算符};/// 响应结构体对象 ///class Response{public:// 构造函数Response(int result, int code): result_(result), code_(code){}Response() {}// 析构函数~Response() {}// 对响应序列化结构化数据→字节流数据std::string Serialize(){ #ifdef MYSELF// version1code_ [空格] result_// 例如0[空格]6912std::string str;str std::to_string(code_);str SPACE;str std::to_string(result_);return str; #elsestd::cout TODO std::endl; #endif}// 对响应反序列化字节流数据→结构化数据bool Deserialized(const std::string str){ #ifdef MYSELF//----------------------------------// version1code_ [空格] result_// 例如0[空格]6912// a、找分隔符std::size_t pos str.find(SPACE);if (pos std::string::npos) // 没找到return false;// b、获取状态码code_ atoi((str.substr(0, pos)).c_str());// c、获取计算结果result_ atoi((str.substr(pos SPACE_LINE)).c_str());return true;//---------------------------------- #elsestd::cout TODO std::endl; #endif}public:int result_; // 计算结果int code_; // 状态码用于判断结果是否正常};// 从网络中读取bool Recv(int sock, std::string out_buffer){char buffer[1024];ssize_t s recv(sock, buffer, sizeof(buffer) - 1, 0); // 阻塞式从网络中读取字节流数据if (s 0) // 读取成功{buffer[s] 0; // 文件尾字符\0out_buffer buffer; // 使用输出到out_buffer中这样在多次读取的情况下能保证数据连续性}else if (s 0){std::cout recv:quit. std::endl;return false;}else{std::cout recv error. std::endl;return false;}return true;}// 向网络中发送void Send(int sock, const std::string str){ssize_t s send(sock, str.c_str(), str.size(), 0);if (s 0){std::cout send error std::endl;}}// length\r\nx_[空格]op_[空格]y_\r\n// 实际正文部分x_[空格]op_[空格]y_后续需要对正文部分序列化std::string Decode(std::string buffer){// 找有没有\r\nstd::size_t pos buffer.find(SEP);if (pos std::string::npos) // 没找到说明本次读取/接收到的报文不完整需要继续读取/接收return ;// 执行到此说明确实有\r\n但不代表数据完整。此时需要提取length值与实际正文做比较判断是否读取到一个完整的报文int length_size atoi(buffer.substr(0, pos).c_str()); // 获取长度信息int remain_size buffer.size() - pos - 2 * SEP_LINE; // 获取剩余长度if (remain_size length_size) // 说明此时缓冲区buffer中存在一个完整的报文可以提取。{// 举例【length\r\nXXXXXXXXX\r\nlength\r\nXXXXXX\r\n】// string erase (size_t pos 0, size_t len npos);buffer.erase(0, pos SEP_LINE); // 移除缓冲区中的length\r\n即【XXXXXXXXX\r\nlength\r\nXXXXXX\r\n 】std::string str buffer.substr(0, length_size); // 获取length长度的字串即【XXXXXXXXX】buffer.erase(0, length_size SEP_LINE); // 移除缓冲区中正文及尾随的\r\n即【length\r\nXXXXXX\r\n 】return str;}elsereturn ; // 说明本次读写缓冲区中报文不完整需要继续读写。}// 返回一个带有长度信息的完整报头实际正文部分为 x_[空格]op_[空格]y_需要为其添加长度信息,变为length\r\nx_[空格]op_[空格]y_\r\n// 例如123 * 456 ---9\r\n123 * 456\r\nstd::string Encode(std::string s){// 1、获取正文长度std::string package std::to_string(s.size());// 2、加上SEP分隔符package SEP;// 3、加上正文package s;// 4、加上SEP分隔符package SEP;return package;}}5.2.6、CalClient.cc客户端 相关代码如下实际也可根据需求将读取、发送分线程执行。 #include iostream #include Sock.hpp #include Protocol.hpp #include Log.hppstatic void Usage(const std::string process) {std::cout \nUsage: process server_ip server_port\n std::endl; }//./CalClient server_ip server_port int main(int argc, char *argv[]) {// 检查命令行参数获取服务端端口号、IP地址if (argc ! 3){Usage(argv[0]);exit(1);}std::string server_ip argv[1];uint16_t server_port atoi(argv[2]);// 创建套接字Sock sock;int sockfd sock.Socket();// 连接服务端if (sock.Connect(sockfd, server_port, server_ip) 0){std::cerr client:连接失败 std::endl;exit(2);}// 与服务端通讯bool quit false;std::string buffer;while (!quit){std::cout std::endl;std::cout ---------------------- std::endl;// 创建一个请求ns_protocol::Request req;std::cout Please Enter # ;std::cin req.x_ req.op_ req.y_;logMessage(DEBUG, 请求结果为, x:%d, y:%d, op:%c, req.x_, req.y_, req.op_);// 将请求序列化std::string send_str req.Serialize();logMessage(DEBUG, 序列化结果为%s, send_str.c_str());// 添加长度信息send_str ns_protocol::Encode(send_str);logMessage(DEBUG, 添加长度信息后, %s, send_str.c_str());// 发送给服务端ns_protocol::Send(sockfd, send_str);// 从服务端读取结果while (true){// 读取响应结果bool ret ns_protocol::Recv(sockfd, buffer);if (!ret) // 读取失败{printf(DEBUG: 获取响应失败退出。\n);quit true; // 退出循环关闭sockfdbreak;}// 对响应解析是否获取到完整报文std::string package ns_protocol::Decode(buffer);if (package.empty()) // 说明本次接收到的报文不完整继续读取{logMessage(DEBUG, 报文不完整继续读取。);continue;}logMessage(DEBUG, 读取到完整报文, %s\n, package.c_str());// 到此步骤获取到了完整报文可以反序列化ns_protocol::Response resp;resp.Deserialized(package);// 显示结果std::string err;switch (resp.code_){case 1:err 除零错误;break;case 2:err 模零错误;break;case -1:err 非法操作;break;default:std::cout result: resp.result_ std::endl;break;}if (!err.empty()) // 显示错误信息std::cout code: err std::endl;break;}}close(sockfd);return 0; }5.3、网络版本的计算器NetCal编写(version2.0json版协议) 5.3.1、守护进程 5.3.1.1、问题引入 说明一什么是前台进程   在 Linux 中前台进程是指当前正在运行的进程它与用户交互并占用终端。当用户在终端中输入命令时该命令所启动的进程就是前台进程。前台进程会占用终端直到它执行完毕或者被中断例如按下 CtrlC。在前台进程运行期间用户可以通过键盘输入命令或者发送信号来与进程交互。   守护进程全部都是在前台运行的。 任何xshell登陆只允许一个前台进程和多个后台进程。 说明二进程有自己的pid、 ppid、组ID 问题说明退出登录不同的shell有不同处理。当我们启动服务端其为前台进程进入同一个会话中若关闭shell退出登录该服务端进程可能会随着会话结束而被杀掉那么此时用户端就无法访问到服务端了。即不符合服务端一直运行的需求。      为了解决这个问题引入守护进程。自成一个会话的进程即守护进程。             5.3.1.2、如何做到setsid、daemon 1、setsid   man 2 setsid NAMEsetsid - creates a session and sets the process group IDSYNOPSIS#include unistd.hpid_t setsid(void);DESCRIPTIONsetsid() creates a new session if the calling process is not a process group leader. The calling process is the leader of the new ses‐sion, the process group leader of the new process group, and has no controlling terminal. The process group ID and session ID of thecalling process are set to the PID of the calling process. The calling process will be the only process in this new process group and inthis new session.RETURN VALUEOn success, the (new) session ID of the calling process is returned. On error, (pid_t) -1 is returned, and errno is set to indicate theerror.ERRORSEPERM The process group ID of any process equals the PID of the calling process. Thus, in particular, setsid() fails if the callingprocess is already a process group leader.说明setsid要成功被调用必须保证当前进程不是进程组的组长。因此可以通过创建子进程的方式保证当前进程不是进程组的组长。            2、daemon   man daemon系统中有相关函数可以为我们做到让一个进程成为守护进程。 NAMEdaemon - run in the backgroundSYNOPSIS#include unistd.hint daemon(int nochdir, int noclose);Feature Test Macro Requirements for glibc (see feature_test_macros(7)):daemon(): _BSD_SOURCE || (_XOPEN_SOURCE _XOPEN_SOURCE 500)DESCRIPTIONThe daemon() function is for programs wishing to detach themselves from the controlling terminal and run in the background as system dae‐mons.If nochdir is zero, daemon() changes the calling processs current working directory to the root directory (/); otherwise, the currentworking directory is left unchanged.If noclose is zero, daemon() redirects standard input, standard output and standard error to /dev/null; otherwise, no changes are made tothese file descriptors.RETURN VALUE(This function forks, and if the fork(2) succeeds, the parent calls _exit(2), so that further errors are seen by the child only.) Onsuccess daemon() returns zero. If an error occurs, daemon() returns -1 and sets errno to any of the errors specified for the fork(2) andsetsid(2). 说明虽然系统提供了相关函数但一般项目里倾向于自定义编写相关代码。即在Linux中正确的写一个让进程守护进程化的代码。             5.3.1.3、Daemon.hpp 1、dev/null   ls /dev/nulldev/null是一个特殊的设备文件该文件接收到的任何数据都会被丢弃也无法从该文件中读取到任何数据。因此其被称为文件黑洞也被成为位桶(bit bucket)。 2、相关编写 #pragma once#include iostream #include signal.h #include unistd.h #include sys/types.h #include sys/stat.h #include fcntl.hvoid MyDaemon() {// 1. 忽略信号SIGPIPESIGCHLDsignal(SIGPIPE, SIG_IGN); // sighandler_t signal(int signum, sighandler_t handler);signal(SIGCHLD, SIG_IGN);// 2. 不要让自己成为组长if (fork() 0)exit(0); // 将父进程退出那么此时运行的就是子进程其不会成为进程组的组长// 3. 调用setsid,该函数能够创建会话并设置进程组idsetsid(); // pid_t setsid(void);// 4. 标准输入标准输出标准错误的重定向int devnull_fd open(/dev/null, O_RDONLY | O_WRONLY); // int open(const char *pathname, int flags);if(devnull_fd 0)//文件打开成功{dup2(devnull_fd, 0);dup2(devnull_fd, 1);dup2(devnull_fd, 2);close(devnull_fd);}} 注意这里dup2的使用         3、结果演示 5.3.1.4、log.hpp 说明一旦服务端成为了守护进程那么其相关的日志信息就不能直接向显示器打印因此日志需要向文件中写入。我们可定期观察日志文件的内容以此检查服务端运行情况。 #pragma once#include iostream #include cstdio #include cstdarg #include ctime #include string// 日志是有日志级别的 #define DEBUG 0 #define NORMAL 1 #define WARNING 2 #define ERROR 3 #define FATAL 4const char *gLevelMap[] {DEBUG,NORMAL,WARNING,ERROR,FATAL };#define LOGFILE ./calculator.log// 完整的日志功能至少: 日志等级 时间 支持用户自定义(日志内容, 文件行文件名) void logMessage(int level, const char *format, ...) { #ifndef DEBUG_SHOWif(level DEBUG) return; #endif//日志的标准部分日志等级、时间char stdBuffer[1024]; time_t timestamp time(nullptr);snprintf(stdBuffer, sizeof stdBuffer, [%s] [%ld] , gLevelMap[level], timestamp);//日志的自定义部分用户自定义输入的内容char logBuffer[1024]; va_list args;va_start(args, format);// vprintf(format, args);vsnprintf(logBuffer, sizeof logBuffer, format, args);va_end(args); 将日志写到显示器上//printf(%s%s\n, stdBuffer, logBuffer);// 将日志写到文件中FILE *fp fopen(LOGFILE, a);fprintf(fp, %s%s\n, stdBuffer, logBuffer);fclose(fp); }关于log可根据自己的需求做调整这里只是简单的使用演示。(实际我们对CalCilent.cc和CalServer.cc都使用了同一个log.hpp为了方便日志观察可分别建立两个不同的log这里只是简单举例log在这种场景编写下的作用)                   5.3.2、使用json完成序列化 5.3.2.1、基本使用介绍 1、安装与编译   JsonCpp是一款开源的C库专用于解析和生成JSON格式的数据。相关文档JsonCpp。 安装指令sudo yum install jsoncpp-devel。            2、jsoncpp中主要的类   Json::Value可以表示所有支持的类型如int , double ,string , object, array等. Json::Reader将文件流或字符串创解析到Json::Value中主要使用parse函数。Json::Reader的构造函数还允许用户使用特性Features来自定义Json的严格等级。 Json::Writer与Json ::Reader相反将Json::Value转换成字符串流等Writer类是一个纯虚类并不能直接使用。在此我们使用 Json::Writer 的子类Json::FastWriter(将数据写入一行,没有格式),Json::StyledWriter(按json格式化输出,易于阅读)。         3、相关使用演示   验证代码 #include string #include iostream #include jsoncpp/json/json.hint main() {// 假设有三个变量int a 19;double b 3.14;char c *;// 使用json存储Json::Value root; // 定义一个万能对象Json::Value用来表示Json中的任何一种value抽象数据类型root[a] a;root[b] b;root[c] c;Json::Value sub;sub[s1] hello;sub[s2] json;root[sub] sub; // 对象中放入对象套娃使用// 序列化Json::StyledWriter writer1;std::string str1 writer1.write(root);Json::FastWriter writer2;std::string str2 writer2.write(root);// 两种方式的结果演示前者会做一些字段分隔处理方便查看。一般直接使用可用后者。std::cout str1 std::endl;printf(\n\n);std::cout str2 std::endl;// 反序列化Json::Value buffer1;Json::Value buffer2;Json::Reader reader;reader.parse(str1, buffer1);reader.parse(str1, buffer2);std::cout buffer1[a].asInt() std::endl;std::cout buffer1[b].asDouble() std::endl;std::cout buffer1[c].asInt() std::endl;return 0; } 结果演示 5.3.3、改动NetCal 基于上述我们对之前的协议做出修改使用json来完成序列化和反序列化。 5.3.3.1、主要部分 对请求使用json版本的序列化和反序列化如下 class Request{public:// 构造Request(){};Request(int x, int y, char op): x_(x), y_(y), op_(op){}// 对请求进行序列化结构化数据→字节流数据std::string Serialize() // {// 序列化Json::Value root; // 定义一个万能对象将需要的键值对存入root[x] x_;root[y] y_;root[op] op_;Json::FastWriter writer; // 进行序列化并将结果返回return writer.write(root);}// 对请求进行反序列化字节流数据→结构化数据bool Deserialized(const std::string str) {// 反序列化Json::Value root;Json::Reader reader;reader.parse(str, root); // 对str进行反序列化将结果取出x_ root[x].asInt();y_ root[y].asInt();op_ root[op].asInt();return true;}public:int x_; // 左运算数int y_; // 右运算数char op_; // 运算符}; 对响应使用json版的序列化和反序列化如下。 class Response{public:// 构造函数Response(int result, int code): result_(result), code_(code){}Response() {}// 析构函数~Response() {}// 对响应序列化结构化数据→字节流数据std::string Serialize(){// 序列化Json::Value root;root[code] code_;root[result] result_;Json::FastWriter writer;return writer.write(root); // 将序列化后的结果返回}// 对响应反序列化字节流数据→结构化数据bool Deserialized(const std::string str){// 反序列化Json::Value root;Json::Reader reader;reader.parse(str, root);code_ root[code].asInt();result_ root[result].asInt();return true;}public:int result_; // 计算结果int code_; // 状态码用于判断结果是否正常};演示结果如下 5.3.3.2、整体Protocol.hpp 以下是完整的代码实现方便观察总览。 #pragma once #include iostream #include cstring #include string #include sys/types.h #include sys/socket.h #include jsoncpp/json/json.h// 这里的协议是用于服务网络版计算器的 namespace ns_protocol {// t控制条件编译自定义方案 现成方案 // #define MYSELF 1// 处理分隔符这里使用的是空格定义成宏方便根据需求修改 #define SPACE #define SPACE_LINE strlen(SPACE) // 加入数据长度并使用特殊字符\r\n区分各段 #define SEP \r\n #define SEP_LINE strlen(SEP)/// 请求结构体对象 ///class Request{public:// 构造Request(){};Request(int x, int y, char op): x_(x), y_(y), op_(op){}// 对请求进行序列化结构化数据→字节流数据std::string Serialize() // 将x_、y_、op_{ #ifdef MYSELF// version1 x_[空格] op_[空格] y_std::string str;str std::to_string(x_); // 先将对应的运算数转换为字符类型例如32--32。这里注意与ASCII中值为32的字符区别str SPACE; // 中间以我们设置的间隔符分割为了反序列化时能够提取每部分str op_; // op_本身就是char类型str SPACE;str std::to_string(y_);return str; #else// 序列化Json::Value root; // 定义一个万能对象将需要的键值对存入root[x] x_;root[y] y_;root[op] op_;Json::FastWriter writer; // 进行序列化并将结果返回return writer.write(root); #endif}// 对请求进行反序列化字节流数据→结构化数据bool Deserialized(const std::string str) // 获取x_、y_、op_{ #ifdef MYSELF//----------------------------------// version1 x_[空格] op_[空格] y_ 根据分隔符提取有效数放入结构化对象中// 例如1234[空格][空格]5678// a、找左运算数std::size_t left_oper str.find(SPACE);if (left_oper std::string::npos) // 没找到return false;// b、找右运算数std::size_t right_oper str.rfind(SPACE);if (right_oper std::string::npos) // 没找到return false;// c、提取运算数赋值给结构化对象成员x_ atoi((str.substr(0, left_oper)).c_str()); // string substr (size_t pos 0, size_t len npos) const;y_ atoi((str.substr(right_oper SPACE_LINE).c_str())); // 注意这里右运算符需要将[空格]跳过if (left_oper SPACE_LINE str.size())return false;elseop_ str[left_oper SPACE_LINE]; // 提取运算符时也要注意跳过分隔符[空格]return true;//---------------------------------- #else// 反序列化Json::Value root;Json::Reader reader;reader.parse(str, root); // 对str进行反序列化将结果取出x_ root[x].asInt();y_ root[y].asInt();op_ root[op].asInt();return true; #endif}public:int x_; // 左运算数int y_; // 右运算数char op_; // 运算符};/// 响应结构体对象 ///class Response{public:// 构造函数Response(int result, int code): result_(result), code_(code){}Response() {}// 析构函数~Response() {}// 对响应序列化结构化数据→字节流数据std::string Serialize(){ #ifdef MYSELF// version1code_ [空格] result_// 例如0[空格]6912std::string str;str std::to_string(code_);str SPACE;str std::to_string(result_);return str; #else// 序列化Json::Value root;root[code] code_;root[result] result_;Json::FastWriter writer;return writer.write(root); // 将序列化后的结果返回 #endif}// 对响应反序列化字节流数据→结构化数据bool Deserialized(const std::string str){ #ifdef MYSELF//----------------------------------// version1code_ [空格] result_// 例如0[空格]6912// a、找分隔符std::size_t pos str.find(SPACE);if (pos std::string::npos) // 没找到return false;// b、获取状态码code_ atoi((str.substr(0, pos)).c_str());// c、获取计算结果result_ atoi((str.substr(pos SPACE_LINE)).c_str());return true;//---------------------------------- #else// 反序列化Json::Value root;Json::Reader reader;reader.parse(str, root);code_ root[code].asInt();result_ root[result].asInt();return true; #endif}public:int result_; // 计算结果int code_; // 状态码用于判断结果是否正常};// 从网络中读取bool Recv(int sock, std::string out_buffer){char buffer[1024];ssize_t s recv(sock, buffer, sizeof(buffer) - 1, 0); // 阻塞式从网络中读取字节流数据if (s 0) // 读取成功{buffer[s] 0; // 文件尾字符\0out_buffer buffer; // 使用输出到out_buffer中这样在多次读取的情况下能保证数据连续性}else if (s 0){std::cout recv:quit. std::endl;return false;}else{std::cout recv error. std::endl;return false;}return true;}// 向网络中发送void Send(int sock, const std::string str){ssize_t s send(sock, str.c_str(), str.size(), 0);if (s 0){std::cout send error std::endl;}}// length\r\nx_[空格]op_[空格]y_\r\n// 实际正文部分x_[空格]op_[空格]y_后续需要对正文部分序列化std::string Decode(std::string buffer){// 找有没有\r\nstd::size_t pos buffer.find(SEP);if (pos std::string::npos) // 没找到说明本次读取/接收到的报文不完整需要继续读取/接收return ;// 执行到此说明确实有\r\n但不代表数据完整。此时需要提取length值与实际正文做比较判断是否读取到一个完整的报文int length_size atoi(buffer.substr(0, pos).c_str()); // 获取长度信息int remain_size buffer.size() - pos - 2 * SEP_LINE; // 获取剩余长度if (remain_size length_size) // 说明此时缓冲区buffer中存在一个完整的报文可以提取。{// 举例【length\r\nXXXXXXXXX\r\nlength\r\nXXXXXX\r\n】// string erase (size_t pos 0, size_t len npos);buffer.erase(0, pos SEP_LINE); // 移除缓冲区中的length\r\n即【XXXXXXXXX\r\nlength\r\nXXXXXX\r\n 】std::string str buffer.substr(0, length_size); // 获取length长度的字串即【XXXXXXXXX】buffer.erase(0, length_size SEP_LINE); // 移除缓冲区中正文及尾随的\r\n即【length\r\nXXXXXX\r\n 】return str;}elsereturn ; // 说明本次读写缓冲区中报文不完整需要继续读写。}// 返回一个带有长度信息的完整报头实际正文部分为 x_[空格]op_[空格]y_需要为其添加长度信息,变为length\r\nx_[空格]op_[空格]y_\r\n// 例如123 * 456 ---9\r\n123 * 456\r\nstd::string Encode(std::string s){// 1、获取正文长度std::string package std::to_string(s.size());// 2、加上SEP分隔符package SEP;// 3、加上正文package s;// 4、加上SEP分隔符package SEP;return package;}}
http://www.huolong8.cn/news/137762/

相关文章:

  • 思睿鸿途北京网站建设实时热榜
  • 做网站一般有几个关键词万网域名的优势
  • 2017网站建设前景郑州做网站外包的公司
  • 中关村在线官方网站电脑wordpress菜单判断
  • 上海网站设计推荐刻百度竞价推广效果好吗
  • 做复印机的模板网站模板公司
  • 做美工用什么素材网站广州vps网站
  • 如何优化网站结构电影wordpress
  • 成都装修网站制作价格wordpress可以放视频播放器
  • 企业信息网站网站建设和管理情况调查表
  • 网易企业邮箱后缀是多少seo外包团队
  • 厦门人才网唯一官方网站登录入口wordpress模仿做slider
  • 英文网站源码下载做网站排名软件
  • 贵州黔东南双控体系建设网站延安做网站电话
  • 一级做爰片软件网站辽宁沈阳做网站
  • 怎么做网站写手做外贸有免费的网站吗
  • 网站平台免费大气红色礼品公司网站源码
  • 中国电力建设企业协会网站2345网址导航删除办法
  • php快速建站系统长沙响应式网站设计有哪些
  • 深圳市网站建设公开一个设计工作室需要什么
  • 有哪些好的网站建设公司jsp网站开发学习心得
  • 公司网站备案需要什么汕头app开发
  • 网站怎么做404页面宁波网络推广优化公司
  • 做签到的网站苍南配网设计
  • 基于wordpress的英文小游戏站青岛手机网站建设电话
  • 威海哪里可以做网站做seo有什么好处
  • 个人购物网站wordpress 什么值得买 主题
  • 网站开发设计实训实训总结wordpress登录工具
  • 可以做微信推送的网站高校网站建设模板
  • 做粤菜的视频网站宁波网络推广软件