
套接字和标准I/O
15.1 标准I/O函数的优点
之前的套接字程序主要通过、等系统函数交换数据。除此之外,也可以把套接字文件描述符转换为指针,再使用、等标准函数。
标准函数主要有两个优点:
- 具有良好的移植性。
- 可以利用额外的缓冲提高性能。
标准函数按照标准定义,不只适用于某一种操作系统。只要目标平台提供对应的标准库,程序中的大部分逻辑就可以保持不变。
标准I/O缓冲与套接字缓冲
创建套接字时,操作系统已经为套接字准备了输入、输出缓冲。使用标准函数后,数据还会经过标准库内部维护的另一层缓冲。

以发送字符串为例,数据的移动过程大致如下:
- 程序先把数据写入标准输出缓冲。
- 标准库在适当时机把数据转移到套接字输出缓冲。
- 操作系统按照协议完成实际传输。
两层缓冲的目的并不完全相同。套接字缓冲主要服务于协议,例如保存尚未确认的数据,以便发生丢包时重新发送;标准缓冲则主要用于把多次少量读写合并起来,减少函数调用、系统调用和数据移动次数。
如果每次只发送字节,重复次,程序会频繁进入系统调用,网络上也可能产生更多首部开销。先在标准缓冲中积累数据,再集中写入套接字缓冲,通常更有效率。数据规模越大、零散操作越多,缓冲带来的收益越明显。
NOTE标准缓冲提高的是一般情况下的批量处理效率,并不意味着每个程序使用标准后都会明显变快。若程序需要严格控制每次收发时机,还要主动处理缓冲刷新问题。
标准I/O函数的缺点
标准并非只有优点。在套接字编程中主要有以下限制:
- 不容易在同一个流上频繁切换读取和写入状态。
- 为了立即发送缓冲中的内容,有时需要频繁调用。
- 套接字创建后得到的是文件描述符,使用标准前还要转换为指针。
标准流存在自己的缓冲。调用只表示把数据交给标准库,不保证数据已经立刻进入套接字输出缓冲。若协议要求当前消息马上发送,需要显式调用:
fputs(message, writefp);fflush(writefp);频繁刷新会削弱缓冲原本用于合并操作的优势。因此,是否使用标准,应根据数据格式、读写模式和发送时机决定。
15.2 使用标准I/O函数
利用fdopen转换为FILE指针
套接字函数返回文件描述符,而、等标准函数需要指针。用于在二者之间建立关联:
#include <stdio.h>
FILE *fdopen(int fildes, const char *mode);//成功时返回FILE指针,失败时返回NULL。是已有文件描述符,表示标准流的使用模式,常见值为读模式和写模式。
int fd;FILE *fp;
fd = open("data.dat", O_WRONLY | O_CREAT | O_TRUNC);fp = fdopen(fd, "w");
fputs("Network C programming\n", fp);fclose(fp);不会复制文件内容或重新打开文件,它只是基于现有描述符创建标准流。调用时,标准库不仅会处理流缓冲,也会关闭与该指针关联的文件描述符,因此之后不应继续使用原来的。
利用fileno取得文件描述符
执行相反的转换,根据指针返回其底层文件描述符:
#include <stdio.h>
int fileno(FILE *stream);//成功时返回文件描述符,失败时返回-1。FILE *fp = fdopen(fd, "w");int same_fd = fileno(fp);不会创建新的描述符。这里的与创建流时使用的指向同一个底层对象,数值通常也相同。它适合在使用标准的同时,调用等只接受文件描述符的系统函数。
15.3 基于套接字使用标准I/O
把标准应用到套接字时,网络连接的建立过程无需改变。服务器仍然通过、、和取得已连接套接字,客户端也仍然通过建立连接;变化只发生在数据交换阶段。
标准I/O回声服务器
服务器端可以根据客户端套接字分别创建读模式和写模式指针:
readfp = fdopen(clnt_sock, "r");writefp = fdopen(clnt_sock, "w");
while (fgets(message, BUF_SIZE, readfp) != NULL){ fputs(message, writefp); fflush(writefp);}
fclose(readfp);fclose(writefp);按字符串读取数据,遇到时返回;把字符串写入标准缓冲,随后通过立即推入下一层缓冲。与直接调用相比,程序不再需要手动在接收数据末尾添加字符串结束符。
标准I/O回声客户端
客户端的数据交换逻辑也可以改为标准函数:
readfp = fdopen(sock, "r");writefp = fdopen(sock, "w");
while (1){ fputs("Input message(Q to quit): ", stdout); fgets(message, BUF_SIZE, stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) break;
fputs(message, writefp); fflush(writefp);
fgets(message, BUF_SIZE, readfp); printf("Message from server: %s", message);}使用标准可以让面向文本行的程序更简洁,但也引入了流的缓冲和资源管理问题。尤其是从同一个套接字描述符创建读、写两个指针后,关闭其中一个流会产生怎样的影响,需要在下一章进一步处理。
关于I/O流分离的其他内容
16.1 分离I/O流
这里的流()可以理解为以数据收发为目的的路径。将输入工具与输出工具分别管理,或把输入、输出代码交给不同执行过程,都可以称为流分离。
前面已经使用过两种分离方式:
- 第章通过把套接字的输入过程和输出过程交给不同进程。
- 第章通过两次创建读模式和写模式指针。
第一种方式侧重拆分执行过程,使发送不必等待接收逻辑完成;第二种方式侧重区分读写工具,也能让输入、输出使用各自的标准缓冲。
分离I/O流的好处
根据分离方式不同,主要收益包括:
- 将输入代码和输出代码分开,降低实现难度。
- 让与输入无关的输出操作独立进行。
- 明确区分读模式与写模式指针。
- 分开管理标准缓冲,简化读写状态切换。
但读写工具被分开后,底层仍可能是同一个套接字和同一个文件描述符。如果忽略这层关系,关闭某个流时就可能意外终止整个连接。
流分离带来的EOF问题
假设服务器从同一个客户端套接字创建两个指针:
readfp = fdopen(clnt_sock, "r");writefp = fdopen(clnt_sock, "w");两者虽然模式不同,但都关联同一个文件描述符,而该描述符又指向同一个套接字。

