TCP之send & recv

接触过网络开发的人,大抵都知道,上层应用使用s函数发送数据,使用recv来接收数据,而s和recv的实现原理又是怎样的呢?
在前面的几篇文章中,我们有提过,TCP是个可靠的、全双工协议。其流量控制或者拥塞控制依赖于滑动窗口和拥塞窗口的滑动来实现,而这两个窗口的滑动实现则是依赖于TCP中的两个buffer,这两个buffer则是TCPsocket在内核中的发送缓冲区(sbuffer)和接收缓冲区(recvbuffer)。
在本文中,我们首先会简单介绍下TCP中发送缓冲区和接收缓冲区的作用(对于后面理解s和recv非常重要),然后讲解Linux系统下,TCP发送和接收数据是如何实现的。
缓冲区缓冲区,可以理解为是一个临时缓存。
对于发送端来说,socket将数据拷贝到发送临时缓冲区,就立即返回到应用层去做其他的事情,而剩下的将临时缓冲区的数据通过内核发送到对端,这就是tcp的事。
对于接收端来说,内核将网络中的数据拷贝到缓冲区,等待上层应用读取。
发送缓冲区上面有讲,进程在调用s()发送的数据的时候,最简单情况(也是一般情况),将数据拷贝进入socket的内核发送缓冲区之中,然后s便会立即返回。
换句话说,在应用层调用s()返回之时,数据不一定会发送到对端去(和write写文件有点类似),s()仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中。
TCPsocket有两种模式,即阻塞模式和非阻塞模式。
在阻塞模式下,s函数的过程是将应用程序请求发送的数据拷贝到发送缓存中发送并得到确认后再返回.但由于发送缓存的存在,表现为:如果发送缓存大小比请求发送的大小要大,那么s函数立即返回,同时向网络中发送数据;否则,s向网络发送缓存中不能容纳的那部分数据,并等待对端确认后再返回(接收端只要将数据收到接收缓存中,就会确认,并不一定要等待应用程序调用recv)
在非阻塞模式下,s函数的过程仅仅是将数据拷贝到协议栈的缓存区而已,如果缓存区可用空间不够,则尽能力的拷贝,返回成功拷贝的大小;如缓存区可用空间为0,则返回-1,同时设置errno为EAGAIN.
在Linux内核中,有两种方式可以查看tcp缓冲区buffer大小。
1、通过查看/etc/下的_wmem值
2、通过命令'cat/proc/sys/net/ipv4/tcp_wmem'
cat/proc/sys/net/ipv4/tcp_wmem4096163844194304
从上面可以看出,在笔者所在的服务器上,tcps缓冲区buffer有3个值,分别是4096163844194304。
第一个值是socket的发送缓存区分配的最少字节数,
第二个值是默认值(该值会被_default覆盖),缓存区在系统负载不重的情况下可以增长到这个值
第三个值是发送缓存区空间的最大字节数(该值会被_max覆盖)
我们可以通过程序,来修改当前tcpsocket的发送缓冲区大小,需要注意的是,如下的代码修改,只会修改当前特定的socket。
intbuffer_len=10240;setsockopt(fd,SOL_SOCKET,SO_SNDBUF,(void*)buffer_len,buffer_len);接收缓冲区
接收缓冲区被TCP用来缓存网络上来的数据,一直保存到应用进程读走为止。
对于TCP,如果应用进程一直没有读取,接收缓冲区满了之后,发生的动作是:收端通知发端,接收窗口关闭(win=0)。这个便是滑动窗口的实现。保证TCP套接口接收缓冲区不会溢出,从而保证了TCP是可靠传输。因为对方不允许发出超过所通告窗口大小的数据。这就是TCP的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它。
与查看发送缓冲区大小的方式一样,接收缓冲区也是通过如上的两种方式。1、通过查看/etc/下的_rmem值
2、通过命令'cat/proc/sys/net/ipv4/tcp_rmem'
cat/proc/sys/net/ipv4/tcp_rmem4096873804194304
TCP接收缓冲区buffer有3个值,分别是4096873804194304。
第一个值是socket的接收缓存区的最少字节数,
第二个值是默认值(该值会被_default覆盖),缓存区在系统负载不重的情况下可以增长到这个值
第三个值是接收缓存区空间的最大字节数(该值会被_max覆盖)
同样的,可以通过如下代码,修改接收缓冲区的大小。
intbuffer_len=10240;setsockopt(fd,SOL_SOCKET,SO_RCVBUF,(void*)buffer_len,buffer_len);实现原理
为了便于我们理解TCP的整个传输过程,我们先了解下TCP的四层模型以及四册模型在数据传输中的流向。后面我们将从四层模型的角度来分析s和recv函数在每层中都做了什么。
s原理NAMEs,sto,smsg-samessageonasocketSYNOPSISincludesys/_ts(intsockfd,constvoid*buf,size_tlen,intflags);DESCRIPTIONThesystemcallss(),sto(),andsmsg()areusedtotransmitamessagetoanothersocket.
当调用该函数时,s函数:1、先比较待发送数据的长度len和套接字sockfd的可用发送缓冲区的长度
如果数据长度len大于发送缓冲区的长度,则分多次发送
如果果len小于或者等于sockfd的缓冲区长度,那么s先检查协议是否正在发送sockfd的发送缓冲中的数据如果是就等待协议把数据发送完否则,如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么s就比较sockfd的发送缓冲区的剩余空间和len如果len大于剩余空间大小,s就一直等待协议把s的发送缓冲中的数据发送完如果len小于剩余空间大小,s就仅仅把buf中的数据copy到剩余空间里。如果s函数copy数据成功,就返回实际copy的字节数,如果s在copy数据时出现错误,那么s就返回SOCKET_ERROR;如果s在等待协议传送数据时网络断开的话,那么s函数也返回SOCKET_ERROR。需要注意s函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个socket函数就会返回SOCKET_ERROR.(每一个除s外的socket函数在执行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该socket函数就返回SOCKET_ERROR)。
如果对具体实现不是很感兴趣,可直接此部分从四层模型的角度来分析s实现。
应用层对于TCP,应用程序在创建socket之后,调用connect()函数,通过socket使客户端和服务端建立连接。然后就可以调用s函数发送数据。
传输层数据在传输层进行处理,以TCP协议为例,其主要有以下功能:
1、构造TCP段
2、计算校验和
3、发送回复(ACK)包
4、滑动窗口(slidingwindown)等操作保证可靠性。
不同的协议有不同的发送函数,TCP调用tcp_smsg函数,而UDP则调用的是sock_smsg函数。
tcp_smsg()的主要工作是传输用户层的数据,将数据放入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)。
上图为s函数源码的调用逻辑图,对源码有兴趣的话,可以在net/找到对应的实现。
recv原理NAMErecv,recvfrom,recvmsg-receiveamessagefromasocketSYNOPSISincludesys/_trecv(intsockfd,void*buf,size_tlen,intflags);ssize_trecvfrom(intsockfd,void*buf,size_tlen,intflags,structsockaddr*src_addr,socklen_t*addrlen);ssize_trecvmsg(intsockfd,structmsghdr*msg,intflags);DESCRIPTIONTherecvfrom()andrecvmsg()callsareusedtoreceivemessagesfromasocket,andmay_addrisnotNULL,andtheunderlyingprotocolprovidesthesourceaddress,_addrisNULL,nothingisfilledin;inthiscase,addrlenisnotused,,whichthecallershouldinitializebeforethecalltothesizeofthebufferassociatedwithsrc_addr,;inthiscase,()callisnormallyusedonlyonaconnectedsocket(seeconnect(2))andisidenticaltorecvfrom()withaNULLsrc_addrargument.
当调用该函数时候:
先检查套接字sockfd的接收缓冲区
如果sockfd接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。
当协议把数据接收完毕,recv函数就把sockft的接收缓冲中的数据copy到buf中,recv函数返回其实际copy的字节数。
如果recv在copy时出错,那么它返回SOCKET_ERROR;
如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
对方优雅的关闭socket并不影响本地recv的正常接收数据;
如果协议缓冲区内没有数据,recv返回0,指示对方关闭;
如果协议缓冲区有数据,则返回对应数据(可能需要多次recv),在最后一次recv时,返回0,指示对方关闭。
如果对具体实现不是很感兴趣,可直接此部分从四层模型的角度来分析recv实现。
数据链路层当数据包到达机器的物理网卡时会触发一个中断,中断处理程序分配skb_buff数据结构,并将从网卡I/O接收到的数据帧复制到skb_buff缓冲区,并设置skb_buff相应的参数。
然后发出软中断,通知内核接收新的数据帧。进入软中断处理流程,调用net_rx_action函数。进入netif_receive_skb处理流程。
netif_receive_skb根据在全局数组ptype_all和ptype_base中注册的网络层数据报类型,将数据报发送到不同的网络层协议接收函数(INET域主要是ip_rcv和arp_rcv)。
网络层ip_rcv函数为网络层的入口函数。该函数做的第一件事就是数据校验,然后调用ip_rcv_finish这个函数。
ip_rcv_finish函数会调用ip_route_input函数来更新路由,然后寻找路由,决定消息是发送到本地机器,转发还是丢弃。
如果发送到本机,则调用ip_local_deliver函数,可以进行碎片整理(合并多个包),并调用ip_local_deliver_finish。最后调用下一层接口,包括tcp_v4_rcv(TCP)、udp_rcv(UDP)、icmp_rcv(ICMP)、igmp_rcv(IGMP)。如果需要转发,则进入转发流程,调用dev_queue_xmit,进入链路层处理流程。如果不是发送到本机,应该是转发,调用ip_forward进行转发。
传输层在该层,我们会做一些完整性检查,如果发现问题就丢包。如果是tcp,则调用tcp_v4_do_rcv。
然后sk-sk_state==TCP_ESTABLISHED,调用tcp_rcv_builted,调用tcp_data_queue方法将消息放入队列。然后使用tcp_ofo_queue方法将消息插入接收到Queued。
应用层应用程序调用读取或者recv的时候,该调用被映射到/net/中的sys_recv系统调用,然后调用sock_recvmsg函数。
TCP会调用tcp_recvmsg。该函数从套接字缓冲区复制数据到缓冲区。
上述过程,我们总结下就是:1、数据帧从外部网络到达网卡2、网卡把帧DMA到内存RingBuffer中3、硬中断通知CPU4、CPU响应硬中断,简单处理后发憷软中断5、软中断进程处理软中断,调用网卡驱动注册的pool函数开始收包6、帧被从RingBuffer中摘下来,存储到skb中7、协议层开始处理网络帧,并将处理完成后的数据放入socket的接收缓冲区中
上图为整个网络数据接收的函数调用过程,对月接收端来说,当有数据来的时候,都是通过终端来通知内核,最终通过回调,调用系统函数。
下图是s和recv完整的函数调用过程
常见问题在实际应用中,如果发送端是非阻塞发送,由于网络的阻塞或者接收端处理过慢,通常出现的情况是,发送应用程序看起来发送了10k的数据,但是只发送了2k到对端缓存中,还有8k在本机缓存中(未发送或者未得到接收端的确认).那么此时,接收应用程序能够收到的数据为2k.假如接收应用程序调用recv函数获取了1k的数据在处理,在这个瞬间,发生了以下情况之一,双方表现为:
发送应用程序认为s完了10k数据,关闭了socket:
发送主机作为tcp的主动关闭者,连接将处于FIN_WAIT1的半关闭状态(等待对方的ack),并且,发送缓存中的8k数据并不清除,依然会发送给对端.如果接收应用程序依然在recv,那么它会收到余下的8k数据(这个前题是,接收端会在发送端FIN_WAIT1状态超时前收到余下的8k数据.),然后得到一个对端socket被关闭的消息(recv返回0).这时,应该进行关闭.
发送应用程序再次调用s发送8k的数据:
假如发送缓存的空间为20k,那么发送缓存可用空间为20-8=12k,大于请求发送的8k,所以s函数将数据做拷贝后,并立即返回8192;
假如发送缓存的空间为12k,那么此时发送缓存可用空间还有12-8=4k,s()会返回4096,应用程序发现返回的值小于请求发送的大小值后,可以认为缓存区已满,这时必须阻塞(或通过select等待下一次socket可写的信号),如果应用程序不理会,立即再次调用s,那么会得到-1的值,在linux下表现为errno=EAGAIN.
接收应用程序在处理完1k数据后,关闭了socket:接收主机作为主动关闭者,连接将处于FIN_WAIT1的半关闭状态(等待对方的ack).然后,发送应用程序会收到socket可读的信号(通常是select调用返回socket可读),但在读取时会发现recv函数返回0,这时应该调用close函数来关闭socket(发送给对方ack);
如果发送应用程序没有处理这个可读的信号,而是在s,那么这要分两种情况来考虑,假如是在发送端收到RST标志之后调用s,s将返回-1,同时errno设为ECONNRESET表示对端网络已断开,但是,也有说法是进程会收到SIGPIPE信号,该信号的默认响应动作是退出进程,如果忽略该信号,那么s是返回-1,errno为EPIPE(未证实);如果是在发送端收到RST标志之前,则s像往常一样工作;
以上说的是非阻塞的s情况,假如s是阻塞调用,并且正好处于阻塞时(例如一次性发送一个巨大的buf,超出了发送缓存),对端socket关闭,那么s将返回成功发送的字节数,如果再次调用s,那么会同上一样.
交换机或路由器的网络断开:
接收应用程序在处理完已收到的1k数据后,会继续从缓存区读取余下的1k数据,然后就表现为无数据可读的现象,这种情况需要应用程序来处理超时.一般做法是设定一个select等待的最大时间,如果超出这个时间依然没有数据可读,则认为socket已不可用.
发送应用程序会不断的将余下的数据发送到网络上,但始终得不到确认,所以缓存区的可用空间持续为0,这种情况也需要应用程序来处理.
如果不由应用程序来处理这种情况超时的情况,也可以通过tcp协议本身来处理,具体可以查看sysctl项中的:_keepalive__keepalive__keepalive_time
结论TCP协议本身是为了保证可靠传输,并不等于应用程序用tcp发送数据就一定是可靠的,必须要容错;
s()只负责拷贝,拷贝到内核就返回
此次s()调用所触发的程序错误,可能会在本次返回,也可能在下次调用网络IO函数的时候被返回。
在进行TCP协议传输的时候,要注意数据流传输的特点,recv和s不一定是一一对应的(一般情况下是一一对应),也就是说并不是s一次,就一定recv一次就接收完,有可能s一次,recv多次才接收完,也可能s多次,一次recv就接收完了。TCP协议会保证数据的有序完整的传输,但是如何去正确完整的处理每一条信息,是开发人员的事情。
参考推荐阅读
-
慧眼识前行 — 五邑智行科技投资13亿元打造智能电单车制造高地
智能科技领航——五邑智行科技的未来出行梦想项目名称:江门五邑智行科技有限公司智能电单车生产制造基地项目项目基本情况及投资逻辑江门五邑智行科技有限公司投资130000万元建设智能电单车生产制造基地,起战新能源汽车产业的烽火。项目选址于江门市新会区,覆盖面积87665.97平方米,总建筑面积达到2200...
-
占据行业先发优势,深眸科技核心团队同频共振布局AI机器视觉市场
随着智能制造进程的持续推进,新一代信息技术引领着第四次工业革命,机器视觉技术乘着东风实现高速发展,其视觉创新应用产品全面铺开,新应用、新模式不断涌现。深眸科技紧抓时代发展机遇,基于领先的图像算法和自主研究的深度学习积累,深入布局各行业场景,助力我国工业企业生产线智能化水平提升。政策加码,占据行业先发...
-
为什么欧洲众多国家从未统一,而中华大地却分久必合?
我们看今天的世界地图,欧洲是众多小国家组成的,为什么欧洲历史上从来没有被统一过?我们再看中国历史上的几次分裂时期,春秋时期国家林立,小国无数;战国时期七国连年战乱最终被秦始皇统一;三国最后归晋;南北朝的纷乱被隋唐取代;五代十国争斗不止也被北宋统一;朱元璋打败陈友谅和张士诚,推翻元朝,建立大明。为什么...
-
电子制作(一)
制作目标5V直流电源5V电源2.材料选型二极管,电解电容,非极性电容,变压器,稳压芯片二极管的作用是组成桥式整流电路将交流电压转换为直流电压,变压器的作用是通过将220V交流电转换为9V的交流电,电解电容的作用是进行平滑滤波,目的是将桥式二极管转换的电压进行平滑滤波处理,非极性电容的目的是进行高频滤...