1.什么是I/O流分离?
I/O流分离最简单的一个例子就是,在回射服务器的程序里,通过fork创建一个子进程,由子进程负责写,父进程负责读。这样就可以认为是将I/O流进行了分离。
2. I/O分离的好处
- 通过分开输入过程(代码)和输出过程降低实现难度
- 与输入无关的输出操作可以提高速度
第一个优点就是简化程序实现,将读写分开进行实现,能够更好的考虑代码逻辑,如果同时考虑读写,会需要注意很多细节。
第二个优点是提高程序的速度,如下图所示,左图是分割前的数据交换方式,右图是分割后的数据交换方式。分割I/O后的客户端不用考虑接收数据的情况,可以连续的发送数据,由此来提高了同一时间内传输的数据量。
3. 分离I/O流的方法以及流分离带来的EOF问题
- 使用fork创建子进程,一个进程负责写,一个进程负责读。
- 使用标准I/O,为文件描述符创建对应的读写FILE结构体指针
但是,使用标准I/O会带来一个问题,那就是半关闭fdopen。使用fork来实现I/O分离流的时候,使用了shutdown来传递EOF和半关闭。那fdopen应该这样半关闭?是否是通过fclose来进行关闭?
EOF的传递方法和半关闭的重要性:
当服务器A约定好向已连接的客户端B传递文件时,需要约定一个字符来通知客户端文件传输完成,否则客户端会一直等待接收文件,不知道何时应该停止,而这个符号就是EOF。那么应该如何传递EOF符?当使用close断开连接的时候,会传输一个EOF符号,但是这是不对的,因为服务器可能还要接收客户端发送的数据,所以才有半关闭这一概念,即关闭输出流或输入流,半关闭后,就保证了发送EOF符,同时也能够继续接收客户端的信息。
现在给出如下两个服务端、客户端程序,观察flose的作用。
serve.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);
fgets(buf, sizeof(buf), readfp);
fputs(buf, stdout);
fclose(readfp);
return 0;
}
clent.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, void *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)
break;
fputs(buf, stdout);
fflush(stdout);
}
fputs("FROM CLIENT: Thank you \n", writefp);
fflush(writefp);
fclose(writefp);
fclose(readfp);
return 0;
}
编译运行:
可见,服务端并没有收到最后的确认消息“FROM CLIENT: Thank you“。
原因是:服务端中的代码fclose(writefp),关闭了整个套接字,而不是半关闭。
4. 文件描述符的复制和半关闭
4.1 无法半关闭的原因
下图描述了服务端中两个FILE指针、文件描述符和套接字的关系:
由图可以看出,两个FILE指针都是由同一文件描述符创建的,因此,无论是哪一个FLIE指针调用fclose函数,都会导致文件描述符的关闭,如下图所示:
销毁套接字后,就再也无法进行文件的读写,那应该如何进入半关闭状态?如下图所示:
只需要创建FILE指针前复制文件描述符即可。复制后创建另一个FILE指针,然后利用各自的读写FILE指针来进行I/O流分离工作。并且套接字和文件描述符还具有如下关系:
销毁所有的文件描述符才能销毁套接字
也就是说,当使用fclose(write)时,只会销毁与这个FILE指针相关联的文件描述符,不会销毁套接字,如下图:
那么此时是否是处于半关闭状态?不是!
原因是销毁一个文件描述符后,依然还存在一个文件描述符,该文件描述符依然可以同时进行I/O。因此,不但没有发送EOF,而且仍然可以使用该文件描述符进行输出。
无论复制多少个文件描述符,最后都应该由shutdown来发送EOF并进入半关闭状态
4.2 复制文件描述符
与调用fork函数不同,调用fork函数将复制整个进程,此处讨论的是同一进程内完成对文件描述符的复制,如图:
复制完成后,两个文件描述符都可对文件进行访问,且这两个文件描述符拥有不同的编号。
下面的两个函数都可以提供这一功能:
#include <unistd.h>
int dup(int fildes);
int dup2(int fildes, int fildes2);
/*
成功时返回文件描述符,失败时返回-1
fildes :需要复制的文件描述符
fildes2 :明确指定的文件描述符编号
*/
dup函数返回的复制文件描述符编号将由系统给出。
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); //复制文件描述符 1
cfd2 = dup2(cfd1, 7); //再次复制文件描述符,定为数值 7
printf("fd1=%d , fd2=%d \n", cfd1, cfd2);
write(cfd1, str1, sizeof(str1)); // 输出 Hi~
write(cfd2, str2, sizeof(str2)); // 输出 It's nice day~
close(cfd1);
close(cfd2); //终止复制的文件描述符,但是仍有一个文件描述符
write(1, str1, sizeof(str1)); // 输出 Hi~
close(1);
write(1, str2, sizeof(str2)); //理应输出 It's nice day~ 但无法完成输出
return 0;
}
编译运行:
修改第二节的serve.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);
int cp_sock = dup(clnt_sock); // 复制文件描述符
readfp = fdopen(clnt_sock, "r");
writefp = fdopen(cp_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);
shutdown(fileno(writefp), SHUT_WR); // 进行半关闭才做,一定要在fclose之前进行半关闭,否则writefp销毁了,
// 就无法调用shutdown。或者直接对readfp调用shutdown也可以
fclose(writefp);
fgets(buf, sizeof(buf), readfp);
fputs(buf, stdout);
fclose(readfp);
return 0;
}
编译运行: