一个基于 select 实现的多路复用 TCP 服务器程序:
/*3 - 使用select实现多路复用 */
#include <stdio.h> // 标准输入输出函数库
#include <stdlib.h> // 标准库函数,包含exit等
#include <string.h> // 字符串处理函数
#include <unistd.h> // Unix标准函数,包含read, write等
#include <sys/socket.h> // 套接字相关函数
#include <sys/types.h> // 基本系统数据类型
#include <sys/select.h> // select相关函数和宏
#include <fcntl.h> // 文件控制函数
#include <sys/stat.h> // 文件状态相关定义
#include <netinet/in.h> // 互联网地址族
#include <arpa/inet.h> // 提供IP地址转换函数
#include <signal.h> // 信号处理函数#define FD_CNT 1000 // 定义最大文件描述符数量int main(int argc,char *argv[])
{if(argc!=2){ // 检查命令行参数数量是否正确printf("Usage:%s port\n",argv[0]); // 输出程序使用方法exit(0); // 退出程序}//1.创建socketint sockfd = socket(AF_INET,SOCK_STREAM,0); // 创建TCP套接字,AF_INET表示IPv4,SOCK_STREAM表示TCPif(sockfd==-1){ // 检查socket创建是否成功perror("socket"); // 输出错误信息exit(-1); // 异常退出}//允许地址复用int optval = 1; // 选项值,1表示启用setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,&optval, sizeof(optval)); // 设置套接字选项,允许地址复用//2.绑定ip和端口号(自己)struct sockaddr_in addr; // 定义IPv4地址结构体addr.sin_family = AF_INET; // 设置协议族为IPv4addr.sin_port = htons(atoi(argv[1])); // 将端口号转换为网络字节序addr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用的网络接口int res = bind(sockfd,(struct sockaddr *)&addr,sizeof(addr)); // 绑定套接字到指定地址和端口if(res==-1){ // 检查绑定是否成功perror("bind"); // 输出错误信息exit(-1); // 异常退出}//3.监听res = listen(sockfd,10); // 开始监听连接,最大等待队列长度为10if(res==-1){ // 检查监听是否成功perror("listen"); // 输出错误信息exit(-1); // 异常退出}//准备描述符集合fd_set set; // 定义文件描述符集合int *fds = malloc(FD_CNT*sizeof(int)); // 分配存储客户端套接字的数组if(!fds){ // 检查内存分配是否成功perror("malloc"); // 输出错误信息exit(-1); // 异常退出}//设置初始值为-1,表示未使用memset(fds,-1,FD_CNT*sizeof(int)); // 将数组初始化为-1int maxfd,i; // maxfd存储最大文件描述符,i用于循环char msg[1024] = {}; // 用于存储接收和发送的消息//4.等待客户端连接while(1){ // 主循环,持续运行服务器maxfd = sockfd; // 初始化最大文件描述符为监听套接字FD_ZERO(&set); // 清空文件描述符集合//监控标准输入FD_SET(0,&set); // 将标准输入(文件描述符0)加入集合FD_SET(sockfd,&set); // 将监听套接字加入集合//将所有已连接的客户端描述符加入setfor(i=0;i<FD_CNT;i++){ // 遍历客户端套接字数组if(fds[i]!=-1){ // 如果该位置有有效的套接字FD_SET(fds[i],&set); // 将其加入文件描述符集合//记录最大描述符值if(fds[i]>maxfd) // 更新最大文件描述符maxfd = fds[i];}}//调用select,等待活动的文件描述符struct timeval tv; // 定义超时结构体tv.tv_sec = 2; // 秒数,设置超时时间为2秒tv.tv_usec = 0; // 微秒数if(select(maxfd+1,&set,NULL,NULL,&tv)<=0){ // 调用select,监控读事件printf("timeout!\n"); // 超时或出错时输出信息continue; // 继续下一次循环}//处理活动的描述符//有键盘输入if(FD_ISSET(0,&set)){ // 检查标准输入是否有活动memset(msg,0,sizeof(msg)); // 清空消息缓冲区read(0,msg,sizeof(msg)); // 从标准输入读取数据printf("输入的内容为:%s\n",msg); // 输出读取到的内容}//1.有客户端连接请求if(FD_ISSET(sockfd,&set)){ // 检查监听套接字是否有活动(新连接)struct sockaddr_in cilent_addr; // 存储客户端地址socklen_t len = sizeof(cilent_addr); // 地址长度int newfd = accept(sockfd,(struct sockaddr *)&cilent_addr,&len); // 接受客户端连接if(newfd==-1){ // 检查连接是否成功perror("accept"); // 输出错误信息exit(-1); // 异常退出}//将新连接的客户端描述符添加到fds数组中for(i=0;i<FD_CNT;i++){ // 遍历数组寻找空位if(fds[i]==-1)break;}if(i<FD_CNT) // 如果找到空位fds[i] = newfd; // 存储新的客户端套接字else{ // 如果数组已满printf("服务器已达到连接数上线!\n"); // 输出提示信息}printf("%s到此一游!\n",inet_ntoa(cilent_addr.sin_addr)); // 输出客户端IP地址}//2.有客户端发送消息for(i=0;i<FD_CNT;i++){ // 遍历所有客户端套接字if(fds[i]!=-1 && FD_ISSET(fds[i],&set)){ // 如果套接字有效且有活动res = read(fds[i],msg,sizeof(msg)); // 从客户端读取消息if(res<=0){ // 如果读取失败或客户端断开连接close(fds[i]); // 关闭套接字fds[i] = -1; // 标记为未使用continue; // 继续下一次循环}//需要长时间通信可以开多任务printf("%s\n",msg); // 输出接收到的消息if(strcmp(msg,"byebye")==0){ // 如果客户端发送"byebye"close(fds[i]); // 关闭套接字fds[i] = -1; // 标记为未使用continue; // 继续下一次循环}//原路发回消息write(fds[i],msg,res); // 将接收到的消息回发给客户端}}}free(fds); // 释放动态分配的内存close(sockfd); // 关闭监听套接字return 0; // 程序正常退出
}
程序功能说明:
这是一个基于 select
实现的多路复用 TCP 服务器程序,主要功能如下:
创建 TCP 服务器:
- 创建套接字、设置地址复用、绑定端口、监听连接
多路复用处理:
- 使用
select
同时监控多个文件描述符(标准输入、监听套接字、客户端套接字) - 实现单进程处理多个客户端连接,避免了多进程 / 多线程的开销
- 使用
支持的操作:
- 接受新的客户端连接
- 读取客户端发送的消息并原样返回(回声服务器功能)
- 处理标准输入(键盘输入)并显示
- 当客户端发送 "byebye" 时断开连接
- 客户端断开连接时清理资源
连接管理:
- 使用数组存储客户端套接字,最大支持 1000 个连接
- 自动管理连接状态,释放断开的连接资源
这个程序展示了 select
函数的典型用法,通过多路复用机制,一个进程就能同时处理多个 I/O 事件,适合处理大量并发连接但每个连接的 I/O 操作不频繁的场景。