15.套接字和标准I/O

前言

我们之前采用的都是默认数据通信手段read&write函数及各种系统I/O函数,可能大家想使用学习C语言时掌握的标准I/O函数。大家

也认为在网络数据交换时使用标准I/O函数是非常有趣的事情吧?

一、标准I/O 函数的优点

这个章节将介绍利用标准/O函数收发数据的方法。如果大家不太熟悉或已忘记多种标准函数,可以去问问豆包。当然,如果熟练掌握了文件操作时使用的fopen、feof、fgetc、fputs函数,就没必要再参考啦。

1.标准1/O函数的两个优点

将标准/O函数用于数据通信并非难事。但仅掌握函数使用方法并没有太大意义,至少应该了解这些函数具有的优点。下面列出的是标准I/O函数的两大优点。

■ 标准I/O函数具有良好的移植性(Portability)。

■ 标准/O函数可以利用缓冲提高性能。

关于移植性无需过多解释。不仅是/O函数,所有标准函数具有良好的移植性。因为,为了支持所有操作系统(编译器),这些函数都是按照ANSIC标准定义的。当然,这并不局限于网络编程,而是适用于所有编程领域。

接下来讨论标准I/O函数的第二个优点。使用标准I/O函数时会得到额外的缓冲支持。这种表达方式也许会带来一些混乱,因为之前讲过,创建套接字时操作系统会准备I/O缓冲。造成更大混乱之前,先说明这两种缓冲之间的关系。创建套接字时,操作系统将生成用于/O的缓冲。此缓冲在执行TCP协议时发挥着非常重要的作用。此时若使用标准I/O函数,将得到额外的另一缓冲的支持,如图:

从图中可以看到,使用标准/O函数传输数据时,经过2个缓冲。例如,通过fputs函数传输字符串"Hello"时,首先将数据传递到标准/O函数的缓冲。然后数据将移动到套接字输出缓冲,最后将字符串发送到对方主机。

既然知道了两个缓冲的关系,接下来再说明各自的用途。设置缓冲的主要目的是为了提高性能,但套接字中的缓冲主要是为了实现TCP协议而设立的。例如,TCP传输中丢失数据时将再次传递,而再次发送数据则意味着在某地保存了数据。存在什么地方呢?套接字的输出缓冲。与之相反,使用标准/O函数缓冲的主要目的是为了提高性能。

"使用缓冲可以大大提高性能吗?"

实际上,缓冲并非在所有情况下都能带来卓越的性能。但需要传输的数据越多,有无缓冲带

来的性能差异越大。可以通过如下两种角度说明性能的提高。

■ 传输的数据量

■ 数据向输出缓冲移动的次数

比较1个字节的数据发送10次(10个数据包)的情况和累计10个字节发送1次的情况。发送数据时使用的数据包中含有头信息。头信息与数据大小无关,是按照一定的格式填人的。即使假设该头信息占用40个字节(实际更大),需要传递的数据量也存在较大差别。

■ 1个字节10次40×10=400字节

■ 10个字节1次40×1=40字节

另外,为了发送数据,向套接字输出缓冲移动数据也会消耗不少时间。但这同样与移动次数有关。1个字节数据共移动10次花费的时间将近10个字节数据移动1次花费时间的10倍。

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

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

c 复制代码
//stdcpy.c
#include <stdio.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函数复制文件。

c 复制代码
#include <stdio.h>
#define BUF_SIZE 3  //用最短数组长度构成

