[轉載]TCP Socket服務器編程,粘包

轉:開發了這麼多年,發現最困難的程序開發就是通信系統。html

 

其餘大部分系統,例如CRM/CMS/權限框架/MIS之類的,不管怎麼複雜,基本上都可以本地代碼本地調試,性能也不過重要。(也許這個就是.net的企業級開發的戰略吧)java

 

但是來到通信系統,一切變得困難複雜。緣由實在太多了,如:web

  • 性能永遠是第一位:有時候一個if判斷都要考慮性能,畢竟要損耗一個CPU指令,而在通信系統服務器,每秒鐘都產生上百萬級別的通信量,這樣一個if就浪費了1個毫秒了。
  • 系統環境極其惡劣:全部咱們能夠想象的惡意攻擊、異常輸入等都要考慮;
  • 網絡說斷就斷:在socket環境下,客戶端能夠以各類理由斷開連接,並且服務器根本不會知道,連一個流水做業的業務邏輯都沒法保證正常執行,所以須要設計各類輔助的協議、架構去監督。
  • 各類網絡連接問題:例如代理、防火牆等等。。。

通過了1年的跌跌撞撞,我總算收穫了點有用的經驗,本文先從設計角度介紹一些我在Socket編程中的經驗,下一篇在放出源代碼。編程

 

------------------c#

現有的Socket編程資源緩存

------------------服務器

1. 首選推薦開源的XMPP框架,也就是Google的Gtalk的開源版本。裏面的架構寫的很是漂亮。特色就是:簡潔、清晰。網絡

 

2. 其次推薦LumaQQ.net,這套框架自己寫的通常般,可是騰訊的服務器很是的猛,這樣必然致使客戶端也要比較猛。經過學習這套框架,可以瞭解騰訊的IM傳輸協議設計,並且他們的協議是TCP/UDP結合,一箭雙鵰。架構

 

3. 最後就是DotMsn。這個寫的實在很通常般,並且也主要針對了MSN的協議特色。是可以學習到一點點的框架知識的,不過要有所鑑別。併發

 

------------------

Socket的選擇

------------------

在Java,到了Java5終於出現了異步編程,NIO,因而各類所謂的框架冒了出來,例如MINA, xsocket等等;而在.NET,微軟一早就爲咱們準備好了完善的Socket模型。主要包括:同步Socket、異步Socket;我還據說 了.net 3.x以後,異步的Socket內置了完成端口。綜合各類模型的性能,我總結以下:

 

1. 若是是短連接,使用同步socket。例如http服務器、轉接服務器等等。

 

2. 若是是長連接,使用異步socket。例如通信系統(QQ / Fetion)、webgame等。

 

3. .net的異步socket的鏈接數性能在 7500/s(每秒併發7500個socket連接)。而據說完成端口在1.5w全部。可是我到目前尚未正式見過所謂的完成端口,不知道到底有多牛逼。

 

4. 我據說了java的NIO性能在5000/s全部,咱們項目內部也進行了連接測試,在4000~5000比較穩定,固然若是代碼調優以後,能提升一點點。

 

------------------

TCP Socket協議定義

------------------

本文從這裏開始,主要介紹TCP的socket編程。

新手們(例如當初的我),第一次寫socket,老是覺得在發送方壓入一個"Helloworld",接收方收到了這個字符串,就「精通」了Socket編程了。而實際上,這種編程根本不可能用在現實項目,由於:

 

1. socket在傳輸過程當中,helloworld有可能被拆分了,分段到達客戶端),例如 hello   +   world,一個分段就是一個包(Package),這個就是分包問題

 

2. socket在傳輸過成功,不一樣時間發送的數據包有可能被合併,同時到達了客戶端,這個就是黏包問題。例如發送方發送了hello+world,而接收方可能一次就接受了helloworld.

 

3. socket會自動在每一個包後面補n個 0x0 byte,分割包。具體怎麼去補,這個我就沒有深刻了解。

 

4. 不一樣的數據類型轉化爲byte的長度是不一樣的,例如int轉爲byte是4位(int32),這樣咱們在製做socket協議的時候要特別當心了。具體可使用如下代碼去測試:

代碼

 

 

儘管socket環境如此惡劣,可是TCP的連接也至少保證了:

  • 包發送順序在傳輸過程當中是不會改變的,例如發送方發送 H E L L,那麼接收方必定也是順序收到H E L L,這個是TCP協議承諾的,所以這點成爲咱們解決分包、黏包問題的關鍵。
  • 若是發送方發送的是helloworld, 傳輸過程當中分割成爲hello+world,那麼TCP保證了在hello與world之間沒有其餘的byte。可是不能保證helloworld和下一個命令之間沒有其餘的byte。

 

所以,若是咱們要使用socket編程,就必定要編寫本身的協議。目前業界主要採起的協議定義方式是:包頭+包體長度+包體。具體以下:

 

