2025 年 04 月 11 日
我们基于 select
显著提升了服务端的并发性能。在高兴之余,一定会有更擅长网络编程的朋友告诉我们,select
过时了,poll
更好,最好的是
epoll
……他们是正确的,但是现在我们需要的,还不是这种正确。select
的缺陷是,通常情况下最多只能支持 1024
个文件描述符,亦即基于它实现的服务端同时最多只能支持 1024
个客户端。事实上,服务端能同时支持这么多个客户端,已经非常了不起了。以后,待我们需要更了不起的时候,再研究
poll
甚至 epoll
也不迟。现在,我们要做的是,将基于 select
的同步 I/O
多路复用机制融入「封装」中所实现的
Sim 项目。
在「select
不负重望」中,我们用了一个固定长度的数组存储了一组套接字,这些套接字由
accept
创建,服务端可通过它们与客户端通信。为了便于叙述,我们今后将这类套接字简称为客户端套接字,有时也会图省事,直呼其为客户端。
我们需要实现一个较固定长度的数组更为灵巧的数据结构,将其用于存储客户端套接字。该数据结构应当由一些称为结点的数据单元构成,每个结点可存储 1 个套接字。这种数据结构通常有两种实现,一种是动态数组,一种是链表。我更喜欢链表。
以下代码定义了一个非常简单的单向链表类 SimList
并声明了其析构函数和 add
方法:
/* sim-list.h */
#ifndef SIM_LIST_H
#define SIM_LIST_H
#include <stdio.h>
typedef struct sim_list SimList;
struct sim_list {
void *data;
struct sim_list *next;
};
void sim_list_free(SimList *list);
/* 获取链表长度 */
size_t sim_list_size(SimList *list);
/* 向链表添加一条数据 */
*sim_list_add(SimList *self, void *data);
SimList /* 从链表删除一个单元(结点) */
*sim_list_delete(SimList *self, SimList *node);
SimList #endif
我没有为 SimList
对象提供构造函数,原因是我已将
SimList
的定义公布于 sim-list.h
里了,这意味着任何人都可以根据它的定义创建 SimList
对象。
以下代码实现了 SimList
对象的析构函数和所有方法:
/* sim-list.c */
#include <stdlib.h>
#include "sim-list.h"
void sim_list_free(SimList *list) {
if (list) {
*it = list;
SimList while (it) {
*next = it->next;
SimList (it);
free= next;
it }
}
}
size_t sim_list_size(SimList *list) {
if (list) {
size_t n = 0;
for (SimList *it = list; it; it = it->next) n++;
return n;
} else return 0;
}
*sim_list_add(SimList *self, void *data) {
SimList *new_node = malloc(sizeof(SimList));
SimList if (new_node) {
->data = data;
new_nodeif (self) new_node->next = self;
else new_node->next = NULL;
} else fprintf(stderr, "sim_list_add error!\n");
return new_node;
}
*sim_list_delete(SimList *self, SimList *node) {
SimList if (!node) return self;
*prev = NULL;
SimList for (SimList *it = self; it; it = it->next) {
if (it == node) break;
if (it->next == node) {
= it;
prev break;
}
}
if (prev) prev->next = node->next;
else self = node->next;
(node);
freereturn self;
}
作为 SimList
类的用法示例,将三个整型变量存储于
SimList
对象:
int a = 4, b = 5, c = 6;
*list = NULL;
SimList = sim_list_add(list, &a);
list = sim_list_add(list, &b);
list = sim_list_add(list, &c); list
以下代码遍历 list
,输出套接字,然后释放
list
:
for (SimList *it = list; it; it = it->next) {
int *i = it->data;
("socket %d\n", *i);
printf}
(list); sim_list_free
若上述 a
、b
、c
皆为局部变量,需要将它们的值存储于
list
,只需像下面这样做:
int a = 4, b = 5, c = 6;
*list = NULL;
SimList int *p;
= malloc(sizeof(int));
p *p = a;
= sim_list_add(list, p);
list = malloc(sizeof(int));
p *p = b;
= sim_list_add(list, p);
list = malloc(sizeof(int));
p *p = c;
= sim_list_add(list, p); list
要释放用于存储 a
、b
、c
值的内存,只需在对 list
析构前,遍历
list
,逐一释放 list->data
:
for (SimList *it = list; it; it = it->next) {
(it->data);
free}
(list); sim_list_free
不得不承认,用 SimList
存储套接字,有些笨,但终归可以摆脱固定长度的数组的桎梏了。实际上,我们可以为
SimList
实现一个更方便的 add
的方法,只不过该方法是一个宏:
/* sim-list.h ++ */
#define SIM_LIST_ADD(self, value, type) do { \
type *p = malloc(sizeof(type)); \
*p = value; \
self = sim_list_add(self, p); \
} while (0)
基于 SIM_LIST_ADD
宏,可将上述将局部变量
a
、b
、c
的值存储于
list
的过程简化为
int a = 4, b = 5, c = 6;
*list = NULL;
SimList
(list, a, int);
SIM_LIST_ADD(list, b, int);
SIM_LIST_ADD(list, c, int); SIM_LIST_ADD
一定要注意,SIM_LIST_ADD
的第一个参数必须是
SimList *
类型的变量。此外,无论是
sim_list_add
还是
SIM_LIST_ADD
,新建的链表单元总是位于链表之首。这个
add
方法,它更准确的名字应该是
prepend
。不过,我喜欢 add
,这个名字短。
SimServer
对象原本只能记录一个客户端套接字,现在我们试图让它具备并发能力,这意味着它需要存储多个客户端套接字,具体数量未知。为此,我们可以用
SimList
对象作为 SimServer
对象存储多个客户端套接字的容器:
/* sim-network.h ++ */
#include <sim-list.h>
/* sim-network.c ++ */
#include <fcntl.h>
#include <sys/select.h>
/* sim-network.c [改] */
struct sim_server {
int listener;
*clients;
SimList ;
fd_set read_fds;
fd_set write_fdsconst char *error;
};
SimServer
对象也许从未想过有一天自己会如此富有,以至于对自己的构造和析构函数都要更加小心翼翼了。
/* sim-network.c [改] */
static int socket_nonblock(int x);
*sim_server(const char *host, const char *port) {
SimServer int fd = first_valid_address(host, port, bind);
if (fd == -1) return NULL;
if (listen(fd, 10) == -1) return NULL;
(fd); /* 将 fd 设为非阻塞状态 */
socket_nonblock
*server = malloc(sizeof(SimServer));
SimServer if (!server) {
(stderr, "sim_server error!\n");
fprintfreturn NULL;
}
->listener = fd;
server->clients = NULL;
server->error = NULL;
serverreturn server;
}
static int socket_nonblock(int x) {
return fcntl(x, F_SETFL, fcntl(x, F_GETFL) | O_NONBLOCK);
}
/* sim-network.c [改] */
void sim_server_free(SimServer *server) {
if (server) {
(server->listener);
closeif (server->clients) {
for (SimList *it = server->clients; it; it = it->next) {
if (it->data) free(it->data);
}
(server->clients);
sim_list_free}
(server);
free}
}
需要注意的是,sim_server
函数中,将用于监听的套接字设成了非阻塞状态,这是实现服务端对 I/O
多路复用机制的支持所必须的。
将基于 select
的 I/O 多路复用机制纳入
sim_server_run
函数:
/* sim-network.c [改] */
void sim_server_run(SimServer *self) {
int fd_max;
/* 尽量让 select 成功运行 */
while (1) {
(&self->read_fds);
FD_ZERO(&self->write_fds);
FD_ZERO(self->listener, &self->read_fds);
FD_SET= self->listener;
fd_max for (SimList *it = self->clients; it; it = it->next) {
int client = *(int *)(it->data);
(client, &self->read_fds);
FD_SET(client, &self->write_fds);
FD_SETif (fd_max < client) fd_max = client;
}
if (select(fd_max + 1,
&self->read_fds,
&self->write_fds,
,
NULL) == -1) {
NULLif (errno == EINTR) continue;
else {
->error = "sim_server_run error!";
selfbreak;
}
} else break;
}
/* 接纳新的连接 */
if (FD_ISSET(self->listener, &self->read_fds)) {
/* 尽量让 accept 运行成功 */
while (1) {
int fd = accept(self->listener, NULL, NULL);
if (fd == -1) {
if (errno == EINTR) continue;
else {
->error = "sim_server_run error!";
selfbreak;
}
} else {
/* 将 fd 设为非阻塞状态 */
(fd);
socket_nonblock/* 将 fd 加入 self->clients */
(self->clients, fd, int);
SIM_LIST_ADD/* 恢复 self 无错状态 */
if (self->error) self->error = NULL;
break;
}
}
}
}
上述代码中,两处 while (1)
循环只是为了尽量保证
select
和 accept
不会受系统中断信号的干扰而出错,详见「可以挽救的错误」,在通常情况下,这两处
while (1)
的循环体只运行一次便可退出,故而不会无限循环。
正常情况下,每运行一次
sim_server_run
,SimServer
对象的
clients
成员便可纳入一个新的客户端套接字。在下一次运行
sim_server_run
时,该套接字会被 select
提交给系统审批。若审批通过,该套接字会被保留在 read_fds
和
write_fds
中。
sim_server_receive
依然基于「可以挽救的错误」中的
recv_robustly
从客户端套接字接收数据,只是现在需要遍历
SimServer
对象的 clients
中的每个客户端套接字,若该套接字在 SimServer
的
read_fds
中,则通过它接收数据。此外,由于可能有多个客户端套接字皆可读,从每个套接字读取的数据保存在一个
SimStr
对象里,故而我们需要用一个 SimList
对象存储这些 SimStr
对象。
/* sim-network.h [改] */
*sim_server_receive(SimServer *self); SimList
/* sim-network.c [改] */
*sim_server_receive(SimServer *self) {
SimList *msgs = NULL;
SimList *it = self->clients;
SimList while (it) {
int client = *(int *)(it->data);
if (FD_ISSET(client, &self->read_fds)
&& FD_ISSET(client, &self->write_fds)) {
*msg = recv_robustly(client);
SimStr if (msg) {
= sim_list_add(msgs, msg);
msgs } else {
/* 客户端套接字无法读取,可能对端已经关闭了连接 */
/* 我们需要从 self->clients 移除 it,并关闭 client */
*next = it->next;
SimList (it->data);
free->clients = sim_list_delete(self->clients, it);
self= next;
it (client);
closecontinue;
}
}
= it->next;
it }
return msgs;
}
sim_server_send
需要遍历 SimServer
对象的
clients
中的每个客户端套接字,若该套接字在
SimServer
的 write_fds
中,则通过它向客户端发送数据。
/* sim-network.c [改] */
void sim_server_send(SimServer *self, SimStr *msg) {
*it = self->clients;
SimList while (it) {
int client = *(int *)(it->data);
if (FD_ISSET(client, &self->read_fds)
&& FD_ISSET(client, &self->write_fds)) {
if (send_robustly(client, msg) == -1) {
/* 客户端套接字无法写入数据,可能对端已经关闭了连接 */
/* 我们需要从 self->clients 移除 it,并关闭 client */
*next = it->next;
SimList (it->data);
free->clients = sim_list_delete(self->clients, it);
self= next;
it (client);
closecontinue;
}
}
= it->next;
it }
}
现在,可以重写 threebody.c,欺骗 ywj 和 other-ywj,让她们都以为自己得到了 threebody 的眷顾,前者很快得到了 threebody 的回复,后者以为 threebody 颇有耐心,容许她的迟疑……
/* threebody.c */
#include "sim-network.h"
int main(void) {
*threebody = sim_server("localhost", "8080");
SimServer if (!threebody) {
(stderr, "sim_server failed!\n");
fprintf(-1);
exit}
/* 服务端程序运转 */
while (1) {
(threebody);
sim_server_runif (sim_server_safe(threebody)) {
/* 从客户端接收信息 */
*msgs = sim_server_receive(threebody);
SimList if (msgs) {
for (SimList *it = msgs; it; it = it->next) {
*msg = it->data;
SimStr ("%s\n", sim_str_raw(msg));
printf(msg);
sim_str_free}
(msgs);
sim_list_free}
/* 向客户端发送信息 */
*msg = sim_str("threebody: Hi");
SimStr (threebody, msg);
sim_server_send(msg);
sim_str_free}
}
(threebody);
sim_server_freereturn 0;
}
本文最突出的工作并非营造一个具备并发能力的 threebody,而是定义了
SimList
这个单向链表类,没有它的支持,服务端便无法以一种可变长度的数据结构存储数量不固定的客户端套接字。单向链表的实现较为简单,但弊端也是有的。例如,获取链表长度,时间复杂度为
O(n),因为要遍历所有结点。删除链表中的某个结点,时间复杂度也为
O(n),因为需要从链表中找到该结点的前一个结点,方能完成删除过程。不过,由于
select
最多只能支持 1024
个客户端并发访问,单向链表的性能并非不可接受。
下面是一些你可以不劳而获的成果: