Files
TCP-IP-NetworkNote/ch15/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

191 lines
6.2 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.
## 第 15 章 套接字和标准I/O
本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。
### 15.1 标准 I/O 的优点
#### 15.1.1 标准 I/O 函数的两个优点
除了使用 read 和 write 函数收发数据外,还能使用标准 I/O 函数收发数据。下面是标准 I/O 函数的两个优点:
- 标准 I/O 函数具有良好的移植性
- 标准 I/O 函数可以利用缓冲提高性能
创建套接字时,操作系统会准备 I/O 缓冲。此缓冲在执行 TCP 协议时发挥着非常重要的作用。此时若使用标准 I/O 函数,将得到额外的缓冲支持。如下图:
![](images/5c500e53ad9aa.png)
假设使用 fputs 函数进行传输字符串 「Hello」时首先将数据传递到标准 I/O 缓冲,然后将数据移动到套接字输出缓冲,最后将字符串发送到对方主机。
设置缓冲的主要目的是为了提高性能。从以下两点可以说明性能的提高:
- 传输的数据量
- 数据向输出缓冲移动的次数。
比较 1 个字节的数据发送 10 次的情况和 10 个字节发送 1 次的情况。发送数据时,数据包中含有头信息。头信息与数据大小无关,是按照一定的格式填入的。假设头信息占 40 个字节,需要传输的数据量也存在较大区别:
- 1 个字节 10 次40*10=400 字节
- 10个字节 1 次40*1=40 字节。
#### 15.1.2 标准 I/O 函数和系统函数之间的性能对比
下面是利用系统函数的示例:
- [syscpy.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/syscpy.c)
下面是使用标准 I/O 函数复制文件
- [stdcpy.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/stdcpy.c)
对于以上两个代码进行测试,明显基于标准 I/O 函数的代码跑的更快。这是因为标准 I/O 函数通过缓冲区减少了系统调用的次数,每次系统调用都有一定的开销(用户态与内核态的切换),而缓冲机制可以将多次小数据量的 I/O 操作合并为较少次数的系统调用,从而提高性能。
#### 15.1.3 标准 I/O 函数的几个缺点
标准 I/O 函数存在以下几个缺点:
- 不容易进行双向通信
- 有时可能频繁调用 fflush 函数
- 需要以 FILE 结构体指针的形式返回文件描述符。
### 15.2 使用标准 I/O 函数
#### 15.2.1 利用 fdopen 函数转换为 FILE 结构体指针
函数原型如下:
```c
#include <stdio.h>
FILE *fdopen(int fildes, const char *mode);
/*
成功时返回转换的 FILE 结构体指针,失败时返回 NULL
fildes 需要转换的文件描述符
mode 将要创建的 FILE 结构体指针的模式信息
*/
```
以下为示例:
- [desto.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/desto.c)
```c
#include <stdio.h>
#include <fcntl.h>
int main()
{
FILE *fp;
int fd = open("data.dat", O_WRONLY | O_CREAT | O_TRUNC); //创建文件并返回文件描述符
if (fd == -1)
{
fputs("file open error", stdout);
return -1;
}
fp = fdopen(fd, "w"); //返回 写 模式的 FILE 指针
fputs("NetWork C programming \n", fp);
fclose(fp);
return 0;
}
```
编译运行:
```
gcc desto.c -o desto
./desto
cat data.dat
```
运行结果:
![](images/5c5018ff07b29.png)
文件描述符转换为 FILE 指针,并可以通过该指针调用标准 I/O 函数。
#### 15.2.2 利用 fileno 函数转换为文件描述符
函数原型如下:
```c
#include <stdio.h>
int fileno(FILE *stream);
/*
成功时返回文件描述符,失败时返回 -1
*/
```
示例:
- [todes.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/todes.c)
```c
#include <stdio.h>
#include <fcntl.h>
int main()
{
FILE *fp;
int fd = open("data.dat", O_WRONLY | O_CREAT | O_TRUNC);
if (fd == -1)
{
fputs("file open error", stdout);
return -1;
}
printf("First file descriptor : %d \n", fd);
fp = fdopen(fd, "w"); //转成 file 指针
fputs("TCP/IP SOCKET PROGRAMMING \n", fp);
printf("Second file descriptor: %d \n", fileno(fp)); //转回文件描述符
fclose(fp);
return 0;
}
```
### 15.3 基于套接字的标准 I/O 函数使用
把第四章的回声客户端和回声服务端的内容改为基于标准 I/O 函数的数据交换形式。
代码如下:
- [echo_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/echo_client.c)
- [echo_stdserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/echo_stdserv.c)
编译运行:
```shell
gcc echo_client.c -o eclient
gcc echo_stdserv.c -o eserver
```
结果:
![](images/5c502001581bc.png)
可以看出,运行结果和第四章相同,这是利用标准 I/O 实现的。
### 15.4 习题
> 以下答案仅代表本人个人观点,可能不是正确答案。
1. **请说明标准 I/O 的 2 个优点。他为何拥有这 2 个优点?**
答:①具有很高的移植性②有良好的缓冲提高性能。
移植性的原因:标准 I/O 函数是由 ANSI C 标准定义的,在任何符合 ANSI C 标准的平台上都能使用,适合所有编程领域。
性能的原因:标准 I/O 函数内部维护了用户态缓冲区,数据首先在缓冲区中积累,当缓冲区填满或显式刷新时才一次性调用系统函数(如 `write`)进行实际 I/O。这减少了用户态与内核态之间上下文切换的次数从而显著提高了性能。
2. **利用标准 I/O 函数传输数据时,下面的说法是错误的**
> 调用 fputs 函数传输数据时,调用后应立即开始发送!
**为何上述说法是错误的?为达到这种效果应该添加哪些处理过程?**
答:因为标准 I/O 函数使用缓冲机制,调用 `fputs` 后数据只是被写入到用户态的缓冲区中,而不是立即发送到套接字输出缓冲或对端主机。只有在缓冲区满、缓冲区方向改变(如从读切换到写)、文件关闭或显式刷新时,数据才会真正发送。
为达到立即发送的效果,应该在调用 `fputs` 后添加 `fflush` 函数来刷新缓冲区,例如:
```c
fputs("Hello", fp);
fflush(fp); // 强制将缓冲区数据发送
```