分离I/O流

“分离I/O流”是一种常用表达,有I/O工具可以区分二者。无论使用何种办法,都可以认为分离I/O流。我们之前通过两种方法分离I/O流,第一种是TCP/IP网络编程之进程间通信中的“TCP I/O过程(Routine)分离”,这种方法通过调用fork函数复制出一个文件描述符,以区分输入和输出中使用的文件描述符。虽然文件描述符本身不会根据输入和输出进行区分,但我们分开了两个文件描述符的用途,因此这也属于“流”的分离。第二种分离是在TCP/IP网络编程之套接字与标准I/O中,通过两次fdopen函数的调用,创建读模式FILE指针和写模式FILE指针。换言之,我们分离了输入工具和输出工具,因此也可视为“流”的分离

分离“流”的好处:

  • 通过分开输入过程(代码)和输出过程降低实现难度
  • 与输入无关的输出操作可以提高速度

这是“流”分离的好处,接下来给出“流”分离的目的:

  • 为了将FILE指针按读模式和写模式加以区分
  • 可以通过区分读写模式降低实现难度
  • 通过区分I/O缓冲提高缓冲性能

“流”分离的方法、情况不同时,带来的好处也有所不同

“流”分离带来的EOF问题

之前介绍过EOF的传递方法和半关闭的必要性,通过shutdown函数来实现半关闭输出流时发送EOF。但是基于之前普通的套接字“流”,这么做是没问题的,但是如果是基于fdopen函数的“流”,可能就会出现问题。可能有人会认为,针对输出模式FILE指针调用fclose函数,这样就可以向对方传递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);
fgets(buf, sizeof(buf), readfp);
fputs(buf, stdout);
fclose(readfp);
return 0;
}

  

  • 第30、31行:通过clnt_sock中保存的文件描述符创建读模式FILE指针和写模式FILE指针
  • 第33~36行:向客户端发送字符串,调用fflush函数结束发送过程
  • 第38、39行:第38行针对写模式FILE指针调用fclose函数,调用fclose函数终止套接字时,对方主机将受到EOF。但还剩下第30行创建的读模式FILE指针,有些人可能认为可以通过第39行的函数调用接收客户端最后发送的字符串。当然,最后的字符串是客户端收到EOF后发送的

上述示例调用fclose函数后的确会发送EOF,稍后给出的客户端收到EOF后也会发送最后的字符串,只是验证第39行的函数调用能否接收,接下来给出客户端代码

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)
break;
fputs(buf, stdout);
fflush(stdout);
} fputs("FROM CLIENT: Thank you! \n", writefp);
fflush(writefp);
fclose(writefp);
fclose(readfp);
return 0;
}

  

  • 第25、26行:为了调用标准I/O函数,创建读模式和写模式FILE指针
  • 第30行:收到EOF时,fgets函数将返回NULL指针。因此,添加if语句使收到NULL时退出循环
  • 第36行:通过该行语句向服务端发送最后的字符串,该字符串是在收到服务端的EOF后发送的

编译sep_serv.c并运行

# gcc sep_serv.c -o sep_serv
# ./sep_serv 8500

  

编译sep_clnt.c 并运行

# gcc sep_clnt.c -o sep_clnt
# ./sep_clnt 127.0.0.1 8500
FROM SERVER: Hi~ client?
I love all of the world
You are awesome!

  

从运行结果可以得到结论,服务端未能接收最后的字符串。很容易判断其原因,sep_serv.c示例的第38行调用fclose函数完全终止了套接字,而不是半关闭。

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

本节主题虽然是针对FILE指针的半关闭,但后面介绍的dup和dup2函数也有助于系统编程经验

图1-1描述的是sep_serv.c示例中的两个FILE指针、文件描述符及套接字之间的关系

图1-1   FILE指针的关系

从图1-1中可以看到,示例sep_serv.c中的读模式FILE指针和写模式FILE指针都是基于同一文件描述符创建的。因此,针对任意一个FILE指针调用fclose函数时都会关闭文件描述符,也就是终止套接字,如图1-2

图1-2   调用fclose函数的调用结果

从图1-2中可以看到,销毁套接字时再也无法进行数据交换。那如何进入可以输入但无法输出的半关闭状态呢?其实很简单,如图1-3所示,创建FILE指针前先复制文件描述符即可

如图1-3所示,复制后另外创建一个文件描述符,然后利用各自的文件描述符生成读模式FILE指针和写模式FILE指针。这就为半关闭准备好了环境,因为套接字和文件描述符之间具有的关系为:销毁所有文件描述符后才能销毁套接字。

