TCP/IP网络编程_基于Linux的编程_第15章套接字和标准I/O

在这里插入图片描述

15.1 标准I/O 函数的优点

本章将介绍利用标准的I/O函数收发数据的方法. 如果各位不太熟悉或已忘记多种标准函数, 最好准备一本C语言的书在开始学习本章的学习. 当然, 如果熟练掌握了文件操作时使用fopen, feof, fgetc, fputs 函数, 就没必要再参考C语言的书了.

标准 I/O 函数的两个优点

将标准 I/O 函数用于数据通信并非难事. 但仅掌握函数的使用方法并没有太大的意义, 至少应该了解这些函数具有的优点. 下面列出的是标准I/O函数的两个优点.
在这里插入图片描述关于移植性无需过多的解析. 不仅是I/O函数, 所有标准函数具有良好的移植性. 因为, 为了支持所有操作系统(编译器), 这些函数都是按照ANSI标准点定义的. 当然, 这并不局限于网络编程, 而是适用于编程领域.

接下来讨论标准I/O函数的第二优点. 使用标准I/O函数时会得到额外的缓冲支持. 这种表达方式也许会带来一些混乱, 因为之前也讲过, 创建套接字时操作系统会准备I/O缓冲. 造成更大混乱之前, 先说明这两种缓冲之间的关系. 创建套接字时, 操作系统将生成用于I/O缓冲. 此缓冲在执行TCP协议时发挥着非常重要的作用. 此时若使用标准I/O函数, 将得到额外的另一缓冲的支持, 如图15-1所示.
在这里插入图片描述从图15-1中可以看到, 使用标准I/O函数传输数据时, 经过2个缓冲. 例如, 会通过fputs 函数传输字符串"Hello"时, 首先将数据传递到标准I/O函数的缓存. 然后数据将移动到套接字输出缓冲, 最后字符串发送到对方主机.

既然知道了两个缓冲的关系, 接下来再说名各自的用途. 设置缓冲的主要目的是为了提高性能, 但套接字中的缓存主要是为了实现TCP 协议而设立的. 例如, TCP传输中丢失的数据再次传递, 而再次发送数据则意味着在某第保存了数据. 存在什么地方呢? 套机字的输出缓冲. 与之相反, 使用标准I/O函数缓冲的主要目的是为了提高性能.
在这里插入图片描述实际上, 缓冲并非在所有情况下都能带来卓越的性能. 但需要传输的数据越多, 有无缓冲带来性能差异越大. 可以通过如下两种角度说明性能的提高.
在这里插入图片描述比较1个字节的数据发送10次(10个数据包)的情况和累积10个字节发送1次的情况. 发送数据时使用的数据包中含有头信息. 头信息与数据大小无关, 是按照一定的格式填入的. 即使假设该头信息占用40个字节(实际更大), 需要传递的数据量也存在较大差别.
在这里插入图片描述另外, 为了发送数据, 向套接字输入缓冲移动数据也会消耗不少时间. 但这同样也移动次数有关. 1个字节数共移动10次花费的时间接近10个字节数据移动1次花费时间的10倍.

标准 I/O 函数和系统函数之间的性能对比

前面讲解了缓冲可以提升性能的原因, 但只停留在理论分析层面. 接下来分别利用标准 I/O 函数和系统函数编写文件复制程序, 这主要是为了检验缓冲提供性能的程度. 首先是利用系统函数复制文件的示例.
在这里插入图片描述

#include <stdlib.h>
#include <fcntl.h>

#define BUF_SIZE 3 /* 用最短数组长度构成 */

int main(int argc, char *argv[])
{
    int fd1, fd2; /* 保存在fd1和fd2中的是文件描述符 */
    int len;
    char buf[BUF_SIZE];

    fd1 = open("news.txt", O_RDONLY);
    fd2 = open("cpy.txt", O_WRONLY|O_CREAT|O_TRUNC);
    
    while ((len = read(fd1, buf, sizeof(buf))) > 0)
    {
        write(fd2, buf, len);
    }

    close(fd1);
    close(fd2);
    return 0;
}

上述示例是各位很容易分析的基于read & write 函数的文件复制程序. 复制对象仅限于文本文件, 并且是300M字节以上的文件! 因为只有这样才能明显感觉到性能差异. 文件名为news.txt, 大家可以适当修改并测试.

