TCP/IP网络编程 第十六章:关于IO流分离的其他内容

分离I/O流

两次I/O流分离

我们之前通过2种方法分离过IO流,第一种是第十章的"TCPI/O过程(Routine)分离"。这种方法通过调用fork函数复制出1个文件描述符,以区分输入和输出中使用的文件描述符。虽然文件描述符本身不会根据输入和输出进行区分,但我们分开了2个文件描述符的用途,因此这也属于"流"的分离。

第二种分离在第十五章。通过2次fdopen函数的调用,创建读模式FILE指针(FILE结构体指针)和写模式FILE指针。换言之,我们分离了输入工具和输出工具,因此也可视为"流"的分离。下面说明分离的理由,讨论尚未提及的问题并给出解决方案。

分离"流"的好处

第10章的"流"分离和第15章的"流"分离在目的上有一定差异。首先分析第10章的"流"分离目的。

□通过分开输入过程(代码)和输出过程降低实现难度。

□与输入无关的输出操作可以提高速度

这是第10章讨论过的内容,故不再解释这些优点的原因。接下来给出第15章"流"分离的目的。

□为了将FILE指针按读模式和写模式如以区分。

□可以通过区分读写模式降低实现难度。

□通过区分IO缓冲提高缓冲性能

"流"分离的方法、情况(目的)不同时,带来的好处也有所不同。

"流"分离带来的EOF问题

下面讲解"流"分离带来的问题。第7章介绍过EOF的传递方法和半关闭的必要性(如果记

不清,请复习相应章节)。各位应该还记得如下函数调用语句:

cpp 复制代码
shutdown(sock, SHUT_WR);

当时讲过调用shutdown函数的基于半关闭的EOF传递方法。10章还利用这些技术在示例中添加了半关闭相关代码。也就是说,第10章的"流"分离没有问题。但第15章的基于fdopen函数的"流"则不同,我们还不知道在这种情况下如何进行半关闭,因此有可能犯如下错误:

"半关闭?不是可以针对输出模式的FILE指针调用fclose函数吗?这样可以向对方传递EOF,变成可以接收数据但无法发送数据的半关闭状态呀。"各位是否也这么认为?这是一种很好的猜测,但希望大家先阅读下列代码。另外,接下来的示例中为了简化代码而未添加异常处理,希望各位不要误解。先给出服务器端代码。

cpp 复制代码
#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_sock,clnt_sock;
    socklen_t clnt_addr_sz;
    char buf[BUF_SIZE]={0,};
  
    serv_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=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi(argv[1]));

    bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
    listen(serv_sock,5);
    clnt_addr_sz=sizof(clnt_sock);
    clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_addr_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;
}

有些人可能认为可以通过第39行的函数调用接受客户端最后发送的字符串。上述示例调用fclose函数后的确会发送EOF。稍后给出的客户端收到EOF后也会发送最后的字符串,只是需要验证第39行的函数调用能否接受。接下来给出客户端代码。

cpp 复制代码
#include<"头文件声明和服务器端声明一样,故省略">
#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,sizepf(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;
}

从运行结果可以得出以下结论:"服务器端未能接受由客户端传来的最后的字符串!"。

很容易判断其原因::在服务器示例中的第38行调用的fclose函数完全终止了套接字,而不是半关闭。以上就是需要通过本章解决的问题。

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

终止"流"时无法半关闭的原因

这张图描述了之前示例中服务器端的两个文件指针。那么一切就真相大白了。当读指针调用fclose函数的时候会关闭文件描述符,此时会关闭套接字。那么如何解决呢?不就是创建FILE指针前先复制文件描述符即可。

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

"销毁所有文件描述符后才能销毁套接字"

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

那此时的状态是否为半关闭状态?不是!只是准备好了半关闭环境。要进入真正的半关闭状态需要特殊处理。仔细观察,还剩1个文件描述符。而且该文件描述符可以同时进行IO因此,不但没有发送EOF,而且仍然可以利用文件描述符进行输出。稍后将介绍发送EOF并进入半关闭状态的方法。首先介绍如何复制文件描述符,之前的fork函数不在考虑范围内。

复制文件描述符

之前所提到的文件描述符复制与fork函数中进行的复制有所区别。调用fork函数时将复制整个进程,此时的复制从某种意义上是复制到另一个进程中。但是这里提到的复制是可以在同一个进程内完成文件描述符的复制。当然,文件描述符的值不能重复,因此要使用互不相同的值。为了形成这种结构,需要复制文件描述符。此处所谓的"复制"具有如下含义:"为了访问同一文件或套接字,创建另外一个文件描述符。"

dup&dup2

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

cpp 复制代码
#include<unistd.h>
int dup(int fildes);
int dup2(int fildes,int fildes2);
//成功时返回复制的文件描述符,失败时返回-1
     fildes     //需要复制的文件描述符
     fildes2    //明确指定的文件描述符整数值

dup2函数明确指定复制的文件描述符整数值。向其传递大于0且小于进程能生成的最大文件描述符值时,该值将成为复制出的文件描述符的值。

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

下面我们的目的就是使之前的服务器客户端模型正常工作。所谓"正常"工作是指,通过服务器端的半关闭状态接收客户端最后发出的字符串。当然,为了完成这一任务,服务端需要同时发送EOF。下面是示例代码:

cpp 复制代码
#include<"头文件和之前的示例相同,故省略">
#define BUF_SIZE 1024

int main(int argc,char*argv[]){
    int serv_sock,clnt_sock;
    FILE *readfp;
    FILE *writefp;
    
    struct sockaddr_in serv_addr,clnt_addr;
    socklen_t clnt_addr_sz;
    char buf[BUF_SIZE]={0,};

    serv_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=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi(argv[1]));

    bind(serv_sock,(struct sockadd*)&serv_addr,sizeof(serv_addr));
    listen(serv_sock,5);
    clnt_addr_sz=sizeof(clnt_addr);
    clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_addr_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);//关闭并发送EOF

    fgets(buf,sizeof(buf),readfp);
    fputs(buf,stdout);
    fclose(readfp);
    return 0;
}

结果证明服务器端在半关闭的情况下向客户端发送了EOF,通过该示例希望大家掌握一点:"

无论复制出多少文件描述符,均应调用shutdown函数发送EOF并进入半关闭状态"

相关推荐
A小辣椒2 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux