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

延庆青岛网站建设什么软件做电影短视频网站

延庆青岛网站建设,什么软件做电影短视频网站,电子商务网站建设服务模式论文,虚拟货币网站开发目录 1、实现一个TCP网络程序#xff08;单进程版#xff09; 1.1、服务端serverTcp.cc文件 服务端创建套接字 服务端绑定 服务端监听 服务端获取连接 服务端提供服务 服务端main函数命令行参数 服务端serverTcp.cc总代码 1.2、客户端clientTcp.cc文件 客户端main函数命令行…目录 1、实现一个TCP网络程序单进程版 1.1、服务端serverTcp.cc文件 服务端创建套接字 服务端绑定 服务端监听 服务端获取连接 服务端提供服务 服务端main函数命令行参数 服务端serverTcp.cc总代码 1.2、客户端clientTcp.cc文件 客户端main函数命令行参数 客户端创建套接字 客户端的bind、listen、accept问题 客户端连接服务器 客户端发起请求 客户端clinetTcp.cc总代码 1.3、服务器测试 1.4、单执行流服务器的问题 2、多进程版的TCP网络程序 捕捉SIGCHLD信号 让孙子进程提供服务 3、多线程版的TCP网络程序 4、线程池版的TCP网络程序 线程池变形 5、总代码gitee链接 1、实现一个TCP网络程序单进程版 1.1、服务端serverTcp.cc文件 我们把服务器封装成一个ServerTcp类该类里主要有如下几个任务 服务端创建套接字服务端绑定服务端监听服务端获取链接服务端提供服务服务端main函数命令行参数下面依次演示 服务端创建套接字 我们把服务器封装成一个ServerTcp类当我们定义出一个服务器对象后需要马上初始化服务器而初始化服务器首先要创建套接字。创建套接字的函数叫做socket函数再回顾下其函数原型 int socket(int domain, int type, int protocol); 这里TCP服务器在调用socket函数创建套接字时参数设置如下 domain协议家族选择AF_INET因为我们要进行的是网络通信。type创建套接字时所需的服务器类型应该是SOCK_STREAM因为我们编写的是TCP服务器SOCK_STREAM提供的就是一个有序的、可靠的、全双工的、基于连接的流式服务。注意我UDP是用户数据报服务。protocol协议类型默认设置为0即可。若socket创建失败则复用logMessage函数打印相关日志信息并直接exit退出程序。 class ServerTcp { public:// 构造函数 析构函数 public:// 初始化void init(){// 1、创建socketsock_ socket(AF_INET, SOCK_STREAM, 0);if (sock_ 0){logMessage(FATAL, socket: %s, strerror(errno)); // 创建失败打印日志exit(SOCKET_ERR);}logMessage(DEBUG, socket: %s, %d, strerror(errno), sock_);} private:int sock_; // socketuint16_t port_; // portstring ip_; // ip }; 服务端绑定 当套接字已经创建成功了但作为一款服务器来讲如果只是把套接字创建好了那我们也只是在系统层面上打开了一个文件操作系统将来并不知道是要将数据写入到磁盘还是刷到网卡此时该文件还没有与网络关联起来。所以我们需要调用bind函数进行绑定操作。绑定的步骤如下 1、绑定网络信息先填充基本信息到struc sockaddr_in结构体。定义struc sockaddr_in结构体对象local复用memset函数对local进行初始化。将协议家族、端口号、IP地址等信息填充到该结构体变量当中。注意协议家族这里设定的是PF_INET。服务器的端口号是要发给对方的在发送到网络之前要复用htons主机转网络函数把端口号port_转成网络序列才能向外发送。ip地址默认是字符串风格点分十进制的这里复用inet_aton函数将字符串IP转换成整数IPinet_addr除了做转换还会自动给我们做主机转网络。注意若ip地址是空的那就用INADDR_ANY这个宏否则再用inet_addr函数。这个宏就是0因此在设置时不需要进行网络字节序的转换。2、绑定网络信息上述local临时变量struc sockaddr_in结构体对象是在用户栈上开辟的要将其写入内核中。复用bind函数完成绑定操作。bind成功与否均复用logMessage函数打印相关日志信息。由于bind函数提供的是通用参数类型因此在传入结构体地址时还需要将struct sockaddr_in*强转为struct sockaddr*类型后再进行传入。class ServerTcp { public:// 构造函数 析构函数 public:// 初始化void init(){// 1、创建socket// 2、bind绑定// 2.1、填充服务器信息struct sockaddr_in local; // 用户栈memset(local, 0, sizeof(local));local.sin_family PF_INET;local.sin_port htons(port_);ip_.empty() ? (local.sin_addr.s_addr INADDR_ANY) : (inet_aton(ip_.c_str(), local.sin_addr));// 2.2、将本地socket信息写入sock_对应的内核区域if (bind(sock_, (const struct sockaddr *)local, sizeof(local)) -1){logMessage(FATAL, bind: %s, strerror(errno)); // 绑定失败打印日志exit(BIND_ERR);}logMessage(DEBUG, bind: %s, %d, strerror(errno), sock_);} private:int sock_;// socketuint16_t port_; // portstring ip_; // ip }; 服务端监听 listen接口说明 UDP服务器的初始化操作只有两步第一步就是创建套接字第二步就是绑定。而TCP服务器是面向连接的客户端在正式向TCP服务器发送数据之前需要先与TCP服务器建立连接然后才能与服务器进行通信。因此TCP服务器需要时刻注意是否有客户端发来连接请求此时就需要将TCP服务器创建的套接字设置为监听状态。设置套接字为监听状态的函数叫做listen该函数的函数原型如下 int listen(int sockfd, int backlog); 参数说明 sockfd需要设置为监听状态的套接字对应的文件描述符。backlog全连接队列的最大长度。如果有多个客户端同时发来连接请求此时未被服务器处理的连接就会放入连接队列该参数代表的就是这个全连接队列的最大长度一般不要设置太大设置为5或10即可。返回值说明 监听成功返回0监听失败返回-1同时错误码会被设置。代码逻辑如下 TCP是面向连接的所以要让TCP服务器时刻注意是否有客户端发来连接请求此时就需要将TCP服务器创建的套接字设置为监听状态。监听失败就打印日志信息并直接退出。因为监听失败就意味着TCP服务器无法接受客户端发来的连接请求。class ServerTcp { public:// 构造函数 析构函数 public:// 初始化void init(){// 1、创建socket// 2、bind绑定// 3、监听socketif (listen(sock_, 5) 0){logMessage(FATAL, listen: %s, strerror(errno)); // 监听失败打印日志exit(LISTEN_ERR);}logMessage(DEBUG, listen: %s, %d, strerror(errno), sock_); } private:int sock_; // socketuint16_t port_; // portstring ip_; // ip }; 初始化TCP服务器时创建的套接字并不是普通的套接字而应该叫做监听套接字。为了表明寓意我们将代码中套接字的名字由sock_改为listensock_。 服务端获取连接 accept接口说明 TCP服务器初始化后就可以开始运行了但TCP服务器在与客户端进行网络通信之前服务器需要先获取到客户端的连接请求。究竟是谁连接我的。获取连接的函数叫做accept该函数的函数原型如下 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 参数说明 sockfd特定的监听套接字表示从该监听套接字中获取连接。addr对端网络相关的属性信息包括协议家族、IP地址、端口号等。addrlen调用时传入期望读取的addr结构体的长度返回时代表实际读取到的addr结构体的长度这是一个输入输出型参数。返回值说明 获取连接成功返回接收到的套接字的文件描述符获取连接失败返回-1同时错误码会被设置。调用accept函数获取连接时是从监听套接字当中获取的。如果accept函数获取连接成功此时会返回接收到的套接字对应的文件描述符。监听套接字与accept函数返回的套接字的作用 监听套接字用于获取客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接。accept函数返回的套接字用于为本次accept获取到的连接提供服务为用户提供网络服务主要是进行IO。监听套接字的任务只是不断获取新连接而真正为这些连接提供服务的套接字是accept函数返回的套接字而不是监听套接字。代码逻辑如下 定义struct sockaddr_in的对象peer定义len为peer的字节数复用accept函数获取连接。若返回值0说明连接失败但是TCP服务器不会因为某个连接失败而退出因此服务端获取连接失败后应该继续获取连接。获取连接成功后要获取客户端的基本信息将客户端的IP地址和端口号信息进行输出需要调用inet_ntoa函数将整数IP转换成字符串IP调用ntohs函数将端口号由网络序列转换成主机序列。class ServerTcp { public:// 构造函数 析构函数 public:// 初始化void init(){// 1、创建socket// 2、bind绑定// 3、监听socket}// 启动服务端void loop(){while (true){// 4、获取连接struct sockaddr_in peer;socklen_t len sizeof(peer);int serviceSock accept(listensock_, (struct sockaddr *)peer, len);if (serviceSock 0){logMessage(WARINING, accept: %s[%d], strerror(errno), serviceSock); // 获取连接失败continue;}// 4.1、获取客户端基本信息uint16_t peerPort ntohs(peer.sin_port);string peerIp inet_ntoa(peer.sin_addr);logMessage(DEBUG, accept: %s | %s:[%d], socket fd: %d, strerror(errno), peerIp.c_str(), peerPort, serviceSock);}}private:int listensock_;// socketuint16_t port_; // portstring ip_; // ip }; 服务端接受连接测试 这里我们客户端还没有写但是我们可以先允许服务端然后在windows下的浏览器上用当前云服务器ip124.71.25.237端口号8080进行访问测试浏览器常见的应用层协议是http或https其底层对应的也是TCP协议因此浏览器也可以向当前这个TCP服务器发起请求连接。测试如下 注意 至于这里为什么浏览器一次会向我们的TCP服务器发起两次请求这个问题这里就不作讨论了我们只是要证明当前TCP服务器能够正常接收外部的请求连接。服务端提供服务 read接口说明 现在TCP服务器已经能够获取连接请求了下面当然就是要对获取到的连接进行处理。为了让通信双方都能看到对应的现象我们这里就实现一个简单的回声TCP服务器服务端在为客户端提供服务时就简单的将客户端发来的数据进行输出并且将客户端发来的数据重新发回给客户端即可。当客户端拿到服务端的响应数据后再将该数据进行打印输出此时就能确保服务端和客户端能够正常通信了。TCP服务器读取数据的函数叫做read该函数的函数原型如下 ssize_t read(int fd, void *buf, size_t count); 参数说明 fd特定的文件描述符表示从该文件描述符中读取数据。buf数据的存储位置表示将读取到的数据存储到该位置。count数据的个数表示从该文件描述符中读取数据的字节数。返回值说明 如果返回值大于0则表示本次实际读取到的字节个数。如果返回值等于0则表示对端已经把连接关闭了。如果返回值小于0则表示读取时遇到了错误。read返回值为0表示对端连接关闭。这实际和本地进程间通信中的管道通信是类似的当使用管道进行通信时可能会出现如下情况 写端进程不写读端进程一直读此时读端进程就会被挂起因为此时数据没有就绪。读端进程不读写端进程一直写此时当管道被写满后写端进程就会被挂起因为此时空间没有就绪。写端进程将数据写完后将写端关闭此时当读端进程将管道当中的数据读完后就会读到0。读端进程将读端关闭此时写端进程就会被操作系统杀掉因为此时写端进程写入的数据不会被读取。这里的写端就对应客户端如果客户端将连接关闭了那么此时服务端将套接字当中的信息读完后就会读取到0因此如果服务端调用read函数后得到的返回值为0此时服务端就不必再为该客户端提供服务了。 write接口说明 TCP服务器写入数据的函数叫做write该函数的函数原型如下ssize_t write(int fd, const void *buf, size_t count); 参数说明 fd特定的文件描述符表示将数据写入该文件描述符对应的套接字。buf需要写入的数据。count需要写入数据的字节个数。返回值说明 写入成功返回实际写入的字节数写入失败返回-1同时错误码会被设置。当服务端调用read函数收到客户端的数据后就可以再调用write函数将该数据再响应给客户端。 代码逻辑如下 注意服务端读取数据是服务套接字中读取的而写入数据的时候也是写入进服务套接字的。也就是说这里为客户端提供服务的套接字既可以读取数据也可以写入数据这就是TCP全双工的通信的体现。这里我们把服务端提供服务的过程封装成一个transService函数其内部完成的主要功能是完成大小写转化首先调用read函数读取客户端发来的数据这里且假定读取的是字符串。read函数返回值为s。若返回值s 0说明读取成功在内部首先调用strcasecmp函数判断客户端是否需要服务端提供服务若不需要quit则打印日志并退出若需要在内部完成大小写转化的功能。转化完成后调用write函数将结果返回给客户端若返回值s 0或s 0此时就应该直接将服务套接字对应的文件描述符关闭。因为文件描述符本质就是数组的下标因此文件描述符的资源是有限的如果我们一直占用那么可用的文件描述符就会越来越少因此服务完客户端后要及时关闭对应的文件描述符否则会导致文件描述符泄漏。class ServerTcp { public:// 构造函数 析构函数 public:// 初始化void init(){// 1、创建socket// 2、bind绑定// 3、监听socket}// 启动服务端void loop(){while (true){// 4、获取连接// 4.1、获取客户端基本信息// 5、提供服务echo ( 小写 - 大写 )// 5.0 v0版本transService(serviceSock, peerIp, peerPort);}}// 大小写转化服务// TCP UDP: 支持全双工void transService(int sock, const string clientIp, uint16_t clientPort){assert(socket 0);assert(!clientIp.empty());assert(clientPort 1024);char inbuffer[BUFFER_SIZE];while (true){ssize_t s read(sock, inbuffer, sizeof(inbuffer) - 1); // 我们认为读取到的都是字符串if (s 0) // 读取成功{inbuffer[s] \0; // read successif (strcasecmp(inbuffer, quit) 0) // strcasecmp是忽略大小写比较的函数{// 客户端输入退出logMessage(DEBUG, client quit -- %s[%d], clientIp.c_str(), clientPort);break;}logMessage(DEBUG, trans before: %s[%d] %s, clientIp.c_str(), clientPort, inbuffer);// 可以进行大小写转化了for (int i 0; i s; i){if (isalpha(inbuffer[i]) islower(inbuffer[i]))inbuffer[i] toupper(inbuffer[i]);}logMessage(DEBUG, trans after: %s[%d] %s, clientIp.c_str(), clientPort, inbuffer);// 进行写回操作write(sock, inbuffer, strlen(inbuffer));}else if (s 0) // 对方关闭{logMessage(DEBUG, client quit -- %s[%d], clientIp.c_str(), clientPort);break;}else // 读取出错{logMessage(DEBUG, %s[%d] - read: %s, clientIp.c_str(), clientPort, strerror(errno));break;}}// 只要走到这里一定是client退出了服务到此结束close(sock); // 如果一个进程对应的文件fd打开了没有被归还则文件描述符泄露logMessage(DEBUG, server close %d done, sock);}private:int listensock_; // 监听套接字socketuint16_t port_; // portstring ip_; // ip }; 服务端main函数命令行参数 将来我们的服务端在启动的时候在命令行中一定是按照如下格式输入的 ./ServerTcp local_port local_ip 我们需要给main函数加上命令行参数内部代码逻辑如下 利用命令行参数的形式若main函数中argc ! 2 argc ! 3则复用提示信息函数Usage并exit退出进程定义port端口为命令行的第二个参数下标为1的参数若argc 3则定义ip地址为命令行的第三个参数下标为2的参数将端口号和ip地址传入ServerTcp服务器的类里调用init和start函数static void Usage(string proc) {cerr Usage:\n\t proc port ip endl;cerr Example:\n\t proc 8080 127.0.0.1\n endl; } // ./ServerTcp local_port local_ip int main(int argc, char *argv[]) {if (argc ! 2 argc ! 3){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port atoi(argv[1]);string ip;if (argc 3)ip argv[2];ServerTcp svr(port, ip);svr.init();svr.loop();return 0; } 服务端serverTcp.cc总代码 ServerTcp类的成员变量如下 listensock_port_ip_ServerTcp类的成员函数如下 ServerTcp构造函数ServerTcp析构函数init初始化函数loop启动服务器函数总代码如下 #include utli.hppclass ServerTcp { public:ServerTcp(uint16_t port, const string ip ): port_(port), ip_(ip), listensock_(-1){}~ServerTcp(){}public:// 初始化void init(){// 1、创建socketlistensock_ socket(AF_INET, SOCK_STREAM, 0);if (listensock_ 0){logMessage(FATAL, socket: %s, strerror(errno)); // 创建失败打印日志exit(SOCKET_ERR);}logMessage(DEBUG, socket: %s, %d, strerror(errno), listensock_);// 2、bind绑定// 2.1、填充服务器信息struct sockaddr_in local; // 用户栈memset(local, 0, sizeof(local));local.sin_family PF_INET;local.sin_port htons(port_);ip_.empty() ? (local.sin_addr.s_addr INADDR_ANY) : (inet_aton(ip_.c_str(), local.sin_addr));// 2.2、将本地socket信息写入listensock_对应的内核区域if (bind(listensock_, (const struct sockaddr *)local, sizeof(local)) -1){logMessage(FATAL, bind: %s, strerror(errno)); // 绑定失败打印日志exit(BIND_ERR);}logMessage(DEBUG, bind: %s, %d, strerror(errno), listensock_);// 3、监听socketif (listen(listensock_, 5) 0){logMessage(FATAL, listen: %s, strerror(errno)); // 监听失败打印日志exit(LISTEN_ERR);}logMessage(DEBUG, listen: %s, %d, strerror(errno), listensock_);// 允许别人连接你了}// 启动服务端void loop(){while (true){// 4、获取连接struct sockaddr_in peer;socklen_t len sizeof(peer);int serviceSock accept(listensock_, (struct sockaddr *)peer, len);if (serviceSock 0){logMessage(WARINING, accept: %s[%d], strerror(errno), serviceSock); // 获取连接失败continue;}// 4.1、获取客户端基本信息uint16_t peerPort ntohs(peer.sin_port);string peerIp inet_ntoa(peer.sin_addr);logMessage(DEBUG, accept: %s | %s:[%d], socket fd: %d, strerror(errno), peerIp.c_str(), peerPort, serviceSock);// 5、提供服务echo ( 小写 - 大写 )// 5.0 v0版本 —— 单进程transService(serviceSock, peerIp, peerPort);}}// 大小写转化服务// TCP UDP: 支持全双工void transService(int sock, const string clientIp, uint16_t clientPort){assert(socket 0);assert(!clientIp.empty());assert(clientPort 1024);char inbuffer[BUFFER_SIZE];while (true){ssize_t s read(sock, inbuffer, sizeof(inbuffer) - 1); // 我们认为读取到的都是字符串if (s 0) // 读取成功{inbuffer[s] \0; // read successif (strcasecmp(inbuffer, quit) 0) // strcasecmp是忽略大小写比较的函数{// 客户端输入退出logMessage(DEBUG, client quit -- %s[%d], clientIp.c_str(), clientPort);break;}logMessage(DEBUG, trans before: %s[%d] %s, clientIp.c_str(), clientPort, inbuffer);// 可以进行大小写转化了for (int i 0; i s; i){if (isalpha(inbuffer[i]) islower(inbuffer[i]))inbuffer[i] toupper(inbuffer[i]);}logMessage(DEBUG, trans after: %s[%d] %s, clientIp.c_str(), clientPort, inbuffer);// 进行写回操作write(sock, inbuffer, strlen(inbuffer));}else if (s 0) // 对方关闭{logMessage(DEBUG, client quit -- %s[%d], clientIp.c_str(), clientPort);break;}else // 读取出错{logMessage(DEBUG, %s[%d] - read: %s, clientIp.c_str(), clientPort, strerror(errno));break;}}// 只要走到这里一定是client退出了服务到此结束close(sock); // 如果一个进程对应的文件fd打开了没有被归还则文件描述符泄露logMessage(DEBUG, server close %d done, sock);}private:int listensock_; // 监听套接字socketuint16_t port_; // portstring ip_; // ip };static void Usage(string proc) {cerr Usage:\n\t proc port ip endl;cerr Example:\n\t proc 8080 127.0.0.1\n endl; } // ./ServerTcp local_port local_ip int main(int argc, char *argv[]) {if (argc ! 2 argc ! 3){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port atoi(argv[1]);string ip;if (argc 3)ip argv[2];ServerTcp svr(port, ip);svr.init();svr.loop();return 0; } 1.2、客户端clientTcp.cc文件 这里我们不像服务端udpServer.cc一样进行封装成类了。其内部主要框架逻辑如下 main函数采用命令行参数客户端创建套接字通讯过程启动客户端下面依次演示 客户端main函数命令行参数 客户端在启动的时候必须要知道服务端的ip和port才能进行连接服务端。未来的客户端程序一定是这样运行的 ./clientTcp serverIp serverPort 如果命令行参数个数argc ! 3复用Usage函数输出相关提示信息并退出程序定义string类型的serverIp变量保存命令行的第二个参数定义serverPort变量保存命令行中的第三个参数static void Usage(string proc) {cerr Usage:\n\t proc serverIp serverPort endl;cerr Example:\n\t proc 127.0.0.1 8080\n endl; } // ./clientTcp serverIp serverPort int main(int argc, char* argv[]) {if (argc ! 3){Usage(argv[0]);exit(USAGE_ERR);}string serverIp argv[1];uint16_t serverPort atoi(argv[2]);return 0; } 客户端创建套接字 客户端创建套接字时选择的协议家族也是AF_INET需要的服务类型也是SOCK_STREAM。与服务端不同的是客户端在初始化时只需要创建套接字就行了而不需要进行绑定操作。 int main() {...// 1、创建socketint sock socket(AF_INET, SOCK_STREAM, 0);if (sock 0){cerr socket: strerror(errno) endl;exit(SOCKET_ERR);}...close(sock);return 0; } 客户端的bind、listen、accept问题 客户端需不需要自己进行bind绑定呢 不需要。所谓的“不需要”指的是客户端不需要用户自己bind端口信息因为OS会自动给你绑定。这个问题和udp的一样客户端需不需要自己进行listen监听呢 不需要。监听本来就是等着别人来连你作为客户端你是要主动连接别人的而不是等着服务端自动向你连接的这属实反客为主了。而服务端需要进行监听是因为服务端需要通过监听来获取新连接但是不会有人主动连接客户端因此客户端是不需要进行监听操作的。客户端需不需要自己进行accept获取呢 不需要因为都没有listen都没有人来连你当然不用accpet客户端连接服务器 connect接口说明 客户端创建完套接字后需要向服务器发送链接请求。发起连接请求的函数叫做connect该函数的函数原型如下int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 参数说明 sockfd特定的套接字表示通过该套接字发起连接请求。addr对端网络相关的属性信息包括协议家族、IP地址、端口号等。addrlen传入的addr结构体的长度。返回值说明 连接或绑定成功返回0连接失败返回-1同时错误码会被设置。代码逻辑如下 定义struct sockaddr_in类型的结构体指针server复用memset函数对其清零填写服务器对应的信息将协议家族、端口号、IP地址等信息填充到该结构体变量当中。注意要复用htons主机转网络函数把端口号转成网络序列才能向外发送。注意要复用inet_aton函数将字符串IP转换成整数IP复用connect函数向服务器发送连接请求int main(int argc, char *argv[]) {// 1、创建socket// 2、connect, 向服务器发起连接请求// 2.1、先填充需要连接的远端主机的基本信息struct sockaddr_in server;memset(server, 0, sizeof(server));server.sin_family AF_INET;server.sin_port htons(serverPort);inet_aton(serverIp.c_str(), server.sin_addr);// 2.2、发起请求connect 回自动帮我们进行bindif (connect(sock, (const struct sockaddr *)server, sizeof(server)) ! 0){cerr connect: strerror(errno) endl;exit(CONN_ERR);}cout info: connect success: sock endl;return 0; } 客户端发起请求 由于我们实现的是一个简单的回声服务器因此当客户端连接到服务端后客户端就可以向服务端发送数据了这里我们可以让客户端将用户输入的数据发送给服务端发送时调用write函数向套接字当中写入数据即可。当客户端将数据发送给服务端后由于服务端读取到数据后还会进行回显因此客户端在发送数据后还需要调用read函数读取服务端的响应数据然后将该响应数据进行打印以确定双方通信无误。int main(int argc, char *argv[]) {// 1、创建socket// 2、connect, 向服务器发起连接请求// 2.1、先填充需要连接的远端主机的基本信息struct sockaddr_in server;memset(server, 0, sizeof(server));server.sin_family AF_INET;server.sin_port htons(serverPort);inet_aton(serverIp.c_str(), server.sin_addr);// 2.2、发起请求connect 回自动帮我们进行bindif (connect(sock, (const struct sockaddr *)server, sizeof(server)) ! 0){cerr connect: strerror(errno) endl;exit(CONN_ERR);}cout info: connect success: sock endl;string message;while (!quit){message.clear();cout 请输入你的消息 ;getline(cin, message);if (strcasecmp(message.c_str(), quit) 0)quit true;ssize_t s write(sock, message.c_str(), message.size());if (s 0){message.resize(1024);ssize_t s read(sock, (char*)(message.c_str()), 1024);if (s 0) message[s] 0;cout Server Echo message endl;}else if (s 0){break;}}return 0; } 客户端clinetTcp.cc总代码 clientTcp.cc文件的内部主要框架逻辑如下 main函数使用命令行参数客户端创建套接字连接过程总代码如下 #include utli.hppvolatile bool quit false;static void Usage(string proc) {cerr Usage:\n\t proc serverIp serverPort endl;cerr Example:\n\t proc 127.0.0.1 8080\n endl; } // ./clientTcp serverIp serverPort int main(int argc, char *argv[]) {if (argc ! 3){Usage(argv[0]);exit(USAGE_ERR);}string serverIp argv[1];uint16_t serverPort atoi(argv[2]);// 1、创建socketint sock socket(AF_INET, SOCK_STREAM, 0);if (sock 0){cerr socket: strerror(errno) endl;exit(SOCKET_ERR);}// 2、connect, 向服务器发起连接请求// 2.1、先填充需要连接的远端主机的基本信息struct sockaddr_in server;memset(server, 0, sizeof(server));server.sin_family AF_INET;server.sin_port htons(serverPort);inet_aton(serverIp.c_str(), server.sin_addr);// 2.2、发起请求connect 回自动帮我们进行bindif (connect(sock, (const struct sockaddr *)server, sizeof(server)) ! 0){cerr connect: strerror(errno) endl;exit(CONN_ERR);}cout info: connect success: sock endl;string message;while (!quit){message.clear();cout 请输入你的消息 ;getline(cin, message);if (strcasecmp(message.c_str(), quit) 0)quit true;ssize_t s write(sock, message.c_str(), message.size());if (s 0){message.resize(1024);ssize_t s read(sock, (char*)(message.c_str()), 1024);if (s 0) message[s] 0;cout Server Echo message endl;}else if (s 0){break;}}close(sock);return 0; } 1.3、服务器测试 现在服务端和客户端均已写好先运行服务端再运行客户端。我们使用如下指令辅助我们观察现象 [xzyecs-333953 tcp]$ sudo netstat -ntp | grep -E serverTcp|clientTcp 如上我服务器的端口是8081它已经和端口43914的客户端相互建立起了连接 现在就可以让客户端向服务端发送消息了当客户端向服务端发送消息后服务端可以通过打印的IP地址和端口号识别出对应的客户端而客户端也可以通过服务端响应回来的消息来判断服务端是否收到了自己发送的消息。 当我客户端发送quit退出动作时服务端识别后确认客户端退出并关闭对应的socket。如果我强制ctrl -c退出客户端OS会自动帮我们关掉对应的文件描述符此时服务端也就知道客户端退出了进而会终止对客户端的服务。 1.4、单执行流服务器的问题 当我们仅用一个客户端连接服务器时这一个客户端能够正常享受到服务端的服务 但当此客户端1正常享受服务端的服务时我们让另一个客户端2也连接此服务器 此时发现两个客户端都是可以正常连接的但是客户端2发给服务端的消息并没有在服务端进行打印服务端也没有将该数据回显给客户端2。相反我客户端1和服务端是能够正常通信的 但是当客户端1退出后服务端才将客户端2发来的数据进行打印并回显到客户端2上 单进程的服务器 通过实验现象可以看到这服务端只有服务完一个客户端后才会服务另一个客户端。因为我们目前所写的是一个单执行流版的服务器这个服务器一次只能为一个客户端提供服务一旦进入transService函数主执行流就无法进行向后执行只能提供完毕服务之后才能进行accept。当服务端调用accept函数获取到连接后就给该客户端提供服务但在服务端提供服务期间可能会有其他客户端发起连接请求但由于当前服务器是单执行流的只能服务完当前客户端后才能继续服务下一个客户端。 解决办法  单执行流的服务器一次只能给一个客户端提供服务此时服务器的资源并没有得到充分利用因此服务器一般是不会写成单执行流的。要解决这个问题就需要将服务器改为多执行流的此时就要引入多进程或多线程。2、多进程版的TCP网络程序 当服务端调用accept函数获取到新连接后不是由当前执行流为该连接提供服务而是当前执行流调用fork函数创建子进程然后让子进程为父进程获取到的连接提供服务。由于父子进程是两个不同的执行流当父进程调用fork创建出子进程后父进程就可以继续从监听套接字当中获取新连接而不用关心获取上来的连接是否服务完毕父进程创建的子进程会继承父进程的套接字文件此时子进程就能够对特定的套接字文件进行读写操作进而完成对对应客户端的服务。等待子进程问题 当父进程创建出子进程后父进程是需要等待子进程退出的否则子进程会变成僵尸进程进而造成内存泄漏。因此服务端创建子进程后需要调用wait或waitpid函数对子进程进行等待。 阻塞式等待与非阻塞式等待 如果服务端采用阻塞的方式等待子进程那么服务端还是需要等待服务完当前客户端才能继续获取下一个连接请求此时服务端仍然是以一种串行的方式为客户端提供服务。如果服务端采用非阻塞的方式等待子进程虽然在子进程为客户端提供服务期间服务端可以继续获取新连接但此时服务端就需要将所有子进程的PID保存下来并且需要不断花费时间检测子进程是否退出。总之服务端要等待子进程退出无论采用阻塞式等待还是非阻塞式等待都不尽人意。此时我们可以考虑让服务端不等待子进程退出。不等待子进程退出的方式如下 捕捉SIGCHLD信号将其处理动作设置为忽略。让父进程创建子进程子进程再创建孙子进程最后让孙子进程为客户端提供服务。捕捉SIGCHLD信号 实际当子进程退出时会给父进程发送SIGCHLD信号如果父进程将SIGCHLD信号进行捕捉并将该信号的处理动作设置为忽略此时父进程就只需专心处理自己的工作不必关心子进程了。 class ServerTcp { public:// 构造 析构 public:// 初始化// 启动服务端void loop(){signal(SIGCHLD, SIG_IGN); // 忽略SIGCHLD信号while (true){// 4、获取连接struct sockaddr_in peer;socklen_t len sizeof(peer);int serviceSock accept(listensock_, (struct sockaddr *)peer, len);if (serviceSock 0){logMessage(WARINING, accept: %s[%d], strerror(errno), serviceSock); // 获取连接失败continue;}// 4.1、获取客户端基本信息uint16_t peerPort ntohs(peer.sin_port);string peerIp inet_ntoa(peer.sin_addr);logMessage(DEBUG, accept: %s | %s:[%d], socket fd: %d, strerror(errno), peerIp.c_str(), peerPort, serviceSock);// 5、提供服务echo ( 小写 - 大写 )// 5.0 v0版本 —— 单进程// transService(serviceSock, peerIp, peerPort);// 5.1 v1版本 —— 多进程pid_t id fork();assert(id ! -1);if (id 0){close(listensock_); // 建议关掉// 子进程transService(serviceSock, peerIp, peerPort);exit(0); // 子进程退出进入僵尸}// 父进程close(serviceSock); // 一定要做}}private:int listensock_; // 监听套接字socketuint16_t port_; // portstring ip_; // ip }; 测试结果 我们使用如下的监控脚本辅助我们观察现象 [xzyecs-333953 tcp]$ ps -axj | head -1 ps axj | grep serverTcp 当我们让客户端1连接服务器后服务器进程会调用fork函数创建出一个子进程并提供服务当我们让客户端2连接服务器后服务器进程同样会调用fork函数创建出一个子进程并提供服务。所以我们会看到3个进程在运行的状态如下我们还应该看到客户端1和客户端2各自向服务端发送信息且都能正常收到服务端的回复。 现在我们让客户端一个一个退出并用如下的监控脚本观察进程数量的变化 [xzyecs-333953 tcp]$ while :; do ps -axj | head -1 ps axj | grep serverTcp ; sleep 1 ;done 当客户端一个一个推出后服务端为之提供的子进程也会相机退出单无论如何服务端都至少会有一个服务进程此进程的任务就是不断获取新连接。 让孙子进程提供服务 我们可以让服务端冲断爷爷进程服务端创建的子进程爸爸进程继续fork创建子进程孙子进程让孙子进程为客户端提供服务过程如下 爷爷进程在服务端调用accept函数获取客户端连接请求的进程。爸爸进程由爷爷进程调用fork函数创建出来的进程。孙子进程由爸爸进程调用fork函数创建出来的进程该进程调用Service函数为客户端提供服务。不需要等待孙子进程退出 我们让爸爸进程创建完孙子进程后立刻退出此时服务进程爷爷进程调用wait/waitpid函数等待爸爸进程就能立刻等待成功此后服务进程就能继续调用accept函数获取其他客户端的连接请求。而由于爸爸进程创建完孙子进程后就立刻退出了因此实际为客户端提供服务的孙子进程就变成了孤儿进程该进程就会被系统领养当孙子进程为客户端提供完服务退出后系统会回收孙子进程所以服务进程爷爷进程是不需要等待孙子进程退出的。关闭对应的文件描述符 对于服务进程来说当它调用fork函数后就必须将从accept函数获取的文件描述符关掉。因为服务进程会不断调用accept函数获取新的文件描述符服务套接字如果服务进程不及时关掉不用的文件描述符最终服务进程中可用的文件描述符就会越来越少。而对于爸爸进程和孙子进程来说还是建议关闭从服务进程继承下来的监听套接字。实际就算它们不关闭监听套接字最终也只会导致这一个文件描述符泄漏但一般还是建议关上。因为孙子进程在提供服务时可能会对监听套接字进行某种误操作此时就会对监听套接字当中的数据造成影响。代码如下 class ServerTcp { public:// 构造 析构 public:// 初始化// 启动服务端void loop(){signal(SIGCHLD, SIG_IGN); // 忽略SIGCHLD信号while (true){// 4、获取连接struct sockaddr_in peer;socklen_t len sizeof(peer);int serviceSock accept(listensock_, (struct sockaddr *)peer, len);if (serviceSock 0){logMessage(WARINING, accept: %s[%d], strerror(errno), serviceSock); // 获取连接失败continue;}// 4.1、获取客户端基本信息uint16_t peerPort ntohs(peer.sin_port);string peerIp inet_ntoa(peer.sin_addr);logMessage(DEBUG, accept: %s | %s:[%d], socket fd: %d, strerror(errno), peerIp.c_str(), peerPort, serviceSock);// 5、提供服务echo ( 小写 - 大写 )// 5.0 v0版本 —— 单进程// transService(serviceSock, peerIp, peerPort);// 5.1 v1版本 —— 多进程 ———— 捕捉SIGCHLD信号// 5.1 v1.1版本 —— 多进程 ———— 让孙子进程提供服务// 爷爷进程pid_t id fork();if (id 0){// 爸爸进程close(listensock_); // 建议关掉if (fork() 0) // 又进行了一次fork让爸爸进程直接终止exit(0);// 孙子进程 ———— 没有爸爸 ———— 孤儿进程 ———— 被系统领养 ———— 回收问题就交给了系统来回收transService(serviceSock, peerIp, peerPort);exit(0);}close(serviceSock); // 一定要做// 爸爸进程直接终止立马得到退出码释放僵尸状态pid_t ret waitpid(id, nullptr, 0); // 就用阻塞式assert(ret 0);(void)ret;}}private:int listensock_; // 监听套接字socketuint16_t port_; // portstring ip_; // ip }; 测试结果 3、多线程版的TCP网络程序 创建进程的成本是很高的创建进程时需要创建该进程对应的进程控制块task_struct、进程地址空间mm_struct、页表等数据结构。而创建线程的成本比创建进程的成本会小得多因为线程本质是在进程地址空间内运行创建出来的线程会共享该进程的大部分资源因此在实现多执行流的服务器时最好采用多线程进行实现。当服务进程调用accept函数获取到一个新连接后就可以直接创建一个线程让该线程为对应客户端提供服务。 当然主线程服务进程创建出新线程后也是需要等待新线程退出的否则也会造成类似于僵尸进程这样的问题。但对于线程来说如果不想让主线程等待新线程退出可以让创建出来的新线程调用pthread_detach函数进行线程分离当这个线程退出时系统会自动回收该线程所对应的资源。此时主线程服务进程就可以继续调用accept函数获取新连接而让新线程去服务对应的客户端。文件描述符关闭的问题 由于此时所有线程看到的都是同一张文件描述符表因此当某个线程要对这张文件描述符表做某种操作时不仅要考虑当前线程还要考虑其他线程。 对于主线程accept上来的文件描述符主线程不能对其进行关闭操作该文件描述符的关闭操作应该又新线程来执行。因为是新线程为客户端提供服务的只有当新线程为客户端提供的服务结束后才能将该文件描述符关闭。对于监听套接字虽然创建出来的新线程不必关心监听套接字但新线程不能将监听套接字对应的文件描述符关闭否则主线程就无法从监听套接字当中获取新连接了。代码逻辑如下 我们使用pthread_create创建线程让新线程内部执行为客户端提供服务transService的操作。所以我们需要在线程执行函数threadRoutine里传入客户端ipportsock。为了能够让线程执行函数threadRoutine获得ipportsock这三个参数我们在pthread_create的最后一个参数传入一个ThreadData结构体该结构体内部包含了这三个参数注意我线程函数是在ServerTcp类内部的成员函数而成员函数有默认的this指针为了避免pthread_create传参出错我们需要给线程执行函数threadRoutine设置为static静态成员函数。一旦设置了静态函数也就意味着此线程执行函数内部无法直接访问ServerTcp类的提供服务transService函数。为了避免这一现象的产生我们对ThreadData结构体内部多定义一个this_变量将来在ServerTcp类创建ThreadData结构体指针时给最后一个参数传入ServerTcp类的this指针。这样我将来在线程执行函数内部就可以通过此ThreadData结构体指针访问this_成员变量再通过this_成员变量访问ServerTcp类的成员函数transService。即可完成线程为对应客户端提供服务。class ServerTcp; // 声明一下 class ThreadData { public:ThreadData(uint16_t port, string ip, int sock, ServerTcp *ts): clientport_(port), clientip_(ip), sock_(sock), this_(ts){}public:uint16_t clientport_;string clientip_;int sock_;ServerTcp *this_; };class ServerTcp { public:// 构造 析构public:// 初始化// 线程执行函数static void *threadRoutine(void *args){pthread_detach(pthread_self()); // 设置线程分离ThreadData *td static_castThreadData *(args);td-this_-transService(td-sock_, td-clientip_, td-clientport_);delete td;return nullptr;}// 启动服务端void loop(){signal(SIGCHLD, SIG_IGN); // 忽略SIGCHLD信号while (true){// 4、获取连接struct sockaddr_in peer;socklen_t len sizeof(peer);int serviceSock accept(listensock_, (struct sockaddr *)peer, len);if (serviceSock 0){logMessage(WARINING, accept: %s[%d], strerror(errno), serviceSock); // 获取连接失败continue;}// 4.1、获取客户端基本信息uint16_t peerPort ntohs(peer.sin_port);string peerIp inet_ntoa(peer.sin_addr);logMessage(DEBUG, accept: %s | %s:[%d], socket fd: %d, strerror(errno), peerIp.c_str(), peerPort, serviceSock);// 5、提供服务echo ( 小写 - 大写 )// 5.0 v0版本 —— 单进程// 5.1 v1版本 —— 多进程// 5.1 v1.1版本 —— 多进程// 5.2 v2版本 —— 多线程// 这里不需要古纳比文件描述符因为多线程是会共享文件描述符表的ThreadData *td new ThreadData(peerPort, peerIp, serviceSock, this);pthread_t tid;pthread_create(tid, nullptr, threadRoutine, (void *)td);}}private:int listensock_; // 监听套接字socketuint16_t port_; // portstring ip_; // ip }; 测试结果 我们使用如下监控脚本辅助我们观察现象 [xzyecs-333953 tcp]$ while :; do ps -aL | head -1 ps -aL | grep serverTcp ; sleep 1 ;done 上述四个客户端提供服务的也是两个不同的执行流因此这四个客户端可以同时享受服务端提供的服务它们发送给服务端的消息也都能够在服务端进行打印并且这四个客户端也都能够收到服务端的回显数据。 当客户端一个个退出后为其提供服务的新线程也就会相继退出最终就只剩下最初的主线程仍在等待新连接的到来。4、线程池版的TCP网络程序 当前多线程版本存在的问题 每当有新连接到来时服务端的主线程都会重新为该客户端创建为其提供服务的新线程而当服务结束后又会将该新线程销毁。这样做不仅麻烦而且效率低下每当连接到来的时候服务端才创建对应提供服务的线程。如果有大量的客户端连接请求此时服务端要为每一个客户端创建对应的服务线程。计算机当中的线程越多CPU的压力就越大因为CPU要不断在这些线程之间来回切换此时CPU在调度线程的时候线程和线程之间切换的成本就会变得很高。此外一旦线程太多每一个线程再次被调度的周期就变长了而线程是为客户端提供服务的线程被调度的周期变长客户端也迟迟得不到应答。解决办法 可以在服务端预先创建一批线程当有客户端请求连接时就让这些线程为客户端提供服务此时客户端一来就有线程为其提供服务而不是当客户端来了才创建对应的服务线程。当某个线程为客户端提供完服务后不要让该线程退出而是让该线程继续为下一个客户端提供服务如果当前没有客户端连接请求则可以让该线程先进入休眠状态当有客户端连接到来时再将该线程唤醒。服务端创建的这一批线程的数量不能太多此时CPU的压力也就不会太大。此外如果有客户端连接到来但此时这一批线程都在给其他客户端提供服务这时服务端不应该再创建线程而应该让这个新来的连接请求在全连接队列进行排队等服务端这一批线程中有空闲线程后再将该连接请求获取上来并为其提供服务。线程池 我们需要在服务端引入线程池因为线程池的存在就是为了避免处理短时间任务时创建与销毁线程的代价此外线程池还能够保证内核充分利用防止过分调度。其中在线程池里面有一个任务队列当有新的任务到来的时候就可以将任务Push到线程池当中在线程池当中我们默认创建了5个线程这些线程不断检测任务队列当中是否有任务如果有任务就拿出任务然后调用该任务对应的Run函数对该任务进行处理如果线程池当中没有任务那么当前线程就会进入休眠状态。我先前已经介绍并实现了线程池这里就直接将线程池的代码接入到当前的TCP服务器因此下面只会讲解线程池接入的方法如果对线程池的实现有疑问的可以去阅读那篇博客。 我们从先前写的线程池取出我们需要的内容放到此tcp目录下 代码逻辑如下 现在服务端引入了线程池因此在服务类当中需要新增一个指向线程池的指针成员 当实例化服务器对象时先将这个线程池指针先初始化为空。当服务器初始化完毕后再实际构造这个线程池对象在构造线程池对象时可以指定线程池当中线程的个数也可以不指定此时默认线程的个数为5。在启动服务器之前对线程池进行初始化此时就会将线程池当中的若干线程创建出来而这些线程创建出来后就会不断检测任务队列从任务队列当中拿出任务进行处理。现在当服务进程调用accept函数获取到一个连接请求后就会根据该客户端的套接字、IP地址以及端口号构建出一个任务然后调用线程池提供的Push接口将该任务塞入任务队列。 class ServerTcp { public:ServerTcp(uint16_t port, const string ip ): port_(port), ip_(ip), listensock_(-1), tp_(nullptr){}~ServerTcp(){}public:// 初始化void init(){// 1、创建socketlistensock_ socket(AF_INET, SOCK_STREAM, 0);if (listensock_ 0){logMessage(FATAL, socket: %s, strerror(errno)); // 创建失败打印日志exit(SOCKET_ERR);}logMessage(DEBUG, socket: %s, %d, strerror(errno), listensock_);// 2、bind绑定// 2.1、填充服务器信息struct sockaddr_in local; // 用户栈memset(local, 0, sizeof(local));local.sin_family PF_INET;local.sin_port htons(port_);ip_.empty() ? (local.sin_addr.s_addr INADDR_ANY) : (inet_aton(ip_.c_str(), local.sin_addr));// 2.2、将本地socket信息写入listensock_对应的内核区域if (bind(listensock_, (const struct sockaddr *)local, sizeof(local)) -1){logMessage(FATAL, bind: %s, strerror(errno)); // 绑定失败打印日志exit(BIND_ERR);}logMessage(DEBUG, bind: %s, %d, strerror(errno), listensock_);// 3、监听socketif (listen(listensock_, 5) 0){logMessage(FATAL, listen: %s, strerror(errno)); // 监听失败打印日志exit(LISTEN_ERR);}logMessage(DEBUG, listen: %s, %d, strerror(errno), listensock_);// 允许别人连接你了// 4、加载线程池tp_ ThreadPoolTask::getInstance();}// 启动服务端void loop(){// 启动线程池tp_-start();logMessage(DEBUG, thread pool start success, thread num: %d, tp_-threadNum());signal(SIGCHLD, SIG_IGN); // 忽略SIGCHLD信号while (true){// 4、获取连接struct sockaddr_in peer;socklen_t len sizeof(peer);int serviceSock accept(listensock_, (struct sockaddr *)peer, len);if (serviceSock 0){logMessage(WARINING, accept: %s[%d], strerror(errno), serviceSock); // 获取连接失败continue;}// 4.1、获取客户端基本信息uint16_t peerPort ntohs(peer.sin_port);string peerIp inet_ntoa(peer.sin_addr);logMessage(DEBUG, accept: %s | %s:[%d], socket fd: %d, strerror(errno), peerIp.c_str(), peerPort, serviceSock);// 5、提供服务echo ( 小写 - 大写 )// 5.0 v0版本 —— 单进程// 5.1 v1版本 —— 多进程// 5.1 v1.1版本 —— 多进程 // 5.2 v2版本 —— 多线程// 5.3 v3版本 —— 线程池// 5.3.1 构建任务// 5.3 v3.1// Task t(serviceSock, peerIp, peerPort, std::bind(ServerTcp::transService, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));// tp_-push(t);// 5.3 v3.2Task t(serviceSock, peerIp, peerPort, transService);tp_-push(t);}} private:int listensock_; // 监听套接字socketuint16_t port_; // portstring ip_; // ip// 引入线程池ThreadPoolTask *tp_; }; 设计任务类 该任务类当中需要包含客户端对应的套接字、IP地址、端口号表示该任务是为哪一个客户端提供服务对应操作的套接字是哪一个。#pragma once #include iostream #include string #include functional #include pthread.h #include log.hppclass Task { public:using callBack_t std::functionvoid(int, std::string, uint16_t);// 等价于typedef std::functionvoid (int, std::string, uint16_t) callBack_t; private:int sock_; // 给用户提供IO服务的sockuint16_t port_; // client portstd::string ip_; // client ipcallBack_t func_; // 回调方法 public:Task(): sock_(-1), port_(-1){}Task(int sock, std::string ip, uint16_t port, callBack_t func): sock_(sock), ip_(ip), port_(port), func_(func){}void operator()(){logMessage(DEBUG, 线程ID[%p]处理%s:%d的请求 开始啦..., pthread_self(), ip_.c_str(), port_);func_(sock_, ip_, port_);logMessage(DEBUG, 线程ID[%p]处理%s:%d的请求 结束啦..., pthread_self(), ip_.c_str(), port_);}~Task(){} }; 代码测试 我们使用如下监控脚本辅助我们观察现象 [xzyecs-333953 tcp]$ while :; do ps -aL | head -1 ps -aL | grep serverTcp ; sleep 1 ;done 当客户端连接服务器后服务端的主线程就会获取该客户端的连接请求并将其封装为一个任务对象后塞入任务队列此时线程池中的5个线程就会有一个线程从任务队列当中获取到该任务并执行该任务的处理函数为客户端提供服务。当第二个客户端发起连接请求时服务端也会将其封装为一个任务类塞到任务队列然后线程池当中的线程再从任务队列当中获取到该任务进行处理此时也是不同的执行流为这两个客户端提供的服务因此这两个客户端也是能够同时享受服务的。与之前不同的是无论现在有多少客户端发来请求在服务端都只会有线程池当中的5个线程为之提供服务线程池当中的线程个数不会随着客户端连接的增多而增多这些线程也不会因为客户端的退出而退出。线程池变形 注意 我们设置了对应的任务是死循环那么线程池提供服务就显得不太合适。一般我们给线程池抛入的任务都是短任务现在对代码进行修改。我们更新线程池的容量为15个。先来看如下这个函数popen #include stdio.h FILE *popen(const char *command, const char *type); int pclose(FILE *stream); popen函数介绍 作用创建一个连接到另一个进程的管道然后读其输出或向其输入端发送数据。原理创建一个管道fork一个子进程关闭未使用的管道端读端或者写端执行一个shell运行命令然后等待命令终止。参数说明 commmand是一个指向以 NULL 结束的 shell 命令字符串的指针。这行命令将被传到 bin/sh 并使用 -c 标志shell 将执行这个命令。type只能是读和写的一种如果是 “r” 则文件指针连接到command的标准输出则返回的文件指针是可读的如果是 “w” 则文件指针连接到command的标准输入则返回的文件指针是可写的。streampopen返回的文件指针。代码思想 我们下面要进行的操作就是让线程服务客户端更换一个服务。先前的服务是进行大小写转化transService现在来更换一个execCommand。我们只需要改变提供服务的接口即可代码主逻辑不用动完成了代码解耦。代码逻辑 定义command数组复用read函数把客户端读到的数据输入到此command数组里将command当成字符串复用popen函数以只读的方式将数据输出到文件指针fp中。复用fgets函数将fp文件的内容读取到定义的line数组里并复用write函数将line数组里的内容全部写回到sock里void execCommand(int sock, const string clientIp, uint16_t clientPort) {assert(socket 0);assert(!clientIp.empty());assert(clientPort 1024);char command[BUFFER_SIZE];while (true){ssize_t s read(sock, command, sizeof(command) - 1); // 我们认为读取到的都是字符串if (s 0) // 读取成功{command[s] \0; // 当成字符串logMessage(DEBUG, [%s:%d] exec [%s], clientIp.c_str(), clientPort, command);// 考虑安全string safe command;if (string::npos ! (safe.find(rm)) || string::npos ! safe.find(unlink)){break;}FILE *fp popen(command, r);if (fp nullptr){logMessage(WARINING, exec %s failed, because: %s, command, strerror(errno));break;}char line[1024];while (fgets(line, sizeof(line) - 1, fp) ! nullptr){write(sock, line, strlen(line));}// dup2(sock, fp-_fileno); // 把本来应该显示到fp文件的重定向到网络sock里// fflush(fp); // 把数据刷到对端pclose(fp);logMessage(DEBUG, [%s:%d] exec [%s] ... done, clientIp.c_str(), clientPort, command);}else if (s 0) // 对方关闭{logMessage(DEBUG, client quit -- %s[%d], clientIp.c_str(), clientPort);break;}else // 读取出错{logMessage(DEBUG, %s[%d] - read: %s, clientIp.c_str(), clientPort, strerror(errno));break;}}// 只要走到这里一定是client退出了服务到此结束close(sock); // 如果一个进程对应的文件fd打开了没有被归还则文件描述符泄露logMessage(DEBUG, server close %d done, sock); }class ServerTcp { public:// 构造 析构 public:// 初始化void init(){}// 启动服务端void loop(){// 启动线程池tp_-start();logMessage(DEBUG, thread pool start success, thread num: %d, tp_-threadNum());signal(SIGCHLD, SIG_IGN); // 忽略SIGCHLD信号while (true){// 4、获取连接struct sockaddr_in peer;socklen_t len sizeof(peer);int serviceSock accept(listensock_, (struct sockaddr *)peer, len);if (serviceSock 0){logMessage(WARINING, accept: %s[%d], strerror(errno), serviceSock); // 获取连接失败continue;}// 4.1、获取客户端基本信息uint16_t peerPort ntohs(peer.sin_port);string peerIp inet_ntoa(peer.sin_addr);logMessage(DEBUG, accept: %s | %s:[%d], socket fd: %d, strerror(errno), peerIp.c_str(), peerPort, serviceSock);// 5、提供服务echo ( 小写 - 大写 )// 5.0 v0版本 —— 单进程// transService(serviceSock, peerIp, peerPort);// 5.1 v1版本 —— 多进程 // 5.1 v1.1版本 —— 多进程// 5.2 v2版本 —— 多线程// 5.3 v3版本 —— 线程池// 5.3.1 构建任务// 5.3 v3.1// 5.3 v3.2// Task t(serviceSock, peerIp, peerPort, transService);// tp_-push(t);// 5.3 v3.3Task t(serviceSock, peerIp, peerPort, execCommand);tp_-push(t);}}private:int listensock_; // 监听套接字socketuint16_t port_; // portstring ip_; // ip// 引入线程池ThreadPoolTask *tp_; }; 测试结果 我们看到的现象是当客户端连接服务器后输入lspwd等指令时服务端提供服务将结果写回到客户端 5、总代码gitee链接 本篇博文所有设计的代码链接如下 gitee传送门TCP套接字源码
文章转载自:
http://www.morning.ndzhl.cn.gov.cn.ndzhl.cn
http://www.morning.wsjnr.cn.gov.cn.wsjnr.cn
http://www.morning.xfmwk.cn.gov.cn.xfmwk.cn
http://www.morning.hrjrt.cn.gov.cn.hrjrt.cn
http://www.morning.jpfpc.cn.gov.cn.jpfpc.cn
http://www.morning.ypzr.cn.gov.cn.ypzr.cn
http://www.morning.nkjxn.cn.gov.cn.nkjxn.cn
http://www.morning.jyzqn.cn.gov.cn.jyzqn.cn
http://www.morning.snkry.cn.gov.cn.snkry.cn
http://www.morning.bpwfr.cn.gov.cn.bpwfr.cn
http://www.morning.prprj.cn.gov.cn.prprj.cn
http://www.morning.bfrff.cn.gov.cn.bfrff.cn
http://www.morning.gbybx.cn.gov.cn.gbybx.cn
http://www.morning.c7501.cn.gov.cn.c7501.cn
http://www.morning.qjrjs.cn.gov.cn.qjrjs.cn
http://www.morning.nkjjp.cn.gov.cn.nkjjp.cn
http://www.morning.ykmg.cn.gov.cn.ykmg.cn
http://www.morning.bljcb.cn.gov.cn.bljcb.cn
http://www.morning.leyuhh.com.gov.cn.leyuhh.com
http://www.morning.qscsy.cn.gov.cn.qscsy.cn
http://www.morning.xwgbr.cn.gov.cn.xwgbr.cn
http://www.morning.lstmq.cn.gov.cn.lstmq.cn
http://www.morning.lrjtx.cn.gov.cn.lrjtx.cn
http://www.morning.xfxqj.cn.gov.cn.xfxqj.cn
http://www.morning.dnhdp.cn.gov.cn.dnhdp.cn
http://www.morning.ntkpc.cn.gov.cn.ntkpc.cn
http://www.morning.gyylt.cn.gov.cn.gyylt.cn
http://www.morning.nlqmp.cn.gov.cn.nlqmp.cn
http://www.morning.nytqy.cn.gov.cn.nytqy.cn
http://www.morning.gwjsm.cn.gov.cn.gwjsm.cn
http://www.morning.hmtft.cn.gov.cn.hmtft.cn
http://www.morning.dcccl.cn.gov.cn.dcccl.cn
http://www.morning.gsjfn.cn.gov.cn.gsjfn.cn
http://www.morning.kyctc.cn.gov.cn.kyctc.cn
http://www.morning.gzgwn.cn.gov.cn.gzgwn.cn
http://www.morning.ghslr.cn.gov.cn.ghslr.cn
http://www.morning.npbgj.cn.gov.cn.npbgj.cn
http://www.morning.mbmh.cn.gov.cn.mbmh.cn
http://www.morning.myfwb.cn.gov.cn.myfwb.cn
http://www.morning.pxbrg.cn.gov.cn.pxbrg.cn
http://www.morning.kcdts.cn.gov.cn.kcdts.cn
http://www.morning.zlcsz.cn.gov.cn.zlcsz.cn
http://www.morning.zmlbq.cn.gov.cn.zmlbq.cn
http://www.morning.tsdqr.cn.gov.cn.tsdqr.cn
http://www.morning.wdxr.cn.gov.cn.wdxr.cn
http://www.morning.njdtq.cn.gov.cn.njdtq.cn
http://www.morning.qrksj.cn.gov.cn.qrksj.cn
http://www.morning.yhdqq.cn.gov.cn.yhdqq.cn
http://www.morning.bpwdc.cn.gov.cn.bpwdc.cn
http://www.morning.hlhqs.cn.gov.cn.hlhqs.cn
http://www.morning.pwmm.cn.gov.cn.pwmm.cn
http://www.morning.srgsb.cn.gov.cn.srgsb.cn
http://www.morning.gqtw.cn.gov.cn.gqtw.cn
http://www.morning.zmlbq.cn.gov.cn.zmlbq.cn
http://www.morning.sryyt.cn.gov.cn.sryyt.cn
http://www.morning.dfdhx.cn.gov.cn.dfdhx.cn
http://www.morning.phjny.cn.gov.cn.phjny.cn
http://www.morning.mrfnj.cn.gov.cn.mrfnj.cn
http://www.morning.rkmsm.cn.gov.cn.rkmsm.cn
http://www.morning.bhwll.cn.gov.cn.bhwll.cn
http://www.morning.lngyd.cn.gov.cn.lngyd.cn
http://www.morning.mcndn.cn.gov.cn.mcndn.cn
http://www.morning.gnyhc.cn.gov.cn.gnyhc.cn
http://www.morning.kdlzz.cn.gov.cn.kdlzz.cn
http://www.morning.lbgsh.cn.gov.cn.lbgsh.cn
http://www.morning.lpqgq.cn.gov.cn.lpqgq.cn
http://www.morning.rlxg.cn.gov.cn.rlxg.cn
http://www.morning.mtsck.cn.gov.cn.mtsck.cn
http://www.morning.jcyyh.cn.gov.cn.jcyyh.cn
http://www.morning.dhqyh.cn.gov.cn.dhqyh.cn
http://www.morning.yjxfj.cn.gov.cn.yjxfj.cn
http://www.morning.jqtb.cn.gov.cn.jqtb.cn
http://www.morning.gpnfg.cn.gov.cn.gpnfg.cn
http://www.morning.nba1on1.com.gov.cn.nba1on1.com
http://www.morning.bxhch.cn.gov.cn.bxhch.cn
http://www.morning.kfjnx.cn.gov.cn.kfjnx.cn
http://www.morning.zzgkk.cn.gov.cn.zzgkk.cn
http://www.morning.brjq.cn.gov.cn.brjq.cn
http://www.morning.lqchz.cn.gov.cn.lqchz.cn
http://www.morning.hwsgk.cn.gov.cn.hwsgk.cn
http://www.tj-hxxt.cn/news/267863.html

相关文章:

  • php网站建设难点wordpress 压力
  • 一个企业建设网站的目的链接购买
  • 台州企业网站建设三亚旅游攻略
  • 汶上公司网站建设百家号seo
  • 西安专业网站开发哪家好酷网站欣赏
  • 网站视频做参考文献腾讯企业邮箱二维码登录
  • 邢台做网站流程网站开发房源岗位
  • 专门做酒店网站怎样制作微信小程序卖东西
  • 深圳在建高铁站备案网站管理系统
  • 公司网站服务器租赁微信公众号小说网站怎么做
  • 四川网站推广广东省建设执业注册中心网站
  • 网站建设基本流程ppt做彩票网站抓到判几年
  • 网站的建设目标是什么意思wordpress怎么开启伪静态
  • 10m网站空间网站建设 技术方案
  • 做网站用的系统锡林郭勒盟建设工程管理网站
  • 全国住房与城乡建设部网站wordpress 301设置
  • 中英语网站制作方法泉州网站制作设计
  • 哈尔滨队网站网页美工要建一个网站怎么做
  • 佛山网站建设锐艺a068优酷有wordpress插件吗
  • 个人网站如何进行网络推广中国做网站找谁
  • 做网站需要什么权限今天建设银行网站无法登录
  • 建一个网站买完域名后应该怎么做住房和城乡建设部网站执业资格注册中心
  • 物流管理网站建设深圳欧啦啦网站建设
  • 苏州工业园区最新通告优化员工
  • 品牌网站建设四川定制产品网站有哪些
  • 可视化网站制作软件还有哪些数据库网站
  • rp做网站wordpress域名授权破解版
  • 做网站需要执照吗西安室内设计公司排名
  • 网站整体设计硬件开发和软件开发
  • 做网站的资源哪里找吕梁网站定制