先不考虑服务器,假设微信是端到端的连接,为了保证消息的可靠性,它们之间用的一定是TCP协议进行通信。
为了发送数据包,两端首先会通过三次握手,建立TCP连接。
一个数据包,从聊天框里发出,消息会从聊天软件所在的用户空间拷贝到内核空间的发送缓冲区(send buffer),数据包就这样顺着传输层、网络层,进入到数据链路层,在这里数据包会经过流控(qdisc),再通过RingBuffer发到物理层的网卡。数据就这样顺着网卡发到了纷繁复杂的网络世界里。这里头数据会经过n多个路由器和交换机之间的跳转,最后到达目的机器的网卡处。
此时目的机器的网卡会通知DMA将数据包信息放到RingBuffer中,再触发一个硬中断给CPU,CPU触发软中断让ksoftirqd去RingBuffer收包,于是一个数据包就这样顺着物理层,数据链路层,网络层,传输层,最后从内核空间拷贝到用户空间里的聊天软件里。
以上就是数据包发送及接收的流程,那么在这个过程中,什么情况下会导致丢包呢?
一:建立tcp连接时丢包
tcp在建立连接时会有三次握手
在服务端,第一次握手之后,会先建立个半连接,然后再发出第二次握手。这时候需要有个地方可以暂存这些半连接。这个地方就叫半连接队列。
如果之后第三次握手来了,半连接就会升级为全连接,然后暂存到另外一个叫全连接队列的地方,坐等程序执行accept()方法将其取走使用。
队列都是有长度的,当长度满了的时候,新来的包就会被丢弃,可以通过命令查看是否存在丢包情况
# 全连接队列溢出次数
# netstat -s | grep overflowed
4343 times the listen queue of a socket overflowed
# 半连接队列溢出次数
# netstat -s | grep -i "SYNs to LISTEN sockets dropped"
109 times the listen queue of a socket overflowed
二:流量控制丢包
因为很多应用都需要收发消息,如果不经控制,一股脑发送到网卡,会导致网卡超载,此时会引入流量控制机制qdisc(Queueing Disciplines,排队规则),让数据按一定的规则排个队依次处理。
队伍的长度可以通过 ifconfig 查到,其中的txqueuelen 字段就是队列长度
查看是否丢包时,可以输入 ifconfig命令,看TX下的dropped字段,当大于0时,说明发生了丢包
三:网卡丢包
不考虑物理层面(接触不良,网线质量问题)等,常见的网卡丢包情况如下:
RingBuffer过小导致丢包
之前提到过,在接收数据时,会把数据缓存在RingBuffer的缓冲区中,然后等着内核触发软中断慢慢收走。如果这个缓冲区过小,而这时候发送的数据又过快,就有可能发生溢出,此时也会产生丢包。
可以通过 ifconfig命令查看是否发生,overruns
指标,它记录了由于RingBuffer
长度不足导致的溢出次数。
查看RingBuffer的大小,可以通过
ethtool命令,想要修改这个长度可以执行ethtool -G eth1 rx 4096 tx 4096
将发送和接收RingBuffer的长度都改为4096。
网卡性能不足
网卡作为硬件,传输速度是有上限的。当网络传输速度过大,达到网卡上限时,就会发生丢包。这种情况一般常见于压测场景。
四:接收缓冲区丢包
我们一般使用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
三个数值分别对应最小值,默认值,最大值
如果缓冲区设置过小,对于发送缓冲区,执行send的时候,如果是阻塞调用,那就会等,等到缓冲区有空位可以发数据。如果是非阻塞调用,就会立刻返回一个 EAGAIN
错误信息,意思是 Try again
。让应用程序下次再重试。这种情况下一般不会发生丢包。
当接受缓冲区满了,事情就不一样了,它的TCP接收窗口会变为0,也就是所谓的零窗口,并且会通过数据包里的win=0
,告诉发送端别发了。一般这种情况下,发送端就该停止发消息了,但如果这时候确实还有数据发来,就会发生丢包。
五: 两端之间的网络丢包
ping命令查看丢包:常用命令,不多介绍。
但是ping命令只能知道你的机器和目的机器之间有没有丢包。
那如果你想知道你和目的机器之间的这条链路,哪个节点丢包了,可以用 mtr 命令
mtr命令可以查看到你的机器和目的机器之间的每个节点的丢包情况
可以看到Host
那一列,出现的都是链路中间每一跳的机器,Loss
的那一列就是指这一跳对应的丢包率。
需要注意的是,中间有一些是host是???
,那个是因为mtr默认用的是ICMP包,有些节点限制了ICMP包,导致不能正常展示。
我们可以在mtr命令里加个-u
,也就是使用udp包,就能看到部分???对应的IP。
以上介绍了丢包的场景,那么如何防止丢包呢
丢包是很常见的,几乎不可避免的一件事情。解决办法之一就是通过tcp传输。
建立了TCP连接的两端,发送端在发出数据后会等待接收端回复ack包
,ack包
的目的是为了告诉对方自己确实收到了数据,但如果中间链路发生了丢包,那发送端会迟迟收不到确认ack,于是就会进行重传。以此来保证每个数据包都确确实实到达了接收端。
假设现在网断了,我们还用聊天软件发消息,聊天软件会使用TCP不断尝试重传数据,如果重传期间网络恢复了,那数据就能正常发过去。但如果多次重试直到超时都还是失败,这时候你将收获一个红色感叹号。
TCP保证的可靠性,是传输层的可靠性。也就是说,TCP只保证数据从A机器的传输层可靠地发到B机器的传输层。
至于数据到了接收端的传输层之后,能不能保证到应用层,TCP并不管。
假设现在,我们输入一条消息,从聊天框发出,走到传输层TCP协议的发送缓冲区,不管中间有没有丢包,最后通过重传都保证发到了对方的传输层TCP接收缓冲区,此时接收端回复了一个ack
,发送端收到这个ack
后就会将自己发送缓冲区里的消息给扔掉。到这里TCP的任务就结束了。
TCP任务是结束了,但聊天软件的任务没结束。
聊天软件还需要将数据从TCP的接收缓冲区里读出来,如果在读出来这一刻,手机由于内存不足或其他各种原因,导致软件崩溃闪退了。
发送端以为自己发的消息已经发给对方了,但接收端却并没有收到这条消息。
于是乎,消息就丢了。
说了这么多,还是会丢包,难道真就没办法解决吗?
开头说了不考虑服务端的情况,端与端之间没办法解决,那把服务器做个处理不就好了吗
大家有没有发现,有时候我们在手机里聊了一大堆内容,然后登录电脑版,它能将最近的聊天记录都同步到电脑版上。也就是说服务器可能记录了我们最近发过什么数据,假设每条消息都有个id,服务器和聊天软件每次都拿最新消息的id进行对比,就能知道两端消息是否一致,就像对账一样。
对于发送方,只要定时跟服务端的内容对账一下,就知道哪条消息没发送成功,直接重发就好了。
如果接收方的聊天软件崩溃了,重启后跟服务器稍微通信一下就知道少了哪条数据,同步上来就是了,所以也不存在上面提到的丢包情况。
可以看出,TCP只保证传输层的消息可靠性,并不保证应用层的消息可靠性。如果我们还想保证应用层的消息可靠性,就需要应用层自己去实现逻辑做保证。
参考文章:用了TCP协议,就一定不会丢包吗?