Linux网络编程 - 多种 I/O 函数(send、recv、readv、writev)

一 send & recv 函数

         前面博文中的示例程序中,基于Linux 的网络编程程序使用的都是 read & write 函数完成数据 I/O 操作的。其实Linux系统也提供了专门用于 I/O 操作的系统调用函数,那便是 send & recv 函数。下面我们将讲解这两个函数的使用方法和注意事项。

知识拓展》对 Linux 系统调用 的理解

1、系统调用是什么?

        简单的说,系统调用就是操作系统内核向用户进程提供系统服务的子程序(函数)。应用程序通过调用操作系统内核提供的功能模块(函数)来访问相应的内核资源。操作系统通过提供一系列相关功能模块的函数作为用户进程访问内核资源的入口。像 read、write、send、recv 等函数都是由操作系统内部实现的系统编程接口函数,提供给我们程序员使用的。从编程的角度来说,用户程序调用由操作系统内核提供的用来访问内核资源的函数称为“系统调用”。

        系统调用函数与C库调用函数是有区别的,C库函数调用是建立在系统调用的基础之上的,也就是说,C库 API 函数封装了系统调用函数。Linux系统中使用的C库是 glibc,它实现了Linux 的主要用户编程API,包括标准C库函数和Linux系统调用。从编程者的观点看,C库调用和系统调用之间的差别是没有关系的:唯一相关的事情就是函数名、参数类型及返回值的含义。然而,从内核设计者的观点看,这种差别确实有关系,因为系统调用属于操作系统内核,而C库函数不属于内核,是属于第三方库函数。

2、为什么需要系统调用?

        在Linux操作系统中,进程的运行状态分为用户态内核态两种模式,而处于用户态的进程是不能直接访问内核资源的,只有在内核态下才能访问。因此,用户态的进程想要访问内核,必须先切换成内核态(通过执行CPU指令完成用户态向内核态的转换)。我们要知道,用户进程是存储在用户空间的,而操作系统内核是存储在内核空间的,为了保证操作系统的稳定和安全,内核数据不被肆意篡改,是不允许用户进程直接访问内核空间的,因此为了与用户空间上运行的进程进行交互,内核提供了一组对外访问接口,通过这个访问接口,用户进程可以访问内核的资源,这便是系统调用的作用。

系统调用相关内容参考链接

Linux系统调用详解(实现机制分析)--linux内核剖析(六)

linux下的系统调用全过程

深入理解Linux内核--系统调用(阅读笔记)(原创)

系统调用与库调用

1.1 Linux 中的 send & recv 系统调用

        send、recv 这两个函数是由 Linux 操作系统内核提供的用于 socket 进行数据收发的系统调用。Windows系统中也提供有自己 send/recv函数,在不同操作系统平台下,这两个函数在使用方法上可能没有什么差别,但是在具体内部实现上还是有所不同的。

  • send() — 从套接字发送消息。
#include 
#include 

//用于TCP数据发送
int send(int sockfd, const void *msg, size_t len, int flags);

//用于UDP数据发送
int sendto(int sockfd, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);

//通用数据发送
int sendmsg(int sockfd, const struct msghdr *msg, int flags);

参数说明

  • sockfd:表示与通信对端进行数据交互的套接字文件描述符。
  • msg:保存待发送数据的缓冲区的地址值。
  • len:待发送数据的字节数。
  • flags:发送数据时指定的可选项信息。

返回值】成功时返回发送的字节数,失败时返回-1。

  • recv() — 从套接字接收消息。
#include 
#include 

//TCP数据接收
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

//UDP数据接收
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                struct sockaddr *src_addr, socklen_t *addrlen);

//通用数据接收
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

参数说明

  • sockfd:表示与通信对端进行数据交互的套接字文件描述符。
  • buf:保存待接收数据的缓冲区地址值。
  • len:可接收的最大字节数。
  • flags:接收数据时指定的可选项信息。

返回值】成功时返回实际接收到的字节数(收到EOF时返回 0),失败时返回 -1。

  • send、recv 函数的可选项

        send、recv 函数的最后一个参数 flags 是收发数据时的可选项。该可选项可通过位或(|)运算同时传递多个可选项信息。下表 1 给出了这两个函数的可选项信息的种类和含义。

