套接字中read/write和send/recv函数

参考:《UNIX 网络编程 · 卷1 : 套接字联网API》

write 和 read 函数

一旦,我们建立好了 TCP 连接之后,我们就可以把得到的 fd 当作文件描述符来使用。由此网络程序里最基本的函数就是 read 和 write 函数了。其定义如下:

#include 
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

write 函数将 buf 中的 count 字节内容写入文件描述符 fd。成功时返回写的字节数。失败时返回 -1。并设置 errno 变量。

在网络程序中,当我们向套接字文件描述符写时有两可能:

  1. write 的返回值大于 0,表示写了部分或者是全部的数据。这样我们用一个 while 循环来不停的写入,但是循环过程中的 buf 参数和 nbyte 参数得由我们来更新。也就是说,网络写函数是不负责将全部数据写完之后在返回的。
  2. 返回的值小于 0,此时出现了错误。我们要根据错误类型来处理。如果错误为 EINTR 表示在写的时候出现了中断错误。如果为 EPIPE 表示网络连接出现了问题(对方已经关闭了连接)。

为了处理以上的情况,我们自己编写一个写函数来处理这几种情况。

int my_write(int fd, void *buffer, int length)
{
    int bytes_left;
    int written_bytes;
    char *ptr;
    ptr = buffer;
    bytes_left = length;
    while (bytes_left > 0)
    {
        written_bytes = write(fd, ptr, bytes_left);
        if (written_bytes <= 0)
        {
            if (errno == EINTR)
                written_bytes = 0;
            else
                return (-1);
        }
        bytes_left -= written_bytes;
        ptr += written_bytes;
    }
    return (0);
}

read 函数是负责从 fd 中读取内容。当读成功时,read 返回实际所读的字节数。

如果返回的值是 0 表示已经读到文件的结束了;小于 0 表示出现了错误;如果错误为 EINTR 说明读是由中断引起的;如果是 ECONNREST 表示网络连接出了问题。

和上面一样,也写一个自己的读函数。

int my_read(int fd, void *buffer, int length)
{
    int bytes_left;
    int bytes_read;
    char *ptr;

    bytes_left = length;
    while (bytes_left > 0)
    {
        bytes_read = read(fd, ptr, bytes_left);
        if (bytes_read < 0)
        {
            if (errno == EINTR)
                bytes_read = 0;
            else
                return (-1);
        }
        else if (bytes_read == 0)
            break;
        bytes_left -= bytes_read;
        ptr += bytes_read;
    }
    return (length - bytes_left);
}

send 和 recv 函数

recv 和 send 函数提供了和 read 和 write 差不多的功能。不过它们提供了第四个参数来控制读写操作。recv 和 send 函数需要一个额外的参数,其定义如下:

#include 
#include 
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

前面的三个参数和 read、write 一样,第四个参数可以是 0 或者是以下的组合:

参数 含义
MSG_PEEK 查看数据,并不从系统缓冲区移走数据
MSG_OOB 接受或者发送带外数据
MSG_WAITALL 等待所有数据
MSG_DONTWAIT 仅本操作非阻塞
MSG_DONTROUTE 不查找表

MSG_DONTROUTE:是 send 函数使用的标志。这个标志告诉 IP 目的主机在本地网络上面,没有必要查找表,这个标志一般用网络诊断和路由程序里面。

MSG_OOB:表示可以接收和发送带外的数据。关于带外数据我们以后会解释的。

MSG_PEEK:是 recv 函数的使用标志,表示只是从系统缓冲区中读取内容,而不清除系统缓冲区的内容。这样下次读的时候,仍然是一样的内容。一般在有多个进程读写数据时可以使用这个标志。

MSG_WAITALL:是 recv 函数的使用标志,表示等到所有的信息到达时才返回。使用这个标志的时候 recv 会一直阻塞,直到指定的条件满足,或者是发生了错误:

  1. 当读到了指定的字节时,函数正常返回。返回值等于 len

  2. 当读到了文件的结尾时,函数正常返回。返回值小于 len

  3. 当操作发生错误时,返回 -1,且设置错误为相应的错误号 (errno)

MSG_DONTWAIT:在无需打开相应套接字的非阻塞标志下,把单个 IO 操作临时指定为非阻塞,接着执行 I/O 操作,然后关闭非阻塞标志。

flags 参数在设计上存在一个基本问题:它是按值传递的,而不是一个值-结果参数。因此它只能用于从进程向内核传递标志。内核无法向进程传回。但对于 TCP/IP 协议不需要从测内核向进程传回标志。

但是之后的标准却指出了随输入操作向进程返送 MSG_EOR 标志的需求。其做出的决定是保持常用的输入函数 recv 和 recvfrom 参数不变,而改变 recvmsg 和 sendmsg 所用的 msghdr 结构,该结构按引用传递,内核返回时会修改其中的 msg_flags 成员标志,如果一个进程需要内核更新标志,就需要调用 recvmsg,而不是调用 recv 或 recvfrom。

套接字读缓冲区的数据量

有一种情况就是我们不想读取数据,但想知道一个套接字缓冲区中有多少数据可以读取。有以下单个方法可以用于获得排队的数据量:

  1. 如果知道已排队数据量的目的在于避免读操作阻塞在内核中,那么可以使用非阻塞IO。
  2. 如果即想查看数据,又想数据仍然留在接收队列中以供本进程其他部分稍后读取,可以使用 MSG_PEEK 标志。如果我们这么做,然而不能肯定是否真有数据可读,那么可以结合非阻塞套接字使用该标志。对一个流式(TCP)套接字而言,指定 MSG_PEEK 两次调用 recv,返回值可能不一样,因为这两次调用之间 TCP 可能又收到了一些数据。对于一个 UDP 套接字假设接收缓冲区中已有数据报,指定 MSG_PEEK 调用 recvfrom 一次,稍后不指定该标志再调用 recvfrom 一次,那么即使另有数据报在这两次调用之间加入该套接字的接收缓冲区,这两个调用的返回值完全相同。
  3. 一些实现支持 ioctl 的 FIONREAD 命令。该命令的第三个 ioctl 参数是指向某个整数的一个指针,内核通过该整数返回的值就是套接字接收缓冲区的当前字节数。该值是已排队字节的总和,对于 UDP 套接字而言包括所有已排队的数据报。

你可能感兴趣的:(Linux环境编程)