前一阵子我对UDP传输大块数据方面做了一些尝试,在本文中总结了一些失败的教训。
对于本文所述“大块数据”,我定义为大小在数MB至数GB之间的数据块。数据可以在内存里,也可以来自文件。
方案1:逐包请求
此方案采用逐包请求模式,先发请求,然后等待接收对方回复,如此循环请求接收。好处是简单粗暴,流程很容易控制,超时未收到就重复发当前包。
1.0 每包载荷大小固定为1KB
考虑到太网最大传输单元(MTU)通常被设置为1500,到了UDP这一层还剩下1472字节,载荷在此范围内可最大程度的避免IP层报文分片,提高传输效率。我们这里确定载荷大小为1KB,再加上应用层消息头也能确保不超过1472字节。
经测试,采用固定1KB载荷的方式,循环发送接收,稳定性是很好的,能实现基本功能,但是传输效率很差,大量时间消耗在等待发送等待接收。
1.1 每包载荷大小固定为32KB
人们联想到最大传输单元(MTU),往往望文生义以为报文载荷不能超过MTU,其实不然,超过也是可以的,IP层仍然可以分片发送,只要系统协议栈发送缓冲区足够大,都是可以发出去的。
我改进1.0中的方案,载荷大小从1KB增加至32K甚至更大。经测试当32K(加上消息头略大于32KB)时,通常也能够稳定发送接收,但是,它挑网,例如在公司里成功率比较高,而在家里成功率就比较低。而且还往往遇到奇怪的现象,例如,前面请求传输了一万包都挺正常的,到第10001包就死活收不到回复了,延时加重试多少次也不管用。调试发现,Server端收到了请求也发出了回复,是Client端收不到,所以猜测是丢在半路了。
总之这个方案还是不行,虽然速度上去了,但是挑网,稳定性不好,某些情况下甚至连基本功能都无法完成。
1.2 动态调整载荷大小 1K~32K
考虑到1.1方案中某一包会卡死,始终收不到回复,延时+重复请求不管用。我决定尝试动态调整载荷的方案,32K包触发多次失败后,降低至16K,不行的话继续降低,直至1KB。
这样做出来的程序,能保证基本功能,完成数据传输,但是因为失败率还是挺高,不断降速导致传输性能也很差,无法接受。
方案2,单次请求批量接收
再改一下方案,把整块数据拆分为1MB为单位的子块,客户端请求一次子块,服务端批量回复,将该子块拆分成1KB的包循环发出去。这样避免了客户端请求报文的发送次数,估计能提高传输性能。
实测不然,稳定性不行,还挑网。服务端发快了不行,容易把自己系统的发送缓冲区打满,想加延时又不知道该延时多少,想找一个实测可行的数,换一个网又废了。再考虑到UDP不保证数据到达也不保证报文顺序,应用层把这些东西做完,代码复杂度又上了一个数量级,恐怕也是得不偿失。
总之,这个方案也不行,速度较快,不稳定,网络适应性查,代码逻辑复杂。
方案3,TCP… FTP…
反思当初选用UDP,不就是想简单粗暴吗,不需要考虑粘包拆包,来了就是一个完整的报文,代码逻辑很简单,性能还过得去,就可以了。但是做到最后发现,要么性能太差了无法接受,要么挑网太不稳定了无法接受,要么逻辑太复杂了无法接受。初步的结论是UDP只适合做小数据收发,不太适合大数据量传输(QUIC/ENET那种二次开发UDP除外)。
其实还不如回归TCP,前期自己稍微麻烦点做报文边界隔离和拆包,后面很多事情都省事了。TCP支持自动重发,支持顺序到达,不香吗?做到媲美FTP的传输速度应该不难吧?