后台核心编程(十一):网络编程-多种I/O函数

1 send & recv 函数

1.1 Linux 中的 send & recv

⾸先看 send 函数定义:

#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
/*
成功时返回发送的字节数,失败时返回 -1
sockfd: 表⽰与数据传输对象的连接的套接字和⽂件描述符
buf: 保存带传输数据的缓冲地址值
nbytes: 待传输字节数
flags: 传输数据时指定的可选项信息
*/

下⾯是 recv 函数的定义:

#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
/*
成功时返回接收的字节数(收到 EOF 返回 0),失败时返回 -1
sockfd: 表⽰数据接受对象的连接的套接字⽂件描述符
buf: 保存接受数据的缓冲地址值
nbytes: 可接收的最⼤字节数
flags: 接收数据时指定的可选项参数
*/

send 和 recv 函数都是最后⼀个参数是收发数据的可选项,该选项可以⽤位或(bit OR)运算符(| 运算符)同时
传递多个信息。
send & recv 函数的可选项意义:
在这里插入图片描述

1.2 MSG_OOB:发送紧急消息

MSG_OOB 可选项⽤于创建特殊发送⽅法和通道以发送紧急消息。下⾯为 MSG_OOB 的⽰例代码:

  • oob_recv.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>

#define BUF_SIZE 30
void error_handling(char *message);
void urg_handler(int signo);

int acpt_sock;
int recv_sock;

int main(int argc, char *argv[])
{
    struct sockaddr_in recv_adr, serv_adr;
    int str_len, state;
    socklen_t serv_adr_sz;
    struct sigaction act;
    char buf[BUF_SIZE];
    if (argc != 2)
    {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }
    act.sa_handler = urg_handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    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");
    listen(acpt_sock, 5);

    serv_adr_sz = sizeof(serv_adr);
    recv_sock = accept(acpt_sock, (struct sockaddr *)&serv_adr, &serv_adr_sz);
    //将文件描述符 recv_sock 指向的套接字拥有者(F_SETOWN)改为把getpid函数返回值用做id的进程
    fcntl(recv_sock, F_SETOWN, getpid());
    state = sigaction(SIGURG, &act, 0); //SIGURG 是一个信号,当接收到 MSG_OOB 紧急消息时,系统产生SIGURG信号

    while ((str_len = recv(recv_sock, buf, sizeof(buf), 0)) != 0)
    {
        if (str_len == -1)
            continue;
        buf[str_len] = 0;
        puts(buf);
    }
    close(recv_sock);
    close(acpt_sock);
    return 0;
}
void urg_handler(int signo)
{
    int str_len;
    char buf[BUF_SIZE];
    str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_OOB);
    buf[str_len] = 0;
    printf("Urgent message: %s \n", buf);
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
  • oob_send.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

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

int main(int argc, char *argv[])
{
    int sock;
    struct sockaddr_in recv_adr;
    if (argc != 3)
    {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }
    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 = inet_addr(argv[1]);
    recv_adr.sin_port = htons(atoi(argv[2]));

    if (connect(sock, (struct sockaddr *)&recv_adr, sizeof(recv_adr)) == -1)
        error_handling("connect() error");

    write(sock, "123", strlen("123"));
    send(sock, "4", strlen("4"), MSG_OOB);
    write(sock, "567", strlen("567"));
    send(sock, "890", strlen("890"), MSG_OOB);
    close(sock);
    return 0;
}

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

运⾏结果:

在这里插入图片描述
从运⾏结果可以看出,send 是客⼾端,recv 是服务端,客⼾端给服务端发送消息,服务端接收完消息之后显⽰出来。

代码中关于:

fcntl(recv_sock, F_1 SETOWN, getpid());

⽂件描述符 recv_sock 指向的套接字引发的 SIGURG 信号处理进程变为 getpid 函数返回值⽤作 ID 进程.

