2721 字
7 分钟
TCP&IP网络编程学习札记_9
2026-09-02
浏览量 8 · 访客 2

壁纸链接

套接字和标准I/O#

15.1 标准I/O函数的优点#

之前的LinuxLinux套接字程序主要通过readreadwritewrite等系统函数交换数据。除此之外,也可以把套接字文件描述符转换为FILEFILE指针,再使用fgetsfgetsfputsfputs等标准I/OI/O函数。

标准I/OI/O函数主要有两个优点:

  1. 具有良好的移植性。
  2. 可以利用额外的缓冲提高性能。

标准I/OI/O函数按照ANSI CANSI\ C标准定义,不只适用于某一种操作系统。只要目标平台提供对应的标准库,程序中的大部分I/OI/O逻辑就可以保持不变。

标准I/O缓冲与套接字缓冲#

创建套接字时,操作系统已经为套接字准备了输入、输出缓冲。使用标准I/OI/O函数后,数据还会经过标准库内部维护的另一层缓冲。

替换文本

fputsfputs发送字符串为例,数据的移动过程大致如下:

  1. 程序先把数据写入标准I/OI/O输出缓冲。
  2. 标准库在适当时机把数据转移到套接字输出缓冲。
  3. 操作系统按照TCPTCP协议完成实际传输。

两层缓冲的目的并不完全相同。套接字缓冲主要服务于TCPTCP协议,例如保存尚未确认的数据,以便发生丢包时重新发送;标准I/OI/O缓冲则主要用于把多次少量读写合并起来,减少函数调用、系统调用和数据移动次数。

如果每次只发送11字节,重复1010次,程序会频繁进入系统调用,网络上也可能产生更多首部开销。先在标准I/OI/O缓冲中积累数据,再集中写入套接字缓冲,通常更有效率。数据规模越大、零散操作越多,缓冲带来的收益越明显。

NOTE

标准I/OI/O缓冲提高的是一般情况下的批量处理效率,并不意味着每个程序使用标准I/OI/O后都会明显变快。若程序需要严格控制每次收发时机,还要主动处理缓冲刷新问题。

标准I/O函数的缺点#

标准I/OI/O并非只有优点。在套接字编程中主要有以下限制:

  • 不容易在同一个流上频繁切换读取和写入状态。
  • 为了立即发送缓冲中的内容,有时需要频繁调用fflushfflush
  • 套接字创建后得到的是文件描述符,使用标准I/OI/O前还要转换为FILEFILE指针。

标准I/OI/O流存在自己的缓冲。调用fputsfputs只表示把数据交给标准库,不保证数据已经立刻进入套接字输出缓冲。若协议要求当前消息马上发送,需要显式调用:

fputs(message, writefp);
fflush(writefp);

频繁刷新会削弱缓冲原本用于合并操作的优势。因此,是否使用标准I/OI/O,应根据数据格式、读写模式和发送时机决定。

15.2 使用标准I/O函数#

利用fdopen转换为FILE指针#

套接字函数返回文件描述符,而fgetsfgetsfputsfputs等标准函数需要FILEFILE指针。fdopenfdopen用于在二者之间建立关联:

#include <stdio.h>
FILE *fdopen(int fildes, const char *mode);
//成功时返回FILE指针,失败时返回NULL。

fildesfildes是已有文件描述符,modemode表示标准I/OI/O流的使用模式,常见值为读模式"r""r"和写模式"w""w"

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);

fdopenfdopen不会复制文件内容或重新打开文件,它只是基于现有描述符创建标准I/OI/O流。调用fclose(fp)fclose(fp)时,标准库不仅会处理流缓冲,也会关闭与该FILEFILE指针关联的文件描述符,因此之后不应继续使用原来的fdfd

利用fileno取得文件描述符#

filenofileno执行相反的转换,根据FILEFILE指针返回其底层文件描述符:

#include <stdio.h>
int fileno(FILE *stream);
//成功时返回文件描述符,失败时返回-1。
FILE *fp = fdopen(fd, "w");
int same_fd = fileno(fp);

filenofileno不会创建新的描述符。这里的same_fdsame\_fd与创建FILEFILE流时使用的fdfd指向同一个底层对象,数值通常也相同。它适合在使用标准I/OI/O的同时,调用shutdownshutdown等只接受文件描述符的系统函数。

15.3 基于套接字使用标准I/O#

把标准I/OI/O应用到套接字时,网络连接的建立过程无需改变。服务器仍然通过socketsocketbindbindlistenlistenacceptaccept取得已连接套接字,客户端也仍然通过connectconnect建立连接;变化只发生在数据交换阶段。

标准I/O回声服务器#

服务器端可以根据客户端套接字分别创建读模式和写模式FILEFILE指针:

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);

fgetsfgets按字符串读取数据,遇到EOFEOF时返回NULLNULLfputsfputs把字符串写入标准I/OI/O缓冲,随后通过fflushfflush立即推入下一层缓冲。与直接调用readread相比,程序不再需要手动在接收数据末尾添加字符串结束符。

标准I/O回声客户端#

客户端的数据交换逻辑也可以改为标准I/OI/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/OI/O可以让面向文本行的程序更简洁,但也引入了FILEFILE流的缓冲和资源管理问题。尤其是从同一个套接字描述符创建读、写两个FILEFILE指针后,关闭其中一个流会产生怎样的影响,需要在下一章进一步处理。

