「Linux 网络编程」基于 poll 的 I/O 复用服务器

0 回顾

I/O 多路复用 允许单个进程同时监视多个文件描述符(如网络套接字),当其中任何一个准备就绪时(可读、可写或发生异常),进程就能得到通知。这种机制避免了为每个连接创建线程或进程的开销。

I/O 多路复用的本质是:一个“消息通知”机制。它的核心工作不是同时处理多个 I/O,而是 高效地查看到哪个 I/O 已经准备就绪。

相比 多进程/多线程 的并发服务器的 真正并行 的实现方式,I/O 多路复用实际上采用的是 时分复用 技术,通过高效地轮询和响应大量任务的事件。

「Linux 网络编程」基于 select 的 I/O 复用服务器



1 poll 函数使用和工作原理

poll 也是一个在 Unix/Linux 系统中用于 I/O 多路复用 的系统调用。它的核心功能是允许一个进程(或线程)同时监视多个文件描述符(如网络套接字、管道等),并 检查其中哪些已经就绪,可以进行非阻塞的读、写或错误处理。


1.1 poll 函数

函数原型

1
2
3
#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数说明

  • fds: 指向 pollfd 结构体数组的指针
  • nfds: 数组中元素的数量
  • timeout: 超时时间(毫秒),-1 表示无限等待

返回值

  • > 0: 就绪的文件描述符数量
  • 0: 超时
  • -1: 出错

1.2 pollfd 结构体

1
2
3
4
5
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 请求的事件 */
short revents; /* 返回的事件 */
};

成员说明

  • fd: 要监视的文件描述符。
  • events: 你关心的事件(例如,POLLIN 表示数据可读,POLLOUT 表示可以写数据)。
  • revents: 由内核返回的事件,表示哪些你关心的事件真正发生了。

常用事件标志

  • POLLIN: 数据可读
  • POLLOUT: 数据可写
  • POLLERR: 发生错误
  • POLLHUP: 连接挂起

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <poll.h>

// 假设有两个套接字文件描述符:sock1 和 sock2
struct pollfd fds[2];
int ret;

// 设置监视 sock1 的读事件
fds[0].fd = sock1;
fds[0].events = POLLIN;

// 设置监视 sock2 的写事件
fds[1].fd = sock2;
fds[1].events = POLLOUT;

// 等待,无限期阻塞直到有事件发生
ret = poll(fds, 2, -1);

if (ret > 0) {
// 检查 sock1 是否可读
if (fds[0].revents & POLLIN) {
// 从 sock1 读取数据,这不会阻塞
read(sock1, buffer, sizeof(buffer));
}
// 检查 sock2 是否可写
if (fds[1].revents & POLLOUT) {
// 向 sock2 写入数据
write(sock2, data, data_len);
}
}

1.3 poll 在服务端的工作流程

  1. 准备文件描述符列表: 服务端首先准备一个 struct pollfd 类型的数组。每个结构体包含三个主要字段:

    • fd: 要监视的文件描述符。
    • events: 关心的事件(例如,POLLIN 表示数据可读,POLLOUT 表示可以写数据)。
    • revents: 由内核返回的事件,表示哪些你关心的事件真正发生了。
  2. 发起系统调用: 服务端调用 poll(fds, nfds, timeout) 函数,将这个数组、数组长度和一个超时时间传递给内核。调用 poll 函数并 阻塞等待 有事件发生或超时。

  3. 内核监视与等待: 内核会挂起这个进程,直到发生以下情况之一:

    • 一个或多个被监视的文件描述符 就绪(有数据可读、可以写等)。
    • 超时,指定的超时时间到了。
  4. 返回结果poll 调用返回,并填充每个 struct pollfd 中的 revents 字段。服务端中遍历这个 pollfd 数组,检查哪些文件描述符的 revents 被设置了相应标志,然后对这些就绪的文件描述符进行相应的 I/O 操作


1.4 poll 相比 select 的优势

  1. 没有文件描述符数量限制

    这是最核心的优势。

    • select:使用一个固定大小的位数组(fd_set)来存储文件描述符。这个数组的大小通常由 FD_SETSIZE 宏定义(在很多系统上,比如 1024)。这意味着 select 能监视的文件描述符数量 有硬性上限(例如 1024)。对于需要处理大量并发连接的高性能服务器来说,这是一个致命的缺陷。
    • poll:使用一个由 struct pollfd 结构体组成的数组,这个数组由调用者分配。理论上,这个数组的大小只受系统内存和进程文件描述符上限的限制。这使得 poll 能够轻松处理成千上万的并发连接。
  2. 更高效的事件处理

    • select:它使用三个独立的位集合(readfds)。每次调用时,需要设置这集合,告诉内核你关心哪些事件。当 select 返回时,它会 修改传入的集合,将其置为就绪的文件描述符。因此,每次调用前,你必须 重置 这些集合(通常通过 FD_ZEROFD_SET),增加了额外的开销。

    • poll:使用一个统一的 struct pollfd 数组。每个结构体包含:

    1
    2
    3
    4
    5
    struct pollfd {
    int fd; // 文件描述符
    short events; // 我们关心的事件(输入)
    short revents; // 实际发生的事件(输出)
    };

    eventsrevents 是分离的。设置 events 来告知内核你关心什么,内核通过修改 revents 来返回结果。这意味着 不需要在每次调用 poll 时重新初始化整个数组,只需根据上一轮的结果重置 revents 字段(或者直接检查)即可。



2 基于 poll 的 I/O 复用服务器

2.1 头文件与宏定义

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <errno.h>

#define MAX_CLIENTS 1024 // 最大连接数
#define BUFFER_SIZE 1024 // 数据缓冲区大小
#define PORT 8080 // 服务器监听端口

参数说明

  • MAX_CLIENTS: 服务器支持的最大并发客户端数
  • BUFFER_SIZE: 数据读写缓冲区大小
  • PORT: 服务器监听端口号

2.2 服务器 Socket 创建和地址配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int server_init(int port) {
int server_fd;
struct sockaddr_in server_addr;

// 创建TCP套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket 创建失败");
exit(EXIT_FAILURE);
}

// 设置SO_REUSEADDR选项,避免地址占用
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
perror("setsockopt 失败");
close(server_fd);
exit(EXIT_FAILURE);
}

// 配置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口
server_addr.sin_port = htons(port); // 端口号转换为网络字节序

// 绑定地址和端口
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind 失败");
close(server_fd);
exit(EXIT_FAILURE);
}

// 开始监听,等待队列长度为128
if (listen(server_fd, 128) == -1) {
perror("listen 失败");
close(server_fd);
exit(EXIT_FAILURE);
}

return server_fd;
}

功能说明:

  • socket(AF_INET, SOCK_STREAM, 0):创建 TCP socket

    • AF_INET:IPv4 地址族
    • SOCK_STREAM:面向连接的 TCP socket
  • setsockopt(SO_REUSEADDR):设置地址重用,避免"client_addr already in use"错误,允许快速重启服务器。

  • bind():将 socket 绑定到指定地址和端口

    • INADDR_ANY:监听所有可用网络接口
    • htons(PORT):将端口号转换为网络字节序
  • listen(server_fd, 128):开始监听连接,backlog=128


2.3 主函数设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
int main() {
int server_fd;
char buffer[BUFFER_SIZE];
struct pollfd fds[MAX_CLIENTS + 1]; // +1 给服务器套接字
int nfds = 1; // 初始只有服务器套接字
int timeout = -1; // 无限等待

// 初始化pollfd数组
memset(fds, 0, sizeof(fds));

server_fd = server_init(PORT);

// 添加服务器套接字到监视列表
fds[0].fd = server_fd;
fds[0].events = POLLIN; // 监视可读事件(新连接)

while (1) {
// 调用poll等待事件
int ret = poll(fds, nfds, timeout);

if (ret < 0) {
perror("poll 失败");
break;
}

if (ret == 0) {
printf("poll 超时\n");
continue;
}

// 遍历所有文件描述符检查事件
for (int i = 0; i < nfds; i++) {
if (fds[i].revents == 0)
continue;

// 服务器套接字就绪 - 处理新连接
if (fds[i].fd == server_fd) {
handle_new_connection(server_fd, fds, &nfds, MAX_CLIENTS);
} else { // 客户端套接字就绪 - 处理数据
handle_client_data(fds[i].fd, fds, &nfds, i, buffer);
}
}
}

return 0;
}

