溫故之.NET Socket通訊

上一篇文章介紹了內存映射文件,這篇文章咱們介紹一種用得更加普遍的方式——Socket 通訊git

Socket 介紹

Socket 稱爲」套接字」,它分爲流式套接字和用戶數據報套接字,分別對應網絡中的 TCP 和 UDP 協議。這兩種都可以實現進程間通訊(不管是不是同一機器)github

TCP 協議是面向鏈接的協議,提供穩定的雙向通訊功能,TCP鏈接的創建是經過三次握手才能完成,穩定性高,建立鏈接的效率相對UDP較低
UDP協議是面向無鏈接的,效率高,但不保證數據必定可以正確傳輸(順序、丟包等)網絡

咱們應該選擇 UDP 仍是 TCP?併發

  • 對數據的可靠性要求很高的場景,應該選擇 TCP,好比涉及錢的地方。固然也能夠選擇 UDP,這時候須要咱們自行來保證數據的可靠性
  • 對速度要求高,但容許數據出現少許錯誤的適合,UDP最合適。好比記錄日誌的場景:一臺機器專用於記錄日誌,其餘的機器將日誌發送給這臺機器便可;還有就是視頻會議的場景

但實際項目中,這樣「純粹」的場景並非那麼多,所以,每每採用的方案都是 TCP、UDP 相結合的方式來實現。固然爲了保證數據的可靠及業務的穩定性,不少框架都不只僅只有這麼兩種技術框架

框架的複雜、輕量與否,與其應對的業務場景是相關的。咱們須要根據不一樣的場景,來選擇適合本身項目的框架。在 C# 中,有 FastSocketSuperSocketSocket 框架供你們選擇。其中 SuperSocket 支持 IOCP,它能夠實現高性能、高併發。其餘語言有 NettyHP-Socket 等,這些也有 .NET 的移植版本tcp

通常狀況下,不建議各位朋友本身去寫一個 Socket 框架來支持項目的業務場景,用現有的框架更加妥當。若是不知道選擇什麼框架,能夠去 Github 上搜索相關的開源框架微服務

選擇 Github 中的框架時,咱們應該注意高併發

  • 選擇 Star 最多的
  • 看做者上一次維護時間是多久,這個框架的 issue 多很少。更新頻繁的,每每能夠選擇,這樣遇到問題也能夠及時的處理
  • 文檔:有一個詳細的開發文檔,能夠提升咱們開發的速度

Socket 通訊,是市面上不少框架的基礎,所以咱們有必要介紹下它的使用方式,及在開發過程當中須要注意的事項性能

使用示例

在 C# 中,不管是 TCP 協議,仍是 UDP 協議,都封裝在了 Socket 這個類中。使用時,只須要咱們指定不一樣的參數便可學習

TCP 與 UDP 區別

  • TCP 面向鏈接(如打電話要先撥號創建鏈接); UDP 是無鏈接的,即發送數據以前不須要創建鏈接(扔出去就不用管了)
  • TCP 提供可靠的服務。也就是說,經過 TCP 鏈接傳送的數據,無差錯,不丟失,不重複,且按序到達;UDP 盡最大努力交付,即不保證可靠交付
  • TCP 面向字節流,其實是 TCP 把數據當作一連串無結構的字節流;UDP 是面向報文的
  • UDP 沒有堵塞控制,所以網絡出現堵塞不會使源主機的發送速率下降(對實時應用頗有用,如IP電話,實時視頻會議等)
  • 每一條 TCP 鏈接只能是點對點的;UDP 支持一對一,一對多,多對一和多對多的交互通訊(羣視頻等場景)
  • TCP 首部開銷 20 字節;UDP 的首部開銷小,只有8個字節
  • TCP 的邏輯通訊信道是全雙工的可靠信道,UDP 則是不可靠信道

在大部分狀況下(針對性能而言),咱們沒法感受到這二者之間的差別;而在高併發的場景下,咱們就能很容易體會到(由於訪問量大了以後,任何細小的變化都能累積起來從而形成巨大的影響)

