【Unity網絡編程知識】使用Socket實現簡單TCP通訊

1、Socket的常用屬性和方法

創建Socket?TCP流套接字

Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

1.1 常用屬性

1)套接字的連接狀態

socketTcp.Connected

2)獲取套接字的類型

socketTcp.SocketType

3)獲取套接字的協議類型

socketTcp.ProtocolType

4)獲取套接字的尋址方案

socketTcp.AddressFamily

5)從網絡中獲取準備讀取的數據數據量

socketTcp.Available

6)獲取本機EndPoint對象(注意:IPEndPoint繼承EndPoint)

socketTcp.LocalEndPoint as IPEndPoint

7)獲取遠程EndPoint對象

socketTcp.RemoteEndPoint as IPEndPoint;

1.2 同步常用方法?

1.2.1?主要用于服務端方法

1)綁定IP和端口, Bind(ip地址和端口)

        IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);socketTcp.Bind(ipPoint);

2)設置客戶端連接的最大數量 , Listen(最大連接數)

socketTcp.Listen(99);

3)等待客戶端連入,Accept()

socketTcp.Accept();
1.3.2 主要用于客戶端方法

1)連接遠程服務端,Connect(ip地址和端口)

socketTcp.Connect(IPAddress.Parse("118.12.123.1"), 8080);
1.4.3 客戶端服務端都會用的方法

1)同步發送和接收數據,Send()和Receive()

發送

socketTcp.Send

接收

socketTcp.Receive()

2)釋放連接并關閉socket,先于close調用

socketTcp.Shutdown(SocketShutdown.Both);

3)關閉連接,釋放所有Socket管理資源

socketTcp.Close();

1.3 異步常用方法

1.3.1 主要用于服務端方法

1)等待客戶端連入方式1,BeginAccept和EndAccept

    socketTcp.BeginAccept(AcceptCallback, socketTcp);private void AcceptCallback(IAsyncResult result){try{//獲取傳入參數Socket s = result.AsyncState as Socket;//通過調用EndAccept就可以得到連入的客戶端SocketSocket clientSocket = s.EndAccept(result);s.BeginAccept(AcceptCallback, s);}catch (SocketException e){print(e.SocketErrorCode);}}

2)等待客戶端連入方式2,?AcceptAsync

        SocketAsyncEventArgs e = new SocketAsyncEventArgs();e.Completed += (socket, args) =>{//首先判斷是否成功if(args.SocketError == SocketError.Success){Socket clientSocket = args.AcceptSocket;(socket as Socket).AcceptAsync(args);}else{print("連入客戶端失敗" + args.SocketError);}};socketTcp.AcceptAsync(e);
1.3.2?主要用于客戶端方法

1)連接遠程服務端方式1,BeginConnect和EndConnect

        IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);socketTcp.BeginConnect(ipPoint, (res) => {Socket s = res.AsyncState as Socket;try{s.EndConnect(res);}catch (SocketException e){print("連接出錯" + e.SocketErrorCode + e.Message);}}, socketTcp);

2)?連接遠程服務端方式2,ConnectAsync

        SocketAsyncEventArgs e2 = new SocketAsyncEventArgs();IPEndPoint ipPoint2 = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);e.RemoteEndPoint = ipPoint2;e.Completed += (socket, args) =>{if(args.SocketError == SocketError.Success){//連接成功}else{//連接失敗print(args.SocketError);}};socketTcp.ConnectAsync(e2);
1.3.3?客戶端服務端都會用的方法

1)接受消息

接受消息方式1,BeginReceive和EndReceive

    socketTcp.BeginReceive(resultBytes, 0, resultBytes.Length, SocketFlags.None, ReceiveCallback, socketTcp);private void ReceiveCallback(IAsyncResult result){try{Socket s = result.AsyncState as Socket;//這個返回值是你收到了多少個字節int num = s.EndReceive(result);//進行消息處理Encoding.UTF8.GetString(resultBytes, 0, num);//如果還要繼續接受s.BeginReceive(resultBytes, 0, resultBytes.Length, SocketFlags.None, ReceiveCallback, s);}catch (SocketException e){print("接受消息出問題" + e.SocketErrorCode + e.Message);}}

接受消息方式2,SendAsync

        SocketAsyncEventArgs e3 = new SocketAsyncEventArgs();byte[] bytes2 = Encoding.UTF8.GetBytes("你好呀,好兄弟");e3.SetBuffer(bytes2, 0, bytes2.Length);e3.Completed += (socket, args) =>{if(args.SocketError != SocketError.Success){print("發送成功");}else{}};socketTcp.SendAsync(e3);

2)發送消息

發送消息方式1,BeginSend和EndSend?

        byte[] bytes = Encoding.UTF8.GetBytes("41414214241");socketTcp.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, (result) =>{try{Socket s = result.AsyncState as Socket;s.EndSend(result);}catch (SocketException e){print("發送錯誤" + e.SocketErrorCode + e.Message);}}, socketTcp);

發送消息方式2,?ReceiveAsync

        e4.SetBuffer(new byte[1024 * 1024], 0, 1024 * 1024);e4.Completed += (socket, args) =>{if(args.SocketError!= SocketError.Success){//收取存儲在容器當中的字節//Buffer是容器//BytesTranferred是收取了多少個字節Encoding.UTF8.GetString(args.Buffer, 0, args.BytesTransferred);args.SetBuffer(0, args.Buffer.Length);//接受完消息 再接受下一條(socket as Socket).ReceiveAsync(args);}};socketTcp.ReceiveAsync(e4);

2、Socket實現TCP通訊流程

2.1 服務端

1)創建套接字socket(TCP)

Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

2)用Bind方法將套接字與本地地址綁定

IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socketTcp.Bind(ipPoint);

3)用Listen方法監聽

socketTcp.Listen(1024);

4)用Accept方法等待客戶端連連接

Socket socketClient = socketTcp.Accept();

5)建立連接,Accept返回新套接字

Socket socketClient = socketTcp.Accept();

6)用send和Receive相關方法收發數據

            //發送PlayerMsg msg = new PlayerMsg();msg.playerID = 666;msg.playerData = new PlayerData();msg.playerData.name = "我是NBB服務端";msg.playerData.atk = 99;msg.playerData.lev = 120;socketClient.Send(msg.Writing());//接受byte[] res = new byte[1024];int receiveNum = socketClient.Receive(res);

7)用shutdown方法釋放連接

socketClient.Shutdown(SocketShutdown.Both);

8)關閉套接字

socketClient.Close();

2.2 客戶端

1)創建套接字socket

Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

2)用connect方法與服務端相連

IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socket.Connect(ipPoint);

3)用send和Receive相關方法收發數據

        //接受數據byte[] receiveBytes = new byte[1024];int receiveNum = socket.Receive(receiveBytes);//首先解析消息的IDint msgID = BitConverter.ToInt32(receiveBytes, 0);switch (msgID){case 1001:PlayerMsg msg = new PlayerMsg();msg.Reading(receiveBytes, 4);Debug.Log(msg.playerID);Debug.Log(msg.playerData.name);Debug.Log(msg.playerData.atk);Debug.Log(msg.playerData.lev);break;default:break;}Debug.Log("收到服務端發來的消息:" + Encoding.UTF8.GetString(receiveBytes, 0, receiveNum));//發送數據socket.Send(Encoding.UTF8.GetBytes("你好,我是客戶端"));

4)用shutdown方法釋放連接

socket.Shutdown(SocketShutdown.Both);

5)關閉套接字

socket.Close();

3、簡單實現TCP通訊完整代碼

實現功能:消息區分、分包黏包、心跳消息客戶端長時間不發送消息自動斷開

3.1 消息類型區分代碼