流程说明

  • 初始化服务器和 poll 数据结构
  • 进入主事件循环
  • 调用 poll 等待事件发生
  • 处理超时和错误情况
  • 遍历检查所有文件描述符的事件
  • 分发事件到相应的处理函数

2.4 处理新的客户端连接请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void handle_new_connection(int server_fd, struct pollfd fds[], int *nfds, int max_clients) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_fd;

// 接受新连接
if ((client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len)) < 0) {
perror("accept");
return;
}

printf("新的客户端连接, socket fd: %d, IP: %s, port: %d\n",
client_fd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

// 检查客户端数量限制
if (*nfds >= max_clients + 1) {
printf("连接过多, rejecting...\n");
close(client_fd);
return;
}

// 发送欢迎消息
const char *welcome_msg = "欢迎连接到TCP服务器!输入 'quit' 退出连接。\n";
send(client_fd, welcome_msg, strlen(welcome_msg), 0);

// 添加新客户端到poll监视列表
fds[*nfds].fd = client_fd;
fds[*nfds].events = POLLIN; // 监视可读事件
fds[*nfds].revents = 0;
(*nfds)++;

printf("当前客户端数量: %d\n", *nfds - 1);
}

流程说明

  • 接受新的客户端连接
  • 检查是否达到最大客户端限制
  • 发送欢迎消息
  • 将新客户端添加到 poll 监视列表

参数说明

  • server_fd: 服务器监听套接字
  • fds[]: poll 文件描述符数组
  • nfds: 当前监视的文件描述符数量(指针)
  • max_clients: 最大客户端数

2.5 处理客户端数据通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void handle_client_data(int client_fd, struct pollfd fds[], int *nfds, int index, char buffer[]) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);

memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区

ssize_t bytes_received = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);

if (bytes_received == 0) {
// 客户端正常断开连接
getpeername(client_fd, (struct sockaddr*)&client_addr, &addr_len);
printf("客户端断开连接, IP: %s, port: %d\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
remove_client(fds, nfds, index);
} else if (bytes_received > 0) {
// 处理接收到的数据
buffer[strcspn(buffer, "\r\n")] = '\0'; // 去除换行符

printf("接收到客户端信息 %d: %s\n", client_fd, buffer);

// 检查是否为退出命令
if (strncmp("quit", buffer, 4) == 0) {
printf("客户端请求断开连接\n");
getpeername(client_fd, (struct sockaddr*)&client_addr, &addr_len);
remove_client(fds, nfds, index);
return;
}

// 回显数据给客户端
send(client_fd, buffer, strlen(buffer), 0);
}
}

流程说明:

  • 接收客户端数据
  • 处理连接断开情况
  • 处理"quit"退出命令
  • 回显数据给客户端

参数说明:

  • client_fd: 客户端套接字描述符
  • index: 客户端在 fds 数组中的索引
  • buffer[]: 数据缓冲区

2.6 客户端移除

1
2
3
4
5
6
7
8
9
10
11
12
void remove_client(struct pollfd fds[], int *nfds, int index) {
printf("移除客户端 fd: %d\n", fds[index].fd);

// 用最后一个元素覆盖要删除的元素,保持数组紧凑
fds[index] = fds[*nfds - 1];
fds[*nfds - 1].fd = -1;
fds[*nfds - 1].events = 0;
fds[*nfds - 1].revents = 0;
(*nfds)--;

printf("当前客户端数量: %d\n", *nfds - 1);
}

流程说明:

  1. 关闭客户端连接
  2. 从 poll 监视列表中移除客户端
  3. 维护 fds 数组的紧凑性

2.7 完整实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <errno.h>

#define MAX_CLIENTS 1024 // 同时处理的活跃连接数上限
#define BUFFER_SIZE 1024
#define PORT 8080

// 函数声明
int server_init(int port);
void handle_new_connection(int server_fd, struct pollfd fds[], int *nfds, int max_clients);
void handle_client_data(int client_fd, struct pollfd fds[], int *nfds, int index, char buffer[]);
void remove_client(struct pollfd fds[], int *nfds, int index);

int server_init(int port) {
int server_fd; // 服务器端套接字文件描述符
struct sockaddr_in server_addr; // 配置的服务器地址

// 创建socket, IPv4, TCP
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket 创建失败");
exit(EXIT_FAILURE);
}

// 设置 socket 选项
// TCP 服务器被杀死后尝试重启,一段时间处于 TIME_WAIT 状态
// SO_REUSEADDR 可以使得服务器快速重启,避免服务器重启绑定同一端口出现失败
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
perror("setsockopt 失败");
close(server_fd);
exit(EXIT_FAILURE);
}

// 配置服务器地址
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听任何端口
server_addr.sin_port = htons(port);

// 绑定地址
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind 失败");
close(server_fd);
exit(EXIT_FAILURE);
}

// 开始监听
// 这个10控制的是已完成三次握手但尚未被accept的连接队列长度
if (listen(server_fd, 128) == -1) { // 设置监听数量上限
perror("listen 失败");
close(server_fd);
exit(EXIT_FAILURE);
}

return server_fd;
}

void handle_new_connection(int server_fd, struct pollfd fds[], int *nfds, int max_clients) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_fd;

// 接受新连接
if ((client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len)) < 0) {
perror("accept");
return;
}

printf("新的客户端连接, socket fd: %d, IP: %s, port: %d\n",
client_fd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

// 检查是否达到最大客户端数
if (*nfds >= max_clients + 1) {
printf("连接过多, rejecting...\n");
close(client_fd);
return;
}

// 发送欢迎消息
const char *welcome_msg = "欢迎连接到TCP服务器!输入 'quit' 退出连接。\n";
send(client_fd, welcome_msg, strlen(welcome_msg), 0);

// 添加新客户端到 poll 监视列表
fds[*nfds].fd = client_fd;
fds[*nfds].events = POLLIN;
fds[*nfds].revents = 0;
(*nfds)++;

printf("当前客户端数量: %d\n", *nfds - 1);
}

void handle_client_data(int client_fd, struct pollfd fds[], int *nfds, int index, char buffer[]) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);

memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区

ssize_t bytes_received = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);

if (bytes_received == 0) {
// 客户端断开连接处理
getpeername(client_fd, (struct sockaddr*)&client_addr, &addr_len);
printf("客户端断开连接, IP: %s, port: %d\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
remove_client(fds, nfds, index);
} else if (bytes_received > 0) {
// 去除换行符
buffer[strcspn(buffer, "\r\n")] = '\0';

printf("接收到客户端信息 %d: %s\n", client_fd, buffer);

// 如果是 "quit" 命令,关闭连接
if (strncmp("quit", buffer, 4) == 0) {
printf("客户端请求断开连接\n");
getpeername(client_fd, (struct sockaddr*)&client_addr, &addr_len);
remove_client(fds, nfds, index);
return;
}

// 回显数据
send(client_fd, buffer, strlen(buffer), 0);
}
}

void remove_client(struct pollfd fds[], int *nfds, int index) {
printf("移除客户端 fd: %d\n", fds[index].fd);

// 用最后一个元素覆盖要删除的元素
fds[index] = fds[*nfds - 1];
fds[*nfds - 1].fd = -1;
fds[*nfds - 1].events = 0;
fds[*nfds - 1].revents = 0;
(*nfds)--;

printf("当前客户端数量: %d\n", *nfds - 1);
}