上述描述中的「处理 SIGURG 信号」指的是「调⽤ SIGURG 信号处理函数」。但是之前讲过,多个进程可以拥有 1个套接字的⽂件描述符。例如,通过调⽤ fork 函数创建⼦进程并同时复制⽂件描述符。此时如果发⽣ SIGURG 信号,应该调⽤哪个进程的信号处理函数呢?可以肯定的是,不会调⽤所有进程的信号处理函数。因此,处理SIGURG 信号时必须指定处理信号所⽤的进程,而 getpid 返回的是调⽤此函数的进程 ID 。上述调⽤语句指当前为处理 SIGURG 信号的主体。

通过 MSG_OOB 可选项传递数据时只返回 1 个字节,而且也不快

的确,通过 MSG_OOB 并不会加快传输速度,而通过信号处理函数 urg_handler 也只能读取⼀个字节。剩余数据只能通过未设置 MSG_OOB 可选项的普通输⼊函数读取。因为 TCP 不存在真正意义上的「外带数据」。实际上,MSG_OOB 中的 OOB 指的是 Out-of-band ,而「外带数据」的含义是:

通过去完全不同的通信路径传输的数据

即真正意义上的 Out-of-band 需要通过单独的通信路径⾼速传输数据,但是 TCP 不另外提供,只利⽤ TCP 的紧急模式(Urgent mode)进⾏传输。

1.3 紧急模式⼯作原理

MSG_OOB 的真正意义在于督促数据接收对象尽快处理数据。这是紧急模式的全部内容,而 TCP 「保持传输顺序」的传输特性依然成⽴。TCP 的紧急消息⽆法保证及时到达,但是可以要求急救。下⾯是 MSG_OOB 可选项状态下的数据传输过程,如图:

上面是:
send(sock, "890", strlen("890"), MSG_OOB);

图上是调⽤这个函数的缓冲状态。如果缓冲最左端的位置视作偏移量 0 。字符 0 保存于偏移量 2 的位置。另外,字符 0 右侧偏移量为 3 的位置存有紧急指针(Urgent Pointer)。紧急指针指向紧急消息的下⼀个位置(偏移量加⼀),同时向对⽅主机传递⼀下信息:

紧急指针指向的偏移量为 3 之前的部分就是紧急消息。

也就是说,实际上只⽤了⼀个字节表⽰紧急消息。这⼀点可以通过图中⽤于传输数据的 TCP 数据包(段)的结构看得更清楚,如图:

TCP 数据包实际包含更多信息。TCP 头部包含如下两种信息:

  • URG=1:载有紧急消息的数据包
  • URG指针:紧急指针位于偏移量为 3 的位置。

指定 MSG_OOB 选项的数据包本⾝就是紧急数据包,并通过紧急指针表⽰紧急消息所在的位置。
紧急消息的意义在于督促消息处理,而⾮紧急传输形式受限的信息。

1.4 检查输⼊缓冲

同时设置 MSG_PEEK 选项和 MSG_DONTWAIT 选项,以验证输⼊缓冲是否存在接收的数据。设置 MSG_PEEK 选项并调⽤ recv 函数时,即使读取了输⼊缓冲的数据也不会删除。因此,该选项通常与 MSG_DONTWAIT 合作,⽤于调⽤以⾮阻塞⽅式验证待读数据存与否的函数。下⾯的⽰例是⼆者的含义:

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

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

