Unity-Socket通信實例詳解

今天我們來講解socket通信。

首先我們需要知道什么是socket通信:

Socket本質上就是一個個進程之間網絡通信的基礎,每一個Socket由IP+端口組成,熟悉計網的同學應該知道IP主要是應用于IP協議而端口主要應用于TCP協議,這也證明了Socket通信是一個多個層共同工作的過程。

總結:Socket是網絡編程的基石,通過簡單API抽象底層協議,實現進程間靈活高效的數據交換。

現在我們用一個實例來看看具體的一個Socket通信是如何實現的,既然涉及到了網絡通信,那當然需要一個客戶端和一個服務器,我們就拿自己的電腦來同時作為客戶端和服務器即可。

Server

我們從服務器開始。

首先來看一個大體服務器代碼的作用:

Main

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Main : MonoBehaviour
{SocketServer _server;private void Awake(){_server = new SocketServer("127.0.0.1", 6854);_server.OnConnect += (client) =>{UnityEngine.Debug.LogFormat("連接成功 >> IP:{0}", client.LocalEndPoint.ToString());};_server.OnDisconnect += (client) =>{UnityEngine.Debug.LogFormat("連接斷開 >> IP:{0}", client.LocalEndPoint.ToString());};_server.OnReceive += (client, data) =>{UnityEngine.Debug.LogFormat("[{0}]接收到數據>>>{1} {2}", client.LocalEndPoint.ToString(), (SocketEvent)data.Type, data.Buff.Length);switch ((SocketEvent)data.Type){case SocketEvent.sc_test:UnityEngine.Debug.LogFormat("接收到測試數據 >>> {0}", System.Text.Encoding.UTF8.GetString(data.Data));break;}};}private void Update(){if (Input.GetKeyDown(KeyCode.A)){// 踢出連接foreach (var item in _server.ClientInfoDic.Keys){_server.KickOutAll();}}}private void OnDestroy(){// 注意由于Unity編譯器環境下,游戲開啟/關閉只影響主線程的開關,游戲關閉回調時需要通過Close函數來關閉服務端/客戶端的線程。if (_server != null){_server.Close();}}
}

光看這一段代碼的話其實也看不出什么名堂,我們只知道有一個SocketServer類的實例_server,我們在Awake()函數中_server分別注冊了三個事件,分別對應連接、斷連和接受信息。在Update中我們如果檢測到按鍵A我們把_server中的客戶端信息全部清空,以及最后關閉服務器。

那顯然我們的重心是來看看SocketServer類的代碼內容了。

SocketServer

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Timers;public class SocketInfo
{public Socket Client;public Thread ReceiveThread;public long HeadTime;
}/// <summary>
/// Socket服務端
/// </summary>
public class SocketServer
{/// <summary>/// 主線程/// </summary>private SynchronizationContext _mainThread;public string IP;public int Port;private const int HEAD_TIMEOUT = 5000;    // 心跳超時 毫秒private const int HEAD_CHECKTIME = 5000;   // 心跳包超時檢測 毫秒public Dictionary<Socket, SocketInfo> ClientInfoDic = new Dictionary<Socket, SocketInfo>();private Socket _server;private Thread _connectThread;private System.Timers.Timer _headCheckTimer;private DataBuffer _dataBuffer = new DataBuffer();public event Action<Socket> OnConnect;  //客戶端建立連接回調public event Action<Socket> OnDisconnect;  // 客戶端斷開連接回調public event Action<Socket, SocketDataPack> OnReceive;  // 接收報文回調public event Action<Socket, SocketDataPack> OnSend;  // 發送報文回調// 目前捕獲異常將觸發OnDisconnect回調 暫不單獨處理// public event Action<SocketException> OnError;   // 異常捕獲回調private bool _isValid = true;public SocketServer(string ip, int port){_mainThread = SynchronizationContext.Current;IP = ip;Port = port;_server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);IPAddress ipAddress = IPAddress.Parse(IP);//解析IP地址_server.Bind(new IPEndPoint(ipAddress, Port));  //綁定IP地址:端口  _server.Listen(10);    //設定最多10個排隊連接請求// 啟動線程監聽連接_connectThread = new Thread(ListenClientConnect);_connectThread.Start();// 心跳包定時檢測_headCheckTimer = new System.Timers.Timer(HEAD_CHECKTIME);_headCheckTimer.AutoReset = true;_headCheckTimer.Elapsed += delegate (object sender, ElapsedEventArgs args){CheckHeadTimeOut();};_headCheckTimer.Start();}/// <summary>  /// 監聽客戶端連接  /// </summary>  private void ListenClientConnect(){while (true){try{if (!_isValid) break;Socket client = _server.Accept();Thread receiveThread = new Thread(ReceiveEvent);ClientInfoDic.Add(client, new SocketInfo() { Client = client, ReceiveThread = receiveThread, HeadTime = GetNowTime() });receiveThread.Start(client);PostMainThreadAction<Socket>(OnConnect, client);}catch{break;}}}/// <summary>/// 獲取當前時間戳/// </summary>/// <returns></returns>private long GetNowTime(){TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);return Convert.ToInt64(ts.TotalMilliseconds);}public void Send(Socket client, UInt16 e, byte[] buff = null, Action<SocketDataPack> onTrigger = null){buff = buff ?? new byte[] { };var dataPack = new SocketDataPack(e, buff);var data = dataPack.Buff;try{client.BeginSend(data, 0, data.Length, SocketFlags.None, new AsyncCallback((asyncSend) =>{Socket c = (Socket)asyncSend.AsyncState;c.EndSend(asyncSend);PostMainThreadAction<SocketDataPack>(onTrigger, dataPack);PostMainThreadAction<Socket, SocketDataPack>(OnSend, client, dataPack);}), client);}catch (SocketException ex){CloseClient(client);// onError(ex);}}/// <summary>/// 線程內接收數據的函數/// </summary>private void ReceiveEvent(object client){Socket tsocket = (Socket)client;while (true){if (!_isValid) return;if (!ClientInfoDic.ContainsKey(tsocket)){return;}try{byte[] rbytes = new byte[8 * 1024];int len = tsocket.Receive(rbytes);if (len > 0){_dataBuffer.AddBuffer(rbytes, len); // 將收到的數據添加到緩存器中var dataPack = new SocketDataPack();if (_dataBuffer.TryUnpack(out dataPack)) // 嘗試解包{if (dataPack.Type == (UInt16)SocketEvent.sc_head){// 接收到心跳包ReceiveHead(tsocket);}else if (dataPack.Type == (UInt16)SocketEvent.sc_disconn){// 客戶端斷開連接CloseClient(tsocket);}else{// 收到消息PostMainThreadAction<Socket, SocketDataPack>(OnReceive, tsocket, dataPack);}}}else{if (tsocket.Poll(-1, SelectMode.SelectRead)){CloseClient(tsocket);return;}}}catch (SocketException ex){CloseClient(tsocket);// onError(ex);return;}}}/// <summary>/// 接收到心跳包/// </summary>private void ReceiveHead(Socket client){SocketInfo info;if (ClientInfoDic.TryGetValue(client, out info)){long now = GetNowTime();long offset = now - info.HeadTime;UnityEngine.Debug.Log("更新心跳時間戳 >>>" + now + "  間隔>>>" + offset);if (offset > HEAD_TIMEOUT){// 心跳包收到但超時邏輯}info.HeadTime = now;}}/// <summary>/// 檢測心跳包超時/// </summary>private void CheckHeadTimeOut(){var tempList = new List<Socket>();foreach (var socket in ClientInfoDic.Keys){tempList.Add(socket);}foreach (var socket in tempList){var info = ClientInfoDic[socket];long now = GetNowTime();long offset = now - info.HeadTime;if (offset > HEAD_TIMEOUT){// 心跳包超時KickOut(socket);}}}public void KickOut(Socket client){// 踢出連接Send(client, (UInt16)SocketEvent.sc_kickout, null, (dataPack) =>{CloseClient(client);});}public void KickOutAll(){var tempList = new List<Socket>();foreach (var socket in ClientInfoDic.Keys){tempList.Add(socket);}foreach (var socket in tempList){KickOut(socket);}}/// <summary>/// 清理客戶端連接/// </summary>/// <param name="client"></param>private void CloseClient(Socket client){PostMainThreadAction<Socket>((socket) =>{if (OnDisconnect != null) OnDisconnect(socket);ClientInfoDic.Remove(socket);socket.Close();}, client);}/// <summary>/// 關閉/// </summary>public void Close(){if (!_isValid) return;_isValid = false;// if (_connectThread != null) _connectThread.Abort();var tempList = new List<Socket>();foreach (var socket in ClientInfoDic.Keys){tempList.Add(socket);}foreach (var socket in tempList){CloseClient(socket);}if (_headCheckTimer != null){_headCheckTimer.Stop();_headCheckTimer = null;}_server.Close();}// /// <summary>// /// 錯誤回調// /// </summary>// /// <param name="e"></param>// private void onError(SocketException ex)// {//     PostMainThreadAction<SocketException>(OnError, ex);// }// <summary>/// 通知主線程回調/// </summary>private void PostMainThreadAction(Action action){_mainThread.Post(new SendOrPostCallback((o) =>{Action e = (Action)o.GetType().GetProperty("action").GetValue(o);if (e != null) e();}), new { action = action });}private void PostMainThreadAction<T>(Action<T> action, T arg1){_mainThread.Post(new SendOrPostCallback((o) =>{Action<T> e = (Action<T>)o.GetType().GetProperty("action").GetValue(o);T t1 = (T)o.GetType().GetProperty("arg1").GetValue(o);if (e != null) e(t1);}), new { action = action, arg1 = arg1 });}public void PostMainThreadAction<T1, T2>(Action<T1, T2> action, T1 arg1, T2 arg2){_mainThread.Post(new SendOrPostCallback((o) =>{Action<T1, T2> e = (Action<T1, T2>)o.GetType().GetProperty("action").GetValue(o);T1 t1 = (T1)o.GetType().GetProperty("arg1").GetValue(o);T2 t2 = (T2)o.GetType().GetProperty("arg2").GetValue(o);if (e != null) e(t1, t2);}), new { action = action, arg1 = arg1, arg2 = arg2 });}
}

非常長的代碼內容啊,我們一點一點來看:

public class SocketInfo
{public Socket Client;public Thread ReceiveThread;public long HeadTime;
}

這是我們的Socket的信息,可以看到有Socket類的實例,對于服務器來說要處理的Socket類當然就是客戶端的Socket,有一個線程和一個時間值,這個時間值的作用我們暫時按下不表。

    /// <summary>/// 主線程/// </summary>private SynchronizationContext _mainThread;public string IP;public int Port;private const int HEAD_TIMEOUT = 5000;    // 心跳超時 毫秒private const int HEAD_CHECKTIME = 5000;   // 心跳包超時檢測 毫秒public Dictionary<Socket, SocketInfo> ClientInfoDic = new Dictionary<Socket, SocketInfo>();private Socket _server;private Thread _connectThread;private System.Timers.Timer _headCheckTimer;private DataBuffer _dataBuffer = new DataBuffer();public event Action<Socket> OnConnect;  //客戶端建立連接回調public event Action<Socket> OnDisconnect;  // 客戶端斷開連接回調public event Action<Socket, SocketDataPack> OnReceive;  // 接收報文回調public event Action<Socket, SocketDataPack> OnSend;  // 發送報文回調// 目前捕獲異常將觸發OnDisconnect回調 暫不單獨處理// public event Action<SocketException> OnError;   // 異常捕獲回調private bool _isValid = true;

可以看到密密麻麻的一系列參數啊,這里就是我們SocketServer類的成員變量了,首先是這個我們似乎第一次見的類:SynchronizationContext。

看名字也知道這個類和異步操作以及上下文有關系,概括來說:

然后是我們的IP和端口,這個不多說。 然后是兩個int時間值,還記得之前SocketInfo里定義的HeadTime嗎?我們稱其為心跳時間:

那在一個Socket網絡通信中心跳時間的意義不用多說了吧,就是檢查連接是否正常的一個時間閾值,具體是怎么個檢查法我們后續介紹。

然后是一個 存儲客戶端Socket信息的字典,代表服務器的Socket類實例,一個線程,一個計時器,然后是一個數據緩沖類(自定義的),然后是一系列event(大家應該都知道什么是event吧?),更準確的說是Action,分別代表連接,斷連,接收和發送。最后一個bool變量表示能否建立連接。

    public SocketServer(string ip, int port){_mainThread = SynchronizationContext.Current;IP = ip;Port = port;_server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);IPAddress ipAddress = IPAddress.Parse(IP);//解析IP地址_server.Bind(new IPEndPoint(ipAddress, Port));  //綁定IP地址:端口  _server.Listen(10);    //設定最多10個排隊連接請求// 啟動線程監聽連接_connectThread = new Thread(ListenClientConnect);_connectThread.Start();// 心跳包定時檢測_headCheckTimer = new System.Timers.Timer(HEAD_CHECKTIME);_headCheckTimer.AutoReset = true;_headCheckTimer.Elapsed += delegate (object sender, ElapsedEventArgs args){CheckHeadTimeOut();};_headCheckTimer.Start();}

SocketServer的有參構造,參數是IP和端口號。

我們把當前線程上下文給到_mainThread,IP和端口也給到。然后是服務器的初始化:

        _headCheckTimer.Elapsed += delegate (object sender, ElapsedEventArgs args){CheckHeadTimeOut();};

這一系列初始化服務器的操作都是在調用Socket類內部的函數。

然后是啟動我們線程的監聽狀態,然后啟動我們的心跳包定時檢測,注意我們在new一個計時器的構造函數的參數:

_headCheckTimer = new System.Timers.Timer(HEAD_CHECKTIME);

這里的HEAD_CHECKTIME代表時間間隔。

我們開啟計時器的自動重置之后可以看到:

        _headCheckTimer.Elapsed += delegate (object sender, ElapsedEventArgs args){CheckHeadTimeOut();};

?這是一個匿名委托:用法類似于匿名函數,我們直接寫委托內容,隨寫隨用,每次委托觸發時執行CheckHeadTimeOut()函數。

這個過程中涉及到兩個函數:

    /// <summary>  /// 監聽客戶端連接  /// </summary>  private void ListenClientConnect(){while (true){try{if (!_isValid) break;Socket client = _server.Accept();Thread receiveThread = new Thread(ReceiveEvent);ClientInfoDic.Add(client, new SocketInfo() { Client = client, ReceiveThread = receiveThread, HeadTime = GetNowTime() });receiveThread.Start(client);PostMainThreadAction<Socket>(OnConnect, client);}catch{break;}}}

用try catch避免異常,從服務器處獲取接受的客戶端Socket類和線程,這里可以看到構造新線程的參數為ReceiveEvent,代表這個線程構造時就會綁定一個委托。

   /// <summary>/// 檢測心跳包超時/// </summary>private void CheckHeadTimeOut(){var tempList = new List<Socket>();foreach (var socket in ClientInfoDic.Keys){tempList.Add(socket);}foreach (var socket in tempList){var info = ClientInfoDic[socket];long now = GetNowTime();long offset = now - info.HeadTime;if (offset > HEAD_TIMEOUT){// 心跳包超時KickOut(socket);}}}

這個是我們在構造函數中關于心跳包超時檢測的函數,我們創建一個存儲Socket的list,把存儲客戶端信息的字典中的鍵更新到list中,然后獲取當前時間之后減去客戶端socket信息之中的HeadTime來得到時間偏差,如果這個時間偏差大于我們的允許的時間值我們就認為這個心跳包超時并執行KickOut函數。

這里又涉及到了兩個函數:GetNowTime()和KickOut();

   /// <summary>/// 獲取當前時間戳/// </summary>/// <returns></returns>private long GetNowTime(){TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);return Convert.ToInt64(ts.TotalMilliseconds);}

這一段代碼是一個獲取當前時間的方法:

而關于KickOut():

    public void KickOut(Socket client){// 踢出連接Send(client, (UInt16)SocketEvent.sc_kickout, null, (dataPack) =>{CloseClient(client);});}

似乎牽扯的函數越來越多了,我們來看看Send是什么:

    public void Send(Socket client, UInt16 e, byte[] buff = null, Action<SocketDataPack> onTrigger = null){buff = buff ?? new byte[] { };var dataPack = new SocketDataPack(e, buff);var data = dataPack.Buff;try{client.BeginSend(data, 0, data.Length, SocketFlags.None, new AsyncCallback((asyncSend) =>{Socket c = (Socket)asyncSend.AsyncState;c.EndSend(asyncSend);PostMainThreadAction<SocketDataPack>(onTrigger, dataPack);PostMainThreadAction<Socket, SocketDataPack>(OnSend, client, dataPack);}), client);}catch (SocketException ex){CloseClient(client);// onError(ex);}}

回到我們的KickOut()函數:

?我們實現了一個異步的解除客戶端連接的方法:我們向客戶端發送事件碼告知其斷開連接之后不會阻塞當前線程。

然后就是兩個負責關閉連接的函數:

    private void CloseClient(Socket client){PostMainThreadAction<Socket>((socket) =>{if (OnDisconnect != null) OnDisconnect(socket);ClientInfoDic.Remove(socket);socket.Close();}, client);}

這是關閉客戶端連接的代碼,其中的PostMainThreadAction<Socket>:

?的作用就是把這個Socket類型的實例放在主線程上進行操作。操作的內容包括檢查是否有方法注冊在OnDisconnect上,有的話就執行;然后從字典中刪除相關Socket信息,關閉相關socket即可。

這里可能涉及到一個問題就是:為什么我們Socket的關閉一定要在主線程上執行呢?

   /// <summary>/// 關閉/// </summary>public void Close(){if (!_isValid) return;_isValid = false;// if (_connectThread != null) _connectThread.Abort();var tempList = new List<Socket>();foreach (var socket in ClientInfoDic.Keys){tempList.Add(socket);}foreach (var socket in tempList){CloseClient(socket);}if (_headCheckTimer != null){_headCheckTimer.Stop();_headCheckTimer = null;}_server.Close();}

這個是關閉整個服務器的函數,我們把所有的客戶端socket先關閉掉,然后停止計時器后最后關閉服務器。

這個是整個SocketServer類的內容,其中還有幾個自定義類的內容我們沒有介紹:


DataBuffer

代碼如下:

using System;
/// <summary>
/// Socket傳輸過程的緩沖區,嘗試拆包獲得數據
/// </summary>
public class DataBuffer
{// 緩存區長度private const int MIN_BUFF_LEN = 1024;private byte[] _buff;private int _buffLength = 0;public DataBuffer(int minBuffLen = MIN_BUFF_LEN){if (minBuffLen <= 0){minBuffLen = MIN_BUFF_LEN;}_buff = new byte[minBuffLen];}/// <summary>/// 添加緩存數據/// </summary>public void AddBuffer(byte[] data, int len){byte[] buff = new byte[len];Array.Copy(data, buff, len);if (len > _buff.Length - _buffLength)  //超過當前緩存{byte[] temp = new byte[_buffLength + len];Array.Copy(_buff, 0, temp, 0, _buffLength);Array.Copy(buff, 0, temp, _buffLength, len);_buff = temp;}else{Array.Copy(data, 0, _buff, _buffLength, len);}_buffLength += len;//修改當前數據標記}public bool TryUnpack(out SocketDataPack dataPack){dataPack = SocketDataPack.Unpack(_buff);if (dataPack == null){return false;}// 清理舊緩存_buffLength -= dataPack.BuffLength;byte[] temp = new byte[_buffLength < MIN_BUFF_LEN ? MIN_BUFF_LEN : _buffLength];Array.Copy(_buff, dataPack.BuffLength, temp, 0, _buffLength);_buff = temp;return true;}
}

我們來一點點解讀:
?

    // 緩存區長度private const int MIN_BUFF_LEN = 1024;private byte[] _buff;private int _buffLength = 0;

定義了緩沖區的長度:注意這里的長度針對的是字節數,也就是至少1024個字節的緩沖區大小,其實也就是初始的緩沖區大小,一個字節數組和一個當前緩沖區長度。

    public DataBuffer(int minBuffLen = MIN_BUFF_LEN){if (minBuffLen <= 0){minBuffLen = MIN_BUFF_LEN;}_buff = new byte[minBuffLen];}

public的構造函數,給參數提供了默認參數,構造時自動生成一個設定好大小的字節數組。

    /// <summary>/// 添加緩存數據/// </summary>public void AddBuffer(byte[] data, int len){byte[] buff = new byte[len];Array.Copy(data, buff, len);if (len > _buff.Length - _buffLength)  //超過當前緩存{byte[] temp = new byte[_buffLength + len];Array.Copy(_buff, 0, temp, 0, _buffLength);Array.Copy(buff, 0, temp, _buffLength, len);_buff = temp;}else{Array.Copy(data, 0, _buff, _buffLength, len);}_buffLength += len;//修改當前數據標記}

我們新生成一個長度為len的數組,然后把data數組拷貝到buff中,如果這個時候我們的len超過了緩沖區的大小我們需要去新開辟一個數組并把現有的數據拷貝到新開辟的數組中;否則我們直接復制即可,然后修改緩沖區長度。這里有一個C#的內置函數Copy。

最后的一個函數:

    public bool TryUnpack(out SocketDataPack dataPack){dataPack = SocketDataPack.Unpack(_buff);if (dataPack == null){return false;}// 清理舊緩存_buffLength -= dataPack.BuffLength;byte[] temp = new byte[_buffLength < MIN_BUFF_LEN ? MIN_BUFF_LEN : _buffLength];Array.Copy(_buff, dataPack.BuffLength, temp, 0, _buffLength);_buff = temp;return true;}

這是一個拆包的函數,我們將緩沖區的Socket數據包進行拆包,如果包是空的則返回false(表示緩沖區內數據不足,無法組成完整的包),否則將數據從緩沖區移除,具體來說首先更新緩沖區大小,檢查緩沖區剩余容量保證不低于最低容量,將未使用的緩沖數據從后續位置移動(復制到)緩沖區前端方便使用。

可以看到這個函數中有一個我們沒有說過的SocketDataPack類。

SocketDataPack

Socket數據包的代碼如下,以下簡稱數據包。

using System;
using System.IO;
/// <summary>
/// Socket通信過程中的數據包 處理具體拆包裝包邏輯
/// </summary>
public class SocketDataPack
{// 消息:數據總長度(4byte) + 數據類型(2byte) + 數據(N byte)public static int HEAD_DATA_LEN = 4;public static int HEAD_TYPE_LEN = 2;public static int HEAD_LEN{get { return HEAD_DATA_LEN + HEAD_TYPE_LEN; }}/// <summary>/// 數據包類型/// </summary>public UInt16 Type;/// <summary>/// 數據包數據/// </summary>public byte[] Data;public byte[] Buff;public int BuffLength{get { return Buff.Length; }}public int DataLength{get { return Data.Length; }}public SocketDataPack(){}public SocketDataPack(UInt16 type, byte[] data){Type = type;Data = data;Buff = GetBuff(Type, Data);}public static byte[] GetBuff(UInt16 type, byte[] data){byte[] buff = new byte[data.Length + HEAD_LEN];byte[] temp;temp = BitConverter.GetBytes(buff.Length);Array.Copy(temp, 0, buff, 0, HEAD_DATA_LEN);temp = BitConverter.GetBytes(type);Array.Copy(temp, 0, buff, HEAD_DATA_LEN, HEAD_TYPE_LEN);Array.Copy(data, 0, buff, HEAD_LEN, data.Length);return buff;}public static SocketDataPack Unpack(byte[] buff){try{if (buff.Length < HEAD_LEN){// 頭部沒取完則返回return null;}byte[] temp;// 取數據長度temp = new byte[HEAD_DATA_LEN];Array.Copy(buff, 0, temp, 0, HEAD_DATA_LEN);int buffLength = BitConverter.ToInt32(temp, 0);if (buffLength <= 0) return null;if (buffLength > buff.Length){// 數據沒取完return null;}int dataLength = buffLength - HEAD_LEN;// 取數據類型temp = new byte[HEAD_TYPE_LEN];Array.Copy(buff, HEAD_DATA_LEN, temp, 0, HEAD_TYPE_LEN);UInt16 dataType = BitConverter.ToUInt16(temp, 0);// 取數據byte[] data = new byte[dataLength];Array.Copy(buff, HEAD_LEN, data, 0, dataLength);var dataPack = new SocketDataPack(dataType, data);// UnityEngine.Debug.LogFormat("buffLen:{0} type:{1} dataLength:{2}", buffLength, dataType, data.Length);return dataPack;}catch{// 存在不完整數據解包 則返回nullreturn null;}}
}

首先看看成員變量:

    // 消息:數據總長度(4byte) + 數據類型(2byte) + 數據(N byte)public static int HEAD_DATA_LEN = 4;public static int HEAD_TYPE_LEN = 2;public static int HEAD_LEN{get { return HEAD_DATA_LEN + HEAD_TYPE_LEN; }}/// <summary>/// 數據包類型/// </summary>public UInt16 Type;/// <summary>/// 數據包數據/// </summary>public byte[] Data;public byte[] Buff;

定義了數據包的格式:數據長度為4,類型長度為2,然后是數據本身,設置為一個只讀的屬性,長度為前二者之和。

數據包類型使用一個UInt16的數據類型來表示,數據分為Data和Buff兩種。

    public int BuffLength{get { return Buff.Length; }}public int DataLength{get { return Data.Length; }}

這兩個也是只讀的屬性,返回的是Data和Buff類型數據的長度。

    public SocketDataPack(UInt16 type, byte[] data){Type = type;Data = data;Buff = GetBuff(Type, Data);}

有參構造,參數就是類型和數據,然后緩沖由GetBuff函數得到。

    public static byte[] GetBuff(UInt16 type, byte[] data){byte[] buff = new byte[data.Length + HEAD_LEN];byte[] temp;temp = BitConverter.GetBytes(buff.Length);Array.Copy(temp, 0, buff, 0, HEAD_DATA_LEN);temp = BitConverter.GetBytes(type);Array.Copy(temp, 0, buff, HEAD_DATA_LEN, HEAD_TYPE_LEN);Array.Copy(data, 0, buff, HEAD_LEN, data.Length);return buff;}

GetBuff函數就是一個根據類型和數據來獲取緩沖的函數,我們新生成一個長度為數據長度加上頭部長度的數組,然后我們使用BitConverter.GetBytes函數來生成字節流之后把這些字節流丟到中,更準確地說,我們把緩沖區的長度信息,類型信息(都轉換為字節流)以及具體的數據都拷貝到數組中。

關于為什么要轉換為字節流:

public static SocketDataPack Unpack(byte[] buff)
{try{if (buff.Length < HEAD_LEN){// 頭部沒取完則返回return null;}byte[] temp;// 取數據長度temp = new byte[HEAD_DATA_LEN];Array.Copy(buff, 0, temp, 0, HEAD_DATA_LEN);int buffLength = BitConverter.ToInt32(temp, 0);if (buffLength <= 0) return null;if (buffLength > buff.Length){// 數據沒取完return null;}int dataLength = buffLength - HEAD_LEN;// 取數據類型temp = new byte[HEAD_TYPE_LEN];Array.Copy(buff, HEAD_DATA_LEN, temp, 0, HEAD_TYPE_LEN);UInt16 dataType = BitConverter.ToUInt16(temp, 0);// 取數據byte[] data = new byte[dataLength];Array.Copy(buff, HEAD_LEN, data, 0, dataLength);var dataPack = new SocketDataPack(dataType, data);// UnityEngine.Debug.LogFormat("buffLen:{0} type:{1} dataLength:{2}", buffLength, dataType, data.Length);return dataPack;}catch{// 存在不完整數據解包 則返回nullreturn null;}}

這里是我們數據包的拆包函數,參數是一個字節數組,我們首先檢測這個數組長度如果沒有頭部長度大的話說明這個數組的內容根本不完整,直接返回null。接著我們分別從參數傳遞的數組中取長度信息、類型信息以及數據本身,將其復制到數組中,最后生成SocketDataPack類型的數據包并返回。

小小的總結一下我們的Server代碼的作用:

對于我們的服務器來說,最重要的部分是通過心跳包來檢查連接是否正常,以及接收信息之后的各種回調事件。

Client

客戶端這邊的DataBuffer和SocketDataPack的內容是完全相同的,主要是Main和SocketClient的區別:

Main

內容如下:
?

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Main : MonoBehaviour
{SocketClient _client;private void Awake(){_client = new SocketClient("127.0.0.1", 6854);_client.OnDisconnect += () =>{UnityEngine.Debug.Log("斷開連接");};_client.OnReceive += (dataPack) =>{UnityEngine.Debug.LogFormat("接收數據>>>{0}", (SocketEvent)dataPack.Type);};_client.OnSend += (dataPack) =>{UnityEngine.Debug.LogFormat("發送數據>>>{0}", (SocketEvent)dataPack.Type);};_client.OnError += (ex) =>{UnityEngine.Debug.LogFormat("出現異常>>>{0}", ex);};_client.OnReConnectSuccess += (num) =>{UnityEngine.Debug.LogFormat("第{0}次重連成功", num);};_client.OnReConnectError += (num) =>{UnityEngine.Debug.LogFormat("第{0}次重連失敗", num);};_client.OnReconnecting += (num) =>{UnityEngine.Debug.LogFormat("正在進行第{0}次重連", num);};_client.Connect(() =>{UnityEngine.Debug.Log("連接成功");// _client.DisConnect();}, () =>{UnityEngine.Debug.Log("連接失敗");});}private void Update(){}public void ClickSendTest(){var bytes = System.Text.Encoding.UTF8.GetBytes("我是測試數據");_client.Send((System.UInt16)SocketEvent.sc_test, bytes);}public void ClickDisConnect(){_client.DisConnect();}private void OnDestroy(){// 注意由于Unity編譯器環境下,游戲開啟/關閉只影響主線程的開關,游戲關閉回調時需要通過Close函數來關閉服務端/客戶端的線程。if (_client != null){_client.Close();}}
}

可以看到客戶端的Main函數內容多得多。

首先是一個SocketClient類的實例,然后就是一系列的委托事件,分別代表:斷開連接、接受消息、發送消息、發現錯誤、第num次重連成功、第num次重連失敗、正在進行第num次重連。

然后是一個關于連接成功與否的包含兩個lambda參數的方法:將是否連接成功打印出來。

然后是三個函數:點擊發送測試消息、點擊斷開連接、關閉客戶端socket。

SocketClient

這是客戶端的Socket代碼:

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Timers;/// <summary>
/// Socket客戶端
/// </summary>
public class SocketClient
{/// <summary>/// 主線程/// </summary>private SynchronizationContext _mainThread;public string IP;public int Port;private const int TIMEOUT_CONNECT = 3000;   // 連接超時時間 毫秒private const int TIMEOUT_SEND = 3000;  // 發送超時時間 毫秒private const int TIMEOUT_RECEIVE = 3000;   //接收超時時間 毫秒private const int HEAD_OFFSET = 2000; //心跳包發送間隔 毫秒private const int RECONN_MAX_SUM = 3;   //最大重連次數private Socket _client;private Thread _receiveThread;private System.Timers.Timer _connTimeoutTimer;private System.Timers.Timer _headTimer;private DataBuffer _dataBuffer = new DataBuffer();public event Action OnConnectSuccess;    // 連接成功回調public event Action OnConnectError;    // 連接失敗回調public event Action OnDisconnect;  // 斷開回調public event Action<SocketDataPack> OnReceive;  // 接收報文回調public event Action<SocketDataPack> OnSend;  // 發送報文回調public event Action<SocketException> OnError;   // 異常捕獲回調public event Action<int> OnReConnectSuccess; // 重連成功回調public event Action<int> OnReConnectError; // 單次重連失敗回調public event Action<int> OnReconnecting;  // 單次重連中回調private bool _isConnect = false;private bool _isReconnect = false;public SocketClient(string ip, int port){_mainThread = SynchronizationContext.Current;IP = ip;Port = port;}public void Connect(Action success = null, Action error = null){Action<bool> onTrigger = (flag) =>{if (flag){PostMainThreadAction(success);PostMainThreadAction(OnConnectSuccess);}else{PostMainThreadAction(error);PostMainThreadAction(OnConnectError);}if (_connTimeoutTimer != null){_connTimeoutTimer.Stop();_connTimeoutTimer = null;}};try{_client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//創建套接字_client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, TIMEOUT_SEND);_client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, TIMEOUT_RECEIVE);IPAddress ipAddress = IPAddress.Parse(IP);//解析IP地址IPEndPoint ipEndpoint = new IPEndPoint(ipAddress, Port);IAsyncResult result = _client.BeginConnect(ipEndpoint, new AsyncCallback((iar) =>{try{Socket client = (Socket)iar.AsyncState;client.EndConnect(iar);_isConnect = true;// 開始發送心跳包_headTimer = new System.Timers.Timer(HEAD_OFFSET);_headTimer.AutoReset = true;_headTimer.Elapsed += delegate (object sender, ElapsedEventArgs args){Send((UInt16)SocketEvent.sc_head);};_headTimer.Start();// 開始接收數據_receiveThread = new Thread(new ThreadStart(ReceiveEvent));_receiveThread.IsBackground = true;_receiveThread.Start();onTrigger(true);}catch (SocketException ex){onTrigger(false);}}), _client);//異步連接_connTimeoutTimer = new System.Timers.Timer(TIMEOUT_CONNECT);_connTimeoutTimer.AutoReset = false;_connTimeoutTimer.Elapsed += delegate (object sender, ElapsedEventArgs args){onTrigger(false);};_connTimeoutTimer.Start();}catch (SocketException ex){onTrigger(false);// throw;}}/// <summary>/// 斷線重連/// </summary>/// <param name="num"></param>public void ReConnect(int num = RECONN_MAX_SUM, int index = 0){_isReconnect = true;num--;index++;if (num < 0){onDisconnect();_isReconnect = false;return;}PostMainThreadAction<int>(OnReconnecting, index);Connect(() =>{PostMainThreadAction<int>(OnReConnectSuccess, index);_isReconnect = false;}, () =>{PostMainThreadAction<int>(OnReConnectError, index);ReConnect(num, index);});}public void Send(UInt16 e, byte[] buff = null, Action<SocketDataPack> onTrigger = null){buff = buff ?? new byte[] { };var dataPack = new SocketDataPack(e, buff);var data = dataPack.Buff;try{_client.BeginSend(data, 0, data.Length, SocketFlags.None, new AsyncCallback((asyncSend) =>{Socket c = (Socket)asyncSend.AsyncState;c.EndSend(asyncSend);PostMainThreadAction<SocketDataPack>(onTrigger, dataPack);PostMainThreadAction<SocketDataPack>(OnSend, dataPack);}), _client);}catch (SocketException ex){onError(ex);}}/// <summary>/// 線程內接收數據的函數/// </summary>private void ReceiveEvent(){while (true){try{if (!_isConnect) break;if (_client.Available <= 0) continue;byte[] rbytes = new byte[8 * 1024];int len = _client.Receive(rbytes);if (len > 0){_dataBuffer.AddBuffer(rbytes, len); // 將收到的數據添加到緩存器中var dataPack = new SocketDataPack();if (_dataBuffer.TryUnpack(out dataPack)) // 嘗試解包{if (dataPack.Type == (UInt16)SocketEvent.sc_kickout){// 服務端踢出onDisconnect();}else{// 收到消息PostMainThreadAction<SocketDataPack>(OnReceive, dataPack);}}}}catch (SocketException ex){onError(ex);// throw;}}}/// <summary>/// 業務邏輯 - 客戶端主動斷開/// </summary>public void DisConnect(){Send((UInt16)SocketEvent.sc_disconn);onDisconnect();}/// <summary>/// 緩存數據清理/// </summary>public void Close(){if (!_isConnect) return;_isConnect = false;if (_headTimer != null){_headTimer.Stop();_headTimer = null;}// if (_receiveThread != null)// {//     _receiveThread.Abort();//     _receiveThread = null;// }if (_connTimeoutTimer != null){_connTimeoutTimer.Stop();_connTimeoutTimer = null;}if (_client != null){_client.Close();_client = null;}}/// <summary>/// 錯誤回調/// </summary>/// <param name="e"></param>private void onError(SocketException ex){Close();PostMainThreadAction<SocketException>(OnError, ex);if (!_isReconnect){ReConnect();}}/// <summary>/// 斷開回調/// </summary>private void onDisconnect(){Close();PostMainThreadAction(OnDisconnect);}/// <summary>/// 通知主線程回調/// </summary>private void PostMainThreadAction(Action action){_mainThread.Post(new SendOrPostCallback((o) =>{Action e = (Action)o.GetType().GetProperty("action").GetValue(o);if (e != null) e();}), new { action = action });}private void PostMainThreadAction<T>(Action<T> action, T arg1){_mainThread.Post(new SendOrPostCallback((o) =>{Action<T> e = (Action<T>)o.GetType().GetProperty("action").GetValue(o);T t1 = (T)o.GetType().GetProperty("arg1").GetValue(o);if (e != null) e(t1);}), new { action = action, arg1 = arg1 });}public void PostMainThreadAction<T1, T2>(Action<T1, T2> action, T1 arg1, T2 arg2){_mainThread.Post(new SendOrPostCallback((o) =>{Action<T1, T2> e = (Action<T1, T2>)o.GetType().GetProperty("action").GetValue(o);T1 t1 = (T1)o.GetType().GetProperty("arg1").GetValue(o);T2 t2 = (T2)o.GetType().GetProperty("arg2").GetValue(o);if (e != null) e(t1, t2);}), new { action = action, arg1 = arg1, arg2 = arg2 });}
}

我們依然先從成員變量開始說起:

    /// <summary>/// 主線程/// </summary>private SynchronizationContext _mainThread;public string IP;public int Port;private const int TIMEOUT_CONNECT = 3000;   // 連接超時時間 毫秒private const int TIMEOUT_SEND = 3000;  // 發送超時時間 毫秒private const int TIMEOUT_RECEIVE = 3000;   //接收超時時間 毫秒private const int HEAD_OFFSET = 2000; //心跳包發送間隔 毫秒private const int RECONN_MAX_SUM = 3;   //最大重連次數private Socket _client;private Thread _receiveThread;private System.Timers.Timer _connTimeoutTimer;private System.Timers.Timer _headTimer;private DataBuffer _dataBuffer = new DataBuffer();public event Action OnConnectSuccess;    // 連接成功回調public event Action OnConnectError;    // 連接失敗回調public event Action OnDisconnect;  // 斷開回調public event Action<SocketDataPack> OnReceive;  // 接收報文回調public event Action<SocketDataPack> OnSend;  // 發送報文回調public event Action<SocketException> OnError;   // 異常捕獲回調public event Action<int> OnReConnectSuccess; // 重連成功回調public event Action<int> OnReConnectError; // 單次重連失敗回調public event Action<int> OnReconnecting;  // 單次重連中回調private bool _isConnect = false;private bool _isReconnect = false;

依然是主線程的線程上下文,端口,IP,然后是連接超時的時間、發送超時時間、接收超時時間,都設置為3000ms(就是3s),然后是心跳包發送的最大間隔為2000ms,以及最大的重連次數為3。

然后是一個Socket類的實例client,一個接收線程,一個用于連接的計時器和一個心跳計時器,以及一個數據緩沖區。

然后是一系列event,注釋里都有寫明,我就不多贅述。

最后是兩個bool變量表示是否連接以及是否重連。

    public SocketClient(string ip, int port){_mainThread = SynchronizationContext.Current;IP = ip;Port = port;}

有參構造:把線程上下文給到主線程,IP和端口都同步。

    public void Connect(Action success = null, Action error = null){Action<bool> onTrigger = (flag) =>{if (flag){PostMainThreadAction(success);PostMainThreadAction(OnConnectSuccess);}else{PostMainThreadAction(error);PostMainThreadAction(OnConnectError);}if (_connTimeoutTimer != null){_connTimeoutTimer.Stop();_connTimeoutTimer = null;}};try{_client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//創建套接字_client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, TIMEOUT_SEND);_client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, TIMEOUT_RECEIVE);IPAddress ipAddress = IPAddress.Parse(IP);//解析IP地址IPEndPoint ipEndpoint = new IPEndPoint(ipAddress, Port);IAsyncResult result = _client.BeginConnect(ipEndpoint, new AsyncCallback((iar) =>{try{Socket client = (Socket)iar.AsyncState;client.EndConnect(iar);_isConnect = true;// 開始發送心跳包_headTimer = new System.Timers.Timer(HEAD_OFFSET);_headTimer.AutoReset = true;_headTimer.Elapsed += delegate (object sender, ElapsedEventArgs args){Send((UInt16)SocketEvent.sc_head);};_headTimer.Start();// 開始接收數據_receiveThread = new Thread(new ThreadStart(ReceiveEvent));_receiveThread.IsBackground = true;_receiveThread.Start();onTrigger(true);}catch (SocketException ex){onTrigger(false);}}), _client);//異步連接_connTimeoutTimer = new System.Timers.Timer(TIMEOUT_CONNECT);_connTimeoutTimer.AutoReset = false;_connTimeoutTimer.Elapsed += delegate (object sender, ElapsedEventArgs args){onTrigger(false);};_connTimeoutTimer.Start();}catch (SocketException ex){onTrigger(false);// throw;}}

這是我們的連接函數,參數中包含了兩個action,分別表示連接是否成功。

然后是一個名為onTrigger的接收參數類型為bool類型的Action委托,接收flag參數來決定回調函數以及計時器的處理。如果flag為true則在主線程中觸發success委托和OnConnectSuccess委托,否則觸發error委托和OnConnectError委托,此時如果存在連接超時計時器則暫停計時并清空。

為什么要清空計時器?

然后是一系列的Socket對象實例和設置,我們創建一個基于IPV4的TCP流式套接字對象,并設置他的發送消息超時閾值和接收消息超時閾值,最后解析得到源IP和目標IP。

接著是發起連接的函數,這里我們采用異步連接的方式,也就是APM模式(Asynchronous Programming Model,異步編程模型):

其中涉及到的核心:IAsyncResult對象的概念:

?

?我們將客戶端的socket對象作為狀態對象傳入函數參數中,可以看到有一個從iar.AsyncState沖取出套接字的過程,然后客戶端結束連接的異步操作。

為什么要在這里執行EndConnect呢?

后續就是一系列的連接成功后要處理的內容比如生成計時器,發送心跳包和生成接收消息的線程。當然,如果連接失敗的話,我們就返回OnTrigger(false)。?

后續是一個單次觸發的連接超時計時器(AutoReset=false代表單次觸發),觸發后執行OnTrigger(false)。

/// <summary>
/// 斷線重連
/// </summary>
/// <param name="num"></param>
public void ReConnect(int num = RECONN_MAX_SUM, int index = 0)
{_isReconnect = true;num--;index++;if (num < 0){onDisconnect();_isReconnect = false;return;}PostMainThreadAction<int>(OnReconnecting, index);Connect(() =>{PostMainThreadAction<int>(OnReConnectSuccess, index);_isReconnect = false;}, () =>{PostMainThreadAction<int>(OnReConnectError, index);ReConnect(num, index);});}

然后是我們重連的函數,我們接收的參數最大的重連次數和重連次數的序號。每次重連都更新isReconnect和最大重連次數和重連次數的序號,如果已經沒有最大的重連次數我們就放棄嘗試重連。通知主線程執行重連回調函數,然后返回Connect根據是否連接成功返回的兩個回調函數。

    public void Send(UInt16 e, byte[] buff = null, Action<SocketDataPack> onTrigger = null){buff = buff ?? new byte[] { };var dataPack = new SocketDataPack(e, buff);var data = dataPack.Buff;try{_client.BeginSend(data, 0, data.Length, SocketFlags.None, new AsyncCallback((asyncSend) =>{Socket c = (Socket)asyncSend.AsyncState;c.EndSend(asyncSend);PostMainThreadAction<SocketDataPack>(onTrigger, dataPack);PostMainThreadAction<SocketDataPack>(OnSend, dataPack);}), _client);}catch (SocketException ex){onError(ex);}}

Send函數,上來有一個null的合并運算符:

?我們將數據類型和數據封裝成data,然后執行Socket的BeginSend和EndSend異步操作,并通知主線程執行OnTrigger回調和OnSend回調。

    /// <summary>/// 線程內接收數據的函數/// </summary>private void ReceiveEvent(){while (true){try{if (!_isConnect) break;if (_client.Available <= 0) continue;byte[] rbytes = new byte[8 * 1024];int len = _client.Receive(rbytes);if (len > 0){_dataBuffer.AddBuffer(rbytes, len); // 將收到的數據添加到緩存器中var dataPack = new SocketDataPack();if (_dataBuffer.TryUnpack(out dataPack)) // 嘗試解包{if (dataPack.Type == (UInt16)SocketEvent.sc_kickout){// 服務端踢出onDisconnect();}else{// 收到消息PostMainThreadAction<SocketDataPack>(OnReceive, dataPack);}}}}catch (SocketException ex){onError(ex);// throw;}}}

接收消息的函數,我們利用while(true)來實時監聽Socket數據流,檢查連接狀態以及socket是否有數據,這里使用了一個Available。

?如果緩沖區無數據可讀而依然執行Receive的話可能會導致CPU的空轉:

有數據的話我們就去接收數據,生成一個新的數組進行數據的接收,接收到的數據我們丟到緩沖區中,并嘗試解包,如果解包函數返回的類型是斷開連接,意味著服務器主動要求客戶端斷開連接,這時候我們就會去執行斷開連接,否則我們都會通知主線程來執行OnReceive回調。

/// <summary>
/// 業務邏輯 - 客戶端主動斷開
/// </summary>
public void DisConnect()
{Send((UInt16)SocketEvent.sc_disconn);onDisconnect();
}

主動斷開連接的函數,我們會向服務器發送預定義好的斷開連接的事件碼,然后執行斷開連接的委托。

/// <summary>
/// 緩存數據清理
/// </summary>
public void Close()
{if (!_isConnect) return;_isConnect = false;if (_headTimer != null){_headTimer.Stop();_headTimer = null;}// if (_receiveThread != null)// {//     _receiveThread.Abort();//     _receiveThread = null;// }if (_connTimeoutTimer != null){_connTimeoutTimer.Stop();_connTimeoutTimer = null;}if (_client != null){_client.Close();_client = null;}}

關閉客戶端的操作就是將一系列連接狀態和心跳包還有計數器和socket本身全部關閉。

    /// <summary>/// 錯誤回調/// </summary>/// <param name="e"></param>private void onError(SocketException ex){Close();PostMainThreadAction<SocketException>(OnError, ex);if (!_isReconnect){ReConnect();}}

錯誤時執行的回調函數,首先是執行關閉客戶端,然后通知主線程執行報錯的委托,同時自動嘗試重連。

    /// <summary>/// 斷開回調/// </summary>private void onDisconnect(){Close();PostMainThreadAction(OnDisconnect);}

斷開連接的回調。

   /// <summary>/// 通知主線程回調/// </summary>private void PostMainThreadAction(Action action){_mainThread.Post(new SendOrPostCallback((o) =>{Action e = (Action)o.GetType().GetProperty("action").GetValue(o);if (e != null) e();}), new { action = action });}private void PostMainThreadAction<T>(Action<T> action, T arg1){_mainThread.Post(new SendOrPostCallback((o) =>{Action<T> e = (Action<T>)o.GetType().GetProperty("action").GetValue(o);T t1 = (T)o.GetType().GetProperty("arg1").GetValue(o);if (e != null) e(t1);}), new { action = action, arg1 = arg1 });}public void PostMainThreadAction<T1, T2>(Action<T1, T2> action, T1 arg1, T2 arg2){_mainThread.Post(new SendOrPostCallback((o) =>{Action<T1, T2> e = (Action<T1, T2>)o.GetType().GetProperty("action").GetValue(o);T1 t1 = (T1)o.GetType().GetProperty("arg1").GetValue(o);T2 t2 = (T2)o.GetType().GetProperty("arg2").GetValue(o);if (e != null) e(t1, t2);}), new { action = action, arg1 = arg1, arg2 = arg2 });}

通知主線程執行委托的函數,這里是三個參數不同的版本,這里的代碼格式有些復雜:

       _mainThread.Post(new SendOrPostCallback((o) =>{Action e = (Action)o.GetType().GetProperty("action").GetValue(o);if (e != null) e();}), new { action = action });

中的(o) =>{...}是一個lambda表達式,表示接收參數為o的一個匿名函數,函數內部的內容是從o處獲取運行時類型,從中獲取運行時屬性中名為“action”的屬性,如果有的話獲取其值并轉換成Action類型給到e。在o處獲取的action會賦值給我們新生成的名為action匿名變量,這一步的目的是:

客戶端的功能總結如下:

Test

大體上這就是我們整個項目的代碼了,我們來看看最終的效果如何吧:

這是服務器的打印內容。?

這是客戶端的打印內容。

我們先測試發送測試消息:

?客戶端發送測試消息:

服務器接收到測試消息。

客戶端斷開連接:

?

服務器的打印信息。

現在我們再來測試主動斷開服務器:

客戶端開始重連。

?到達最大重連次數之后就斷開連接。

我們再開啟服務器之后發送測試信息:

就這樣我們實現了一個基于C#的Socket通信項目。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/79873.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/79873.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/79873.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

使用Go語言對接全球股票數據源API實踐指南

使用Go語言對接全球股票數據API實踐指南 概述 本文介紹如何通過Go語言對接支持多國股票數據的API服務。我們將基于提供的API文檔&#xff0c;實現包括市場行情、K線數據、實時推送等核心功能的對接。 一、準備工作 1. 獲取API Key 聯系服務提供商獲取訪問密鑰&#xff08;替…

LeetCode 熱題 100 17. 電話號碼的字母組合

LeetCode 熱題 100 | 17. 電話號碼的字母組合 大家好&#xff0c;今天我們來解決一道經典的算法題——電話號碼的字母組合。這道題在 LeetCode 上被標記為中等難度&#xff0c;要求給定一個僅包含數字 2-9 的字符串&#xff0c;返回所有它能表示的字母組合。下面我將詳細講解解…

OpenCV計算機視覺實戰(3)——計算機圖像處理基礎

OpenCV計算機視覺實戰&#xff08;3&#xff09;——計算機圖像處理基礎 0. 前言1. 像素和圖像表示1.1 像素 2. 色彩空間2.1 原色2.2 色彩空間2.3 像素和色彩空間 3. 文件類型3.1 圖像文件類型3.2 視頻文件3.3 圖像與視頻 4. 計算機圖像編程簡史5. OpenCV 概述小結系列鏈接 0. …

Vite 的工作流程

Vite 的工作流程基于其創新的 “預構建 按需加載” 機制&#xff0c;通過利用現代瀏覽器對原生 ES 模塊的支持&#xff0c;顯著提升了開發效率和構建速度。以下是其核心工作流程的詳細分析&#xff1a; 一、開發環境工作流程 1. 啟動開發服務器 冷啟動&#xff1a;通過 npm …

線性DP(動態規劃)

線性DP的概念&#xff08;視頻&#xff09; 學習線性DP之前&#xff0c;請確保已經對遞推有所了解。 一、概念 1、動態規劃 不要去看網上的各種概念&#xff0c;什么無后效性&#xff0c;什么空間換時間&#xff0c;會越看越暈。從做題的角度去理解就好了&#xff0c;動態規劃…

MySQL中sql_mode的設置

■ 57版本原來配置 show variables like %sql_mode%; STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION ■ 修改配置文件 注釋掉sql_mode&#xff0c;并重啟&#xff0c;查看57版本的默認設置 ONL…

MCAL學習(1)——AutoSAR

1.了解AutoSAR及一些概念 AutoSAR是Automotive Open System Architecture ,汽車開放系統架構。 針對汽車ECU的軟件開發架構。已經是汽車電子軟件開發的標準。 OS服務&#xff1a;Freertos 整車廠&#xff08;OEM&#xff09;主要負責應用層算法 一級供應商&#xff1a;生產制…

Vue報錯:Cannot read properties of null (reading ‘xxx‘)

一、報錯問題 Cannot read properties of null (reading style)at patchStyle (runtime-dom.esm-bundler.js:104:22)二、錯誤排查 這類報錯一般是在已經開發好后&#xff0c;后面測試時突然發現的&#xff0c;所以不好排查錯誤原因。 三、可能原因及解決方案 v-if 導致 在 …

25G 80km雙纖BIDI光模塊:遠距傳輸的創新標桿

目錄 一、產品優勢&#xff1a;雙纖與BIDI的獨特價值 易天光通信25G SFP28 ZR 80KM 易天光通信25G SFP28 BIDI ZR 80KM 二、權威認證與技術突破 三、雙纖與BIDI的核心差異解析 四、應用場景&#xff1a;驅動多領域高效互聯 總結 在5G、云計算與數字化轉型的推動下&#xff0c;光…

2025-05-06 學習記錄--Python-注釋 + 打印變量 + input輸入

合抱之木&#xff0c;生于毫末&#xff1b;九層之臺&#xff0c;起于累土&#xff1b;千里之行&#xff0c;始于足下。&#x1f4aa;&#x1f3fb; 一、注釋 ?? &#xff08;一&#xff09;、塊注釋 &#x1f36d; 舉例&#xff1a; &#x1f330; # 打印數字 print(2025) …

基于mediapipe深度學習的眨眼檢測和計數系統python源碼

目錄 1.算法運行效果圖預覽 2.算法運行軟件版本 3.部分核心程序 4.算法理論概述 5.算法完整程序工程 1.算法運行效果圖預覽 (完整程序運行后無水印) 2.算法運行軟件版本 人工智能算法python程序運行環境安裝步驟整理_本地ai 運行 python-CSDN博客 3.部分核心程序 &…

怎樣通過API 實現python調用Chatgpt,gemini

怎樣通過API 實現python調用Chatgpt,gemini 以下為你詳細介紹如何設置和調用這些參數,以創建一個類似的 ChatCompletion 請求: 1. 安裝依賴庫 如果你使用的是 OpenAI 的 API 客戶端,需要先安裝 openai 庫。可以使用以下命令進行安裝: pip install openai2. 代碼示例 …

Linux 下MySql主從數據庫的環境搭建

測試環境&#xff1a;兩臺服務器&#xff0c;Mysql版本 8.0&#xff0c;linux版本&#xff1a;Ubuntu 20.04.3&#xff1b; 1.在兩臺服務器上安裝MySql&#xff1b; 2.選一臺作為主服務器&#xff0c;在主服務器上以root用戶進入Mysql&#xff0c;執行以下語句&#xff1a; …

力扣1812題解

記錄 2025.5.7 題目&#xff1a; 思路&#xff1a; 從左下角開始&#xff0c;棋盤的行數和列數&#xff08;均從 1 開始計數&#xff09;之和如果為奇數&#xff0c;則為白色格子&#xff0c;如果和為偶數&#xff0c;則為黑色格子。 代碼&#xff1a; class Solution {pu…

適合java程序員的Kafka消息中間件實戰

創作的初心&#xff1a; 我們在學習kafka時&#xff0c;都是基于大數據的開發而進行的講解&#xff0c;這篇文章為java程序員為核心&#xff0c;助力大家掌握kafka實現。 什么是kafka: 歷史&#xff1a; 誕生與開源&#xff08;2010 - 2011 年&#xff09; 2010 年&#xf…

PDF智能解析與知識挖掘:基于pdfminer.six的全棧實現

前言 在數字化信息爆炸的時代&#xff0c;PDF&#xff08;便攜式文檔格式&#xff09;作為一種通用的電子文檔標準&#xff0c;承載著海量的結構化與非結構化知識。然而&#xff0c;PDF格式的設計初衷是用于展示而非數據提取&#xff0c;這使得從PDF中挖掘有價值的信息成為數據…

Python爬蟲+代理IP+Header偽裝:高效采集亞馬遜數據

1. 引言 在當今大數據時代&#xff0c;電商平臺&#xff08;如亞馬遜&#xff09;的數據采集對于市場分析、競品監控和價格追蹤至關重要。然而&#xff0c;亞馬遜具有嚴格的反爬蟲機制&#xff0c;包括IP封禁、Header檢測、驗證碼挑戰等。 為了高效且穩定地采集亞馬遜數據&am…

架構思維:探討架構師的本質使命

文章目錄 軟件工程1. 軟件工程的定義與核心目標2. 軟件工程 vs. 軟件項目管理3. 軟件工程的兩大特性4. 軟件工程的關鍵活動與方法論5. 架構師在軟件工程中的職責架構師的職責和思維架構師心性修煉三大核心能力架構設計的基本準則 團隊共識“設計文檔”的統一結構框架閱讀他人代…

QT設計權限管理系統

Qt能夠簡單實現系統的權限設計 首先我們需要一個登陸界面 例如這樣 然后一級權限&#xff0c;可以看到所有的內容&#xff0c;不設置菜單欄的隱藏。 然后其他權限&#xff0c;根據登陸者的身份進行菜單欄不同的展示。 菜單欄的隱藏代碼如下&#xff1a; ui->actionuser-…

Debezium 架構詳解與實戰示例

Debezium 架構詳解與實戰示例 1. 整體架構圖 #mermaid-svg-tkAquOxA2pylXzON {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-tkAquOxA2pylXzON .error-icon{fill:#552222;}#mermaid-svg-tkAquOxA2pylXzON .error-t…