浅谈基于TCP和UDP的协议设计
From:http://blog.sina.com.cn/s/blog_48d4cf2d0101859x.html
一個(gè)基于TCP/WebSockets的超級(jí)精簡的長連接消息協(xié)議:https://studygolang.com/articles/10506
github 上 一個(gè)簡單的消息協(xié)議:https://github.com/acrazing/stmp
google protobuf:https://github.com/google/protobuf
google ProtoBuf開發(fā)者指南:http://blog.csdn.net/henryzhang2009/article/details/40508413
知乎關(guān)于QQ使用 TCP 和 UDP 的討論:https://www.zhihu.com/question/20292749
網(wǎng)絡(luò)協(xié)議設(shè)計(jì):https://www.cnblogs.com/youxin/p/4050394.html
簡書 游戲開發(fā)—協(xié)議設(shè)計(jì):https://www.jianshu.com/p/c5cc603e60a3
WinSock網(wǎng)絡(luò)編程經(jīng)絡(luò)_源碼:http://download.csdn.net/download/geoff08zhang/4571358
----------------------------------------------------------------------------------------------------------------
通信協(xié)議:就是通信時(shí)所遵守的規(guī)則,只有雙方按照這個(gè)規(guī)則“說話”,對(duì)方才能理解或?yàn)橹?wù)。
1. 基于TCP的協(xié)議設(shè)計(jì)
是否分幀
大部分網(wǎng)絡(luò)應(yīng)用是需要分幀的。舉IM為例,用戶登錄是一個(gè)幀,用戶發(fā)送文本信息是一個(gè)幀。少部分應(yīng)用可以不需要分幀,比如:echo服務(wù)器,接收到什么直接回復(fù)即可;轉(zhuǎn)發(fā)服務(wù)器,同樣是接收到數(shù)據(jù)直接轉(zhuǎn)給目標(biāo)機(jī)器;更常見的情況是一個(gè)TCP連接只發(fā)送/處理一個(gè)請(qǐng)求之后就直接關(guān)閉,這種也就沒必要分幀了。考慮到除了學(xué)習(xí)網(wǎng)絡(luò)編程,沒人做echo server。所以只要服務(wù)端不是一次連接只處理一個(gè)請(qǐng)求,或者純轉(zhuǎn)發(fā),就應(yīng)該采用分幀的設(shè)計(jì)。
如何分幀?
注意:幀是業(yè)務(wù)處理的單元,是具體應(yīng)用Care的,但這不關(guān)TCP的事情!初學(xué)者往往認(rèn)為tcp這端 write一次,tcp那端就會(huì)read一次,然后驚呼“粘包”、“丟包”,其實(shí)這都是程序處理不當(dāng)。在這邊推薦一本書籍《TCP/IP協(xié)議詳解 卷1》,挺薄的,看完可以減少很多對(duì)TCP的錯(cuò)誤認(rèn)識(shí)。實(shí)際上發(fā)送方發(fā)送一幀,接收方可能要N次才能讀取完成,而且可能同時(shí)讀到下幀的數(shù)據(jù)。那要怎么在接收方把一幀數(shù)據(jù)不多不少的讀取出來呢?
常用做法有兩個(gè):基于長度和基于終結(jié)符(Delimiter)。
基于長度:就是在幀前先發(fā)送幀的長度,一般用固定長度的字節(jié)來發(fā)送此長度,比如2個(gè)字節(jié)(最大幀長不能大于65535),4個(gè)字節(jié)。(ps:我也見過使用可變長度的字節(jié)來發(fā)送此長度,比如netty中的ProtobufVarint32FrameDecoder,看代碼那是相當(dāng)?shù)牡疤?#xff0c;我覺得完全是折騰自己,強(qiáng)烈不推薦。)使用基于長度的分幀方式,接受方處理流程一般是這樣:“讀取固定長度的字節(jié) -> 解析出幀長 -> 讀取幀長字節(jié) -> 處理幀”。
基于終結(jié)符(Delimiter):最典型的應(yīng)用就是HTTP協(xié)議了,使用/r/n/r/n作為終結(jié)符。使用基于終結(jié)符的分幀方式,接收方的處理流程一般是這樣:“讀數(shù)據(jù) -> 在讀取的數(shù)據(jù)中定位終結(jié)符 -> 沒找到,將數(shù)據(jù)緩存 -> 繼續(xù)讀數(shù)據(jù) -> 定位終結(jié)符 -> 找到終結(jié)符,將終結(jié)符之前的數(shù)據(jù)作為一幀進(jìn)行處理”。
使用終結(jié)符的方式務(wù)必要考慮轉(zhuǎn)義問題,不然在幀的數(shù)據(jù)中出現(xiàn)終結(jié)符,樂子就大了。
注意不管采用哪種方式,在開發(fā)的時(shí)候都需要考慮最大幀長的問題。不然如果對(duì)方說要發(fā)送4G長度的幀(惡意or程序錯(cuò)誤),真的去new 4G字節(jié)的緩存;或者對(duì)方一直發(fā)送數(shù)據(jù),沒有終結(jié)符。都可能造成程序內(nèi)存耗盡。
一般來說,基于長度的分幀方式。開發(fā)更簡單,程序執(zhí)行效率也更高,使用更廣泛些。基于終結(jié)符也不是一無是處:可讀性更好,容易模擬和測(cè)試(如用telnet)。下面重點(diǎn)討論基于長度的分幀方式。
基于長度的的幀設(shè)計(jì)(length based frame design)
一般來說,我們會(huì)將幀分為幀頭(frame header,一般是固定長度)和幀體(frame body,一般是可變長度,也有固定長度的)。如上所述,最簡單的幀頭只要一個(gè)字段——幀長。但在實(shí)際應(yīng)用中,一個(gè)典型的幀頭可能還有以下字段:
a)消息類型(message type):在一個(gè)網(wǎng)絡(luò)應(yīng)用中,往往有多種類型的幀。比如對(duì)于IM,有登陸/登出/發(fā)送消息/……。接收方需要根據(jù)幀頭的消息類型字段,解碼出不同種類的消息,交給相應(yīng)處理模塊進(jìn)行處理。也就是幀的結(jié)構(gòu)是Length-Type-Message,Length-Type可以視為幀頭,Message是幀體。消息類型一般也是使用固定長度,比如Length 4個(gè)字節(jié),Type 4個(gè)字節(jié),那么幀頭的長度就是8個(gè)字節(jié)。接收方處理流程:“讀幀頭長度字節(jié)數(shù)據(jù) - 解碼幀頭獲得長度和消息類型 - 讀幀體長度字節(jié)數(shù)據(jù) - 根據(jù)消息類型解碼消息 - 處理消息”。Length-Type-Message結(jié)構(gòu)的幀設(shè)計(jì)是使用最廣泛的,普適性最好也最精簡的設(shè)計(jì)。
b)請(qǐng)求序列號(hào)(serials):這個(gè)不是必選項(xiàng),但我覺得對(duì)于非echo式的服務(wù)(echo式的服務(wù):總是客戶端發(fā)送請(qǐng)求-服務(wù)端針對(duì)該請(qǐng)求應(yīng)答,應(yīng)答保證嚴(yán)格按照請(qǐng)求順序),加上這個(gè)字段肯定不后悔。這樣對(duì)于亂序(如果有消息隊(duì)列后臺(tái)線程池,很正常)的執(zhí)行結(jié)果,才能夠和請(qǐng)求對(duì)上號(hào),從而做出正確的處理。一般來說,高性能的服務(wù)端要保證響應(yīng)的嚴(yán)格有序,是比較麻煩和影響性能的。
c)版本號(hào)(version):很多人這么用,但我覺得大部分情況下這不是個(gè)好主意。幀頭應(yīng)該放大部分/全部幀都需要的字段。而版本號(hào)可能只有少數(shù)包如登錄會(huì)用到,所以放到登錄包體里可能更合適。單獨(dú)維護(hù)每個(gè)協(xié)議的版本工作量會(huì)比較大,開發(fā)起來會(huì)比較繁瑣易錯(cuò)。至于擔(dān)心解碼失敗,更好的方式是采用類似Protobuf這種可以向下兼容的編解碼方案。
注意:在幀頭設(shè)計(jì)時(shí)應(yīng)該要盡可能的精簡和通用,因?yàn)閹^長度是每個(gè)幀都需要的額外開銷。如果某個(gè)字段(如序列號(hào))只有少數(shù)幀會(huì)使用到,完全可以放在幀體里去。反之,如果某個(gè)字段大部分包都有,卻不定義在包頭,會(huì)導(dǎo)致難以統(tǒng)一處理,增加開發(fā)工作量。這些需要根據(jù)具體業(yè)務(wù)需求來進(jìn)行權(quán)衡,沒有統(tǒng)一的答案。舉個(gè)例子,Length-Type-Message結(jié)構(gòu)適用于大部分情況,但如果業(yè)務(wù)要求每個(gè)幀都需要表明操作者,在幀頭增加UID字段變成Length-Type-UID-Message,程序的開發(fā)會(huì)更簡單。
幀體的設(shè)計(jì)
幀體就是字段的集合,舉個(gè)例子,登錄幀體包含用戶名、密碼這兩個(gè)字段(只是舉例,現(xiàn)實(shí)的登錄包往往復(fù)雜得多)。在幀體設(shè)計(jì)上,大家往往也是八仙過海各顯神通。比如基于XML、json,基于字段Pos(舉登錄包為例,就先寫/讀用戶名,再寫/讀密碼。這種方式不是太好,很難向下兼容:比如登錄包需要在用戶名和密碼間加一個(gè)用戶狀態(tài),如果服務(wù)端/客戶端沒有同步升級(jí),就會(huì)斯巴達(dá))。我甚至見過狂野得離譜的直接使用C struct的,這種腦殘到爆:兼容性渣不說,類對(duì)齊(可以用pragma pack避免不一致)、byte order、機(jī)器字長都會(huì)造成麻煩。
比較推薦的做法:使用 Google Protobuf ,如果要可讀性好,json 相比 XML 更省帶寬。
2. 基于UDP的協(xié)議設(shè)計(jì)
典型的UDP的協(xié)議設(shè)計(jì)就是:Type-Message。Type長度固定,用于說明消息類型;Message是消息體,和tcp的幀體設(shè)計(jì)同樣即可。
------------------------------------------------------------------------------------------------------------------
總結(jié)
以上是生活随笔為你收集整理的浅谈基于TCP和UDP的协议设计的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: error LNK2019: 无法解析的
- 下一篇: 网络层(学习笔记)