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協議的時候要特別小心了。具體可以使用以下代碼去測試:


代碼 
        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環境如此惡劣,但是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的負荷。


 


 


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


後記


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


本文從架構設計分析了一個socket服務器的設計要點。如果您有其他見解,歡迎留言與討論。
本文轉載自:http://wenku.baidu.com/link?url=mL2XFLQeLU1btWRynwCp2XDHFThaZeqrvj05OZCsSJHEg6icYGcZbhi3YbpGAZOQ_VqoXmupx8vRvTHd3ZTIhSiyqHcMw4cWjeJjYRDvDpq###



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章