Linux 软件编程(十二)网络编程:TCP 并发服务器构建与 IO 多路复用
在网络编程中,服务器需要应对多个客户端的连接与请求,单循环服务器因同一时刻仅能处理一个客户端任务,无法满足高并发场景。而 TCP 并发服务器可实现多客户端同时处理 。
一、TCP 并发服务器
(一)单循环与并发服务器区别
- 单循环服务器:同一时刻仅能处理一个客户端任务,处理完当前客户端才能响应下一个,效率低。
- 并发服务器:可同时处理多个客户端任务,提升服务端资源利用率与响应能力。
(二)TCP 连接特性
TCP 基于 “三次握手” 建立 一对一连接 ,可靠传输数据,但需高效并发模型支撑多客户端交互。
二、TCP 服务端并发模型
(一)多进程模型
- 原理:服务端通过
fork()
创建子进程,父进程负责监听新连接,子进程处理客户端交互。 - 特点:
- 资源开销大:进程有独立地址空间,创建、切换成本高。
- 安全性高:进程间资源隔离,一个进程异常一般不影响其他进程。
- 代码示例:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>#define SER_PORT 50000
#define SER_IP "192.168.0.149"struct sockaddr_in seraddr;void hander(int handnum)
{wait(NULL);
}int init_tcp_recv()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){perror("sockfd error");return -1;}seraddr.sin_family = AF_INET;seraddr.sin_port = htons(SER_PORT);seraddr.sin_addr.s_addr = inet_addr(SER_IP);int ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));if(ret < 0){perror("bind error");return -1;}ret =listen(sockfd, 10);if(ret < 0){perror("listen error");return -1;}return sockfd;
}int main(int argc, char const *argv[])
{signal(SIGCHLD, hander);int sockfd = init_tcp_recv();struct sockaddr_in cliaddr;socklen_t lenaddr = sizeof(cliaddr);while(1){int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &lenaddr);if(connfd < 0){perror("accept error");return -1;}pid_t pid = fork();if(pid > 0){}else if(pid == 0){char buff[1024] = {0};while(1){memset(buff, 0, sizeof(buff));int ret = recv(connfd, buff, sizeof(buff), 0);if(ret < 0){perror("error recv");break;}else if(0 == ret){printf("[%s:%d]: client off\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));close(connfd);break;}printf("[%s:%d]:%s\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), buff);strcat(buff, "---->ok");int cnt = send(connfd, buff, strlen(buff), 0);if(cnt < 0){perror("send error");close(connfd);break;}}}}close(sockfd);return 0;}
(二)多线程模型
- 原理:借助
pthread_create()
创建线程,主线程负责accept
新连接,子线程处理客户端数据收发。 - 特点:
- 资源开销小:线程共享进程地址空间,创建、切换成本低于进程,相同资源环境下并发量更高。
- 需注意同步:线程共享资源,如需客户端地址信息等,需用锁机制(如互斥锁)避免竞争,否则易引发数据混乱,或者创建结构体将客户端信息和通信套接字放在里面,然后线程传参时第四个参数传入结构体的地址,然后进行访问内容。
- 伪代码示例:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include<pthread.h>#define SER_PORT 50000
#define SER_IP "192.168.0.149"struct sockaddr_in seraddr;int init_tcp_recv()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){perror("sockfd error");return -1;} //允许绑定处于TIME_WAIT状态的地址,避免端口占用问题:int optval = 1;setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));seraddr.sin_family = AF_INET;seraddr.sin_port = htons(SER_PORT);seraddr.sin_addr.s_addr = inet_addr(SER_IP);int ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));if(ret < 0){perror("bind error");return -1;}ret =listen(sockfd, 10);if(ret < 0){perror("listen error");return -1;}return sockfd;
}// struct sockaddr_in cliaddr;
// socklen_t lenaddr = sizeof(cliaddr);void *recv_senf(void *arg)
{int connfd = *((int *)arg);char buff[1024] = {0};while(1){memset(buff, 0, sizeof(buff));int ret = recv(connfd, buff, sizeof(buff), 0);if(ret < 0){perror("error recv");break;}else if(0 == ret){printf("client off\n");close(connfd);break;}printf("%s\n", buff);strcat(buff, "---->ok");int cnt = send(connfd, buff, strlen(buff), 0);if(cnt < 0){perror("send error");close(connfd);break;} }return NULL;
}int main(int argc, char const *argv[])
{int sockfd = init_tcp_recv();if(sockfd < 0){return -1;}pthread_t tid;while(1){int connfd = accept(sockfd, NULL, NULL);if(connfd < 0){perror("accept error");return -1;}pthread_create(&tid, NULL, recv_senf, &connfd);pthread_detach(tid);}close(sockfd);return 0;}
(三)线程池模型
- 背景:多线程 / 多进程模型中,频繁创建、销毁线程 / 进程会产生大量时间消耗。线程池基于 生产者 - 消费者模型 与 任务队列 ,预先创建一定数量线程,复用线程处理任务,减少资源开销。
- 原理:
- 生产者(主线程):负责
accept
客户端连接,将任务(如处理客户端请求)放入任务队列。 - 消费者(线程池中的次线程):从任务队列取出任务并执行,执行完可继续获取新任务,无需频繁创建销毁。
- 生产者(主线程):负责
- 模型示意图:
主线程作为生产者生产任务(如客户端连接处理任务),放入任务队列;多个次线程作为消费者,从队列取任务执行,实现任务高效复用与并发处理。
(四)IO 多路复用模型
- 核心思想:让一个进程 / 线程 复用多个文件描述符(fd)的读写操作 ,不创建新进程 / 线程,通过一个进程监测、处理多个文件(如 socket 连接)的读写事件。
- 应用场景:需同时监听多个
fd
(如stdin
、connfd
等),避免阻塞 IO 导致程序 “卡主”,典型函数有select
、poll
、epoll
。
(1)select 函数
- 函数与操作步骤:
- 创建文件描述符集合:用
fd_set
类型,通过FD_ZERO
清空集合,FD_SET
添加关注的fd
。 - 传递集合给内核监测:调用
select
函数,内核监测集合中fd
的事件(读、写、异常等),监测期间进程阻塞。 - 处理事件:内核监测到事件后,
select
解除阻塞,通过FD_ISSET
判断具体fd
事件,执行对应读写操作。
- 创建文件描述符集合:用
- 关键函数与
select
原型: void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:传递文件描述符结合表给内核并等待获取事件结果
参数:
nfds : 关注的最大文件描述符+1
readfds:读事件的文件描述符集合
writefds:写事件的文件描述符集合
exceptfds:其他事件的文件描述符集合
timeout:设置select监测时的超时时间
NULL : 不设置超时时间(select一直阻塞等待)返回值:
成功:返回内核监测的到达事件的个数
失败:-1
0 : 超时时间到达,但没有事件发生,则返回- 伪代码示例(结合 TCP 服务端):
#include <sys/select.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/time.h>
#include<string.h>
#include <stdio.h>
#include <sys/types.h> /* See NOTES */#include <sys/socket.h>
#include <netinet/ip.h> /* superset of previous */
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include<pthread.h>#define SER_PORT 50000
#define SER_IP "192.168.0.149"struct sockaddr_in seraddr;int init_tcp_recv()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){perror("sockfd error");return -1;} //允许绑定处于TIME_WAIT状态的地址,避免端口占用问题:int optval = 1;setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));seraddr.sin_family = AF_INET;seraddr.sin_port = htons(SER_PORT);seraddr.sin_addr.s_addr = inet_addr(SER_IP);int ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));if(ret < 0){perror("bind error");return -1;}ret =listen(sockfd, 10);if(ret < 0){perror("listen error");return -1;}return sockfd;
}int main(int argc, char const *argv[])
{int sockfd = init_tcp_recv();if(sockfd < 0){return -1;}fd_set rdfds;fd_set rdfdstem;FD_ZERO(&rdfds);FD_ZERO(&rdfdstem);FD_SET(sockfd, &rdfds);int maxfd = sockfd; char buff[1024] = {0};while(1){rdfdstem = rdfds;int ret = select(maxfd + 1, &rdfds, NULL, NULL, NULL);if(ret < 0){perror("select error");return -1;}if(FD_ISSET(sockfd, &rdfdstem)){int connfd = accept(sockfd, NULL, NULL);if(connfd < 0){perror("accept error");return -1;}FD_SET(connfd, &rdfds);maxfd = maxfd > connfd ? maxfd : connfd;}for(int i = sockfd + 1;i < maxfd + 1;++i){if(FD_ISSET(i, &rdfdstem)){memset(buff, 0, sizeof(buff));ssize_t cnt = recv(i, buff, sizeof(buff), 0);if(cnt < 0){perror("recv error");FD_CLR(i, &rdfds);close(i);continue;}else if(cnt == 0){FD_CLR(i, &rdfds);close(i);continue;}printf("%s\n", buff);strcat(buff, "---->ok");cnt = send(i, buff, strlen(buff), 0);if(cnt < 0){perror("send error");FD_CLR(i, &rdfds);close(i);continue;}}} }close(sockfd);return 0;
}
- 特点:
- 需维护
fd
集合,每次select
后需重新添加fd
(因内核会修改集合)。 fd
数量受限(受系统默认限制,如 1024 ),高并发场景可能不够用。
- 需维护
(2)poll/epoll
- poll:与
select
类似,通过struct pollfd
数组传递监测的fd
及事件,解决select
中fd
数量受限问题,但本质仍需遍历fd
判断事件,高并发下效率一般。 - epoll:Linux 下高效的 IO 多路复用机制,通过 红黑树 维护监测的
fd
,回调机制 通知事件,无需遍历所有fd
,高并发场景(如大量客户端连接)效率远高于select
/poll
。
三、总结
TCP 并发服务器构建有多种模式:
- 多进程模型资源开销大但安全;
- 多线程模型轻量但需注意同步;
- 线程池模型优化线程资源管理,适配高频任务场景;
- IO 多路复用(
select
/poll
/epoll
)让单进程实现多fd
监测,高效处理并发连接。