
多客户端-服务器结构总结
一、普通CS架构的局限性
- 核心问题 :单线程中
accept
(阻塞等待连接)与read
(阻塞读取数据)函数互相干扰,无法同时处理多客户端。 - 本质原因 :阻塞型函数需独立执行,若顺序调用会导致流程停滞(如先执行
accept
会阻塞后续read
,反之亦然)。
二、多线程与多进程解决方案
1. 多线程服务器
- 原理:为每个客户端连接创建独立线程,主线程负责监听连接,子线程处理客户端数据交互。
- 优势:避免主线程阻塞,可同时维护多个客户端。
2. 多进程服务器
- 原理:与多线程类似,通过创建新进程维护每个客户端连接,进程间资源独立。
- 优势:进程隔离性强,单个进程崩溃不影响整体服务。
三、多路IO复用技术
1. 核心目标
- 在单线程 中高效处理多个阻塞IO操作(如
read
、accept
),实现描述符间无干扰响应。
2. 工作机制
- 监视列表:用户手动添加需监控的套接字/文件描述符。
- 激活列表:系统自动识别并记录"可读/可写"的描述符,用户仅需处理激活的描述符,避免阻塞。
四、select模型详解
1. 核心结构
- 监视列表类型 :
fd_set
(本质为栈空间数组,容量固定且无法动态扩容)。 - 核心操作函数 :
FD_SET(fd, &set)
:添加描述符到监视列表。FD_CLR(fd, &set)
:从列表移除描述符。FD_ISSET(fd, &set)
:判断描述符是否激活。FD_ZERO(&set)
:初始化列表。
2. 监视函数select
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- 参数 :
nfds
:最大描述符值+1。readfds/writefds/exceptfds
:分别监视"可读/可写/异常"事件的列表。timeout
:阻塞超时时间(NULL
表示永久阻塞)。
- 特点:监视列表与激活列表共用同一块内存,激活后会覆盖原列表,需手动备份。
3. 优缺点
- 优点:逻辑简单直观,易于实现。
- 缺点 :
- 监视列表容量固定,无法动态扩容。
- 需额外维护描述符数组,遍历效率低(双重循环)。
- 每次调用需复制列表,浪费系统资源。
五、poll模型详解
1. 核心结构
-
监视列表类型 :
struct pollfd
数组(可动态扩容,栈/堆空间均可)。|---|-------------------------------------------------------|
| |struct pollfd {
|
| |int fd; // 目标描述符
|
| |short events; // 监控事件(如POLLIN=可读,POLLOUT=可写)
|
| |short revents; // 激活事件(0=未激活,POLLIN/POLLOUT=对应事件激活)
|
| |};
|
2. 监视函数poll
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 参数 :
fds
:struct pollfd
数组(同时作为监视列表和激活列表)。nfds
:数组长度(用户自行管理)。timeout
:超时时间(毫秒,-1=永久阻塞,0=非阻塞)。
- 特点 :激活事件通过
revents
标记,不会覆盖原列表,无需额外备份。
3. 优缺点
- 优点 :
- 监视列表可动态扩容,灵活性高。
- 无需反复复制列表,资源开销低。
- 缺点:仍需遍历整个列表判断激活状态,效率较低(未解决select的遍历问题)。
六、作业要求
1. 服务器端
- 使用select模型 实现:
- 接受多个客户端连接。
- 转发客户端间的聊天消息。
2. 客户端
-
使用poll模型 实现:
- 同时处理"读取服务器消息"(
read
)和"键盘输入"(scanf
)。 - 限制:不允许开启额外线程或进程。
- 同时处理"读取服务器消息"(
-
hesd.h
#ifndef HEAD_H
#define HEAD_H#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <sys/types.h>
#include <poll.h>#define PORT 8888 // 服务器端口
#define MAX_CLIENTS 10 // 最大客户端数
#define BUF_SIZE 1024 // 消息缓冲区大小// 客户端数组操作函数声明
void insert_client(int arr[], int newclient, int* len);
void remove_client(int arr[], int client, int* len);#endif
server.c
#include "head.h"
// 添加新客户端到数组
void insert_client(int arr[], int newclient, int* len) {
arr[*len] = newclient;
(*len)++;
}
// 从数组中移除客户端
void remove_client(int arr[], int client, int* len) {
for (int i = 0; i < *len; i++) {
if (arr[i] == client) {
for (int j = i; j < *len - 1; j++) {
arr[j] = arr[j + 1]; // 前移覆盖
}
(*len)--;
break;
}
}
}
int main(int argc, const char* argv[]) {
if (argc < 2) {
printf("用法:%s <端口>\n", argv[0]);
return 1;
}
short port = atoi(argv[1]);
int client_arr[MAX_CLIENTS] = {0}; // 存储客户端套接字
int client_count = 0; // 当前客户端数量
// 创建服务器套接字
int server = socket(AF_INET, SOCK_STREAM, 0);
if (server == -1) { perror("socket"); return 1; }
// 绑定地址
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(server, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind"); close(server); return 1;
}
// 开始监听
if (listen(server, 10) == -1) { perror("listen"); close(server); return 1; }
printf("服务器启动,监听端口 %d...\n", port);
// 初始化select监视图(仅监控服务器套接字)
fd_set list;
FD_ZERO(&list);
FD_SET(server, &list);
int max_fd = server + 1; // select需要的最大fd+1
while (1) {
fd_set temp = list; // 复制监视图(select会修改temp)
int ret = select(max_fd, &temp, NULL, NULL, NULL);
if (ret == -1) { perror("select"); continue; }
// 处理新客户端连接
if (FD_ISSET(server, &temp)) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client = accept(server, (struct sockaddr*)&client_addr, &client_len);
if (client == -1) { perror("accept"); continue; }
FD_SET(client, &list); // 加入监视图
insert_client(client_arr, client, &client_count); // 加入数组
printf("新客户端连接:%s:%d(套接字:%d)\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), client);
if (client + 1 > max_fd) max_fd = client + 1; // 更新最大fd
}
// 处理客户端消息(转发给其他客户端)
for (int i = 0; i < client_count; i++) {
int client = client_arr[i];
if (FD_ISSET(client, &temp)) {
char buf[BUF_SIZE] = {0};
int res = read(client, buf, sizeof(buf));
if (res == -1) { // 读出错
perror("read");
FD_CLR(client, &list);
remove_client(client_arr, client, &client_count);
close(client);
continue;
} else if (res == 0) { // 客户端断开
printf("客户端 %d 断开连接\n", client);
FD_CLR(client, &list);
remove_client(client_arr, client, &client_count);
close(client);
} else { // 转发消息
printf("转发消息:%s(来自客户端 %d)\n", buf, client);
for (int j = 0; j < client_count; j++) {
int target = client_arr[j];
if (target != client) { // 排除自己
write(target, buf, res);
}
}
}
}
}
}
close(server);
return 0;
}
client1/2
#include "head.h"
int main() {
// 连接服务器
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) { perror("socket"); return 1; }
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("connect"); close(sockfd); return 1;
}
printf("连接服务器成功!\n");
// poll 监控两个fd:服务器套接字 + 标准输入(0)
struct pollfd fds[2];
fds[0].fd = sockfd; fds[0].events = POLLIN; // 监控服务器消息
fds[1].fd = 0; fds[1].events = POLLIN; // 监控键盘输入
while (1) {
int ret = poll(fds, 2, -1); // 阻塞等待事件
if (ret == -1) { perror("poll"); continue; }
// 处理服务器消息(fds[0])
if (fds[0].revents & POLLIN) {
char buf[BUF_SIZE] = {0};
int res = read(sockfd, buf, sizeof(buf));
if (res == -1) { perror("read"); break; }
if (res == 0) { printf("服务器断开!\n"); break; }
printf("收到消息:%s\n", buf);
}
// 处理键盘输入(fds[1])
if (fds[1].revents & POLLIN) {
char buf[BUF_SIZE] = {0};
fgets(buf, sizeof(buf), stdin); // 读取一行输入
buf[strcspn(buf, "\n")] = '\0'; // 去掉换行符
if (strlen(buf) == 0) continue; // 空输入跳过
write(sockfd, buf, strlen(buf)); // 发消息给服务器
}
}
close(sockfd);
return 0;
}
七、关键对比
特性 | select模型 | poll模型 |
---|---|---|
监视列表容量 | 固定(栈数组) | 动态(可堆数组) |
列表操作 | 需备份原列表(激活覆盖原列表) | 无需备份(revents标记激活) |
效率 | 双重循环,低 | 单循环,中等(仍需遍历) |
灵活性 | 低 | 高 |