Socket理解。
?
其他大部分系統(tǒng),例如CRM/CMS/權(quán)限框架/MIS之類的,無論怎么復雜,基本上都能夠本地代碼本地調(diào)試,性能也不太重要。(也許這個就是.net的企業(yè)級開發(fā)的戰(zhàn)略吧)
?
可是來到通訊系統(tǒng),一切變得困難復雜。原因?qū)嵲谔嗔?#xff0c;如:
- 性能永遠是第一位:有時候一個if判斷都要考慮性能,畢竟要損耗一個CPU指令,而在通訊系統(tǒng)服務(wù)器,每秒鐘都產(chǎn)生上百萬級別的通訊量,這樣一個if就浪費了1個毫秒了。
- 系統(tǒng)環(huán)境極其惡劣:所有我們可以想象的惡意攻擊、異常輸入等都要考慮;
- 網(wǎng)絡(luò)說斷就斷:在socket環(huán)境下,客戶端可以以各種理由斷開鏈接,而且服務(wù)器根本不會知道,連一個流水作業(yè)的業(yè)務(wù)邏輯都無法保證正常執(zhí)行,因此需要設(shè)計各種輔助的協(xié)議、架構(gòu)去監(jiān)督。
- 各種網(wǎng)絡(luò)鏈接問題:例如代理、防火墻等等。。。
經(jīng)過了1年的跌跌撞撞,我總算收獲了點有用的經(jīng)驗,本文先從設(shè)計角度介紹一些我在Socket編程中的經(jīng)驗,下一篇在放出源代碼。
?
------------------
現(xiàn)有的Socket編程資源
------------------
1. 首選推薦開源的XMPP框架,也就是Google的Gtalk的開源版本。里面的架構(gòu)寫的非常漂亮。特點就是:簡潔、清晰。
?
2. 其次推薦LumaQQ.net,這套框架本身寫的一般般,但是騰訊的服務(wù)器非常的猛,這樣必然導致客戶端也要比較猛。通過學習這套框架,能夠了解騰訊的IM傳輸協(xié)議設(shè)計,而且他們的協(xié)議是TCP/UDP結(jié)合,一舉兩得。
?
3. 最后就是DotMsn。這個寫的實在很一般般,而且也主要針對了MSN的協(xié)議特點。是能夠?qū)W習到一點點的框架知識的,不過要有所鑒別。
?
------------------
Socket的選擇
------------------
在Java,到了Java5終于出現(xiàn)了異步編程,NIO,于是各種所謂的框架冒了出來,例如MINA, xsocket等等;而在.NET,微軟一早就為我們準備好了完善的Socket模型。主要包括:同步Socket、異步Socket;我還聽說了.net 3.x之后,異步的Socket內(nèi)置了完成端口。綜合各種模型的性能,我總結(jié)如下:
?
1. 如果是短鏈接,使用同步socket。例如http服務(wù)器、轉(zhuǎn)接服務(wù)器等等。
?
2. 如果是長鏈接,使用異步socket。例如通訊系統(tǒng)(QQ / Fetion)、webgame等。
?
3. .net的異步socket的連接數(shù)性能在 7500/s(每秒并發(fā)7500個socket鏈接)。而聽說完成端口在1.5w所有。但是我到目前還沒有正式見過所謂的完成端口,不知道到底有多牛逼。
?
4. 我聽說了java的NIO性能在5000/s所有,我們項目內(nèi)部也進行了鏈接測試,在4000~5000比較穩(wěn)定,當然如果代碼調(diào)優(yōu)之后,能提高一點點。
?
------------------
TCP Socket協(xié)議定義
------------------
本文從這里開始,主要介紹TCP的socket編程。
新手們(例如當初的我),第一次寫socket,總是以為在發(fā)送方壓入一個"Helloworld",接收方收到了這個字符串,就“精通”了Socket編程了。而實際上,這種編程根本不可能用在現(xiàn)實項目,因為:
?
1. socket在傳輸過程中,helloworld有可能被拆分了,分段到達客戶端),例如 hello???+?? world,一個分段就是一個包(Package),這個就是分包問題。
?
2. socket在傳輸過成功,不同時間發(fā)送的數(shù)據(jù)包有可能被合并,同時到達了客戶端,這個就是黏包問題。例如發(fā)送方發(fā)送了hello+world,而接收方可能一次就接受了helloworld.
?
3. socket會自動在每個包后面補n個 0x0 byte,分割包。具體怎么去補,這個我就沒有深入了解。
?
4. 不同的數(shù)據(jù)類型轉(zhuǎn)化為byte的長度是不同的,例如int轉(zhuǎn)為byte是4位(int32),這樣我們在制作socket協(xié)議的時候要特別小心了。具體可以使用以下代碼去測試:
代碼 <!--Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->????????public?void?test()
????????{
????????????int?myInt?=?1;
????????????byte[]?bytes?=?new?byte[1024];
????????????BinaryWriter?writer?=?new?BinaryWriter(new?MemoryStream(bytes));
????????????writer.Write(myInt);
????????????writer.Write("j");
????????????writer.Close();
????????}
?
?
盡管socket環(huán)境如此惡劣,但是TCP的鏈接也至少保證了:
- 包發(fā)送順序在傳輸過程中是不會改變的,例如發(fā)送方發(fā)送 H E L L,那么接收方一定也是順序收到H E L L,這個是TCP協(xié)議承諾的,因此這點成為我們解決分包、黏包問題的關(guān)鍵。
- 如果發(fā)送方發(fā)送的是helloworld, 傳輸過程中分割成為hello+world,那么TCP保證了在hello與world之間沒有其他的byte。但是不能保證helloworld和下一個命令之間沒有其他的byte。
?
因此,如果我們要使用socket編程,就一定要編寫自己的協(xié)議。目前業(yè)界主要采取的協(xié)議定義方式是:包頭+包體長度+包體。具體如下:
?
1. 一般包頭使用一個int定義,例如int = 173173173;作用是區(qū)分每一個有效的數(shù)據(jù)包,因此我們的服務(wù)器可以通過這個int去切割、合并包,組裝出完整的傳輸協(xié)議。有人使用回車字符去分割包體,例如常見的SMTP/POP協(xié)議,這種做法在特定的協(xié)議是沒有問題的,可是如果我們傳輸?shù)男畔?nèi)容自帶了回車字符串,那么就糟糕了。所以在設(shè)計協(xié)議的時候要特別小心。
?
2. 包體長度使用一個int定義,這個長度表示包體所占的比特流長度,用于服務(wù)器正確讀取并分割出包。
?
3. 包體就是自定義的一些協(xié)議內(nèi)容,例如是對像序列化的內(nèi)容(現(xiàn)有的系統(tǒng)已經(jīng)很常見了,使用對象序列化、反序列化能夠極大簡化開發(fā)流程,等版本穩(wěn)定后再轉(zhuǎn)入手工壓入byte操作)。
?
一個實際編寫的例子:比如我要傳輸2個整型 int = 1, int = 2,那么實際傳輸?shù)臄?shù)據(jù)包如下:
?? 173173173?????????????? 8????????????????? 1?????????2
|------包頭------|----包體長度----|--------包體--------|
這個數(shù)據(jù)包就是4個整型,總長度 = 4*4? = 16。
?
說說我走的彎路:
我曾經(jīng)偷懶,使用特殊結(jié)束符去分割包體,這樣傳輸?shù)臄?shù)據(jù)包就不需要指名長度了。可是后來高人告訴我,如果使用特殊結(jié)束符去判斷包,性能會損失很大,因為我們每次讀取一個byte,都要做一次if判斷,這個性能損失是非常嚴重的。所以最終還是走主流,使用以上的結(jié)構(gòu)體。
?
?
------------------
Socket接收的邏輯概述
------------------
針對了我們的數(shù)據(jù)包設(shè)計+socket的傳輸特點,我們的接收邏輯主要是:
1. 尋找包頭。這個包頭就是一個int整型。但是寫代碼的時候要非常注意,一個int實際上占據(jù)了4個byte,而可悲的是這4個byte在傳輸過程中也可能被socket 分割了,因此讀取判斷的邏輯是:
- 判斷剩余長度是否大于4
- 讀取一個int,判斷是否包頭,如果是就跳出循環(huán)。
- 如果不是包頭,則倒退3個byte,回到第一點。
- 如果讀取完畢也沒有找到,則有可能包頭被分割了,因此當前已讀信息壓入接收緩存,等待下一個包到達后合并判斷。
2. 讀取包體長度。由于長度也是一個int,因此判斷的時候也要小心,同上。
3. 讀取包體,由于已知包體長度,因此讀取包體就變得非常簡單了,只要一直讀取到長度未知,剩余的又回到第一條尋找包頭。
?
這個邏輯不要小看,就這點東西忙了我1天時間。而非常奇怪的是,我發(fā)現(xiàn)c#寫的socket,似乎沒有我說的這么復雜邏輯。大家可以看看LumaQQ.net / DotMsn等,他們的socket接收代碼都非常簡單。我猜想:要么是.net的socket進行了優(yōu)化,不會對int之類的進行分割傳輸;要么就是作者偷懶,隨便寫點代碼開源糊弄一下。
?
------------------
Socket服務(wù)器參數(shù)概述
------------------
我在開篇也說了,Socket服務(wù)器的環(huán)境是非常糟糕了,最糟糕的就是客戶端斷線之后服務(wù)器沒有收到通知。 因為socket斷線這個也是個信息,也要從客戶端傳遞到我們socket服務(wù)器。有可能網(wǎng)絡(luò)阻塞了,導致服務(wù)器連斷開的通知都沒有收到。
因此,我們寫socket服務(wù)器,就要面對2個環(huán)境:
1. 服務(wù)器在處理業(yè)務(wù)邏輯中的任何時候都會收到Exception, 任何時候都會因為鏈接中斷而斷開。
2. 服務(wù)器接收到的客戶端請求可以是任意字符串,因此在處理業(yè)務(wù)邏輯的時候,必須對各種可能的輸入都判斷,防止惡意攻擊。
?
針對以上幾點,我們的服務(wù)器設(shè)計必須包含以下參數(shù):
1. 客戶端鏈接時間記錄:主要判斷客戶端空連接情況,防止連接數(shù)被惡意占用。
2. 客戶端請求頻率記錄:要防止客戶端頻繁發(fā)送請求導致服務(wù)器負荷過重。
3. 客戶端錯誤記錄:一次錯誤可能導致服務(wù)器產(chǎn)生一次exception,而這個性能損耗是非常嚴重的,因此要嚴格監(jiān)控客戶端的發(fā)送協(xié)議錯誤情況。
4. 客戶端發(fā)送信息長度記錄:有可能客戶端惡意發(fā)送非常長的信息,導致服務(wù)器處理內(nèi)存爆滿,直接導致宕機。
?
5. 客戶端短時間暴漲:有可能在短時間內(nèi),客戶端突然發(fā)送海量數(shù)據(jù),直接導致服務(wù)器宕機。因此我們必須有對服務(wù)器負荷進行監(jiān)控,一旦發(fā)現(xiàn)負荷過重,直接對請求的socket返回處理失敗,例如我們常見的“404”。
?
6. 服務(wù)器短時間發(fā)送信息激增:有可能在服務(wù)器內(nèi)部處理邏輯中,突然產(chǎn)生了海量的數(shù)據(jù)需要發(fā)送,例如游戲中的“群發(fā)”;因此必須對發(fā)送進行隊列緩存,然后進行合并發(fā)送,減輕socket的負荷。
?
?
------------------
后記
------------------
本文從架構(gòu)設(shè)計分析了一個socket服務(wù)器的設(shè)計要點。如果您有其他見解,歡迎留言與討論。
我們的最新動態(tài) (Bamboo@pixysoft.net)
-
1.解決comet在多頁面中沖突的問題.[2011-1-23]
-
2.優(yōu)化部分后臺邏輯代碼.提升首頁訪問性能[2011-1-20]
-
3.網(wǎng)絡(luò)Comet平臺再次成功對接上線.[2011-1-9]
總結(jié)
- 上一篇: 网站SEO优化中内部链接的优化
- 下一篇: char a[]和char *a的比较,