TCP IP网络编程 IO分离

IO流分离

  在《套接字与标准IO》中介绍,调用fopen函数打开文件后,可以与文件进行交换数据。因此可以说调用fopen函数之后创建了"流(stream)",此处的"指"数据流动,但是通常可以理解为"以数据手法为目的的一种桥梁"。

1. IO分离的方法

  在之前的文章中介绍过两种分离IO流的方法,第一种是是在《TCP/IP网络编程(8) 基于Linux的多进程服务器 》中介绍的多进程方式分离IO流,通过调用fork()函数赋值出一个文件描述符,以区分输入和输出中使用的文件描述符;第二种分离是在《套接字与标准IO》中介绍,通过调用fdopen函数,分别创建读模式的FILE结构体指针和写模式的FILE结构体指针,这种方法分离的输出工具和输入工具,因此也可以视为流的分离。

1.2 流分离带来的好处

  在不同的应用场景下,流的分离在目的上有不同的差异,在《TCP/IP网络编程(8) 基于Linux的多进程服务器 》中,流分离的目的为:

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

《套接字与标准IO》中,流分离的目的:

  • 为了将FILE结构体指针按照读模式和写模式进行区分
  • 可以通过区分读模式和写模式降低实现难度
  • 通过区分IO缓冲提高缓冲性能
1.3 流分离带来的EOF问题

  C的标准IO中EOF的概念:EOF不是一个字符,它只是一个控制条件,函数返回EOF表示当前的调用出现了以下两种情况:

  • 文件达到了末尾
  • 文件读写过程中出现了错误

EOF的定义为: #define EOF (-1)

1.3.1 例如fgets()函数从stream流中读取一个字符:
int fgets(FILE *stream);

函数的读取过程为:

  1. 函数每次调用都返回stream流的下一个字符
  2. 函数的返回类型为 unsigned char,最终会强制转换为int进行返回
  3. 如果读取过程中发生了错误,或者到达了文件的末尾,则会返回EOF,可通过feof函数和ferror函数区分是达到了文件末尾还是发生了错误
1.3.2例如UNIX中的read()函数
ssize_t read(int fd, void *buf, size_t count)

读取过程如下:

  1. 读取成功,返回读取的字节数
  2. 返回0表示到达文件末尾
  3. 读取失败,返回-1
1.4 网络编程中的EOF

  在TCP通信中,调用如下语句:

shutdown(sock, SHUT_WR);

是一种基于半关闭的EOF传递方法,此时即发送了EOF,又保证了输入流不关闭,还可以接受对方的信息。那么在《套接字与标准IO》中,在使用fdopen()的情况下,如何进行半关闭?

server示例代码:

#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BUF_SIZE 30

#define SERVER_ADDR  "127.0.0.1"
#define SERVER_PORT  12000

void error_handler(const char* msg);