1)BaseData
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;public abstract class BaseData
{/// <summary>/// 用于子類重寫的 獲取字節數組容器大小的方法/// </summary>/// <returns></returns>public abstract int GetBytesNum();/// <summary>/// 把成員變量序列化為 對應的字節數組/// </summary>/// <returns></returns>public abstract byte[] Writing();/// <summary>/// 把二進制字節數組 反序列化 到成員變量當中/// </summary>/// <param name="bytes">反序列化使用的字節數組</param>/// <param name="beginIndex">從該字節數組的第幾個位置開始解析 默認是 0</param>public abstract int Reading(byte[] bytes, int beginIndex = 0);/// <summary>/// 存儲int類型變量到指定的字節數組/// </summary>/// <param name="bytes">指定字節數組</param>/// <param name="value">具體的int值</param>/// <param name="index">每次存儲后用于記錄當前索引位置的變量</param>protected void WriteInt(byte[] bytes, int value, ref int index){BitConverter.GetBytes(value).CopyTo(bytes, index);index += sizeof(int);}protected void WriteShort(byte[] bytes, short value, ref int index){BitConverter.GetBytes(value).CopyTo(bytes, index);index += sizeof(short);}protected void WriteLong(byte[] bytes, long value, ref int index){BitConverter.GetBytes(value).CopyTo(bytes, index);index += sizeof(long);}protected void WriteFloat(byte[] bytes, float value, ref int index){BitConverter.GetBytes(value).CopyTo(bytes, index);index += sizeof(float);}protected void WriteByte(byte[] bytes, byte value, ref int index){bytes[index] = value;index += sizeof(byte);}protected void WriteBool(byte[] bytes, bool value, ref int index){BitConverter.GetBytes(value).CopyTo(bytes, (int)index);index += sizeof(bool);}protected void WriteString(byte[] bytes, string value, ref int index){//先存儲string字節數組長度byte[] strBytes = Encoding.UTF8.GetBytes(value);int num = strBytes.Length;BitConverter.GetBytes(num).CopyTo(bytes, index);index += sizeof(int);//在存儲 string 字節數組strBytes.CopyTo(bytes, index);index += num;}protected void WriteData(byte[] bytes, BaseData data, ref int index){data.Writing().CopyTo(bytes, index);index += data.GetBytesNum();}/// <summary>/// 根據字節數組讀取整形/// </summary>/// <param name="bytes">讀取數組</param>/// <param name="index">開始讀取的索引位置</param>/// <returns></returns>protected int ReadInt(byte[] bytes, ref int index){int value = BitConverter.ToInt32(bytes, index);index += sizeof(int);return value;}protected short ReadShort(byte[] bytes, ref int index){short value = BitConverter.ToInt16(bytes, index);index += sizeof(short);return value;}protected long ReadLong(byte[] bytes, ref int index){long value = BitConverter.ToInt64(bytes, index);index += sizeof(long);return value;}protected float ReadFloat(byte[] bytes, ref int index){float value = BitConverter.ToSingle(bytes, index);index += sizeof(float);return value;}protected byte ReadByte(byte[] bytes, ref int index){byte b = bytes[index];index++;return b;}protected bool ReadBool(byte[] bytes, ref int index){bool value = BitConverter.ToBoolean(bytes, index);index += sizeof(bool);return value;}protected string ReadString(byte[] bytes, ref int index){int length = BitConverter.ToInt32(bytes, index);index += sizeof(int);string value = Encoding.UTF8.GetString(bytes, index, length);index += length;return value;}protected T ReadData<T>(byte[] bytes, ref int index) where T : BaseData, new(){T value = new T();index += value.Reading(bytes, index);return value;}}
2)BaseMsg
using System.Collections;
using System.Collections.Generic;public class BaseMsg : BaseData
{public override int GetBytesNum(){throw new System.NotImplementedException();}public override int Reading(byte[] bytes, int beginIndex = 0){throw new System.NotImplementedException();}public override byte[] Writing(){throw new System.NotImplementedException();}public virtual int GetID(){return 0;}
}
?3)PlayerData
using System.Collections;
using System.Collections.Generic;
using System.Text;/// <summary>
/// 玩家數據類
/// </summary>
public class PlayerData : BaseData
{public string name;public int atk;public int lev;public override int GetBytesNum(){return 4 + 4 + 4 + Encoding.UTF8.GetBytes(name).Length;}public override int Reading(byte[] bytes, int beginIndex = 0){int index = beginIndex;name = ReadString(bytes, ref index);atk = ReadInt(bytes, ref index);lev = ReadInt(bytes, ref index);return index - beginIndex;}public override byte[] Writing(){int index = 0;byte[] bytes = new byte[GetBytesNum()];WriteString(bytes, name, ref index);WriteInt(bytes, atk, ref index);WriteInt(bytes, lev, ref index);return bytes;}
}
4)PlayerMsg
using System.Collections;
using System.Collections.Generic;public class PlayerMsg : BaseMsg
{public int playerID;public PlayerData playerData;public override byte[] Writing(){int index = 0;byte[] bytes = new byte[GetBytesNum()];//先寫消息IDWriteInt(bytes, GetID(), ref index);//寫消息的成員變量WriteInt(bytes, playerID, ref index);WriteData(bytes, playerData, ref index);return bytes;}public override int Reading(byte[] bytes, int beginIndex = 0){//反序列化不需要取解析ID 因為在這一步之前 就應該把ID反序列化出來//用來判斷到底使用哪一個自定義類來反序列化int index = beginIndex;playerID = ReadInt(bytes, ref index);playerData = ReadData<PlayerData>(bytes, ref index);return index - beginIndex;}public override int GetBytesNum(){return 4 + //消息ID長度4 + //palyerIDplayerData.GetBytesNum(); //playerData}/// <summary>/// 自定義的消息ID 主要用于區分是哪一個消息類/// </summary>/// <returns></returns>public override int GetID(){return 1001;}
}
?5)QuitMsg
using System.Collections;
using System.Collections.Generic;public class QuitMsg : BaseMsg
{public override int GetBytesNum(){return 8;}public override int Reading(byte[] bytes, int beginIndex = 0){return 0;}public override byte[] Writing(){int index = 0;byte[] bytes = new byte[GetBytesNum()];WriteInt(bytes, GetID(), ref index);WriteInt(bytes, 0, ref index);return bytes;}public override int GetID(){return 1003;}
}
6)HeartMsg
using System.Collections;
using System.Collections.Generic;public class HeartMsg : BaseMsg
{public override int GetBytesNum(){return 8;}public override byte[] Writing(){int index = 0;byte[] bytes = new byte[GetBytesNum()];WriteInt(bytes, GetID(), ref index);WriteInt(bytes, 0, ref index);return bytes;}public override int Reading(byte[] bytes, int beginIndex = 0){return 0;}public override int GetID(){return 999;}
}

3.2?服務端

