刷题

服务器多客户端连接核心要点
多进程服务器
实现原理
fork
子进程 :每次accept
新客户端后,调用fork
创建子进程。- 独立处理 :子进程负责与客户端通信(如
read
/write
),父进程继续监听新连接。
特点
- 隔离性高:进程间资源独立,避免数据竞争。
- 资源消耗大:频繁创建进程导致内存和CPU开销高。
多线程服务器
实现原理
- 创建新线程 :
accept
后生成线程处理客户端请求。 - 线程函数 :在线程内执行
read
/write
操作。
特点
- 轻量级:线程共享进程资源,开销低于进程。
- 需同步机制 :需使用互斥锁(
mutex
)或信号量避免数据竞争。
多路I/O模型
核心问题
- 阻塞冲突 :传统
accept
和read
相互阻塞,无法同时处理多客户端。 - 解决方案 :通过非阻塞模型(
select
/poll
)统一监听所有描述符。
Select模型
流程
-
初始化监视列表:
fd_set read_fds;
FD_ZERO(&read_fds); // 清空列表
FD_SET(server_fd, &read_fds); // 添加服务端套接字 -
循环监听 :
select(max_fd + 1, &read_fds, NULL, NULL, NULL); // 阻塞直至描述符激活
-
处理激活描述符 :
- 遍历描述符,通过
FD_ISSET
判断是否激活。 - 服务端套接字激活:调用
accept
接受新客户端。 - 客户端套接字激活:调用
read
读取数据。
- 遍历描述符,通过
缺点
- 效率低:每次调用需遍历所有描述符。
- 监视列表上限:默认限制1024个描述符。
Poll模型
流程
-
初始化监视列表:
struct pollfd fds[MAX_CLIENTS];
fds[0].fd = server_fd;
fds[0].events = POLLIN; // 监视可读事件 -
循环监听 :
poll(fds, nfds, -1); // 永久阻塞直至事件触发
-
处理激活描述符 :
- 遍历
fds
数组,检查revents
是否为POLLIN
。 - 服务端激活:
accept
新客户端并加入列表。 - 客户端激活:
read
数据并响应。
- 遍历
优点
- 无描述符数量限制:支持动态扩展。
- 事件类型灵活:可同时监视读写和异常事件。
模型对比
特性 | Select | Poll |
---|---|---|
描述符上限 | 1024(系统默认) | 无限制 |
效率 | O(n)遍历 | O(n)遍历 |
事件类型 | 仅可读/可写/异常 | 支持自定义事件(如POLLIN) |
跨平台支持 | 广泛支持 | Linux专用 |
实际应用场景
-
作业要求 :
- 服务器端 :使用
poll
模型处理多客户端。 - 客户端1 :
select
模型实现非阻塞通信。 - 客户端2:多线程处理输入/输出。
- 服务器端 :使用
-
典型场景 :
- 实时聊天系统 :服务器通过
poll
管理多个客户端会话。 - 高并发服务器 :
select
/poll
适用于低并发场景,epoll
适合高并发。
- 实时聊天系统 :服务器通过
-
pollk.c
#include "head.h" #define MAX_CLIENTS 10 // 最多10个客户端 // 将一个拥有{描述符,监视方式,激活形式}的结构体变量fd,存入监视列表arr中,并且监视列表的长度len自增 void insert_fd(struct pollfd arr[], struct pollfd newfd, int* len) { arr[*len] = newfd; // 将新的pollfd结构体存入数组指定位置 (*len)++; // 监视列表长度加1 } // 将要移除的描述符fd,从监视列表arr中删除,并且监视列表长度len自减1 void remove_fd(struct pollfd* arr, int fd, int* len) { for (int i = 0; i < *len; i++) { if (arr[i].fd == fd) { // 查找要移除的描述符在数组中的位置 for (int j = i; j < *len - 1; j++) { arr[j] = arr[j + 1]; // 后面的元素向前移动,覆盖要移除的元素 } break; } } (*len)--; // 监视列表长度减1 } int main(int argc, const char *argv[]) { if (argc < 2) { // 检查命令行参数,确保输入了端口号 printf("请输入端口号\n"); return 1; } short port = atoi(argv[1]); // 将命令行输入的端口号字符串转换为short类型 // 创建TCP类型的服务器套接字 int server = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in addr = {0}; addr.sin_family = AF_INET; // 设置地址族为IPv4 addr.sin_port = htons(port); // 将端口号转换为网络字节序 addr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 监听所有可用的本地IP地址 // 将套接字绑定到指定的地址和端口 if (bind(server, (struct sockaddr*)&addr, sizeof(addr)) == -1) { perror("bind"); return 1; } listen(server, MAX_CLIENTS); // 开始监听,设置最大等待连接数 struct pollfd list[MAX_CLIENTS + 1] = {0}; // 定义pollfd结构体数组,用于存储要监视的描述符及其相关信息 int list_len = 0; // 记录监视列表中描述符的数量 // 准备要监视的服务器描述符(关注读事件) struct pollfd poll_server = {.fd = server,.events = POLLIN,.revents = 0}; insert_fd(list, poll_server, &list_len); // 将服务器描述符加入监视列表 printf("服务器启动,监听端口 %d...\n", port); while (1) { // 调用poll函数,阻塞等待监视列表中的描述符有事件发生 poll(list, list_len, -1); for (int i = 0; i < list_len; i++) { int fd = list[i].fd; // 检查描述符的revents是否有读事件(POLLIN)发生 if (list[i].revents & POLLIN) { if (fd == server) { // 服务器描述符有读事件,说明有新客户端连接 struct sockaddr_in client_addr; socklen_t client_addr_len = sizeof(client_addr); int client = accept(server, (struct sockaddr *)&client_addr, &client_addr_len); if (client == -1) { perror("accept"); continue; } printf("新客户端连接: %d\n", client); // 准备要监视的新客户端描述符(关注读事件) struct pollfd poll_client = {.fd = client,.events = POLLIN,.revents = 0}; insert_fd(list, poll_client, &list_len); // 将新客户端描述符加入监视列表 } else { // 客户端描述符有读事件,说明客户端发送了数据 char buf[1024] = {0}; int res = read(fd, buf, sizeof(buf)); if (res <= 0) { // 处理客户端断开 if (res == 0) { printf("客户端 %d 断开连接\n", fd); } else { perror("read"); } close(fd); remove_fd(list, fd, &list_len); // 从监视列表中移除客户端描述符 } else { // 转发数据给其他客户端 for (int j = 0; j < list_len; j++) { if (list[j].fd != fd && list[j].fd != server) { write(list[j].fd, buf, res); } } } } } } } return 0; }
selectk.c
#include "head.h" int main(int argc, const char *argv[]) { if (argc < 2) { printf("请输入端口号\n"); return 1; } int port = atoi(argv[1]); int client_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in addr = {0}; addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = INADDR_ANY; if (connect(client_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { perror("connect"); return 1; } fd_set rset; int max_fd = client_fd; while (1) { FD_ZERO(&rset); FD_SET(client_fd, &rset); FD_SET(0, &rset); // 监控标准输入 int ret = select(max_fd + 1, &rset, NULL, NULL, NULL); if (ret == -1) { perror("select"); continue; } if (FD_ISSET(0, &rset)) { // 处理键盘输入 char buf[1024] = {0}; fgets(buf, sizeof(buf), stdin); buf[strcspn(buf, "\n")] = 0; // 去除换行符 write(client_fd, buf, strlen(buf)); } if (FD_ISSET(client_fd, &rset)) { // 处理接收消息 char buf[1024] = {0}; int res = read(client_fd, buf, sizeof(buf)); if (res <= 0) { if (res == 0) { printf("服务器断开连接\n"); } else { perror("read"); } close(client_fd); return 1; } printf("收到消息: %s\n", buf); } } close(client_fd); return 0; }
xiank.c
#include "head.h" void *send_msg(void *arg) { int client_fd = *(int *)arg; while (1) { char buf[1024] = {0}; fgets(buf, sizeof(buf), stdin); buf[strcspn(buf, "\n")] = 0; // 去除换行符 write(client_fd, buf, strlen(buf)); } } void *recv_msg(void *arg) { int client_fd = *(int *)arg; while (1) { char buf[1024] = {0}; int res = read(client_fd, buf, sizeof(buf)); if (res <= 0) { // 处理服务器断开 if (res == 0) { printf("服务器断开连接\n"); } else { perror("read"); } close(client_fd); pthread_exit(NULL); } printf("收到消息: %s\n", buf); } } int main(int argc, const char *argv[]) { if (argc < 2) { printf("请输入端口号\n"); return 1; } int port = atoi(argv[1]); int client_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in addr = {0}; addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = INADDR_ANY; if (connect(client_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { perror("connect"); return 1; } pthread_t send_tid, recv_tid; pthread_create(&send_tid, NULL, send_msg, &client_fd); // 启动发送线程 pthread_create(&recv_tid, NULL, recv_msg, &client_fd); // 启动接收线程 pthread_join(send_tid, NULL); pthread_join(recv_tid, NULL); close(client_fd); return 0; }
总结 :多进程/线程适合简单场景,多路I/O模型(select
/poll
)通过非阻塞监听提升效率,需根据并发需求选择模型。