TCP/IP网络编程(11) 套接字和标准IO

标准IO函数的优点

标准IO函数具备两大优点:

  • 标准IO函数具备良好的移植性
  • 标准IO函数可以利用缓冲提高性能

不仅是IO函数,所有的标准函数都具有很好的移植性,为了支持所有的操作系统和编译器,这些函数都是按照ANSI C标准定义的; 此外,使用标准IO函数会得到额外的缓冲支持。这里的缓冲区应该与套接字的缓冲区进行区分,避免混淆。再创建套接字的时候,系统会生成用于IO的缓冲,此缓冲在执行TCP协议的时候发挥着非常重要的作用。如果此时使用标准IO函数,将得到除此之外的另一缓冲区支持。

由上图可知,使用标准IO进行数据传输的时候,经过两个缓冲区。套接字中的缓冲区主要是为了实现TCP协议而设立的,TCP在传输数据的过程中,如果丢失了数据,将会再次进行传输,而再次发送数据,意味着数据保存在了某个地方,并没有丢失,保存的地方就是套接字的输出缓冲区。与之相反,使用标准IO函数缓冲的主要目的是为了提高性能。

实际上,缓冲区并非在所有情况下都能带来卓越的性能,但是如果传输的数据量越大,有无缓冲带来的性能差异就越大。

采用不带缓冲的read/write函数进行文件读写:

#include <stdio.h>
#include <fcntl.h>
#include <cstdlib>
#include <ctime>
#include <unistd.h>

#define BUFF_SIZE 3

// 采用未提供缓冲区技术的read,write方法进行拷贝文件

int main(int argc, char* argv[])
{
    int fd1, fd2;       // 文件描述符
    int len;
    char buffer[BUFF_SIZE];

    fd1 = open("../news.txt", O_RDONLY);
    fd2 = open("../cpy.txt", O_WRONLY | O_CREAT | O_TRUNC);

    clock_t start = clock();

    len = read(fd1, buffer, BUFF_SIZE);

    while (len > 0)
    {
        /* code */
        write(fd2, buffer, len);
        len = read(fd1, buffer, BUFF_SIZE);
    }

    close(fd1);
    close(fd2);

    clock_t end = clock();

    double timespan = ((double)(end - start)) / CLOCKS_PER_SEC;

    printf("Total time consume is %f ms.\n", timespan * 1000);

    return 0;
}

文件读取以及写入时间如下所示:

标准IO进行文件内容读取:

#include <stdio.h>
#include <fcntl.h>
#include <cstdlib>
#include <ctime>

#define BUFF_SIZE 3


int main(int argc, char* argv[])
{
    FILE *fp1, *fp2;

    char buffer[BUFF_SIZE];

    fp1 = fopen("../news.txt", "r");
    fp2 = fopen("cpy.txt", "w");

    if (fp1 == NULL || fp2 == NULL)
    {
        printf("Failed to open file.\n");
        return -1;
    }
    
    // 程序计数
    clock_t start = clock();

    char* res = fgets(buffer, BUFF_SIZE, fp1);

    while (res != NULL)
    {
        /* code */
        fputs(buffer, fp2);

        res = fgets(buffer, BUFF_SIZE, fp1);
    }

    fclose(fp1);
    fclose(fp2);

    clock_t end = clock();

    // 计算消耗时间
    double timespan = ((double)(end - start)) / CLOCKS_PER_SEC;
    printf("The total time consume is %f ms.\n", timespan* 1000);
    
    return 0;
}

文件读取及写入时间如下所示:

可以看到具有缓冲区的标准IO函数具备更快的读写速度

标准IO函数的缺点:

  • 不容易进行双向通信
  • 有时可能频繁调用fflush函数 
  • 需要以FILE结构体指针的形式返回文件描述符

打开文件如果希望同时进行读写操作,则应该以r+,w+,a+的模式打开文件。但是由于缓冲的缘故,每次切换读写状态的时候,应该调用fflush函数,这个也会影响基于缓冲的性能提高。此外,为了使用标准的IO函数,需要传入FILE结构体指针,而在创建套接字的时候,默认返回的是文件描述符,因此需要额外将文件描述符转换为FILE指针。

利用标准IO函数实现套接字通讯

在创建套接字的时候会返回文件描述符,而为了使用标准IO函数,需要将其转换为FILE结构体指针,同时在某些应用场合,需要将FILE结构体指针转换为文件描述符,相互转换方法如下:

利用fdopen函数将文件描述符转换为FILE结构体指针

#include <stdio.h>

FILE* fdopen(int fildes, const char* mode)

/**
** fildes: 需要转换的文件描述符
** mode:   将要创建的FILE结构体指针的模式(mode)信息
**/

mode参数的形式与fopen函数的打开模式参数相同,常用的模式有读模式(r), 写模式(w)

示例代码

#include <stdio.h>
#include <fcntl.h>

int main(int argc, char* argv[])
{
    FILE* fp;
    
    int fd = open("data.txt", O_WRONLY | O_CREAT | O_TRUNC);
    if (fd < 0)
    {
        fputs("Faled to open file.\n", stdout);
        return -1;
    }

    fp = fdopen(fd, "w");   // 返回写模式的FILE指针

    if (fp == NULL)
    {
        fputs("Faled to convert file descriptor.\n", stderr);
        return -1;
    }

    fputs("This is a test content.\n", fp);    // 调用标准输出函数写入文件

    fclose(fp);

    return 0;
}

注: 利用FILE指针关闭文件,会使文件完全关闭,无需再通过文件描述符进行关闭,因为调用fclose函数之后,文件描述符已经变成毫无意义的整数。

利用fileno函数将FILE结构体指针转换为文件描述符:

在某些应用场合,需要将FILE结构体指针转换为文件描述符,可通过fileno函数进行抓换

#include <stdio.h>

int fileno(FILE* stream)

示例代码:

#include <stdio.h>
#include <fcntl.h>

int main(int argc, char* argv[])
{
    FILE* fp;
    
    int fd = open("data.txt", O_WRONLY | O_CREAT | O_TRUNC);
    if (fd < 0)
    {
        fputs("Faled to open file.\n", stdout);
        return -1;
    }

    printf("The first file descriptor is %d.\n", fd);

    fp = fdopen(fd, "w");

    if (fp == NULL)
    {
        fputs("Faled to convert file descriptor.\n", stderr);
        return -1;
    }

    fputs("This is a test content.\n", fp);

    printf("The second file descriptor is %d.\n", fileno(fp));

    fclose(fp);

    return 0;
}

输出结果:

 基于套接字的标准IO函数使用

下面以回声服务器为例,介绍将标准IO函数应用于套接字的实例:
服务端示例代码:

#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 10

#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];

    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.");
    }

    while (true)
    {
        /* code */
        fputs("Waiting for client connection...\n", stdout);
        clientSock = accept(serverSock, (struct sockaddr*)&clientAddr, &addrSize);

        if (clientSock == -1)
        {
            continue;
        }

        // fputs("Connected client %s.\n", inet_ntoa(clientAddr.sin_addr), stdout);
        printf("Connected client %s.\n", inet_ntoa(clientAddr.sin_addr));

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

        while (!feof(readfp))
        {
            fgets(buffer, BUF_SIZE, readfp);    // 标准IO读取
            fputs(buffer, writefp);
            fflush(writefp);      // 保证数据立即传输到客户端
        }

        fclose(readfp);
        fclose(writefp);
    }

    close(serverSock);
    
    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 10
#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", inet_ntoa(servAddr.sin_addr), ntohs(servAddr.sin_port));

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

    while(true)
    {
        fputs("Input message(Q or q to quit):", stdout);
        fgets(buffer, BUF_SIZE, stdin);

        if (!strcmp(buffer, "q\n") || !strcmp(buffer, "Q\n"))
        {
            break;
        }

        // 向服务端写数据
        fputs(buffer, writefp);
        fflush(writefp);
        
        // 清空缓冲区
        memset(buffer, 0, BUF_SIZE);

        // 读取父服务端数据
        fgets(buffer, BUF_SIZE, readfp);
        printf("Message from server is: %s\n", buffer);
    }

    fclose(readfp);
    fclose(writefp);

    return 0;
}

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

标准IO函数为了提高性能,内部提供额外的缓冲区,因此,若不调用fflush函数,则无法保证立即将数据传输到对端。使用标准IO函数还有一个好处,就是可以按字符串单位进行交换,而在前面的回声服务器的示例代码中,接收到的数据需要先转换为字符串(在数据的尾部插入0)。

...

write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE-1);
message[strlen] = 0;
printf(""Message from server is: %s\n, message);
...

posted @ 2022-12-29 11:08  Alpha205  阅读(54)  评论(0编辑  收藏  举报