int main(int argc, char *argv[])
{
    FILE*fP1;//保存在fP1中的是FILE结构体指针
    FILE*fp2;//保存在fp2中的是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字节并非大数据,即便如此,在单纯的文件复制操作中也会有如此大的差异。可以想象,在实际网络环境中将产生更大区别。

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

如果就此结束说明,各位可能认为标准I/O函数只有优点。其实它同样有缺点:

■ 不容易进行双向通信。

■ 有时可能频繁调用flush函数。

■ 需要以FILE结构体指针的形式返回文件描述符。

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

二、使用标准IO 函数

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

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

c 复制代码
#include <stdio.h>
FILE * fdopen(int fildes, const char *mode);
// 成功时返回转换的FILE结构体指针,失败时返回NULL。
// fildes:需要转换的文件描述符。
// mode:将要创建的FILE结构体指针的模式(mode)信息。

上述函数的第二个参数与fopen函数中的打开模式相同。常用的参数有读模式""和写模式"w"。下面通过简单示例给出上述函数的使用方法。

c 复制代码
#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;
    }

    fp=fdopen(fd, "w");
    fputs("Network C programming \n",fp);
    fclose(fp);
    return 0;
}

●第7行:使用open函数创建文件并返回文件描述符。

●第14行:调用fdopen函数将文件描述符转换为FlLE指针。此时向第二个参数传递了"w",因此返回写模式的FILE指针。

●第15行:利用第14行获取的指针调用标准输出函数fputs。

●第16行:利用FILE指针关闭文件。此时完全关闭,因此无需再通过文件描述符关闭。而且调用fclose函数后,文件描述符也变成毫无意义的整数。

文件描述符转换为FILE指针,并可以通过该指针调用标准I/O函数。

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

c 复制代码
#include <stdio.h>
int fileno(FILE * stream);
// 成功时返回转换后的文件描述符,失败时返回-1。

这个函数的用法也非常简单,向该函数传递FILE指针参数时就会返回相应文件描述符:

c 复制代码
#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;
    }

    fp=fdopen(fd, "w");
    fputs("Network C programming \n",fp);
    fclose(fp);
    return 0;
}

●第14行:输出第7行返回的文件描述符整数值。

●第15、17行:第15行调用fdopen函数将文件描述符转换为FlLE指针,第17行调用fileno函数再次转回文件描述符,并输出该整数值。

第14行和第17行输出的文件描述符值相同,证明我们调用fileno函数正确转换了文件描述符。

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

前面介绍了标准I/O函数的优缺点,同时介绍了文件描述符转换为FILE指针的方法。下面将其适用于套接字。虽然是套接字操作,但并没有需要另外说明的内容,只需简单应用这些函数。接下来将之前的回声服务器端和客户端改为基于标准I/O函数的数据交换形式,更改对象如下。

■ 回声服务器端:第4章的echo_server.c

■ 回声客户端:第4章的echo_client.c

无论是服务器端还是客户端,更改方式并无差异。只需调用fdopen函数并使用标准/O函数,相信大家也能自行更改。

c 复制代码
#include<"头文件声明与第4章节的echo_server.c一致。">
#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[e]);
        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;
}

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

c 复制代码
#include<"头文件声明与第4章的echo_client.C一致。">
#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, e, 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") II !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;
}

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


总结

主要说明了关于C语言的文件指针函数有关的用法,大家看一下,都是带'f' 的,很好记。

(有些日子没写了,前面这段时间工作有点忙,emmmmm)

相关推荐
利刃大大7 小时前
【高并发服务器】十二、LoopThreadPool线程池管理模块
服务器·c++·项目
ai旅人7 小时前
深入理解OkHttp超时机制:连接、读写、调用超时全面解析
java·网络·okhttp
我也要当昏君7 小时前
4.2 IPv4【2009统考真题】
网络·智能路由器
爱奥尼欧7 小时前
【Linux笔记】网络部分——网络层IP协议
linux·网络·笔记
Jie_jiejiayou8 小时前
按键防抖 — 工业级标准实现总结(STM32)
c语言·stm32·按键消抖
埃伊蟹黄面8 小时前
告别手动编译:用Makefile自动化你的Linux项目
linux·服务器
Once_day8 小时前
Linux错误(7)接口处于Down状态不通告IPv6地址变更事件
linux·运维·服务器
任风雨8 小时前
15.1.2.linux常见操作用例
linux·服务器
攀小黑8 小时前
docker 容器内nacos(若依plus打包)连接另一台内网服务器显示数据库连接失败
服务器·数据库·docker