服务器发送完数据后,可能希望关闭输出流、向客户端传递,同时保留输入流接收客户端的最后一条响应。直觉上可以这样写:
fflush(writefp);fclose(writefp);
fgets(buf, sizeof(buf), readfp);但会关闭它关联的文件描述符。由于读、写指针使用的是同一个描述符,底层套接字也会随之终止,无法再接收客户端响应。

因此,关闭写模式指针并不等于只关闭套接字输出流。负责关闭标准流和描述符,而套接字的方向性半关闭仍应通过完成。
16.2 文件描述符的复制和半关闭
要让读、写指针能够分别关闭,首先需要让它们关联不同的文件描述符。可以在调用前复制套接字描述符,再分别创建标准流。

复制后,两个描述符的整数值不同,但都指向同一个套接字对象。这样关闭写模式指针时,只会释放它持有的描述符;读模式指针仍然持有另一描述符,因此套接字不会立即销毁。

不过,这一步只是为半关闭准备了环境。只要还有一个描述符指向套接字,该描述符仍然具备读写能力,单独其中一个流并不会自动向对方发送。真正关闭输出方向仍需调用。
复制文件描述符
文件描述符复制不是复制文件或套接字,也不是生成一个相同的描述符整数值。它是在同一进程中创建另一个描述符,使两个描述符共同指向同一底层对象。

中可以使用或:
#include <unistd.h>
int dup(int fildes);int dup2(int fildes, int fildes2);//成功时返回复制出的文件描述符,失败时返回-1。返回当前可用的最小描述符;则尝试把副本放到指定的描述符编号上。
int copied_fd1 = dup(fd);int copied_fd2 = dup2(fd, 7);复制得到的描述符数值彼此不同,但它们共享同一个底层文件或套接字状态。关闭其中一个只会减少一次引用,直到相关描述符全部关闭,底层对象才会被真正释放。
这与后的描述符复制也有所区别:复制整个进程的描述符表,原描述符与副本分别存在于父子进程;和则直接在同一进程内产生新的描述符。
复制描述符后分离标准I/O流
基于复制描述符创建读写流:
readfp = fdopen(clnt_sock, "r");writefp = fdopen(dup(clnt_sock), "w");此时与持有不同的描述符,但底层仍是同一条连接。服务器完成发送后,应先刷新标准缓冲,再通过写流的描述符执行半关闭:
fputs("FROM SERVER: Hi client?\n", writefp);fputs("You are awesome!\n", writefp);fflush(writefp);
shutdown(fileno(writefp), SHUT_WR);fclose(writefp);
fgets(buf, sizeof(buf), readfp);fputs(buf, stdout);fclose(readfp);这里的执行顺序十分重要:
- 把标准输出缓冲中的数据交给套接字。
- 关闭套接字输出方向,剩余数据发送完成后向客户端传递。
- 释放写模式流及其描述符。
- 仍可通过保留的输入方向接收客户端最后的响应。
操作的是套接字的通信方向。无论同一套接字复制出多少文件描述符,调用都会使这条连接进入输出半关闭状态;而或只负责释放某个流或描述符引用。
因此,文件描述符复制解决的是“分别管理两个流”的问题,解决的才是“发送但继续接收数据”的问题。二者配合后,才能在使用标准的同时实现正确的半关闭。