1)ClientSocket
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace TeachTcpServerExercises2
{class ClientSocket{private static int CLIENT_BIGIN_ID = 1;public int clientID;public Socket socket;//用于處理分包時 緩存的 字節數組 和字節數組長度private byte[] cacheBytes = new byte[1024 * 1024];private int cacheNum = 0;//上一次收到消息的時間private long frontTime = -1;//超時時間private static int TIME_OUT_TIME = 10;public ClientSocket(Socket socket){this.clientID = CLIENT_BIGIN_ID;this.socket = socket;++CLIENT_BIGIN_ID;//為了方便理解 所有開一個線程專門計時 但是這種方式比較消極性能 不建議這樣使用ThreadPool.QueueUserWorkItem(CheckTimeOut);}/// <summary>/// 間隔一段時間 檢測一次超時 如果超時就會主動斷開該客戶端的連接/// </summary>/// <param name="obj"></param>private void CheckTimeOut(object obj){while (Connected){if (frontTime != -1 &&DateTime.Now.Ticks / TimeSpan.TicksPerSecond - frontTime >= TIME_OUT_TIME){Program.socket.AddDelSocket(this);break;}Thread.Sleep(5000);}}/// <summary>/// 是否是連接狀態/// </summary>public bool Connected => this.socket.Connected;//我們應該封裝一些方法//關閉public void Close(){if(socket != null){socket.Shutdown(SocketShutdown.Both);socket.Close();socket = null;}}//發送public void Send(BaseMsg info){if(Connected){try{socket.Send(info.Writing());}catch (Exception e){Console.WriteLine("發消息出錯" + e.Message);Program.socket.AddDelSocket(this);//Close();}}else{Program.socket.AddDelSocket(this);}}//接收public void Receive(){if (!Connected){Program.socket.AddDelSocket(this);return;}try{if(socket.Available > 0){byte[] res = new byte[1024 * 5];int receiveNum = socket.Receive(res);HandleReceiveMsg(res, receiveNum);收到數據后 先讀取4個字節 轉為ID 才知道用哪一個類型去處理反序列化//int msgID = BitConverter.ToInt32(res, 0);//BaseMsg msg = null;//switch (msgID)//{//    case 1001://        msg = new PlayerMsg();//        msg.Reading(res, 4);//        break;//    default://        break;//}//if (msg == null)//    return;//ThreadPool.QueueUserWorkItem(MsgHandle, msg);}}catch (Exception e){Console.WriteLine("收消息出錯" + e.Message);//解析消息錯誤 也認為 要把socket斷開了Program.socket.AddDelSocket(this);//Close();}}//處理接收消息 分包、黏包問題的方法private void HandleReceiveMsg(byte[] receiveBytes, int receiveNum){int msgID = 0;int msgLength = 0;int nowIndex = 0;//收到消息時 應該看看 之前有沒有緩存的 如果有的話 我們直接拼接到后面receiveBytes.CopyTo(cacheBytes, cacheNum);cacheNum += receiveNum;while (true){//每次將長度設置為-1 是避免上一次解析的數據 影響這一次的判斷msgLength = -1;//處理解析一條消息if (cacheNum - nowIndex >= 8){//解析IDmsgID = BitConverter.ToInt32(cacheBytes, nowIndex);nowIndex += 4;//解析長度msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);nowIndex += 4;}if (cacheNum - nowIndex >= msgLength && msgLength != -1){//解析消息體BaseMsg baseMsg = null;switch (msgID){case 1001:baseMsg = new PlayerMsg();baseMsg.Reading(cacheBytes, nowIndex);break;case 1003:baseMsg = new QuitMsg();//由于該消息沒有消息體 所有都不用反序列化break;case 999:baseMsg = new HeartMsg();//由于該消息沒有消息體 所有都不用反序列化break;default:break;}if (baseMsg != null)ThreadPool.QueueUserWorkItem(MsgHandle, baseMsg);//receiveQueue.Enqueue(baseMsg);nowIndex += msgLength;if (nowIndex == cacheNum){cacheNum = 0;break;}}else{//如果不滿足 證明有分包//那么我們需要把當前收到的內容 記錄下來//有待下次接收到消息后 再做處理//receiveBytes.CopyTo(cacheBytes, 0);//cacheNum = receiveNum;//如果進行了 ID和長度的解析 但是 沒有成功解析消息體 那么我們需要減去nowIndex移動的位置if (msgLength != -1)nowIndex -= 8;//就是把剩余沒有解析的字節數組內容 移到前面來 用于緩存下次繼續解析Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);cacheNum = cacheNum - nowIndex;break;}}}private void MsgHandle(object obj){BaseMsg msg = obj as BaseMsg;if(msg is PlayerMsg){Console.WriteLine("PlayerMeg");PlayerMsg playerMsg = msg as PlayerMsg;Console.WriteLine(playerMsg.playerID);Console.WriteLine(playerMsg.playerData.name);Console.WriteLine(playerMsg.playerData.atk);Console.WriteLine(playerMsg.playerData.lev);}else if(msg is QuitMsg){//收到斷開連接消息 把字跡添加到待移除的列表當中Program.socket.AddDelSocket(this);}else if(msg is HeartMsg){//收到心跳消息 記錄收到消息的時間frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;Console.WriteLine("收到心跳消息");}}}
}
2)ServerSocket
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace TeachTcpServerExercises2
{class ServerSocket{//服務端Socketpublic Socket socket;//客戶端連接的所有Socketpublic Dictionary<int, ClientSocket> clientDic = new Dictionary<int, ClientSocket>();//有待移除的客戶端socket 避免在foreach時直接從字典中移除 出現問題private List<ClientSocket> delList = new List<ClientSocket>();private bool isClose;//開啟服務器端public void Start(string ip, int port, int num){isClose = false;socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);socket.Bind(ipPoint);socket.Listen(num);ThreadPool.QueueUserWorkItem(Accept);ThreadPool.QueueUserWorkItem(Receive);}//關閉服務器端public void Close(){isClose = true;foreach (ClientSocket client in clientDic.Values){client.Close();}clientDic.Clear();socket.Shutdown(SocketShutdown.Both);socket.Close();socket = null;}//接受客戶端連入private void Accept(object obj){while (!isClose){try{//連入一個客戶端Socket clientSocket = socket.Accept();ClientSocket client = new ClientSocket(clientSocket);lock (clientDic){clientDic.Add(client.clientID, client);}Console.WriteLine("客戶端" + clientSocket.RemoteEndPoint + "連入服務器");}catch (Exception e){Console.WriteLine("客戶端連入報錯" + e.Message);}}}//接收客戶端消息private void Receive(object obj){while (!isClose){if(clientDic.Count > 0){lock (clientDic){foreach (ClientSocket client in clientDic.Values){client.Receive();}CloseDelListSocket();}}}}public void Broadcast(BaseMsg info){lock (clientDic){foreach (ClientSocket client in clientDic.Values){client.Send(info);}}}//添加待移除的 socket內容public void AddDelSocket(ClientSocket socket){if (!delList.Contains(socket)){delList.Add(socket);}}//判斷有沒有 斷開連接的 把其 移除public void CloseDelListSocket(){//判斷有沒有 斷開連接的 把其 移除for (int i = 0; i < delList.Count; i++){CloseClientSocket(delList[i]);}delList.Clear();}//關閉客戶端連接 從字典中移除public void CloseClientSocket(ClientSocket socket){lock (clientDic){socket.Close();if (clientDic.ContainsKey(socket.clientID)){clientDic.Remove(socket.clientID);Console.WriteLine("客戶端{0}主動斷開連接了", socket.clientID);}}}}
}
?3)啟動代碼
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace TeachTcpServerExercises2
{class Program{public static ServerSocket socket;static void Main(string[] args){socket = new ServerSocket();socket.Start("127.0.0.1", 8080, 1024);Console.WriteLine("服務器開啟成功");while (true){string input = Console.ReadLine();if(input == "Quit"){socket.Close();}else if(input.Substring(0, 2) == "B:"){if(input.Substring(2) == "1001"){PlayerMsg msg = new PlayerMsg();msg.playerID = 9833;msg.playerData = new PlayerData();msg.playerData.name = "服務器端發來的消息";msg.playerData.atk = 1313;msg.playerData.lev = 9999;socket.Broadcast(msg);}}}}}
}

