TCP/IP网络编程_基于Linux的编程_第16章关于I/O分流的其他内容

在这里插入图片描述

16.1 分离 I/O 流

“分离I/O流” 是一种常用表达. 有 I/O 工具可以区分二者, 无论使用何种方法, 都可以认为
分离了I/O流.

2次 I/O 分流

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

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

分离 “流” 的好处

第10章的 “流” 分离和第15章的 “流” 分离在目的上有一定差异. 首先分析第10章的 “流” 分离目的.
在这里插入图片描述
这是第10章讨论过的内容, 故不再解析. 接下来给出第15章"流"分离的目的.
在这里插入图片描述
“流” 分离的方法, 情况(目的)不同时, 带来的好处也有所不同.

“流” 分离带来的 EOF 问题

下面讲解"流" 分离带来的问题. 第7章介绍过EOF的传递方法和半关闭的必要性(如果记不清, 请复习相关章节). 各位应该还在记得如下函数的调用语句:
在这里插入图片描述当时讲过调用 shutdown 函数的基于半关闭的EOF传递方法. 第10章还利用这些技术在 echo_mpclient.c 示例中添加了半关闭相关代码. 也就是说, 第10章的 “流” 分离没有问题. 但第15章的基于 fdopen 函数的 “流” 则不同, 我们还不知道在这种情况下如何进行半关闭, 因此有可能如下错误:
在这里插入图片描述各位是否也这么认为? 这是一种很好的猜测, 但希望大家先阅读下列代码. 另外, 接下来的示例中为了简化代码而未添加异常处理, 希望各位不要误解. 先给出服务器端代码.
在这里插入图片描述

#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"); /* 30~31: 通过clnt_sock中保存的文件描述符创建读模式FILE指针和写模式FILE指针. */
    writefp = fdopen(clnt_sock, "w");

    fputs("FROM SERVER: Hi~ client? \n", writefp); /* 34~37: 向客户端发送字符串, 调用fflush函数结束发送过程. */
    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;
}

上述示例调用fclose函数后的确发送EOF. 稍后给出的客户端收到EOF后也会发送最后的字符串, 只是需要验证第40行的函数调用能否接收. 接下来给出客户端代码.
在这里插入图片描述

#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"); /* 26~27: 为了调用标准I/O函数, 创建读模式和写模式FILE指针 */
    writefp = fdopen(sock, "w");

    while (1)
    {
        if (fgets(buf, sizeof(buf), readfp) == NULL) /* 31: 收到EOF时, fgets函数将返回NULL指针. 因此, 添加if语句使收到的NULL时退出循环. */
        {
            break;
        }
        fputs(buf, stdout);
        fflush(stdout);
    }

    fputs("FROM CLIENT: Thank you! \n", writefp); /* 39: 通过该行语句向服务器端发送最后的字符串. 当然, 该字符串是在收到服务端的EOF后发送的. */
    fflush(writefp);

    fclose(writefp);
    fclose(readfp);

    return 0;
}

各位在分析代码中的过程中应该得知需要通过该示例验证哪些事例. 接下来通过运行结果验证服务器端是否收到客户端最后发送的字符串.
在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述
从运行结果可得如下结论:
在这里插入图片描述很容易判断其原因: sep_serv.c 示例的第39行调用的fclose函数完全终止了套接字, 而不是半关闭. 以上就是需要通过本章解决的问题. 半关闭在多种情况下到有非常有用, 各位必须能够针对 fdopen 函数调用时生成的 FILE 指针进行半关闭操作.

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

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

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

