欢迎光临
我们一直在努力

Socket编程进阶:百万并发服务器

Socket编程进阶:百万并发服务器

        本文旨在解决Socket编程快速入门(全面干货)中提到的服务器高并发性能问题。在日常应用中,传统服务器往往面临两难困境:要么支持少量客户端连接但响应迅速,要么容纳大量连接却响应迟缓。这两种情况都会严重影响用户体验,也无法充分利用服务器硬件资源。为此,服务器需实现真正的高并发能力——既要支持大规模客户端同时连接,又要确保对每个连接的快速响应,同时保持系统运行的稳定性。

目录

Socket编程进阶:百万并发服务器

客户端

一、多进程高并发服务器

基本思路  

详细实现(示例代码(详细注释))

1.常规实现多进程并发服务器方式

2.采用预fork进程池方式实现并发服务器

优缺点

二、多线程高并发服务器

基本思路  

详细实现(示例代码(详细注释))

1.常规的多线程高并发服务器实现方式

2.fork进程池+多线程高并发服务器实现方式

优缺点

三、 IO多路复用方式实现高并发服务器

相比于多进程或多线程实现的并发服务器的优缺点

基本思路

详细实现(示例代码(详细注释))

ET模式下的epoll方式的实现细节

四、BIO模型和NIO模型

1. BIO模型(阻塞I/O模型)

2. NIO模型(非阻塞I/O模型)

两种模型总结

五、多进程、多线程、ET模式epoll方式实现高并发服务器比较总结

1. 资源消耗

2. 性能 (高并发场景下)

3. 可扩展性 (连接数)

4. 适用场景

5.总结对比表


客户端

实现思想:在客户端进程中继续创建多个子进程启动客户端程序,模拟在高并发环境下,多个客户端几乎同时连接服务器情况,然后在客户端主进程中回收所有子进程,客户端的实现遵循TCP客户端的基本实现流程。下面高并发服务器均使用相同的客户端。

#include <stdio.h>//提供用于输入输出的函数,如printf()、scanf()、fprintf()、fscanf(),包含了文件操作的一些函数,如fopen()、fclose()、fread()、fwrite()等。
#include <stdlib.h>//提供各种通用的工具函数,如内存分配(malloc()、calloc()、realloc()、free())、随机数生成(rand()、srand())、环境查询(getenv())、程序控制(exit()、system())。
#include <unistd.h>//提供对POSIX操作系统API的访问包括sleep函数,主要用于Unix-like系统(如Linux、macOS),在Windows系统上不可用,因为它是Unix特有的。
#include <string.h>//提供用于处理C风格字符串(即以'\\0'结尾的字符数组)的函数,字符串复制(strcpy())、连接(strcat())、比较(strcmp())、长度计算(strlen())等函数。
// 线程相关
#include <pthread.h>//提供了一套创建和管理线程以及线程间同步的机制,使得开发者能够在Unix-like系统(如Linux和macOS)上实现多线程编程,具体实现在动态库libpthread.so中
#include <semaphore.h> //包括信号量sem_t
// 进程相关
#include <sys/resource.h> // 进程优先级相关函数
#include <sys/wait.h> // 进程等待函数
#include <sys/types.h> // 类型定义
#include <signal.h> // 信号函数
// socket网络编程相关
#include <arpa/inet.h>
#include <sys/socket.h> //Socket编程的数据结构和函数
#include <sys/select.h> //Socket编程IO复用的select方式
#include <sys/epoll.h> //Socket编程IO复用的epoll方式
#include<fcntl.h> //文件属性控制

