Socket編程技術

1.基本原理

本文記錄對Socket通訊技術的彙總,現在想對.NET/C#程序員說:想要掌握異步Socket通訊技術,首先應該掌握C#語言裏的異步編程,然後再學習Socket可能會容易理解,這裏有特別強調了異步Socket通訊,因爲當下生產環境基本上沒人再使用同步實現了。本文主要記錄TCP/IP協議的Socket通訊,不包括UDP協議的Socket通訊。

1.1.I/O完成端口(IOCP)

IOCP全稱I/O Completion Port,它是Windows平臺下異步通訊實現的方式之一,相對於其他幾種實現機制,它是高效的、使用簡單的,但是內部實現是複雜的。在Windows平臺下想要打造一款高性能服務端程序,沒有比它更合適了。它特別適合C/S模式網絡服務器端模型,在處理大量用戶併發請求時,如果採用一個用戶對一個線程的方式,那麼將造成CPU在這成千上萬的線程間來回切換,後果不堪設想。而IOCP不會爲每一個用戶創建一個線程。

IOCP模型包含三部分:完成端口(存放重疊I/O請求),客戶端請求處理,工作線程,首先解釋一下幾個概念

  1. 重疊結構Overlapped:它是一個很重要的I/O數據結構,Windows裏所有的異步通信實現都是基於它。
    可以把它理解成爲一個網絡操作的ID,我們利用重疊I/O提供的異步機制,每一個網絡操作都要有一個唯一的ID,因爲進入系統內核後,就由系統內核控制了,在外面不清楚裏面在做什麼,系統內核一看到有重疊I/O的調用進來,就會使用其異步機制,並且操作系統就只能靠這個重疊結構帶有的ID來區分是哪一個網絡操作,然後內核裏面處理完畢後,根據這個ID繼續操作。
    至於爲什麼會叫重疊結構,其作者解釋是因爲“執行I/O請求的時間與線程執行其他任務的時間是重疊的”。
  2. 完成端口:有人說叫它“完成隊列”更合適,因爲這裏的端口和我們平時所說的網絡通訊中的端口完全不是一個東西,實際上IOCP對象內部有一個先進先出隊列(簡稱消息隊列)。它之所以叫“完成”端口,就是說系統會在網絡I/O操作“完成”之後纔會通知我們,即我們在接收到系統通知的時候,其實網絡操作已經完成了。
  3. 工作線程:它是專門用來和客戶端進行通信的,而且工作線程的數量要等於系統中CPU的數量(CPU的核心數)。但是實踐證實最好建立CPU核心數*2數量的工作線程,這樣便可以充分利用CPU資源,因爲工作線程有時會出現Sleep()等情況,此時同一個CPU核心上的另一個線程就可以代替這個Sleep的線程執行了。

通常情況下,我們會用線程池維護工作線程,一個IOCP對象,在操作系統中可關聯着多個Socket或文件控制端。IOCP對象內部的消息隊列,用於存放IOCP所關聯的I/O服務請求完成消息。請求I/O服務的進程不接受I/O服務完成通知,而是檢查IOCP的消息隊列以確定I/O請求的狀態。IOCP隊列中的請求完成後,應用程序會收到通知。工作線程負責從IOCP消息隊列中取走完成通知並執行數據處理。如果隊列中沒有消息,那麼線程阻塞掛起在該隊列,不佔用CPU週期,工作線程從而實現負載均衡。

IOCP模型之所以是Windows平臺下C/S通信模式中性能最好的網絡通訊模型,是因爲它充分利用Windows內核來進行I/O的調度。它實現高性能、高併發可以概括爲以下三點:

  1. 採用異步I/O操作,彌補了同步操作線程阻塞耗時的缺點。
  2. 採用線程池進行處理,減少了線程創建、上下文切換佔用過多系統資源的問題。
  3. 採用重疊I/O技術,幫助維持可以重複使用內存池。

1.2.通信原理

1.2.1.Socket模式

Socket技術有兩種編程模式:同步模式和異步模式,我們常常把它們混淆爲阻塞和非阻塞,前者是指通信模式,後者則指是否等待I/O操作完成。I/O操作相對於CPU的執行效率猶如老牛破車,I/O操作主要指網絡I/O和存儲設備I/O。

同步模式是最基本的Socket編程模式,正如其名,同步模式的Socket在執行耗時的I/O操作時,會阻塞當前線程,等待I/O操作完成返回結果,否則停止向下執行。

異步模式是相對於同步模式定義的,它在執行過程中,遭遇耗時的I/O操作時,不會阻塞當前線程,它會向系統委託一個異步過程,然後繼續向下執行,當系統接收到I/O操作完成的消息後,系統會自動觸發委託的異步過程,從而完成一個完整的流程。

強調一個概念,Socket編程的同步和異步,與線程間的同步不是一個概念,線程間的同步指不同線程具有先後關聯關係,而Socket同步和異步指兩種不同的工作方式。

1.2.2.Socket原理

 

 

 

 

 

 

 

 

 

 

 

編寫網絡通信程序,首先想到的應該是OSI七層協議模型,或者是TCP/IP五層協議模型,不過無論是OSI7還是TCP/IP5都不存在Socket這個概念?

如上圖所示,Socket是基於TCP/IP協議族封裝的一套標準規範的、可調用的接口,它介於應用層與運輸層中間,由於TCP/IP協議族的概念和實現過於複雜,所以就將它們抽象接口化,抽象接口與其協議一一對應,最後開發者只需要調用接口即可使用。

由於Socket是在TCP/IP協議族上工作的,所以要想實現和靈活運用這門技術,必須要理解TCP/IP協議是如何通訊的,即TCP/IP協議三次握手建立連接,和四次握手釋放連接,如下圖所示:

1.3.通信程序

 

上圖內容展示了socket客戶端和服務端程序通信建立的步驟和過程。

1.3.1.服務端

  1. 創建套接字:new Socket()
    查看C#中Socket類的構造函數提供了三種創建方式,主要分析常用的、傳遞三個參數的構造函數
    函數簽名:public Socket(AddressFamily af, SocketType st, ProtocolType pt);
    參數說明:
    1. AddressFamily:地址族,常用的地址族有InterNetwork、InterNetworkV6、Unix,它決定了socket的地址類型,在通信中必須採用對應的地址。
    2. SocketType:類型,常用的類型有Stream、Dgram、Raw。
    3. ProtocolType:協議類型,常用的協議有TCP、UDP、ICMP。

  2. 指定本地地址:socket.Bind()
    方法簽名:public void Bind(EndPoint localEP);
    參數說明:此方法傳遞EndPoint對象,並沒有給傳遞字符串的機會,避開了傳遞IP地址時可能會出現的大小端的問題。

  3. 監聽連接:socket.Listen()
    方法簽名:public void Listen(int backlog);
    參數說明:backlog表示請求連接隊列的最大長度,用於限制排隊請求的個數,即最多允許多少個客戶端接入。

  4. 接收連接:socket.Accept()
    1. 在.NET/C#中,Socket編程之所以會有幾種實現方案,是因爲異步編程的發展。而不同實現方案的切入點,服務端就是從這裏開始的,所以這一步非常重要。
    2. 同步實現:public Socket Accept();
    3. 異步實現
      1. APM模式:就是通過“BeginXXX/EndXXX”方法對回調函數方式實現的,由於採用這種模式是會產生一個IAsyncResult託管對象,會帶來GC方面回收壓力,特別是消息收發時。這就不符合打造高性能服務器的要求,所以現在主要使用EAP模式的Socket通信。Socket類提供了三個不同參數的BeginAccept方法,不過大家還是習慣使用兩個參數的方法,其簽名如下:
               public IAsyncResult BeginAccept(AsyncCallback callback, object state);
      2. EAP模式:它是基於SocketAsyncEventArgs類實現Socket通信,所以要了解更多就去研究SocketAsyncEventArgs類,EAP模式對應的Accept()方法簽名是:
               public bool AcceptAsync(SocketAsyncEventArgs e);
      3. TAP模式:Socket對象不直接支持TAP模式,而是通過TcpListener提供TAP模式的實現。在.NET Core或.NET5+的環境裏支持TAP模式。

  5. 消息收發:socket.Send()/socket.Receive()
    1. 與Socket.Accept()方法相同,消息收發方法根據異步編程的發展提供了幾種實現方案。實現消息的收發是Socket通信的根本目標,所以這一步是核心。
    2. 同步實現:
      1.  發送消息方法:Socket類提供了8個參數不同的Send()方法,常用其中兩個方法
        1. 發送短消息:public int Send(byte[] buffer, SocketFlags socketFlags);
        2. 發送長消息:public int Send(byte[] buffer, int offset, int size, SocketFlags socketFlags);
      2. 接收消息方法:Socket類也提供了8個參數不同的Receive()方式,常用方法簽名:public int Receive(byte[] buffer);
    3. 異步實現:
      1. APM模式:
        1. 發送消息方法:Socket類提供了多種實現方法,常用方法簽名:
            public IAsyncResult BeginSend(byte[] b, int o, int s, SocketFlags sf, AsyncCallback cb, object state);
        2. 接收消息方法:Socket類提供了多種實現方法,常用方法簽名:
                 public IAsyncResult BeginReceive(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state);
      2. EAP模式:
        1. 發送消息方法簽名:public bool SendAsync(SocketAsyncEventArgs e);
        2. 接收消息方法簽名:public bool ReceiveAsync(SocketAsyncEventArgs e);
      3. TAP模式:Socket對象不直接支持TAP模式,而是通過TcpListener提供TAP模式的實現。在.NET Core或.NET5+的環境裏支持TAP模式。

  6. 關閉客戶端連接:socket.CloseClient()
    1. 此步驟並沒有在客戶端-服務端Socket通信流程圖中出現,主要爲了更好的介紹兩端本身,實際上本步驟非常重要,因爲服務端的資源非常昂貴、且需要提供可靠的穩定性服務。
    2. 在客戶端與服務端建立連接後,客戶端需要設計心跳功能,在沒有發送或接受消息時,讓服務端Socket知道,自己仍處於活躍狀態。否則服務端爲了充分節約計算機資源而釋放客戶端的連接。
    3. 客戶端與服務端建立的連接,還會因爲各種不可控因素(人爲、非人爲)斷開,所以一定要做好異常處理。否則會造成程序崩潰、服務不可用。
  7. 關閉套接字:socket.Close()
    執行該步驟前,應當優雅的關閉所有與客戶端建立的連接,避免消息丟失,以及造成客戶端程序異常、不可用。

1.3.2.客戶端

  1. 創建套接字:new Socket()
    查看C#中Socket類的構造函數提供了三種創建方式,主要分析常用的、傳遞三個參數的構造函數
    函數簽名:public Socket(AddressFamily af, SocketType st, ProtocolType pt);
    參數說明:
    1. AddressFamily:地址族,常用的地址族有InterNetwork、InterNetworkV6、Unix,它決定了socket的地址類型,在通信中必須採用對應的地址。
    2. SocketType:類型,常用的類型有Stream、Dgram、Raw。
    3. ProtocolType:協議類型,常用的協議有TCP、UDP、ICMP。
  2. 建立連接:socket.Connect()
    1. 服務端被動接收客戶端發起的建立Socket連接請求,所以此處與服務端不同。也是因爲受同步和異步編程的影響,也存在三種實現方案。
    2. 同步實現:Socket類提供了4種參數不同的Connect()方法。注意:程序執行此方法時,可能會出現異常,如果出現異常了,可能涉及到了“字節序”的概念,如有需要可看本文“基礎原理-擴展信息-字節序”部分內容。
    3. 異步實現
      1. APM模式:Socket類提供了4中參數不同的BeginConnect()方法
      2. EAP模式:它是基於SocketAsyncEventArgs類實現Socket通信,所以要了解更多就去研究SocketAsyncEventArgs類,EAP模式對應的Accept()方法簽名是:public bool ConnectAsync(SocketAsyncEventArgs e);
      3. TAP模式:Socket對象不直接支持TAP模式,而是通過TcpListener提供TAP模式的實現。在.NET Core或.NET5+的環境裏支持TAP模式。

  3. 消息收發:socket.Send()/socket.Receive()
    客戶端消息收發與服務端消息收發的實現沒有不同,所以此處不再贅述。

  4. 關閉套接字: socket.Close()
    對於基於Socket通信的程序來說,關閉Socket連接,表示關閉程序了,所以需要兩端都要釋放用戶申請的資源。

1.4.通信安全

在.NET/C#下,採用Socket+異步編程打造高性能程序,有三種方案可以選擇:APM、EAP和TAP,在上述內容中提到了採用EAP模式更好。但是EAP模式不支持消息的安全傳輸,即消息發送前或接收後SSL/TLS的加解密,這可能是個麻煩的事情,當然你可以自己對消息進行加解密實現,但終究不是通用的解決方案,特別是遭遇多方相互通信時。

鑑於EAP模式不具備安全傳輸消息的遺憾,因此,如果你要進行安全通信,就要選擇TAP模式了。

1.5.擴展信息

1.5.1.字節序

現在做socket編程時,基本上很少遭遇“字節序”問題,這源於UTF-8編碼的盛行。想要把字節序的概念搞清楚,需要花費一定的時間和精力查查歷史,以及學學涉及到的幾個其他概念,比如:字符編碼、大端小端、編程語言對大小端默認選擇、CPU對大小端默認選擇、字節序的種類等,有沒有出現恐慌的心理和情緒變化?不要慌、也不要激動,不懂不要緊、忘了也不要緊,接下來一個個說。

1.5.1.1.字符編碼

