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

C/C++ Linux网络编程4 - 解决TCP服务器并发的方式

上篇文章:C/C++ Linux网络编程3 - Socket编程与TCP服务器客户端-CSDN博客

代码仓库:橘子真甜 (yzc-YZC) - Gitee.com

        上篇文章中,我们实现了一个简单的 TCP server - client通信。最后测试的时候发现如果有多个客户端同时连接服务器,不能并发发送数据。如何解决这个问题?

一. 多进程并发服务器

1.1 创建子进程和孙子进程

        最简单的方式当然是为每一个客户端创建一个进程每一个进程管理服务器与客户端之间的连接和通信。

        使用多进程就需要如何创建新进程?通信结束后如何回收进程的资源?使用fork就能创建子进程。

        但是,创建子进程必须要回收子进程,否则子进程会变为僵尸进程,占用系统资源。

        所以,简单的使用waitpid等待子进程吗?waitpid等待子进程有阻塞方式和非阻塞方式。使用阻塞方式,主进程不还是阻塞无法并发处理连接吗?使用非阻塞,无法调用waitpid等待子进程,子进程变为僵尸进程。

        解决方法:

        1 父进程阻塞等待子进程,子进程创建后创建孙子进程管理连接和数据收发,然后子进程直接退出,父进程回收子进程资源。(这种方式,对于孙子进程来说,他没有被回收但是其父进程被关闭了。孙子进程直接被进程1收养为孤儿进程)。孙子进程的资源由进程1进行管理。

代码如下:socket/bind/listen等函数直接看上篇文章即可,这里没有修改,不过多赘述。

  void run(){while (true){struct sockaddr_in clientaddr;memset(&clientaddr, 0, sizeof(clientaddr));socklen_t len = sizeof(clientaddr);int sockfd = accept(_listensock, (struct sockaddr *)&clientaddr, &len);if (sockfd < 0){std::cerr << "accept err" << std::endl;exit(-1);}std::cout << "accept success" << std::endl;// 创建子进程和孙子进程pid_t id = fork();if (id == 0){// 子进程创建孙子进程,然后之间退出if (fork() > 0)exit(-1);// 孙子进程执行相应任务//  通过clientaddr获取对方的ip/portstd::string clientip = inet_ntoa(clientaddr.sin_addr);uint16_t clientport = ntohs(clientaddr.sin_port);printf("获取连接成功,对方的ip/port为[%s][%d]", clientip.c_str(), clientport);// 执行响应的回调函数处理数据_callback(sockfd);// 关闭fdclose(sockfd);}// 父进程等待子进程资源pid_t ret = waitpid(id, nullptr, 0);if (ret > 0){printf("father wait success!\n");}}}

        然后我们简单修改服务端的服务代码方便我们测试。

代码如下:无脑发送一个打印 hello world的http响应

  void serviceIO(int sockfd){// 这里仅做简单的数据收发while (true){char buffer[128] = {0};int count = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (count < 0){std::cerr << "recv err" << std::endl;exit(-1);}if (count == 0){// 对方关闭break;}printf("client --> server:%s\n", buffer);std::string outbuffer;std::string body = "<h1>hello world</h1>";outbuffer ="HTTP/1.1 200 OK\r\n""Content-Type: text/html; charset=utf-8\r\n""Content-Length: " +std::to_string(body.size()) + "\r\n""Server: Apache/2.4.41\r\n""Date: Mon, 18 Dec 2023 08:32:10 GMT\r\n""X-Frame-Options: DENY\r\n""X-Content-Type-Options: nosniff\r\n""Referrer-Policy: strict-origin-when-cross-origin\r\n""\r\n" // 空行分隔头部和正文+ body;// 无脑向客户端发送一个简单http响应count = send(sockfd, outbuffer.c_str(), outbuffer.size(), 0);if (count < 0){std::cerr << "send err" << std::endl;exit(-1);}}close(sockfd);}

测试结果如下:这里借用网络调试工具进行测试:工具地址如下软件下载-野人家园-物联网技术专家平台 (cmsoft.cn)

        可以发现,能够支持两个客户端发送数据。再测试一下浏览器能否解析响应发现浏览器也是能够获取我们的响应的。

全部代码如下:

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>#include <functional>namespace YZC
{// 设置默认端口和最大backlogconst int defaultPort = 8080;const int maxBacklog = 128;// 设置回调函数using func_t = std::function<void(int)>;// typedef void (*func_t)(int);class tcpServer{public:tcpServer(func_t func, int port = defaultPort): _port(port), _callback(func) {}void init(){// 1.创建socket_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){std::cerr << "sockte err" << std::endl;exit(-1);}std::cout << "socket success" << std::endl;// 2 bind绑定fd和端口struct sockaddr_in serveraddr;memset(&serveraddr, 0, sizeof(serveraddr));// 设置地址的信息(协议,ip,端口)serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定任意网卡ip,通常我们访问某一个IP地址是这个服务器的公网网卡IP地址serveraddr.sin_port = htons(_port);             // 注意端口16位,2字节需要使用htons。不可socklen_t len = sizeof(serveraddr);if (bind(_listensock, (struct sockaddr *)&serveraddr, len) < 0){std::cerr << "bind err" << std::endl;exit(-1);}std::cout << "bind success" << std::endl;// 3 设置sockfd为监听fdif (listen(_listensock, maxBacklog) < 0){std::cerr << "listen err" << std::endl;exit(-1);}std::cout << "listen success" << std::endl;}void run(){while (true){struct sockaddr_in clientaddr;memset(&clientaddr, 0, sizeof(clientaddr));socklen_t len = sizeof(clientaddr);int sockfd = accept(_listensock, (struct sockaddr *)&clientaddr, &len);if (sockfd < 0){std::cerr << "accept err" << std::endl;exit(-1);}std::cout << "accept success" << std::endl;// 创建子进程和孙子进程pid_t id = fork();if (id == 0){// 子进程创建孙子进程,然后之间退出if (fork() > 0)exit(-1);// 孙子进程执行相应任务//  通过clientaddr获取对方的ip/portstd::string clientip = inet_ntoa(clientaddr.sin_addr);uint16_t clientport = ntohs(clientaddr.sin_port);printf("获取连接成功,对方的ip/port为[%s][%d]", clientip.c_str(), clientport);// 执行响应的回调函数处理数据_callback(sockfd);// 关闭fdclose(sockfd);}// 父进程等待子进程资源pid_t ret = waitpid(id, nullptr, 0);if (ret > 0){printf("father wait success!\n");}}}private:int _listensock;int _port;func_t _callback;};void serviceIO(int sockfd){// 这里仅做简单的数据收发while (true){char buffer[128] = {0};int count = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (count < 0){std::cerr << "recv err" << std::endl;exit(-1);}if (count == 0){// 对方关闭break;}printf("client --> server:%s\n", buffer);std::string outbuffer;std::string body = "<h1>hello world</h1>";outbuffer ="HTTP/1.1 200 OK\r\n""Content-Type: text/html; charset=utf-8\r\n""Content-Length: " +std::to_string(body.size()) + "\r\n""Server: Apache/2.4.41\r\n""Date: Mon, 18 Dec 2023 08:32:10 GMT\r\n""X-Frame-Options: DENY\r\n""X-Content-Type-Options: nosniff\r\n""Referrer-Policy: strict-origin-when-cross-origin\r\n""\r\n" // 空行分隔头部和正文+ body;// 无脑向客户端发送一个简单http响应count = send(sockfd, outbuffer.c_str(), outbuffer.size(), 0);if (count < 0){std::cerr << "send err" << std::endl;exit(-1);}}close(sockfd);}
}

1.2 设置信号SIG_IGN

        我们知道,子进程退出后会向父进程发送信号SIGCHLD来通知父进程关闭子进程并且回收子进程资源只要使用signal将SIGCHLD设置为SIGIGN让父进程不去关心子进程状态,让其自动回收回收子进程资源。

代码如下:使用signal需要包含头文件 <signal.h>

        void run(){signal(SIGCHLD, SIG_IGN);while (true){struct sockaddr_in clientaddr;memset(&clientaddr, 0, sizeof(clientaddr));socklen_t len = sizeof(clientaddr);int sockfd = accept(_listensock, (struct sockaddr *)&clientaddr, &len);if (sockfd < 0){std::cerr << "accept err" << std::endl;exit(-1);}std::cout << "accept success" << std::endl;pid_t id = fork();if (id == 0){std::string clientip = inet_ntoa(clientaddr.sin_addr);uint16_t clientport = ntohs(clientaddr.sin_port);printf("获取连接成功,对方的ip/port为[%s][%d]", clientip.c_str(), clientport);// 执行响应的回调函数处理数据_callback(sockfd);// 关闭fdclose(sockfd);exit(0);}}}

测试如下:

        这样也能保证多客户端的并发连接和通信。可运行代码如下:

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <signal.h>
#include <cstring>#include <functional>namespace YZC
{// 设置默认端口和最大backlogconst int defaultPort = 8080;const int maxBacklog = 128;// 设置回调函数using func_t = std::function<void(int)>;// typedef void (*func_t)(int);class tcpServer{public:tcpServer(func_t func, int port = defaultPort): _port(port), _callback(func) {}void init(){// 1.创建socket_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){std::cerr << "sockte err" << std::endl;exit(-1);}std::cout << "socket success" << std::endl;// 2 bind绑定fd和端口struct sockaddr_in serveraddr;memset(&serveraddr, 0, sizeof(serveraddr));// 设置地址的信息(协议,ip,端口)serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定任意网卡ip,通常我们访问某一个IP地址是这个服务器的公网网卡IP地址serveraddr.sin_port = htons(_port);             // 注意端口16位,2字节需要使用htons。不可socklen_t len = sizeof(serveraddr);if (bind(_listensock, (struct sockaddr *)&serveraddr, len) < 0){std::cerr << "bind err" << std::endl;exit(-1);}std::cout << "bind success" << std::endl;// 3 设置sockfd为监听fdif (listen(_listensock, maxBacklog) < 0){std::cerr << "listen err" << std::endl;exit(-1);}std::cout << "listen success" << std::endl;}void run(){signal(SIGCHLD, SIG_IGN);while (true){struct sockaddr_in clientaddr;memset(&clientaddr, 0, sizeof(clientaddr));socklen_t len = sizeof(clientaddr);int sockfd = accept(_listensock, (struct sockaddr *)&clientaddr, &len);if (sockfd < 0){std::cerr << "accept err" << std::endl;exit(-1);}std::cout << "accept success" << std::endl;// // 创建子进程和孙子进程// pid_t id = fork();// if (id == 0)// {//     // 子进程创建孙子进程,然后之间退出//     if (fork() > 0)//         exit(-1);//     // 孙子进程执行相应任务//     //  通过clientaddr获取对方的ip/port//     std::string clientip = inet_ntoa(clientaddr.sin_addr);//     uint16_t clientport = ntohs(clientaddr.sin_port);//     printf("获取连接成功,对方的ip/port为[%s][%d]", clientip.c_str(), clientport);//     // 执行响应的回调函数处理数据//     _callback(sockfd);//     // 关闭fd//     close(sockfd);// }// // 父进程等待子进程资源// pid_t ret = waitpid(id, nullptr, 0);// if (ret > 0)// {//     printf("father wait success!\n");// }pid_t id = fork();if (id == 0){std::string clientip = inet_ntoa(clientaddr.sin_addr);uint16_t clientport = ntohs(clientaddr.sin_port);printf("获取连接成功,对方的ip/port为[%s][%d]", clientip.c_str(), clientport);// 执行响应的回调函数处理数据_callback(sockfd);// 关闭fdclose(sockfd);exit(0);}}}private:int _listensock;int _port;func_t _callback;};void serviceIO(int sockfd){// 这里仅做简单的数据收发while (true){char buffer[128] = {0};int count = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (count < 0){std::cerr << "recv err" << std::endl;exit(-1);}if (count == 0){// 对方关闭break;}printf("client --> server:%s\n", buffer);std::string outbuffer;std::string body = "<h1>hello world</h1>";outbuffer ="HTTP/1.1 200 OK\r\n""Content-Type: text/html; charset=utf-8\r\n""Content-Length: " +std::to_string(body.size()) + "\r\n""Server: Apache/2.4.41\r\n""Date: Mon, 18 Dec 2023 08:32:10 GMT\r\n""X-Frame-Options: DENY\r\n""X-Content-Type-Options: nosniff\r\n""Referrer-Policy: strict-origin-when-cross-origin\r\n""\r\n" // 空行分隔头部和正文+ body;// 无脑向客户端发送一个简单http响应count = send(sockfd, outbuffer.c_str(), outbuffer.size(), 0);if (count < 0){std::cerr << "send err" << std::endl;exit(-1);}}close(sockfd);}
}

二. 多线程并发服务器

        为每一个连接都分配一个进程对于资源消耗的太大了,并且进程之间的切换需要消耗的资源也是非常大的(需要更换虚拟内存,页表和其映射关系,切换进程的上下文,也会导致catch更新导致命中率下降等)有没有更好的方式呢?

        线程是一个更好的方式,先的开辟和切换所消耗的资源远小于进程。再linux中可以使用pthread.h原生线程库来实现线程,本文使用c++11提供的thread来创造线程,更为方便。

代码如下:

 void run(){while (true){struct sockaddr_in clientaddr;memset(&clientaddr, 0, sizeof(clientaddr));socklen_t len = sizeof(clientaddr);int sockfd = accept(_listensock, (struct sockaddr *)&clientaddr, &len);if (sockfd < 0){std::cerr << "accept err" << std::endl;exit(-1);}std::cout << "accept success" << std::endl;std::thread t1(_callback, sockfd);t1.detach();}}

        可以看到,通过创建进程也能实现服务器的并发处理。

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>#include <functional>
#include <thread>namespace YZC
{// 设置默认端口和最大backlogconst int defaultPort = 8080;const int maxBacklog = 128;// 设置回调函数using func_t = std::function<void(int)>;// typedef void (*func_t)(int);class tcpServer{public:tcpServer(func_t func, int port = defaultPort): _port(port), _callback(func) {}void init(){// 1.创建socket_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){std::cerr << "sockte err" << std::endl;exit(-1);}std::cout << "socket success" << std::endl;// 2 bind绑定fd和端口struct sockaddr_in serveraddr;memset(&serveraddr, 0, sizeof(serveraddr));// 设置地址的信息(协议,ip,端口)serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定任意网卡ip,通常我们访问某一个IP地址是这个服务器的公网网卡IP地址serveraddr.sin_port = htons(_port);             // 注意端口16位,2字节需要使用htons。不可socklen_t len = sizeof(serveraddr);if (bind(_listensock, (struct sockaddr *)&serveraddr, len) < 0){std::cerr << "bind err" << std::endl;exit(-1);}std::cout << "bind success" << std::endl;// 3 设置sockfd为监听fdif (listen(_listensock, maxBacklog) < 0){std::cerr << "listen err" << std::endl;exit(-1);}std::cout << "listen success" << std::endl;}void run(){while (true){struct sockaddr_in clientaddr;memset(&clientaddr, 0, sizeof(clientaddr));socklen_t len = sizeof(clientaddr);int sockfd = accept(_listensock, (struct sockaddr *)&clientaddr, &len);if (sockfd < 0){std::cerr << "accept err" << std::endl;exit(-1);}std::cout << "accept success" << std::endl;std::thread t1(_callback, sockfd);t1.detach();}}private:int _listensock;int _port;func_t _callback;};void serviceIO(int sockfd){// 这里仅做简单的数据收发while (true){char buffer[128] = {0};int count = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (count < 0){std::cerr << "recv err" << std::endl;exit(-1);}if (count == 0){// 对方关闭break;}printf("client --> server:%s\n", buffer);std::string outbuffer;std::string body = "<h1>hello world</h1>";outbuffer ="HTTP/1.1 200 OK\r\n""Content-Type: text/html; charset=utf-8\r\n""Content-Length: " +std::to_string(body.size()) + "\r\n""Server: Apache/2.4.41\r\n""Date: Mon, 18 Dec 2023 08:32:10 GMT\r\n""X-Frame-Options: DENY\r\n""X-Content-Type-Options: nosniff\r\n""Referrer-Policy: strict-origin-when-cross-origin\r\n""\r\n" // 空行分隔头部和正文+ body;// 无脑向客户端发送一个简单http响应count = send(sockfd, outbuffer.c_str(), outbuffer.size(), 0);if (count < 0){std::cerr << "send err" << std::endl;exit(-1);}}close(sockfd);}}

        对于多进程来说,还可以使用线程池的方式来提高效率和稳定性。

三. 效率测试和总结

        使用wrk这个工具来测试多进程/多线程服务器的效率如何。最后获取的数据如下。

        服务器配置为 2G/2核cpu

并发数架构QPS总请求数平均延迟吞吐量错误数错误类型测试状态
1,000多进程7,28173,625100ms1.84MB/s482482超时✅ 正常
1,000多线程8,65087,421126ms2.19MB/s7373超时✅ 最佳
10,000多进程5,52255,745102ms1.40MB/s433123读+310超时✅ 正常
10,000多线程7,37574,453194ms1.86MB/s353107读+246超时✅ 最佳
25,000多进程1,04235,604420ms270KB/s10,97277读+8932写+1963超时⚠️ 高压稳定
25,000多线程31324,298205ms81KB/s953691读+262超时⚠️ 性能衰减
55,555多进程000us0B/s37,17037170写错误❌ 崩溃
55,555多线程N/AN/AN/AN/AN/A测试被终止❌ 崩溃

        可见无论是多进程还是多线程在连接大量增多的情况下都无法有效解决并发连接问题。为了解决这个问题,可以使用更有效的IO多路复用(select poll epoll)

http://www.dtcms.com/a/609147.html

相关文章:

  • AI取名大师 | uni-app + Wot UI 跟随设备自动切换明暗主题
  • 镜像站更新
  • 《uni-app跨平台开发完全指南》- 07 - 数据绑定与事件处理
  • 福州网站建设方案咨询免费观看电视剧软件
  • 虚拟机网站建设与管理wordpress前台修改密码
  • 福州绿光网站建设工作室合肥那个公司做网站优化好
  • Java基础——方法
  • 设计模式实战篇(二):业务逻辑“随时切换招式”——策略模式(Strategy Pattern)解析
  • 从“能说会道”到“自主思考”:一文读懂AI的过去、现在与未来
  • Python语言编译器 | Python语言编译器的使用与原理解析
  • 【JAVA 进阶】Spring Boot 注解体系与工程实践
  • Effective Python 第51条:优先考虑通过类修饰器来提供可组合的扩充功能,不要使用元类
  • Rust时序数据库实现:从压缩算法到并发优化的实战之旅
  • SpringCloud-Consul服务注册与发现
  • 网站建设原因分析wordpress 页面分页
  • SSH级知识管理:通过CPolar暴露Obsidian vault构建你的知识API服务,实现跨设备无缝同步
  • 《Linux系统编程之进程基础》【进程入门】
  • Hello-agents TASK03 第四章节 智能体经典范式构建
  • C++ 二叉搜索树(BST)完全指南:从概念原理、核心操作到底层实现
  • 电子电气架构 --- 哨兵模式初入门
  • 桌面开发,在线%考试管理%系统,基于eclipse,java,swing,mysql数据库。
  • 超融合架构的核心组件与协同机制深度解析
  • 桌面开发,在线%图书管理%系统,基于eclipse,jdk,java,swing,sqlserver数据库
  • 快速学会做网站网站建设公司怎么推广
  • 无需 iTunes 备份与恢复 iPhone 的 2 种方法
  • 【Linux】Ubuntu图形界面崩溃(无法进入)的解决方法汇总
  • Lidar调试记录Ⅳ之Ubuntu22.04+ROS2+Livox_SDK2环境下编译Livox ROS Driver 2
  • 网站收录查询网摘抄一则新闻
  • 做电影网站违法么深圳网站建设10强
  • 荆州北京网站建设如何自己做网页链接