使用 TCP 面臨的一個主要問題就是粘包,業界主流的解決方案可概括以下

  • 消息定長:如每一個數據包的大小固定爲 1024 字節,若是不足 1024 字節,使用空格填充剩下的部分
  • 在包尾增長回車換行符進行分隔,好比 FTP 協議
  • 將消息分爲消息頭、消息體。消息頭包含了消息的總長度,及其餘的一些元數據,消息體存儲具體的數據包。通常地,消息頭能夠採用定長的方式,好比分配 40 個字節,其中16字節用於存放消息的長度信息,其他部分存放其餘數據。
  • 自定義應用層協議:這種方式是爲具體的業務場景而實現的,好比騰訊就有一套他們本身的通訊框架

另外,若是以爲自定義協議太麻煩,咱們也能夠根據 MQTT 協議來寫一套符合它的解決方案

針對 TCP 的使用,咱們給出一個例子。其中咱們採用 Jil 來實現序列化

/// <summary>
/// 傳輸使用的包
/// </summary>
public class Packet {
    public const int TYPE_LOGIN = 10001;
    public const int TYPE_MSG = 10000;
    public const int TYPE_LOGOUT = 10002;
    public const int TYPE_INVALID = 40000;

    /// <summary>
    /// 這個包的類型。在實際業務場景中,通常會使用 int、short 等來表示,而不是 enum
    /// </summary>
    public int Type { get; set; }
    /// <summary>
    /// 具體的業務數據
    /// </summary>
    public string Data { get; set; }
}
複製代碼

如下爲服務端代碼

using Jil;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace App {
    class Program {
        static void Main(string[] args) {
            TcpListener tcpListener = new TcpListener(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9999));
            tcpListener.Start();
            /// 此處僅僅用於處理客戶端的鏈接
            /// 而不涉及具體的業務邏輯
            while (true) {
                TcpClient remoteClient = tcpListener.AcceptTcpClient();
                ClientPacketHandlers packetHandlers = new ClientPacketHandlers(remoteClient);
            }
        }

    }

    /// <summary>
    /// 將業務邏輯處理分開
    /// </summary>
    public class ClientPacketHandlers {
        Dictionary<int, Action<NetworkStream, string>> clientHandlers = new Dictionary<int, Action<NetworkStream, string>>();
        TcpClient remoteClient;
        NetworkStream stream;
        Task processTask;
        CancellationTokenSource cancellationTokenSource;

        public ClientPacketHandlers(TcpClient client) {
            this.remoteClient = client;
            this.stream = remoteClient.GetStream();

            // 這個能夠經過配置文件來添加處理器
            clientHandlers.Add(Packet.TYPE_LOGIN, HandleLogin);
            clientHandlers.Add(Packet.TYPE_MSG, HandleMsg);
            clientHandlers.Add(Packet.TYPE_LOGOUT, HandleLogout);

            cancellationTokenSource = new CancellationTokenSource();

            // 爲該客戶端開闢一個 Task,用於與該客戶端通訊
            // 在高併發場景中,每每不會這樣作。而是採用 IOCP 或者其餘的高性能的方式
            // 爲每一個客戶端開闢一個 Task 不合理,也很浪費系統資源(由於不是每一個客戶端都會頻繁發送消息)
            processTask = Task.Run(() => {
                byte[] buffer = new byte[1024];
                while (true) {
                    int bytesRead = stream.Read(buffer, 0, 1024);
                    if (bytesRead > 0) {
                        byte[] realBytes = new byte[bytesRead];
                        Buffer.BlockCopy(buffer, 0, realBytes, 0, bytesRead);

                        Packet packet = JSON.Deserialize<Packet>(Encoding.UTF8.GetString(realBytes));
                        if (packet != null) {
                            if (clientHandlers.ContainsKey(packet.Type)) {
                                clientHandlers[packet.Type].Invoke(stream, packet.Data);
                            } else {
                                SendPacket(stream, new Packet() { Type = Packet.TYPE_INVALID, Data = "No handlers for your type" });
                            }
                        }
                    }

                    if (cancellationTokenSource == null || cancellationTokenSource.IsCancellationRequested) {
                        break;
                    }
                }
            }, cancellationTokenSource.Token);
        }

        public void HandleLogin(NetworkStream stream, string data) {
            if (stream == null || string.IsNullOrEmpty(data)) return;
            SendPacket(stream, new Packet() { Type = Packet.TYPE_LOGIN, Data = $"Hello, {data}" });
        }

        public void HandleMsg(NetworkStream stream, string data) {
            if (stream == null || string.IsNullOrEmpty(data)) return;
            SendPacket(stream, new Packet() { Type = Packet.TYPE_MSG, Data = $"Received Msg : {data}" });
        }

        public void HandleLogout(NetworkStream stream, string data) {
            if (stream == null || string.IsNullOrEmpty(data)) return;
            SendPacket(stream, new Packet() { Type = Packet.TYPE_LOGOUT, Data = $"Logout, {data}" });
            try {
                if (cancellationTokenSource != null) {
                    cancellationTokenSource.Cancel();
                    cancellationTokenSource.Dispose();
                }
            } catch (Exception e) {
            } finally {
                cancellationTokenSource = null;
            }
        }


        public void SendPacket(NetworkStream stream, Packet packet) {
            byte[] packetBytes = Encoding.UTF8.GetBytes(JSON.Serialize(packet));
            stream.Write(packetBytes, 0, packetBytes.Length);
        }
    }
}
複製代碼

