TCP/IP网络编程之I/O流分离
分离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流分离的更多相关文章
- TCP/IP网络编程之socket交互流程
一.概要 本篇文章主要讲解基于.net中tcp/ip网络通信编程.在自我进步的过程中记录这些内容,方便自己记忆的同时也希望可以帮助到大家.技术的进步源自于分享和不断的自我突破. 技术交流QQ群:580 ...
- TCP/IP网络编程之I/O复用
基于I/O复用的服务端 在前面章节的学习中,我们看到了当有新的客户端请求时,服务端进程会创建一个子进程,用于处理和客户端的连接和处理客户端的请求.这是一种并发处理客户端请求的方案,但并不是一个很好的方 ...
- TCP/IP网络编程之优于select的epoll(一)
epoll的理解及应用 select复用方法由来已久,因此,利用该技术后,无论如何优化程序性能也无法同时接入上百个客户端.这种select方式并不适合以web服务端开发为主流的现代开发环境,所以要学习 ...
- 网络编程之TCP/IP各层详解
网络编程之TCP/IP各层详解 我们将应用层,表示层,会话层并作应用层,从TCP/IP五层协议的角度来阐述每层的由来与功能,搞清楚了每层的主要协议,就理解了整个物联网通信的原理. 首先,用户感知到的只 ...
- linux网络编程之TCP/IP基础
(一):TCP/IP协议栈与数据包封装 一.ISO/OSI参考模型 OSI(open system interconnection)开放系统互联模型是由ISO(International Organi ...
- java网络编程之TCP通讯
java中的网络编程之TCP协议的详细介绍,以及如何使用,同时我在下面举2例说明如何搭配IO流进行操作, /* *TCP *建立连接,形成传输数据的通道: *在连接中进行大数据量传输: *通过三次握手 ...
- Java网络编程之TCP、UDP
Java网络编程之TCP.UDP 2014-11-25 15:23 513人阅读 评论(0) 收藏 举报 分类: java基础及多线程(28) 版权声明:本文为博主原创文章,未经博主允许不得转载. ...
- 网络编程之TCP编程
网络编程之TCP编程 前面已经介绍过关于TCP协议的东西,这里不做赘述.Java对于基于TCP协议的网络通信提供了良好的封装,Java使用socket对象来代表两端的通信窗口,并通过Socket产生I ...
- Python网络编程之TCP套接字简单用法示例
Python网络编程之TCP套接字简单用法示例 本文实例讲述了Python网络编程之TCP套接字简单用法.分享给大家供大家参考,具体如下: 上学期学的计算机网络,因为之前还未学习python,而jav ...
随机推荐
- TAS5508 output changing
1.如果信号从3th通道输入,正常就是从PWM5,6输出,现在要想从PWM7,8输出,就按照以下红线部分选择DAP CH5和DAP CH6,然后写入相应寄存器产生的12 bytes的数组数据即可.
- echo -e的用法
root@bt:~# echo -e "HEAD /HTTP/1.0\n\n"HEAD /HTTP/1.0 root@bt:~# echo -e "HEAD /HTTP/ ...
- cms-帖子管理
mapper: <?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapperPUBLIC & ...
- 博客系统-后台页面搭建:eazy
业务分析:布局为四个模块上边是系统描述,左边是导航菜单,中间是每个窗口的内容,下边是版权信息 点击左边的导航按钮,在右边窗口显示 代码: <%@ page language="java ...
- oracle 的启动与连接
1. Oracle的启动 oracle的服务如下图所示: 启动oracle有两个重要的服务(如上图标识处): l OracleOraDb11g_home1TNSListener:监听服务,主要用于客户 ...
- UVA Live Archive 4394 String painter(区间dp)
区间dp,两个str一起考虑很难转移. 看了别人题解以后才知道是做两次dp. dp1.str1最坏情况下和str2完全不相同,相当于从空白串开始刷. 对于一个区间,有两种刷法,一起刷,或者分开来刷. ...
- Aizu 2304 Reverse Roads(无向流)
把有向图修改成无向图,并保证每条边的流量守恒并满足有向容量(即abs(flow(u,v) - flow(v,u)) <= 1)满足限制. 得到最大流,根据残流输出答案. 因为最后少了'\n'而W ...
- python_50_函数与函数式编程
import time def logger(): """追加写""" time_format='%Y-%m-%d %X'#年-月-日 小时 ...
- java算法面试题:编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串,但要保证汉字不被截取半个, 如“我ABC”,4,应该截取“我AB”,输入“我ABC汉DEF”,6,应该输出“我ABC”,而不是“我ABC+汉的半个”。
package com.swift; import java.util.Scanner; public class Hanzi_jiequ { public static void main(Stri ...
- 第十五篇、OC_同一个View实现两个手势响应
#pragma mark-UIGestureRecognizerDelegate Methods // 只要实现这个方法,就可以实现两个手势同时响应 - (BOOL)gestureRecognizer ...