首先,我们两个手机的绿皮聊天软件客户端,要通信,中间会通过它们家服务器。大概长这样。
但为了简化模型,我们把中间的服务器给省略掉,假设这是个端到端的通信。且为了保证消息的可靠性,我们盲猜它们之间用的是TCP协议进行通信。
为了发送数据包,两端首先会通过三次握手,建立TCP连接。
一个数据包,从聊天框里发出,消息会从聊天软件所在的用户空间拷贝到内核空间的发送缓冲区(send buffer) ,数据包就这样顺着传输层、网络层,进入到数据链路层,在这里数据包会经过流控(qdisc),再通过RingBuffer发到物理层的网卡。数据就这样顺着网卡发到了纷繁复杂的网络世界里。这里头数据会经过n多个路由器和交换机之间的跳转,最后到达目的机器的网卡处。
此时目的机器的网卡会通知DMA将数据包信息放到RingBuffer
中,再触发一个硬中断给CPU
,CPU
触发软中断让ksoftirqd
去RingBuffer
收包,于是一个数据包就这样顺着物理层,数据链路层,网络层,传输层,最后从内核空间拷贝到用户空间里的聊天软件里。
到这里,抛开一些细节,大家大概知道了一个数据包从发送到接收的宏观过程。
TCP协议会通过三次握手建立连接。大概长下面这样。
在服务端,第一次握手之后,会先建立个半连接,然后再发出第二次握手。这时候需要有个地方可以暂存这些半连接。这个地方就叫半连接队列。
如果之后第三次握手来了,半连接就会升级为全连接,然后暂存到另外一个叫全连接队列的地方,坐等程序执行accept()
方法将其取走使用。
是队列就有长度,有长度就有可能会满,如果它们满了,那新来的包就会被丢弃。
应用层能发网络数据包的软件有那么多,如果所有数据不加控制一股脑冲入到网卡,网卡会吃不消,那怎么办?让数据按一定的规则排个队依次处理,也就是所谓的qdisc(Queueing Disciplines,排队规则),这也是我们常说的流量控制机制。
排队,得先有个队列,而队列有个长度。
我们可以通过ifconfig
命令查看,里面涉及到的txqueuelen
后面的数字1000
,其实就是流控队列的长度。
当发送数据过快,流控队列长度txqueuelen
又不够大时,就容易出现丢包现象。
网卡和它的驱动导致丢包的场景也比较常见,原因很多,比如网线质量差,接触不良。除此之外,我们来聊几个常见的场景。
上面提到,在接收数据时,会将数据暂存到RingBuffer
接收缓冲区中,然后等着内核触发软中断慢慢收走。如果这个缓冲区过小,而这时候发送的数据又过快,就有可能发生溢出,此时也会产生丢包。
网卡作为硬件,传输速度是有上限的。当网络传输速度过大,达到网卡上限时,就会发生丢包。这种情况一般常见于压测场景。
我们可以通过ethtool
加网卡名,获得当前网卡支持的最大速度。
# ethtool eth0
Settings for eth0:
Speed: 10000Mb/s
可以看到,我这边用的网卡能支持的最大传输速度speed=1000Mb/s。
也就是俗称的千兆网卡,但注意这里的单位是Mb,这里的b是指bit,而不是Byte。1Byte=8bit。所以10000Mb/s还要除以8,也就是理论上网卡最大传输速度是1000/8 = 125MB/s
。
我们一般使用TCP socket
进行网络编程的时候,内核都会分配一个发送缓冲区和一个接收缓冲区。
当我们想要发一个数据包,会在代码里执行send(msg)
,这时候数据包并不是一把梭直接就走网卡飞出去的。而是将数据拷贝到内核发送缓冲区就完事返回了,至于什么时候发数据,发多少数据,这个后续由内核自己做决定。
而接收缓冲区作用也类似,从外部网络收到的数据包就暂存在这个地方,然后坐等用户空间的应用程序将数据包取走。
这两个缓冲区是有大小限制的,可以通过下面的命令去查看。
# 查看接收缓冲区
# sysctl net.ipv4.tcp_rmem
net.ipv4.tcp_rmem = 4096 87380 6291456
# 查看发送缓冲区
# sysctl net.ipv4.tcp_wmem
net.ipv4.tcp_wmem = 4096 16384 4194304
不管是接收缓冲区还是发送缓冲区,都能看到三个数值,分别对应缓冲区的最小值,默认值和最大值 (min、default、max)。缓冲区会在min和max之间动态调整。
那么问题来了,如果缓冲区设置过小会怎么样?
对于发送缓冲区,执行send的时候,如果是阻塞调用,那就会等,等到缓冲区有空位可以发数据。
如果是非阻塞调用,就会立刻返回一个 EAGAIN
错误信息,意思是 Try again
。让应用程序下次再重试。这种情况下一般不会发生丢包。
当接受缓冲区满了,事情就不一样了,它的TCP接收窗口会变为0,也就是所谓的零窗口,并且会通过数据包里的win=0
,告诉发送端,“球球了,顶不住了,别发了”。一般这种情况下,发送端就该停止发消息了,但如果这时候确实还有数据发来,就会发生丢包。
前面提到的是两端机器内部的网络丢包,除此之外,两端之间那么长的一条链路都属于外部网络,这中间有各种路由器和交换机还有光缆啥的,丢包也是很经常发生的。
这些丢包行为发生在中间链路的某些个机器上,我们当然是没权限去登录这些机器。但我们可以通过一些命令观察整个链路的连通情况。
比如我们知道目的地的域名是 baidu.com
。想知道你的机器到baidu服务器之间,有没有产生丢包行为。可以使用ping命令。
mtr命令可以查看到你的机器和目的机器之间的每个节点的丢包情况。
说了这么多。只是想告诉大家,丢包是很常见的,几乎不可避免的一件事情。
但问题来了,发生丢包了怎么办?
这个好办,用TCP协议去做传输。
建立了TCP连接的两端,发送端在发出数据后会等待接收端回复ack包
,ack包
的目的是为了告诉对方自己确实收到了数据,但如果中间链路发生了丢包,那发送端会迟迟收不到确认ack,于是就会进行重传。以此来保证每个数据包都确确实实到达了接收端。
假设现在网断了,我们还用聊天软件发消息,聊天软件会使用TCP不断尝试重传数据,如果重传期间网络恢复了,那数据就能正常发过去。但如果多次重试直到超时都还是失败,这时候你将收获一个红色感叹号。
我们知道TCP位于传输层,在它的上面还有各种应用层协议,比如常见的HTTP或者各类RPC协议。
TCP保证的可靠性,是传输层的可靠性。也就是说,TCP只保证数据从A机器的传输层可靠地发到B机器的传输层。
至于数据到了接收端的传输层之后,能不能保证到应用层,TCP并不管。
假设现在,我们输入一条消息,从聊天框发出,走到传输层TCP协议的发送缓冲区,不管中间有没有丢包,最后通过重传都保证发到了对方的传输层TCP接收缓冲区,此时接收端回复了一个ack
,发送端收到这个ack
后就会将自己发送缓冲区里的消息给扔掉。到这里TCP的任务就结束了。
TCP任务是结束了,但聊天软件的任务没结束。
聊天软件还需要将数据从TCP的接收缓冲区里读出来,如果在读出来这一刻,手机由于内存不足或其他各种原因,导致软件崩溃闪退了。
发送端以为自己发的消息已经发给对方了,但接收端却并没有收到这条消息。
于是乎,消息就丢了。
虽然概率很小,但它就是发生了。
大家应该还记得我们文章开头提到过,为了简单,就将服务器那一方给省略了,从三端通信变成了两端通信,所以才有了这个丢包问题。
大家有没有发现,有时候我们在手机里聊了一大堆内容,然后登录电脑版,它能将最近的聊天记录都同步到电脑版上。也就是说服务器可能记录了我们最近发过什么数据,假设每条消息都有个id,服务器和聊天软件每次都拿最新消息的id进行对比,就能知道两端消息是否一致,就像对账一样。
对于发送方,只要定时跟服务端的内容对账一下,就知道哪条消息没发送成功,直接重发就好了。
如果接收方的聊天软件崩溃了,重启后跟服务器稍微通信一下就知道少了哪条数据,同步上来就是了,所以也不存在上面提到的丢包情况。
可以看出,TCP只保证传输层的消息可靠性,并不保证应用层的消息可靠性。如果我们还想保证应用层的消息可靠性,就需要应用层自己去实现逻辑做保证。
那么问题叒来了,两端通信的时候也能对账,为什么还要引入第三端服务器?
主要有三个原因。
1000个
好友,你就得建立1000个
连接。但如果引入服务端,你只需要跟服务器建立1个
连接就够了,聊天软件消耗的资源越少,手机就越省电。所以看到这里大家应该明白了,我把服务端去掉,并不单纯是为了简单。
数据从发送端到接收端,链路很长,任何一个地方都可能发生丢包,几乎可以说丢包不可避免。
平时没事也不用关注丢包,大部分时候TCP的重传机制保证了消息可靠性。
当你发现服务异常的时候,比如接口延时很高,总是失败的时候,可以用ping或者mtr命令看下是不是中间链路发生了丢包。
TCP只保证传输层的消息可靠性,并不保证应用层的消息可靠性。如果我们还想保证应用层的消息可靠性,就需要应用层自己去实现逻辑做保证。
原文:用了TCP协议,就一定不会丢包吗?