表1 send&recv 函数的可选项及含义
可选项(Option) 含义 send recv
MSG_OOB 用于传输带外数据(Out-of-band data) Y Y
MSG_PEEK 验证输入(接收)缓冲中是否存在待接收的数据 N Y
MSG_DONTROUTE 数据传输过程中不参照路由(Routing)表,在本地(Local)网络中寻找目的地 Y N
MSG_DONTWAIT 调用 I/O 函数时不阻塞,用于使用非阻塞(Non-blocking)I/O Y Y
MSG_WAITALL 防止函数返回,直到接收到全部请求的字节数才返回 N Y

        另外,不同操作系统对上述可选项的支持也不同。因此,为了使用不同可选项,在实际开发中需要对采用的操作系统有一定了解。下面选取上表1中的一部分可选项(主要是不受操作系统差异影响的)进行详细讲解。

1.2 MSG_OOB:发送紧急消息

        MSG_OOB 可选项用于发送 “带外数据” 紧急消息。假设医院里有很多病人在等待看病,此时若有急诊患者该怎么办?

当然应该优先处理。

        如果急诊患者较多,需要得到等待看病的普通病人的谅解。正因如此,医院一般会设立单独的急诊室。需紧急处理时,应采用不同的处理方法和通道。同样的道理,在网络编程中,MSG_OOB 可选项就是用于创建特殊发送方法和通道以发送紧急消息。

编程实例:下面示例将通过 MSG_OOB 可选项收发紧急数据。

  • oob_send.c
#include 
#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE  256
void error_handling(char *message);

int main(int argc, char *argv[])
{
    int sockfd;
    struct sockaddr_in recv_adr;

    if(argc != 3){
        printf("Usage: %s  \n", argv[0]);
        exit(1);
    }
    
    sockfd = socket(PF_INET, SOCK_STREAM, 0);
    memset(&recv_adr, 0, sizeof(recv_adr));
    recv_adr.sin_family = AF_INET;
    recv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    recv_adr.sin_port = htons(atoi(argv[2]));
    
    if((connect(sockfd, (struct sockaddr*)&recv_adr, sizeof(recv_adr))) == -1){
        error_handling("connect() error!");
        exit(-1);
    }
    
    write(sockfd, "123", strlen("123"));            //发送普通数据
    send(sockfd, "4", strlen("4"), MSG_OOB);        //发送紧急数据
    write(sockfd, "567", strlen("567"));
    send(sockfd, "890", strlen("890"), MSG_OOB);    //发送紧急数据
    
    close(sockfd);
    return 0;
}

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

 【代码说明

  • 第32~35行:传输数据。第33行和第35行传输紧急数据。正常顺序应该是:123、4、567、890,但紧急传输了 4 和 890,由此可知接收顺序也将改变。

紧急消息的发送比起下面要介绍的接收过程要简单,只需在调用 send 函数时指定 MSG_OOB 可选项。接收紧急消息的过程要相对复杂一些。

  • oob_recv.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE  100
void error_handling(char *message);
void urg_handler(int sig);

//声明全局变量
int acpt_sock;
int recv_sock;

int main(int argc, char *argv[])
{
    struct sockaddr_in recv_adr, send_adr;
    int rcv_len;
    socklen_t send_adr_sz;
    struct sigaction act;      //声明sigaction结构体变量,用于信号处理
    char buf[BUF_SIZE];

    if(argc != 2){
        printf("Usage: %s \n", argv[0]);
        exit(1);
    }

    //act变量初始化
    act.sa_handler = urg_handler;  //添加信号 SIGURG 的信号处理函数
    sigemptyset(&act.sa_mask);
    act.sa_flags |= SA_RESTART;

    //recv_adr变量初始化
    acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&recv_adr, 0, sizeof(recv_adr));
    recv_adr.sin_family = AF_INET;
    recv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    recv_adr.sin_port = htons(atoi(argv[1]));

    if(bind(acpt_sock, (struct sockaddr*)&recv_adr, sizeof(recv_adr)) == -1)
        error_handling("bind() error!");

    if(listen(acpt_sock, 5) == -1)
        error_handling("listen() error!");

    send_adr_sz = sizeof(send_adr);
    if((recv_sock = accept(acpt_sock, (struct sockaddr*)&send_adr, &send_adr_sz)) == -1)
        error_handling("accept() error!");
    printf("recv new client[%s:%d] conn success...conn_fd=%d\n",
            inet_ntoa(send_adr.sin_addr), ntohs(send_adr.sin_port), recv_sock);

    fcntl(recv_sock, F_SETOWN, getpid());   //fcntl函数用于控制文件描述符
    sigaction(SIGURG, &act, NULL);          //注册SIGURG信号

    while((rcv_len = recv(recv_sock, buf, BUF_SIZE-1, 0)) != 0)  //接收正常数据
    {
        if(rcv_len == -1)
            continue;
        buf[rcv_len] = '\0';
        printf("msg_len=%d, recv normal message: %s\n", rcv_len, buf);
    }
    close(recv_sock);
    close(acpt_sock);
    return 0;
}

void urg_handler(int signo)
{
    int rcv_len;
    char buf[BUF_SIZE];
    rcv_len = recv(recv_sock, buf, BUF_SIZE-1, MSG_OOB);  //接收紧急数据
    buf[rcv_len] = '\0';
    printf("msg_len=%d, recv urgent message: %s\n", rcv_len, buf);
}

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

代码说明

  • 第34、58行:该示例中需要重点观察 SIGURG 信号相关部分代码。当接收端(recv)收到 MSG_OOB 紧急消息时,操作系统将产生一个SIGURG 信号,并调用已注册的信号处理函数 urg_handler。另外需要注意的是,第72行的信号处理函数内部调用了接收紧急消息的 recv 函数,在 recv 函数中设置了 MSG_OOB 可选项。
  • 第57行:调用 fcntl 函数,关于此函数将单独说明。
  • fcntl() —— 用来控制文件描述符的系统调用函数。
#include 
#include 

int fcntl(int fd, int cmd, ... /* arg */ );  //这是一个可变参函数

/*参数说明
fd: 文件描述符
cmd: 操作命令
arg: 供命令使用的参数,这部分参数的个数是可变的
*/

//返回值: 成功时返回 cmd 参数相关值,失败时返回-1,并设置errno

【关于 fcntl() 函数参考链接】

Linux系统调用--fcntl函数详解

fcntl(recv_sock, F_SETOWN, getpid());

fcntl() 函数用于控制文件描述符,但上述执行语句的含义是:“将文件描述符 recv_sock 指向的套接字拥有者(F_SETOWN)改成把 getpid() 函数返回值作为进程ID的进程。

关于套接字资源的说明

套接字资源是由操作系统实际创建和拥有,并不属于进程资源,指向套接字资源的文件描述符才属于进程资源。所以从严格意义上说,“套接字拥有者” 是操作系统。只是此处所谓的 “拥有者” 是指负责套接字所有事务的主体—进程。上述描述可简要概括如下:

文件描述符 recv_sock 指向的套接字引发的 SIGURG 信号处理进程变为将 getpid() 函数返回值作为进程ID的进程。

        我们知道,多个进程可以共同拥有一个套接字的文件描述符。例如,通过调用 fork 函数创建子进程并同时复制文件描述符。此时如果产生 SIGURG 信号,应该调用哪个进程的信号处理函数呢?可以肯定的是,不会调用所有进程的信号处理函数(这可能会引发更多问题)。因此,处理 SIGURG 信号时必须指定处理信号的进程主体,而 getpid() 函数返回调用此函数的进程ID。因此,上述执行语句指定当前进程为处理 SIGURG 信号的主体。

  • 运行结果
  • 先运行接收端程序(服务器端):oob_recv.c

$ gcc oob_recv.c -o recv

$ ./recv 9190
recv new client[127.0.0.1:42194] conn success...conn_fd=4
msg_len=3, recv normal message: 123
msg_len=1, recv urgent message: 4
msg_len=3, recv normal message: 567
msg_len=1, recv urgent message: 0
msg_len=2, recv normal message: 89

  • 接着运行发送端程序(客户端):oob_send.c

$ gcc oob_send.c -o send
$ ./send 127.0.0.1 9190

        由输出结果可知,“通过 MSG_OOB 可选项传递数据时只返回1个字节?而且也不会很快啊!

        的确!通过 MSG_OOB 可选项传递数据时不会加快数据传输速度,而且通过信号处理函数 urg_handler 读取数据时也只能读 1 个字节。剩余数据只能通过未设置 MSG_OOB 可选项的 recv 函数读取。这是因为 TCP 不存在真正意义上的 “带外数据”。实际上,MSG_OOB 中的 OOB是指 Out-of-band,而 “带外数据” 的含义是:“通过完全不同的通信路径传输的数据。” 即真正意义上的 Out-of-band 需要通过单独的通信路径高速传输数据,但 TCP 不另外提供,只利用 TCP 的紧急模式(Urgent mode)进行传输。

1.3 TCP紧急模式工作原理

        在 send() 函数中设置 MSG_OOB 可选项,可以带来如下效果:

嗨!这里有数据需要紧急处理紧急,别磨蹭啦!

        MSG_OOB 的真正意义在于督促数据接收对象尽快处理数据。这是紧急模式的全部内容,而且TCP “保持按序传输” 的传输特性依然成立。

        这确实是紧急消息。因为发送者是在督促数据处理的情况下传输数据的。就如同急诊患者的及时得到救治需要如下两个条件一样。

  • 迅速入院。
  • 医院急救。

        无法快速把患者送到医院,并不意味着不需要医院进行急救准备。TCP的紧急消息无法保证及时入院,但可以要求急救准备。当然,急救措施应由程序员完成。上面的示例 oob_recv.c 的运行过程中也接收了紧急消息,这可以通过 SIGURG 信号处理函数确认。

        下面给出设置 MSG_OOB 可选项状态下的数据传输过程。如下图 1 所示。

Linux网络编程 - 多种 I/O 函数(send、recv、readv、writev)_第1张图片 图1  紧急消息传输阶段的发送缓冲

         上图 1 给出的是示例 oob_send.c 的第33行中调用如下函数后的发送缓冲状态。此处假设已传输之前的数据。

send(sockfd, "890", strlen("890"), MSG_OOB);  //发送紧急数据

        如果将缓冲最左端的位置视作偏移量为0,则字符 '0' 保存在偏移量为2的位置。另外,字符 '0' 右侧偏移量为3 的位置存有紧急指针(Urgent Pointer)。紧急指针指向紧急消息(OOB)的下一个位置(偏移量加1),同时向对方主机传递如下信息:

紧急指针指向的偏移量为3之前的部分就是紧急消息。”  即把字符 '0' 成了紧急消息,因为只用1个字节数据来表示紧急消息。

        也就是说,实际只用1个字节表示紧急消息信息。这一点可以通过下图2 中用于传输数据的TCP报文段的结构看得更清楚,如下图2所示。

Linux网络编程 - 多种 I/O 函数(send、recv、readv、writev)_第2张图片 图2  设置URG的TCP报文段

         TCP报文段实际包含更多信息,但上图2只标注了与我们的主题相关的内容。TCP报文段头部结构中含有如下两种信息:

  • URG=1:表示该TCP报文段载有紧急信息。
  • URG指针:紧急指针位于偏移量为3的位置。

这两个信息都是在 TCP 头部结构对应的字段中设置的,分别是:表示标志位的URG(占1位)、紧急指针(占16位)。

具体内容可以去了解一下 TCP报文段的头部结构。

TCP报文段首部结构

        指定 MSG_OOB 可选项的数据包(TCP报文段)本身就是紧急数据包,并通过紧急指针表示紧急消息所在位置。但通过上图2 无法得知如下事实:

