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