3.3?客戶端

1)NetMgr
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;public class NetMgr : MonoBehaviour
{private static NetMgr instance;public static NetMgr Instance => instance;//客戶端Socketprivate Socket socket;//用于發送消息的隊列 公共容器 主線程里面放 發送線程從里面取private Queue<BaseMsg> sendMsgQueue = new Queue<BaseMsg>();//用于接受消息的隊列 公共容器 子線程往里面放 主線程從里面取private Queue<BaseMsg> receiveQueue = new Queue<BaseMsg>();用于收消息的水桶(容器)//private byte[] receiveBytes = new byte[1024 * 1024];返回收到的字節數//private int receiveNum;//用于處理分包時 緩存的 字節數組 和字節數組長度private byte[] cacheBytes = new byte[1024 * 1024];private int cacheNum = 0;//是否連接private bool isConneted = false;//發送心跳消息間隔時間private int SEND_HEARTMSG_TIME = 2;private HeartMsg heartMsg = new HeartMsg();private void Awake(){instance = this;DontDestroyOnLoad(this.gameObject);//客戶端循環定時給服務端發送心跳消息InvokeRepeating("SendHeartMsg", 0, SEND_HEARTMSG_TIME);}private void SendHeartMsg(){if(isConneted)Send(heartMsg);}// Update is called once per framevoid Update(){if(receiveQueue.Count > 0){BaseMsg msg = receiveQueue.Dequeue();if(msg is PlayerMsg){PlayerMsg playerMsg = (PlayerMsg)msg;Debug.Log(playerMsg.playerID);Debug.Log(playerMsg.playerData.name);Debug.Log(playerMsg.playerData.atk);Debug.Log(playerMsg.playerData.lev);}}}//連接服務端public void Connect(string ip, int port){//如果是連接狀態直接返回if (isConneted)return;if(socket == null)socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//連接服務端IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);try{socket.Connect(ipPoint);isConneted = true;//開啟發送線程ThreadPool.QueueUserWorkItem(SendMsg);//開啟接收線程ThreadPool.QueueUserWorkItem(ReceiveMsg);}catch (SocketException e){if (e.ErrorCode == 10061)Debug.Log("服務拒絕連接");elseDebug.Log("連接失敗:" + e.ErrorCode + e.Message);}}//發送消息public void Send(BaseMsg msg){sendMsgQueue.Enqueue(msg);}/// <summary>/// 用于測試直接發字節數組的方法/// </summary>/// <param name="bytes"></param>public void SendTest(byte[] bytes){socket.Send(bytes);}private void SendMsg(object obj){while (isConneted){if(sendMsgQueue.Count > 0){socket.Send(sendMsgQueue.Dequeue().Writing());}}}//不停的接受消息private void ReceiveMsg(object obj){while (isConneted){if(socket.Available > 0){byte[] receiveBytes = new byte[1024 * 1024];int receiveNum = socket.Receive(receiveBytes);首先把收到字節數組的前4個字節 讀取出來得到ID//int msgID = BitConverter.ToInt32(receiveBytes, 0);//BaseMsg baseMsg = null;//switch (msgID)//{//    case 1001://        PlayerMsg msg = new PlayerMsg();//        msg.Reading(receiveBytes, 4);//        baseMsg = msg;//        break;//    default://        break;//}如果消息為空 那證明是不知道類型的消息 沒有解析//if (baseMsg == null)//    continue;收到消息 解析消息為字符串 并放入公共容器//receiveQueue.Enqueue(baseMsg);HandleReceiveMsg(receiveBytes, receiveNum);}}}//處理接收消息 分包、黏包問題的方法private void HandleReceiveMsg(byte[] receiveBytes, int receiveNum){int msgID = 0;int msgLength = 0;int nowIndex = 0;//收到消息時 應該看看 之前有沒有緩存的 如果有的話 我們直接拼接到后面receiveBytes.CopyTo(cacheBytes, cacheNum);cacheNum += receiveNum;while (true){//每次將長度設置為-1 是避免上一次解析的數據 影響這一次的判斷msgLength = -1;//處理解析一條消息if(cacheNum - nowIndex >= 8){//解析IDmsgID = BitConverter.ToInt32(cacheBytes, nowIndex);nowIndex += 4;//解析長度msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);nowIndex += 4;}if(cacheNum - nowIndex >= msgLength && msgLength != -1){//解析消息體BaseMsg baseMsg = null;switch (msgID){case 1001:PlayerMsg msg = new PlayerMsg();msg.Reading(cacheBytes, nowIndex);baseMsg = msg;break;default:break;}if (baseMsg != null)receiveQueue.Enqueue(baseMsg);nowIndex += msgLength;if (nowIndex == cacheNum){cacheNum = 0;break;}}else{//如果不滿足 證明有分包//那么我們需要把當前收到的內容 記錄下來//有待下次接收到消息后 再做處理//receiveBytes.CopyTo(cacheBytes, 0);//cacheNum = receiveNum;//如果進行了 ID和長度的解析 但是 沒有成功解析消息體 那么我們需要減去nowIndex移動的位置if (msgLength != -1)nowIndex -= 8;//就是把剩余沒有解析的字節數組內容 移到前面來 用于緩存下次繼續解析Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);cacheNum = cacheNum - nowIndex;break;}}}public void Close(){if(socket != null){Debug.Log("客戶端主動斷開連接");//主動發送一條斷開連接的消息給服務端//QuitMsg quitMsg = new QuitMsg();//socket.Send(quitMsg.Writing());//socket.Shutdown(SocketShutdown.Both);//socket.Disconnect(false);//socket.Close();socket = null;isConneted = false;}}private void OnDestroy(){Close();}}
2)啟動代碼
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Main : MonoBehaviour
{// Start is called before the first frame updatevoid Start(){if(NetMgr.Instance == null){GameObject obj = new GameObject("Net");obj.AddComponent<NetMgr>();}NetMgr.Instance.Connect("127.0.0.1", 8080);}// Update is called once per framevoid Update(){}
}
3)消息發送測試代碼

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR;public class Lesson7 : MonoBehaviour
{public Button btn;public Button btn1;public Button btn2;public Button btn3;public InputField input;// Start is called before the first frame updatevoid Start(){btn.onClick.AddListener(() =>{if(input.text != ""){PlayerMsg ms = new PlayerMsg();ms.playerID = 123123;ms.playerData = new PlayerData();ms.playerData.name = "hellow";ms.playerData.atk = 0;ms.playerData.lev = 1;NetMgr.Instance.Send(ms);}});//黏包測試btn1.onClick.AddListener(() =>{PlayerMsg msg = new PlayerMsg();msg.playerID = 1001;msg.playerData = new PlayerData();msg.playerData.name = "當老師1";msg.playerData.atk = 1;msg.playerData.lev = 1;PlayerMsg msg2 = new PlayerMsg();msg2.playerID = 1002;msg2.playerData = new PlayerData();msg2.playerData.name = "當老師2";msg2.playerData.atk = 2;msg2.playerData.lev = 2;byte[] bytes = new byte[msg.GetBytesNum() + msg2.GetBytesNum()];msg.Writing().CopyTo(bytes, 0);msg2.Writing().CopyTo(bytes, msg.GetBytesNum());NetMgr.Instance.SendTest(bytes);});//分包測試btn2.onClick.AddListener(async () =>{PlayerMsg msg = new PlayerMsg();msg.playerID = 1001;msg.playerData = new PlayerData();msg.playerData.name = "當老師1";msg.playerData.atk = 1;msg.playerData.lev = 1;byte[] bytes = msg.Writing();//分包byte[] bytes1 = new byte[10];byte[] bytes2 = new byte[bytes.Length - 10];//分成第一個包Array.Copy(bytes, 0, bytes1, 0, 10);//分第二個包Array.Copy(bytes, 10, bytes2 , 0, bytes.Length - 10);NetMgr.Instance.SendTest(bytes1);await Task.Delay(500);NetMgr.Instance.SendTest(bytes2);});//分包、黏包測試btn3.onClick.AddListener(async () =>{PlayerMsg msg = new PlayerMsg();msg.playerID = 1001;msg.playerData = new PlayerData();msg.playerData.name = "當老師1";msg.playerData.atk = 1;msg.playerData.lev = 1;PlayerMsg msg2 = new PlayerMsg();msg2.playerID = 1002;msg2.playerData = new PlayerData();msg2.playerData.name = "當老師2";msg2.playerData.atk = 2;msg2.playerData.lev = 2;byte[] bytes1 = msg.Writing();//消息Abyte[] bytes2 = msg2.Writing();//消息Bbyte[] bytes2_1 = new byte[10];byte[] bytes2_2 = new byte[bytes2.Length - 10];//分成第一個包Array.Copy(bytes2, 0, bytes2_1, 0, 10);//分第二個包Array.Copy(bytes2, 10, bytes2_2, 0, bytes2.Length - 10);//消息A和消息B前一段的 黏包byte[] bytes = new byte[bytes1.Length + bytes2_1.Length];bytes1.CopyTo(bytes, 0);bytes2_1.CopyTo(bytes, bytes1.Length);NetMgr.Instance.SendTest(bytes);await Task.Delay(1000);NetMgr.Instance.SendTest(bytes2_2);});}// Update is called once per framevoid Update(){}
}

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

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