// 实现思路:在客户端进程中继续创建多个子进程启动客户端程序,模拟在高并发环境下,多个客户端几乎同时连接服务器情况,然后在客户端主进程中回收所有子进程
void client(int client_number, int communicate_number) {
// 主进程回收终止子进程的信号函数
struct sigaction sigact_chld;
sigact_chld.sa_handler = [](int signal) {
switch (signal) {
case SIGCHLD:
// 子进程终止信号,非阻塞回收子进程的子进程
int cpid;
while ((cpid = waitpid(-1, NULL, WNOHANG)) > 0)
printf("client son process:%d recycle son process:%d\\n", getpid(), cpid);
break;

default:
break;
}
};
sigact_chld.sa_flags = SA_RESTART;
sigemptyset(&sigact_chld.sa_mask); // 信号屏蔽位置空,默认只屏蔽自身
// 注册信号和信号处理函数
sigaction(SIGCHLD, &sigact_chld, NULL);

// 子进程模拟创建多个并发的多进程客户端进行与服务器进行通信
int spid; // 子进程fork的返回值
for (int i = 0; i < client_number; i++) {
spid = fork(); // 创建子进程
if (spid == 0)
break; // 保证不会使创建的子进程迭代创建,只有父进程进行子进程的创建
}

// 子进程作为客户端访问服务器
printf("son process:%d start client!\\n", getpid());
// 1.创建与服务器进行通信的客户端socket
int communicate_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (communicate_sockfd == -1) {
printf("client:%d socket() error!\\n", getpid());
// 主进程退出前阻塞回收所有以终止子进程,其他子进程交给内核处理
if (spid > 0) {
// 阻塞回收子进程的子进程
int cpid;
while ((cpid = waitpid(-1, NULL, 0)) > 0)
printf("client son process:%d recycle son process:%d\\n", getpid(), cpid);
}
exit(-1);
}

// 2.客户端连接服务器,需要和服务器bind绑定的地址相同
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 查看自己主机网口的所有IP地址的任意一个,其中127.0.0.1是必有的
server_addr.sin_port = 10086;
int ret = connect(communicate_sockfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
if (ret == -1) {
printf("client:%d connect() error!\\n", getpid());;
close(communicate_sockfd);
// 主进程退出前阻塞回收所有以终止子进程,其他子进程交给内核处理
if (spid > 0) {
// 阻塞回收子进程的子进程
int cpid;
while ((cpid = waitpid(-1, NULL, 0)) > 0)
printf("client son process:%d recycle son process:%d\\n", getpid(), cpid);
}
exit(-1);
}

// 客户端和服务器进行循环通信
// 为观察输出,假设客户端和服务器只通信1次,客户端就主动关闭
for (int i = 0; i < communicate_number; i++) {
// 3.客户端给服务器发送数据
char wrbuf[1024];
sprintf(wrbuf, "Hello World form client:%d", getpid());
int wrbyte = write(communicate_sockfd, wrbuf, strlen(wrbuf));
// 根据返回值判断状态,执行相应行为
if (wrbyte == -1) {
printf("client:%d connect server error, client disconnect!\\n", getpid());
close(communicate_sockfd);
// 主进程退出前阻塞回收所有以终止子进程,其他子进程交给内核处理
if (spid > 0) {
// 阻塞回收子进程的子进程
int cpid;
while ((cpid = waitpid(-1, NULL, 0)) > 0)
printf("client son process:%d recycle son process:%d\\n", getpid(), cpid);
}
exit(-1);
}
else
printf("client:%d send numbers:%d data.\\n", getpid(), sizeof(wrbuf));

sleep(2);

// 4.客户端等待接收服务器的数据
char rdbuf[1024];
memset(rdbuf, 0, sizeof(rdbuf)); // 清空接收缓冲区
int rdbyte = read(communicate_sockfd, rdbuf, sizeof(rdbuf));
// 根据返回值判断状态,执行相应行为
if (rdbyte == -1) {
printf("client:%d connect server error, client disconnect!\\n", getpid());
close(communicate_sockfd);
// 主进程退出前阻塞回收所有以终止子进程,其他子进程交给内核处理
if (spid > 0) {
// 阻塞回收子进程的子进程
int cpid;
while ((cpid = waitpid(-1, NULL, 0)) > 0)
printf("client son process:%d recycle son process:%d\\n", getpid(), cpid);
}
exit(-1);
}
else if (rdbyte == 0) {
printf("server disconnect, client:%d disconnect!\\n", getpid());
close(communicate_sockfd);
// 主进程退出前阻塞回收所有以终止子进程,其他子进程交给内核处理
if (spid > 0) {
// 阻塞回收子进程的子进程
int cpid;
while ((cpid = waitpid(-1, NULL, 0)) > 0)
printf("client son process:%d recycle son process:%d\\n", getpid(), cpid);
}
exit(-1);
}
else
printf("client:%d receive server numbers:%d data:%s\\n", getpid(), rdbyte, rdbuf);
}

// 客户端主动关闭套接字,关闭与服务器连接
close(communicate_sockfd);

// 主进程退出前非阻塞回收所有以终止子进程,其他子进程交给内核处理
if (spid > 0) {
// 阻塞回收子进程的子进程
int cpid;
while ((cpid = waitpid(-1, NULL, 0)) > 0)
printf("client son process:%d recycle son process:%d\\n", getpid(), cpid);
}
exit(0);

// 客户端子进程运行结束
}


一、多进程高并发服务器

基本思路  

        在多进程高并发服务器中,服务器采用主进程循环监听客户端连接请求。每当有新客户端成功连接时,系统会为其创建一个独立的子进程负责通信处理,通信结束后该子进程立即终止。这种架构设计确保主进程专注于接收连接,而将实际的通信任务交由子进程完成。既避免主进程因通信操作阻塞而影响新连接处理,又充分利用了服务器硬件资源,显著提升了并发性能和客户端响应速度。

详细实现(示例代码(详细注释))

1.常规实现多进程并发服务器方式

        先介绍一种常见的多进程服务器实现思路,这种方式实现的服务器需要循环创建和回收子进程,创建和回收进程的开销很大,效率较低。

实现思想:在主进程中运行服务器程序,主进程服务器循环监听连接客户端,每次连接客户端成功后就创建一个子进程用于与该客户端进行通信。在子进程中运行客户端程序,并在该子进程中再次创建多个子进程都运行客户端程序,以模拟多个客户端几乎同时连接服务器的高并发情况。

#include <stdio.h>//提供用于输入输出的函数,如printf()、scanf()、fprintf()、fscanf(),包含了文件操作的一些函数,如fopen()、fclose()、fread()、fwrite()等。
#include <stdlib.h>//提供各种通用的工具函数,如内存分配(malloc()、calloc()、realloc()、free())、随机数生成(rand()、srand())、环境查询(getenv())、程序控制(exit()、system())。
#include <unistd.h>//提供对POSIX操作系统API的访问包括sleep函数,主要用于Unix-like系统(如Linux、macOS),在Windows系统上不可用,因为它是Unix特有的。
#include <string.h>//提供用于处理C风格字符串(即以'\\0'结尾的字符数组)的函数,字符串复制(strcpy())、连接(strcat())、比较(strcmp())、长度计算(strlen())等函数。
// 线程相关
#include <pthread.h>//提供了一套创建和管理线程以及线程间同步的机制,使得开发者能够在Unix-like系统(如Linux和macOS)上实现多线程编程,具体实现在动态库libpthread.so中
#include <semaphore.h> //包括信号量sem_t
// 进程相关
#include <sys/resource.h> // 进程优先级相关函数
#include <sys/wait.h> // 进程等待函数
#include <sys/types.h> // 类型定义
#include <signal.h> // 信号函数
// socket网络编程相关
#include <arpa/inet.h>
#include <sys/socket.h> //Socket编程的数据结构和函数
#include <sys/select.h> //Socket编程IO复用的select方式
#include <sys/epoll.h> //Socket编程IO复用的epoll方式
#include<fcntl.h> //文件属性控制

sigjmp_buf environment_1; // 保存服务器进程堆栈状态
void multiprocess1_TCP_server_client() {
// 创建信号量集实现主进程服务器和子进程客户端进行先后同步关系,避免出现客户端先运行,服务器后运行,直接出现问题
int semmid = semget(ftok(".", 1), 1, 0666 | IPC_CREAT); // 创建信号量
if (semmid == -1)
return; // 创建失败直接返回
semctl(semmid, 0, SETVAL, 0); // 信号量初始值设置为0

int pid = fork(); // 创建子进程
if (pid > 0) {
// 主进程
printf("main process:%d start server!\\n", getpid());

// lambda表达式创建清理函数,用于退出前打印信息,回收子进程,并是否执行清理套接字操作
void (*clear)(const char*, int) = [](const char* tips, int sockfd = -1) {
// 打印提示的错误信息tips
printf("%s\\n", tips);

// 判断是否清理已经创建的套接字sockfd
if (sockfd > 0)
close(sockfd);

// 退出主进程先阻塞回收所有已终止的子线程,然后退出主线程,还在运行的子线程退出交给内核管理
int pid; // 保存子进程pid
int status; // 子进程状态
while ((pid = waitpid(-1, &status, 0)) > 0)
printf("子进程%d退出,退出状态%d!\\n", pid, WEXITSTATUS(status));
};

// 主进程信号回调函数,包括SIGINT、SIGCHLD信号
struct sigaction sigact;
sigact.sa_handler = [](int signal) {
switch (signal) {
case SIGCHLD:
// 子进程终止信号,非阻塞回收子进程的子进程
int cpid;
while ((cpid = waitpid(-1, NULL, WNOHANG)) > 0)
printf("main process:%d recycle son process:%d\\n", getpid(), cpid);
break;
case SIGINT:
// 直接跳转恢复堆栈状态,并传出SIGINT信号
siglongjmp(environment_1, signal);
break;
default:
break;
}
};
sigact.sa_flags = SA_RESTART;
sigemptyset(&sigact.sa_mask); // 信号屏蔽位置空,默认只屏蔽自身
// 注册信号和信号处理函数
sigaction(SIGINT, &sigact, NULL);
sigaction(SIGCHLD, &sigact, NULL);

// 开始socket编程
// 1.主进程服务器创建用于监听的套接字socket
int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 创建socket失败,直接执行清理函数退出
if (listen_sockfd == -1) {
char buf[64];
sprintf(buf, "server:%d socket() error!", getpid()); // 打印错误

// 执行V操作,避免子进程无限制卡住
struct sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = +1; // V操作
sem_buf.sem_flg = 0; // 默认采取阻塞方式
semop(semmid, &sem_buf, 1); // 执行V操作

clear(buf, listen_sockfd);// 退出函数
return;
}
// 服务器创建socket成功后,设置socket的属性为可重用地址,实现出错时服务器可重用实现快速重启
int opt = 1; // 表示启用SO_REUSEAADDR
int ret = setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
ret = setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
if (ret == -1) {
char buf[64];
sprintf(buf, "server:%d socket() error!", getpid()); // 打印错误

// 执行V操作,避免子进程无限制卡住
struct sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = +1; // V操作
sem_buf.sem_flg = 0; // 默认采取阻塞方式
semop(semmid, &sem_buf, 1); // 执行V操作

clear(buf, listen_sockfd); // 退出函数
return;
}

// 2.主进程服务器绑定监听套接字和本地地址
struct sockaddr_in sockaddr;
sockaddr.sin_family = AF_INET; // 指定协议族IPV4
sockaddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 点分十进制的0.0.0.0表示本地任意地址
sockaddr.sin_port = 10086; // 选择任意一个注册端口号
ret = bind(listen_sockfd, (struct sockaddr*)&sockaddr, sizeof(sockaddr));
// 绑定bind失败,直接执行清理函数退出
if (ret == -1) {
char buf[64];
sprintf(buf, "server:%d bind() error!", getpid()); // 打印错误

// 执行V操作,避免子进程无限制卡住
struct sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = +1; // V操作
sem_buf.sem_flg = 0; // 默认采取阻塞方式
semop(semmid, &sem_buf, 1); // 执行V操作

clear(buf, listen_sockfd); // 退出函数
return;
}

//sleep(5);
// 3.主进程服务器进行监听客户端的连接
ret = listen(listen_sockfd, 16);
// 监听listen失败,直接执行清理函数退出
if (ret == -1) {
char buf[64];
sprintf(buf, "server:%d listen() error!", getpid()); // 打印错误

// 执行V操作,避免子进程无限制卡住
struct sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = +1; // V操作
sem_buf.sem_flg = 0; // 默认采取阻塞方式
semop(semmid, &sem_buf, 1); // 执行V操作

clear(buf, listen_sockfd); // 退出函数
return;
}
printf("server:%d listening(port:%d)……..\\n", getpid(), sockaddr.sin_port);
// 服务器主进程初始化完毕,执行信号量V操作唤醒子进程客户端
struct sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = +1; // V操作
sem_buf.sem_flg = 0; // 默认采取阻塞方式
semop(semmid, &sem_buf, 1); // 执行V操作

// 循环接收客户端的连接,并进行排队通信,这里需要并发处理
while (1) {
// 保存堆栈状态,保证在ctrl+c时跳转后,直接终止服务器循环
ret = sigsetjmp(environment_1, 1); // 保存堆栈状态
if (ret)
break; // 堆栈返回值出现错误时,直接退出服务器

// 4.接收客户段的连接
struct sockaddr_in client_addr; // 传出参数,用于保存连接的客户端地址信息
memset(&client_addr, 0, sizeof(client_addr)); // 清空保存的客户端数据
socklen_t len = sizeof(client_addr); // 传出参数,client_addr长度
int communciate_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &len);
// 检查返回的通信套接字成功,失败直接退出与该客户端的连接,成功则继续进行通信
if (communciate_sockfd == -1)
continue; // 直接进行下个客户端的连接

// 第一种方式,采取创建服务器创建子进程与客户端进行通信,实现服务器高并发
int pid_c = fork();
if (pid_c == 0) {
close(listen_sockfd); // 子进程关闭监听套接字,

// 创建服务器创建子进程与客户端通信
printf("server's son process:%d connect client:%s:%d success! communicate socket:%d.\\n", getpid(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), communciate_sockfd);
// 子进程与本连接成功的客户端循环通信,假设是一个长连接的场景
while (1) {
// 5.服务器等待客户端的信息,这里需要粘包处理
char rdbuf[1024]; // 读取缓冲区
// 清理读缓冲区数据,以避免影响下次数据的接收
memset(rdbuf, 0, sizeof(rdbuf));
// 采用read函数等待读取客户端发送数据
int rdbyte = read(communciate_sockfd, rdbuf, sizeof(rdbuf));
// 根据返回值判断状态,执行相应操作
if (rdbyte == -1) {
// read函数返回-1,表示连接出现错误,直接关闭和客户端的通信
printf("server's son process:%d connect client:%s:%d error, server disconnect!\\n", getpid(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
close(communciate_sockfd);
break; // 直接进行下个客户端的连接
}
else if (rdbyte == 0) {
// read函数返回0,表示客户端断开连接,直接关闭和客户端的通信
printf("client:%s:%d disconnect, server's son process:%d disconnect!\\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), getpid());
close(communciate_sockfd);
break; // 直接进行下个客户端的连接
}
else
printf("server's son process:%d receive client:%s:%d numbers %d data:%s.\\n", getpid(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), rdbyte, rdbuf);

sleep(2);

// 6.服务器成功读取客户端的信息,进行处理然后使用发送数据给客户端
char wrbuf[] = "Hello World form server"; // 服务器发送给客户端的数据
int wrbyte = write(communciate_sockfd, wrbuf, sizeof(wrbuf));
// 根据返回值判断状态,执行相应操作
if (wrbyte == -1) {
// write函数返回-1,表示连接出现错误,直接关闭和客户端的通信
printf("server's son process:%d connect client:%s:%d error, server disconnect!\\n", getpid(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
close(communciate_sockfd);
break; // 直接进行下个客户端的连接
}
else
printf("server's son process:%d send client:%s:%d number %d data.\\n", getpid(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), wrbyte);
}
// 服务器子进程不会主动关闭与客户端通信,只有在客户端关闭通信或其他方式引发错误后,服务器子进程关闭套接字而停止通信
exit(0);
}
else if (pid_c > 0) {
close(communciate_sockfd); // 父进程关闭通信套接字

printf("server:%d create son process:%d to communicate client, communicate socket:%d.\\n", getpid(), pid_c, communciate_sockfd);
continue; // 服务器主进程直接进行下一个客户端的连接
}
else {
printf("server fork() son process fail. disconnect client:%s:%d! close communicate socket:%d.\\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), communciate_sockfd);
// 服务器创建子进程失败,直接关闭与客户端通信的套接字,继续进行下一个客户端的连接
close(communciate_sockfd);
continue; // 服务器主进程直接进行下一个客户端的连接
}
}
// 服务器运行结束前,清理资源
close(listen_sockfd); // 关闭监听套接字

// 由于是主进程,主进程运行结束前,阻塞回收所有子进程
int spid;
while ((spid = waitpid(-1, NULL, 0)) > 0)
printf("main process:%d recycle son process:%d\\n", getpid(), spid);

// 主进程创建的信号量在所有子进程结束后才回收信号量
semctl(semmid, 0, IPC_RMID); // 删除信号量
// 主进程服务器运行结束
}
else if (pid == 0) {

// 执行信号量的P操作,避免出现客户端子进程比服务器先运行的情况
sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = -1; // P操作
sem_buf.sem_flg = 0; // 默认阻塞方式
semop(semmid, &sem_buf, 1); // 执行P操作

// 子进程模拟创建多个并发的多进程客户端进行与服务器进行通信
client(2, 1); // 创建2+1个客户端子进程,每个客户端只发送1次消息就退出,便于观察
}
else
printf("fork() error!\\n");
}

实现要注意的几个细节

1.主进程服务器要负责回收所有与客户端通信结束后终止的子进程,避免主进程服务器运行期间产生大量僵尸进程消耗系统资源,本实现中通过在服务器运行期间使用子进程终止的SIGCHLD信号,并注册回收函数实现,并在服务器运行退出前阻塞回收其创建的所有子进程方式实现。

#include <stdio.h>//提供用于输入输出的函数,如printf()、scanf()、fprintf()、fscanf(),包含了文件操作的一些函数,如fopen()、fclose()、fread()、fwrite()等。
#include <stdlib.h>//提供各种通用的工具函数,如内存分配(malloc()、calloc()、realloc()、free())、随机数生成(rand()、srand())、环境查询(getenv())、程序控制(exit()、system())。
#include <unistd.h>//提供对POSIX操作系统API的访问包括sleep函数,主要用于Unix-like系统(如Linux、macOS),在Windows系统上不可用,因为它是Unix特有的。
#include <string.h>//提供用于处理C风格字符串(即以'\\0'结尾的字符数组)的函数,字符串复制(strcpy())、连接(strcat())、比较(strcmp())、长度计算(strlen())等函数。
// 线程相关
#include <pthread.h>//提供了一套创建和管理线程以及线程间同步的机制,使得开发者能够在Unix-like系统(如Linux和macOS)上实现多线程编程,具体实现在动态库libpthread.so中
#include <semaphore.h> //包括信号量sem_t
// 进程相关
#include <sys/resource.h> // 进程优先级相关函数
#include <sys/wait.h> // 进程等待函数
#include <sys/types.h> // 类型定义
#include <signal.h> // 信号函数
// socket网络编程相关
#include <arpa/inet.h>
#include <sys/socket.h> //Socket编程的数据结构和函数
#include <sys/select.h> //Socket编程IO复用的select方式
#include <sys/epoll.h> //Socket编程IO复用的epoll方式
#include<fcntl.h> //文件属性控制

// 主进程信号回调函数,包括SIGINT、SIGCHLD信号
struct sigaction sigact;
sigact.sa_handler = [](int signal) {
switch (signal) {
case SIGCHLD:
// 子进程终止信号,非阻塞回收子进程的子进程
int cpid;
while ((cpid = waitpid(-1, NULL, WNOHANG)) > 0)
printf("main process:%d recycle son process:%d\\n", getpid(), cpid);
break;
case SIGINT:
// 直接跳转恢复堆栈状态,并传出SIGINT信号
siglongjmp(environment_1, signal);
break;
default:
break;
}
};
sigact.sa_flags = SA_RESTART;
sigemptyset(&sigact.sa_mask); // 信号屏蔽位置空,默认只屏蔽自身
// 注册信号和信号处理函数
sigaction(SIGINT, &sigact, NULL);
sigaction(SIGCHLD, &sigact, NULL);

// 由于是主进程,主进程服务器运行结束前,阻塞回收其创建的所有子进程
int spid;
while ((spid = waitpid(-1, NULL, 0)) > 0)
printf("main process:%d recycle son process:%d\\n", getpid(), spid);

2.主进程服务器的退出方式,ctrl+c键发出SIGINT信号,并注册该信号处理函数如上代码会执行siglongjmp()函数跳转到堆栈恢复函数sigsetjmp()跳转到下面循环接收中,采用break终止服务器循环监听连接客户端的循环,最后关闭监听套接字回收资源。

// 服务器循环接收客户端发送的UDP数据并回复
while (1) {
// 保存堆栈状态,返回错误值时,直接关闭服务器的通信
ret = sigsetjmp(environment, 1);
if (ret)
break;
// 这里直接会直接退出服务器监听连接客户单循环

……..(连接客户端操作创建子进程操作)

}

// 服务器退出前回收资源
close(listen_sockfd);

3.在主进程服务器中只进行监听连接客户端,不与客户端通信,需要关闭accept函数连接成功返回的与客户端通信的套接字communicate_sockfd。在服务器创建的与客户端通信的子进程,由于子进程会完整复制一份主进程资源,在子进程中需要立即关闭服务器监听客户端的套接字listen_sockfd。这么做的目的是防止出现子进程循环也会使用accept函数出现只有一个进程成功、其他进程阻塞失败的“惊群现象”,并保证文件描述符引用计数的正确,保证系统资源的释放。

4.采取这种方式会发现每次主进程服务器连接客户端成功后,accept函数的返回值都是相同的套接字文件描述符,这是因为主进程服务器只需要进行监听连接客户端,由创建的子进程与客户端通信,主进程会主动关闭accept返回的套接字文件描述符,造成每次主进程服务器accept成功后返回的最小套接字文件描述符相同,虽然文件描述符相同,但是accept函数在内核被调用时会使用全局唯一未使用的内核对象创建绑定服务器进程最小套接字文件描述符返回,该内核对象引用计算在创建子进程继承后会+1,并在引用计算为0时由内核回收,并继承给子进程使用与客户端通信,因此虽然各个子进程与客户端通信的在本地进程中的套接字文件描述符相同,但实质内部使用绑定的内核对象不同。

5.采用信号量机制确保主进程服务器比子进程客户端先运行,在服务器与客户端分开时,不需要使用这部分内容。

// 创建信号量集实现主进程服务器和子进程客户端进行先后同步关系,避免出现客户端先运行,服务器后运行,直接出现问题
int semmid = semget(ftok(".", 1), 1, 0666 | IPC_CREAT); // 创建信号量
if (semmid == -1)
return; // 创建失败直接返回
semctl(semmid, 0, SETVAL, 0); // 信号量初始值设置为0

// 在子进程运行前使用信号量初始值为0的P操作会先阻塞子进程
// 执行信号量的P操作,避免出现客户端子进程比服务器先运行的情况
sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = -1; // P操作
sem_buf.sem_flg = 0; // 默认阻塞方式
semop(semmid, &sem_buf, 1); // 执行P操作

// 在主进程服务器进行监听开始后,执行信号量初始值为0的V操作会唤醒阻塞的子进程,子进程启动客户端
// 服务器主进程初始化完毕,执行信号量V操作唤醒子进程客户端
struct sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = +1; // V操作
sem_buf.sem_flg = 0; // 默认采取阻塞方式
semop(semmid, &sem_buf, 1); // 执行V操作

2.采用预fork进程池方式实现并发服务器

        相比于常规方式,“预fork进程池”方式采用事先创建指定数量的工作子进程专门用于循环连接客户端并进行通信,并在通信结束后不回收工作子进程,而是继续循环连接其他客户端并进行通信,相比于常规方式,对系统资源的开销更小。

实现思路:主进程用于创建高并发服务器,子进程用于模拟创建多个客户端。主进程采用"预fork进程池"方式循环创建多个工作子进程,每个工作子线程与客户端进行accept连接,连接成功后然后在该子进程与客户端进行通信,在与客户端通信结束后关闭套接字,然后进行下个客户端连接。采取这种方式主进程服务器不需要关闭accept生成的文件描述符,从而不会出现文件描述符重用问题,主进程服务器甚至可以关闭监听套接字并直接退出,工作子进程不关闭监听套接字,退出后系统内核回收。

fork进程池中的工作子进程执行的任务:连接客户端成功后,与客户端循环通信,在与当前客户端通信后,循环连接下个客户端。

// fork进程池中的工作子进程任务
void (*server_forkprocess_work)(int) = [](int listen_sockfd) {
// 工作子线程循环与客户端连接、通信,由内核调用accept函数时实现子线程间的同步和文件描述符的分配
while (1) {
// 4.接收客户段的连接
struct sockaddr_in client_addr; // 传出参数,用于保存连接的客户端地址信息
memset(&client_addr, 0, sizeof(client_addr)); // 清空保存的客户端数据
socklen_t len = sizeof(client_addr); // 传出参数,client_addr长度
int communciate_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &len);
// 检查返回的通信套接字成功,失败直接退出与该客户端的连接,成功则继续进行通信
if (communciate_sockfd == -1)
continue; // 连接失败,直接进行下个客户端的连接

// 服务器的子进程与客户端通信
printf("server's son process:%d connect client:%s:%d success! communicate socket:%d.\\n", getpid(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), communciate_sockfd);
// 子进程与本连接成功的客户端循环通信,假设是一个长连接的场景
while (1) {
// 5.服务器等待客户端的信息,这里需要粘包处理
char rdbuf[1024]; // 读取缓冲区
// 清理读缓冲区数据,以避免影响下次数据的接收
memset(rdbuf, 0, sizeof(rdbuf));
// 采用read函数等待读取客户端发送数据
int rdbyte = read(communciate_sockfd, rdbuf, sizeof(rdbuf));
// 根据返回值判断状态,执行相应操作
if (rdbyte == -1) {
// read函数返回-1,表示连接出现错误,直接关闭和客户端的通信
printf("server's son process:%d connect client:%s:%d error, server's son process disconnect!\\n", getpid(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
close(communciate_sockfd);
break; // 直接进行下个客户端的连接
}
else if (rdbyte == 0) {
// read函数返回0,表示客户端断开连接,直接关闭和客户端的通信
printf("client:%s:%d disconnect, server's son process:%d disconnect!\\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), getpid());
close(communciate_sockfd);
break; // 直接进行下个客户端的连接
}
else
printf("server's son process:%d receive client:%s:%d numbers %d data:%s.\\n", getpid(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), rdbyte, rdbuf);

sleep(2); // 模拟服务器子进程处理2秒

// 6.服务器成功读取客户端的信息,进行处理然后使用发送数据给客户端
char wrbuf[] = "Hello World form server"; // 服务器发送给客户端的数据
int wrbyte = write(communciate_sockfd, wrbuf, sizeof(wrbuf));
// 根据返回值判断状态,执行相应操作
if (wrbyte == -1) {
// write函数返回-1,表示连接出现错误,直接关闭和客户端的通信
printf("server's son process:%d connect client:%s:%d error, server disconnect!\\n", getpid(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
close(communciate_sockfd);
break; // 直接进行下个客户端的连接
}
else
printf("server's son process:%d send client:%s:%d number %d data.\\n", getpid(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), wrbyte);
}
}
// fork进程池中的工作子进程一般不会主动退出
exit(0);
};

完整代码实现:

#include <stdio.h>//提供用于输入输出的函数,如printf()、scanf()、fprintf()、fscanf(),包含了文件操作的一些函数,如fopen()、fclose()、fread()、fwrite()等。
#include <stdlib.h>//提供各种通用的工具函数,如内存分配(malloc()、calloc()、realloc()、free())、随机数生成(rand()、srand())、环境查询(getenv())、程序控制(exit()、system())。
#include <unistd.h>//提供对POSIX操作系统API的访问包括sleep函数,主要用于Unix-like系统(如Linux、macOS),在Windows系统上不可用,因为它是Unix特有的。
#include <string.h>//提供用于处理C风格字符串(即以'\\0'结尾的字符数组)的函数,字符串复制(strcpy())、连接(strcat())、比较(strcmp())、长度计算(strlen())等函数。
// 线程相关
#include <pthread.h>//提供了一套创建和管理线程以及线程间同步的机制,使得开发者能够在Unix-like系统(如Linux和macOS)上实现多线程编程,具体实现在动态库libpthread.so中
#include <semaphore.h> //包括信号量sem_t
// 进程相关
#include <sys/resource.h> // 进程优先级相关函数
#include <sys/wait.h> // 进程等待函数
#include <sys/types.h> // 类型定义
#include <signal.h> // 信号函数
// socket网络编程相关
#include <arpa/inet.h>
#include <sys/socket.h> //Socket编程的数据结构和函数
#include <sys/select.h> //Socket编程IO复用的select方式
#include <sys/epoll.h> //Socket编程IO复用的epoll方式
#include<fcntl.h> //文件属性控制

#define MAX_PROCESS 3 // 预fork进程工作池的工作子线程数量
void multiprocess2_TCP_server_client() {
// 创建信号量集实现主进程服务器和子进程客户端进行先后同步关系,避免出现客户端先运行,服务器后运行,直接出现问题
int semmid = semget(ftok(".", 1), 1, 0666 | IPC_CREAT); // 创建信号量
if (semmid == -1)
return; // 创建失败直接返回
semctl(semmid, 0, SETVAL, 0); // 信号量初始值设置为0

int pid = fork(); // 创建子进程
if (pid > 0) {
// 主进程
printf("main process:%d start server!\\n", getpid());

// lambda表达式创建清理函数,用于退出前打印信息,回收子进程,并是否执行清理套接字操作
void (*clear)(const char*, int) = [](const char* tips, int sockfd = -1) {
// 打印提示的错误信息tips
printf("%s\\n", tips);

// 判断是否清理已经创建的套接字sockfd
if (sockfd > 0)
close(sockfd);

// 退出主进程先阻塞回收所有已终止的子线程,然后退出主线程,还在运行的子线程退出交给内核管理
int pid; // 保存子进程pid
int status; // 子进程状态
while ((pid = waitpid(-1, &status, 0)) > 0)
printf("子进程%d退出,退出状态%d!\\n", pid, WEXITSTATUS(status));
};

// 开始socket编程
// 1.主进程服务器创建用于监听的套接字socket
int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 创建socket失败,直接执行清理函数退出
if (listen_sockfd == -1) {
char buf[64];
sprintf(buf, "server:%d socket() error!", getpid()); // 打印错误

// 执行V操作,避免子进程无限制卡住
struct sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = +1; // V操作
sem_buf.sem_flg = 0; // 默认采取阻塞方式
semop(semmid, &sem_buf, 1); // 执行V操作

clear(buf, listen_sockfd);// 退出函数
return;
}
// 服务器创建socket成功后,设置socket的属性为可重用地址,实现出错时服务器可重用实现快速重启
int opt = 1; // 表示启用SO_REUSEAADDR
int ret = setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
ret = setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
if (ret == -1) {
char buf[64];
sprintf(buf, "server:%d socket() error!", getpid()); // 打印错误

// 执行V操作,避免子进程无限制卡住
struct sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = +1; // V操作
sem_buf.sem_flg = 0; // 默认采取阻塞方式
semop(semmid, &sem_buf, 1); // 执行V操作

clear(buf, listen_sockfd); // 退出函数
return;
}

// 2.主进程服务器绑定监听套接字和本地地址
struct sockaddr_in sockaddr;
sockaddr.sin_family = AF_INET; // 指定协议族IPV4
sockaddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 点分十进制的0.0.0.0表示本地任意地址
sockaddr.sin_port = 10086; // 选择任意一个注册端口号
ret = bind(listen_sockfd, (struct sockaddr*)&sockaddr, sizeof(sockaddr));
// 绑定bind失败,直接执行清理函数退出
if (ret == -1) {
char buf[64];
sprintf(buf, "server:%d bind() error!", getpid()); // 打印错误

// 执行V操作,避免子进程无限制卡住
struct sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = +1; // V操作
sem_buf.sem_flg = 0; // 默认采取阻塞方式
semop(semmid, &sem_buf, 1); // 执行V操作

clear(buf, listen_sockfd); // 退出函数
return;
}

//sleep(5);
// 3.主进程服务器进行监听客户端的连接
ret = listen(listen_sockfd, 16);
// 监听listen失败,直接执行清理函数退出
if (ret == -1) {
char buf[64];
sprintf(buf, "server:%d listen() error!", getpid()); // 打印错误

// 执行V操作,避免子进程无限制卡住
struct sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = +1; // V操作
sem_buf.sem_flg = 0; // 默认采取阻塞方式
semop(semmid, &sem_buf, 1); // 执行V操作

clear(buf, listen_sockfd); // 退出函数
return;
}
printf("server:%d listening(port:%d)……..\\n", getpid(), sockaddr.sin_port);
// 服务器主进程初始化完毕,执行信号量V操作唤醒子进程客户端
struct sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = +1; // V操作
sem_buf.sem_flg = 0; // 默认采取阻塞方式
semop(semmid, &sem_buf, 1); // 执行V操作

// fork进程池中的工作子进程任务
void (*server_forkprocess_work)(int) = [](int listen_sockfd) {
// 工作子线程循环与客户端连接、通信,由内核调用accept函数时实现子线程间的同步和文件描述符的分配
while (1) {
// 4.接收客户段的连接
struct sockaddr_in client_addr; // 传出参数,用于保存连接的客户端地址信息
memset(&client_addr, 0, sizeof(client_addr)); // 清空保存的客户端数据
socklen_t len = sizeof(client_addr); // 传出参数,client_addr长度
int communciate_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &len);
// 检查返回的通信套接字成功,失败直接退出与该客户端的连接,成功则继续进行通信
if (communciate_sockfd == -1)
continue; // 连接失败,直接进行下个客户端的连接

// 服务器的子进程与客户端通信
printf("server's son process:%d connect client:%s:%d success! communicate socket:%d.\\n", getpid(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), communciate_sockfd);
// 子进程与本连接成功的客户端循环通信,假设是一个长连接的场景
while (1) {
// 5.服务器等待客户端的信息,这里需要粘包处理
char rdbuf[1024]; // 读取缓冲区
// 清理读缓冲区数据,以避免影响下次数据的接收
memset(rdbuf, 0, sizeof(rdbuf));
// 采用read函数等待读取客户端发送数据
int rdbyte = read(communciate_sockfd, rdbuf, sizeof(rdbuf));
// 根据返回值判断状态,执行相应操作
if (rdbyte == -1) {
// read函数返回-1,表示连接出现错误,直接关闭和客户端的通信
printf("server's son process:%d connect client:%s:%d error, server's son process disconnect!\\n", getpid(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
close(communciate_sockfd);
break; // 直接进行下个客户端的连接
}
else if (rdbyte == 0) {
// read函数返回0,表示客户端断开连接,直接关闭和客户端的通信
printf("client:%s:%d disconnect, server's son process:%d disconnect!\\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), getpid());
close(communciate_sockfd);
break; // 直接进行下个客户端的连接
}
else
printf("server's son process:%d receive client:%s:%d numbers %d data:%s.\\n", getpid(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), rdbyte, rdbuf);

sleep(2); // 模拟服务器子进程处理2秒

// 6.服务器成功读取客户端的信息,进行处理然后使用发送数据给客户端
char wrbuf[] = "Hello World form server"; // 服务器发送给客户端的数据
int wrbyte = write(communciate_sockfd, wrbuf, sizeof(wrbuf));
// 根据返回值判断状态,执行相应操作
if (wrbyte == -1) {
// write函数返回-1,表示连接出现错误,直接关闭和客户端的通信
printf("server's son process:%d connect client:%s:%d error, server disconnect!\\n", getpid(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
close(communciate_sockfd);
break; // 直接进行下个客户端的连接
}
else
printf("server's son process:%d send client:%s:%d number %d data.\\n", getpid(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), wrbyte);
}
}
// fork进程池中的工作子进程一般不会主动退出
exit(0);
};

// 采用预fork进程工作池方式,池大小为MAX_PROCESS,每个创建的工作子线程进行与客户端通信
for(int i = 0;i < MAX_PROCESS; i++) {

// 创建子进程进行accept连接,然后与客户端通信,循环进行,避免出现文件描述符重用问题
int pid_c = fork();
if (pid_c == 0) {
// 工作子进程任务:循环监听连接客户端,在连接成功后与客户端循环通信
server_forkprocess_work(listen_sockfd);
}
else if (pid_c > 0) {
printf("server:%d create son process:%d add fork_pool:%d.\\n", getpid(), pid_c, i+1);
continue; // 服务器主进程直接进行fork进程池的下个工作子进程创建
}
else {
printf("server fork() son process fail.\\n");
// 服务器创建子进程失败,直接关闭与客户端通信的套接字,继续进行下一个客户端的连接
continue; // 服务器主进程直接进行fork进程池的下个工作子进程创建
}
}

// 在保证服务器比客户端先运行后,客户端使用完后,直接删除信号量
semctl(semmid, 0, IPC_RMID); // 删除信号量

// fork进程池方式,主进程服务器用于监测工作子进程状态,在发现子进程异常退出时,重新创建工作子进程
while (1) {
int spid;
while ((spid = waitpid(-1, NULL, 0)) > 0) {
printf("main process:%d recycle son process:%d\\n", getpid(), spid);
if (spid != pid) {
// 重新创建工作子进程
int new_pid = fork();
if (new_pid == 0)
server_forkprocess_work(listen_sockfd); // 新创建的工作子进程
else if (new_pid)
printf("main process:%d create new son process:%d.\\n", getpid(), new_pid);
else
printf("main process fork() error!\\n");
}
}
}
// ctrl+c退出时,由内核回收进程的文件描述符表中的监听文件描述符listen_sockfd
}
else if (pid == 0) {

// 执行信号量的P操作,避免出现客户端子进程比服务器先运行的情况
sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = -1; // P操作
sem_buf.sem_flg = 0; // 默认阻塞方式
semop(semmid, &sem_buf, 1); // 执行P操作

// 子进程模拟创建多个并发的多进程客户端进行与服务器进行通信
client(2, 1); // 创建2+1个客户端子进程,每个客户端只发送1次消息就退出,便于观察
}
else
printf("fork() error!\\n");
}

 实现细节: 1.采用"预fork进程池"方式的主进程服务器可以直接关闭监听套接字,然后退出,工作子线程变成“孤儿进程”,交给内核处理。也可以阻塞等待所有工作子线程终止,或检查异常的工作子线程终止时,再次创建工作子线程。由于主进程不使用监听套接字,可以直接关闭监听套接字,工作子线程需要使用不关闭监听套接字。  这里采用主进程服务器进行循环监视fork进程池中各个子进程状态,发现有终止工作子进程,立即回收并马上重新创建。

// fork进程池方式,主进程服务器用于监测工作子进程状态,在发现子进程异常退出时,重新创建工作子进程
while (1) {
int spid;
while ((spid = waitpid(-1, NULL, 0)) > 0) {
printf("main process:%d recycle son process:%d\\n", getpid(), spid);
// 重新创建工作子进程
int new_pid = fork();
if (new_pid == 0)
server_forkprocess_work(listen_sockfd); // 新创建的工作子进程
else if (new_pid)
printf("main process:%d create new son process:%d.\\n", getpid(), new_pid);
else
printf("main process fork() error!\\n");
}
}
// ctrl+c退出时,由内核回收进程的文件描述符表中的监听文件描述符listen_sockfd

2.fork进程池中的工作子进程采取完整的与客户端连接、通信流程,存在多个工作子进程共同竞争使用accept函数,其由内核实现子进程间同步,并分配使用全局唯一未使用的内核对象的本进程的套接字文件描述符,所以不会出错。

3.可增加数据结构管理这些创建的fork进程池中的工作子进程,以进行优化监测。

优缺点

优点
  • 高稳定性: 这是最大的优点。每个客户端连接由独立的子进程处理。如果一个子进程崩溃(例如,由于处理某个客户端请求时发生错误),它不会影响父进程(主服务器进程)或其他正在处理其他客户端连接的服务子进程。服务器整体依然能够继续运行。
  • 充分利用多核 CPU: 多个进程可以在不同的 CPU 核心上并行运行,充分利用现代多核处理器的计算能力,提高整体吞吐量。
  • 实现相对简单 (基于 fork): 在类 Unix 系统上,利用 fork() 系统调用创建子进程来实现并发是一种经典且相对直接的模式。父进程负责监听和接受连接,子进程负责具体的请求处理。
  • 进程间天然隔离: 每个进程拥有自己独立的地址空间和资源,减少了共享数据带来的复杂性和潜在风险(如竞态条件)。
  • 缺点
  • 资源消耗大: 创建新进程需要复制父进程的大部分资源(如地址空间),这比创建线程的开销要大得多。当并发连接数很高时,频繁创建和销毁进程会消耗大量的 CPU 时间和内存资源,成为性能瓶颈。
  • 进程间通信 (IPC) 复杂且开销大: 由于进程间内存空间隔离,它们之间的通信(例如,需要共享某些数据或状态)需要通过操作系统提供的 IPC 机制(如管道、消息队列、共享内存、信号量等)。这些操作通常比线程间的通信(如通过共享内存)要慢且编程更复杂。
  • 上下文切换开销大: 操作系统在多个进程之间切换(上下文切换)时,需要保存和恢复更多的状态信息(如整个地址空间的映射),这比线程间的上下文切换开销更大。
  • 可扩展性受限: 受限于进程创建、销毁和 IPC 的开销,多进程模型在处理海量并发连接时,其扩展性通常不如多线程或事件驱动(如 epoll)模型好。
  • 管理复杂: 需要处理僵尸进程的回收(通过信号或 wait 族函数),管理多个子进程的生命周期增加了编程的复杂性。

  • 二、多线程高并发服务器

            客户端代码均是相同的,采用子进程内部创建多个子进程模拟高并发环境下,出现的多个客户端进程几乎同时连接服务器的情况,以测试服务器的运行情况,完整服务器代码实现均有,直接在Linux环境下运行。

            服务器端遵循TCP服务器的基本流程,只不过为实现高并发,采用线程、进程等Liunx系统编程知识,这部分内容的干货后续会出。

    基本思路  

            在高并发服务器设计中,系统采用主从线程架构实现高效连接管理。主线程持续监听客户端连接请求,每当检测到新连接时,系统会动态创建专用子线程进行通信处理,并在会话结束后自动回收线程资源。这种架构将连接监听与数据处理逻辑分离,既保证了主线程专注于新连接的高效接收,又通过多线程并行处理充分发挥硬件性能。该设计有效避免了通信阻塞对连接处理的干扰,显著提升了系统的并发处理能力和响应速度。多线程高并发服务器实现的基本思想和多进程并发服务器的实现基本一致,但线程创建、切换和销毁,相比于进程,对系统资源的开销更少,速度更快。

    详细实现(示例代码(详细注释))

    1.常规的多线程高并发服务器实现方式

    实现思想:在主线程中只负责监听客户端的连接,并在连接客户端成功后,创建子线程用于和客户端进行通信,主线程进行循环监听客户端。客户端采用子进程实现,并在该子进程中再次创建多个子进程模拟在高并发环境下,多个客户端进程高并发同时连接服务器的情况。

    子线程的工作函数:与客户端循环通信,在客户端终止后,回收套接字等其他资源,退出子线程。

    主线程服务器传递给子线程的结构体指针,包含子线程与客户端通信需要的客户端信息。

    // 构建传递数据的连接客户端的结构体
    struct CLIENTINFO {
    int communicate_sockfd; // 与客户端通信的套接字文件描述符
    char ip[32]; // IPV4地址
    unsigned short port; // port端口号
    };
    // 创建服务器子线程与客户端通信的任务函数
    void* (*pthread_work)(void*) = [](void* pclient_info) {

    // 还原参数数据
    CLIENTINFO* client_info = (CLIENTINFO*)pclient_info; // 传递的客户端信息

    // 子线程与客户端通信
    printf("server's son pthread:%ld connect client:%s:%d success! communicate socket:%d.\\n", pthread_self(), client_info->ip, client_info->port, client_info->communicate_sockfd);

    // 子线程与主线程连接成功的客户端循环通信,假设是一个长连接的场景
    while (1) {
    // 5.服务器等待客户端的信息,这里需要粘包处理
    char rdbuf[1024]; // 读取缓冲区
    // 清理读缓冲区数据,以避免影响下次数据的接收
    memset(rdbuf, 0, sizeof(rdbuf));
    // 采用read函数等待读取客户端发送数据
    int rdbyte = read(client_info->communicate_sockfd, rdbuf, sizeof(rdbuf));
    // 根据返回值判断状态,执行相应操作
    if (rdbyte == -1) {
    // read函数返回-1,表示连接出现错误,直接关闭和客户端的通信
    printf("server's son pthread:%ld connect client:%s:%d error, server disconnect!\\n", pthread_self(), client_info->ip, client_info->port);
    close(client_info->communicate_sockfd);
    break; // 直接进行下个客户端的连接
    }
    else if (rdbyte == 0) {
    // read函数返回0,表示客户端断开连接,直接关闭和客户端的通信
    printf("client:%s:%d disconnect, server's son pthread:%ld disconnect!\\n", client_info->ip, client_info->port, pthread_self());
    close(client_info->communicate_sockfd);
    break; // 直接进行下个客户端的连接
    }
    else
    printf("server's son pthread:%ld receive client:%s:%d numbers %d data:%s.\\n", pthread_self(), client_info->ip, client_info->port, rdbyte, rdbuf);

    sleep(2);

    // 6.服务器成功读取客户端的信息,进行处理然后使用发送数据给客户端
    char wrbuf[] = "Hello World form server"; // 服务器发送给客户端的数据
    int wrbyte = write(client_info->communicate_sockfd, wrbuf, sizeof(wrbuf));
    // 根据返回值判断状态,执行相应操作
    if (wrbyte == -1) {
    // write函数返回-1,表示连接出现错误,直接关闭和客户端的通信
    printf("server's son pthread:%ld connect client:%s:%d error, server disconnect!\\n", pthread_self(), client_info->ip, client_info->port);
    close(client_info->communicate_sockfd);
    break; // 直接进行下个客户端的连接
    }
    else
    printf("server's son pthread:%ld send client:%s:%d number %d data.\\n", pthread_self(), client_info->ip, client_info->port, wrbyte);
    }
    // new创建的数据传递,子线程使用完后释放
    delete client_info;
    // 子线程退出
    pthread_exit(NULL);

    return (void*)NULL;
    };

    完整代码实现:

    #include <stdio.h>//提供用于输入输出的函数,如printf()、scanf()、fprintf()、fscanf(),包含了文件操作的一些函数,如fopen()、fclose()、fread()、fwrite()等。
    #include <stdlib.h>//提供各种通用的工具函数,如内存分配(malloc()、calloc()、realloc()、free())、随机数生成(rand()、srand())、环境查询(getenv())、程序控制(exit()、system())。
    #include <unistd.h>//提供对POSIX操作系统API的访问包括sleep函数,主要用于Unix-like系统(如Linux、macOS),在Windows系统上不可用,因为它是Unix特有的。
    #include <string.h>//提供用于处理C风格字符串(即以'\\0'结尾的字符数组)的函数,字符串复制(strcpy())、连接(strcat())、比较(strcmp())、长度计算(strlen())等函数。
    // 线程相关
    #include <pthread.h>//提供了一套创建和管理线程以及线程间同步的机制,使得开发者能够在Unix-like系统(如Linux和macOS)上实现多线程编程,具体实现在动态库libpthread.so中
    #include <semaphore.h> //包括信号量sem_t
    // 进程相关
    #include <sys/resource.h> // 进程优先级相关函数
    #include <sys/wait.h> // 进程等待函数
    #include <sys/types.h> // 类型定义
    #include <signal.h> // 信号函数
    // socket网络编程相关
    #include <arpa/inet.h>
    #include <sys/socket.h> //Socket编程的数据结构和函数
    #include <sys/select.h> //Socket编程IO复用的select方式
    #include <sys/epoll.h> //Socket编程IO复用的epoll方式
    #include<fcntl.h> //文件属性控制

    sigjmp_buf environment_2; // 保存服务器进程堆栈状态
    void multipthread1_TCP_server_client() {
    // 创建信号量集实现主进程服务器和子进程客户端进行先后同步关系,避免出现客户端先运行,服务器后运行,直接出现问题
    int semmid = semget(ftok(".", 1), 1, 0666 | IPC_CREAT); // 创建信号量
    if (semmid == -1)
    return; // 创建失败直接返回
    semctl(semmid, 0, SETVAL, 0); // 信号量初始值设置为0

    int pid = fork(); // 创建子进程
    if (pid > 0) {
    // 主进程的第一个线程即为主线程
    printf("main process:%d main pthread:%ld start server!\\n", getpid(), pthread_self());

    // lambda表达式创建清理函数,用于退出前打印信息,回收子进程,并是否执行清理套接字操作
    void (*clear)(const char*, int) = [](const char* tips, int sockfd = -1) {
    // 打印提示的错误信息tips
    printf("%s\\n", tips);

    // 判断是否清理已经创建的套接字sockfd
    if (sockfd > 0)
    close(sockfd);

    // 退出主进程先阻塞回收所有已终止的子线程,然后退出主线程,还在运行的子线程退出交给内核管理
    int pid; // 保存子进程pid
    int status; // 子进程状态
    while ((pid = waitpid(-1, &status, 0)) > 0)
    printf("子进程%d退出,退出状态%d!\\n", pid, WEXITSTATUS(status));
    };

    // 主进程信号回调函数,包括SIGINT信号
    struct sigaction sigact;
    sigact.sa_handler = [](int signal) {
    switch (signal) {
    case SIGINT:
    // 直接跳转恢复堆栈状态,并传出SIGINT信号
    siglongjmp(environment_2, signal);
    break;
    default:
    break;
    }
    };
    sigact.sa_flags = SA_RESTART;
    sigemptyset(&sigact.sa_mask); // 信号屏蔽位置空,默认只屏蔽自身
    // 注册信号和信号处理函数
    sigaction(SIGINT, &sigact, NULL);

    // 开始socket编程
    // 1.主进程的主线程服务器创建用于监听的套接字socket
    int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    // 创建socket失败,直接执行清理函数退出
    if (listen_sockfd == -1) {
    char buf[64];
    sprintf(buf, "server:%d socket() error!", getpid()); // 打印错误

    // 执行V操作,避免子进程无限制卡住
    struct sembuf sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_op = +1; // V操作
    sem_buf.sem_flg = 0; // 默认采取阻塞方式
    semop(semmid, &sem_buf, 1); // 执行V操作

    clear(buf, listen_sockfd);// 退出函数
    return;
    }
    // 服务器创建socket成功后,设置socket的属性为可重用地址,实现出错时服务器可重用实现快速重启
    int opt = 1; // 表示启用SO_REUSEAADDR
    int ret = setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    ret = setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
    if (ret == -1) {
    char buf[64];
    sprintf(buf, "server:%d socket() error!", getpid()); // 打印错误

    // 执行V操作,避免子进程无限制卡住
    struct sembuf sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_op = +1; // V操作
    sem_buf.sem_flg = 0; // 默认采取阻塞方式
    semop(semmid, &sem_buf, 1); // 执行V操作

    clear(buf, listen_sockfd); // 退出函数
    return;
    }

    // 2.主进程服务器绑定监听套接字和本地地址
    struct sockaddr_in sockaddr;
    sockaddr.sin_family = AF_INET; // 指定协议族IPV4
    sockaddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 点分十进制的0.0.0.0表示本地任意地址
    sockaddr.sin_port = 10086; // 选择任意一个注册端口号
    ret = bind(listen_sockfd, (struct sockaddr*)&sockaddr, sizeof(sockaddr));
    // 绑定bind失败,直接执行清理函数退出
    if (ret == -1) {
    char buf[64];
    sprintf(buf, "server:%d bind() error!", getpid()); // 打印错误

    // 执行V操作,避免子进程无限制卡住
    struct sembuf sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_op = +1; // V操作
    sem_buf.sem_flg = 0; // 默认采取阻塞方式
    semop(semmid, &sem_buf, 1); // 执行V操作

    clear(buf, listen_sockfd); // 退出函数
    return;
    }

    //sleep(5);
    // 3.主进程服务器进行监听客户端的连接
    ret = listen(listen_sockfd, 16);
    // 监听listen失败,直接执行清理函数退出
    if (ret == -1) {
    char buf[64];
    sprintf(buf, "server:%d listen() error!", getpid()); // 打印错误

    // 执行V操作,避免子进程无限制卡住
    struct sembuf sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_op = +1; // V操作
    sem_buf.sem_flg = 0; // 默认采取阻塞方式
    semop(semmid, &sem_buf, 1); // 执行V操作

    clear(buf, listen_sockfd); // 退出函数
    return;
    }
    printf("server:main process:%d main pthread:%ld listening(port:%d)……..\\n", getpid(), pthread_self(), sockaddr.sin_port);
    // 服务器主进程初始化完毕,执行信号量V操作唤醒子进程客户端
    struct sembuf sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_op = +1; // V操作
    sem_buf.sem_flg = 0; // 默认采取阻塞方式
    semop(semmid, &sem_buf, 1); // 执行V操作

    // 构建传递数据的连接客户端的结构体
    struct CLIENTINFO {
    int communicate_sockfd; // 与客户端通信的套接字文件描述符
    char ip[32]; // IPV4地址
    unsigned short port; // port端口号
    };
    // 创建服务器子线程与客户端通信的任务函数
    void* (*pthread_work)(void*) = [](void* pclient_info) {

    // 还原参数数据
    CLIENTINFO* client_info = (CLIENTINFO*)pclient_info; // 传递的客户端信息

    // 子线程与客户端通信
    printf("server's son pthread:%ld connect client:%s:%d success! communicate socket:%d.\\n", pthread_self(), client_info->ip, client_info->port, client_info->communicate_sockfd);

    // 子线程与主线程连接成功的客户端循环通信,假设是一个长连接的场景
    while (1) {
    // 5.服务器等待客户端的信息,这里需要粘包处理
    char rdbuf[1024]; // 读取缓冲区
    // 清理读缓冲区数据,以避免影响下次数据的接收
    memset(rdbuf, 0, sizeof(rdbuf));
    // 采用read函数等待读取客户端发送数据
    int rdbyte = read(client_info->communicate_sockfd, rdbuf, sizeof(rdbuf));
    // 根据返回值判断状态,执行相应操作
    if (rdbyte == -1) {
    // read函数返回-1,表示连接出现错误,直接关闭和客户端的通信
    printf("server's son pthread:%ld connect client:%s:%d error, server disconnect!\\n", pthread_self(), client_info->ip, client_info->port);
    close(client_info->communicate_sockfd);
    break; // 直接进行下个客户端的连接
    }
    else if (rdbyte == 0) {
    // read函数返回0,表示客户端断开连接,直接关闭和客户端的通信
    printf("client:%s:%d disconnect, server's son pthread:%ld disconnect!\\n", client_info->ip, client_info->port, pthread_self());
    close(client_info->communicate_sockfd);
    break; // 直接进行下个客户端的连接
    }
    else
    printf("server's son pthread:%ld receive client:%s:%d numbers %d data:%s.\\n", pthread_self(), client_info->ip, client_info->port, rdbyte, rdbuf);

    sleep(2);

    // 6.服务器成功读取客户端的信息,进行处理然后使用发送数据给客户端
    char wrbuf[] = "Hello World form server"; // 服务器发送给客户端的数据
    int wrbyte = write(client_info->communicate_sockfd, wrbuf, sizeof(wrbuf));
    // 根据返回值判断状态,执行相应操作
    if (wrbyte == -1) {
    // write函数返回-1,表示连接出现错误,直接关闭和客户端的通信
    printf("server's son pthread:%ld connect client:%s:%d error, server disconnect!\\n", pthread_self(), client_info->ip, client_info->port);
    close(client_info->communicate_sockfd);
    break; // 直接进行下个客户端的连接
    }
    else
    printf("server's son pthread:%ld send client:%s:%d number %d data.\\n", pthread_self(), client_info->ip, client_info->port, wrbyte);
    }
    // new创建的数据传递,子线程使用完后释放
    delete client_info;
    // 子线程退出
    pthread_exit(NULL);

    return (void*)NULL;
    };

    // 循环接收客户端的连接,并进行排队通信,这里需要并发处理
    while (1) {
    // 保存堆栈状态,保证在ctrl+c时跳转后,直接终止服务器循环
    ret = sigsetjmp(environment_2, 1); // 保存堆栈状态
    if (ret)
    break; // 堆栈返回值出现错误时,直接退出服务器

    // 4.接收客户段的连接
    struct sockaddr_in client_addr; // 传出参数,用于保存连接的客户端地址信息
    memset(&client_addr, 0, sizeof(client_addr)); // 清空保存的客户端数据
    socklen_t len = sizeof(client_addr); // 传出参数,client_addr长度
    int communciate_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &len);
    // 检查返回的通信套接字成功,失败直接退出与该客户端的连接,成功则继续进行通信
    if (communciate_sockfd == -1)
    continue; // 直接进行下个客户端的连接

    // 采取创建服务器创建子线程与客户端进行通信,实现服务器高并发
    pthread_t stid; // 子线程tid
    // 保存客户端信息传递给子线程
    CLIENTINFO* pclient = new CLIENTINFO;
    pclient->communicate_sockfd = communciate_sockfd;
    pclient->port = ntohs(client_addr.sin_port);
    strncpy(pclient->ip, inet_ntoa(client_addr.sin_addr), sizeof(pclient->ip));
    // 创建子线程与客户端通信
    int ret = pthread_create(&stid, NULL, pthread_work, pclient);
    if (ret != 0) {
    printf("server main process:%d main pthread:%d create son pthread:%ld failed, disconnect client:%s:%d! close communicate socket:%d.\\n", getpid(), pthread_self(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), communciate_sockfd);
    // 关闭通信套接字,直接进行下个客户端的连接
    close(communciate_sockfd);
    continue;
    }
    // 服务器主线程主动分离子线程交给内核管理
    pthread_detach(stid);
    }
    // 服务器运行结束前,清理资源
    close(listen_sockfd); // 关闭监听套接字

    // 由于是主进程,主进程运行结束前,阻塞回收所有子进程
    int spid;
    while ((spid = waitpid(-1, NULL, 0)) > 0)
    printf("main process:%d recycle son process:%d\\n", getpid(), spid);

    // 主进程创建的信号量在所有子进程结束后才回收信号量
    semctl(semmid, 0, IPC_RMID); // 删除信号量

    // 主进程的主线程服务器运行结束,采用pthread_exit退出,不回收资源,避免子线程使用主进程资源出错
    pthread_exit(0);
    }
    else if (pid == 0) {

    // 执行信号量的P操作,避免出现客户端子进程比服务器先运行的情况
    sembuf sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_op = -1; // P操作
    sem_buf.sem_flg = 0; // 默认阻塞方式
    semop(semmid, &sem_buf, 1); // 执行P操作

    // 子进程模拟创建多个并发的多进程客户端进行与服务器进行通信
    client(2, 1); // 创建2+1个客户端子进程,每个客户端只发送1次消息就退出,便于观察
    }
    else
    printf("fork() error!\\n");
    }

    2.fork进程池+多线程高并发服务器实现方式

    基本思想:fork进程池+多线程并发,在fork进程池的基础上,对其每个工作子进程负责连接客户端,每个连接成功后的客户端创建一个子线程与其进行通信。可以认为是“预fork进程池的多进程并发服务器方式+多线程并发服务器实现方式”。相比于fork进程池,改变的代码块如下:在每个工作子进程中,改进工作子进程的主线程用于监听竞争accpet客户端的连接,连接客户端成功后,创建工作子进程的子线程与客户端通信。 fork进程池的工作子进程任务逻辑:

    // fork进程池中的工作子进程任务
    void (*server_forkprocess_work)(int,void*(*)(void*)) = [](int listen_sockfd, void* (*pthread_work)(void*)) {
    // 工作子线程循环与客户端连接、通信,由内核调用accept函数时实现子线程间的同步和文件描述符的分配
    while (1) {
    // 4.工作子进程的主线程接收客户段的连接
    struct sockaddr_in client_addr; // 传出参数,用于保存连接的客户端地址信息
    memset(&client_addr, 0, sizeof(client_addr)); // 清空保存的客户端数据
    socklen_t len = sizeof(client_addr); // 传出参数,client_addr长度
    int communciate_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &len);
    // 检查返回的通信套接字成功,失败直接退出与该客户端的连接,成功则继续进行通信
    if (communciate_sockfd == -1)
    continue; // 连接失败,直接进行下个客户端的连接

    // 创建工作子进程的子线程进行客户端的通信,通信完毕后就销毁子线程
    pthread_t stid; // 子线程tid
    // 保存客户端信息传递给子线程
    CLIENTINFO* pclient = new CLIENTINFO;
    pclient->communicate_sockfd = communciate_sockfd;
    pclient->port = ntohs(client_addr.sin_port);
    strncpy(pclient->ip, inet_ntoa(client_addr.sin_addr), sizeof(pclient->ip));
    // 创建子线程与客户端通信
    int ret = pthread_create(&stid, NULL, pthread_work, pclient);
    if (ret != 0) {
    printf("server son process:%d main pthread:%ld create son pthread:%ld failed, disconnect client:%s:%d! close communicate socket:%d.\\n", getpid(), pthread_self(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), communciate_sockfd);
    // 关闭通信套接字,直接进行下个客户端的连接
    close(communciate_sockfd);
    continue;
    }
    // 服务器主线程主动分离子线程交给内核管理
    pthread_detach(stid);
    }

    // fork进程池中的工作子进程一般不会主动退出
    exit(0);
    };

    工作子进程的与客户端通信的子线程工作函数:


    // 构建传递数据的连接客户端的结构体
    struct CLIENTINFO {
    int communicate_sockfd; // 与客户端通信的套接字文件描述符
    char ip[32]; // IPV4地址
    unsigned short port; // port端口号
    };
    // 创建服务器子线程与客户端通信的任务函数
    void* (*pthread_work)(void*) = [](void* pclient_info) {

    // 还原参数数据
    CLIENTINFO* client_info = (CLIENTINFO*)pclient_info; // 传递的客户端信息

    // 子线程与客户端通信
    printf("server son process:%d son pthread:%ld connect client:%s:%d success! communicate socket:%d.\\n", getpid(), pthread_self(), client_info->ip, client_info->port, client_info->communicate_sockfd);

    // 子线程与主线程连接成功的客户端循环通信,假设是一个长连接的场景
    while (1) {
    // 5.服务器等待客户端的信息,这里需要粘包处理
    char rdbuf[1024]; // 读取缓冲区
    // 清理读缓冲区数据,以避免影响下次数据的接收
    memset(rdbuf, 0, sizeof(rdbuf));
    // 采用read函数等待读取客户端发送数据
    int rdbyte = read(client_info->communicate_sockfd, rdbuf, sizeof(rdbuf));
    // 根据返回值判断状态,执行相应操作
    if (rdbyte == -1) {
    // read函数返回-1,表示连接出现错误,直接关闭和客户端的通信
    printf("server son process:%d son pthread:%ld connect client:%s:%d error, server disconnect!\\n", getpid(), pthread_self(), client_info->ip, client_info->port);
    close(client_info->communicate_sockfd);
    break; // 直接进行下个客户端的连接
    }
    else if (rdbyte == 0) {
    // read函数返回0,表示客户端断开连接,直接关闭和客户端的通信
    printf("client:%s:%d disconnect, server son process:%d son pthread:%ld disconnect!\\n", client_info->ip, client_info->port, getpid(), pthread_self());
    close(client_info->communicate_sockfd);
    break; // 直接进行下个客户端的连接
    }
    else
    printf("server son process:%d son pthread:%ld receive client:%s:%d numbers %d data:%s.\\n", getpid(), pthread_self(), client_info->ip, client_info->port, rdbyte, rdbuf);

    sleep(2);

    // 6.服务器成功读取客户端的信息,进行处理然后使用发送数据给客户端
    char wrbuf[] = "Hello World form server"; // 服务器发送给客户端的数据
    int wrbyte = write(client_info->communicate_sockfd, wrbuf, sizeof(wrbuf));
    // 根据返回值判断状态,执行相应操作
    if (wrbyte == -1) {
    // write函数返回-1,表示连接出现错误,直接关闭和客户端的通信
    printf("server son process:%d son pthread:%ld connect client:%s:%d error, server disconnect!\\n", getpid(), pthread_self(), client_info->ip, client_info->port);
    close(client_info->communicate_sockfd);
    break; // 直接进行下个客户端的连接
    }
    else
    printf("server son process:%d son pthread:%ld send client:%s:%d number %d data.\\n", getpid(), pthread_self(), client_info->ip, client_info->port, wrbyte);
    }
    // new创建的数据传递,子线程使用完后释放
    delete client_info;
    // 子线程退出
    pthread_exit(NULL);

    return (void*)NULL;
    };

    完整实现:

    #include <stdio.h>//提供用于输入输出的函数,如printf()、scanf()、fprintf()、fscanf(),包含了文件操作的一些函数,如fopen()、fclose()、fread()、fwrite()等。
    #include <stdlib.h>//提供各种通用的工具函数,如内存分配(malloc()、calloc()、realloc()、free())、随机数生成(rand()、srand())、环境查询(getenv())、程序控制(exit()、system())。
    #include <unistd.h>//提供对POSIX操作系统API的访问包括sleep函数,主要用于Unix-like系统(如Linux、macOS),在Windows系统上不可用,因为它是Unix特有的。
    #include <string.h>//提供用于处理C风格字符串(即以'\\0'结尾的字符数组)的函数,字符串复制(strcpy())、连接(strcat())、比较(strcmp())、长度计算(strlen())等函数。
    // 线程相关
    #include <pthread.h>//提供了一套创建和管理线程以及线程间同步的机制,使得开发者能够在Unix-like系统(如Linux和macOS)上实现多线程编程,具体实现在动态库libpthread.so中
    #include <semaphore.h> //包括信号量sem_t
    // 进程相关
    #include <sys/resource.h> // 进程优先级相关函数
    #include <sys/wait.h> // 进程等待函数
    #include <sys/types.h> // 类型定义
    #include <signal.h> // 信号函数
    // socket网络编程相关
    #include <arpa/inet.h>
    #include <sys/socket.h> //Socket编程的数据结构和函数
    #include <sys/select.h> //Socket编程IO复用的select方式
    #include <sys/epoll.h> //Socket编程IO复用的epoll方式
    #include<fcntl.h> //文件属性控制

    #define MAX_PROCESS 3 // 预fork进程工作池的工作子线程数量
    void multipthread2_TCP_server_client() {
    // 创建信号量集实现主进程服务器和子进程客户端进行先后同步关系,避免出现客户端先运行,服务器后运行,直接出现问题
    int semmid = semget(ftok(".", 1), 1, 0666 | IPC_CREAT); // 创建信号量
    if (semmid == -1)
    return; // 创建失败直接返回
    semctl(semmid, 0, SETVAL, 0); // 信号量初始值设置为0

    int pid = fork(); // 创建子进程
    if (pid > 0) {
    // 主进程的第一个线程即为主线程
    printf("main process:%d main pthread:%ld start server!\\n", getpid(), pthread_self());

    // lambda表达式创建清理函数,用于退出前打印信息,回收子进程,并是否执行清理套接字操作
    void (*clear)(const char*, int) = [](const char* tips, int sockfd = -1) {
    // 打印提示的错误信息tips
    printf("%s\\n", tips);

    // 判断是否清理已经创建的套接字sockfd
    if (sockfd > 0)
    close(sockfd);

    // 退出主进程先阻塞回收所有已终止的子线程,然后退出主线程,还在运行的子线程退出交给内核管理
    int pid; // 保存子进程pid
    int status; // 子进程状态
    while ((pid = waitpid(-1, &status, 0)) > 0)
    printf("子进程%d退出,退出状态%d!\\n", pid, WEXITSTATUS(status));
    };

    // 主进程信号回调函数,包括SIGINT信号
    struct sigaction sigact;
    sigact.sa_handler = [](int signal) {
    switch (signal) {
    case SIGINT:
    // 直接跳转恢复堆栈状态,并传出SIGINT信号
    siglongjmp(environment_2, signal);
    break;
    default:
    break;
    }
    };
    sigact.sa_flags = SA_RESTART;
    sigemptyset(&sigact.sa_mask); // 信号屏蔽位置空,默认只屏蔽自身
    // 注册信号和信号处理函数
    sigaction(SIGINT, &sigact, NULL);

    // 开始socket编程
    // 1.主进程的主线程服务器创建用于监听的套接字socket
    int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    // 创建socket失败,直接执行清理函数退出
    if (listen_sockfd == -1) {
    char buf[64];
    sprintf(buf, "server:%d socket() error!", getpid()); // 打印错误

    // 执行V操作,避免子进程无限制卡住
    struct sembuf sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_op = +1; // V操作
    sem_buf.sem_flg = 0; // 默认采取阻塞方式
    semop(semmid, &sem_buf, 1); // 执行V操作

    clear(buf, listen_sockfd);// 退出函数
    return;
    }
    // 服务器创建socket成功后,设置socket的属性为可重用地址,实现出错时服务器可重用实现快速重启
    int opt = 1; // 表示启用SO_REUSEAADDR
    int ret = setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    ret = setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
    if (ret == -1) {
    char buf[64];
    sprintf(buf, "server:%d socket() error!", getpid()); // 打印错误

    // 执行V操作,避免子进程无限制卡住
    struct sembuf sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_op = +1; // V操作
    sem_buf.sem_flg = 0; // 默认采取阻塞方式
    semop(semmid, &sem_buf, 1); // 执行V操作

    clear(buf, listen_sockfd); // 退出函数
    return;
    }

    // 2.主进程服务器绑定监听套接字和本地地址
    struct sockaddr_in sockaddr;
    sockaddr.sin_family = AF_INET; // 指定协议族IPV4
    sockaddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 点分十进制的0.0.0.0表示本地任意地址
    sockaddr.sin_port = 10086; // 选择任意一个注册端口号
    ret = bind(listen_sockfd, (struct sockaddr*)&sockaddr, sizeof(sockaddr));
    // 绑定bind失败,直接执行清理函数退出
    if (ret == -1) {
    char buf[64];
    sprintf(buf, "server:%d bind() error!", getpid()); // 打印错误

    // 执行V操作,避免子进程无限制卡住
    struct sembuf sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_op = +1; // V操作
    sem_buf.sem_flg = 0; // 默认采取阻塞方式
    semop(semmid, &sem_buf, 1); // 执行V操作

    clear(buf, listen_sockfd); // 退出函数
    return;
    }

    //sleep(5);
    // 3.主进程服务器进行监听客户端的连接
    ret = listen(listen_sockfd, 16);
    // 监听listen失败,直接执行清理函数退出
    if (ret == -1) {
    char buf[64];
    sprintf(buf, "server:%d listen() error!", getpid()); // 打印错误

    // 执行V操作,避免子进程无限制卡住
    struct sembuf sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_op = +1; // V操作
    sem_buf.sem_flg = 0; // 默认采取阻塞方式
    semop(semmid, &sem_buf, 1); // 执行V操作

    clear(buf, listen_sockfd); // 退出函数
    return;
    }
    printf("server:main process:%d main pthread:%ld listening(port:%d)……..\\n", getpid(), pthread_self(), sockaddr.sin_port);
    // 服务器主进程初始化完毕,执行信号量V操作唤醒子进程客户端
    struct sembuf sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_op = +1; // V操作
    sem_buf.sem_flg = 0; // 默认采取阻塞方式
    semop(semmid, &sem_buf, 1); // 执行V操作

    // 构建传递数据的连接客户端的结构体
    struct CLIENTINFO {
    int communicate_sockfd; // 与客户端通信的套接字文件描述符
    char ip[32]; // IPV4地址
    unsigned short port; // port端口号
    };
    // 创建服务器子线程与客户端通信的任务函数
    void* (*pthread_work)(void*) = [](void* pclient_info) {

    // 还原参数数据
    CLIENTINFO* client_info = (CLIENTINFO*)pclient_info; // 传递的客户端信息

    // 子线程与客户端通信
    printf("server son process:%d son pthread:%ld connect client:%s:%d success! communicate socket:%d.\\n", getpid(), pthread_self(), client_info->ip, client_info->port, client_info->communicate_sockfd);

    // 子线程与主线程连接成功的客户端循环通信,假设是一个长连接的场景
    while (1) {
    // 5.服务器等待客户端的信息,这里需要粘包处理
    char rdbuf[1024]; // 读取缓冲区
    // 清理读缓冲区数据,以避免影响下次数据的接收
    memset(rdbuf, 0, sizeof(rdbuf));
    // 采用read函数等待读取客户端发送数据
    int rdbyte = read(client_info->communicate_sockfd, rdbuf, sizeof(rdbuf));
    // 根据返回值判断状态,执行相应操作
    if (rdbyte == -1) {
    // read函数返回-1,表示连接出现错误,直接关闭和客户端的通信
    printf("server son process:%d son pthread:%ld connect client:%s:%d error, server disconnect!\\n", getpid(), pthread_self(), client_info->ip, client_info->port);
    close(client_info->communicate_sockfd);
    break; // 直接进行下个客户端的连接
    }
    else if (rdbyte == 0) {
    // read函数返回0,表示客户端断开连接,直接关闭和客户端的通信
    printf("client:%s:%d disconnect, server son process:%d son pthread:%ld disconnect!\\n", client_info->ip, client_info->port, getpid(), pthread_self());
    close(client_info->communicate_sockfd);
    break; // 直接进行下个客户端的连接
    }
    else
    printf("server son process:%d son pthread:%ld receive client:%s:%d numbers %d data:%s.\\n", getpid(), pthread_self(), client_info->ip, client_info->port, rdbyte, rdbuf);

    sleep(2);

    // 6.服务器成功读取客户端的信息,进行处理然后使用发送数据给客户端
    char wrbuf[] = "Hello World form server"; // 服务器发送给客户端的数据
    int wrbyte = write(client_info->communicate_sockfd, wrbuf, sizeof(wrbuf));
    // 根据返回值判断状态,执行相应操作
    if (wrbyte == -1) {
    // write函数返回-1,表示连接出现错误,直接关闭和客户端的通信
    printf("server son process:%d son pthread:%ld connect client:%s:%d error, server disconnect!\\n", getpid(), pthread_self(), client_info->ip, client_info->port);
    close(client_info->communicate_sockfd);
    break; // 直接进行下个客户端的连接
    }
    else
    printf("server son process:%d son pthread:%ld send client:%s:%d number %d data.\\n", getpid(), pthread_self(), client_info->ip, client_info->port, wrbyte);
    }
    // new创建的数据传递,子线程使用完后释放
    delete client_info;
    // 子线程退出
    pthread_exit(NULL);

    return (void*)NULL;
    };

    // fork进程池中的工作子进程任务
    void (*server_forkprocess_work)(int,void*(*)(void*)) = [](int listen_sockfd, void* (*pthread_work)(void*)) {
    // 工作子线程循环与客户端连接、通信,由内核调用accept函数时实现子线程间的同步和文件描述符的分配
    while (1) {
    // 4.工作子进程的主线程接收客户段的连接
    struct sockaddr_in client_addr; // 传出参数,用于保存连接的客户端地址信息
    memset(&client_addr, 0, sizeof(client_addr)); // 清空保存的客户端数据
    socklen_t len = sizeof(client_addr); // 传出参数,client_addr长度
    int communciate_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &len);
    // 检查返回的通信套接字成功,失败直接退出与该客户端的连接,成功则继续进行通信
    if (communciate_sockfd == -1)
    continue; // 连接失败,直接进行下个客户端的连接

    // 创建工作子进程的子线程进行客户端的通信,通信完毕后就销毁子线程
    pthread_t stid; // 子线程tid
    // 保存客户端信息传递给子线程
    CLIENTINFO* pclient = new CLIENTINFO;
    pclient->communicate_sockfd = communciate_sockfd;
    pclient->port = ntohs(client_addr.sin_port);
    strncpy(pclient->ip, inet_ntoa(client_addr.sin_addr), sizeof(pclient->ip));
    // 创建子线程与客户端通信
    int ret = pthread_create(&stid, NULL, pthread_work, pclient);
    if (ret != 0) {
    printf("server son process:%d main pthread:%ld create son pthread:%ld failed, disconnect client:%s:%d! close communicate socket:%d.\\n", getpid(), pthread_self(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), communciate_sockfd);
    // 关闭通信套接字,直接进行下个客户端的连接
    close(communciate_sockfd);
    continue;
    }
    // 服务器主线程主动分离子线程交给内核管理
    pthread_detach(stid);
    }

    // fork进程池中的工作子进程一般不会主动退出
    exit(0);
    };

    // 采用fork进程池,创建多个工作子进程进行客户端的连接、通信
    for(int i=0;i<MAX_PROCESS;i++) {

    int pid_c = fork(); // 创建工作子进程
    if (pid_c == 0) {
    // 工作子进程,直接运行子进程任务函数:循环连接客户端,连接成功后创建子线程通信
    server_forkprocess_work(listen_sockfd, pthread_work);
    }
    else if (pid_c > 0) {
    printf("server:%d create son process:%d add fork_pool:%d.\\n", getpid(), pid_c, i + 1);
    continue; // 服务器主进程直接进行fork进程池的下个工作子进程创建
    }
    else {
    printf("server fork() son process fail.\\n");
    // 服务器创建子进程失败,直接关闭与客户端通信的套接字,继续进行下一个客户端的连接
    continue; // 服务器主进程直接进行fork进程池的下个工作子进程创建
    }
    }

    // 在保证服务器比客户端先运行后,客户端使用完后,直接删除信号量
    semctl(semmid, 0, IPC_RMID); // 删除信号量

    // fork进程池方式,主进程服务器用于监测工作子进程状态,在发现子进程异常退出时,重新创建工作子进程
    while (1) {
    int spid;
    while ((spid = waitpid(-1, NULL, 0)) > 0) {
    printf("main process:%d recycle son process:%d\\n", getpid(), spid);
    if (spid != pid) {
    // 重新创建工作子进程
    int new_pid = fork();
    if (new_pid == 0)
    server_forkprocess_work(listen_sockfd, pthread_work); // 新创建的工作子进程
    else if (new_pid)
    printf("main process:%d create new son process:%d.\\n", getpid(), new_pid);
    else
    printf("main process fork() error!\\n");
    }
    }
    }

    }
    else if (pid == 0) {
    // 执行信号量的P操作,避免出现客户端子进程比服务器先运行的情况
    sembuf sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_op = -1; // P操作
    sem_buf.sem_flg = 0; // 默认阻塞方式
    semop(semmid, &sem_buf, 1); // 执行P操作

    // 子进程模拟创建多个并发的多进程客户端进行与服务器进行通信
    client(2, 1); // 创建2+1个客户端子进程,每个客户端只发送1次消息就退出,便于观察
    }
    else
    printf("fork() error!\\n");
    }

    优缺点

    优点
  • 高响应性:服务器可以同时处理多个客户端请求。当一个线程被阻塞(例如等待I/O操作完成)时,其他线程可以继续处理其他请求,提高了整体的响应速度和吞吐量。
  • 资源利用率高:相比于为每个请求创建新进程的多进程模型,线程更轻量级,创建和切换的开销较小,能更高效地利用系统资源(尤其是CPU多核资源)。
  • 共享数据方便:同一进程内的线程共享内存空间,使得数据(如缓存、连接池)的共享和通信相对简单直接。
  • 开发模型相对成熟:线程编程模型已被广泛研究和应用,有成熟的库和框架支持。
  • 缺点
  • 线程安全问题:多个线程并发访问共享资源(如全局变量、数据库连接)时,需要引入同步机制(如锁、信号量),否则会导致数据不一致、竞态条件等问题。编程复杂,容易出错。
  • 资源消耗:虽然比进程轻量,但每个线程仍需独立的栈空间(通常数MB)和内核资源。大量线程会消耗可观的内存和CPU调度资源。
  • 调度开销:线程数量过多时,线程间的上下文切换会带来显著的CPU开销,可能反而降低性能。
  • 阻塞影响:如果一个线程执行了耗时操作或阻塞操作,它所在的线程可能无法及时释放,影响整体效率。需要谨慎设计任务分配。
  • 调试困难:多线程程序的调试和问题定位(如死锁、活锁)比单线程程序更复杂。

  • 三、 IO多路复用方式实现高并发服务器

            IO多路复用是一种高效的输入输出处理技术,允许单个线程或进程同时监控多个文件描述符(如套接字),并在其中任何一个准备好进行读、写或错误操作时通知程序。这种技术避免了为每个IO操作创建单独的线程,从而节省系统资源,提高并发性能。它常用于网络服务器、数据库系统等需要处理大量并发连接的应用场景。常见的实现方式包括select、poll、epoll(Linux)等。

    相比于多进程或多线程实现的并发服务器的优缺点

    优点
  • 资源消耗低:I/O多路复用(如 select, poll, epoll)通常在单个进程/线程中处理多个连接。这避免了为每个连接创建新线程或进程的开销(如内存、上下文切换),系统资源利用率更高。
  • 可扩展性好:由于资源消耗主要与连接数线性相关而非指数级增长(如线程数=连接数),因此能更好地支持高并发连接(如 C10K 问题)。
  • 避免锁竞争:单线程模型(如 Reactor 模式)避免了多线程编程中复杂的锁竞争、死锁等问题,编程模型相对简单。
  • 无上下文切换开销:单线程内通过事件循环处理多个连接,没有线程/进程间切换的开销。
  • 缺点
  • CPU密集型任务处理能力弱:如果单个请求涉及大量计算(CPU密集型),单线程会被阻塞,导致所有连接响应变慢。多线程/进程可以利用多核并行处理此类任务。
  • 编程复杂度较高:事件驱动模型(回调、状态机)的代码逻辑可能比直观的线性线程代码更难编写、调试和维护。
  • 响应不及时的风险:如果一个请求处理时间过长(如阻塞在某个慢速操作),会延迟后续所有请求的处理。
  • 单点故障:单线程进程崩溃会导致所有连接中断,而多线程/进程模型可能只影响部分连接。

  • 基本思路

            ET模式下的epoll方式实现并发服务器可以实现百万级,IO多路复用只介绍这种方式。

            本文只介绍一下能实现百万级并发的ET模式的epoll方式实现高并发服务器,对于其他IO多路复用方式,包括:select方式、poll方式、epoll方式(LT模式)实现的高并发服务器可以查看这篇文章:Socket编程入门:IO多路复用方式实现高并发服务器。         采用ET模式的epoll实现百万级并发服务器的基本思路:先遵循TCP服务器构建的基本流程,sokcet()创建监听文件描述符用于接收客户端的连接请求、bind()绑定监听文件描述符和本地服务器IP地址和端口号、listen()监听客户端的连接请求。然后服务器在采用accpet()、read()、write()这3类IO系统调用时可能会发生阻塞,采取使用epoll方式先进行探路,检查文件描述符的就绪状态,就绪后才进行IO系统调用,这样服务器就不会阻塞。具体流程如下:

    服务器端:ET模式    1.使用socket()函数创建服务器用于监听的套接字listen_sockfd,在ET模式下采用fcntl函数将监听文件描述符为非阻塞O_NONBLOCK方式。    2.使用bind()函数将监听的套接字listen_sockfd和服务器本地的IP地址和端口号绑定    3.使用listen()给监听的套接字文件描述符listen_sockfd设置监听    4.使用epoll_create函数创建一个epoll文件描述符,其用于保存所有待观察的文件描述符和相应的事件使用epoll_ctl函数添加监听套接字文件描述符listen_sockfd及其相应的事件epoll_event,events保存ET读事件检查,data保存数据如监听文件描述符。    5.循环使用epoll_wait函数,周期性的对添加到epoll实例所有的文件描述符进行相应事件的检测,服务器线程等待epoll_wait() 解除阻塞返回,得到内核传出的满足相应读事件就绪的套接字文件描述符结构体struct epoll_event数组events,遍历这个events数组逐个进行以下处理。      ①判断event是否为读事件就绪,在读事件就绪的情况下        (1)检查文件描述符为监听文件描述符时,此时有客户端发起连接服务器。在ET模式下,需要循环accept读取所有连接。采用while循环调用accept函数接收客户端的连接,直到返回-1和errno为EAGAIN或EWOULDBLOCK,每链接成功一个客户端,epoll_ctl添加客户端的epoll_event事件在epoll文件描述符中,events设置为服务器监听ET读事件就绪,data并保存客户端信息。并将与该客户端通信的文件描述符为O_NONBLOCK非阻塞方式。        (2)检查文件描述符为通信文件描述符时,此时有已经连接成功的客户端给服务器发送信息。在ET模式下,需要循环read读取完缓冲区内所有数据。           ①采用while循环调用read函数读取内核接收缓冲区的数据,直到返回-1和errno为EAGAIN或EWOULDBLOCK,采用固定大小buf的用户缓冲区读取数据,每次都将读取的buf数据放入放在malloc开辟的足够大的堆区缓冲区中保存。循环过程中read出现下述返回值时:            『1』返回=0,客户端断开连接,则服务器也断开连接并回收与客户端通信的数据,必须先使用epoll_ctl()将该通信的套接字文件描述符connect_sockfd从epoll实例中删除,再关闭该套接字文件描述符(顺序搞反epoll_ctl函数会报错),最后回收相关保存与该客户端通信的数据,回收recv_data数据,退出循环读取。            『2』返回>0,rbuf本次循环成功读取数据,将数据保存在malloc开辟的足够大的堆区缓冲区中。            『3』返回<0,接收客户端发送的数据失败/读取完,errno==EAGAIN或EWOULDBLOCK时,服务器正常循环读取完所有内核缓冲区数据,退出循环读取, 否则服务器接收数据失败,并且释放资源,包括该通信的套接字文件描述符connect_sockfd从epoll实例中删除,再关闭该套接字文件描述符(顺序搞反epoll_ctl函数会报错),最后回收相关保存与该客户端通信的数据,回收堆区数据,退出循环读取。          ②在①中实现服务器循环read读取客户端的数据成功后,服务器处理数据。将数据发送给客户端。在ET模式下,需要通过以下方式保证服务器可以发送完所有数据。先尝试使用一次write函数将带发送的data_len字节的用户缓冲区数据buf发送给客户端,检查write函数返回值和发送数据长度len。            『1』返回值>0,发送数据成功,还需要检查是否全部发送完毕。                1)发送字节数len<待发送数据buf字节数data_len时,未发送完毕,修改该通信文件描述符在epoll文件描述符中观察的事件添加ET写事件。这样保证在ET模式下,下次epoll_wait可以传出该通信文件描述符的写就绪事件,以将剩余数据继续发送。因此需要保存待发送的数据保存在struct event_epoll中的用户数据data的ptr指针中,events同时监听ET读事件和ET写事件,采用epoll_ctl函数设置。                2)发送字节数len=待发送数据buf字节数data_len时,发送完毕。            『2』返回值=-1,发送数据失败,在非阻塞文件描述符,需要先检查是否是内核发送缓冲区满、还是其他错误                1)errno为EAGAIN或EWOULDBLOCK时,内核发送缓冲区满,此时未发送完毕,执行和1)相同的操作。                2)errno其他错误时,write函数调用失败出现未知错误,此时清理资源,包括该通信的套接字文件描述符connect_sockfd从epoll实例中删除,再关闭该套接字文件描述符(顺序搞反epoll_ctl函数会报错),最后回收相关保存与该客户端通信的数据。        ②判断event是否为写事件就绪,在写事件就绪时,直接可以判断为在①中注册的写事件就绪,需要继续发送服务器剩下未发送完的给客户端数据,类似的先尝试使用一次write函数将带发送的data_len字节的用户缓冲区数据buf发送给客户端,检查write函数返回值和发送数据长度len。            『1』返回值>0,发送数据成功,还需要检查是否全部发送完毕。                1)发送字节数len<待发送数据buf字节数data_len时,未发送完毕,已经添加写事件就绪观察,无需再次添加。这样保证在ET模式下,下次epoll_wait可以传出该通信文件描述符的写就绪事件,以将剩余数据继续发送。因此需要保存待发送的数据保存在struct event_epoll中的用户数据data的ptr指针中,events同时监听ET读事件和ET写事件,采用epoll_ctl函数设置。                2)发送字节数len=待发送数据buf字节数data_len时,发送完毕,需要在epoll中删除该文件描述符的写事件就绪观察。            『2』返回值=-1,发送数据失败,在非阻塞文件描述符,需要先检查是否是内核发送缓冲区满、还是其他错误。                1)errno为EAGAIN或EWOULDBLOCK时,内核发送缓冲区满,此时未发送完毕,执行和1)相同的操作。                2)errno其他错误时,write函数调用失败出现未知错误,此时清理资源,包括该通信的套接字文件描述符connect_sockfd从epoll实例中删除,再关闭该套接字文件描述符(顺序搞反epoll_ctl函数会报错),最后回收相关保存与该客户端通信的数据。     6.重复第5步,实现服务器单线程循环接受多个客户端的连接和通信,在退出服务器前,需要关闭监听文件符和epoll文件描述符。

     epoll方式使用流程:先使用epoll_creat()创建epoll文件描述符,然后采用epoll_ctl()添加要检查的文件描述符,循环采用epoll_wait()获取当前就绪的不会阻塞的文件描述符,对于监听文件描述符就执行accept()操作连接客户端并将连接成功的通信文件描述符采用epoll_ctl添加到epoll文件描述符中、对于与客户端通信的文件描述符就执行read/write操作与客户端通信。

    详细实现(示例代码(详细注释))

    服务器的实现遵循上述ET模式下的epoll方式实现高并发服务器流程,客户端和上述代码均相同。

    完整代码

    #include <stdio.h>//提供用于输入输出的函数,如printf()、scanf()、fprintf()、fscanf(),包含了文件操作的一些函数,如fopen()、fclose()、fread()、fwrite()等。
    #include <stdlib.h>//提供各种通用的工具函数,如内存分配(malloc()、calloc()、realloc()、free())、随机数生成(rand()、srand())、环境查询(getenv())、程序控制(exit()、system())。
    #include <unistd.h>//提供对POSIX操作系统API的访问包括sleep函数,主要用于Unix-like系统(如Linux、macOS),在Windows系统上不可用,因为它是Unix特有的。
    #include <string.h>//提供用于处理C风格字符串(即以'\\0'结尾的字符数组)的函数,字符串复制(strcpy())、连接(strcat())、比较(strcmp())、长度计算(strlen())等函数。
    // 线程相关
    #include <pthread.h>//提供了一套创建和管理线程以及线程间同步的机制,使得开发者能够在Unix-like系统(如Linux和macOS)上实现多线程编程,具体实现在动态库libpthread.so中
    #include <semaphore.h> //包括信号量sem_t
    // 进程相关
    #include <sys/resource.h> // 进程优先级相关函数
    #include <sys/wait.h> // 进程等待函数
    #include <sys/types.h> // 类型定义
    #include <signal.h> // 信号函数
    // socket网络编程相关
    #include <arpa/inet.h>
    #include <sys/socket.h> //Socket编程的数据结构和函数
    #include <sys/select.h> //Socket编程IO复用的select方式
    #include <sys/epoll.h> //Socket编程IO复用的epoll方式
    #include<fcntl.h> //文件属性控制

    // epoll方式的EI工作模式的高并发服务器和客户端
    sigjmp_buf env_epollet; // 保存服务器状态
    void epollET_TCP_server_client() {
    // 创建条件变量
    int semmid = semget(ftok(".", 1), 1, 0666 | IPC_CREAT);
    if (semmid == -1) {
    printf("semget() error.\\n");
    return;
    }
    // 初始化条件变量
    semctl(semmid, 0, SETVAL, 0); // 条件变量初始化为0

    // 创建子进程,主进程运行服务器,子进程运行并发客户端
    int pid = fork();
    if (pid > 0) {
    // 主进程运行服务器
    printf("main process:%d start server!\\n", getpid());

    // 注册信号和处理函数,主要包括两个信号:SIGCHLD和SIGINT
    struct sigaction sigact;
    sigact.sa_handler = [](int signal) {
    switch (signal)
    {
    case SIGCHLD:
    // 子进程终止信号,非阻塞回收
    int spid;
    while ((spid = waitpid(-1, NULL, WNOHANG)) > 0)
    printf("main process:%d recycle son process:%d.\\n", getpid(), spid);
    break;

    case SIGINT:
    // ctrl+c终止信号
    siglongjmp(env_epollet, signal);
    break;

    default:
    break;
    }
    };
    // 被本信号打断的系统调用在处理完本信号的函数后会重新启动,不会让原系统调用返回错误,但不是所有系统都支持
    sigact.sa_flags = SA_RESTART;
    sigemptyset(&sigact.sa_mask); // 默认只屏蔽自身信号
    // 信号和处理函数注册
    sigaction(SIGCHLD, &sigact, NULL);
    sigaction(SIGINT, &sigact, NULL);

    // 错误提示和退出函数
    void (*clear)(char*, int, int) = [](char* tips, int semmid, int sockfd = -1) {
    // 打印错误信息
    printf("%s\\n", tips);
    // 执行V操作释放信号量
    sembuf sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_flg = 0;
    sem_buf.sem_op = +1; // V操作
    semop(semmid, &sem_buf, 1);

    // 关闭套接字文件描述符
    if (sockfd > 0)
    close(sockfd);

    // 主进程退出前,回收子进程
    int spid;
    int status;
    while ((spid = waitpid(-1, &status, 0)) > 0)
    printf("main process:%d recycle son process:%d.\\n", getpid(), spid);

    return;
    };

    // 1.创建TCP监听socket
    int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sockfd == -1) {
    // 创建失败直接返回
    clear("server socket() error!", semmid, listen_sockfd);
    return;
    }
    // 设置服务器可以快速重用sock地址重启
    int opt = 1;
    int ret = setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    if (ret == -1) {
    // 创建失败直接返回
    clear("server setsockopt() error!", semmid, listen_sockfd);
    return;
    }

    // ET模式下的epoll方式,显式设置监听文件描述符为非阻塞方式
    int flag = fcntl(listen_sockfd, F_GETFL);
    // 添加非阻塞属性设置回去
    fcntl(listen_sockfd, F_SETFL, flag | O_NONBLOCK);

    // 2.绑定本地地址
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("0.0.0.0");//htonl(INADDR_ANY); // 绑定0.0.0.0
    server_addr.sin_port = 10086;
    ret = bind(listen_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if (ret == -1) {
    // 绑定失败直接返回
    clear("server bind() error!", semmid, listen_sockfd);
    return;
    }

    // 3.监听客户端的连接
    ret = listen(listen_sockfd, 16);
    if (ret == -1) {
    // 监听失败直接返回
    clear("server listen() error!", semmid, listen_sockfd);
    return;
    }

    // 4.创建epoll文件描述符并初始化加入监听套接字文件描述符
    int epfd = epoll_create(100); // 创建epoll文件描述符
    if (ret == -1) {
    // 创建epoll文件描述符失败,直接返回
    clear("server epoll_create() error!", semmid, listen_sockfd);
    return;
    }
    // 创建epoll事件,需要包括检测事件和检测的文件描述符,设置事件检查为ET方式,而不是默认的LT方式
    struct epoll_event listenfd_event;
    listenfd_event.data.fd = listen_sockfd; // 保存监听套接字数据
    listenfd_event.events = EPOLLIN | EPOLLET; // 监控读事件,并显示设置为ET模式触发
    // 添加监听套接字及其监听事件struct epoll_event
    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sockfd, &listenfd_event);
    if (ret == -1) {
    // 添加监听文件描述符到epoll文件描述符失败,直接返回
    clear("server epoll_ctl() error!", semmid, listen_sockfd);
    return;
    }
    // 创建保存客户端信息的结构体,可以使用链表结构
    struct CLIENTINFO {
    char ip[32]; // 保存连接客户端的IP地址
    unsigned short port; // 保存连接客户端的端口号port
    int communicate_sockfd; // 保存连接客户端的通信文件描述符
    // 新增:服务器给该客户端需要发送的数据相关字段
    char* send_buf; // 发送缓冲区
    size_t send_len; // 待发送数据总长度
    size_t sent_len; // 已发送数据长度
    bool need_send; // 是否需要发送数据
    };

    printf("main process:%d start sever listening(port:%d)……….\\n", getpid(), server_addr.sin_port);
    // 服务器启动成功,执行V操作释放启动子进程客户端
    sembuf sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_flg = 0; // 表示使用默认阻塞方式
    sem_buf.sem_op = +1;
    semop(semmid, &sem_buf, 1); // 执行V操作

    // 5.循环采用epoll_wait监听
    while (true) {
    ret = sigsetjmp(env_epollet, 1); // 保存服务器堆栈状态
    if (ret)
    break; // 出现错误,直接终止退出循环

    // 使用epoll_wait函数循环监听要观察的套接字文件描述符状态
    struct epoll_event already_fd[16]; // 保存就绪的文件描述符,假设一次最多传出16个
    ret = epoll_wait(epfd, already_fd, sizeof(already_fd) / sizeof(already_fd[0]), 1000); // 设置1s进行一次epoll检测
    // 根据epoll_wait函数的返回值判断状态
    if (ret == -1) {
    // epoll_wait系统调用被其他信号中断后会直接返回-1,errno被设置为EINTR,但epoll_wait没错误,需要继续循环
    if (errno == EINTR)
    continue;
    printf("server epoll_wait() error! errno=%d\\n", errno);
    break;
    }
    else if (ret == 0)
    continue; // 表示在超时时间内,没有观察的套接字就绪
    else
    // 有监听的文件描述符就绪,进行遍历寻找就绪文件描述符,然后区分是服务器监听文件描述符或客户端通信文件描述符
    for (int i = 0; i < ret; i++) {
    // 该文件描述符的读事件就绪的操作
    if (already_fd[i].events & EPOLLIN) // 读事件就绪
    {
    // 读事件就绪需要判断当前就绪文件描述符是监听套接字、通信套接字
    if (already_fd[i].data.fd == listen_sockfd) // 监听文件描述符:服务器接受客户端的连接
    {
    // ET模式设置文件描述符为非阻塞方式后,要采用循环读取读完缓冲区内所有数据为止,并以返回值为-1,errno为EAGAIN或EWOULDBLOCK作为循环结束标志
    while (1) {
    // 监听套接字,说明有客户端发起连接请求
    struct sockaddr_in client_addr;
    socklen_t len = sizeof(client_addr);
    memset(&client_addr, 0, len);
    // 接受客户端的连接
    int communicate_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &len);
    if (communicate_sockfd == -1) {
    // 非阻塞方式读取完,接收缓冲区为空,结束循环读取
    if (errno == EAGAIN || errno == EWOULDBLOCK)
    break;

    // 其他errno错误码说明accept发生未知错误,直接放弃这个客户端的连接,进行下个客户端的连接
    continue; // 连接客户端失败,直接返回进行下一个客户端的连接
    }
    else {
    // 连接客户端成功,将该与客户端通信的文件描述符添加到epoll文件描述符中,实现监听
    // ET模式需要将监听事件显式设置为ET方式,通信文件描述符显式设置为非阻塞属性
    int flag = fcntl(communicate_sockfd, F_GETFL); // 获取当前文件描述符属性
    fcntl(communicate_sockfd, F_SETFL, flag | O_NONBLOCK); // 添加非阻塞属性

    // 构建该文件描述符的监听事件,服务器只需要保证一直监听客户端的读事件,写事件在服务器给客户端发送数据未发送完时进行监听
    struct epoll_event clientfd_event;
    clientfd_event.events = EPOLLIN | EPOLLET; // 监听读取事件,在ET模式下显示设置

    // 创建保存客户端信息的结构体,必须采用堆区地址,因为其要求保存一个地址,栈区局部变量只在这里有效
    CLIENTINFO* pclient_info = (struct CLIENTINFO*)malloc(sizeof(struct CLIENTINFO));
    pclient_info->port = ntohs(client_addr.sin_port); // 保存客户端端口号port
    pclient_info->communicate_sockfd = communicate_sockfd; // 保存客户端通信文件描述符
    strncpy(pclient_info->ip, inet_ntoa(client_addr.sin_addr), sizeof(pclient_info->ip)); // 保存客户端IP地址
    pclient_info->send_buf = NULL; // 待发送数据缓冲区为空
    pclient_info->send_len = 0;
    pclient_info->sent_len = 0;
    pclient_info->need_send = false; // 初始化不需要发送
    clientfd_event.data.ptr = pclient_info; // 保存客户端信息

    // 将客户端通信的文件描述符加入到epoll文件描述符中,实现监听
    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, communicate_sockfd, &clientfd_event);
    if (ret == -1) {
    // 添加客户端通信文件描述符失败,直接关闭与客户端通信的文件描述符
    close(communicate_sockfd);
    continue;
    }
    printf("server connect client:%s:%d success, communicatefd:%d.\\n", pclient_info->ip, pclient_info->port, communicate_sockfd);
    }

    }

    }
    else // 通信文件描述符:服务器读取客户端发送的数据,然后处理后给客户端发送数据
    {
    // epoll_ctl加入时保存,还原客户端信息
    struct CLIENTINFO* pclient_info = (struct CLIENTINFO*)already_fd[i].data.ptr;
    int communicate_sockfd = pclient_info->communicate_sockfd; // 客户端通信套接字
    unsigned short client_port = pclient_info->port; // 客户端端口号
    char* client_ip = pclient_info->ip; // 客户端IPV4地址

    // 通信文件描述符,说明有连接成功的客户端发送信息,在ET模式下需要循环读取完内核读缓冲区内所有数据
    char rdbuf[8]; // 用户接收缓冲区,固定较小容量
    memset(rdbuf, 0, sizeof(rdbuf));
    char* recvbuf = (char*)malloc(524288); // 假设用于存放内核接收缓冲区内循环读取的所有数据,假设当前内核读缓冲区大小不超过524288B=512K
    int pos = 0; // 记录当前循环读取长度,保存在recvbuf中的位置
    bool rd_error = false; // 记录当前循环读取是否出现错误终止,还是读完终止,默认开始无错误
    while (1) {
    // 读取客户端的发送的信息到用户接收缓冲区rebuf中
    memset(rdbuf, 0, sizeof(rdbuf)); // 读取前先清空原数据
    int rdbyte = read(communicate_sockfd, rdbuf, sizeof(rdbuf));
    // 根据信息的返回值判断读取状态
    if (rdbyte == -1) {
    // 在ET模式下的非阻塞文件描述符,还需要检查是否是读完内核接收缓冲区数据返回的错误
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
    rd_error = false;
    break;
    }

    // 读取信息出现错误,直接在epoll文件描述符中去除观察,并关闭与客户端通信套接字
    epoll_ctl(epfd, EPOLL_CTL_DEL, communicate_sockfd, NULL); // 从epoll文件描述符中去除
    close(communicate_sockfd); // 关闭文件描述符
    // 释放保存客户端信息的堆区资源
    if (pclient_info->send_buf)
    free(pclient_info->send_buf);
    free(pclient_info);
    printf("server read() error! close communicate:%d, disconnect client:%s:%d.\\n", communicate_sockfd, client_ip, client_port);
    // 退出前记录错误状态
    rd_error = true;
    break;
    }
    else if (rdbyte == 0) {
    // 客户端断开,服务器断开连接
    printf("cient:%s:%d disconnect, server disconnect! close communicatefd:%d.\\n", client_ip, client_port, communicate_sockfd);
    epoll_ctl(epfd, EPOLL_CTL_DEL, communicate_sockfd, NULL); // 从epoll文件描述符中去除
    close(communicate_sockfd); // 关闭文件描述符
    // 释放保存客户端信息的堆区资源
    if (pclient_info->send_buf)
    free(pclient_info->send_buf);
    free(pclient_info);
    // 退出前记录错误状态
    rd_error = true;
    break;
    }
    else {
    // 本次循环读取成功,记录读取长度,并拷贝数据到recvbuf
    memcpy(recvbuf + pos, rdbuf, rdbyte);
    // 更新pos位置
    pos += rdbyte;
    }
    }

    // 检查服务器在ET模式循环读取内核接收缓冲区的数据是否发生错误
    if (rd_error) {
    // 释放堆区空间
    free(recvbuf);
    // 循环读取发生错误,直接进行跳过与该客户端的通信,进行下个就绪事件处理
    continue;
    }
    else {
    // 服务器成功循环读取完内核接收缓冲区内所有数据
    printf("server cycle read client:%s:%d %d number data:%s\\n", client_ip, client_port, pos, recvbuf);
    // 释放堆区空间
    free(recvbuf);

    // 模拟处理数据
    sleep(1);

    // 在服务器读取完接收缓冲区内所有数据后,准备好数据发送给客户端,在ET模式下采用非阻塞文件描述符,可能会造成要发送的数据只写入一部分
    char wrbuf[128];// 准备要发送的数据
    sprintf(wrbuf, "Hello server form client:%s:%d.", client_ip, client_port);
    size_t data_len = strlen(wrbuf);

    // 分配发送缓冲区并保存数据在epoll_event事件的用户信息结构体指针ptr中,以方便epoll_wait方式传出
    pclient_info->send_buf = (char*)malloc(data_len); // 保存待发送的数据
    memcpy(pclient_info->send_buf, wrbuf, data_len);
    pclient_info->send_len = data_len; // 待发送的数据长度
    pclient_info->sent_len = 0; // 已发送的数据长度
    pclient_info->need_send = true; // 是否需要发送的标志

    // 先尝试非阻塞方式直接写入数据到内核缓冲区,根据返回值判断发送结果执行相应操作
    // 没有发送完的数据写入在客户端epoll_event事件的data的ptr成员中保存并监听写事件,以方便下沉epoll_wait时传出时继续发送
    ssize_t wrbyte = write(communicate_sockfd, pclient_info->send_buf, data_len);
    if (wrbyte > 0) {
    // 在write返回值大于0时
    pclient_info->sent_len = wrbyte; // 更新已经发送的数据长度

    // 判断本次非阻塞发送是否将本次待发送的数据全部写完
    if (pclient_info->sent_len < pclient_info->send_len) {
    // 只写完一部分待发送数据,在非阻塞情况下说明已经写满内核发送缓冲区,需要关注写事件是否就绪,以方便下次检查到写就绪时继续发送
    // 修改服务器监听的通信文件描述符的触发事件,在ET读事件的基础上,额外增加ET写事件
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLOUT | EPOLLET; // 额外增强ET写事件
    ev.data.ptr = pclient_info; // 保存客户端信息在epoll_event事件中,以方便在epoll_wait中可以获取
    epoll_ctl(epfd, EPOLL_CTL_MOD, communicate_sockfd, &ev);
    printf("Partial write, waiting for EPOLLOUT to continue…\\n");
    }
    else {
    // 本次待发送缓冲区数据全部写完,清理发送缓冲区,由于已经发送完,不需要关注写事件是否就绪,并且客户端待发送信息也就不需要保存
    free(pclient_info->send_buf);
    pclient_info->send_buf = NULL;
    pclient_info->need_send = false;
    printf("server first send client:%s:%d %zd number data completely.\\n", client_ip, client_port, wrbyte);
    }
    }
    else if (wrbyte == -1) {
    // 非阻塞情况下,write发送错误-1时,需要先判断是否是EAGAIN或EWOULDBLOCK错误
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
    // 内核发送缓冲区满,说明本次发送没有发送完毕,需要关注写事件,在ET读事件的基础上添加ET写事件
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLOUT | EPOLLET; // 额外添加写ET写事件
    ev.data.ptr = pclient_info; // 完全没有发送任何数据,不需要更改发送数据信息
    epoll_ctl(epfd, EPOLL_CTL_MOD, communicate_sockfd, &ev);
    printf("Write would block, waiting for EPOLLOUT…\\n");
    }
    else {
    // 其他写入错误,直接关闭套接字文件描述符、在epoll文件描述符中去除事件监听、清理保存的客户端数据
    printf("server write() error! close communicate:%d, disconnect client:%s:%d.\\n", communicate_sockfd, client_ip, client_port);
    epoll_ctl(epfd, EPOLL_CTL_DEL, communicate_sockfd, NULL); // 删除监听
    close(communicate_sockfd); // 关闭通信文件描述符
    free(pclient_info->send_buf); // 释放客户端保存的资源
    free(pclient_info); // 释放客户端堆区资源
    }
    }
    }
    }
    }

    // 该文件描述符的写事件就绪
    if (already_fd[i].events & EPOLLOUT) {
    // 写事件就绪,只有一种可能,服务器上次在该文件描述符上发送数据没有发送完毕被注册写事件,在这次写事件就绪发送数据
    // epoll_ctl加入时保存,还原客户端信息
    struct CLIENTINFO* pclient_info = (struct CLIENTINFO*)already_fd[i].data.ptr;
    int communicate_sockfd = pclient_info->communicate_sockfd; // 客户端通信套接字
    unsigned short client_port = pclient_info->port; // 客户端端口号
    char* client_ip = pclient_info->ip; // 客户端IPV4地址
    size_t& sent_len = pclient_info->sent_len; // 已发送数据长度
    size_t& send_len = pclient_info->send_len; // 发送数据缓冲区全部长度

    // 读取客户端信息判断本次是否需要继续发送
    if (pclient_info->need_send && pclient_info->send_buf) {
    // 需要发送
    // 先尝试再次使用write发送上次没发送完的数据,根据返回值判断发送完毕或错误执行相应的操作
    int wrbyte = write(communicate_sockfd, pclient_info->send_buf + sent_len, send_len – sent_len);
    if (wrbyte > 0) {
    sent_len += wrbyte; // 更新已发送数据长度
    // 判读本次是否将数据发送完毕
    if (sent_len < send_len) {
    printf("Partial write, waiting for EPOLLOUT to continue…\\n");
    // 还是没有发送完毕,继续观察写事件就绪,等待下次发送,不需要进行修改
    }
    else {
    printf("server send client:%s:%d %zd number data completely.\\n", client_ip, client_port, sent_len);
    // 发送数据完毕,取消观察写事件就绪,服务器只需持续观察该客户端发送数据的读事件就绪即可
    struct epoll_event event;
    // 清空客户端信息中保存的已经发送完毕的数据
    free(pclient_info->send_buf);
    pclient_info->send_buf = NULL;
    send_len = 0;
    sent_len = 0;
    pclient_info->need_send = false;
    event.data.ptr = pclient_info; // 保存客户端信息
    event.events = EPOLLIN | EPOLLET; // 只需观察读事件
    epoll_ctl(epfd, EPOLL_CTL_MOD, communicate_sockfd, &event);
    }
    }
    else if (wrbyte == -1) {
    // 非阻塞情况下,write发送错误-1时,需要先判断是否是EAGAIN或EWOULDBLOCK错误,但在写就绪情况下不会发生这种情况,还是判断一下
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
    // 内核发送缓冲区满,说明本次发送没有发送完毕,需要关注写事件,无需更改
    printf("Write would block, waiting for EPOLLOUT…\\n");
    }
    else {
    // 其他写入错误,直接关闭套接字文件描述符、在epoll文件描述符中去除事件监听、清理保存的客户端数据
    printf("server write() error! close communicate:%d, disconnect client:%s:%d.\\n", communicate_sockfd, client_ip, client_port);
    epoll_ctl(epfd, EPOLL_CTL_DEL, communicate_sockfd, NULL); // 删除监听
    close(communicate_sockfd); // 关闭通信文件描述符
    free(pclient_info->send_buf); // 释放客户端保存的资源
    free(pclient_info); // 释放客户端堆区资源
    }
    }
    }
    else{
    // 不需要发送,但观察写就绪事件,需要进行关闭
    struct epoll_event event;
    event.data.ptr = pclient_info;
    event.events = EPOLLIN | EPOLLET;
    epoll_ctl(epfd, EPOLL_CTL_MOD, communicate_sockfd, &event);
    }

    }
    // 本次文件描述符的就绪读事件、就绪写事件处理完毕
    }
    // 本次epoll_wait循环结束
    }

    // 6.退出服务器,释放资源
    close(listen_sockfd); // 关闭监听文件描述符
    close(epfd); // 关闭epoll文件描述符

    // 由于还是主进程,需要阻塞回收子进程
    int spid;
    while ((spid = waitpid(-1, NULL, 0)) > 0)
    printf("main process:%d recycle son process:%d.\\n", getpid(), spid);
    // 释放信号量
    semctl(semmid, 0, IPC_RMID);

    // 主进程服务器结束
    }
    else if (pid == 0) {
    // 执行信号量的P操作,避免出现客户端子进程比服务器先运行的情况
    sembuf sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_op = -1; // P操作
    sem_buf.sem_flg = 0; // 默认阻塞方式
    semop(semmid, &sem_buf, 1); // 执行P操作

    // 子进程模拟创建多个并发的多进程客户端进行与服务器进行通信
    client(2, 1); // 创建2+1个客户端子进程,每个客户端只发送1次消息就退出,便于观察
    }
    else
    printf("fork() error!\\n");
    }

    ET模式下的epoll方式的实现细节

    • ①对于接收/写入数据必须一次性接收完毕/写满,一般不可能允许使用一个足够大的用户缓冲区,一次性接收/写完所有数据,而考虑使用固定较小的用户缓冲区,采用循环read接收直到读完读缓冲区或write循环写入直到写满写缓冲区。但是这时采用阻塞的通信文件描述符就会发生阻塞,不适用于单线程或单进程。
    • ②必须使用非阻塞的文件描述符,以recv/read函数或write/send函数返回-1和errno设置为EAGAIN作为循环recv/read读取结束标志或循环write/send写入结束标志。
    • ③由于采取非阻塞的文件描述符,并以返回-1和errno设置为EAGAIN作为循环结束标志。在文件描述符不处于就绪状态,如读缓冲区本身为空或写缓冲区本身已满,也会出现相同的返回-1和errno设置为EAGAIN作为循环结束标志,因此必须采用select/epoll的就绪检查,以保证是就绪状态才去读取/接收,ET模式需要设置检查事件为ET读事件或ET写事件。
    • ④持续观察读事件就绪:服务器需要持续接收客户端的数据,需要一直注册观察读事件就绪,在epoll_wait传出读就绪事件时,循环读取完读缓冲区内所有数据。
    • ⑤按需观察写事件就绪:服务器只在需要给客户端发送数据时,一次write发送数据写满缓冲区没发送完时,才注册观察写事件就绪,然后等待下次epoll_wait传出写事件就绪时,服务器继续上次未发送完的数据,循环直到发送完数据,然后取消观察写事件就绪,只观察读事件就绪。

    四、BIO模型和NIO模型

            在介绍服务器的高并发问题前,先介绍服务器实现的两种模型:BIO模型和NIO模型。BIO(阻塞I/O)和NIO(非阻塞I/O)是两种常见的网络编程模型,用于处理客户端连接和I/O操作。它们各有优缺点,适用于不同的场景。下面我将逐步介绍这两种模型。

    1. BIO模型(阻塞I/O模型)

    BIO模型是一种传统的、基于线程的阻塞模型。在这种模型中,每个客户端连接都会分配一个单独的线程来处理。当线程执行I/O操作(如读取或写入数据)时,它会一直阻塞(等待),直到数据准备好或操作完成。这意味着线程在等待期间无法执行其他任务。

    工作原理:

    • 服务器启动后,监听一个端口。
    • 当客户端连接时,服务器为每个连接创建一个新线程。
    • 线程在I/O操作期间阻塞,例如等待数据从网络接收。
    • 处理完一个连接后,线程关闭或释放。

    优缺点:

    • 优点:实现简单,易于理解,适合连接数较少的场景。
    • 缺点:当连接数增加时,线程资源消耗大(每个线程需要内存和CPU),可能导致性能瓶颈和资源浪费。例如,如果有1000个连接,就需要1000个线程,这在资源受限的环境中不可行。

    BIO模型适用于小型应用或开发测试阶段,但不适合高并发场景。

    2. NIO模型(非阻塞I/O模型)

    NIO模型是一种事件驱动的非阻塞模型,旨在解决BIO的可伸缩性问题。它使用选择器(selector)机制,允许一个线程同时处理多个连接。在NIO中,I/O操作是非阻塞的:线程不会等待数据,而是轮询事件,当数据准备好时再处理。

    工作原理:

    • 服务器使用一个或多个线程管理多个连接。
    • 连接被注册到选择器上,选择器监控这些连接的I/O事件(如可读或可写)。
    • 线程轮询选择器,当事件发生时(如数据到达),线程处理该事件而不阻塞。
    • 处理是异步的:线程可以快速切换到其他任务。

    优缺点:

    • 优点:资源利用率高,一个线程可处理大量连接,支持高并发;性能更好,适合大规模应用。
    • 缺点:实现复杂,需要处理事件循环和回调,调试难度较高;如果事件处理不当,可能导致延迟或错误。

    NIO模型广泛应用于现代高并发服务器,如Web服务器和实时系统。

    两种模型总结

    • BIO:简单但资源消耗大,适合低并发场景。
    • NIO:高效但复杂,适合高并发场景。 选择哪种模型取决于具体需求:如果连接数少且开发快速,BIO更合适;如果追求高性能和可伸缩性,NIO是更好的选择。

    五、多进程、多线程、ET模式epoll方式实现高并发服务器比较总结

    1. 资源消耗

    • 多线程: 共享父进程的内存空间(堆、全局变量等),主要开销在线程栈和线程控制块(TCB)。创建和切换开销相对较小。
    • 多进程: 每个进程拥有独立的地址空间、文件描述符表等。创建(fork)和切换开销较大。
    • epoll (ET): 通常采用单线程或少量线程配合事件循环。epoll实例本身占用资源很小。主要开销在维持连接状态(如每个连接的socket和缓冲区)。资源消耗最低,尤其适合海量连接。

    2. 性能 (高并发场景下)

    • 多线程:
      • 优点:可利用多核CPU并行处理请求。
      • 缺点:线程间同步(锁)可能成为瓶颈,引发竞争和上下文切换开销。线程数量过多时性能急剧下降。
    • 多进程:
      • 优点:进程间隔离性好,一个进程崩溃不影响其他进程。可利用多核。
      • 缺点:进程创建、销毁、切换开销大。进程间通信(IPC)成本高(管道、共享内存等)。同样存在进程数量限制。
    • epoll (ET):
      • 优点:事件驱动,无轮询开销。非阻塞IO,单线程即可处理大量连接(C10K问题)。上下文切换开销极低。效率最高。
      • 缺点:编程模型复杂。需要确保一次性处理就绪事件(ET模式要求),避免饥饿。

    3. 可扩展性 (连接数)

    • 多线程/多进程: 受限于线程/进程数量上限(ulimit -u)和创建/切换开销。通常难以支持极高并发(如数万以上连接)。
    • epoll (ET): 可扩展性最好。epoll能高效管理大量文件描述符(FD)。系统支持的FD上限(fs.file-max)通常是主要瓶颈,远高于可创建的线程/进程数。能轻松应对C10K甚至C100K问题。

    4. 适用场景

    • 多线程: 适合计算密集型任务并行化,或连接数不高但请求处理逻辑较重的场景。需要充分利用多核CPU时。
    • 多进程: 适合需要强隔离性的场景(如需要重启部分服务而不影响整体),或者旧系统改造。也适用于可以利用fork特性的地方(如CGI)。
    • epoll (ET): 是构建高并发、IO密集型网络服务(如聊天服务器、游戏服务器、实时消息推送、负载均衡器、高性能HTTP服务器)的首选。尤其当连接数巨大且活跃连接比例不高(长连接)时优势明显。

    5.总结对比表

    特性多线程并发多进程并发epoll (ET模式) 高并发
    资源消耗 中等 (共享内存,栈开销) 高 (独立内存空间) 低 (单/少线程 + 连接状态开销)
    性能 (高并发) 线程数多时下降 (锁竞争、切换) 进程数多时下降 (创建、切换、IPC) 最高 (事件驱动,低切换开销)
    编程复杂度 中等 (需线程同步) 中等偏高 (需IPC, 进程管理) 高 (异步编程,状态机,缓冲区管理)
    可扩展性 受限于线程数上限 受限于进程数上限 最好 (受限于FD上限,支持海量连接)
    适用场景 计算密集型,中低并发 需强隔离,旧系统 IO密集型,超高并发网络服务

    结论:

    • 对于现代高性能、高并发的网络服务器程序,采用ET模式的epoll(或类似的kqueue, IOCP)是主流和推荐的选择,它能以最少的资源提供最高的并发处理能力。
    • 多线程模型在需要利用多核进行并行计算或连接数可控的场景仍有价值,但需谨慎处理同步问题。
    • 多进程模型在隔离性要求高的场景或特定传统架构中仍有应用,但在高并发性能上通常不如前两者。
    赞(0)
    未经允许不得转载:171主机测评 » Socket编程进阶:百万并发服务器
    分享到: 更多 (0)

    评论 抢沙发

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