16.关于IO流分离的其他内容
前言
调用fopen函数打开文件后可以与文件交换数据,因此说调用fopen函数后创建了“流”(Stream)。此处的“流”是指“数据流动”,但通常可以比喻为“以数据收发为目的的一种桥梁”。希望大家将“流”理解为数据收发路径。
一、分离I/O流
“分离I/O流”是一种常用表达。有I/O工具可以区分二者,无论使用何种方法,都可以认为分离了IO流。
1.2次 1/O 流分离
“分离I/O流”是一种常用表达。有IO工具可以区分二者,无论使用何种方法,都可以认为分离了IO流。
我们之前通过2种方法分离过I/O流,第一种是第10章节的“TCPI/O过程(Routine)分离”。这种方法通过调用fork函数复制出1个文件描述符,以区分输人和输出中使用的文件描述符。虽然文件描述符本身不会根据输人和输出进行区分,但我们分开了2个文件描述符的用途,因此这也属于“流”的分离。
第二种分离在第15章节。通过2次fdopen函数的调用,创建读模式FILE指针(FILE结构体指针)和写模式FILE指针。换言之,我们分离了输人工具和输出工具,因此也可视为“流”的分离。下面说明分离的理由,讨论尚未提及的问题并给出解决方案。
2.分离“流”的好处
第10章的“流”分离和第15章的“流”分离在目的上有一定差异。首先分析第10章节的“流”
分离目的。
■ 通过分开输人过程(代码)和输出过程降低实现难度。
■ 与输人无关的输出操作可以提高速度。
这是第10章节讨论过的内容,故不再解释这些优点的原因。接下来给出第15章节“流”分离的目的。
■ 为了将FILE指针按读模式和写模式加以区分。
■ 可以通过区分读写模式降低实现难度。
■ 通过区分I/O缓冲提高缓冲性能。
“流”分离的方法、情况(目的)不同时,带来的好处也有所不同。
3.流”分离带来的EOF问题
下面讲解“流”分离带来的问题。第7章介绍过EOF的传递方法和半关闭的必要性(如果记不清,请复习一下相应章节哦)。各位应该还记得如下函数调用语句:
shutdown(sock, SHUT_WR);
当时讲过调用shutdown函数的基于半关闭的EOF传递方法。第10章节还利用这些技术在echo_mpclient.c示例中添加了半关闭相关代码。也就是说,第10章节的“流”分离没有问题。但第15章节的基于fdopen函数的“流”则不同,我们还不知道在这种情况下如何进行半关闭,因此有可能犯如下错误:
“半关闭?不是可以针对输出模式的FILE指针调用fclose函数吗?这样可以向对方传递EOF,变成可以接收数据但无法发送数据的半关闭状态呀。”
大家是否也这么认为?这是一种很好的猜测,但希望大家先阅读下列代码。另外,接下来的示例中为了简化代码而未添加异常处理,希望大家不要误解。先给出服务器端代码。
#include <stdio.h>
#include<stdlib.h>
#include <string.h>
#include <unistd.h>
#include<arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024int main(int argc, char *argv[])
{int serv_sock, clnt_sock;FILE * readfp;FILE * writefp;struct sockaddr_in serv_adr,clnt_adr;socklen_t clnt_adr_sz;char buf[BUF_SIZE]={0};serv_sock=socket(PF_INET, SOCK_STREAM, 0);memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET;serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);serv_adr.sin_port=htons(atoi(argv[1]));bind(serv_sock,(struct sockaddr*) &serv_adr, sizeof(serv_adr));listen(serv_sock,5);clnt_adr_sz=sizeof(clnt_adr);clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);readfp=fdopen(clnt_sock,"r");writefp=fdopen(clnt_sock,"w");fputs("FROM SERVER: Hi~ Client? \n",writefp);fputs("I love all of the world \n",writefp);fputs("You are awesome! \n", writefp);fflush(writefp);fclose(writefp);fgets(buf,sizeof(buf),readfp);fputs(buf,stdout);fclose(readfp);return 0;
}
●第30、31行:通过cInt_SOCk中保存的文件描述符创建读模式FILE指针和写模式FILE指针。
●第33~36行:向客户端发送字符串,调用fflush函数结束发送过程。
●第38、39行:第38行针对写模式FILE指针调用fclose函数。调用fclose函数终止套接字时,对方主机将收到EOF。但还剩下第30行创建的读模式FILE指针,有些人可能认为可以通过第39行的函数调用接收客户端最后发送的字符串。当然,最后的字符串是客户端收到EOF后发送的。
上述示例调用fclose函数后的确会发送EOF。稍后给出的客户端收到EOF后也会发送最后的字符串,只是需要验证第39行的函数调用能否接收。接下来给出客户端代码。
#include <stdio.h>
#include<stdlib.h>
#include <string.h>
#include <unistd.h>
#include<arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024int main(int argc, char *argv[])
{int sock;char buf[BUF_SIZE];struct sockaddr_in serv_addr;FILE * readfp;FILE * writefp;sock=socket(PF_INET,SOCK_STREAM, 0);memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family=AF_INET;serv_addr.sin_addr.s_addr=inet_addr(argv[1]);serv_addr.sin_port=htons(atoi(argv[2]));connect(sock,(struct sockaddr*)&serv_addr, sizeof(serv_addr));readfp=fdopen(sock,"r");writefp=fdopen(sock, "w");while(1){if(fgets(buf,sizeof(buf),readfp)==NULL)break;fputs(buf, stdout);fflush(stdout);}fputs("FROM CLIENT: Thank you! \n",writefp);fflush(writefp);fclose(writefp); fclose(readfp);return 0;
}
●第25、26行:为了调用标准/O函数,创建读模式和写模式FILE指针。
●第30行:收到EOF时,fgets函数将返回NULL指针。因此,添加if语句使收到NULL时退出循环。
●第36行:通过该行语句向服务器端发送最后的字符串。当然,该字符串是在收到服务器端的EOF后发送的。
大家在分析代码过程中应该得知需要通过该示例验证哪些事项。接下来通过运行结果验证服务器端是否收到客户端最后发送的字符串。


从运行结果可得出如下结论:
“服务器端未能接收最后的字符串!”
很容易判断其原因:sep_serv.c示例的第38行调用的fclose函数完全终止了套接字,而不是半关闭。以上就是需要通过本章解决的问题。半关闭在多种情况下都非常有用,各位必须能够针对fdopen函数调用时生成的FILE指针进行半关闭操作。
二、文件描述符的复制和半关闭
本章节主题虽然是针对FILE指针的半关闭,但本节介绍的dup和dup2函数也有助于增加系统编程经验。
1.终止“流”时无法半关闭的原因
下图描述的是sep_serv.c示例中的2个FILE指针、文件描述符及套接字之间的关系。
1
从图中可以看到,示例sep_serv.c中的读模式FILE指针和写模式FILE指针都是基于同一文件描述符创建的。因此,针对任意一个FILE指针调用fclose函数时都会关闭文件描述符,也就终止套接字。
2
从图中可以看到,销毁套接字时再也无法进行数据交换。那如何进人可以输人但无法输出的半关闭状态呢?其实很简单。创建FILE指针前先复制文件描述符即可。
3
如上图,复制后另外创建1个文件描述符,然后利用各自的文件描述符生成读模式FILE指针和写模式FILE指针。这就为半关闭准备好了环境,因为套接字和文件描述符之间具有如下关系:“销毁所有文件描述符后才能销毁套接字。”
也就是说,针对写模式FILE指针调用fclose函数时,只能销毁与该FILE指针相关的文件描述符,无法销毁套接字。
4
如上图,调用fclose函数后还剩1个文件描述符,因此没有销毁套接字。那此时的状态是否为半关闭状态?不是!图中讲过,只是准备好了半关闭环境。要进人真正的半关闭状态需要特殊处理。
“图16-4中好像已经进入半关闭状态了啊?”
当然可以这么看。但仔细观察,还剩1个文件描述符呢。而且该文件描述符可以同时进行I/O。因此,不但没有发送EOF,而且仍然可以利用文件描述符进行输出。稍后将介绍根据图3和图4的模型发送EOF并进人半关闭状态的方法。首先介绍如何复制文件描述符,之前的fork函数不在考虑范围内。
代码如下(示例):
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
2.复制文件描述符
之前提到的文件描述符的复制与fork函数中进行的复制有所区别。调用fork函数时将复制整个进程,因此同一进程内不能同时有原件和副本。但此处讨论的复制并非针对整个进程,而是在同一进程内完成描述符的复制,下图。

上面这个图给出的是同一进程内存在2个文件描述符可以同时访问文件的情况。当然,文件描述符的值不能重复,因此各使用5和7的整数值。为了形成这种结构,需要复制文件描述符。此处所谓的“复制”具有如下含义:
“为了访问同一文件或套接字,创建另一个文件描述符。”
通常的“复制”很容易让人理解为将包括文件描述符整数值在内的所有内容进行复制,而此处的“复制”方式却不同。
2.dup & dup2
下面给出文件描述符的复制方法,通过下列2个函数之一完成。
#include <unistd.h>
int dup(int fildes);
int dup2(int fildes, int fildes2);
// 成功时返回复制的文件描述符,失败时返回-1。
//fildes -- 需要复制的文件描述符。
// fildes2 -- 明确指定的文件描述符整数值。
dup2函数明确指定复制的文件描述符整数值。向其传递大于0且小于进程能生成的最大文件描述符值时,该值将成为复制出的文件描述符值。下面给出示例验证函数功能,示例中将复制自动打开的标准输出的文件描述符1,并利用复制出的描述符进行输出。另外,自动打开的文件描述符0、1、2与套接字文件描述符没有区别,因此可以用来验证dup函数的功能。
#include <stdio.h>
#include <unistd.h>int main(int argc, char *argv[])
{int cfd1, cfd2;char str1[]="HI~ \n";char str2[]="It's nice day~ \n";cfd1=dup(1);cfd2=dup2(cfd1,7);printf("fd1=%d, fd2=%d \n", cfd1, cfd2);write(cfd1,str1, sizeof(str1));write(cfd2, str2, sizeof(str2));close(cfd1);close(cfd2);write(1, str1, sizeof(str1));close(1);write(1, str2, sizeof(str2));return 0;
}

●第10、11行:第10行调用dup函数复制了文件描述符1。第11行调用dup2函数再次复制了文件描述符,并指定描述符整数值为7。
●第14、15行:利用复制出的文件描述符进行输出。通过该输出结果可以验证是否进行了实际复制。
●第17~19行:终止复制的文件描述符。但仍有1个描述符,因此可以进行输出。可以从第19行得到验证。
●第20、21行:第20行终止最后的文件描述符,因此无法完成第21行的输出。
3.复制文件描述符后“流”的分离
下面更改sep_serv.c和sep_clnt.c示例,使其能够正常工作(只需更改sep_serv.c示例)。所谓“正常工作”是指,通过服务器端的半关闭状态接收客户端最后发送的字符串。当然,为了完成这一任务,服务器端需要同时发送EOF。发送EOF的代码并不难,通过示例给出。
#include <stdio.h>
#include<stdlib.h>
#include <string.h>
#include <unistd.h>
#include<arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024int main(int argc, char *argv[])
{int serv_sock, clnt_sock;FILE * readfp;FILE * writefp;struct sockaddr_in serv_adr, clnt_adr;socklen_t clnt_adr_sz;char buf[BUF_SIZE]={0};serv_sock=socket(PF_INET,SOCK_STREAM,0);memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET;serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);serv_adr.sin_port=htons(atoi(argv[1]));bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr));listen(serv_sock, 5);clnt_adr_sz=sizeof(clnt_adr);clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);readfp=fdopen(clnt_sock, "r");writefp=fdopen(dup(clnt_sock), "w");fputs("FROM SERVER: HI~ Client? \n", writefp);fputs("I love all of the world \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);return 0;
}
●第30、31行:调用fdopen函数生成FILE指针。特别是第26行针对dup函数的返回值生成FILE指针,因此函数调用后将进入图16-3所示状态。
●第38行:针对fileno函数返回的文件描述符调用shutdown函数。因此,服务器端进入半关闭状态,并向客户端发送EOF。这一行就是之前所说的发送EOF的方法。调用shutdown函数时,无论复制出多少文件描述符都进入半关闭状态,同时传递EOF。

运行结果证明服务器端在半关闭状态下向客户端发送了EOF。通过该示例希望大家掌握一点:
“无论复制出多少文件描述符,均应调用shutdown函数发送EOF并进入半关闭状态。”
第10章的echo_mpclient.c示例运用过shutdown函数的这种功能,当时通过fork函数生成了2个文件描述符,并在这种情况下调用shutdown函数发送了EOF。
总结
这个章节主要讨论了分离IO之后发送EOF的问题,希望大家都能够理解!
