1. 什么是I/O流分离?
回忆我们之前写的程序,最开始写的程序只要获得一个套接字并成功连接服务器,就能实现与服务器之间的数据交换,而这个数据交换从客户端角度看就是发送数据与接收数据; 而这个收发数据的过程称为流。而I/O流分离是指接收数据的流与发送数据的流实现分离,这里的分离指的是操作彼此隔离,一个流关闭不影响另外一个流的使用。这才是I/O流分离的真正理解。其实在这之前我们已经实现了几次I/O流分离,一次是使用fork()实现多线程服务端回声服务器那一章,只不过输入流与输出流分布在不同的进程当中;另外一次是在第十五章、通过将套接字文件描述符转化成两个FILE结构体指针(一个负责写、一个负责读),实现了读写流之间的分流。但值得注意的是这两次流的分离存在一定的差异,主要是流分离的目的不同:
使用多进程实现流分离是为了通过分开输入过程与输出过程简化代码降低难度;使用FILE结构体指针实现I/O分流通过区分I/O缓冲提高缓冲的性能。
2. 为什么需要I/O分流
这一块在上面一小节略讲了以下,主要有以下优点:
- 分开输入过程与输出过程降低实现难度。
- 与输入无关的输出操作可以 提高速度。
- 可以通过区分读写模式降低实现难度。
- 通过区分I/O缓冲提高缓冲性能。
当然,流分离带来的问题也不小,主要是如何实现半关闭? 在之前我们通过shutdown函数实现半关闭,但是这是在没有使用标准I/O流的情况下进行半关闭,如果在第十五章的情况下(即将套接字转换成FILE结构体指针)如何实现半关闭?我们可以试着去猜测:半关闭?在标准I/O函数中有一个fclose()函数,这个函数不就是可以让FILE结构体指针调用并实现半关闭吗?
这个猜测我们接着去用实验看看是不是跟我们想的那样能实现半关闭。
//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 const *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()函数实现关闭流,看看能不能实现关闭。
fclose(writefp);
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 const *argv[])
{
int sock;
struct sockaddr_in serv_adr;
char buf[BUF_SIZE];
FILE* readfp;
FILE* writefp;
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=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
connect(sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr));
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;
}
测试结果:
//客户端输出:
FROM SERVER: Hi~ client?
I love all of the world
You are awesome!
//服务端没有收到任何来自客户端的内容
结果分析:
可知,fclose()函数不能实现半关闭。原因在于当服务端调用fclose()试图去关闭写端(writefp)时就已经将整套接字给终止了,因此无法收到来自客户端的消息。
3. 当我们用fclose()无法实现半关闭的原因
在这之前我们需要理清楚一个容易忽略的事实,我们所说“创建套接字”是指在计算机内部创建一种结构体,这个结构体(数据格式)能够实现网络数据的交换,类似创建一个文件。套接字只有一个,但是套接字文件描述符可以有多个,就像一个文件可以有多个链接(软链接)类似。没有文件描述符,套接字就无法使用 ,即使在计算机中依然存在。好,理解上面这段话我们接下来看一组图来说明为啥之前猜测是错误的。
我们上面程序中设置的读写FILE结构体指针与文件描述符之间是如下图的关系。
当我们使用fclose()关闭写模式时,同时也会关闭相关的文件描述符,如下所示。
之所以连套接字也注销了,是因为系统会自动注销没有文件描述符指向的套接字。现在知道原因了吧?原因就是读写FILE结构体指针都是指向同一个文件描述符,当我们希望关闭其中一种模式,不影响另外一种模式使用的情况下完成半关闭的目的。因此弄清楚原因,解决方案也显而易见了,就是多复制一个文件描述符指向同一个套接字,不同的套接字指向各自的文件描述符。
当我们关闭其中一种模式,也就删除了一个文件描述符,但是由于并没有完全删除套接字的其他的描述符,套接字不会因此消失。
文件描述符复制函数
#include<unistd.h>
int dup(int fildes);//成功返回复制的文件描述符(系统自动分配)
int dup2(int fildes,int fildes2)
//fildes:复制文件描述母本
//fildes2:明确指定希望得到的文件描述符(即由开发者自己选择)
也许不太明白,我们通过小程序实现看看
//dup.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
int cdf1,cfd2;
char str1[]="Hi~ \n";
char str2[]="It's nice day~ \n";
cdf1=dup(1);
cfd2=dup2(cdf1,8);
printf("cfd1=%d cfd2=%d \n",cdf1,cfd2);
write(cdf1,str1,sizeof(str1));
write(cfd2,str2,sizeof(str2));
close(cdf1);
close(cfd2);
write(1, str1,sizeof(str1));
close(1);
write(1,str2,sizeof(str2));
return 0;
}
运行结果:
cfd1=3 cfd2=8
Hi~
It's nice day~
Hi~
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 const *argv[])
{
int serv_sock,clnt_sock;
FILE* readfp,*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");
/**
* @brief 利用dup复制文件描述符
* @note
* @retval
*/
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);
//关闭写模式,fileno函数是将FILE文件指针转化成对应的文件描述符,这就是给客户端发送EOF(终止连接)的方法,值得注意的是,不论复制了多少文件描述符,一旦调用了shutdown 函数,都会进入半关闭状态。
shutdown(fileno(writefp),SHUT_WR);
fclose(writefp);
fgets(buf,sizeof(buf),readfp);
fputs(buf,stdout);
fclose(readfp);
return 0;
}
//客户端依然是上面的sep_client.c
运行结果
//客户端返回结果
FROM SERVER: Hi~ client?
I love all of the world
You are awesome!
//服务端返回结果
FROM CLIENT: Thank you~
当服务端发送完字符串之后关闭了写端,但是读端依然接收到了来自客户端发来的信息,并显示在终端。
记住:“无论复制出多少文件描述符,均应该调用shutdown函数发送EOF并进入半关闭状态”。