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(){}
}