图`16-1描述的是sept_serv.c 示例中的2个 FILE 指针, 文件描述符及套接字之间的关系.
在这里插入图片描述从图16-1中可以看到, 示例sep_serv.c 中的读模式 FILE 指针和 写模式 FILE 指针都是基于同一文件描述符创建的. 因此, 针对任意一个FILE指针调用 fclose 函数时都会关闭文件描述符, 也就终止套接字, 如图16-2所示.
在这里插入图片描述从图16-2中可以看到, 销毁套接字时再也无法进行数据交换. 那如何进入可以输入但无法输出的半关闭状态呢? 其实很简单.如图16-3 所示, 创建FILE 指针前先复制文件描述符即可.
在这里插入图片描述如图16-3 所示, 复制后另外传创建1个文件描述符, 然后利用各自的文件描述符生成读模式FILE指针和写模式FILE指针. 这就为半关闭准备好了环境, 因为套接字和文件描述符之间具有如下关系:
在这里插入图片描述
也就是说, 针对写模式FILE指针调用fclose函数时, 只能销毁与该FILE指针相关的文件描述符, 无法销毁套接字(参考图16-4).
在这里插入图片描述
如图16-4所示, 调用fclose函数后还剩1个文件描述符, 因此没有销毁套接字. 那此时的状态是否为半关闭状态? 不是! 图16-3 中讲过, 只是准备好了半关闭环境. 要进入真正的半关闭状态需要特殊处理.
在这里插入图片描述
当然可以这么看. 但仔细观察, 还剩1个文件描述符呢. 而且该文件描述符可以同时进行 I/O . 因此, 不但没有发送 EOF, 而且仍然可以利用文件描述符进行输出. 稍后将介绍根据图16-3和图16-4的模型发送EOF 并进入半关闭状态的方法. 首先介绍如何复制文件描述符, 之前的fork 函数不在考虑范围内.

复制文件描述符

之前提到的文件描述符的复制与 fork 函数中进行的复制有所区别. 调用fork函数时将复制整个进程, 因此同一进程内不能有原件和副本. 但此处讨论的复制并非针对整个进程, 而是在同一进程内完成的描述符的复制, 如图16-5所示.
在这里插入图片描述图16-5给出的是同一进程内存在2个文件描述符可以同时访问的情况. 当然, 文件描述符的值不能重复, 因此各使用5和7的整数值. 为了形成这种结构, 需要复制文件描述符. 此处所谓的"复制" 具有如下含义:
在这里插入图片描述通常的"复制"很容易让人理解为将包括文件描述符整数值在内的所有内容进行复制, 而此处的 “复制” 方式却不同.

dup & dup2

下面给出文件描述符的复制方法, 通过下列2个函数之一完成.
在这里插入图片描述dup2函数明确指定复制的文件描述符整数值. 向其传递大于0且小于进程能生成的最大文件描述符值时, 该值将成为复制出的文件描述符值. 下面给出示例验证函数功能, 示例中将复制自动打开的标准输出的文件描述符1, 并利用复制出的描述符进行输出. 另外, 自动打开的文件描述符0, 1, 2与套接字文件描述符没有区别, 因此可以用来验证 dup 函数的功能.
在这里插入图片描述

#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); /* 第10 11行: 调用dup函数复制了文件描述符1. 第11行调用dup2函数再次复制了文件描述符, 并指定描述符整数值为7 */
    cfd2 = dup2(cfd1, 7);

    printf("fd1 = %d, fd2 = %d \n", cfd1, cfd2);
    write(cfd1, str1, sizeof(str1)); /* 14 15: 利用复制出的文件描述符进行输出. 通过该输出结果可以验证是否进行了实际复制 */
    write(cfd2, str2, sizeof(str2));

    close(cfd1);/* 17 ~ 19: 终止复制的文件描述符. 但仍有1个描述符, 因此可以进行输出. 可以从第19行验证. */
    close(cfd2);
    write(1, str1, sizeof(str1));
    close(1); /* 20 21: 第20行终止最后的文件描述符, 因此无法完成第21行的输出. */
    write(1, str2, sizeof(str2));    

    return 0;
}

在这里插入图片描述
在这里插入图片描述
示例虽然简单, 但足以表达文件描述符相关内容.

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

下面更改sep_serv.c 和 sep_clnt.c 示例, 使其能够正常工作(只需更改sep_serv.c示例).所谓"正常工作" 是指, 通过服务器端的半关闭状态接收客户端最后发送的字符串. 当然, 为了完成这一任务, 服务器端需要同时发送EOF. 发送EOF 的代码并不难, 通过示例给出.
在这里插入图片描述

#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"); /* 31 32: 调用fdopen函数生成FILE指针. 特别是第32行针对dup函数的返回值生成FILE指针, 因此函数调用后将进入图16-3所示的状态. */
    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); /* 39: 针对fileno函数返回的文件描述符调用shutdown函数. 因此, 服务器进入半关闭状态, 并向客户端发送EOF. 这一行就是之前所说的发送EOF的方法. 调用shutdown函数时, 无论复制出多少文件描述符都进入半关闭状态, 同时传递EOF */
    fclose(writefp);

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

上述示例可以结合sep_clnt.c 运行.我们关心的是服务器端能否收到客户端最后 消息, 因此只给出服务器端运行结果.
服务器端:
在这里插入图片描述客户端:
在这里插入图片描述
运行结果证明服务器端在半关闭状态下向客户端发送了EOF. 通过该示例希望各位掌握一点:
在这里插入图片描述第10章的 echo_mpclient.c 示例运用过shutdown 函数的这个功能, 当时通过fork 函数生成了2个文件描述符, 并在这种情况下调用shutdown函数发送了EOF.

结语:

你可以下面这个网站下载这本书<TCP/IP网络编程>
https://www.jiumodiary.com/

时间: 2020-06-08

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值