《UNP》随笔——I/O流分离

1.什么是I/O流分离?

I/O流分离最简单的一个例子就是,在回射服务器的程序里,通过fork创建一个子进程,由子进程负责写,父进程负责读。这样就可以认为是将I/O流进行了分离。
在这里插入图片描述

2. I/O分离的好处

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

第一个优点就是简化程序实现,将读写分开进行实现,能够更好的考虑代码逻辑,如果同时考虑读写,会需要注意很多细节。
第二个优点是提高程序的速度,如下图所示,左图是分割前的数据交换方式,右图是分割后的数据交换方式。分割I/O后的客户端不用考虑接收数据的情况,可以连续的发送数据,由此来提高了同一时间内传输的数据量。
在这里插入图片描述

3. 分离I/O流的方法以及流分离带来的EOF问题

  1. 使用fork创建子进程,一个进程负责写,一个进程负责读。
  2. 使用标准I/O,为文件描述符创建对应的读写FILE结构体指针

但是,使用标准I/O会带来一个问题,那就是半关闭fdopen。使用fork来实现I/O分离流的时候,使用了shutdown来传递EOF和半关闭。那fdopen应该这样半关闭?是否是通过fclose来进行关闭?

EOF的传递方法和半关闭的重要性:
当服务器A约定好向已连接的客户端B传递文件时,需要约定一个字符来通知客户端文件传输完成,否则客户端会一直等待接收文件,不知道何时应该停止,而这个符号就是EOF。那么应该如何传递EOF符?当使用close断开连接的时候,会传输一个EOF符号,但是这是不对的,因为服务器可能还要接收客户端发送的数据,所以才有半关闭这一概念,即关闭输出流或输入流,半关闭后,就保证了发送EOF符,同时也能够继续接收客户端的信息。

现在给出如下两个服务端、客户端程序,观察flose的作用。
serve.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;
}

clent.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, void *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;
}

编译运行:
在这里插入图片描述
在这里插入图片描述

可见,服务端并没有收到最后的确认消息“FROM CLIENT: Thank you“。
原因是:服务端中的代码fclose(writefp),关闭了整个套接字,而不是半关闭。

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

4.1 无法半关闭的原因

下图描述了服务端中两个FILE指针、文件描述符和套接字的关系:
在这里插入图片描述
由图可以看出,两个FILE指针都是由同一文件描述符创建的,因此,无论是哪一个FLIE指针调用fclose函数,都会导致文件描述符的关闭,如下图所示:
在这里插入图片描述
销毁套接字后,就再也无法进行文件的读写,那应该如何进入半关闭状态?如下图所示:
在这里插入图片描述
只需要创建FILE指针前复制文件描述符即可。复制后创建另一个FILE指针,然后利用各自的读写FILE指针来进行I/O流分离工作。并且套接字和文件描述符还具有如下关系:

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

也就是说,当使用fclose(write)时,只会销毁与这个FILE指针相关联的文件描述符,不会销毁套接字,如下图:
在这里插入图片描述
那么此时是否是处于半关闭状态?不是!
原因是销毁一个文件描述符后,依然还存在一个文件描述符,该文件描述符依然可以同时进行I/O。因此,不但没有发送EOF,而且仍然可以使用该文件描述符进行输出。

无论复制多少个文件描述符,最后都应该由shutdown来发送EOF并进入半关闭状态

4.2 复制文件描述符

与调用fork函数不同,调用fork函数将复制整个进程,此处讨论的是同一进程内完成对文件描述符的复制,如图:
在这里插入图片描述
复制完成后,两个文件描述符都可对文件进行访问,且这两个文件描述符拥有不同的编号。
下面的两个函数都可以提供这一功能:

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

dup函数返回的复制文件描述符编号将由系统给出。
dup2函数能够由用户指定复制的文件描述符的编号,向其传递大于0且小于进程能生成的最大文件描述符值时,该值将成为复制文件描述符的编号值。

测试代码:

#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);        //复制文件描述符 1
    cfd2 = dup2(cfd1, 7); //再次复制文件描述符,定为数值 7

    printf("fd1=%d , fd2=%d \n", cfd1, cfd2);
    write(cfd1, str1, sizeof(str1)); // 输出 Hi~
    write(cfd2, str2, sizeof(str2)); // 输出 It's nice day~

    close(cfd1);
    close(cfd2); //终止复制的文件描述符,但是仍有一个文件描述符
    write(1, str1, sizeof(str1)); // 输出 Hi~
    close(1);
    write(1, str2, sizeof(str2)); //理应输出 It's nice day~ 但无法完成输出
    return 0;
}

编译运行:
在这里插入图片描述
修改第二节的serve.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);

    int cp_sock = dup(clnt_sock); // 复制文件描述符

    readfp = fdopen(clnt_sock, "r");
    writefp = fdopen(cp_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销毁了,
                                        // 就无法调用shutdown。或者直接对readfp调用shutdown也可以
    fclose(writefp);
    
    fgets(buf, sizeof(buf), readfp);
    fputs(buf, stdout);
    fclose(readfp);
    return 0;
}

编译运行:
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值