上一篇文章介紹了記憶體對映檔案,這篇文章我們介紹一種用得更加廣泛的方式——Socket
通訊
Socket 介紹
Socket
稱為”套接字”,它分為流式套接字和使用者資料包套接字,分別對應網路中的 TCP 和 UDP 協議。這兩種均可以實現程式間通訊(無論是否是同一機器)
TCP 協議是面向連線的協議,提供穩定的雙向通訊功能,TCP連線的建立是通過三次握手才能完成,穩定性高,建立連線的效率相對UDP較低
UDP協議是面向無連線的,效率高,但不保證資料一定能夠正確傳輸(順序、丟包等)
我們應該選擇 UDP 還是 TCP?
- 對資料的可靠性要求很高的場景,應該選擇 TCP,比如涉及錢的地方。當然也可以選擇 UDP,這時候需要我們自行來保證資料的可靠性
- 對速度要求高,但允許資料出現少量錯誤的適合,UDP最合適。比如記錄日誌的場景:一臺機器專用於記錄日誌,其他的機器將日誌傳送給這臺機器即可;還有就是視訊會議的場景
但實際專案中,這樣“純粹”的場景並不是那麼多,因此,往往採用的方案都是 TCP、UDP 相結合的方式來實現。當然為了保證資料的可靠及業務的穩定性,很多框架都不僅僅只有這麼兩種技術
框架的複雜、輕量與否,與其應對的業務場景是相關的。我們需要根據不同的場景,來選擇適合自己專案的框架。在 C# 中,有 FastSocket
、SuperSocket
等 Socket
框架供大家選擇。其中 SuperSocket
支援 IOCP
,它可以實現高效能、高併發。其他語言有 Netty
、HP-Socket
等,這些也有 .NET 的移植版本
一般情況下,不建議各位朋友自己去寫一個 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~