int main() {
int server_fd; // 服务器套接字
char buffer[BUFFER_SIZE]; // 数据 I/O 临时缓冲区
struct pollfd fds[MAX_CLIENTS + 1]; // pollfd 数组
int nfds = 1; // 初始只有服务器套接字
int timeout = -1; // 无限等待

// 初始化 pollfd 数组
memset(fds, 0, sizeof(fds));

printf("启动基于 poll 的 I/O 复用服务器,端口: %d\n", PORT);
printf("最大客户端数: %d\n", MAX_CLIENTS);

server_fd = server_init(PORT);
printf("服务器准备就绪,等待连接...\n");

// 添加服务器套接字到监视列表
fds[0].fd = server_fd;
fds[0].events = POLLIN;

while (1) {
int ret = poll(fds, nfds, timeout);

if (ret < 0) {
perror("poll 失败");
break;
}

if (ret == 0) {
printf("poll 超时\n");
continue;
}

// 检查所有文件描述符
for (int i = 0; i < nfds; i++) {
if (fds[i].revents == 0)
continue;

// 服务器套接字就绪 - 新连接
if (fds[i].fd == server_fd) {
handle_new_connection(server_fd, fds, &nfds, MAX_CLIENTS);
} else { // 客户端套接字就绪
handle_client_data(fds[i].fd, fds, &nfds, i, buffer);
}
}
}

return 0;
}

3 BACK_LOG 和 MAX_CLIENTS 的关系

3.1 listen(server_fd, 128): 监听队列大小

1
2
3
4
5
// 这个10控制的是已完成三次握手但尚未被accept的连接队列长度
if (listen(server_fd, 128) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}

作用:控制内核中等待被 accept 的连接队列的最大长度

详细解释

  • 当客户端发起连接时,完成 TCP 三次握手后,连接进入 ESTABLISHED 状态
  • 但此时应用程序还没有调用 accept() 获取这个连接
  • 这些"已建立但未被接受"的连接存放在内核的监听队列中
  • backlog=128 表示这个队列最多能存放 128 个这样的连接

队列满时的行为

  • 当第11个连接完成三次握手时,内核可能:
    • 直接拒绝连接(返回 RST)
    • 忽略 SYN 包,让客户端超时重传
  • 具体行为取决于操作系统配置

3.2 MAX_CLIENTS 1024: 应用程序管理的连接数

1
#define MAX_CLIENTS 1024  // 应用程序同时处理的活跃连接数上限

作用:控制应用程序已经接受并正在处理的活跃连接数量

详细解释

  • 这些是已经通过 accept() 获取并添加到 pollfd 数组中的连接
  • 应用程序需要为每个连接分配资源(内存、文件描述符等)
  • 这个限制防止应用程序资源耗尽

3.3 连接建立的生命周期

1
客户端 SYN → 服务器 SYN-ACK → 客户端 ACK → 进入监听队列 → 被 accept() → 成为活跃连接

实际场景示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 假设当前状态:
// - 监听队列大小:10
// - 最大客户端数:1024
// - 当前活跃连接:1000个

// 场景1:突然有20个新连接同时到达
// - 前10个进入监听队列(队列满)
// - 后10个可能被拒绝或延迟
// - 服务器从队列中accept前10个,现在活跃连接=1010个

// 场景2:活跃连接达到1024时
if (*nfds >= MAX_CLIENTS + 1) { // 包括服务器socket
printf("Too many connections, rejecting...\n");
close(new_client_fd); // 即使accept了也要立即关闭
return;
}

其实,backlog 大小设置既不能太小也不能太大:

  • 太小(如 10):在高并发场景下容易丢连接
  • 太大:浪费内核内存,且可能掩盖性能问题
  • 推荐:128-1024,根据预期并发连接数调整


4 I/O 复用服务器测试

4.1 测试客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
int sock;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];

