Linux IO 多路复用:select 实现高并发 TCP 服务器
·
一、前言
上一篇我们学习了 select 监听键盘输入,理解了 IO 多路复用的基础用法。
本篇我们进入网络编程实战: 使用 select 实现一个高并发 TCP 服务器,可以同时接收多个客户端连接、接收消息、回复消息。
这是 Linux 后端开发最经典、最基础的高并发模型
二、实现功能
- 服务器监听
127.0.0.1:6000 - 支持多个客户端同时连接
- 使用
select同时监听所有客户端 - 接收客户端消息并打印
- 给客户端回复
ok - 客户端断开自动清理资源
- 5 秒无事件超时打印
三、核心知识点
1. select 作用
- 同时监听多个文件描述符(服务端 socket + 多个客户端 socket)
- 阻塞等待,直到有事件发生(新连接 / 消息 / 超时)
- 单线程实现高并发,不需要创建大量线程
2. 文件描述符数组
我们用一个数组 fds[MAXFD] 管理所有需要监听的描述符:
-1表示空闲- 有效值表示需要监听的 fd
3. 四个 fd_set 操作宏
FD_ZERO(&fdset); // 清空集合
FD_SET(fd, &fdset); // 添加 fd 到集合
FD_ISSET(fd, &fdset);// 判断 fd 是否有事件
FD_CLR(fd, &fdset); // 删除 fd(本例没用到)
4. select 返回值
-1:出错0:超时>0:有事件发生(新连接 / 新消息)
四、完整代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>
#include <sys/time.h>
#define MAXFD 10
int socket_init();
void fds_init(int fds[])
{
for (int i = 0; i < MAXFD; i++)
{
fds[i] = -1; // 数组中每个元素置为-1,代表空闲
}
}
void fds_add(int fds[], int fd)
{
for (int i = 0; i < MAXFD; i++)
{
if (fds[i] == -1)
{
fds[i] = fd;
break;
}
}
}
void fds_del(int fds[], int fd)
{
for (int i = 0; i < MAXFD; i++)
{
if (fds[i] == fd)
{
fds[i] = -1; // 空闲
break;
}
}
}
void accept_client(int sockfd, int fds[])
{
int c = accept(sockfd,NULL,NULL);
if( c < 0 )
{
return;
}
printf("accept c=%d\n",c);
fds_add(fds,c);//将新产生的描述符c,添加到数组中
}
void recv_data(int c, int fds[])
{
char buff[128] = {0};
int n = recv(c,buff,1,0);
if( n <= 0 )
{
fds_del(fds,c);
close(c);
printf("close(%d)\n",c);
return;
}
printf("recv(%d)=%s\n",c,buff);
send(c,"ok",2,0);
}
int main()
{
int sockfd = socket_init();
if (sockfd == -1)
{
exit(1);
}
int fds[MAXFD]; // 收集描述符 fd_set
fds_init(fds); // 数组置空
fds_add(fds, sockfd); // 将sockfd,添加到数组,在此时数组只有一个有效描述符
fd_set fdset; // select 集合,读事件
while (1)
{
FD_ZERO(&fdset); // 清空集合
int maxfd = -1; // 记录描述符最大值
for (int i = 0; i < MAXFD; i++)
{
if( fds[i] == -1 )
{
continue;//跳过无效描述符 -1
}
FD_SET(fds[i],&fdset);//将描述符添加到集合
if( fds[i] > maxfd)
{
maxfd = fds[i];
}
}
struct timeval tv;
tv.tv_sec = 5;//5秒
tv.tv_usec = 0;//微秒
int n = select(maxfd +1,&fdset,NULL,NULL,&tv);
printf("--------\n");
if( n == -1)
{
printf("select err\n");
}
else if ( n == 0 )
{
printf("time out\n");
}
else
{
for(int i = 0; i < MAXFD; i++)
{
if( fds[i] == -1 )
{
continue;
}
if( FD_ISSET(fds[i],&fdset))
{
if( fds[i] == sockfd)//接受连接accept
{
accept_client(sockfd,fds);
}
else//接收数据,recv
{
recv_data(fds[i],fds);
}
}
}
}
}
}
int socket_init()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
return -1;
}
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (res == -1)
{
printf("bind err\n");
return -1;
}
res = listen(sockfd, 5);
if (res == -1)
{
return -1;
}
return sockfd;
}
五、代码结构讲解
1. fds 数组管理
fds_init:初始化数组为 -1fds_add:添加新 fdfds_del:删除断开的 fd
2. socket_init()
服务器初始化四件套:
- socket()
- bind()
- listen()
- 返回监听 fd
3. accept_client()
接收新客户端连接,把新客户端 fd 加入数组。
4. recv_data()
接收客户端消息,回复 ok,客户端断开则清理资源。
5. main 函数(select 核心逻辑)
- 循环清空 fd 集合
- 将所有有效 fd 加入 select 监听
- select 阻塞等待事件
- 判断事件类型:
- 监听 fd → 新连接
- 客户端 fd → 接收消息
六、学习收获总结
本代码基于 select IO 多路复用 实现高并发 TCP 服务器,核心知识点涵盖:使用数组管理所有文件描述符,实现监听套接字与客户端套接字的统一维护;通过 FD_ZERO、FD_SET、FD_ISSET 完成描述符集合操作,每次循环重新构建集合以保证监听有效性;利用 select 单线程阻塞监听多路 IO 事件,区分新连接与数据接收事件,分别调用 accept 和 recv 处理;通过超时机制避免无限阻塞,对客户端断开做自动关闭与资源清理。整体实现了单线程处理多客户端连接、数据交互、异常处理的完整服务端逻辑,是理解网络 IO 多路复用最经典的入门案例。
更多推荐




所有评论(0)