紧急消息是字符串890,还是90?如若不是,是否为单个字符0呢?

        但这并不重要。如前所述,除紧急指针的前面1个字节外,数据接收方将通过调用接收函数(read、recv)读取剩余部分。换言之,紧急消息的意义在于督促接收方尽快处理消息,而非督促发送方紧急发送消息

  •  关于 MSG_OOB 的进一步说明

MSG_OOB 是已过时的可选项,在实际网络编程中尽量不要使用它。

  • MSG_OOB 相关参考链接

UNIX网络编程——带外数据

如何处理 TCP 紧急数据(MSG_OOB)?

【知识拓展 计算机领域的偏移量(offset)

偏移量的含义是:“参照基准位置表示相对位置的量。

为了理解这一点,请看下图3所示,图中标有实际地址和偏移地址。

Linux网络编程 - 多种 I/O 函数(send、recv、readv、writev)_第3张图片 图3  偏移地址

上图3 给出了以实际地址 3 为基址计算偏移地址的过程。可以看到,偏移量表示距离基准点向哪个方向偏移多长的距离。因此,与普通地址不同,偏移地址每次从 0 开始。

1.4 MSG_PEEK | MSG_DONTWAIT:检查接收缓冲

        同时设置 MSG_PEEK 和 MSG_DONTWAIT 可选项,以验证接收缓冲中是否存在接收的数据设置 MSG_PEEK 选项并调用 recv 函数时,即使读取了套接字接收缓冲中的数据也不会从接收缓冲中删除。因此,该选项通常与 MSG_DONTWAIT 选项一起使用,用于调用以非阻塞方式验证待读数据存在与否的函数。

  • MSG_PEEK:检测套接字接收缓冲区中是否有数据。只支持 recv 函数。
  • MSG_DONTWAIT:调用 I/O 函数时不阻塞,用于非阻塞 I/O。

编程实例:下面通过示例了解二者的使用方法。

  • peek_send.c
#include 
#include 
#include 
#include 
#include 
#include 

void error_handling(char *message);

int main(int argc, char *argv[])
{
    int sockfd;
    struct sockaddr_in recv_adr;

    if(argc != 3){
        printf("Usage: %s  \n", argv[0]);
        exit(1);
    }
    
    sockfd = socket(PF_INET, SOCK_STREAM, 0);
    memset(&recv_adr, 0, sizeof(recv_adr));
    recv_adr.sin_family = AF_INET;
    recv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    recv_adr.sin_port = htons(atoi(argv[2]));
    
    if((connect(sockfd, (struct sockaddr*)&recv_adr, sizeof(recv_adr))) == -1)
        error_handling("connect() error!");
    
    send(sockfd, "123", strlen("123"), 0);   //发送数据
    
    close(sockfd);
    return 0;
}

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

下面示例给出了使用 MSG_PEEK 和 MSG_DONTWAIT 选项的结果。

  • peek_recv.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUG_SIZE 100
void error_handling(char *message);

int main(int argc, char *argv[])
{
    int acpt_sock, recv_sock;
    struct sockaddr_in acpt_adr, recv_adr;
    int msg_len, state;
    socklen_t recv_adr_sz;
    char buf[BUG_SIZE];
    
    if(argc != 2){
        printf("Usage: %s \n", argv[0]);
        exit(1);
    }
    
    acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&acpt_adr, 0, sizeof(acpt_adr));
    acpt_adr.sin_family = AF_INET;
    acpt_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    acpt_adr.sin_port = htons(atoi(argv[1]));
    
    if(bind(acpt_sock, (struct sockaddr*)&acpt_adr, sizeof(acpt_adr)) == -1)
        error_handling("bind() error!");
    if(listen(acpt_sock, 5) == -1)
        error_handling("listen() error!");
    
    recv_adr_sz = sizeof(recv_adr);
    if((recv_sock=accept(acpt_sock, (struct sockaddr*)&recv_adr, &recv_adr_sz)) == -1)
        error_handling("accept() error!");
    printf("recv new client[%s:%d] conn success...conn_fd=%d\n",
            inet_ntoa(recv_adr.sin_addr), ntohs(recv_adr.sin_port), recv_sock);

    while(1)
    {
        msg_len = recv(recv_sock, buf, BUG_SIZE-1, MSG_PEEK|MSG_DONTWAIT);
        if(msg_len > 0)
            break;
    }
    buf[msg_len] = '\0';
    printf("msg_len=%d, recv normal message: %s\n", msg_len, buf);
    
    msg_len = recv(recv_sock, buf, BUG_SIZE-1, 0);
    buf[msg_len] = '\0';
    printf("Read again: %s\n", buf);
    
    close(recv_sock);
    close(acpt_sock);
    return 0;
}

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

