前言 本人從事編程開發十余年,因為工作關系,很早就接觸socket通訊編程。常言道:人在壓力下,才可能出非凡的成果。我從事的幾個項目都涉及到通訊,為我研究通訊提供了平台,也帶來了動力。處理socket通訊對初學者而言,具有很大的挑戰性。我有個夢想:能不能開發一套系統,能很好的實現性能和易用性的統一。高性能socket采用iocp(完成端口)是唯一選擇。iocp像一匹烈馬,雖然性能優良,但不宜馴服。本套系統為這匹烈馬套上了枷鎖,讓他變得溫順;但是,當你需要他時,又能迸發出強勁的動力。本文就介紹該系統如何實現易用性和高性能的統一。
此庫的特點:高性能與易用性完美統一;全部自主編碼,反復測試,盡最大程度做到了bug free。
如果你的系統需要高性能網絡通信,可以聯系我。根據你的系統特點定制開發。
點擊下載執行程序 或加QQ群:877371250.
系統簡介
1 系統采用c#,可以在.net core平台編譯通過。所以可運行在windows、linux平台。
2 系統有兩個模塊組成IocpCore,EasyNetMessage。IocpCore對完成端口進行了封裝,EasyNetMessage在IocpCore基礎上進一步封裝,實現了易用性。可在EasyNetMessage基礎上,進一步擴展,實現分布式系統(類似WCF)。
3 系統只實現了TCP通訊,秉承simple is best的理念,不為過於冗余的功能干擾。
4 系統突出專業性(professional)。為了測試穩定性,開發了專門的測試程序,反復對系統蹂躪,檢驗系統的穩定性。為了測試性能,做了精確計時,檢驗每個功能點的效率。
網上也有很多第三方網絡庫,好像沒有必要再另起爐灶。但這些庫大部分無法滿足專業性、易用性要求。通過對系統API封裝,可以完全了解底層特性,由於所有代碼都是自己親自編寫,做到了心中有數,對所有代碼了然於心。即使系統出現bug,也可以很快解決。
性能指標
Iocp是可擴展性通訊模型,就是不隨着連接數增加而導致性能下降。所支持的連接數只與平台硬件有關。本系統保守估計可以支持10萬個連接。普通平台下,可以滿足千兆網傳輸需求。
設計思路
如果網絡庫可以用到各種場景,所處理的邏輯必須與業務無關。系統采用分層處理,底層處理字節流的收發,完全與業務無關。底層的目標就是收發速度足夠快。再上一層,就是對完整的數據包處理,處理的關鍵是如何將數據流分割成完整的數據包。再向上就是應用層,將收到的數據包轉換成類,上層只需對c#類處理,不用關心底層細節。
IocpCore 模塊介紹
本模塊對iocp封裝,充分挖掘iocp的潛質;可以處理字節流也可以處理一個完整的包。
對外接口:
public class SocketEventParam { public EN_SocketEvent SocketEvent; public SocketClientInfo ClientInfo; public Socket Socket; public byte[] Data { get; set; } public SocketEventParam(EN_SocketEvent socketEvent, Socket socket) { SocketEvent = socketEvent; Socket = socket; } } public enum EN_SocketEvent { connect, accept, close, read, send, packetLenError }
程序接口非常簡單就只有一個類。這個類對socket事件做了封裝,就是告訴你socket 連接、關閉、讀取這些事件。不需要關心任何底層的細節,所以使用起來非常簡單。
使用舉例
NetServer _netServer; _netServer = new NetServer(this, 100); _netServer.OnSocketPacketEvent += SocketPacketEvent; _netServer.AddListenPort(5668, 1); private void DealPacket(SocketEventParam socketParam) { if (socketParam.SocketEvent == EN_SocketEvent.read) { } else if (socketParam.SocketEvent == EN_SocketEvent.accept) { } else if (socketParam.SocketEvent == EN_SocketEvent.close) { } }
內部處理及優化說明
1 可以應對突發大數據量連接
每秒可以起送應對幾千個客戶端連接。接收對方監聽采用AcceptAsync,也是異步操作。有單獨的線程負責處理Accept。
int MaxAcceptInPool = 20; private void DealNewAccept() { try { if (_acceptAsyncCount <= MaxAcceptInPool) { StartAccept(); } while (true) { AsyncSocketClient client = _newSocketClientList.GetObj(); if (client == null) break; DealNewAccept(client); } } catch (Exception ex) { _log.LogException(0, "DealNewAccept 異常", ex); } }
線程會同時投遞多個AcceptAsync,就是已經建立好多個socket,等待客戶端連接。當客戶端到達時,可以迅速生成可用socket。
2 接收優化
當收到接收完成消息后,立即投遞下一次接收操作,再處理接收的數據。這樣可以提高數據處理的實時性。
private void ReceiveEventArgs_Completed(object sender, SocketAsyncEventArgs readArgs) { try { bool readError = false; lock (_readLock) { _inReadPending = false; if (readArgs.BytesTransferred > 0 && readArgs.SocketError == SocketError.Success) { //加入到緩沖中 AddToReadList(readArgs.BufferList, readArgs.BytesTransferred); readArgs.BufferList = null; } else { readError = true; } } if (IsSocketError || readError) { OnReadError(); } else { TryReadData(); } } catch (Exception ex) { _log.LogException(0, "ReceiveEventArgs_Completed", ex); } } internal int TryReadData() { int readCount = 0; while (true) { EN_SocketReadResult result = ReadNextData(); if (result != EN_SocketReadResult.ReadError) readCount++; if (result == EN_SocketReadResult.HaveRead) continue; else { break; } } ProcessReadData(); return readCount; }
3 發送優化
發送時,將數據先放到發送緩沖。在對多個可發送數據,一次性發送。SocketAsyncEventArgs類中有屬性public IList<ArraySegment<byte>> BufferList { get; set; },可以將多個發送buffer放入該列表,一次性發送走。
EasyNetMessage模塊介紹
1 對外接口
public enum EasyNetEvent { connect, accept, close, read, send, connectError = 100, } public class EasyNetParam { public EasyNetEvent NetEvent { get; set; } public SocketClientInfo ClientInfo { get; set; } public Socket Socket { get; set; } public NetPacket Packet { get; set; } }
這個接口和IocpCore有些類似。主要的區別是 public NetPacket Packet { get; set; }。NetPacket包含的不再是字節流,而是封裝好的類。用戶不必再處理容易出錯的字節流。當然,客戶端和服務器都必須使用EasyNetMessage才可以。
NetPacket使用說明
客戶端和服務器之間傳輸的是NetPacket類,完全忽略底層細節。客戶端構造一個NetPacket,在服務端會收到一個完全一樣的NetPacket。以發送文件為例:
--->發送端 NetPacket netPacket = new NetPacket(); netPacket.AddInt("packetType", 1); //包類型 netPacket.AddString("fileName", fileName);//文件名字 netPacket.AddInt("sendIndex", fileIndex); //文件塊序列號 netPacket.AddInt("sendOver", 0); //是否發送完標志 netPacket.AddBuffer("fileData", readData);//文件數據 <---接收端 private void DealFileRcv(NetPacket netPacket) { FileRcvInfo info = new FileRcvInfo(); info.IsSendOver = netPacket.GetInt("sendOver") == 1; info.FileName = netPacket.GetString("fileName"); info.SendIndex = netPacket.GetInt("sendIndex").Value; info.FileData = netPacket.GetBuffer("fileData"); } }
以上只是使用NetPacket一個簡單的例子。使用EasyNetMessage,短時間內可以開發出一個高性能的文件傳輸系統。
NetPacket詳細定義
public class NetPacket { public NetPacket(); public List<NetValuePair> Items { get; set; } public int Param1 { get; set; } public int PacketType { get; set; } public int Param2 { get; set; } public void AddBuffer(string key, byte[] value); public void AddByte(string key, byte value); public void AddInt(string key, int value); public void AddListInt(string key, List<int> value); public void AddListLong(string key, List<long> value); public void AddListString(string key, List<string> listValue); public void AddLong(string key, long value); public void AddString(string key, string name); public List<KeyBuffer> GetAllBuffer(); public List<KeyString> GetAllString(); public byte[] GetBuffer(string key); public List<byte[]> GetBufferOfSameKey(string key); public byte? GetByte(string key); public List<byte> GetByteOfSameKey(string key); public int? GetInt(string key); public List<int> GetIntOfSameKey(string key); public List<int> GetListInt(string key); public List<long> GetListLong(string key); public List<string> GetListString(string key, int startIndex = 0); public long? GetLong(string key); public List<long> GetLongOfSameKey(string key); public string GetString(string key); public List<string> GetStringOfSameKey(string key); }
NetPacket中數據采用key、value的方式存儲。可以存儲int,string,List<int>,List<string>,byte[]等類型,可以滿足多種應用場景。
處理邏輯說明
處理的重點是NetPacket的序列化和反序列化。將NetPacket序列化為多個內存塊,而不是序列化為一個單獨的內存塊。這樣做意義就是:當NetPacket包含大量的數據(比如幾百兆),如果只序列化為一個內存塊,則需要系統分配連續的幾百兆內存,這樣很可能導致分配失敗;序列化為多個小內存塊就可以防止這種問題,所以NetPacket一次可以傳輸大量數據,而不用擔心系統是否可以分配連續的大內存塊。
性能驗證測試
前文剖析了系統內部處理邏輯,系統的性能還需要現實檢驗。任何成功都不是一蹴而就,為了追求性能的極致,對系統做了多次優化,才達到了滿意的效果。
系統的性能有兩個指標:傳輸量、響應時間。響應時間指的是:數據發送到對端,再從對端返回的時長。傳輸量、響應時間這兩個指標有關聯,而又不完全一樣。很多系統傳輸量大非常大,但是響應不夠及時。響應及時是開發遠程過程調用的基礎,是更高一個層次的要求。這里主要測試響應時間。
主要測試數據發送到對方,再從對方返回數據所用時長。因為條件所限,客戶端與服務器都在同一台機器上。
測試平台: i5第4代cpu;
1) 50個字節數據發送
平均時間小於1毫秒,也就是說每秒可以執行1000次函數調用。
2)1K字節數據收發
和50字節調用差別不大。
3)100K 字節數據收發
響應時間大概為12毫秒,每秒可以執行80次調用。
4)10000K字節數據收發 (接近10M數據)
時間剛超過1秒。這是10M數據發送,再接收的時間。相當於占用200M帶寬。
響應時間測試總結:小數據量,基本可以達到每秒1000次函數調用。10M的數據收發剛剛超過1秒。注意這還不能完全反應網絡層處理的能力。因為這是單個線程調用,如果多個線程同時調用,可以達到更高的調用次數。
傳輸量測試
我使用c++寫的模擬程序,對該系統測試。收發數據總計超過50M,暨占用500M帶寬,cpu占用率23%。測試平台為筆記本,硬件配置比較低。如果采用高性能服務器,達到千兆帶寬傳輸,cpu占用也不會很高。
總結:筆者從事軟件開發多年,對於socket通訊編程非常有經驗。 一個好的通訊模塊有很多指標,比如:復用性高、耦合性低、性能高、易用性好;本系統在設計時就綜合考慮了這些要求。對於如何設計好通訊層,我進行了很多思考,將其付諸於代碼;公司的多款產品通訊層就是采用該系統,該系統經過了實踐的檢驗,完全滿足了多個產品的要求。
當然,一款產品在任何條件都是最優的,這很難做到。網絡層亦是如此。根據上層數據收發的特點,來調整網絡層的一些配置參數,這樣才能達到最優。如果你公司的產品需要高性能網絡層做支持,可以聯系我。我會根據產品的特點做優化。
下載執行程序加QQ群:877371250.