TCP之深入浅出send&recv
接触过网络开发的人,大抵都知道,上层应用使用send函数发送数据,使用recv来接收数据,而send和recv的实现原理又是怎样的呢?
在前面的几篇文章中,我们有提过,TCP是个可靠的、全双工协议。其流量控制或者拥塞控制依赖于滑动窗口和拥塞窗口的滑动来实现,而这两个窗口的滑动实现则是依赖于TCP中的两个buffer,这两个buffer则是TCP socket在内核中的发送缓冲区(send buffer)和接收缓冲区(recv buffer)。
在本文中,我们首先会简单介绍下TCP中发送缓冲区和接收缓冲区的作用(对于后面理解send和recv非常重要),然后讲解Linux系统下,TCP发送和接收数据是如何实现的。
缓冲区
缓冲区,可以理解为是一个临时缓存。
对于发送端来说,socket将数据拷贝到发送临时缓冲区,就立即返回到应用层去做其他的事情,而剩下的将临时缓冲区的数据通过内核发送到对端,这就是tcp的事。
对于接收端来说,内核将网络中的数据拷贝到缓冲区,等待上层应用读取。
发送缓冲区
上面有讲,进程在调用send()发送的数据的时候,最简单情况(也是一般情况), 将数据拷贝进入socket的内核发送缓冲区之中,然后send便会立即返回。
换句话说,在应用层调用send()返回之时,数据不一定会发送到对端去(和write写文件有点类似),send()仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中。
TCP socket有两种模式,即阻塞模式和非阻塞模式。
- 在阻塞模式下, send函数的过程是将应用程序请求发送的数据拷贝到发送缓存中发送并得到确认后再返回.但由于发送缓存的存在,表现为:如果发送缓存大小比请求发送的大小要大,那么send函数立即返回,同时向网络中发送数据;否则,send向网络发送缓存中不能容纳的那部分数据,并等待对端确认后再返回(接收端只要将数据收到接收缓存中,就会确认,并不一定要等待应用程序调用recv)
- 在非阻塞模式下,send函数的过程仅仅是将数据拷贝到协议栈的缓存区而已,如果缓存区可用空间不够,则尽能力的拷贝,返回成功拷贝的大小;如缓存区可用空间为0,则返回-1,同时设置errno为EAGAIN.
在Linux内核中,有两种方式可以查看tcp缓冲区buffer大小。
1、通过查看/etc/sysctl.ronf下的net.ipv4.tcp_wmem值
2、 通过命令'cat /proc/sys/net/ipv4/tcp_wmem'
cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4194304
从上面可以看出,在笔者所在的 服务器 上,tcp send缓冲区buffer有3个值,分别是4096 16384 4194304。
- 第一个值是socket的发送缓存区分配的最少字节数,
- 第二个值是默认值(该值会被net.core.wmem_default覆盖),缓存区在系统负载不重的情况下可以增长到这个值
- 第三个值是发送缓存区空间的最大字节数(该值会被net.core.wmem_max覆盖)
我们可以通过程序,来修改当前tcp socket的发送缓冲区大小,需要注意的是,如下的代码修改,只会修改当前特定的socket。
int buffer_len = 10240;
setsockopt(fd, SOL_SOCKET, SO_SNDBUF, (void*)&buffer_len, buffer_len);
接收缓冲区
接收缓冲区被TCP用来缓存网络上来的数据,一直保存到应用进程读走为止。
对于TCP,如果应用进程一直没有读取,接收缓冲区满了之后,发生的动作是:收端通知发端,接收窗口关闭(win=0)。这个便是滑动窗口的实现。保证TCP套接口接收缓冲区不会溢出,从而保证了TCP是可靠传输。因为对方不允许发出超过所通告窗口大小的数据。这就是TCP的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它。
与查看发送缓冲区大小的方式一样,接收缓冲区也是通过如上的两种方式。1、通过查看/etc/sysctl.ronf下的net.ipv4.tcp_rmem值
2、通过命令'cat /proc/sys/net/ipv4/tcp_rmem'
cat /proc/sys/net/ipv4/tcp_rmem
4096 87380 4194304
TCP接收缓冲区buffer有3个值,分别是4096 87380 4194304。
- 第一个值是socket的接收缓存区的最少字节数,
- 第二个值是默认值(该值会被net.core.rmem_default覆盖),缓存区在系统负载不重的情况下可以增长到这个值
- 第三个值是接收缓存区空间的最大字节数(该值会被net.core.rmem_max覆盖)
同样的,可以通过如下代码,修改接收缓冲区的大小。
int buffer_len = 10240;
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, (void*)&buffer_len, buffer_len);
实现原理
为了便于我们理解TCP的整个传输过程,我们先了解下TCP的四层模型以及四层模型在数据传输中的流向。后面我们将从四层模型的角度来分析send和recv函数在每层中都做了什么。
send原理
NAME
send, sendto, sendmsg - send a message on a socket
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
DESCRIPTION
The system calls send(), sendto(), and sendmsg() are used to transmit a message to another socket.
当调用该函数时,send函数:1、先比较待发送数据的长度len和套接字sockfd的可用发送缓冲区的长度
- 如果数据长度len大于发送缓冲区的长度,则分多次发送
-
如果果len小于或者等于sockfd的缓冲区长度,那么send先检查协议是否正在发送sockfd的发送缓冲中的数据
- 如果len大于剩余空间大小,send就一直等待协议把s的发送缓冲中的数据发送完
- 如果len小于剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里。如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。需要注意send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个socket函数就会返回SOCKET_ERROR。
- 如果是就等待协议把数据发送完
- 否则,如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么send就比较sockfd的发送缓冲区的剩余空间和len
如果对具体实现不是很感兴趣,可直接此部分
从四层模型的角度来分析send实现。
应用层
对于TCP,应用程序在创建socket之后,调用connect()函数,通过socket使客户端和服务端建立连接。然后就可以调用send函数发送数据。
传输层
数据在传输层进行处理,以TCP协议为例,其主要有以下功能:
- 1、构造TCP段
- 2、计算校验和
- 3、发送回复(ACK)包
- 4、滑动窗口(sliding windown)等操作保证可靠性。
不同的协议有不同的发送函数,TCP调用tcp_sendmsg函数,而UDP则调用的是sock_sendmsg函数。
tcp_sendmsg()的主要工作是传输用户层的数据,将数据放入skb中。然后调用tcp_push()发送,tcp_push函数调用tcp_write_xmit() 函数,依次调用发送函数tcp_transmit_skb将skb封装tcp头之后,回调ip_queue_xmit。
网络层
ip_queue_xmit(skb)主要有路由查找校验、封装ip头和ip选项,最后通过ip_local_out发送数据包。
数据链路层
数据链路层在不可靠的物理介质上提供可靠的传输。该层的功能包括:物理地址寻址、数据成帧、流量控制、数据错误检测、重发等。这一层的数据单位称为帧(frame)。
上图为send函数源码的调用逻辑图,对源码有兴趣的话,可以在net/tcp.c找到对应的实现。
recv原理
NAME
recv, recvfrom, recvmsg - receive a message from a socket
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
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);
DESCRIPTION
The recvfrom() and recvmsg() calls are used to receive messages from a socket, and may be used to receive data on a socket whether or not it is connection-oriented.