代码说明

  • 第45行:调用 recv 函数的同时传递 MSG_PEEK|MSG_DONTWAIT 可选项,这是为了保证即使接收缓冲区中不存在待读数据也不会进入阻塞状态,而是立即返回。
  • 第52行:再次调用 recv 函数。这次并未设置任何可选项,因此,本次读取的数据将从套接字接收缓冲中删除。
  • 运行结果
  • 接收方(服务器端):peek_recv.c

$ gcc peek_send.c -o send
$ ./recv 9190
recv new client[127.0.0.1:55936] conn success...conn_fd=4
msg_len=3, recv normal message: 123
Read again: 123

  • 发送方(客户端):peek_send.c

$ gcc peek_send.c -o send
$ ./send 127.0.0.1 9190

        通过运行结果可以看到,仅发送一次的数据被接收端读取了2次,因为第一次调用 recv 函数时设置了 MSG_PEEK 可选项,所以第一次调用 recv 函数读走套接字接收缓冲中的数据后,并不会从接收缓冲中删除数据,故第二次调用 recv 函数时,仍能读取一次数据。

二 readv & writev 函数

接下来要介绍的 readv & writev 函数有助于提供数据通信效率。先介绍这两个函数的使用方法,再讨论其合理的应用场景。

2.1 使用 readv & writev 函数

        readv & writev 函数的功能可概括为:“对数据进行整合接收及发送的函数。

        也就是说,通过 writev 函数可以将分散保存在多个缓冲中的数据一并发送,通过 readv 函数可以由多个缓冲分别接收。因此,适当使用这两个函数可以减少 I/O 函数的调用次数。下面先介绍 writev 函数。

说明】保存数据的缓冲区和套接字的缓冲区

1、需要注意的是,保存数据的缓冲区是在用户进程地址空间中,而套接字的缓冲区是在操作系统内核地址空间中,这二者是有区别的。

2、writev 函数的作用是:将用户进程空间中多个缓冲中保存的数据先全部复制到套接字的发送缓冲区中(此时数据是保存在内核空间中),然后再将套接字发送缓冲中的数据组装成TCP报文段发送出去,如果数据量比较小,就组装成一个TCP报文段发送出去;如果数据量比较大,就分成多个TCP报文段按序发送出去。

3、readv 函数的作用是:当数据接收端接收到数据发送端传递过来的数据后,是先存放在套接字的接收缓冲中的(此时数据是保存在内核空间中),然后再将套接字接收缓冲中的数据复制到用户进程空间中的多个缓冲中保存,并从套接字接收缓冲中删除掉已读走的数据。

  • writev() — 将多个缓冲中的数据一并发送。
 #include 

ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

/*参数说明
fd: 表示数据传输对象的套接字文件描述符。但该函数并不只限于套接字,因此,可以像read函数一样向其传递文件或标准输出文件描述符。
iov: iovec结构体数组的地址值,结构体iovec中包含待发送数据的位置和大小信息
iovcnt: 向第二个参数传递的数组长度。
*/

//返回值: 成功时返回发送的字节数,失败时返回-1,并设置 errno
  • iovec 结构体的定义如下:
struct iovec
{
    void    *iov_base;    //缓冲首地址
    size_t  iov_len;      //缓冲大小
};

可以看到,结构体 iovec 由保存待发送数据的缓冲(char型数组)地址值和实际发送的数据长度信息构成。先通过下图4 了解该函数的使用方法。

