基于 UDP 的安全可靠数据包传输协议(UDPack)
前言
众所周知,UDP 协议简单,报文收发速度快。但是 UDP 协议本身是不可靠的,数据报文在路由过程中可能会被丢弃。由于互联网拓扑结构的复杂性和动态性,连续发送的数据报文,路由也会不同,由于中间节点的处理延时,不能保证数据报文的交付顺序,先发送的报文可能会更晚收到。
如果直接采用 UDP 协议来开发应用程序,就要自己解决好上面的两个主要问题。为了了解类似 TCP 这样的面向连接的可靠协议存在的必要性,我们来分析下基于 UDP 开发应用程序具体要怎样做。
因为数据包可能会被中间节点丢弃,而且发送端不会被通知。那么接收端在收到数据后必须予以回应,发送端收到回应才能知晓数据已发送成功。如果发送端一段时间后没有收到回应就可以认为,数据可能已被丢弃,接收端没有接收到数据,此时就要重新发送该数据。从上面的分析来看,发送端收不到接收端回应的原因有两个,要么数据被丢弃,要么回应被丢弃,无论是哪个,发送端都会选择重新发送数据。
这就带来了另一个数据重复问题,如果接收端已接收到数据,只是发出的回应被丢弃了,发送端会重发,此时接收端会再次收到该数据。如果接收端不能识别出这是重复的数据,就会造成应用程序出错。所以每次发送的数据包要有唯一编号,而且接收端在收到数据后不能立刻消费,必须先放到缓存里等待一段时间,在这段时间里,接收端如果遇到相同的数据包则丢弃,然后予以回应,发送端收到回应后确认数据已发送成功,继续处理其他数据包。
从上面的分析可知,由于发送端可能会重发数据,所以要有发送缓存。接收端为了避免接收重复数据,也要设置接收缓存。发送端的数据缓存时间取决于多长时间收到该数据包的回应,接收端的缓存时间取决于数据已接收完整,且发送端已收到回应。对于接收端的回应,发送端是不会再给予回应的,发送端只会继续发送其他数据。所以对于接收端来说,通常会设置较长一段时间,确保发送端已收到回应。但这不是百分之百的,极小的概率是刚刚删除接收缓存中的数据包,就收到了重复的数据,此时应用程序就会报错。
再来看另一个顺序问题,如果接收者按照接收到数据的先后顺序消费数据,几乎可以肯定一定会遇到数据无法正确解析的错误。就算网络情况理想,先发后至这样的事情也会发生。所以要给数据包从小到大递增编号。接收端根据编号来在缓存中排序,依次消费数据。
此时会遇到另一个问题,接收端后面和前面的编号都已经收到了,唯独中间有个编号迟迟未收到,此编号前的数据可以安全消费,此编号后的数据暂时不能消费,就会阻塞在这里。
而且数据包有长度限制,不能超过 MTU 大小,超过 MTU 大小的数据包很可能被默默丢弃。应用程序需要传输的数据可能是非常长的,一张 JPG 图片几百 KB 甚至 几 MB,不可能一个数据包发送出去,此时就要对数据进行分片,每个分片单独编号,按照编号从小到大依次发送,然后在接收端进行数据包重新组装。
只需要用 UDP 写过一次应用程序,就可以体会到上面诸多需要考虑的问题,以及 TCP 是怎样默默地解决掉了这些问题。使用 TCP 协议,应用程序开发者只需要建立并维持连接,然后写入或者读取字节流,发送或接收完成后关闭连接即可。但是 TCP 协议也有自己的问题,就是“队首则塞”,TCP 协议严格保证数据顺序,如果网络抖动丢包较多,所丢包的编号以及后面的编号都要重传,造成阻塞。即使后面的数据和前面的是独立的,没有依赖关系。TCP 协议是通过建立多个连接来解决这个问题的,不同的数据通过不同的连接发送,避免了被不相关的数据阻塞住。但是建立和管理一个 TCP 连接的成本是很高的,创建连接时延时很大,这个问题可以通过连接池解决。但是客户端维持一个连接池,服务端可能就要管理超大规模数量的连接,因为一个客户端就维持了多个连接,而服务端承载的客户端数量可能是几千万甚至上亿的,在内存中维持这么多的连接数据是非常消耗资源的。
有个好消息是 QUIC 协议已经正式发布了,主流浏览器和 Web Server 已经全部支持该协议了,QUIC 也是基于 UDP 实现的,拥有 TCP 几乎所有的好处,解决了“队首阻塞”问题,同时也有更好的传输性能。但是这个协议非常复杂,PDF 版本协议内容有 151 页之多。可见实现这样一个协议是非常困难的,好在有很多开源实现。
QUIC 除了复杂之外,也是很难定制的。我想定义实现一个更简单、且容易定制扩展的传输协议 UDPack,UDPack 是 UDP Packet 两个单词的缩写。如同两个单词的含义,UDPack 协议是基于 UDP 协议的数据包传输协议。