分离 I/O 流
(一)之前学习过的两种 I/O 流分离方法
(1)TCP I/O 过程分离
实现方法:
通过调用 fork 函数复制出一个文件描述符,以区分输入和输出中使用的文件描述符。虽然这种方法中文件描述符本身不会根据输入和输出进行区分,但是分开了2个文件描述符的用途,所以这也属于 “流” 的分离。
优点以及目的:
- 通过分开输入过程(代码)和输出过程降低实现难度。
- 与输入无关的输出操作可以提高速度。
(2) fdopen 函数调用实现 I/O 流分离
实现方法:
通过2次 fdopen 函数的调用,创建读模式 FILE 指针和写模式 FILE 指针。即分离了输入工具和输出工具,因此也可视为 “流” 的分离。
优点以及目的:
- 为了将
FILE指针按读模式和写模式加以区分。 - 可以通过区分读写模式降低实现难度。
- 通过区分 I/O 缓冲提高缓冲性能。
(二)“流” 分离带来的 EOF 问题
在 TCP/IP 网络编程(六)中介绍过 EOF 的传递方法和版关闭的重要性。TCP I/O 分离过程可以通过调用 shutdown 函数传递基于半关闭的 EOF。而基于 fdopen 函数的 “流” 则不同,可能犯如下错误:
针对输出模式的 FILE 指针调用 fclose 函数,这样可以向对方传递 EOF,变成可以接收数据但无法发送数据的半关闭状态。
下面通过示例验证上述观点的正确性:
(1)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[])
{
#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);
// 通过 clnt_sock 中保存的文件描述符创建读模式 FILE 指针和写模式 FILE 指针
readfp=fdopen(clnt_sock, "r");
writefp=fdopen(clnt_sock, "w");
// 向客户端发送字符串,调用 fflush 函数结束发送过程
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 指针调用 fclose 函数,对方主机将接收到 EOF
// 读模式 FILE 指针尚未关闭,fgets函数还能接收到客户端最后发送的字符串吗?
fclose(writefp);
fgets(buf, sizeof(buf), readfp); fputs(buf, stdout);
fclose(readfp);
return 0;
}
(2)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)
{
// 收到 EOF 时,fgets 函数将返回 NULL 指针,此时跳出循环
if(fgets(buf, sizeof(buf), readfp)==NULL)
break;
fputs(buf, stdout);
fflush(stdout);
}
// 向服务器端发送最后的字符串,当然,该字符串是在接收到服务器端的 EOF 之后发送的。
fputs("FROM CLIENT: Thank you! \n", writefp);
fflush(writefp);
fclose(writefp); fclose(readfp);
return 0;
}
(3)运行结果
从运行结果可知服务器端未能接收最后的字符串。原因是 sep_serv.c 示例的第51行调用的 fclose 函数完全终止了套接字,而不是半关闭,接下来解决针对 fdopen 函数调用时生成的 FILE 指针进行半关闭操作的问题。
文件描述符的复制和半关闭
(一)终止 “流” 时无法半关闭的原因
下图描述的是 sep_serv.c 中的2个 FILE 指针、文件描述符及套接字之间的关系。
读模式 FILE 指针和写模式 FILE 指针都是基于同一文件描述符创建的。因此针任意一个 FILE 指针调用 fclose 函数时都会关闭文件描述符,也就是终止套接字:
销毁套接字时再也无法进行数据交换。下图描述了进入半关闭状态的方法:创建 FILE 指针前先复制文件描述符即可:
如上图所示,复制后另外创建一个文件描述符,然后利用各自的文件描述符生成读模式 FILE 指针和写模式 FILE 指针,这就为半关闭准备好了环境,因为套接字和文件描述符之间具有如下关系:
“销毁所有文件描述符后才能销毁套接字。”
如上图所示,调用 fclose 函数后还剩1个文件描述符,因此没有销毁套接字。那此时的状态是否为半关闭状态?并不是!因为剩下的1个文件描述符可以同时进行 I/O。 要进入真正的半关闭状态还需要特殊处理。在介绍根据上图模型发送 EOF 并进入半关闭状态的方法之前,先介绍如何复制文件描述符,之前的 fork 函数不在考虑范围内。
(二)复制文件描述符
现在要实现的文件描述符的复制与 fork 函数中进行的复制有所区别。调用 fork 函数时将复制整个进程,因此同一进程内不能同时有原件和副本。此处讨论的复制并非针对整个进程,而是在同一进程内完成文件描述符的复制:
复制文件描述符的函数:
#include <unistd.h>
// 成功时返回复制的文件描述符,失败时返回-1
int dup(int fildes);
int dup2(int fildes, int fildes2);
fildes:需要复制的文件描述符。
fildes2:明确指定要复制的文件描述符。
示例:
#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("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));
close(1);
write(1, str2, sizeof(str2));
return 0;
}
-
dup和dup2的操作:dup(1)复制标准输出(stdout)的文件描述符1,返回一个新的文件描述符cfd1,这个文件描述符通常是最小的可用文件描述符(在这个例子中可能是3)。dup2(cfd1, 7)将文件描述符cfd1复制到文件描述符7,如果7已经打开,dup2会首先关闭它,然后将cfd1的副本复制到7。
-
printf("fd1=%d, fd2=%d \n", cfd1, cfd2);:- 这行代码会通过标准输出显示
cfd1和cfd2的值,比如fd1=3, fd2=7(具体值取决于系统分配的文件描述符)。
- 这行代码会通过标准输出显示
-
write操作:write(cfd1, str1, sizeof(str1));使用cfd1写入str1到标准输出,显示Hi~ \n。write(cfd2, str2, sizeof(str2));使用cfd2写入str2到标准输出,显示It's nice day~ \n。
-
关闭文件描述符:
close(cfd1);和close(cfd2);关闭了cfd1和cfd2,因此它们不再指向任何资源。write(1, str1, sizeof(str1));仍然使用标准输出(1)写入str1,此时应该仍能显示Hi~ \n。close(1);关闭了标准输出,后续的写入将无效。
-
最后的
write(1, str2, sizeof(str2));:- 因为标准输出(
1)已经关闭,这个写操作会失败,不会显示str2,在许多系统上可能会导致程序输出错误信息或静默失败。
- 因为标准输出(
(三)复制文件描述符后 “流” 的分离
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]));
// 将套接字绑定到指定的IP地址和端口
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 将套接字转换为 FILE*,方便使用标准 I/O 函数
readfp = fdopen(clnt_sock, "r");
writefp = fdopen(dup(clnt_sock), "w");
// 通过 writefp 向客户端发送消息
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);
// 从客户端读取数据并输出到标准输出
fgets(buf, sizeof(buf), readfp);
fputs(buf, stdout);
// 关闭读端
fclose(readfp);
return 0;
}
结果是服务器端接收到了客户端最后的消息。所以之前提到的进入半关闭状态的特殊操作就是:
“无论复制出多少文件描述符,均应调用 shutdown 函数发送 EOF 并进入半关闭状态。”