相關文章

青少年編程與數學 02-013 初中數學知識點 02課題、概要

青少年編程與數學 02-013 初中數學知識點 02課題、概要 一、數與代數二、圖形與幾何三、統計與概率四、綜合與實踐五、課程理念與目標 根據2022年版義務教育數學課程標準&#xff0c;初中數學知識點可以總結為以下四大領域。 一、數與代數 數與式 有理數與實數&#xff1a;理解…

深入探索 libarchive

深入探索 libarchive&#xff1a;跨平臺歸檔處理的終極解決方案 一、背景與歷史沿革 1.1 歸檔處理的演進之路 從1979年tar格式的誕生到現代云存儲時代&#xff0c;歸檔技術經歷了四個關鍵階段&#xff1a; Unix時代&#xff1a;tar/cpio主導系統備份互聯網黎明期&#xff1…

2025最新“科研創新與智能化轉型“暨AI智能體開發與大語言模型的本地化部署、優化技術實踐

第一章、智能體(Agent)入門 1、智能體&#xff08;Agent&#xff09;概述&#xff08;什么是智能體&#xff1f;智能體的類型和應用場景、典型的智能體應用&#xff0c;如&#xff1a;Google Data Science Agent等&#xff09; 2、智能體&#xff08;Agent&#xff09;與大語…

