【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(){}
}
相关文章:
【Unity网络编程知识】使用Socket实现简单TCP通讯
1、Socket的常用属性和方法 创建Socket TCP流套接字 Socket socketTcp new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 1.1 常用属性 1)套接字的连接状态 socketTcp.Connected 2)获取套接字的类型 socketTcp.So…...
深入解析SSL/TLS证书:构建现代网络安全的密码学基石1.1 TLS 1.3握手流程优化
一、协议层深度解析:TLS握手全流程拆解 1.1 TLS 1.3握手流程优化 (此处展示TLS 1.3握手流程图,使用Mermaid语法) Client Server ClientHello (supported_versions, cipher_suites, key_share) ServerHello (selected_version, ci…...
【深度学习】GAN生成对抗网络:原理、应用与发展
GAN生成对抗网络:原理、应用与发展 文章目录 GAN生成对抗网络:原理、应用与发展1. 引言2. GAN的基本原理2.1 核心思想2.2 数学表达2.3 训练过程 3. GAN的主要变体3.1 DCGAN (Deep Convolutional GAN)3.2 CGAN (Conditional GAN)3.3 CycleGAN3.4 StyleGAN…...
Live555+Windows+MSys2 编译Androidso库和运行使用(二,实验篇)
文章目录 实验下载推流服务端版本运行 摘要:书接上回 https://blog.csdn.net/qq_20330595/article/details/146412411?spm1001.2014.3001.5502 我们先做几个试验,方便我们理解rtsp推流,先把采集和播放体验一下,我们最后回到代码…...
树莓派ollama docker报错尝试网上方法
ollama ps运行bash:ollama:未找到命令 sudo curl -sSL https://get.docker.com | sh 显示“连接被对方重置” sudo docker run hello-world测试 sudo: docker找不到命令 ollama pull qwen2:7b push:ollama 未找到命令 安装 docker sudo apt update sudo apt …...
工作杂谈(十七)——研发阶段术语
EVT/DVT/PVT/MP是指在制造行业一个产品研发导入从试产到量产的不同阶段: EVT:Engineering Verification Test工程验证测试阶段 DVT:Design Verification Test设计验证测试 PVT:Production Verification Test 小批量生…...
Spring MVC:从历史演变到实战入门
1. Java Web的发展历史与MVC模式 1.1 Model I与Model II的演进 Model I(JSPJavaBean) 作为早期Java Web开发的主流模式,其核心架构如下: graph LR A[客户端] --> B[JSP页面] B --> C{业务逻辑} C --> D[JavaBean] D -…...
C++中的std::move函数详解:移动语义与资源管理
在C中,std::move 是一个用于将对象转换为右值引用的函数模板,通常用于实现资源的转移(如动态内存、文件句柄等),从而避免不必要的拷贝操作。std::move 是C11引入的一个重要特性,主要用于支持移动语义。 目录…...
2025 polarctf春季个人挑战赛web方向wp
来个弹窗 先用最基础的xss弹窗试一下 <script>alert("xss")</script>没有内容,猜测过滤了script,双写绕过一下 <scrscriptipt>alert("xss")</scscriptript>background 查看网页源代码 查看一下js文件 类…...
RabbitMQ 学习整理1 - 基础使用
项目代码:RabbitMQDemo: 学习RabbitMQ的一些整理 基本概念 RabbitMQ是一种基于AMQP协议的消息队列实现框架RabbitMQ可以用于在系统与系统之间或者微服务节点之间,进行消息缓存,消息广播,消息分配以及限流消峰处理RabbitMQ-Serve…...
分布式渲染与云渲染:技术与应用的黄金搭档
一、核心概念:先区分再关联 分布式渲染是通过多台设备并行计算拆分渲染任务的技术(如将一帧拆分为 64 个小块,64 台电脑同时渲染); 云渲染是基于云计算的渲染服务,本质是分布式渲染的商业化落地—— 用户无…...
【实战ES】实战 Elasticsearch:快速上手与深度实践-5.2.1 多字段权重控制(标题、品牌、类目)
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 电商商品搜索实战:多字段权重控制策略1. 业务场景与核心挑战1.1 典型搜索问题1.2 权重失衡的影响数据 2. 权重控制核心方案2.1 字段权重分配矩阵2.2 多策略组合方…...
如何避免测试数据准备不充分或不可复用
避免测试数据准备不充分或不可复用的关键方法包括明确数据需求、统一数据管理工具、建立数据复用机制、定期维护更新测试数据以及加强团队沟通与协作。 其中,统一数据管理工具对确保数据质量和复用性尤为重要。例如,许多团队采用专门的测试数据管理工具以…...
使用AI一步一步实现若依(23)
功能23:从后端获取路由/菜单数据 功能22:用户管理 功能21:使用axios发送请求 功能20:使用分页插件 功能19:集成MyBatis-Plus 功能18:创建后端工程 功能17:菜单管理 功能16:角色管理…...
C语言的内存模型 (堆区,栈区,静态区,常量区,代码区 )概念讲解
C语言的内存模型分为5个区: 堆区,栈区,静态区,常量区,代码区 。 1、栈区 存放函数的参数值,局部变量等。 由编译器自动分配和释放。通常在函数执行完了就释放了。其操作方式类似于数据结构中的栈。栈内存…...
Vue3 知识点总结
Vue3 知识点总结 1. 核心概念 1.1 Composition API 1.1.1 setup 函数 setup是Vue3中的新的配置项,是组件内使用Composition API的入口在setup中定义的变量和方法需要return才能在模板中使用setup执行时机在beforeCreate之前,this不可用 export defa…...
第一天学爬虫
阅读提示:我今天才开始尝试爬虫,写的不好请见谅。 一、准备工具 requests库:发送HTTP请求并获取网页内容。BeautifulSoup库:解析HTML页面并提取数据。pandas库:保存抓取到的数据到CSV文件中。 二、爬取步骤 发送请求…...
W、M、C练题笔记(持续更新中)
web here are the flag 点击,页面跳转404.php,用bp抓包访问/flag.php页面,得到flag用base64解码 TryToFindFlag 打开后查看源代码 发现是robots协议,访问robots.txt 访问flllaaa......,得到空白页面,查看…...
CVE-2021-45232未授权接口练习笔记
CVE-2021-45232 是 Apache APISIX Dashboard 中的一个严重权限漏洞,类似于攻击者无需密码即可拿到整个网关系统的“万能钥匙”。攻击者利用此漏洞,可直接操控网关流量转发规则,甚至远程执行代码,引发服务器沦陷。 默认账户密码导致…...
贪心算法——c#
贪心算法通俗解释 贪心算法是一种"每一步都选择当前最优解"的算法策略。它不关心全局是否最优,而是通过局部最优的累积来逼近最终解。优点是简单高效,缺点是可能无法得到全局最优解。 一句话秒懂 自动售货机找零钱:用最少数量的…...
Retrofit中scalars转换html为字符串
简介 在Retrofit中,如果你想直接获取HTML或其他文本格式的响应内容而不是将其映射到一个模型类,ScalarsConverterFactory 就派上用场了。ScalarsConverterFactory 是一个转换器工厂,它能够将响应体转换为Java基本类型如String、Integer或Byte…...
【微服务架构】SpringCloud(七):配置中心 Spring Cloud Config
文章目录 配置中心为什么需要配置中心配置中心介绍 服务搭建基于GITHUB1.创建仓库2.新建微服务作为配置中心服务3.启动测试拉取 匹配规则分支读取 客户端配置配置文件引入依赖使用远程配置 刷新配置手动配置热更新自动刷新erlang安装RabbitMQ安装环境变量管理界面服务配置测试 …...
突破次元壁:基于Unity的MCP方案,用Claude一键生成完整游戏
在当今快速发展的技术领域,AI与游戏开发的结合正带来前所未有的创新。今天,我们将介绍一种革命性的解决方案——基于Unity的MCP(Model-Code-Pipeline)方案,通过Claude的强大自然语言处理能力,直接生成可玩的游戏!只需简单输入提示词,AI就能自动打开Unity并为你开发出一…...
Linux学习笔记(应用篇二)
基于I.MX6ULL.MINI开发板 开发板与电脑相互通信电脑与开发板互传文件 开发板与电脑相互通信 用网线将电脑与开发板连接 本人使用的是Ubuntu系统,不是虚拟机 一般来说刚开始电脑和开发板是ping不通的 首先查看电脑的 IP WinR,cmd调出终端 我使用的是…...
记录一次部署k3s后,服务404 page not found,nginx显示正常
服务部署k3s后,正常入口端怎么返回都是80,且返回错误 TRAEFIK DEFAULT CERT ERR_CERT_AUTHORITY_INVALID ngnix显示也是正常,怎么找也找不到问题 后来通过 iptables -L -n -t nat|grep 80 发现入口端流量被DNAT转到新的服务 而k3s中&#…...
mac上安装nvm及nvm的基本语法使用!!
种一棵树,最好是十年前,其次是现在!想要改变,从此刻开始,一切都不晚! 目录 nvm是什么?前提条件:安装homebrew如果系统已经有node版本:在mac上安装nvm:用nvm安…...
(基本常识)C++中const与引用——面试常问
作者:求一个demo 版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处 内容通俗易懂,没有废话,文章最后是面试常问内容(建议通过标题目录学习) 废话不多…...
ES 加入高亮设置
searchTextQueryOne new MatchQuery.Builder().field(searchFieldOne).query(searchText).build();// 帮助中心文档切分 只查询6条Integer finalTopK 10;List<String> newReturnFileds returnFields;newReturnFileds.add("kid"); // 需要返回kidHighlight h…...
dfs(深度优先)——太抽象了
1. 两种方法 #include<bits/stdc.h> using namespace std; //void dfs(int index,int n,vector<int> current) //{ // if(index>n){ // for(int i0;i<current.size();i){ // cout<<current[i]<<" "; // } // cout<<endl;…...
5分钟学会interface(纯标题党)
Golang中的interface(接口) 接口的定义 在 Go 语言中,接口(interface) 是一种特殊的类型,它定义了一组方法,而不关心具体的实现。任何类型只要实现了这些方法,就可以被认为满足这个…...
