轉:開發了這麼多年,發現最困難的程序開發就是通信系統。html
其餘大部分系統,例如CRM/CMS/權限框架/MIS之類的,不管怎麼複雜,基本上都可以本地代碼本地調試,性能也不過重要。(也許這個就是.net的企業級開發的戰略吧)java
但是來到通信系統,一切變得困難複雜。緣由實在太多了,如:web
通過了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的連接也至少保證了:
所以,若是咱們要使用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 分割了,所以讀取判斷的邏輯是:
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的負荷。