分离 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 并进入半关闭状态。”