我們知道計算機只能識別010101這樣的字符串,字母、數字、漢字、圖片、音視頻等等最終都需要轉換成01字符串,當然需要制定轉換規則,否則就亂套了。計算機是老美髮明的,所以剛開始的時候就設計出了ASCII字符集,它的全稱American Standard Code for Information Interchange,中文名稱美國信息交換標準碼,它使用7 bits來表示一個字符,總共表示128個字符,我們一般都是用字節(byte,即8個01串)來作爲基本單位.那麼怎麼當用一個字節來表示字符時第一個bit總是0,剩下的七個字節就來表示實際內容。後來IBM公司在此基礎上進行了擴展,用8bit來表示一個字符,總共可以表示256個字符.也就是當第一個bit是0時仍表示之前那些常用的字符.當爲1時就表示其他補充的字符。

對於說英文的美國來說,256個字符是使用不完的,但是對於說其他語言的國家就不行了,就像我們的漢字有幾萬個字。於是就出現Unicode和ISO這樣的組織來統一制定一個標準,使得任何一個字符只對應一個確定的數字.ISO取名字叫UCS(Universal Character Set),Unicode取的名字就叫unicode。接下來就詳細說說unicode編碼。

  1. Unicode版本
    Unicode編碼有兩個版本。第一個版本規定用兩個字節來表示所有字符,總計可以表示65535個字符, 65535是2的16次方,所以常常會把Unicode編碼等同於UTF-16。
    後來在發現第一個版本65535不算多,要是加上特殊的字符就不夠了,於是從1996年開始制定了第二個版本。第二個版本規定用四個字節來表示所有字符,所以就出現了UTF-32。
    UTF-8是怎麼出現的?在制定Unicode第一個版本時,就發現英文一個字節就可以表示了,爲什麼要浪費兩個字節表示呢,所以就出現UTF-8。看到8/16/32,可能誤以爲8表示一個字節,不是的。
    再說一點,Unicode編碼規範是給所有字符指定一個唯一的數字,如何把數字轉換成01串保存到計算機中,就有不同的方式了,所以纔出現了UTF-8/UTF-16/UTF-32的出現。

  2. UTF-8/UTF-16
    當用UTF-8時表示一個字符是可變長度,有可能是用一個字節表示一個字符,也可能是兩個、三個、甚至是四個,反正是根據字符對應的數字大小來確定。
    舉例說明兩者的區別,假如中文字"漢"對應的unicode是6C49(十六進制,十進制是27721)
    1. UTF-16表示:就是01101100 01001001(共16 bit,兩個字節).程序解析的時候知道是UTF-16就把兩個字節當成一個單元來解析。
    2. UTF-8表示:就是1110xxxx 10xxxxxx 10xxxxxx(共24bit,三個字節)。
      換算關係:
                一個字節只能表示2的7次方128個字符
                     兩個字節只能表示2的11次方2048個字符
                     三個字節能表示2的16次方65536個字符
      因爲27721>2048,所以需要三個字節表示。所以開頭有三個數字111。
  3. 如何區分文本內容採用哪種編碼方式?
    1. EF BB BF    UTF-8
    2. FE FF     UTF-16/UCS-2, little endian
    3. FF FE     UTF-16/UCS-2, big endian
    4. FF FE 00 00  UTF-32/UCS-4, little endian
    5.  00 00 FE FF  UTF-32/UCS-4, big-endian

            根據上面的映射關係,在文件開頭處對照看看就知道了。

1.5.1.2.字節序種類

字節序有兩種類型:主機字節序 和 網絡字節序。

主機字節序:不同的CPU有不同的字節序類型,這些字節序是指整數在內存中保存的順序,這個叫做主機序。

網絡字節序:TCP/IP中規定好的一種數據表示格式,它與具體的CPU類型、操作系統等無關,從而可以保證數據在不同主機之間傳輸時能夠被正確解釋。網絡字節順序採用big-endian(大端)排序方式。

