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

16.1 分离I/O流

1. 2次I/O分离

  • 第一种:前面(第十章)是“TCP I/O过程分离”,通过调用fork函数复制出1个文件描述符,区分输入和输出中使用的文件描述符。
  • 第二种:第15章,通过两次fdopen函数的调用,创建读模式和写模式FILE指针,换而言之就是分离了输入工具和输出工具,因此也可以视为流的分离。

2.分离“流”的好处

第十章用fork函数复制文件描述符的目的:

  • 通过分开输入过程和输出过程降低实现难度
  • 与输入无关的输出操作可以提高速度 第十五章创建输入输出FILE指针的目的:
  • 为了将FILE指针按读模式和写模式加以区分
  • 可以通过区分读写模式降低实现难度
  • 通过区分I/O缓冲提高缓冲性能

3.“流”分离带来的EOF问题

之前的章节介绍过EOF的传递方法和半关闭的必要性,shutdown函数可以实现半关闭还能发送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);
	
	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);//关闭写文件指针(会发送EOF,并不会半关闭而是套接字输入输出全关闭)
	fgets(buf, sizeof(buf), readfp); fputs(buf, stdout); //输出客户端最后发来的消息
	fclose(readfp);
	return 0;
}

sep_clnt.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 * 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)//接收到EOF后fget会返回NULL指针,跳出循环 
			break;
		fputs(buf, stdout);//输出信息到屏幕
		fflush(stdout);
	 }  

	fputs("FROM CLIENT: Thank you! \n", writefp);//发送最后的消息
	fflush(writefp);
	fclose(writefp); fclose(readfp);
	return 0;
}

通过运行上述程序发现并没有输出最后客户端发来的消息

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

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

读模式的FILE指针和写模式的FILE指针都是基于同一个文件描述符创建的,对任意一个FILE指针调用fclose函数时都会关闭文件描述符,所以不能实现半关闭。

那怎么实现半关闭?

只要创建FILE指针之前先复制文件描述符即可 利用两个文件描述符分别创建读模式和写模式FILE指针,对不同FILE指针的关闭,只能关闭对应的文件描述符,只有销毁所有文件描述符之后才能关闭套接字,所以可以实现I/O分离

这时候用fclose关闭掉一个文件描述符并不会实现半关闭,因为另一个套接字文件描述符可以完成独立的输入输出,而且因为套接字并没有销毁也不会发送EOF,所以要用shutdown强制实现半关闭(不能输入或者输出)并发送EOF。

2.复制文件描述符

调用fork函数会复制整个进程,因此同一进程不能同时有原件和副本,但是后面的函数不是针对整个进程而是文件描述符。 文件描述符的值不能重复,因此所用的整数值不同,实际上是为了访问同一文件和套接字而创建的另一个文件描述符,这里的“复制”不会复制包括文件描述符整数值在内的所有内容。

3.dup&dup2

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

第二个dup2是可以明确指定文件描述符整数值,向其传递大于0且小于进程能够生成的最大文件描述符值,这个值将成为复制出的文件描述符值。

#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);//指定复制的文件描述符为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));//虽然前面关掉了两个文件描述符,但是还有1个能够输出
	close(1);
	write(1, str2, sizeof(str2));//无法输出
	return 0;
}

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

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);
	
	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);//强制刷新输出缓冲,确保信息尽快发送
	
	shutdown(fileno(writefp), SHUT_WR);//关闭输出流,发送EOF,半关闭时要用shutdown(参数是文件描述符不是FILE指针),
                                       //用shutdown的时候无论复制了多少文件描述符, 都会进入半关闭状态(无法发送或者接收),都会发送EOF
	fclose(writefp);
	
	fgets(buf, sizeof(buf), readfp); fputs(buf, stdout); //仍然能够收到客户端最后发来的消息
	fclose(readfp);//关闭输入流,此时用于服务客户端的套接字(clnt_sock)彻底关闭
	return 0;
}

无论复制出多少文件描述符,均应调用shutdown函数发送EOF并进入半关闭状态,因为单纯的fclose只会关闭一个文件描述符,既不会实现半关闭,也不会发送EOF。


本文章使用limfx的vscode插件快速发布