图1-3   半关闭模型1

也就是说,针对写模式FILE指针调用fclose函数时,只能销毁与该FILE指针相关的文件描述符,无法销毁套接字,如图1-4

图1-4   半关闭模型2

如图1-4所示,调用fclose函数后还剩一个文件描述符,因此没有销毁套接字。那此时的状态是否为半关闭状态?不是。图1-3中讲过,只是准备好了半关闭环境。要进入真正的半关闭状态需要特殊处理。尽管图1-4看似已经进入半关闭状态,但还剩一个文件描述符呢。而且该文件描述符可以同时进行I/O。因此,不但没有发送EOF,而且仍然可以利用文件描述符进行输出。稍后将介绍根据图1-3和图1-4的模型发送EOF并进入半关闭状态的方法

复制文件描述符

之前提到的文件描述符的复制与fork函数中进行的复制有所区别,调用fork函数时将复制整个进程,因此同一进程内不能同时有原件和副本。但此处讨论的复制并非针对整个进程,而是在同一进程内完成描述符的复制,如图1-5

图1-5   文件描述符的复制

图1-5给出的是同一进程内存在两个文件描述符可以同时访问文件的情况,当然,文件描述符的值不能重复,因此各使用5和7的整数值。为了形成这种结构,需要复制文件描述符,此处的“复制”的含义为:为了访问同一文件或套接字,创建另一个文件描述符。通常的“复制”很容易让人理解为将包括文件描述符整数值在内的所有内容进行复制,而此处的“复制”方式却不同

dup和dup2

下面给出文件描述符的复制方法,通过下面两个函数之一完成

#include <unistd.h>
int dup(int fildes);
int dup2(int fildes, int fildes2);
//成功时返回复制的文件描述符,失败时返回-1

  

  • fildes:需要复制的文件描述符
  • fildes2:明确指定的文件描述符整数值

dup2函数明确指定的文件描述符整数值,向其传递大于0且小于进程能生成的最大文件描述符值,该值将成为复制出的文件描述符值。下面给出示例验证函数功能,示例中将复制自动打开的标准输出的文件描述符1,并利用复制出的描述符进行输出。另外,自动打开的文件描述符0、1、2与套接字文件描述符没有区别,因此可以用来验证dup函数的功能

dup.c

#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;
}

  

  • 第10、11行:第10行调用dup函数复制了文件描述符1,第11行调用dup2函数再次复制了文件描述符,并指定描述符整数值为7
  • 第14、15行:利用复制出的文件描述符进行输出,通过该输出结果可以验证是否进行了实际复制
  • 第17~19行:终止复制的文件描述符,但仍有一个描述符,因此可以进行输出,可以从第19行得到验证
  • 第20、21行:第20行终止最后的文件描述符,因此无法完成第21行的输出

编译dup.c并运行

# gcc dup.c -o dup
# ./dup
fd1=3, fd2=7
Hi~
It's nice day~
Hi~

  

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

下面更改sep_serv.c,使其通过服务端的半关闭状态接收客户端最后发送的字符串。当然,为了完成这一任务,服务端需要同时发送EOF

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"); 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;
}

  

  • 第30、31行:调用fdopen函数生成FILE指针,特别是第31行针对dup函数的返回值生成的FILE指针,因此函数调用后将进入图1-3状态
  • 第38行:针对fileno函数返回的文件描述符调用shutdown函数,因此,服务端进入半关闭状态,并向客户端发送EOF。这一行就是之前所说的发送EOF的方法。调用shutdown函数时,无论复制出多少描述符都进入半关闭状态,同时传递EOF

编译sep_serv2.c 并运行

# gcc sep_serv2.c -o sep_serv2
# ./sep_serv2 8500
FROM CLIENT: Thank you!

  

运行sep_clnt

# ./sep_clnt 127.0.0.1 8500
FROM SERVER: Hi~ client?
I love all of the world
You are awesome!

  

运行结果证明服务端在半关闭状态下向客户端发送EOF,这里希望大家掌握一点:无论复制出多少个文件描述符,均应调用shutdown函数发送EOF并进入半关闭状态

TCP/IP网络编程之I/O流分离的更多相关文章

  1. TCP/IP网络编程之socket交互流程

    一.概要 本篇文章主要讲解基于.net中tcp/ip网络通信编程.在自我进步的过程中记录这些内容,方便自己记忆的同时也希望可以帮助到大家.技术的进步源自于分享和不断的自我突破. 技术交流QQ群:580 ...

  2. TCP/IP网络编程之I/O复用

    基于I/O复用的服务端 在前面章节的学习中,我们看到了当有新的客户端请求时,服务端进程会创建一个子进程,用于处理和客户端的连接和处理客户端的请求.这是一种并发处理客户端请求的方案,但并不是一个很好的方 ...

  3. TCP/IP网络编程之优于select的epoll(一)

    epoll的理解及应用 select复用方法由来已久,因此,利用该技术后,无论如何优化程序性能也无法同时接入上百个客户端.这种select方式并不适合以web服务端开发为主流的现代开发环境,所以要学习 ...

  4. 网络编程之TCP/IP各层详解

    网络编程之TCP/IP各层详解 我们将应用层,表示层,会话层并作应用层,从TCP/IP五层协议的角度来阐述每层的由来与功能,搞清楚了每层的主要协议,就理解了整个物联网通信的原理. 首先,用户感知到的只 ...

  5. linux网络编程之TCP/IP基础

    (一):TCP/IP协议栈与数据包封装 一.ISO/OSI参考模型 OSI(open system interconnection)开放系统互联模型是由ISO(International Organi ...

  6. java网络编程之TCP通讯

    java中的网络编程之TCP协议的详细介绍,以及如何使用,同时我在下面举2例说明如何搭配IO流进行操作, /* *TCP *建立连接,形成传输数据的通道: *在连接中进行大数据量传输: *通过三次握手 ...

  7. Java网络编程之TCP、UDP

    Java网络编程之TCP.UDP 2014-11-25 15:23 513人阅读 评论(0) 收藏 举报 分类: java基础及多线程(28) 版权声明:本文为博主原创文章,未经博主允许不得转载.   ...

  8. 网络编程之TCP编程

    网络编程之TCP编程 前面已经介绍过关于TCP协议的东西,这里不做赘述.Java对于基于TCP协议的网络通信提供了良好的封装,Java使用socket对象来代表两端的通信窗口,并通过Socket产生I ...

  9. Python网络编程之TCP套接字简单用法示例

    Python网络编程之TCP套接字简单用法示例 本文实例讲述了Python网络编程之TCP套接字简单用法.分享给大家供大家参考,具体如下: 上学期学的计算机网络,因为之前还未学习python,而jav ...

随机推荐

  1. css钻石旋转实现

    css钻石旋转实现: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> &l ...

  2. css动画-小球撞壁反弹

    小球碰到一面壁之后一般都会反弹,反射角=入射角: 其实用css3来实现这个效果也非常简单. 首先,分解一下小球的运动:水平运动和垂直运动. 当小球往右下方向运动时,如果碰到了下面的壁,那么由于碰撞,小 ...

  3. Spring之IOC核心模块详解

    Spring IOC简述 IOC称为控制反转,也有一种说法叫DI(依赖注入).IOC也是spring最核心的模块,Spring的所有工作几乎都围绕着IOC展开. 什么是控制反转呢?简单的说,控制反转就 ...

  4. raw_input功能

    摘要: raw_input()  &  input() raw_input的功能是方便的从控制台读入数据.  input与raw_input都是Python的内建函数,实现与用户的交互,但是功 ...

  5. Netweaver工作进程的内存限制 VS CloudFoundry应用的内存限制

    Netweaver 一个会话进程能够在堆上申请的内存大小上限, 在事务码RZ11里查看参数abap/heap_area_dia: CloudFoundry 每个应用可以在manifest.yml里定义 ...

  6. innobackupex基于binlog日志的恢复 -- 模拟slave恢复

    说明:一般来说,如果恢复的binlog量不大,可以使用此方法来恢复:mysqlbinlog /data/mysqlbak/binlogbak/restoredb-bin.000018 |mysql - ...

  7. hdu-1874 畅通工程续---模板题

    题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=1874 题目大意: 求起点到终点的最短距离 解题思路: 注意重边,其他的就是模板 #include&l ...

  8. Problem L: 搜索基础之马走日

    Problem L: 搜索基础之马走日 Time Limit: 1 Sec  Memory Limit: 128 MBSubmit: 134  Solved: 91[Submit][Status][W ...

  9. 20.JSON

    JSON是javascript的一个子集,利用js中的一些儿模式来表示结构化数据.不是只有javascript才使用JSON,JSON是一种数据格式,很多编程语言都有针对JSON的解析器和序列化器. ...

  10. mongdb 一些操作

    一.命令操作数据库1.管理员身份打开cmd2.进到mongdb的mongo.exe文件所在路径3.show dbs 查看mongodb4.连接远程数据库:mongo ip:端口/数据库5.打开某个数据 ...