各位是否正在复制文件? 如果按照我的要求正在复制300M字节以上的文件, 可以去一趟洗手间; 若不想去, 可以喝杯咖啡. 如果使用未提及缓冲的read & write 函数传输数据, 向目的地发送要花费很长时间. 下列示例采用标准I/O函数复制文件.
在这里插入图片描述

#include <stdio.h>

#define BUF_SIZE 30

int main(int argc, char *argv[])
{
    FILE *fp1; /* 保存在fd1中的是FILE结构体指针 */
    FILE *fp2; /* 保存在fd1中的是FILE结构体指针 */
    char buf[BUF_SIZE];

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

    while (fgets(buf, BUF_SIZE, fp1) != NULL)
    {
        fputs(buf, fp2);
    }

    fclose(fp1);
    fclose(fp2);
    return 0;
}

上述示例利用示例syscpy.c 中复制的文件再次进行复制. 该示例利用 fputs & fgets 函数复制文件, 因此是一种基于缓冲的复制. 各位是否执行过复制? 不用到别的地方歇息, 只需原地活动片刻即可完成. 其实现在的300M字节并非大数据, 即便如此, 在单纯的文件复制操作中也会有如此大差别. 可以想象, 在实际网络环境中将产生更大的区别.

标准 I/O 函数的几个缺点

如果就此结束说明, 各位可能认为标准 I/O 函数只有优点. 其实它同样有缺点, 整理如下.
在这里插入图片描述假设各位已掌握了C语言中的绝大部分文件I/O 相关知识. 打开文件时, 如果希望同时进行读写操作, 则应以r+ w+ a+ 模式打开. 但因为缓冲的缘故, 每次切换读写工作状态时应调用 fflush 函数. 这也是会影响基于缓冲的性能提高. 而且, 为了使用标准 I/O 函数, 需要 FILE 结构体指针 (以下简称 “FILE指针”). 而创建套接字是默认返回文件描述符, 因此需要将文件描述符转化为 FILE 指针. 若各位难以分清FILE 指针和文件描述符, 可以通过上述syscpy.c 和stdcpy.c 示例加以区分.

15.2 使用标准 I/O 函数

如前所述, 创建套接字时返回文件描述符, 而为了使用标准I/O 函数, 只能将其转换为 FILE 结构体指针. 先介绍其转换方法.

利用 fdopen 函数转换为 FILE 结构体指针

可以通过 fdopen 函数将创建套接字时返回的文件描述符转换为标准I/O函数中使用的 FILE 结构体指针
在这里插入图片描述上述函数的第二个参数与 fopen函数中的打开模式相同. 常用的参数有读模式 “r” 和写模式"w". 下面通过简单实例给出上述的函数的使用方法.
在这里插入图片描述

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

int main(void)
{
    FILE *fp;
    int fd = open("data.dat", O_WRONLY|O_CREAT|O_TRUNC); /* 使用open函数创建文件并返回文件描述符 */
    if (fd == -1)
    {
        fputs("file oper error", stdout);
        return -1;
    }

    fp = fdopen(fd, "w"); /* 调用fdopen函数将文件描述符转换为FILE指针. 此时向第二个参数传递了 "W", 因此返回写模式的FILE指针 */
    fputs("Network C programming \n", fp); /* 利用第14行获取的指针调用标准输出函数fputs. */
    fclose(fp); /* 利用FILE指针关闭文件. 此时完全关闭, 因此无需通过文件描述符关闭. 而且调用fclose函数后, 文件描述符也变成毫无意义的整数. */

    return 0;
}

运行结果;
在这里插入图片描述此示例中需要注意的是, 文件描述符转换为FILE指针, 并可以通过该指针调用标准I/O函数.

利用 fileno 函数换换为文件描述符

接下来介绍与 fdopen 函数提供相反功能的函数, 该函数在有些情况下非常有用.
在这里插入图片描述此函数的用法也非常简单, 向该函数传递FILE指针参数时返回相应文件描述符. 接下来给出 fileno 函数的调用示例.
在这里插入图片描述

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

