高性能、高可用性Socket通訊庫介紹 - 采用完成端口、歷時多年調優!(附文件傳輸程序)


前言 本人從事編程開發十余年,因為工作關系,很早就接觸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.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM