Part 2 Linux programming:chapter 16:关于IO流分离的其他内容

mac2026-03-11  4

第16章:关于I/O流分离的其他内容

什么是流? 调用fopen函数打开文件后,可以通过返回值 与文件进行数据交换。 因此说调用fopen函数后创建了流(stream)。

FILE *fopen(const char *path,const char *mode); 其中,path是我们要打开的流,而mode就是我们打开文件的方式了,也就决定你所打开的文件将被怎样的去对待啦,有如下几种方式: "r":只读方式打开,打开的文件必须存在。 "r+" :读写方式打开,文件必须存在。 "w" : 只写方式打开,文件不存在则创建,文件存在则清空。 "w+" : 读写方式打开,文件不存在则创建,文件存在则清空。 "a" : 只写方式打开,追加的方式写到文件的尾部,文件不存在则创建。 "a+": 读写方式打开,文件不存在创建,从头开始读,从尾开始写。

此处的流是指数据流动,通常可以比喻为:以数据收发为目的的一种桥梁。 我们将流理解为数据收发路径

16.1 分离I/O流

分离I/O流是一种参见表达。有I/O工具可以区分两者,无论使用什么方法都可以认为分离了I/O流。

16.1.1 2次I/O流分离

先回顾一下:我们曾经使用过两种方法来分离I/O流。

第十章中的“TCP I/O过程分离”。 通过调用fork函数复制一个文件描述符,用来区分输入和输出种使用的文件描述符。虽然这种方法并非从本质上分开,而仅仅是我们通过子进程分开了两个文件描述符的用途,但这也是属于流的分离。

上一章:第十五章中使用2次fopen函数,创建读模式FILE指针和写模式FILE指针。 我们分离了输入工具和输出工具,因此也可以视为流的分离,下面说明分离的理由以及尚未说明的问题。

16.1.2 分离流的好处

上面说的两种流分离方式有所不同。

第十章的流分离目的:

通过分开输入过程(代码)和输出过程降低实现难度。与输入无关的输出操作可以提高速度。 这些已经在第十章中讨论过了,具体可以去看第十章的内容 下面主要讨论一下使用系统函数的流分离过程。

第十五章流分离目的:

为了将FILE指针按照读模式和写模式加以区分可以通过区分读写模式减低实现难度。通过区分I/O缓冲提高缓冲性能。 流分离的方法、情况目的不同时,带来的好处也不同。

16.1.3 “流”分离带来的EOF问题

第七章介绍过EOF的传递方法和半关闭的必要性。 是否还记得下面的函数调用语句

shoudown(sock,SHUT_WR);

当时讲过调用shutdown函数的基于半关闭的EOF传递方法。 第四章还利用这些技术在echo_mpclient.c中添加的半关闭相关代码。

也就是说10章中的流分离是没有问题的,而15章中的基于fdopen函数的流则不同。我们不知道在这种情况下如何进行半关闭。 可能听到这个问题的第一反应是: “是不是可以针对输出模式的FILE指针调用fclose函数,这样可以向对方传递EOF,变成能够可以接收数据但无法发送数据的半关闭状态~~” (我也是这么想的,先不管对不对,我们通过一个示例感受一下,为了简化没有写异常处理)

服务器端:sep_serv.c

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 1024 int 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); // 使用fdopen函数进行套接字文件描述符转化、分离I/O流操作 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函数接受客户端的信息。 fgets(buf, sizeof(buf), readfp); fputs(buf, stdout); fclose(readfp); // 关闭输入端 return 0; }

客户端:sep_client.c

在这里插入代码片 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 1024 int main(int argc, char *argv[]) { // 定义客户端需要的变量 int sock; char buf[BUF_SIZE]; struct sockaddr_in serv_addr; // 定义FILE 结构体指针,用于一会调用fdopen函数分离io流做准备 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])); // 连接服务器端并分离io流 connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); readfp=fdopen(sock, "r"); writefp=fdopen(sock, "w"); // 循环接受客户端的信息,并将信息打印在控制台中 while(1) { // 当服务器端调用fclose(writefp)函数时,会向客户端发送eof这时fgets函数返回NULL,退出循环 if(fgets(buf, sizeof(buf), readfp)==NULL) break; fputs(buf, stdout); fflush(stdout); } // 此时服务器端已经调用了fclose(writefp),我们想测试一下服务器的readfp还好用么? // 因此继续向服务器端发送数据 fputs("FROM CLIENT: Thank you! \n", writefp); fflush(writefp); fclose(writefp); fclose(readfp); return 0; }

从上面的结果中可以看到:服务器端未能接收到最后的字符串!

看来在服务器端的fclose(writefp)不仅仅关闭了输出流啊~ 看来并不是想象中的半关闭!而是两边都关闭了!

但是半关闭真的非常有用,使用标准系统函数的f系列函数配合fdopen函数生成FILE指针进行半关闭操作也是必须会的东西。

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

这里将讲解如何针对FILE指针进行半关闭,同时介绍dup dup2函数

16.2.1 终止“流”时无法半关闭的原因

下图描述的是sep_serv.c示例中的2个FILE指针、文件描述符以及套接字之间的关系

上图什么意思呢?也就是说在上面的服务器端代码中(sep_serv.c)中的读模式FILE指针和写模式FILE指针都是用基于同一个文件描述符创建的。因此,针对任意一个FILE指针调用fclose函数时都会关闭文件描述符,也就代表着终止套接字。如下图所示: 既然销毁了套接字无法在进行数据交换,那么如何进入可以输入(read)但是无法输出(write)的半关闭状态呢? 只需要在创建FILE指针前先复制文件描述符即可。

下面提供一个可行的模型方案:(半关闭模型1)

如上图所示:在复制后另外创建一个文件描述符,然后利用搁置的文件描述符生成读模式FILE指针和写模式FILE指针。这就为半关闭做好了环境准备,因为套接字和文件描述符具有:销毁所有文件描述符后才能销毁套接字

也就是说:针对写模式FILE指针调用fclose寒十四,只能销毁与该FILE指针相关的文件描述符,无法销毁套接字。 既然已经保住了套接字,同时还保留了针对读模式的FILE指针,是不是现在就是半关闭模式了呢?

显然不是啊~ 半关闭模式指的是套接字与套接字之间进行数据传输时的单向性 上图的结构,是否只能完成单向传输呢??????

这是是利用FILE结构指针进行了 io分流而已,,文件描述符仍然可以 接收和发送数据

那我们这样做的意义是啥啊???显然,是建立了 只关闭一个FILE指针时,不会断开套接字连接的结构! 现在只是准备好了半关闭的环境而已~

下面我们讲介绍如何根据上面的图模型发送EOF并进入半关闭状态的方法,在这之前我们先说说图中复制文件描述符的方法(之前使用的是创建子进程的fork方法,这里并不使用)

16.2.2 复制文件描述符

之前使用fork函数复制的文件描述符是分布在两个不同的进程中,而不能在一个进程中同时拥有文件描述符的原件和复件

这里我们使用新的方法在同一进程中复制文件描述符,如下图所示: 可以看到,这种方法的结果是在同一进程中存在2个文件描述符可以同时访问一个文件。 因为文件描述符不能重复,因此各使用5和7的整数值。

为了形成这样的结构,我们需要 “创建另一个文件描述符,以达到访问同一文件或套接字的目的”

下面给出使用的函数

16.2.3 dup&dup2

通过这两个函数之一完成 文件描述符的复制方法

#include <unistd.h> int dup(int fildes); int dup2(int fildes, int fildes2); -> 成功时返回复制的文件描述符,失败时返回-1 fildes:需要复制的文件描述符 fildes2: 明确指定的文件描述符整数值

dup2函数明确指定复制的文件描述符整数值,向其传递大于0且小于进程能生成的最大文件描述符值时,该值将成为复制出的文件描述符值。下面给出示例验证函数功能。

在下面示例中:复制自动打开的标准输出文件描述符1.并利用复制出的描述符进行输出。 另外,自动打开的文件描述符0、1、2余套接字文件描述符没有区别,所以使用他们来进行验证。

dup.c

#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("复制出来的文件描述符为:\n"); printf("cfd1=%d, cfd2=%d \n", cfd1, cfd2); printf("下面使用cfd1和cfd2进行数据传输:\n"); write(cfd1, str1, sizeof(str1)); write(cfd2, str2, sizeof(str2)); printf("传输完成,关闭cfd1....关闭cfd2.....\n"); close(cfd1); close(cfd2); printf("下面使用文件描述符 1 进行数据传输:\n"); write(1, str1, sizeof(str1)); printf("关闭文件描述符 1 ....\n"); close(1); printf("再次使用文件描述符 1 进行数据传输:\n"); write(1, str2, sizeof(str2)); return 0; }

从结果中可以看到,在关闭了所有标准输出文件描述符后,无法再进行输出,最后一个printf以及write函数中的数据没有成功输出到标准输出。

16.2.4 复制文件描述符后“流”的分离

下面更改sep_serv.c和sep_client使其能够半关闭状态下接收到客户端最后发送的字符串。 为了完成这个任务,只需要更改服务器端即可,服务端需要同时发送EOF。

如何完成这样的过程呢?

首先:对连接到的客户端套接字进行分流(使用fdopen函数分别控制读和写,注意这里应使用dup函数复制客户端文件描述符,使得io控制形成两条线路)在使用o端输出信息后,利用shutdown函数,关闭其中o端。这样就实现了,标准io函数的读写半关闭控制。 来看代码:

sep_serv2.c

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 1024 int 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); // 复制客户端连接文件描述符 并 转换文件描述符为FILE结构体指针,再进行io分流 readfp=fdopen(clnt_sock, "r"); writefp=fdopen(dup(clnt_sock), "w"); // 使用FILE结构体指针进行数据传输 fputs("FROM SERVER: Hi~ client? \n", writefp); fputs("I love all of the world \n", writefp); fputs("You are awesome! \n", writefp); fflush(writefp); // 将FILE指针转换为文件描述符并 利用shutdown函数进行半关闭操作 shutdown(fileno(writefp), SHUT_WR); // 半关闭后,writefp指针已经失去了作用,直接(通过关闭FILE指针)关闭和这个复制出来的文件描述符 fclose(writefp); fgets(buf, sizeof(buf), readfp); fputs(buf, stdout); fclose(readfp); return 0; }

最新回复(0)