使用select函数创建多线程TCP服务端
前文
https://blog.csdn.net/ke_wu/article/details/145268764?spm=1001.2014.3001.5501
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/select.h>
#define PORT 2048
#define BUFFER_SIZE 128
int main() {
// 创建一个 TCP 套接字,AF_INET 表示使用 IPv4 协议族,SOCK_STREAM 表示使用面向连接的 TCP 协议
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 检查套接字创建是否成功,如果返回 -1 表示创建失败
if (sockfd == -1) {
// 输出错误信息,错误信息会包含函数名和具体错误描述
perror("socket");
return -1;
}
// 定义服务器地址结构体,用于存储服务器的 IP 地址和端口号等信息
struct sockaddr_in serveraddr;
// 将 serveraddr 结构体的内存初始化为 0,确保没有随机数据
memset(&serveraddr, 0, sizeof(serveraddr));
// 设置地址族为 IPv4
serveraddr.sin_family = AF_INET;
// 设置 IP 地址为本地任意地址,意味着服务器可以接受来自任何网络接口的连接
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
// 将端口号 2048 从主机字节序转换为网络字节序
serveraddr.sin_port = htons(PORT);
// 将套接字 sockfd 绑定到指定的服务器地址和端口
if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {
// 若绑定失败,输出错误信息
perror("bind");
// 关闭已创建的套接字,避免资源泄漏
close(sockfd);
return -1;
}
// 开始监听客户端的连接请求,允许最多 10 个连接在队列中等待处理
if (listen(sockfd, 10) == -1) {
// 若监听失败,输出错误信息
perror("listen");
// 关闭套接字
close(sockfd);
return -1;
}
// 定义两个文件描述符集合,rfds 用于存储需要监视的文件描述符,rset 用于 select 函数调用时的临时集合
fd_set rfds, rset;
// 清空 rfds 文件描述符集合
FD_ZERO(&rfds);
// 将监听套接字 sockfd 添加到 rfds 集合中,表示要监视该套接字的可读事件
FD_SET(sockfd, &rfds);
// 记录当前最大的文件描述符,初始为监听套接字的文件描述符
int maxfd = sockfd;
// 打印提示信息,表示服务器已启动,正在监听指定端口
printf("Server started, listening on port %d...\n", PORT);
// 进入无限循环,持续处理客户端连接和数据收发
while (1) {
// 将 rfds 集合复制到 rset 集合,因为 select 函数会修改传入的集合
rset = rfds;
// 调用 select 函数,监视 rset 集合中文件描述符的可读事件,超时时间设置为 NULL 表示一直阻塞直到有事件发生
int nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
// 检查 select 函数是否调用失败
if (nready == -1) {
// 若失败,输出错误信息并跳出循环
perror("select");
break;
}
// 检查监听套接字 sockfd 是否有可读事件,即是否有新的客户端连接请求
if (FD_ISSET(sockfd, &rset)) {
// 定义客户端地址结构体,用于存储客户端的 IP 地址和端口号等信息
struct sockaddr_in clientaddr;
// 客户端地址结构体的长度
socklen_t clientlen = sizeof(clientaddr);
// 接受一个新的客户端连接,返回一个新的套接字描述符 clientfd 用于与该客户端通信
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &clientlen);
// 检查 accept 函数是否调用失败
if (clientfd == -1) {
// 若失败,输出错误信息并跳过本次循环
perror("accept");
continue;
}
// 打印新连接的客户端的 IP 地址、端口号和对应的套接字描述符
printf("New connection from %s:%d, clientfd: %d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port), clientfd);
// 将新的客户端套接字描述符添加到 rfds 集合中,以便后续监视其可读事件
FD_SET(clientfd, &rfds);
// 如果新的客户端套接字描述符大于当前最大的文件描述符,更新最大文件描述符
if (clientfd > maxfd) {
maxfd = clientfd;
}
}
// 遍历从监听套接字之后的所有文件描述符,检查是否有可读事件
for (int i = sockfd + 1; i <= maxfd; i++) {
// 检查文件描述符 i 是否有可读事件
if (FD_ISSET(i, &rset)) {
// 定义一个缓冲区,用于存储从客户端接收的数据
char buffer[BUFFER_SIZE] = {0};
// 从客户端套接字 i 接收数据,存储到 buffer 中,最多接收 BUFFER_SIZE 字节
int count = recv(i, buffer, BUFFER_SIZE, 0);
// 检查 recv 函数是否调用失败
if (count == -1) {
// 若失败,输出错误信息,关闭套接字并从文件描述符集合中移除
perror("recv");
close(i);
FD_CLR(i, &rfds);
continue;
} else if (count == 0) {
// 如果 recv 返回 0,表示客户端关闭了连接
printf("Client disconnected, clientfd: %d\n", i);
// 关闭客户端套接字
close(i);
// 从文件描述符集合中移除该套接字
FD_CLR(i, &rfds);
// 如果关闭的是最大的文件描述符,需要重新计算最大文件描述符
if (i == maxfd) {
for (int j = maxfd; j > sockfd; j--) {
if (FD_ISSET(j, &rfds)) {
maxfd = j;
break;
}
}
}
} else {
// 将接收到的数据原样发送回客户端
if (send(i, buffer, count, 0) == -1) {
// 若发送失败,输出错误信息,关闭套接字并从文件描述符集合中移除
perror("send");
close(i);
FD_CLR(i, &rfds);
}
// 打印接收到的客户端数据和对应的客户端套接字描述符
printf("Received from clientfd %d: %s\n", i, buffer);
}
}
}
}
// 关闭监听套接字,释放资源
close(sockfd);
return 0;
}
代码整体功能解释
这段代码实现了一个基于 TCP 协议的服务器程序,使用 select
函数实现 I/O 多路复用,能够同时处理多个客户端的连接和数据收发。具体流程如下:
- 创建套接字:使用
socket
函数创建一个 TCP 套接字。 - 绑定地址和端口:将套接字绑定到本地的指定 IP 地址和端口(这里使用
INADDR_ANY
表示接受来自任何网络接口的连接,端口号为 2048)。 - 开始监听:使用
listen
函数开始监听客户端的连接请求。 - 使用
select
进行 I/O 多路复用:- 将监听套接字添加到文件描述符集合中,使用
select
函数监视其可读事件。 - 当有新的客户端连接请求时,使用
accept
函数接受连接,并将新的客户端套接字添加到文件描述符集合中。 - 遍历所有客户端套接字,检查是否有可读事件,若有则接收数据并原样发送回客户端。
- 当客户端关闭连接时,关闭对应的客户端套接字,并从文件描述符集合中移除,同时更新最大文件描述符
maxfd
。
- 将监听套接字添加到文件描述符集合中,使用
- 错误处理:对各个关键函数(如
socket
、bind
、listen
、accept
、recv
和send
等)的返回值进行详细的错误处理,确保程序的健壮性。 - 资源释放:在程序结束时,关闭监听套接字,避免资源泄漏。