1.5.1.3.大端小端

  1. 大端小端的定義
    1. 大端字節序(Big-Endian,簡稱大端):就是高位字節排放在內存的低地址端,低位字節排放在內存的高地址端。
    2. 小端字節序(Little-Endian,簡稱小端):就是低位字節排放在內存的低地址端,高位字節排放在內存的高地址端。

  2. 如何更好的理解大端小端?
    1. 名稱由來
      1726年的Jonathan Swift的《格列佛遊記》,其中一篇講到有兩個國家因爲喫雞蛋究竟是先打破較大的一端還是先打破較小的一端而爭執不休,甚至爆發了戰爭。1981年10月,Danny Cohen的文章《論聖戰以及對和平的祈禱》(On holy wars and a plea for peace)將這一對詞語引入了計算機界。

    2. 高地址和低地址

      計算機內存地址是有編號的,從小到大進行編號,編號小的地址相對於編號大的地址稱爲低地址,反過來編號大的地址就被稱爲高地址。

    3. 高位字節和低位字節
      舉例說明:int a=16777220,換算成十六進制是0x01000004,相對來說,04就是低位字節,01則是高位字節

    4. 示例
      1. 16bit寬的數0x1234在內存中的存放方式

        內存地址

        小端模式存放順序

        大端模式存放順序

        0x4000

        34

        12

        0x4001

        12

        34

      2. 32bit寬的數0x12345678在內存中的存放方式

        內存地址

        小端模式存放順序

        大端模式存放順序

        0x4000

        78

        12

        0x4001

        56

        34

        0x4002

        34

        56

        0x4003

        12

        78

    5. 總結 
      經過上述四步分析,應該理解什麼是大端小端了吧。其實就是數據在內存或外部設備存儲順序。

  3. 爲什麼要區分大端小端?
    因爲在計算機系統中,數據是以字節爲單位的,每個地址單元都對應着一個字節,一個字節8bit。但是在C語言中除了8bit的char外,還有16bit的short型、32bit的long型。對於位數大於8位的處理器,如16位或32位處理器,由於寄存器寬度大於一個字節,那麼必然存在着一個如何將多個字節排序的問題

  4. 大端小端各自優勢
    1. 大端字節序的優勢:符合人類的閱讀習慣。
    2. 小端字節序的優勢:因爲計算機電路先處理低位字節,效率比較高,計算都是從低位開始的,所以計算機的內部處理都是小端字節序。

  5. 其他信息
    其實,處理器在讀取外部設備的數據,處理字節序的時候,並不知道什麼是高位字節,什麼是地位字節,它只知道按照順序讀取字節。即使是向外部設備寫入數據,也不用考慮字節序,正常寫入一個值即可,外部設備會自己處理字節序的問題。

1.5.1.4.編程語言對大小端默認值

下述列表測試平臺爲Windows10操作系統 和 Intel CPU i7。

       1)   VB6:小端

       2)   C:小端

       3)   C++:小端

       4)   C#:小端

       5)   Golang:小端

       6)   STM32:小端

       7)   C51&C52:大端

       8)   Java:大端

所以在與C51&C52和Java程序進行Socket通信時,一定要注意大小端問題。

1.5.1.5.處理器對大小端默認值

       1)   x86,MOS Technology 6502,Z80,VAX,PDP-11等處理器爲Little endian

       2)   Motorola 6800,Motorola 68000,PowerPC 970,System/370,SPARC(除V9外)等處理器爲Big endian

       3)   ARM, PowerPC (除PowerPC 970外), DEC Alpha, SPARC V9, MIPS, PA-RISC and IA64的字節序是可配置的

1.5.1.6.字符編碼與大小端的關係

從上面1到5個主題看,並沒有看出字符編碼與大小端有直接關係,現在就舉個有關係的例子。

        題目1:使用C#語言編寫一個Socket程序,與C51機器進行網絡通信,雙方傳輸的數據採用Unicode編碼(UTF-16),C#程序通過System.Text.Encoding.Unicode.GetBytes(string str)方法轉換字符串爲byte數據。
        測試:你可能會驚奇的發現,雙方無法通訊。
        原因:這是因爲System.Text.Encoding.Unicode.GetBytes()換出來的byte數組是小端字節序,而C51那邊是大端字節序。其實System.Text.Encoding對象下還提供了BigEndianUnicode.GetBytes()方法。

        題目2:在題目1的基礎上修改編碼方式,雙方都改爲GB2312或UTF-8
        測試:沒有問題
        原因:爲什麼呢?因爲gbk和utf-8編碼都是以單個字節表示數字的,不存在字節序的問題,而UTF-16編碼是以雙字節表示一個數字,因此會有字節序問題,即大小端,所以會受影響。

現在知道字符編碼與大小端也是有關係的。

1.5.1.7.總結

經過上述分析彙總,應該清楚了,在進行網絡通信編程時,如果出現了跨平臺、跨語言的場景,一定要注意字節序的影響,除此之外還要注意字符編碼、通訊協議的實現、驅動程序等也會出現字節序的問題,影響開發調試進度。

2.實現方案

在.NET/C#中對Socket的支持都是基於Windows I/O Completion Port完成端口技術的封裝,通過不同的Non-Blocking封裝結構滿足不同的編程需求。如果從異步編程的角度看,就是對不同時期異步編程的支持。

2.1.APM模式

以下內容等想寫了再寫,有些不快了吧,哈哈哈……

2.2.EAP模式

以下內容等想寫了再寫,有些不快了吧,哈哈哈……

2.3.TAP模式

以下內容等想寫了再寫,有些不快了吧,哈哈哈……

3.技術設計

3.1.功能設計

3.1.1.消息邊界

3.1.1.1.TCP和UDP協議

在Socket網絡編程中,TCP是面向連接的傳輸層協議,它的目標是提供可靠的端到端連接,保證消息有序無誤的傳輸。爲了提升消息的網絡傳輸效率,消息發送端發送消息時,採用合併算法,將發送間隔短、數據量小的多包數據合併成爲一大包數據後傳輸。消息接收端接收到消息後,採用對應的拆解算法,把大包數據拆解還原爲多包數據。這種合併與拆解操作又被稱爲封包和拆包。

UDP不是面向連接的協議,提供一對多的消息傳輸服務,它沒有使用合併與拆解算法優化數據包。它的消息接收端採用了鏈式結構存儲每一包接收的數據,且每包數據都帶有消息頭,其中包含來源地址和端口等信息,所以UDP通信不存在粘包問題。

3.1.1.2.保護消息邊界和流

保護消息邊界指傳輸協議把數據封裝成一包獨立的消息在網上傳輸,接收端只能接收到一包獨立的消息,也就是說發送端發送一包數據,接收端一次只能接收一包數據,所以又被稱爲面向消息傳輸。無保護消息邊界指傳輸協議把數據視爲一串字節流,接收端在一次接收操作還原數據時,可能會發現接收了多包數據,因此又被稱爲面向流式傳輸。

TCP爲了保證可靠傳輸,減少額外開銷,採用面向流式傳輸。面向流式傳輸可以減少數據包的發送數量,從而減少了如數據包驗證等操作額外開銷。由於TCP協議通過合併數據包的方式傳輸,對於數據傳輸頻繁的需求來說,就會出現粘包問題,同時也會增加接收端拆包的工作量。所以TCP面向流式傳輸方式,更適合對數據傳輸要求可靠、且不需要太頻繁傳輸的場景。

UDP是面向消息傳輸,不存在粘包問題。但是當發送的數據包(有效載荷)大小較小時,就會因爲發送次數的增加造成發送端和接收端的資源開銷,如系統調度、硬件設備等。同時由於在網絡上傳輸數據包的次數相對TCP來說增多了,所以可靠性也就降低了。因此UDP面向消息傳輸方式,更適合面向大數據包(不大於UDP協議最大載荷),且對可靠性要求沒那麼嚴苛的場景。

3.1.1.3.解決粘包問題

基於TCP協議實現的面向流式傳輸的數據包,之所以出現粘包問題,既有可能因爲發送方合併多包消息、或發送頻率過快造成的,也有可能因爲接收方處理消息速度太慢,導致緩衝區裏的多包數據連在了一起引發的。

解決TCP粘包問題,一般會採用以下三種方式,可以根據實際場景選擇不同的方式。

  1. 發送固定長度的消息
    1. 優勢:容易簡單,只要通信雙方都按照固定長度發送和接收數據即可。
    2. 缺陷:由於長度是固定的,長度值會使數據包的數量增加或發送延遲,比如原始包較大,需要按照長度值拆分爲多次發送;原始包沒有達到長度值,延遲發送。
  2. 消息長度和消息一起發送
    1. 優勢:容易簡單,適合任何場景。不存在發送固定長度消息的多次發送和延遲問題。
    2. 缺陷:這種方式通常是在消息前面增加固定幾個字節表示消息長度,所以通訊雙方需要提前協商好消息長度佔用的字節數,如4個字節。
  3. 使用特殊標記處理(分隔符)
    1. 優勢:擴展性強,適合任何場景。不存在第二步中協商消息長度問題,也不存在第一步中多次發送和延遲問題,只需要通信雙方按照約定的分隔符對數據進行封包和拆包傳輸即可。
    2. 缺陷:要求消息體中不能出現和分隔符相同的字符串,所以通常採用的處理方式是對消息體進行編碼,編碼自然會增加系統資源的開銷。

3.1.2. 消息編碼

3.1.2.1.字符編碼

如果採用特殊標記解決TCP傳輸的粘包問題,通常的做法是使用Base64編碼方式對要傳輸的數據進行編碼,保證編碼後的數據不會出現特殊標記,當然也可以自定義更高效的編碼方式。

3.1.2.2.加解密

此處提到加解密是指在進入Socket編程之前實現對數據加密,以及數據結束Socket編程之後進行解密。這主要是因爲EAP模式不支持SSL/TLS方式的安全傳輸,不過這樣做也不好,如果實現方式不好會降低效率(業務層面,不影響Socket)。

3.1.2.3.解壓縮

不論技術如何發展,網絡通信帶寬資源都是非常寶貴的,所以對傳輸數據進行壓縮任何時期都是必要的,特別對於某些行業來說通信帶寬極其珍貴,比如通過衛星通信的航海、軍事等行業,通信帶寬依然處於2Mbit/s,即256kb/s。