int main(int argc, char* argv[])
{
    int serverSock;
    int clientSock;

    char buffer[BUF_SIZE];
    memset(buffer, 0, BUF_SIZE);

    struct sockaddr_in servAddr;
    struct sockaddr_in clientAddr;

    socklen_t addrSize = sizeof(servAddr);

    FILE* readfp;   
    FILE* writefp;

    serverSock = socket(PF_INET, SOCK_STREAM, 0);

    if (serverSock == -1)
    {
        error_handler("Failed to create socket.");
    }

    // 服务端地址初始化
    memset(&servAddr, 0, sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servAddr.sin_port = htons(SERVER_PORT);

    if (bind(serverSock, (struct sockaddr*)&servAddr, sizeof(servAddr)) == -1)
    {
        // failed to bind the socket
        error_handler("Failed to bind the server address.");
    }

    if (listen(serverSock, 5) == -1)
    {
        error_handler("Failed to listen.");
    }

    fputs("Waiting for client...\n", stdout);

    clientSock = accept(serverSock, (struct sockaddr*)&clientAddr, &addrSize);     // 接受客户端链接

    printf("Receive connect from %s %d \n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

    readfp  = fdopen(clientSock, "r");
    writefp = fdopen(clientSock, "w");

    // 向客户端发送信息
    fputs("Hello client, this message is from server.\n", writefp);
    fputs("This is a test message from server.\n", writefp);
    fputs("We will close the write stream.\n", writefp);
    fflush(writefp);

    // 关闭writefp, 看能否模拟类似TCP半关闭状态,向客户端发送EOF
    fclose(writefp);

    // 看能否再接受到客户端的消息 
    fgets(buffer, BUF_SIZE, readfp);

    // 将接收到的消息输出到标准输出
    fputs(buffer, stdout);

    fclose(readfp);

    return 0;
}

void error_handler(const char* msg)
{
    printf("%s\n", msg);
    exit(-1);
}

client.cpp示例代码:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

#define BUF_SIZE 30
#define SERVER_ADDR  "127.0.0.1"
#define SERVER_PORT  12000


void error_handler(const char* msg);

int main(int argc, char* argv[])
{
    int sock;
    char buffer[BUF_SIZE];

    struct sockaddr_in servAddr;

    FILE* readfp;
    FILE* writefp;

    sock = socket(PF_INET, SOCK_STREAM, 0);

    if (sock == -1)
    {
        error_handler("Failed to create sock");
    }

    memset(buffer, 0 , BUF_SIZE);

    memset(&servAddr, 0, sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = inet_addr(SERVER_ADDR);
    servAddr.sin_port = htons(SERVER_PORT);

    if (connect(sock, (struct sockaddr*)&servAddr, sizeof(servAddr)) == -1)
    {
        error_handler("Failed to connect to server.\n");
    }

    printf("Connecting to server: %s : %d \n", inet_ntoa(servAddr.sin_addr), ntohs(servAddr.sin_port));

    readfp = fdopen(sock, "r");
    writefp = fdopen(sock, "w");

    while (true)
    {
        char *res = fgets(buffer, BUF_SIZE, readfp);   // 收到EOF后,fgets函数会返回NULL指针

        if (!res)
            break;

        fputs(buffer, stdout);
        fflush(stdout);
    }

    // 此时客户端只关闭了writefp, 向服务端发送消息,看此时的服务端能否受到消息
    fputs("Hello server, this is a message from client.\n", writefp);
    fflush(writefp);       // 立即发送
    
    fclose(readfp);
    fclose(writefp);

    return 0;
}

void error_handler(const char* msg)
{
    printf("%s\n", msg);
    exit(-1);
}

运行结果:

img

img

上述的示例代码中,在服务端程序中,发送完消息之后,针对写模式的FILE结构体指针调用fclose()函数,此时会终止套接字,客户端在收到EOF后,也会发送最后的字符串,服务端读模式的FILE结构体指针并没有关闭,那么此时服务端还能否收到客户端发来的消息呢?根据运行的结果来看,此时即使服务端读模式的FILE结构体指针未关闭,服务端也收不到客户端发来的消息。

服务器端未能收到客户端最后发来的字符串,因为服务器端在调用fclose()函数之后,完全关闭了套接字,而不是半关闭。

2. 文件描述符的赋值和半关闭

  半关闭状态在很多情况下都非常有用,因此实现对fdopen()函数调用生成的FILE结构体指针的半关闭非常有必要。

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

  在上述的服务端与客户端的示例代码中,FILE结构体指针,文件描述符,以及套接字的关系如下所示:

如上图所示,读模式的FILE指针和写模式的FILE指针都是基于同一个文件描述符创建的,因此,针对任何一个FILE结构体指针调用fclose()函数的时候,都会将文件描述符关闭,导致套接字完全终止,如下图所示:

如上图所示,无论关闭哪一个FILE结构体指针,都会导致文件描述符被销毁,导致套接字完全终止,无法进行数据传输。为了能够实现可以输入但是无法输出的半关闭状态,可在创建FILE结构整体指针之前先复制文件描述符,如下图所示:

因为当销毁所有的文件描述符之后,才能销毁套接字,因此上图中通过拷贝一个文件描述符,避免套接字被销毁

如上图所示,如果调用fclose()函数后还剩余一个文件描述符,因此没有销毁套接字,但是,此时的状态并不是半关闭状态,只是准备好了半关闭环境,要真正进入半关闭状态,还需要进行特殊处理,因为从上图可知,还剩余一个文件描述符,且该文件描述符可以同时进行IO,因此,不但没有发送EOF,而且还可以利用该文件描述符进行输出。

2.2 文件描述符的复制方法

《TCP/IP网络编程(8) 基于Linux的多进程服务器 》中介绍过套接字的复制,通过调用fork()函数会对套接字和文件描述符进行复制,但是调用fork()函数的时候,将会复制整个进程,因此在同一个进程中不会存在原本和副本。此处”复制“文件描述符的含义是,为了访问同一文件或者套接字,创建的另一个文件描述符。

通过下面的函数可以实现文件描述符的复制:

#include <unistd.h>

int dpu(int fildes);
int dpu2(int fildes, int fildes2);

// fildes: 需要复制的文件描述符
// fildes: 明确指定的文件描述符的指定值 大于0,小于文件描述符最大值

注:dpu2()函数明确指定复制的文件描述符的整数值,该值将成为复制出的文件描述符的值

利用文件描述符0,1,2验证函数的功能,因为这三个文件描述符和套接字文件描述符在使用上没有区别:

#include <stdio.h>
#include <unistd.h>

int main(int argc, char* argv[])
{
    int cfd1, cfd2;

    char str1[] = "Hello world.\n";
    char str2[] = "This is a test mesage.\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;
}

运行结果:
img

从运行结果可容易分析得到上述文件描述符无法半关闭的原因。

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

通过对上述服务端的代码进行改进,可实现服务端进入半关闭状态发送EOF,且接收客户端最后发送的字符串。

服务端示例代码:

#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BUF_SIZE 256

#define SERVER_ADDR  "127.0.0.1"
#define SERVER_PORT  12000

void error_handler(const char* msg);

int main(int argc, char* argv[])
{
    int serverSock;
    int clientSock;

    char buffer[BUF_SIZE];
    memset(buffer, 0, BUF_SIZE);

    struct sockaddr_in servAddr;
    struct sockaddr_in clientAddr;

    socklen_t addrSize = sizeof(servAddr);

    FILE* readfp;   
    FILE* writefp;

    serverSock = socket(PF_INET, SOCK_STREAM, 0);

    if (serverSock == -1)
    {
        error_handler("Failed to create socket.");
    }

    // 服务端地址初始化
    memset(&servAddr, 0, sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servAddr.sin_port = htons(SERVER_PORT);

    if (bind(serverSock, (struct sockaddr*)&servAddr, sizeof(servAddr)) == -1)
    {
        // failed to bind the socket
        error_handler("Failed to bind the server address.");
    }

    if (listen(serverSock, 5) == -1)
    {
        error_handler("Failed to listen.");
    }

    fputs("Waiting for client...\n", stdout);

    clientSock = accept(serverSock, (struct sockaddr*)&clientAddr, &addrSize);     // 接受客户端链接

    printf("Receive connect from %s %d \n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

    readfp  = fdopen(clientSock, "r");
    writefp = fdopen(dup(clientSock), "w");

    // 向客户端发送信息
    fputs("Hello client, this message is from server.\n", writefp);
    fputs("This is a test message from server.\n", writefp);
    fputs("We will close the write stream.\n", writefp);
    fflush(writefp);

    // 关闭writefp, 向客户端发送EOF
    shutdown(fileno(writefp), SHUT_WR);    // 发送EOF
    fclose(writefp);

    // 看能否再接受到客户端的消息 
    fgets(buffer, BUF_SIZE, readfp);

    // 将接收到的消息输出到标准输出
    fputs(buffer, stdout);

    fclose(readfp);

    return 0;
}

void error_handler(const char* msg)
{
    printf("%s\n", msg);
    exit(-1);
}

客户端示例代码:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

#define BUF_SIZE 256
#define SERVER_ADDR  "127.0.0.1"
#define SERVER_PORT  12000


void error_handler(const char* msg);

int main(int argc, char* argv[])
{
    int sock;
    char buffer[BUF_SIZE];

    struct sockaddr_in servAddr;

    FILE* readfp;
    FILE* writefp;

    sock = socket(PF_INET, SOCK_STREAM, 0);

    if (sock == -1)
    {
        error_handler("Failed to create sock");
    }

    memset(buffer, 0 , BUF_SIZE);

    memset(&servAddr, 0, sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = inet_addr(SERVER_ADDR);
    servAddr.sin_port = htons(SERVER_PORT);

    if (connect(sock, (struct sockaddr*)&servAddr, sizeof(servAddr)) == -1)
    {
        error_handler("Failed to connect to server.\n");
    }

    printf("Connecting to server: %s : %d \n", inet_ntoa(servAddr.sin_addr), ntohs(servAddr.sin_port));

    readfp = fdopen(sock, "r");
    writefp = fdopen(sock, "w");

    while (true)
    {
        char *res = fgets(buffer, BUF_SIZE, readfp);    // fgets()函数接收到EOF返回的是NULL

        if (!res)
            break;

        fputs(buffer, stdout);
        fflush(stdout);
    }

    // 此时客户端只关闭了writefp, 向服务端发送消息,看此时的服务端能否受到消息
    fputs("Hello server, this is the last message from client.\n", writefp);
    fflush(writefp);       // 立即发送
    
    fclose(readfp);
    fclose(writefp);

    return 0;
}

void error_handler(const char* msg)
{
    printf("%s\n", msg);
    exit(-1);
}

输出结果:

img

img

由上图输出结果可知,通过复制文件描述符,成功实现了TCP套接字的半关闭状态。在服务端代码中:

// 关闭writefp, 向客户端发送EOF
shutdown(fileno(writefp), SHUT_WR);    // 发送EOF
fclose(writefp);

针对fileno()函数返回的文件描述符调用shutdown()函数,此时服务器端套接字进入半关闭状态,并向客户端发送EOF。

调用shutdown()函数时,无论复制出多少文件描述符都进入半关闭状态,同时传递EOF

posted @ 2022-12-31 17:34  Alpha205  阅读(65)  评论(0编辑  收藏  举报