Linux网络编程 - 多种 I/O 函数(send、recv、readv、writev)_第4张图片 图6  writev & iovec

        上图6中 writev 函数的第一个实参值1是标准输出文件描述符的值,因此向控制台(终端)输出数据,ptr 是存有待发送数据信息的 iovec 结构体数组指针。第三个实参值为 2,因此,从 ptr 指向的地址开始,共浏览 2 个 iovec 结构体变量,发送这些指针指向的缓冲数据。接下来观察图6中的 iovec 结构体数组。ptr[0](数组第一个元素)的  iov_base 指针指向以字符 'A' 开头的字符串,同时 iov_len 为3,故发送 “ABC”字符串数据。而 ptr[1](数组的第二个元素)的 iov_base 指向字符 '1',同时 iov_len 为 4,故发送 “1234”。

编程实例:writev 函数的使用方法示例。

  • writev.c
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])
{
    struct iovec vec[2];
    char buf1[] = "ABCDEFG";
    char buf2[] = "1234567";
    int str_len;
    
    //设置第一个待发送数据的保存位置和大小
    vec[0].iov_base = buf1;
    vec[0].iov_len = 3;
    
    //设置第二个待发送数据的保存位置和大小
    vec[1].iov_base = buf2;
    vec[1].iov_len = 4;

    str_len = writev(STDOUT_FILENO, vec, 2);  //STDOUT_FILENO是标准输出的宏名(其值为1)
    puts("");
    printf("write bytes: %d\n", str_len);
    return 0;
}

运行结果

$ gcc writev.c -o writev
$ ./writev
ABC1234
write bytes: 7

  • readv() —— 由多个缓冲分别接收数据。
#include 

ssize_t readv(int fd, const struct iovec *iov, int iovcnt);

/*参数说明
fd: 用于接收数据的文件(套接字)文件描述符。
iov: 包含数据保存位置和大小信息的 iovec 结构图数组的地址值。
iovcnt: 第二个参数中数组的长度。
*/

//返回值: 成功时返回接收到的字节数,失败时返回-1,并设置 errno。

编程实例:readv 函数的使用方法示例。

  • readv.c
#include 
#include 
#include 
#include 

#define BUF_SIZE 100

int main(int argc, char *argv[])
{
    struct iovec vec[2];
    char buf1[BUF_SIZE] ={0};
    char buf2[BUF_SIZE] ={0};
    int str_len;
    
    vec[0].iov_base = buf1;
    vec[0].iov_len = 5;
    vec[1].iov_base = buf2;
    vec[1].iov_len = BUF_SIZE;
    
    str_len = readv(STDIN_FILENO, vec, 2);  //STDIN_FILENO是标准输入的宏名(其值为0)
    printf("Read bytes: %d\n", str_len);
    printf("First message: %s\n", buf1);
    printf("Second message: %s\n", buf2);
    return 0;
}
  • 代码说明
  • 第15、16行:设置第一个数据的保存位置和大小。接收数据的大小指定为5,因此,无论buf1的大小是多少,最多仅能保存5个字节的数据。
  • 第17、18行:vec[0] 中注册的缓冲中保存5个字节数据,剩余数据将保存到 vec[1] 中注册的缓冲。结构体 iovec 的成员 iov_len 中应填入接收的最大字节数。
  • 第20行:readv 函数的第一个参数 STDIN_FILENO 表示的是标准输入文件描述符的宏名(其值为0),因此从标准输入接收数据。
  • 运行结果

$ gcc readv.c -o readv
[wxm@centos7 io]$ ./readv
I like TCP/IP socket programming~
Read bytes: 34
First message: I lik
Second message: e TCP/IP socket programming~

2.2 合理使用 readv & writev 函数

        哪种情况适合使用 readv 和 writev 函数呢?实际上,能使用该函数的所有情况都适用。例如,需要传输的数据分别位于不同缓冲(数组)中,需要多次调用 write 函数。此时可以通过调用一次 writev 函数调用来代替发送操作,当然会提供效率。同样,需要将文件(套接字)接收缓冲中的数据读入不同位置时,可以不必多次调用 read 函数,而是通过一次 readv 函数调用就能大大提高数据读取效率。

        即使即从 C 语言角度来看,减少函数调用次数也能相应提高性能。但其更大的意义在于减少数据包个数。假设为了提高效率而在服务器端明确禁用了 Nagle(纳格)算法。其实 writev 函数在不采用 Nagle 算法时更有价值,如下图 7 所示。

