第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;
}