所以要像加解密操作一樣,在進入Socket處理之前需要對數據進行壓縮,以及Socket處理完之後進行解壓縮。

3.2.產品設計

3.2.1. 上位機設計

在設計上位機程序時,很多人喜歡將UI可視化交互調控功能和上位機網絡通信功能放在一起,個人認爲這是不嚴謹的、不規範的,不要求把上位機程序設計成爲消息服務器,但至少要將上述兩大功能分爲兩個程序進行設計,最好將網絡通信功能做成系統服務程序運行在後臺,這樣就會降低和減少UI操作對網絡通信功能的影響。

 

如果將UI交互調控與網絡通信傳輸分爲兩個程序,就會遇到進程間通信的問題,這就涉及到進程間通信技術了。進程間通信技術有多種實現方案,如:共享內存、命名管道和匿名管道、發送消息、Socket通信等。

  1. 共享內存:就會遇到另一個問題:數據同步。可以使用“互斥量Mutex”、“信號量Semaphore”或“事件Event”解決。
    1. 優點:比較適合兩個進程間大數據量的交換。
    2. 缺點:僅限於同一臺計算機。且設計到非託管代碼和資源操作,可能會出現安全問題。

  2. Remoting:它是通過通道(channel)來實現兩個應用程序域之間對象通信的。
    1. 優點:利用TCP通道速度非常快,可以遠程調用。
    2. 缺點:不是標準的技術,對平臺等有依賴性;傳輸的數據會被序列化,會使性能下降。
  3. 命名管道:它是一種從一個進程到另一個進程用內核對象來進行信息傳輸。和一般的管道不同,命名管道可以被不同進程以不同的方式方法調用(可以跨權限、跨語言、跨平臺)。
    1. 優點:不會對數據進行序列化。
    2. 缺點:僅限於同一臺計算機或同一局域網內部。且受網卡質量和性能的影響。通訊時沒有安全層。
  4. 發送消息:利用系統SendMessage和PostMessage等函數實現進程間通訊。
    1.  優點:速度快、效率高。
    2.  缺點:僅限於同一臺計算機。且需要獲取消息接收方的窗口句柄,顯然不適合系統服務。發送消息還會受消息接收方窗口狀態的影響。
  5. Socket通信:應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口,把複雜的TCP/IP協議族隱藏在Socket接口後面。通過IP地址和端口進行數據的傳輸。
    1. 優點:不受地理位置和網絡空間的限制,更加靈活。
    2. 缺點:受網卡質量和性能的影響。

綜上所述,從企業發展、靈活運用、技術服務等多角度分析,得出結論是:還是基於消息服務器的模式,並採用Socket技術,進行上位機程序的設計和研發會更有現實意義和價值。

3.2.2.消息服務器設計

一提到消息服務器,你首先想到的應該是QQ和WeChat吧?它們算是即時通信領域裏的王者。不過作爲一名碼農,你可能更想知道如何設計出像它們一樣強大的消息服務平臺,面對億萬級用戶量的即時通信,與工業領域中規模有限的上位機服務器遠不在一個層次。即時通信的用戶是雙向互發消息,不同於主要負責單向接受消息的上位機。面對真正海量用戶,必須設計專門的通信協議、通信模型,和具有完整性、伸縮性的程序架構、服務架構、存儲架構,以及管理運維架構,否則難以支撐海量用戶的通信需求。即時通信領域有標準的通信協議XMPP,大多數的即時通信軟件協議都是基於它,或是在其基礎上定製的。

 

即時通信從功能管理上可分爲三部分:消息管理,服務管理,功能應用,以下將進一步它們的具體功能設計。

  1. 消息管理
    1. 消息服務器轉發器:負責將消息轉發給目標用戶。
    2. 消息服務器存儲器:負責將消息寫入數據庫持久化存儲。
  2. 服務管理
    1. 消息服務器管理:管理和維護消息服務器資源。
    2. 地理區域管理:負責地理空間和行政區域劃分下的消息服務器管理。
    3. 跨網通訊管理:更多是指國家間的互聯互通。
    4. 安全通訊管理:涉及通信安全的技術、行政、信仰、文化等安全控制。
    5. 文件存儲管理:用於存儲通信雙方傳遞的文件,如圖片、音視頻、文本文檔等。
  3. 功能應用
    1. 自我管理:信息維護、狀態管理、所屬消息服務器位置等。
    2. 朋友管理:信息維護、狀態管理、所屬消息服務器位置等。
    3. 羣組管理:信息維護、成員管理、所屬消息服務器位置等。
    4. 查找管理:添加好友,創建/加入羣組。
    5. 消息管理:已讀/未讀,文本字符/圖片/音視頻文件,歷史消息管理
    6. 通知管理:消息發送和接受時的通知方式等。