1. 通常包頭使用一個int定義,例如int = 173173173;做用是區分每個有效的數據包,所以咱們的服務器能夠經過這個int去切割、合併包,組裝出完整的傳輸協議。有人使用回車字符去分割 包體,例如常見的SMTP/POP協議,這種作法在特定的協議是沒有問題的,但是若是咱們傳輸的信息內容自帶了回車字符串,那麼就糟糕了。因此在設計協議 的時候要特別當心。

 

2. 包體長度使用一個int定義,這個長度表示包體所佔的比特流長度,用於服務器正確讀取並分割出包。

 

3. 包體就是自定義的一些協議內容,例如是對像序列化的內容(現有的系統已經很常見了,使用對象序列化、反序列化可以極大簡化開發流程,等版本穩定後再轉入手工壓入byte操做)。

 

一個實際編寫的例子:好比我要傳輸2個整型 int = 1, int = 2,那麼實際傳輸的數據包以下:

   173173173               8                  1         2

|------包頭------|----包體長度----|--------包體--------|

這個數據包就是4個整型,總長度 = 4*4  = 16。

 

說說我走的彎路:

我曾經偷懶,使用特殊結束符去分割包體,這樣傳輸的數據包就不須要指名長度了。但是後來高人告訴我,若是使用特殊結束符去判斷包,性能會損失很大,由於咱們每次讀取一個byte,都要作一次if判斷,這個性能損失是很是嚴重的。因此最終仍是走主流,使用以上的結構體。

 

 

------------------

Socket接收的邏輯概述

------------------

針對了咱們的數據包設計+socket的傳輸特色,咱們的接收邏輯主要是:

1. 尋找包頭。這個包頭就是一個int整型。可是寫代碼的時候要很是注意,一個int實際上佔據了4個byte,而可悲的是這4個byte在傳輸過程當中也可能被socket 分割了,所以讀取判斷的邏輯是:

  • 判斷剩餘長度是否大於4
  • 讀取一個int,判斷是否包頭,若是是就跳出循環。
  • 若是不是包頭,則倒退3個byte,回到第一點。
  • 若是讀取完畢也沒有找到,則有可能包頭被分割了,所以當前已讀信息壓入接收緩存,等待下一個包到達後合併判斷。

2. 讀取包體長度。因爲長度也是一個int,所以判斷的時候也要當心,同上。

3. 讀取包體,因爲已知包體長度,所以讀取包體就變得很是簡單了,只要一直讀取到長度未知,剩餘的又回到第一條尋找包頭。

 

這個邏輯不要小看,就這點東西忙了我1天時間。而很是奇怪的是,我發現c#寫的socket,彷佛沒有我說的這麼複雜邏輯。你們能夠看看 LumaQQ.net / DotMsn等,他們的socket接收代碼都很是簡單。我猜測:要麼是.net的socket進行了優化,不會對int之類的進行分割傳輸;要麼就是做 者偷懶,隨便寫點代碼開源糊弄一下。

 

------------------

Socket服務器參數概述

------------------

我在開篇也說了,Socket服務器的環境是很是糟糕了,最糟糕的就是客戶端斷線以後服務器沒有收到通知。 由於socket斷線這個也是個信息,也要從客戶端傳遞到咱們socket服務器。有可能網絡阻塞了,致使服務器連斷開的通知都沒有收到。

所以,咱們寫socket服務器,就要面對2個環境:

1. 服務器在處理業務邏輯中的任什麼時候候都會收到Exception, 任什麼時候候都會由於連接中斷而斷開。

2. 服務器接收到的客戶端請求能夠是任意字符串,所以在處理業務邏輯的時候,必須對各類可能的輸入都判斷,防止惡意攻擊。

 

針對以上幾點,咱們的服務器設計必須包含如下參數:

1. 客戶端連接時間記錄:主要判斷客戶端空鏈接狀況,防止鏈接數被惡意佔用。

2. 客戶端請求頻率記錄:要防止客戶端頻繁發送請求致使服務器負荷太重。

3. 客戶端錯誤記錄:一次錯誤可能致使服務器產生一次exception,而這個性能損耗是很是嚴重的,所以要嚴格監控客戶端的發送協議錯誤狀況。

4. 客戶端發送信息長度記錄:有可能客戶端惡意發送很是長的信息,致使服務器處理內存爆滿,直接致使宕機。

 

5. 客戶端短期暴漲:有可能在短期內,客戶端忽然發送海量數據,直接致使服務器宕機。所以咱們必須有對服務器負荷進行監控,一旦發現負荷太重,直接對請求的socket返回處理失敗,例如咱們常見的「404」。

 

6. 服務器短期發送信息激增:有可能在服務器內部處理邏輯中,忽然產生了海量的數據須要發送,例如遊戲中的「羣發」;所以必須對發送進行隊列緩存,而後進行合併發送,減輕socket的負荷。

相關文章
相關標籤/搜索