关于I/O流分离的其他内容#

16.1 分离I/O流#

这里的StreamStream)可以理解为以数据收发为目的的路径。将输入工具与输出工具分别管理,或把输入、输出代码交给不同执行过程,都可以称为I/OI/O流分离。

前面已经使用过两种分离方式:

  1. 1010章通过forkfork把套接字的输入过程和输出过程交给不同进程。
  2. 1515章通过两次fdopenfdopen创建读模式和写模式FILEFILE指针。

第一种方式侧重拆分执行过程,使发送不必等待接收逻辑完成;第二种方式侧重区分读写工具,也能让输入、输出使用各自的标准I/OI/O缓冲。

分离I/O流的好处#

根据分离方式不同,主要收益包括:

  • 将输入代码和输出代码分开,降低实现难度。
  • 让与输入无关的输出操作独立进行。
  • 明确区分读模式与写模式FILEFILE指针。
  • 分开管理标准I/OI/O缓冲,简化读写状态切换。

但读写工具被分开后,底层仍可能是同一个套接字和同一个文件描述符。如果忽略这层关系,关闭某个FILEFILE流时就可能意外终止整个连接。

流分离带来的EOF问题#

假设服务器从同一个客户端套接字创建两个FILEFILE指针:

readfp = fdopen(clnt_sock, "r");
writefp = fdopen(clnt_sock, "w");

两者虽然模式不同,但都关联同一个文件描述符,而该描述符又指向同一个套接字。

替换文本

服务器发送完数据后,可能希望关闭输出流、向客户端传递EOFEOF,同时保留输入流接收客户端的最后一条响应。直觉上可以这样写:

fflush(writefp);
fclose(writefp);
fgets(buf, sizeof(buf), readfp);

fclose(writefp)fclose(writefp)会关闭它关联的文件描述符。由于读、写FILEFILE指针使用的是同一个描述符,底层套接字也会随之终止,readfpreadfp无法再接收客户端响应。

替换文本

因此,关闭写模式FILEFILE指针并不等于只关闭套接字输出流。fclosefclose负责关闭标准I/OI/O流和描述符,而套接字的方向性半关闭仍应通过shutdownshutdown完成。

16.2 文件描述符的复制和半关闭#

要让读、写FILEFILE指针能够分别关闭,首先需要让它们关联不同的文件描述符。可以在调用fdopenfdopen前复制套接字描述符,再分别创建标准I/OI/O流。

替换文本

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

替换文本

不过,这一步只是为半关闭准备了环境。只要还有一个描述符指向套接字,该描述符仍然具备读写能力,单独fclosefclose其中一个流并不会自动向对方发送EOFEOF。真正关闭输出方向仍需调用shutdownshutdown

复制文件描述符#

文件描述符复制不是复制文件或套接字,也不是生成一个相同的描述符整数值。它是在同一进程中创建另一个描述符,使两个描述符共同指向同一底层对象。

替换文本

LinuxLinux中可以使用dupdupdup2dup2

#include <unistd.h>
int dup(int fildes);
int dup2(int fildes, int fildes2);
//成功时返回复制出的文件描述符,失败时返回-1。

dupdup返回当前可用的最小描述符;dup2dup2则尝试把副本放到fildes2fildes2指定的描述符编号上。

int copied_fd1 = dup(fd);
int copied_fd2 = dup2(fd, 7);

复制得到的描述符数值彼此不同,但它们共享同一个底层文件或套接字状态。关闭其中一个只会减少一次引用,直到相关描述符全部关闭,底层对象才会被真正释放。

这与forkfork后的描述符复制也有所区别:forkfork复制整个进程的描述符表,原描述符与副本分别存在于父子进程;dupdupdup2dup2则直接在同一进程内产生新的描述符。

复制描述符后分离标准I/O流#

基于复制描述符创建读写流:

readfp = fdopen(clnt_sock, "r");
writefp = fdopen(dup(clnt_sock), "w");

此时readfpreadfpwritefpwritefp持有不同的描述符,但底层仍是同一条TCPTCP连接。服务器完成发送后,应先刷新标准I/OI/O缓冲,再通过写流的描述符执行半关闭:

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);

这里的执行顺序十分重要:

  1. fflushfflush把标准I/OI/O输出缓冲中的数据交给套接字。
  2. shutdown(...,SHUT_WR)shutdown(..., SHUT\_WR)关闭套接字输出方向,剩余数据发送完成后向客户端传递EOFEOF
  3. fclose(writefp)fclose(writefp)释放写模式FILEFILE流及其描述符。
  4. readfpreadfp仍可通过保留的输入方向接收客户端最后的响应。

shutdownshutdown操作的是套接字的通信方向。无论同一套接字复制出多少文件描述符,调用shutdown(sock,SHUT_WR)shutdown(sock, SHUT\_WR)都会使这条连接进入输出半关闭状态;而fclosefclosecloseclose只负责释放某个流或描述符引用。

因此,文件描述符复制解决的是“分别管理两个FILEFILE流”的问题,shutdownshutdown解决的才是“发送EOFEOF但继续接收数据”的问题。二者配合后,才能在使用标准I/OI/O的同时实现正确的TCPTCP半关闭。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

TCP&IP网络编程学习札记_9
https://mkrari.cn/posts/tcp_ip_learn_9/
作者
Mkrari
发布于
2026-09-02
许可协议
CC BY-NC-SA 4.0