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

第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流。

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

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

16.1.2 分离流的好处

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

第十章的流分离目的

  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.csep_client使其能够半关闭状态下接收到客户端最后发送的字符串。
为了完成这个任务,只需要更改服务器端即可,服务端需要同时发送EOF。

如何完成这样的过程呢?

  1. 首先:对连接到的客户端套接字进行分流(使用fdopen函数分别控制读和写,注意这里应使用dup函数复制客户端文件描述符,使得io控制形成两条线路)
  2. 在使用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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值