上述內容簡單說明了,即時通信系統的重點對象和功能管理,接下來就說說工作流程設計。首先要說明一下采用類似於電子郵件方式設計,這種方式屬於無主機模式,而且具有很強的彈性調控,主要分爲五個步驟,如下:

  1. 身份鑑別過程
    參數(權鑑服務器地址,ID和密碼),用戶使用客戶端程序登錄權鑑服務器,權鑑程序驗證ID是否存在,存在則繼續驗證密碼是否正確,最後向客戶端輸出結果。
  2. 申請消息服務器
    登錄成功,表示客戶端有資格申請消息服務器,客戶端到資源分配服務器申請消息服務器。資源服務器依據實際情況分配消息服務器。
  3. 資源服務器
    資源池維護着可提供服務的消息服務器數量和信息,按照就近原則資源服務器爲客戶端用戶分配消息服務器,消息服務器可服務的客戶端數量*消息服務器數量,就是服務能力上限,也是資源池的最大值。資源池的大小可通過增減消息服務器的數量和服務能力動態調控。
  4. P2P即時通信
    客戶端用戶申請到消息服務器,便可以和好友進行即時通信。但是與好友通訊前,首先需要知道好友所在的消息服務器。
  5. 羣組即時通信
    客戶端用戶要查詢羣組的信息,同時也需要把自己的消息服務器地址告訴羣組消息服務器,這樣就可以和羣組中的人進行羣聊。

通過對上述功能管理和工作流程的設計與分析,是不是對即時通信系統的設計有點感覺了,看似簡單的一款聊天軟件,想不到其架構設計如此深奧吧。即時通信軟件最適合鍛鍊和檢驗socket編程技術能力,而大多數程序員都是基於業務需求做功能性的應用開發,工作中很少會涉及到網絡通信層面,所以經常會有人談socket色變的情況。其實還好啦,靜下心來慢慢啃,總會有收穫的。

 

4.擴展信息

4.1.XMPP

XMPP是一種基於標準通用標記語言的子集XML的協議,它繼承了在XML環境中靈活的發展性。因此,基於XMPP的應用具有超強的可擴展性。經過擴展以後的XMPP可以通過發送擴展的信息來處理用戶的需求,以及在XMPP的頂端建立如內容發佈系統和基於地址的服務等應用程序。而且,XMPP包含了針對服務器端的軟件協議,使之能與另一個進行通話,這使得開發者更容易建立客戶應用程序或給一個配好系統添加功能。

 

如果做即時通信消息服務器軟件,那麼XMPP協議是你應該非常清楚的,它就像Http協議對於瀏覽器一樣重要。

XMPP官網:https://xmpp.org/

4.2.WebSocket

因爲WebSocket本身也是基於Socket實現的,所以可以嘗試基於已經實現的EAP模式或TAP模式的Socket功能進行擴展。像其他協議也可以在此基礎上實現,如Http/Https,FTP等等。

5.總結

最近幾年忙於雲平臺和架構的設計與研發,現在終於有時間來整理一下知識點了。一開始沒有整理這篇文章的計劃,搜了半天文章,感覺都不是很全面,爲此就有了寫此文的想法。

 

6.參考信息

IOCP:https://blog.csdn.net/PiggyXP/article/details/6922277?spm=1001.2014.3001.5502

Socket:https://blog.csdn.net/weixin_39634961/article/details/80236161

APM:https://www.cnblogs.com/sunev/archive/2012/08/07/2625688.html

EAP:https://www.cnblogs.com/tuyile006/p/10980391.html

EAP:https://segmentfault.com/a/1190000003834832?utm_source=tag-newest

SocketAsyncEventArgs:https://docs.microsoft.com/zh-cn/dotnet/api/system.net.sockets.socketasynceventargs?redirectedfrom=MSDN&view=net-6.0#code-snippet-1

SocketAsyncEventArgsPool:https://referencesource.microsoft.com/#System.ServiceModel/System/ServiceModel/Channels/SocketAsyncEventArgsPool.cs

BufferManager:https://docs.microsoft.com/zh-cn/dotnet/api/system.net.sockets.socketasynceventargs.setbuffer?redirectedfrom=MSDN&view=net-6.0#System_Net_Sockets_SocketAsyncEventArgs_SetBuffer_System_Byte___System_Int32_System_Int32_

Unicode:https://www.cnblogs.com/kingcat/archive/2012/10/16/2726334.html

ByteOrder: http://www.ruanyifeng.com/blog/2016/11/byte-order.html

MessageBoundary:https://www.cnblogs.com/kex1n/p/6502002.html

 

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