Yolo_v8的安裝測試

前言 如何安裝Python版本的Yolo&#xff0c;有一段時間不用了&#xff0c;Yolo的版本也在不斷地發展&#xff0c;所以重新安裝了運行了一下&#xff0c;記錄了下來&#xff0c;供參考。 一、搭建環境 1.1、創建Pycharm工程 首先創建好一個空白的工程&#xff0c;如下圖&…

時尚界正在試圖用AI,創造更多沖擊力

數字藝術正以深度融合的方式&#xff0c;在時尚、游戲、影視等行業實現跨界合作&#xff0c;催生了多樣化的商業模式&#xff0c;為創作者和品牌帶來更多機會&#xff0c;數字藝術更是突破了傳統藝術的限制&#xff0c;以趣味觸達用戶&#xff0c;尤其吸引了年輕一代的消費群體…

藍橋杯省模擬賽 01串個數

問題描述 請問有多少個長度為 24 的 01 串&#xff0c;滿足任意 5 個連續的位置中不超過 3 個位置的值為 1。 所有長度為24的01串組合有2*24種 思路&#xff1a;遍歷所有長度為24的01串組合&#xff0c;選擇出符合題意的 #include<iostream> #include<cmath> us…

【軟考備考】系統架構設計論文完整范文示例

本文由AI輔助創造 題目:基于微服務與云原生的智慧政務平臺架構設計與實踐 摘要(約300字) 本文以某省級智慧政務平臺建設項目為背景,針對傳統政務系統存在的"信息孤島"、擴展性差、維護成本高等問題,提出了一套基于微服務與云原生技術的解決方案。通過領域驅動…

數據庫原理及應用mysql版陳業斌實驗二

&#x1f3dd;?專欄&#xff1a;Mysql_貓咪-9527的博客-CSDN博客 &#x1f305;主頁&#xff1a;貓咪-9527-CSDN博客 “欲窮千里目&#xff0c;更上一層樓。會當凌絕頂&#xff0c;一覽眾山小。” 目錄 實驗二單表查詢 1.實驗數據如下 student 表&#xff08;學生表&#…

SDL —— 將sdl渲染畫面嵌入Qt窗口顯示(附:源碼)

?? SDL/SDL2 相關技術、疑難雜癥文章合集(掌握后可自封大俠 ?_?)(記得收藏,持續更新中…) 效果 使用QWidget加載了SDL的窗口,渲染器使用硬件加速跑GPU的。支持Qt窗口縮放或顯示隱藏均不影響SDL的圖像刷新。 ? 操作步驟 1、在創建C++空工程時加入SDL,引入頭文件時需…

C語言之鏈表增刪查改

1.知識百科 鏈表&#xff08;Linked List&#xff09;是計算機科學中一種基礎的數據結構&#xff0c;通過節點&#xff08;Node&#xff09;的鏈式連接來存儲數據。每個節點包含兩部分&#xff1a;存儲數據的元素和指向下一個節點的指針&#xff08;單鏈表&#xff09;或前后兩…

Windows環境下AnythingLLM安裝與Ollama+DeepSeek集成指南

前面已經完成了Ollama的安裝并下載了deepseek大模型包&#xff0c;下面介紹如何與anythingLLM 集成 Windows環境下AnythingLLM安裝與OllamaDeepSeek集成指南 一、安裝準備 1. 硬件要求 如上文說明 2. 前置條件 已安裝Ollama并下載DeepSeek模型&#xff08;如deepseek-r1:…

當貝AI知識庫評測 AI如何讓知識檢索快人一步

近日,國內領先的人工智能服務商當貝AI正式推出“個人知識庫”功能,這一創新性工具迅速引發行業關注。在信息爆炸的時代,如何高效管理個人知識資產、快速獲取精準答案成為用戶的核心需求。當貝AI通過將“閉卷考試”變為“開卷考試”的獨特設計,為用戶打造了一個高度個性化的智能…

HarmonyOS NEXT——【鴻蒙原生應用加載Web頁面】

鴻蒙客戶端加載Web頁面&#xff1a; 在鴻蒙原生應用中&#xff0c;我們需要使用前端頁面做混合開發&#xff0c;方法之一是使用Web組件直接加載前端頁面&#xff0c;其中WebView提供了一系列相關的方法適配鴻蒙原生與web之間的使用。 效果 web頁面展示&#xff1a; Column()…

嵌入式開發場景中Shell腳本執行方式的對比

?Shell腳本執行方式對比表? ?執行方式??命令示例??是否需要執行權限??是否啟動子Shell??環境變量影響范圍??適用場景??嵌入式開發中的典型應用??直接執行腳本?./script.sh是是子Shell內有效獨立運行的腳本&#xff0c;需固定環境自動化構建腳本&#xff08;…

MES系統需要采集的數據及如何采集

?數據采集在企業信息化建設中占據著舉足輕重的地位&#xff0c;是實現物料跟蹤、生產計劃制定、產品歷史記錄維護以及其他生產管理活動的基石。數據的準確性和實時性直接關系到企業信息化能否成功落地&#xff0c;是企業邁向高效生產的關鍵因素。 數據收集對于MES制造執行系統…

閉環管理:借助數字化管理平臺實現客戶反饋的價值升級

在競爭激烈的市場環境中&#xff0c;客戶反饋已成為企業優化服務、提升競爭力的核心資源。如何高效處理客戶反饋&#xff0c;將其轉化為企業持續改進的動力&#xff0c;是每個企業面臨的重要課題。作為服務管理數字化轉型服務商&#xff0c;瑞云服務云為大中型企業提供了一套完…

C++Primer學習(13.6 對象移動)

13.6 對象移動 新標準的一個最主要的特性是可以移動而非拷貝對象的能力。如我們在13.1.1節(第440頁)中所見&#xff0c;很多情況下都會發生對象拷貝。在其中某些情況下&#xff0c;對象拷貝后就立即被銷毀了。在這些情況下&#xff0c;移動而非拷貝對象會大幅度提升性能。 如我…

Uni-app頁面信息與元素影響解析

獲取窗口信息uni.getWindowInfo {pixelRatio: 3safeArea:{bottom: 778height: 731left: 0right: 375top: 47width: 375}safeAreaInsets: {top: 47, left: 0, right: 0, bottom: 34},screenHeight: 812,screenTop: 0,screenWidth: 375,statusBarHeight: 47,windowBottom: 0,win…

大模型 API 調用中的流式輸出與非流式輸出全面對比:原理、場景與最佳實踐

流式輸出與非流式輸出應用場景 流式輸出的理想應用場景 實時對話系統聊天機器人和虛擬助手客服系統和用戶支持平臺實時問答和教育輔導應用 漸進式內容生成代碼補全和編程輔助工具&#xff08;如 GitHub Copilot&#xff09;實時文檔協作和編輯系統創意寫作和內容創作平臺 用戶…

Problem A: 計算奇數和

補充&#xff08;牢騷&#xff09;&#xff1a; 必須要 Main 類&#xff0c;自己自定義的類不能跑&#xff0c;說實話我被惡心到了&#xff0c;真沒力扣好用。后面都默認為Main 類。真惡心&#xff0c;其實不止這一點。。。 1.題目問題 2.輸入 3.輸出 4.樣例 5.代碼實現 imp…