tcp为我们做了什么事情?
总得来说,tcp做了这几件事:
- 通过序列号和基于确认的超时重传机制,为上层提供了可靠的字节流服务;
- 通过滑动窗口、拥塞窗口提供了流量控制;
- 默认情况下,为了有效利用带宽,tcp的报文一次会尽量携带更多的数据。但与此同时,为了避免IP层的分片,又不会发送超过MTU大小的数据包。
udp为我们做了什么事情?
首先应该清楚的是,一个udp数据包仅仅是在IP数据包之上加了一个udp协议头。这个协议头十分精简,仅有的四个字段是:目的端口号、源端口号、数据包长度、校验和。通过sendto这个syscall发送一个udp数据包时,实际上就是包装成一个IP数据包然后直接发送出去。这里并不会像tcp那样在本地缓存数据,直到达到一个长度限制后再发送出去。另外由于有检验和,所以我们不需要在udp之上再做校验的事情了。
一个可靠的udp协议
在清楚tcp做了什么事后,我们就有了实现一个可靠udp的思路了。首先需要说明的是,在udp之上实现一个tcp是没有意义的。实现一个可靠的udp协议往往是,根据业务需要,牺牲掉一部分的tcp的功能,来换取更高的性能。但是,在动手造轮子之前,我们应该分清的是,哪些功能是tcp已经为我们实现了的,而哪些功能是要通过自己定制一个可靠udp来实现的。比如说,上述的第3条,这个特性有时候会造成tcp的延迟性,但是通常可以通过设置TCP_NODELAY来解决。
实现一个最基础的可靠udp通讯协议,我们只需要提供一个重传机制即可。在这我实现了一个简单的可靠udp协议,这个协议为每一个发送出去的udp数据包分配一个包id,每次接收方收到一个数据包时,都要回应发送方一个ack对应这个包id。协议通过这种确认机制来保证接收方能收到发送方发出的udp数据包,在发出的时候,发送方应该设置一个计时器,超时的话会重传数据包。
这个协议做的就是这么多,它仅能确保接收方能收到发送方的udp包,并没有做其它的事情。
具体来说它没做这些事情:
- 它没有保证包的有序性。发送方连续发送几个udp数据包,接收方可以以任何顺序收到这几个数据包。如果想要做到有序性,必须由应用层来完成。
- 它没做流量控制。发送方连续大量发送数据包会导致网络性能变差,丢包次数增大。
- 它没对数据包大小做控制。为了避免IP层对数据包进行分片,应用层应该要保证每个数据包的大小不超过MTU。如果这个数据包会经过广域网(一般情况下)这个值应该不超过576。考虑到IP头的20字节,udp头的8个字节,以及这个协议头的字节。最好每次发送的数据大小在512以内。
这个协议这么简陋,那要它何用?
首先,对于互相独立的数据包,没有必要保证包的有序性。以游戏开发为例子,玩家在客户端发起一个开宝箱请求的同时,又迅速的切换界面,打开了角色面板又发起一个查看角色数据的请求。保证这两个请求的先后顺序真的有意义吗?
其次,对于大部分的小数据包,没有必要考虑IP分片。玩家发起一个开宝箱的请求,可能整个数据包的内容就是一个请求id和一个宝箱id。假设这两个id都是64位,也就是16字节,整个IP包的大小并不足以让IP层进行分片处理。如果你的系统绝大部分都是这种小包,或许你能不考虑IP分片。
再来谈谈tcp的字节流服务。正如大家所知道的,tcp是提供可靠的字节流服务的。但是,一般情况下我们真的需要传输层提供这种服务吗?以游戏服务器为例子,在用tcp与客户端进行网络通讯上,我们现在的做法是在字节流上对请求、响应进行封包解包,也就是在字节流之上,模拟一个数据包的传输。并且我相信很多其它游戏也是这么做的。数据在IP层上本来就是通过包的形式进行传输,tcp对此做了抽象实现了字节流,应用层又在字节流之上模拟出数据包。是否可以用udp去减少这些不必要的抽象呢?或许我们仅需要的是一个有序可靠的数据包服务,又或许我们连有序性都不需要。。。
总结
最后,我想说的是并没有一套解决方案能适用与所有情况。tcp作为传输层给出的是一套通用性的解决方案,并致力于满足大多数的需求,但是总会有其不适合的地方。这个时候我们就可以使用udp来自己定制一套时候自己业务的协议。这套协议不一定是很复杂,其实实现一个可靠的udp也并不是很复杂,关键要看你需要协议提供什么功能!
参考资料
- 可靠UDP传输。地址:http://blog.codingnow.com/2016/03/reliable_udp.html
- KCP。地址:https://github.com/skywind3000/kcp