如下爲客戶端代碼

using Jil;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace App {
    class Program {
        static void Main(string[] args) {
            TcpClient tcpClient = new TcpClient();
            tcpClient.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9999));
            NetworkStream networkStream = tcpClient.GetStream();

            Task.Run(() => {
                byte[] buffer = new byte[1024];
                while (true) {
                    int bytesRead = networkStream.Read(buffer, 0, 1024);
                    if (bytesRead > 0) {
                        byte[] realBytes = new byte[bytesRead];
                        Buffer.BlockCopy(buffer, 0, realBytes, 0, bytesRead);

                        Packet packet = JSON.Deserialize<Packet>(Encoding.UTF8.GetString(realBytes));
                        if (packet != null) {
                            Console.WriteLine($"RECEIVED DATA: {packet.Data}");
                        }
                    }
                }
            });

            while (true) {
                string line = Console.ReadLine();
                string[] strs = line.Split(':');
                if(strs.Length >= 2) {
                    if(strs[0] == "login") {
                        SendPacket(networkStream, new Packet() { Type = Packet.TYPE_LOGIN, Data = strs[1] });
                    } else if (strs[0] == "msg") {
                        SendPacket(networkStream, new Packet() { Type = Packet.TYPE_MSG, Data = strs[1] });
                    } else if (strs[0] == "logout") {
                        SendPacket(networkStream, new Packet() { Type = Packet.TYPE_LOGOUT, Data = strs[1] });
                    }
                }
            }
        }

        private static void SendPacket(NetworkStream networkStream, Packet packet) {
            byte[] packetBytes = Encoding.UTF8.GetBytes(JSON.Serialize(packet));
            networkStream.Write(packetBytes, 0, packetBytes.Length);
        }
    }
}
複製代碼

這即是 TCP 通訊的基礎示例了,在更復雜的場景中,系統的設計將會更加複雜。但宗旨都只有一個,提供更加穩定可靠的服務

UDP 的使用與 TCP 相似,所以就不一一舉例了

開發建議

  • 儘可能將對客戶端的管理,與具體的業務邏輯分開,這樣能夠提升系統的可維護性
  • 若是使用 TCP,除了解決粘包以外,還須要使用心跳包來使鏈接處於活動狀態
  • 在使用 UDP 的時候,若是須要保證數據的可靠性,此時須要經過其餘的方式來輔助
  • 若是要採用 GitHub 上的一些框架,必定要參考前面給出的建議
  • 在不增長系統複雜度的狀況下,可使用微服務來提高系統的擴展性。但切記不可濫用,過多的微服務會形成系統的可維護性降低,而且是指數級的降低
  • 在高併發、高性能的場景下,須要採用其餘的方式。好比 IOCP 等框架。除了避免系統資源的浪費,更是爲了提高系統的響應能力

至此,這篇文章的內容講解完畢。歡迎關注公衆號【嘿嘿的學習日記】,全部的文章,都會在公衆號首發,Thank you~

公衆號二維碼
相關文章
相關標籤/搜索