Files
TCP-IP-NetworkNote/ch13/README.md
riba2534 a0535654be fix: 将错误命名的 .svg 文件重命名为 .png
- 8 个文件扩展名错误(.svg 但内容是 PNG)
- 已重命名为正确的 .png 扩展名
- 更新所有 README.md 中的图片引用

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

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

384 lines
15 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.
## 第 13 章 多种 I/O 函数
本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。
### 13.1 send & recv 函数
#### 13.1.1 Linux 中的 send & recv
首先看 send 函数定义:
```c
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
/*
成功时返回发送的字节数,失败时返回 -1
sockfd: 表示与数据传输对象的连接的套接字和文件描述符
buf: 保存待传输数据的缓冲地址值
nbytes: 待传输字节数
flags: 传输数据时指定的可选项信息
*/
```
下面是 recv 函数的定义:
```c
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
/*
成功时返回接收的字节数(收到 EOF 返回 0失败时返回 -1
sockfd: 表示数据接收对象的连接的套接字文件描述符
buf: 保存接收数据的缓冲地址值
nbytes: 可接收的最大字节数
flags: 接收数据时指定的可选项参数
*/
```
send 和 recv 函数的最后一个参数是收发数据的可选项该选项可以用位或bit OR运算符| 运算符)同时传递多个信息。
send & recv 函数的可选项意义:
| 可选项Option | 含义 | send | recv |
| ---------------- | ------------------------------------------------------------ | ---- | ---- |
| MSG_OOB | 用于传输带外数据Out-of-band data | O | O |
| MSG_PEEK | 验证输入缓冲中是否存在接收的数据 | X | O |
| MSG_DONTROUTE | 数据传输过程中不参照本地路由Routing在本地Local网络中寻找目的地 | O | X |
| MSG_DONTWAIT | 调用 I/O 函数时不阻塞用于使用非阻塞Non-blockingI/O | O | O |
| MSG_WAITALL | 防止函数返回,直到接收到全部请求的字节数 | X | O |
#### 13.1.2 MSG_OOB发送紧急消息
MSG_OOB 可选项用于创建特殊发送方法和通道以发送紧急消息。下面为 MSG_OOB 的示例代码:
- [oob_recv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch13/oob_recv.c)
- [oob_send.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch13/oob_send.c)
编译运行:
```shell
gcc oob_send.c -o send
gcc oob_recv.c -o recv
```
运行结果:
![](images/5c4bda167ae08.png)
![](images/5c4bdb4d99823.png)
从运行结果可以看出send 是客户端recv 是服务端,客户端给服务端发送消息,服务端接收完消息之后显示出来。可以从图中看出,每次运行的效果,并不是一样的。
代码中关于:
```c
fcntl(recv_sock, F_SETOWN, getpid());
```
的意思是:
> 文件描述符 recv_sock 指向的套接字引发的 SIGURG 信号处理进程变为 getpid 函数返回值用作 ID 进程.
上述描述中的「处理 SIGURG 信号」指的是「调用 SIGURG 信号处理函数」。但是之前讲过,多个进程可以拥有 1 个套接字的文件描述符。例如,通过调用 fork 函数创建子进程并同时复制文件描述符。此时如果发生 SIGURG 信号,应该调用哪个进程的信号处理函数呢?可以肯定的是,不会调用所有进程的信号处理函数。因此,处理 SIGURG 信号时必须指定处理信号所用的进程,而 getpid 返回的是调用此函数的进程 ID 。上述调用语句指当前为处理 SIGURG 信号的主体。
输出结果,可能出乎意料:
> 通过 MSG_OOB 可选项传递数据时只返回 1 个字节,而且也不快
的确,通过 MSG_OOB 并不会加快传输速度,而通过信号处理函数 urg_handler 也只能读取一个字节。剩余数据只能通过未设置 MSG_OOB 可选项的普通输入函数读取。因为 TCP 不存在真正意义上的「外带数据」。实际上MSG_OOB 中的 OOB 指的是 Out-of-band ,而「外带数据」的含义是:
> 通过完全不同的通信路径传输的数据
即真正意义上的 Out-of-band 需要通过单独的通信路径高速传输数据,但是 TCP 不另外提供,只利用 TCP 的紧急模式Urgent mode进行传输。
#### 13.1.3 紧急模式工作原理
MSG_OOB 的真正意义在于督促数据接收对象尽快处理数据。这是紧急模式的全部内容,而 TCP 「保持传输顺序」的传输特性依然成立。TCP 的紧急消息无法保证及时到达,但是可以要求急救。下面是 MSG_OOB 可选项状态下的数据传输过程,如图:
![](images/5c4be222845cc.png)
上面是:
```c
send(sock, "890", strlen("890"), MSG_OOB);
```
图上是调用这个函数的缓冲状态。如果缓冲最左端的位置视作偏移量 0 。字符 0 保存于偏移量 2 的位置。另外,字符 0 右侧偏移量为 3 的位置存有紧急指针Urgent Pointer。紧急指针指向紧急消息的下一个位置偏移量加一同时向对方主机传递以下信息
> 紧急指针指向的偏移量为 3 之前的部分就是紧急消息。
也就是说,实际上只用了一个字节表示紧急消息。这一点可以通过图中用于传输数据的 TCP 数据包(段)的结构看得更清楚,如图:
![](images/5c4beeae46b4e.png)
TCP 数据包实际包含更多信息。TCP 头部包含如下两种信息:
- URG=1载有紧急消息的数据包
- URG指针紧急指针位于偏移量为 3 的位置。
指定 MSG_OOB 选项的数据包本身就是紧急数据包,并通过紧急指针表示紧急消息所在的位置。
紧急消息的意义在于督促消息处理,而非紧急传输形式受限的信息。
#### 13.1.4 检查输入缓冲
同时设置 MSG_PEEK 选项和 MSG_DONTWAIT 选项,以验证输入缓冲是否存在接收的数据。设置 MSG_PEEK 选项并调用 recv 函数时,即使读取了输入缓冲的数据也不会删除。因此,该选项通常与 MSG_DONTWAIT 配合,用于以非阻塞方式验证待读数据存在与否。下面的示例是二者的含义:
- [peek_recv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch13/peek_recv.c)
- [peek_send.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch13/peek_send.c)
编译运行:
```shell
gcc peek_recv.c -o recv
gcc peek_send.c -o send
./recv 9190
./send 127.0.0.1 9190
```
结果:
![](images/5c4c0d1dc83af.png)
可以通过结果验证,仅发送了一次的数据被读取了 2 次,因为第一次调用 recv 函数时设置了 MSG_PEEK 可选项。
### 13.2 readv & writev 函数
#### 13.2.1 使用 readv & writev 函数
readv & writev 函数的功能可概括如下:
> 对数据进行整合传输及发送的函数
也就是说,通过 writev 函数可以将分散保存在多个缓冲中的数据一并发送,通过 readv 函数可以由多个缓冲分别接收。因此,适用这 2 个函数可以减少 I/O 函数的调用次数。下面先介绍 writev 函数。
```c
#include <sys/uio.h>
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
/*
成功时返回发送的字节数,失败时返回 -1
filedes: 表示数据传输对象的套接字文件描述符。但该函数并不仅限于套接字,因此,可以像 read 一样向其传递文件或标准输出描述符.
iov: iovec 结构体数组的地址值,结构体 iovec 中包含待发送数据的位置和大小信息
iovcnt: 向第二个参数传递数组长度
*/
```
上述第二个参数中出现的数组 iovec 结构体的声明如下:
```c
struct iovec
{
void *iov_base; //缓冲地址
size_t iov_len; //缓冲大小
};
```
下图是该函数的使用方法:
![](images/5c4c61b07d207.png)
writev 的第一个参数是文件描述符因此向控制台输出数据ptr 是存有待发送数据信息的 iovec 数组指针。第三个参数为 2因此从 ptr 指向的地址开始,共浏览 2 个 iovec 结构体变量,发送这些指针指向的缓冲数据。
下面是 writev 函数的使用方法:
- [writev.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch13/writev.c)
```c
#include <stdio.h>
#include <sys/uio.h>
int main(int argc, char *argv[])
{
struct iovec vec[2];
char buf1[] = "ABCDEFG";
char buf2[] = "1234567";
int str_len;
vec[0].iov_base = buf1;
vec[0].iov_len = 3;
vec[1].iov_base = buf2;
vec[1].iov_len = 4;
str_len = writev(1, vec, 2);
puts("");
printf("Write bytes: %d \n", str_len);
return 0;
}
```
编译运行:
```shell
gcc writev.c -o writev
./writev
```
结果:
```
ABC1234
Write bytes: 7
```
下面介绍 readv 函数,功能和 writev 函数正好相反.函数为:
```c
#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);
/*
成功时返回接收的字节数,失败时返回 -1
filedes: 表示数据传输对象的套接字文件描述符。但该函数并不仅限于套接字,因此,可以像 write 一样向其传递文件或标准输出描述符.
iov: iovec 结构体数组的地址值,结构体 iovec 中包含待数据保存的位置和大小信息
iovcnt: 第二个参数中数组的长度
*/
```
下面是示例代码:
- [readv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch13/readv.c)
```c
#include <stdio.h>
#include <sys/uio.h>
#define BUF_SIZE 100
int main(int argc, char *argv[])
{
struct iovec vec[2];
char buf1[BUF_SIZE] = {
0,
};
char buf2[BUF_SIZE] = {
0,
};
int str_len;
vec[0].iov_base = buf1;
vec[0].iov_len = 5;
vec[1].iov_base = buf2;
vec[1].iov_len = BUF_SIZE;
str_len = readv(0, vec, 2);
printf("Read bytes: %d \n", str_len);
printf("First message: %s \n", buf1);
printf("Second message: %s \n", buf2);
return 0;
}
```
编译运行:
```shell
gcc readv.c -o rv
./rv
```
运行结果:
![](images/5c4c718555398.png)
从图上可以看出,首先截取了长度为 5 的数据输出,然后再输出剩下的。
#### 13.2.2 合理使用 readv & writev 函数
实际上,能使用该函数的所有情况都适用。例如,需要传输的数据分别位于不同缓冲(数组)时,需要多次调用 write 函数。此时可通过 1 次 writev 函数调用替代操作,当然会提高效率。同样,需要将输入缓冲中的数据读入不同位置时,可以不必多次调用 read 函数,而是利用 1 次 readv 函数就能大大提高效率。
其意义在于减少数据包个数。假设为了提高效率在服务器端明确禁用了 Nagle 算法。其实 writev 函数在不采用 Nagle 算法时更有价值,如图:
![](images/5c4c731323e19.png)
### 13.3 基于 Windows 的实现
Windows 下的 Winsock 提供了与 Linux 类似的 I/O 函数,主要有以下区别:
#### 13.3.1 send 和 recv 函数
Winsock 中的 send 和 recv 函数原型与 Linux 基本一致:
```c
#include <winsock2.h>
int send(SOCKET s, const char *buf, int len, int flags);
int recv(SOCKET s, char *buf, int len, int flags);
```
主要区别:
- 参数类型Linux 使用 `int sockfd`Windows 使用 `SOCKET s`(实际是 `typedef UINT_PTR SOCKET;`
- 缓冲区类型Linux 使用 `void *`Windows 使用 `char *`
- 返回值Linux 返回 `ssize_t`Windows 返回 `int`(失败时返回 `SOCKET_ERROR`,即 -1
#### 13.3.2 WSASend、WSARecv 和 WSARecvEx
Windows 还提供了扩展版本的异步 I/O 函数:
```c
int WSASend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent, DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
int WSARecv(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
```
这些函数支持重叠 I/OOverlapped I/O和完成端口Completion Port模型适合高性能服务器开发。
#### 13.3.3 可选项差异
Windows 与 Linux 在可选项上存在一些差异:
| 可选项 | Linux | Windows |
| ------ | ----- | ------- |
| MSG_OOB | 支持 | 支持 |
| MSG_PEEK | 支持 | 支持 |
| MSG_DONTWAIT | 支持 | 不支持(需通过 ioctlsocket 设置非阻塞模式) |
| MSG_WAITALL | 支持 | 支持 |
| MSG_PARTIAL | 不支持 | 支持(仅用于流式套接字) |
Windows 不支持 MSG_DONTWAIT需要通过 `ioctlsocket` 函数设置套接字为非阻塞模式:
```c
u_long mode = 1;
ioctlsocket(sock, FIONBIO, &mode);
```
### 13.4 习题
> 以下答案仅代表本人个人观点,可能不是正确答案。
1. **下列关于 MSG_OOB 可选项的说法错误的是**
答:以下加粗的字体代表说法正确。
1. MSG_OOB 指传输 Out-of-band 数据,是通过其他路径高速传输数据
2. MSG_OOB 指通过其他路径高速传输数据,因此 TCP 中设置该选项的数据先到达对方主机
3. **设置 MSG_OOB 是数据先到达对方主机后,以普通数据的形式和顺序读取。也就是说,只是提高了传输速度,接收方无法识别这一点**
4. **MSG_OOB 无法脱离 TCP 的默认数据传输方式,即使脱离了 MSG_OOB ,也会保持原有的传输顺序。该选项只用于要求接收方紧急处理**
2. **利用 readv & writev 函数收发数据有何优点?分别从函数调用次数和 I/O 缓冲的角度给出说明**
答:需要传输的数据分别位于不同缓冲(数组)时,需要多次调用 write 函数。此时可通过 1 次 writev 函数调用替代操作,当然会提高效率。同样,需要将输入缓冲中的数据读入不同位置时,可以不必多次调用 read 函数,而是利用 1 次 readv 函数就能大大提高效率。
从 I/O 缓冲的角度来看writev 函数可以将分散的数据整合为一次系统调用,减少用户态与内核态之间的上下文切换次数,同时减少网络数据包的个数(尤其在禁用 Nagle 算法时效果更明显)。
3. **通过 recv 函数验证输入缓冲中是否存在数据时(确认后立即返回时),如何设置 recv 函数最后一个参数中的可选项?分别说明各可选项的含义**
答:应同时设置 `MSG_PEEK``MSG_DONTWAIT` 两个可选项。
各可选项的含义:
- **MSG_PEEK**验证输入缓冲中是否存在待接收的数据。设置此选项后recv 函数会读取输入缓冲中的数据但不会将其删除(数据仍保留在缓冲中),可以再次读取。
- **MSG_DONTWAIT**:调用 I/O 函数时不阻塞用于非阻塞Non-blockingI/O。设置此选项后如果输入缓冲中没有数据recv 函数会立即返回错误errno 设为 EAGAIN 或 EWOULDBLOCK而不是阻塞等待。
示例代码:
```c
int len = recv(sockfd, buf, sizeof(buf), MSG_PEEK | MSG_DONTWAIT);
if (len > 0) {
// 缓冲中有数据
} else if (len == 0) {
// 连接已关闭
} else {
// 无数据或出错
}
```