int main(void)
{
    FILE *fp;
    int fd = open("data.dat", O_WRONLY|O_CREAT|O_TRUNC);
    if (fd == -1)
    {
        fputs("file open error", stdout);
        return -1;
    }

    printf("First file descriptor: %d \n", fd); /* 输出第7行返回的文件描述符整数值. */
    fp = fdopen(fd, "w"); /* 第15行调用fdopen函数将文件描述符转换为FILE指针, 第17行调用fileno函数再次转回文件描述符, 并输出该整数值. */
    fputs("TCP/IP SOCKET PROGRAMMING \n", fp);
    printf("Second file descriptor: %d \n", fileno(fp));
    fclose(fp);

    return 0;
}

运行结果:
在这里插入图片描述第14行和第17行输出的文件描述符值相同, 证明 fileno函数正确转换了文件描述符.

15.3 基于套接字的标准 I/O 函数使用

前面介绍了标准 I/O 函数的优缺点, 同时介绍了文件描述符转换为 FILE 指针的方法. 下面将适用于套接字. 虽然是套接字操作, 但并没有需要另外说明的内容, 只需简单应用这些函数. 接下来将之前的回声服务器和客户端改为基于标准 I/O 函数的数据交换形式, 更改对象如下.
在这里插入图片描述无论是服务器端还是客户端, 更改方式并无差异. 只需调用fdopen 函数并使用标准 I/O函数, 相信各位也能自行更改. 首先给出更改后的服务器端代码.
在这里插入图片描述

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

#define BUF_SIZE 1024

void error_handling(char *message);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    char message[BUF_SIZE];
    int str_len, i;

    struct sockaddr_in serv_adr;
    struct sockaddr_in clnt_adr;
    socklen_t clnt_adr_sz;
    FILE *readfp;
    FILE *writefp;
    if (argc != 2)
    {
        printf("Usage : %s <port> \n", argv[0]);
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
    {
        error_handling("socket() error");
    }

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

    if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
    {
        error_handling("bind() error");
    }

    if (listen(serv_sock, 5) == -1)
    {
        error_handling("listen() error");
    }

    clnt_adr_sz = sizeof(clnt_adr);

    for (i=0; i<5; i++)
    {
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
        if (clnt_sock == -1)
        {
            error_handling("accept() error");
        }
        else
        {
            printf("Connected client %d \n", i+1);
        }

        readfp = fdopen(clnt_sock, "r");
        writefp = fdopen(clnt_sock, "w");
        while(!feof(readfp))
        {
            fgets(message, BUF_SIZE, readfp);
            fputs(message, writefp);
            fflush(writefp);
        }

        fclose(readfp);
        fclose(writefp);
    }

    close(serv_sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

上述示例中需要注意的是第xx行的循环语句. 调用基于字符串的fgets, fputs 函数提供服务, 并在第xx行调用fflush函数. 标准I/O函数为了提高性能, 内部提供额外的缓冲. 因此, 若不调用 fflush 函数则无法保证立即将数据传输到客户端. 接下来给出回声客户端代码.
在这里插入图片描述

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

#define BUF_SIZE 1024

void error_handling(char *message);

int main(int argc, char *argv[])
{
    int sock;
    char message[BUF_SIZE];
    int str_len;
    struct sockaddr_in serv_adr;
    FILE *readfp;
    FILE *writefp;
    if (argc != 3)
    {
        printf("Usage : %s <IP> <port> \n", argv[0]);
        exit(1);
    }

    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1)
    {
        error_handling("socket() error");
    }

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));

    if (connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
    {
        error_handling("connect() error");
    }
    else
    {
        puts("Connected.........");
    }

    readfp = fdopen(sock, "r");
    writefp = fdopen(sock, "w");
    while (1)
    {
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
        {
            break;
        }

        fputs(message, writefp);
        fflush(writefp);
        fgets(message, BUF_SIZE, readfp);
        printf("Message from server: %s", message);
    }

    fclose(writefp);
    fclose(readfp);

    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

第四章的回声客户端需要将接收的数据转换为字符串(数据的尾部插入0), 但上述示例中并没有这一过程. 因为, 使用标准 I/O 函数后可以按字符串单位进行数据交换. 运行结果与第4章的程序并无差异, 故省略. 以上就是标准 I/O 函数在套接字编程中的应用方法, 因为需要编写额外的代码, 所以并不想象中那么常用. 当某些情况下也是非常有用的, 而且可以再次复习标准 I/O 函数, 对大家也非常意义.

运行结果:
服务器端:
在这里插入图片描述客户端:
在这里插入图片描述

结语:

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

时间: 2020-06-07

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值