0%

Socket(二)recv 与 send 函数

引言

不论客户端还是服务端,都用 send 函数向对端发送数据。对于客户端来说,用 send 发送请求,对于服务端来说,用 send 发送响应。同时,她们都用 recv 函数从对端接收数据。

1. 函数声明

1
2
3
4
5
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);

ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);

1.1 参数

  • sockfd

    对端的套接字描述符,用来从该描述符接收数据或者将数据发往该描述符。

  • buff

    用来存放数据的缓冲区,由于 send 函数是将缓冲区中的数据发往对端,所以 buff 被修饰为 const。

  • nbytes

    要发送数据的字节长度。

  • flags

    该值为 0 或者为以下表中的一个或者多个常值的逻辑或。

    flags 说明 recv send
    MSG_DONTROUTE 绕过路由表查找
    MSG_DONTWAIT 仅本操作非阻塞
    MSG_OOB 发送或接收带外数据
    MSG_PEEK 窥看外来数据
    MSG_WAITALL 等待所有消息

    MSG_DONTROUTE
    本标志告知内核目的主机在某个直接连接的本地网络上,因而无需执行路由表查找。

    MSG_DONTWAIT
    本标志在无需打开相应套接字的非阻塞标志的前提下,把单个 I/O 操作临时指定为非阻塞,接着执行 I/O 操作,然后关闭非阻塞标志。该标志是随 Net/3 新增设的,可能并非所有系统都支持它。

    MSG_OOB
    对于 send,本标志志明即将发送带外数据。对于 recv,本标志指明即将读入的是带外数据而不是普通数据。

    MSG_PEEK
    本标志适用于 recv 和 recvfrom,它允许我们查看已可读取的数据,而且系统不在 recv 或 recvfrom 返回后丢弃这些数据。

    MSG_WAITALL
    本标志随 4.3 BSD Reno 引入。它告知内核不要在尚未读入请求数据的字节之前让一个读操作返回。如果系统支持本标志,我们就可以省略掉 readn 函数,而替之以如下宏:

    1
    #define readn(fd, ptr, n) recv(fd, ptr, n, MSG_WAITALL)

    即使指定了 MSG_WAITALL,如果发生下列情况之一:

    • 捕获一个信号
    • 连接被终止
    • 套接字发绳一个错误

    相应的读函数仍有可能返回比请求字节数要少的数据

📚 Tips

另有一些标志适用于 TCP/IP 以外的协议族。举例来说,OSI 的传输层是基于记录的(不像 TCP 那样基于一个字节流),其输出操作支持 MSG_EOR 标志,指示逻辑记录的结束。

flags 参数在设计上存在一个基本问题:它是按值传递的,而不是一个值-结果参数。因此它只能用于从进程向内核传递标志。内核无法向进程传回标志。对于 TCP/IP 协议这一点不成问题,因为 TCP/IP 几乎不需要从内核向进程传回标志。然而随着 OSI 协议被加到 4.3 BSD Reno 中,却提出了随输入操作向进程返送 MSG_EOR 标志的需求。4.3 BSD Reno 做出的决定是保持常用输入函数(recv 和 recvfrom)的参数不变,而改变 recvmsg 和 sendmsg 所用的 msghdr 结构(该结构新增一个整数 msg_flags 成员)。而且,该结构是按引用传递,内核就可以在返回时修改这些标志。这个决定同时意味着如果一个进程需要有内核更新标志,它就必须调用 recvmsg,而不是调用 recv 和 recvfrom。

1.2 返回值

  • 成功

    读入或者写出的字节数

  • 失败

    返回 -1

2. 函数执行流程

以下流程均以同步式调用进行说明。

2.1 recv 函数

  1. recv 先等待 sockfd 的发送缓冲中的数据被协议传送完毕,如果协议在传送 sockfd 的发送缓冲中的数据时出现网络错误,那么 recv 函数返回 SOCKET_ERROR。
  2. 如果 sockfd 的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv 先检查套接字 sockfd 的接收缓冲区,如果 sockfd 接收缓冲区中没有数据或者协议正在接收数据,那么 recv 就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv 函数就把 sockfd 的接收缓冲中的数据 copy 到 buff 中(注意协议接收到的数据可能大于 buff 的长度,所以 在这种情况下要调用几次 recv 函数才能把 sockfd 的接收缓冲中的数据 copy 完。recv 函数仅仅是 copy 数据,真正的接收数据是协议来完成的),

📚 Tips

recv 函数返回其实际 copy 的字节数。如果 recv 在 copy 时出错,那么它返回 SOCKET_ERROR;如果 recv 函数在等待协议接收数据时网络中断了,那么它返回 0。在 Unix 系统下,如果 recv 函数在等待协议接收数据时网络断开了,那么调用 recv 的进程会接收到一个 SIGPIPE 信号,进程对该信号的默认处理是进程终止。

2.2 send 函数

  1. send 先比较待发送数据的长度 nbytes 和套接字 sockfd 的发送缓冲的长度, 如果 nbytes 大于 sockfd 的发送缓冲区的长度,该函数返回 SOCKET_ERROR;
  2. 如果 nbytes 小于或者等于 sockfd 的发送缓冲区的长度,那么 send 先检查协议是否正在发送 sockfd 的发送缓冲中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送 sockfd 的发送缓冲中的数据或者 sockfd 的发送缓冲中没有数据,那么 send 就比较 sockfd 的发送缓冲区的剩余空间和 nbytes。
  3. 如果 nbytes 大于剩余空间大小,send 就一直等待协议把 sockfd 的发送缓冲中的数据发送完。
  4. 如果 nbytes 小于剩余空间大小,send 就仅仅把 buff 中的数据 copy 到剩余空间里(注意并不是 send 把 sockfd 的发送缓冲中的数据传到连接的另一端的,而是协议传的,send 仅仅是把 buff 中的数据 copy 到 sockfd 的发送缓冲区的剩余空间里)。

📚 Tips

如果 send 函数 copy 数据成功,就返回实际 copy 的字节数,如果 send 在 copy 数据时出现错误,那么 send 就返回 SOCKET_ERROR;如果 send 在等待协议传送数据时网络断开的话,那么 send 函数也返回 SOCKET_ERROR。要注意 send 函数把 buff 中的数据成功 copy 到 sockfd 的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个 socket 函数就会返回 SOCKET_ERROR。(每一个除 send 外的 socket 函数在执 行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该 socket 函数就返回 SOCKET_ERROR)。在 Unix 系统下,如果 send 在等待协议传送数据时网络断开的话,调用 send 的进程会接收到一个 SIGPIPE 信号,进程对该信号的默认处理是进程终止。通过测试发现,异步 socket 的 send 函数在网络刚刚断开时还能发送返回相应的字节数,同时使用 select 检测也是可写的,但是过几秒钟之后,再 send 就会出错了,返回 -1。select 也不能检测出可写了。


参考