Linux网络编程 - 多种 I/O 函数(send、recv、readv、writev)_第5张图片 图7  Nagle算法关闭状态下的数据传输

         上述示例中待发送的数据分别存放在三个不同的地方,此时如果使用 write 函数则需要3次函数调用。但若为提高速度而关闭了 Nagle 算法,则极有可能需要使用 3 个数据包(TCP报文段)传递数据。反之,若使用 writev 函数将所有数据一次性写入套接字输出(发送)缓冲中,则很有可能仅需要使用一个数据包(即一个TCP报文段)传输数据。所以,writev 函数 和 readv 函数非常有用。

        再考虑一种情况:将不同位置的数据按照发送顺序移动(复制)到一个大数组中,并通过一次 write 函数调用进行传输。这种方式是否与调用 writev 函数效果相同呢?当然!但使用 writev 函数更为方便。因此,如果遇到 writev 和 readv 函数的适用情况,建议优先选择使用这两个函数来进行数据的收发操作

知识补充》Nagle(纳格)算法

Nagle算法是为了解决发送端产生的糊涂窗口综合征的问题而提出的。这个算法是为发送端的TCP用的。

糊涂窗口综合征 与 Nagle算法 博文链接

糊涂窗口综合症和Nagle算法

三 习题

1、下列关于 MSG_OOB 可选项的说法错误的是?

a. MSG_OOB 指传输 Out-of-band 数据,是通过其他路径高速传输数据。

b. MSG_OOB 指通过其他路径高速传输数据,因此,TCP 中设置该选项的数据先到达对方主机。

c. 设置 MSG_OOB 使数据先到达对方主机后,以 普通数据的形式和顺序读取。也就是说,只是提供了传输速度,接收方无法识别这一点。

d. MSG_OOB 无法脱离TCP的默认数据传输方式。即使设置了MSG_OOB,也会保持原有传输顺序。该选项只用于要求接收方紧急处理。

:b、c。分析如下:

  • b:TCP是通过设置紧急模式(Urgent mode)来传输紧急数据的,它不能保证紧急数据优先到达通信对端,MSG_OOB的真正意义是在于督促接收方如果接收到紧急数据就请尽快处理。因此,b中的描述是错误的。
  • c:设置了 MSG_OOB 的数据,在TCP报文段的首部结构中会设置 URG 标记位和 紧急指针的偏移量,通过这两个信息可以识别出这是一个紧急数据包,并且通过紧急指针的偏移量找到紧急数据所在位置。因此,c中的描述是错误的。

2、利用readv&writev函数收发数据有何优点?分别从函数调用次数和I/O缓冲的角度给出说明。

:readv & writev 函数可以将分散保存在多个缓冲中的数据一并接收和发送,是对数据进行整合接收及发送的函数,因此可以进行更有效的数据收发操作。而且,输入输出函数的调用次数也能相应减少,也会相应提高性能。

3、通过recv函数验证输入缓冲是否存在数据时(确认后立即返回时),如何设置recv函数最后一个参数中的可选项?分别说明各可选项的含义。

:同时设置 MSG_PEEK 可选项和 MSG_DONTWAIT 可选项,以验证输入缓冲是否存在可接收的数据,并确认后立即返回。

设置 MSG_PEEK 可选项并调用recv函数时,即使读取了输入(接收)缓冲数据也不会删除。因此,该选项通常与 MSG_DONTWAIT 可选项一起使用,用于调用以非阻塞方式验证待读数据存在与否的 IO 函数。

参考

《TCP-IP网络编程(尹圣雨)》第12章 - 多种 I/O 函数

《TCP/IP网络编程》课后练习答案第一部分11~14章 尹圣雨

你可能感兴趣的:(Linux编程,#,网络编程,#,Linux系统调用,Linux网络编程,socket编程,TCP/IP网络编程,I/O函数,Linux编程)