// 创建socket
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket 创建失败");
exit(1);
}

// 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);

// 连接服务器
if (connect(sock, (struct sockaddr*)&server_addr,
sizeof(server_addr)) == -1) {
perror("连接服务器失败");
close(sock);
exit(1);
}

printf("已连接到服务器 %s:%d\n", SERVER_IP, PORT);

// 接收欢迎消息
memset(buffer, 0, BUFFER_SIZE);
recv(sock, buffer, BUFFER_SIZE - 1, 0);
printf("%s", buffer);

// 与服务器交互
while (1) {
printf("请输入消息: ");
fgets(buffer, BUFFER_SIZE, stdin);

// 发送消息
send(sock, buffer, strlen(buffer), 0);

// 检查是否退出
if (strncmp(buffer, "quit", 4) == 0) {
break;
}

// 接收回复
memset(buffer, 0, BUFFER_SIZE);
ssize_t bytes_received = recv(sock, buffer, BUFFER_SIZE - 1, 0);
if (bytes_received > 0) {
buffer[bytes_received] = '\0';
printf("服务器回复: %s\n", buffer);
}
}

close(sock);
printf("连接已关闭\n");

return 0;
}

4.2 运行测试

编译和运行

1
2
3
4
5
6
7
8
gcc poll_server.c -o poll_server
gcc client.c -o client

# 在终端运行服务器
./poll_server

# 在多个其他终端中运行客户端
./client

测试结果

服务端输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
marisa@marisa-virtual-machine:~/poll_test$ ./poll_server 
启动基于 poll 的 I/O 复用服务器,端口: 8080
最大客户端数: 1024
服务器准备就绪,等待连接...
新的客户端连接, socket fd: 4, IP: 127.0.0.1, port: 45660
当前客户端数量: 1
接收到客户端信息 4: hello 1
新的客户端连接, socket fd: 5, IP: 127.0.0.1, port: 40788
当前客户端数量: 2
接收到客户端信息 5: hello 2
接收到客户端信息 5: This is marisamagic
新的客户端连接, socket fd: 6, IP: 127.0.0.1, port: 57924
当前客户端数量: 3
接收到客户端信息 4: quit
客户端请求断开连接
移除客户端 fd: 4
当前客户端数量: 2
接收到客户端信息 6: hello 3
接收到客户端信息 6: This is alicedoll
接收到客户端信息 6: quit
客户端请求断开连接
移除客户端 fd: 6
当前客户端数量: 1
接收到客户端信息 5: quit
客户端请求断开连接
移除客户端 fd: 5
当前客户端数量: 0
^C

客户端 1 输出

1
2
3
4
5
6
7
marisa@marisa-virtual-machine:~/poll_test$ ./client
已连接到服务器 127.0.0.1:8080
欢迎连接到TCP服务器!输入 'quit' 退出连接。
请输入消息: hello 1
服务器回复: hello 1
请输入消息: quit
连接已关闭

客户端 2 输出

1
2
3
4
5
6
7
8
9
marisa@marisa-virtual-machine:~/poll_test$ ./client
已连接到服务器 127.0.0.1:8080
欢迎连接到TCP服务器!输入 'quit' 退出连接。
请输入消息: hello 2
服务器回复: hello 2
请输入消息: This is marisamagic
服务器回复: This is marisamagic
请输入消息: quit
连接已关闭

客户端 3 输出

1
2
3
4
5
6
7
8
9
marisa@marisa-virtual-machine:~/poll_test$ ./client
已连接到服务器 127.0.0.1:8080
欢迎连接到TCP服务器!输入 'quit' 退出连接。
请输入消息: hello 3
服务器回复: hello 3
请输入消息: This is alicedoll
服务器回复: This is alicedoll
请输入消息: quit
连接已关闭

「Linux 网络编程」基于 poll 的 I/O 复用服务器
https://marisamagic.github.io/2025/10/16/20251016/
作者
MarisaMagic
发布于
2025年10月16日
许可协议