int main(int argc, char *argv[])
{
    int acpt_sock, recv_sock;
    struct sockaddr_in acpt_adr, recv_adr;
    int str_len, state;
    socklen_t recv_adr_sz;
    char buf[BUF_SIZE];
    if (argc != 2)
    {
        printf("Usage : %s <port>\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");
    listen(acpt_sock, 5);

    recv_adr_sz = sizeof(recv_adr);
    recv_sock = accept(acpt_sock, (struct sockaddr *)&recv_adr, &recv_adr_sz);

    while (1)
    {
        //保证就算不存在待读取数据也不会阻塞
        str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_PEEK | MSG_DONTWAIT);
        if (str_len > 0)
            break;
    }

    buf[str_len] = 0;
    printf("Buffering %d bytes : %s \n", str_len, buf);
    //再次调用 recv 函数,这一次没有设置任何可选项,所以可以直接从缓冲区读出
    str_len = recv(recv_sock, buf, sizeof(buf) - 1, 0);
    buf[str_len] = 0;
    printf("Read again: %s \n", buf);
    close(acpt_sock);
    close(recv_sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
  • peek_send.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
void error_handling(char *message);
 
int main(int argc, char *argv[])
{
    int sock;
    struct sockaddr_in send_adr;
    if (argc != 3) {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }
 
    sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&send_adr, 0, sizeof(send_adr));
    send_adr.sin_family = AF_INET;
    send_adr.sin_addr.s_addr = inet_addr(argv[1]);
    send_adr.sin_port = htons(atoi(argv[2]));
 
    if (connect(sock, (struct sockaddr *)&send_adr, sizeof(send_adr)) == -1)
        error_handling("connect() error!");
 
    write(sock, "123", strlen("123"));
    close(sock);
    return 0;
}
 
void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

通过运行结果可以验证,仅发送一次的数据被读取两次,因为第一次调用recv函数时设置了MSG_PEEK选项,以上就是MSG_PEEK可选项的功能

  • readv和writev函数
    本节介绍的readv和writev函数有助于提高数据通信效率,先介绍这些函数的使用方法,再讨论其合理的应用场景。readv和writev函数的功能可概括为:对数据进行整合传输及发送的函数。也就是说,通过writev函数可以将分散保存在多个缓冲的数据一并发送,通过readv函数可以由多个缓冲分别接收。因此,适当使用这两个函数可以减少I/O函数的调用次数:
#include <sys/uio.h>
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);//成功时返回发送的字节数,失败时返回-1
  • filedes:表示数据传输对象的套接字文件描述符,但该函数并不只限于套接字,因此,可以像read函数一样向其传递文件或标准输出描述符
  • iov:iovec结构体数组的地址值,结构体iovec中包含待发送数据的位置和大小信息
  • iovcnt:向第二个参数传递的数组长度

上述函数的第二个参数中出现的数组iovec结构体的声明如下:

struct iovec
{
    void      *iov_base;    //缓冲地址
    size_t    iov_len;      //缓冲大小
};

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

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

接下来给出关于writev函数的调用示例

  • writev.c
#include <stdio.h>
#include <sys/uio.h>
 
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(1, vec, 2);
    puts("");
    printf("Write bytes: %d \n", str_len);
    return 0;
}

编译writev.c并运行

# gcc writev.c -o writev
# ./writev
ABC1234
Write bytes: 7

下面介绍readv函数,它与writev函数正好相反:

#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);//成功时返回接收的字节数,失败时返回-1
  • filedes:传递接收数据的文件(或套接字)描述符
  • iov:包含数据保存位置和大小信息的iovec结构体数组的地址值
  • iovcnt:第二个参数中数组的长度
#include <stdio.h>
#include <sys/uio.h>
#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(0, vec, 2);
    printf("Read bytes: %d \n", str_len);
    printf("First message: %s \n", buf1);
    printf("Second message: %s \n", buf2);
    return 0;
}

编译readv.c并运行

# gcc readv.c -o readv
# ./readv
I like TCP/IP socket programming
Read bytes: 33
First message: I lik
Second message: e TCP/IP socket programming

合理使用readv和writev函数:

哪种情况适合使用readv和writev函数?实际上,能使用该函数的所有情况都适用。例如:需要传输的数据分别位于不同缓冲(数组)时,需要多次调用write函数,此时可以通过一次writev函数调用来提高效率。同样,需要将输入缓冲中的数据读入不同位置时,可以不必多次调用read函数,而是利用一次readv函数就能大大提高效率

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值