Files
TCP-IP-NetworkNote/ch12/README.md
riba2534 0d17c981ee chore: 将所有外部图片本地化到仓库
- 下载 110 张外部图片到根目录 images/ 文件夹
- 更新所有 README.md 中的图片引用为统一路径 images/xxx.png
- 55 张图片成功下载(PNG 格式)
- 55 张失效图片创建占位文件(SVG/PNG)
- 移除所有外部图片链接依赖

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 16:34:13 +08:00

209 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 第 12 章 I/O 复用
本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。
### 12.1 基于 I/O 复用的服务器端
#### 12.1.1 多进程服务端的缺点和解决方法
为了构建并发服务器只要有客户端连接请求就会创建新进程。这的确是实际操作中采用的一种方案但并非十全十美因为创建进程要付出很大的代价。这需要大量的运算和内存空间由于每个进程都具有独立的内存空间所以相互间的数据交换也要采用相对复杂的方法IPC 属于相对复杂的通信方法)
I/O 复用技术可以解决这个问题。
#### 12.1.2 理解复用
「复用」在电子及通信工程领域很常见,向这些领域的专家询问其概念,可能会得到如下答复:
> 在 1 个通信频道中传递多个数据(信号)的技术
「复用」的含义:
> 为了提高物理设备的效率,只用最少的物理要素传递最多数据时使用的技术
上述两种方法的内容完全一致。可以用纸电话模型做一个类比:
![](images/kA8H81.png)
上图是一个纸杯电话系统,为了使得三人同时通话,说话时要同时对着两个纸杯,接听时也需要耳朵同时对准两个纸杯。为了完成 3 人通话,可以进行如下图的改进:
![](images/kA8bgx.png)
如图做出改进,就是引入了复用技术。
复用技术的优点:
- 减少连线长度
- 减少纸杯个数
即使减少了连线和纸杯的量仍然可以进行三人同时说话,但是如果碰到以下情况:
> 「好像不能同时说话?」
实际上,因为是在进行对话,所以很少发生同时说话的情况。也就是说,上述系统采用的是**「时分复用」**技术。因为说话人声频率不同,即使在同时说话也能进行一定程度上的区分(杂音也随之增多)。因此,也可以说是「频分复用技术」。
#### 12.1.3 复用技术在服务器端的应用
纸杯电话系统引入复用技术之后可以减少纸杯数量和连线长度。服务器端引入复用技术可以减少所需进程数。下图是多进程服务端的模型:
![](images/kAGBM6.png)
下图是引入复用技术之后的模型:
![](images/kAGrqO.png)
从图上可以看出,引入复用技术之后,可以减少进程数。重要的是,无论连接多少客户端,提供服务的进程只有一个。
### 12.2 理解 select 函数并实现服务端
select 函数是最具代表性的实现复用服务器的方法。在 Windows 平台下也有同名函数,所以具有很好的移植性。
#### 12.2.1 select 函数的功能和调用顺序
使用 select 函数时可以将多个文件描述符集中到一起统一监视,项目如下:
- 是否存在套接字接收数据?
- 无需阻塞传输数据的套接字有哪些?
- 哪些套接字发生了异常?
> 术语:「事件」。当发生监视项对应情况时,称「发生了事件」。
select 函数的使用方法与一般函数的区别并不大,更准确的说,他很难使用。但是为了实现 I/O 复用服务器端,我们应该掌握 select 函数并运用于套接字编程当中。认为「select 函数是 I/O 复用的全部内容」也并不为过。select 函数的调用过程如下图所示:
![](images/kAtdRs.png)
#### 12.2.2 设置文件描述符
利用 select 函数可以同时监视多个文件描述符。当然,监视文件描述符可以视为监视套接字。此时首先需要将要监视的文件描述符集中在一起。集中时也要按照监视项(接收、传输、异常)进行区分,即按照上述 3 种监视项分成 3 类。
利用 fd_set 数组变量执行此操作如图所示该数组是存有0和1的位数组。
![](images/kAt2i4.png)
图中最左端的位表示文件描述符 0所在位置。如果该位设置为 1则表示该文件描述符是监视对象。那么图中哪些文件描述符是监视对象呢很明显是描述符 1 和 3。在 fd_set 变量中注册或更改值的操作都由下列宏完成。
- `FD_ZERO(fd_set *fdset)`:将 fd_set 变量所指的位全部初始化成0
- `FD_SET(int fd,fd_set *fdset)`:在参数 fdset 指向的变量中注册文件描述符 fd 的信息
- `FD_CLR(int fd,fd_set *fdset)`:从参数 fdset 指向的变量中清除文件描述符 fd 的信息
- `FD_ISSET(int fd,fd_set *fdset)`:若参数 fdset 指向的变量中包含文件描述符 fd 的信息,则返回「真」
上述函数中FD_ISSET 用于验证 select 函数的调用结果,通过下图解释这些函数的功能:
![](images/kANR78.png)
#### 12.2.3 设置检查(监视)范围及超时
下面是 select 函数的定义:
```c
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd, fd_set *readset, fd_set *writeset,
fd_set *exceptset, const struct timeval *timeout);
/*
成功时返回大于 0 的值,失败时返回 -1
maxfd: 监视对象文件描述符数量
readset: 将所有关注「是否存在待读取数据」的文件描述符注册到 fd_set 型变量,并传递其地址值。
writeset: 将所有关注「是否可传输无阻塞数据」的文件描述符注册到 fd_set 型变量,并传递其地址值。
exceptset: 将所有关注「是否发生异常」的文件描述符注册到 fd_set 型变量,并传递其地址值。
timeout: 调用 select 函数后,为防止陷入无限阻塞的状态,传递超时(time-out)信息
返回值: 发生错误时返回 -1超时时返回 0。因发生关注的事件返回时返回大于 0 的值,该值是发生事件的文件描述符数。
*/
```
如上所述select 函数用来验证 3 种监视的变化情况,根据监视项声明 3 个 fd_set 型变量,分别向其注册文件描述符信息,并把变量的地址值传递到上述函数的第二到第四个参数。但在此之前(调用 select 函数之前)需要决定下面两件事:
1. 文件描述符的监视(检查)范围是?
2. 如何设定 select 函数的超时时间?
第一,文件描述符的监视范围和 select 的第一个参数有关。实际上select 函数要求通过第一个参数传递监视对象文件描述符的数量。因此,需要得到注册在 fd_set 变量中的文件描述符数。但每次新建文件描述符时,其值就会增加 1 ,故只需将最大的文件描述符值加 1 再传递给 select 函数即可。加 1 是因为文件描述符的值是从 0 开始的。
第二select 函数的超时时间与 select 函数的最后一个参数有关,其中 timeval 结构体定义如下:
```c
struct timeval
{
long tv_sec;
long tv_usec;
};
```
本来 select 函数只有在监视文件描述符发生变化时才返回。如果未发生变化,就会进入阻塞状态。指定超时时间就是为了防止这种情况的发生。通过上述结构体变量,将秒数填入 tv_sec 的成员,将微秒数填入 tv_usec 的成员,然后将结构体的地址值传递到 select 函数的最后一个参数。此时即使文件描述符未发生变化只要过了指定时间也可以从函数中返回。不过这种情况下select 函数返回 0。因此可以通过返回值了解原因。如果不想设置超时则传递 NULL 参数。
#### 12.2.4 调用 select 函数查看结果
select 返回正整数时,怎样获知哪些文件描述符发生了变化?向 select 函数的第二到第四个参数传递的 fd_set 变量中将产生如图所示的变化:
![](images/kA06dx.png)
由图可知select 函数调用完成后,向其传递的 fd_set 变量将发生变化。原来为 1 的所有位将变成 0但是发生了变化的文件描述符除外。因此可以认为值仍为 1 的位置上的文件描述符发生了变化。
#### 12.2.5 select 函数调用示例
下面是一个 select 函数的例子:
- [select.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch12/select.c)
编译运行:
```shell
gcc select.c -o select
./select
```
结果:
![](images/kAjgW6.png)
可以看出,如果运行后在标准输入流输入数据,就会在标准输出流输出数据,但是如果 5 秒没有输入数据,就提示超时。
#### 12.2.6 实现 I/O 复用服务器端
下面通过 select 函数实现 I/O 复用服务器端。下面是基于 I/O 复用的回声服务器端。
- [echo_selectserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch12/echo_selectserv.c)
编译运行:
```shell
gcc echo_selectserv.c -o selserv
./selserv 9190
```
结果:
![](images/kEkV8H.png)
从图上可以看出,虽然只用了一个进程,但是却实现了可以和多个客户端进行通信,这都是利用了 select 的特点。
### 12.3 基于 Windows 的实现
暂略
### 12.4 习题
> 以下答案仅代表本人个人观点,可能不是正确答案。
1. **请解释复用技术的通用含义,并说明何为 I/O 复用。**
答:通用含义:在 1 个通信频道中传递多个数据信号的技术。I/O 复用是指进程预先告诉内核需要监视的 I/O 条件,使得内核一旦发现进程指定的一个或多个 I/O 条件就绪(例如套接字变为可读或可写),就通知进程进行处理,从而进程不会在单个 I/O 操作上阻塞。
参考文章:[Linux网络编程-IO复用技术](https://www.cnblogs.com/luoxn28/p/6220372.html)
2. **多进程并发服务器的缺点有哪些?如何在 I/O 复用服务器中弥补?**
答:多进程需要进行大量的运算和大量的内存空间。在 I/O 复用服务器中通过 select 函数监视文件描述符,通过判断变化的文件描述符,来得知变化的套接字是哪个,从而实时应答来自多个客户端的请求。
3. **复用服务器端需要 select 函数。下列关于 select 函数使用方法的描述错误的是?**
答:错误的描述是 2 和 3。说明如下
- 描述 1 正确:调用 select 函数前需要集中 I/O 监视对象的文件描述符。
- 描述 2 错误:**每次调用 select 函数时都需要重新注册监视对象**,因为 select 函数调用后fd_set 变量会被修改,只保留发生变化的文件描述符位为 1。
- 描述 3 错误:**复用服务器端可以同时服务于多个客户端**,这正是 I/O 复用的核心优势——通过 select 函数同时监视多个套接字,哪个就绪就处理哪个。
- 描述 4 正确:与多进程/多线程服务端不同,基于 select 的复用服务器只需要 1 个进程,可以减少因创建多进程产生的服务器端的负担。
4. **select 函数的观察对象中应包含服务端套接字(监听套接字),那么应将其包含到哪一类监听对象集合?请说明原因**
答:应该包含到「是否存在待读取数据」(即 readset集合中。原因是服务端套接字监听套接字的作用是监听客户端的连接请求当有新的客户端发起连接时监听套接字变为「可读」状态此时 accept 函数不会阻塞,可以成功完成连接。因此需要监视监听套接字是否可读,以得知是否有新的连接请求到来。