欢迎光临
我们一直在努力

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

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

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

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

目录

一. 多进程并发服务器

1.1 创建子进程和孙子进程

1.2 设置信号SIG_IGN

二. 多线程并发服务器

三. 效率测试和总结


一. 多进程并发服务器

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/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");
}
}
}

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

代码如下:无脑发送一个打印 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
{
// 设置默认端口和最大backlog
const 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为监听fd
if (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/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");
}
}
}

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);
// 关闭fd
close(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
{
// 设置默认端口和最大backlog
const 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为监听fd
if (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);
// 关闭fd
close(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);
}
}

二. 多线程并发服务器

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

        线程是一个更好的方式,线程的开辟和切换所消耗的资源远小于进程。在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
{
// 设置默认端口和最大backlog
const 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为监听fd
if (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,281 73,625 100ms 1.84MB/s 482 482超时 ✅ 正常
1,000 多线程 8,650 87,421 126ms 2.19MB/s 73 73超时 ✅ 最佳
10,000 多进程 5,522 55,745 102ms 1.40MB/s 433 123读+310超时 ✅ 正常
10,000 多线程 7,375 74,453 194ms 1.86MB/s 353 107读+246超时 ✅ 最佳
25,000 多进程 1,042 35,604 420ms 270KB/s 10,972 77读+8932写+1963超时 ⚠️ 高压稳定
25,000 多线程 313 24,298 205ms 81KB/s 953 691读+262超时 ⚠️ 性能衰减
55,555 多进程 0 0 0us 0B/s 37,170 37170写错误 ❌ 崩溃
55,555 多线程 N/A N/A N/A N/A N/A 测试被终止 ❌ 崩溃

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

C/C++ Linux网络编程5 – 网络IO模型与select解决客户端并发连接

C/C++ Linux网络编程6 – poll解决客户端并发连接问题

C/C++ Linux网络编程7 – epoll解决客户端并发连接问题

C/C++ Linux网络编程8 – epoll + ET Reactor TCP服务器-CSDN博客

赞(0)
未经允许不得转载:171主机测评 » C/C++ Linux网络编程4 - 解决TCP服务器并发的方式
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址