Unity网络开发基础

概述

基础知识

网络开发必备理论

网络基本概念

IP、端口、Mac地址

客户端和服务器

数据通信模型

网络协议

网络协议概述

OSI模型

TCP/IP协议

TCP/IP协议

TCP和UDP

网络通信

网络游戏通信方案概述

通信前的必备知识

IP地址和端口类

域名解析

序列化和反序列化2进制

概述

字符编码

序列化

BaseData 基类

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;public abstract class BaseData 
{/// <summary>/// 用于子类重写的 获取字节数组容量大小的方法/// </summary>/// <returns></returns>public abstract int GetByteNum();/// <summary>/// 把成员变量 序列化为 对应的字节数组/// </summary>/// <returns></returns>public abstract byte[] Writing();/// <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);}// 存储 shortprotected void WriteShort(byte[] bytes, short value, ref int index){BitConverter.GetBytes(value).CopyTo(bytes, index);index += sizeof(short);}// 存储 longprotected void WriteLong(byte[] bytes, long value, ref int index){BitConverter.GetBytes(value).CopyTo(bytes, index);index += sizeof(long);}//存储 floatprotected void WriteFloat(byte[] bytes, float value, ref int index){BitConverter.GetBytes(value).CopyTo(bytes, index);index += sizeof(float);}//存储 byteprotected void WriteByte(byte[] bytes, byte value, ref int index){bytes[index] = value;index += 1;}//存储 boolprotected void WriteBool(byte[] bytes, bool value, ref int index){BitConverter.GetBytes(value).CopyTo(bytes, index);index += sizeof(bool);}//存储 字符串protected void WriteString(byte[] bytes, string value, ref int index){//先存储string字节数组的长度byte[] strBytes = Encoding.UTF8.GetBytes(value);WriteInt(bytes, strBytes.Length, ref index);//再存 string字节数组strBytes.CopyTo(bytes, index);index += strBytes.Length;}//存储自定义类protected void WriteData(byte[] bytes, BaseData data, ref int index){data.Writing().CopyTo(bytes, index);index += data.GetByteNum();}
}

测试代码

using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;public class Test : BaseData
{public short lev;public Player p;public int hp;public string name;public bool sex;public override int GetByteNum(){return sizeof(short) + p.GetByteNum() + sizeof(int) +4 + Encoding.UTF8.GetBytes(name).Length + sizeof(bool);}public override byte[] Writing(){int index = 0;byte[] bytes = new byte[GetByteNum()];WriteShort(bytes, lev, ref index);WriteData(bytes, p, ref index);WriteInt(bytes, hp, ref index);WriteString(bytes, name, ref index);WriteBool(bytes, sex, ref index);return bytes;}}public class Player : BaseData
{public int atk;public override int GetByteNum(){return sizeof(int);}public override byte[] Writing(){int index = 0;byte[] bytes = new byte[GetByteNum()];WriteInt(bytes, atk, ref index);return bytes;}
}
反序列化

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;public abstract class BaseData 
{/// <summary>/// 用于子类重写的 获取字节数组容量大小的方法/// </summary>/// <returns></returns>public abstract int GetByteNum();/// <summary>/// 把成员变量 序列化为 对应的字节数组/// </summary>/// <returns></returns>public abstract byte[] Writing();/// <summary>/// 把2进制字节数组 反序列化到 成员变量当中/// </summary>/// <param name="bytes">反序列化使用的字节数组</param>/// <param name="beginIndex">从第几个位置开始解析</param>public abstract int Reading(byte[] bytes, int beginIndex = 0);#region 序列化相关方法/// <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);}// 存储 shortprotected void WriteShort(byte[] bytes, short value, ref int index){BitConverter.GetBytes(value).CopyTo(bytes, index);index += sizeof(short);}// 存储 longprotected void WriteLong(byte[] bytes, long value, ref int index){BitConverter.GetBytes(value).CopyTo(bytes, index);index += sizeof(long);}//存储 floatprotected void WriteFloat(byte[] bytes, float value, ref int index){BitConverter.GetBytes(value).CopyTo(bytes, index);index += sizeof(float);}//存储 byteprotected void WriteByte(byte[] bytes, byte value, ref int index){bytes[index] = value;index += 1;}//存储 boolprotected void WriteBool(byte[] bytes, bool value, ref int index){BitConverter.GetBytes(value).CopyTo(bytes, index);index += sizeof(bool);}//存储 字符串protected void WriteString(byte[] bytes, string value, ref int index){//先存储string字节数组的长度byte[] strBytes = Encoding.UTF8.GetBytes(value);WriteInt(bytes, strBytes.Length, ref index);//再存 string字节数组strBytes.CopyTo(bytes, index);index += strBytes.Length;}//存储自定义类protected void WriteData(byte[] bytes, BaseData data, ref int index){data.Writing().CopyTo(bytes, index);index += data.GetByteNum();}#endregion#region 反序列化相关方法/// <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;}// 都 shortprotected short ReadShort(byte[] bytes, ref int index){short value = BitConverter.ToInt16(bytes, index);index += sizeof(short);return value;}//读 longprotected long ReadLong(byte[] bytes, ref int index){long value = BitConverter.ToInt64(bytes, index);index += sizeof(long);return value;}//读 floatprotected float ReadFloat(byte[] bytes, ref int index){float value = BitConverter.ToSingle(bytes, index);index += sizeof(float);return value;}//读 byteprotected byte ReadByte(byte[] bytes, ref int index){byte value = bytes[index];index += 1;return value;}//读 boolprotected bool ReadBool(byte[] bytes, ref int index){bool value = BitConverter.ToBoolean(bytes, index);index += sizeof(bool);return value;}//读  stringprotected string ReadString(byte[] bytes, ref int index){//先读取长度int length = ReadInt(bytes, ref index);//再读取stringstring 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;}#endregion}

测试:

套接字Socket

Socket的主要API

TCP通信

概述

同步

服务端

不需要在Unity中学习,服务端知识点可以直接创建 VS项目来编写

客户端

服务端综合练习

练习一:

用到线程和线程池

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 StudyTcpServerExercises
{class Program{static Socket socket;//用于存储 客户端连入的 Socket 之后可以获取它们来进行通信static List<Socket> clientSockets = new List<Socket>();//关闭线程的标识符static bool isClose = false;static void Main(string[] args){//1.建立Socket 绑定 监听socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9090);socket.Bind(ipPoint);socket.Listen(1024);//2.等待客户端连接 (需要特别处理)Thread acceptThread = new Thread(AcceptClientConnect);acceptThread.Start();//3.收发消息 (需要特别处理)Thread receiveThread = new Thread(ReceiveMsg);receiveThread.Start();//4.关闭相关while (true){string input = Console.ReadLine();//定义一个规则 关闭服务器 断开所有连接if (input == "Quit"){for (int i = 0; i < clientSockets.Count; i++){clientSockets[i].Shutdown(SocketShutdown.Both);clientSockets[i].Close();}clientSockets.Clear();isClose = true;break;}//定义一个规则 广播消息 就是让所有客户端收到服务端发送的消息if (input.Substring(0, 2) == "B:")  //从第0个索引读取到第2个索引{for (int i = 0; i < clientSockets.Count; i++){clientSockets[i].Send(Encoding.UTF8.GetBytes(input.Substring(2)));}}}}//单独为等待客户端连接 开的线程static void AcceptClientConnect(){while (!isClose){Socket clientSocket = socket.Accept();clientSockets.Add(clientSocket);clientSocket.Send(Encoding.UTF8.GetBytes("欢迎连入服务端!"));}}//单独为接收消息 开的线程static void ReceiveMsg(){Socket clientSocket;byte[] result = new byte[1024 * 1024];int receiveNum;int i;while (!isClose){for (i = 0; i < clientSockets.Count; i++){clientSocket = clientSockets[i];//判断 该socket是否有可以接收的消息 返回值就是字节数if(clientSocket.Available > 0){receiveNum = clientSocket.Receive(result);//如果直接在这收取信息 就处理 可能会造成问题//不能够及时的处理其他人的消息//为了不影响别人消息的处理,我们把消息处理 交给新的线程//为了节约线程相关的开销,我们使用线程池ThreadPool.QueueUserWorkItem(HandleMsg, (clientSocket, Encoding.UTF8.GetString(result, 0, receiveNum)));}}}}//消息处理 的线程static void HandleMsg(object obj){(Socket s, string str) info = ((Socket s, string str))obj;Console.WriteLine("收到客户端{0}发来的信息:{1}", info.s.RemoteEndPoint, info.str);}}
}

练习二:

客户端Socket

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace StudyTcpServerExercise2
{class ClientSocket{private static int CLIENT_BEGIN_ID = 1;public int clientID;public Socket socket;public ClientSocket(Socket socket){clientID = CLIENT_BEGIN_ID;this.socket = socket;++CLIENT_BEGIN_ID;}/// <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(string info){if(socket != null){try{socket.Send(Encoding.UTF8.GetBytes(info));}catch (Exception e){Console.WriteLine("发送消息出错:" + e.Message);Close();}}}//接收public void Receive(){if (socket == null)return;try{if (socket.Available > 0)  //即将收到的消息大小 大于0{byte[] result = new byte[1024 * 5];int receiveNum = socket.Receive(result);ThreadPool.QueueUserWorkItem(MsgHandle, Encoding.UTF8.GetString(result, 0, receiveNum));}}catch (Exception e){Console.WriteLine("收消息出错:" + e.Message);Close();                }}//处理收到数据的 单独线程private void MsgHandle(object obj){string str = obj as string;Console.WriteLine("收到客户端{0}发来的消息{1}", socket.RemoteEndPoint, str);}}
}

服务端Socket

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 StudyTcpServerExercise2
{class ServerSocket{//服务端 Socketpublic Socket socket;//客户端连接的所有Socketpublic Dictionary<int, ClientSocket> clientDic = new Dictionary<int, ClientSocket>();//停止 While循环的标识符private bool isClose;/// <summary>/// 开启服务器端/// </summary>/// <param name="ip">IP地址</param>/// <param name="port">端口号</param>/// <param name="num">最大容量</param>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);client.Send("欢迎连入服务器");clientDic.Add(client.clientID, client);}catch (Exception e){Console.WriteLine("客户端连入报错:" + e.Message);}}}//接收客户端消息 的单独线程private void Receive(object obj){while (!isClose){if (clientDic.Count > 0){foreach (ClientSocket client in clientDic.Values){byte[] bytes = new byte[1024 * 1024];client.Receive();}}}}// 广播的方法public void Broadcast(string info){foreach (ClientSocket client in clientDic.Values){client.Send(info);}}}
}

调用测试

客户端综合练习

创建 NetMgr 类

using System.Collections;
using System.Collections.Generic;
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<string> sendMsgQueue = new Queue<string>();//用于接收消息的对象 公共容器 子线程往里面放 主线程从里面取private Queue<string> receiveQueue = new Queue<string>();//用于收消息的容器private byte[] receiveBytes = new byte[1024 * 1024];//返回收到的字节数private int receiveNum;//是否连接private bool isConnected = false;void Awake(){instance = this;DontDestroyOnLoad(this.gameObject);}// Update is called once per framevoid Update(){//取出消息if (receiveQueue.Count > 0){print(receiveQueue.Dequeue());}}//连接服务端public void Connect(string ip, int port){//如果是连接状态 就直接返回if (isConnected)return;if(socket == null)socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//连接服务端try{socket.Connect(ip, port);isConnected = true;//开启发送线程ThreadPool.QueueUserWorkItem(SendMsg);//开启接收线程ThreadPool.QueueUserWorkItem(ReceiveMsg);}catch (SocketException e){if (e.ErrorCode == 10061)print("服务器拒绝连接");elseprint("连接失败" + e.ErrorCode + e.Message);}}//发送消息public void Send(string info){sendMsgQueue.Enqueue(info);}private void SendMsg(object obj){while (isConnected){if (sendMsgQueue.Count > 0){socket.Send(Encoding.UTF8.GetBytes(sendMsgQueue.Dequeue()));}}}//不停的接收消息private void ReceiveMsg(object obj){while (isConnected){if (socket.Available > 0){receiveNum = socket.Receive(receiveBytes);//收到消息 解析消息为字符串 并放入公共容器receiveQueue.Enqueue(Encoding.UTF8.GetString(receiveBytes, 0, receiveNum));}}}//关闭套接字public void Close(){if (socket != null){socket.Shutdown(SocketShutdown.Both);socket.Close();//线程也要一起关闭isConnected = false;}}private void OnDestroy(){Close();}}

Main 主路口

测试 Lesson7

区分消息类型

1.创建消息基类,基类继承BaseData,基类添加获取消息ID的方法或者属性

BaseData 在Lesson3 练习题里写的

2.让想要被发送的消息继承该类,实现序列化反序列化方法

3.修改客户端和服务端收发消息的逻辑

用到 Lesson6 里最原始的 客户端

再用 StudyTcpServer 最原始的服务端 测试

先给 Lesson7 写的 NetMgr

再改服务器综合练习2  

CilentSocket

ServerSocket

分包、黏包

基本概念和逻辑实现

在Lesson7 NetMgr 基础上实现

using System;
using System.Collections;
using System.Collections.Generic;
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 isConnected = false;void Awake(){instance = this;DontDestroyOnLoad(this.gameObject);}// Update is called once per framevoid Update(){//取出消息if (receiveQueue.Count > 0){BaseMsg msg = receiveQueue.Dequeue();if (msg is PlayerMsg){PlayerMsg playerMsg = msg as PlayerMsg;print(playerMsg.playerID);print(playerMsg.playerData.name);print(playerMsg.playerData.atk);print(playerMsg.playerData.lev);}}}//连接服务端public void Connect(string ip, int port){//如果是连接状态 就直接返回if (isConnected)return;if(socket == null)socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//连接服务端try{socket.Connect(ip, port);isConnected = true;//开启发送线程ThreadPool.QueueUserWorkItem(SendMsg);//开启接收线程ThreadPool.QueueUserWorkItem(ReceiveMsg);}catch (SocketException e){if (e.ErrorCode == 10061)print("服务器拒绝连接");elseprint("连接失败" + e.ErrorCode + e.Message);}}//发送消息public void Send(BaseMsg msg){sendMsgQueue.Enqueue(msg);}private void SendMsg(object obj){while (isConnected){if (sendMsgQueue.Count > 0){socket.Send(sendMsgQueue.Dequeue().Writing());}}}//不停的接收消息private void ReceiveMsg(object obj){while (isConnected){if (socket.Available > 0){byte[] receiveBytes = new byte[1024 * 1024];int receiveNum = socket.Receive(receiveBytes);HandleReceiveMsg(receiveBytes, receiveNum);首先把收到字节数组的前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;//}如果消息为空 那证明是不知道类型的消息 没有解析//if (baseMsg == null)//    continue;收到消息 解析消息为字符串 并放入公共容器//receiveQueue.Enqueue(baseMsg);}}}//处理接收消息 分包、黏包问题的方法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)  //判断这条数据能否解析出 ID 和 长度{                //解析IDmsgID = BitConverter.ToInt32(cacheBytes, nowIndex);nowIndex += 4;//解析长度msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);nowIndex += 4;}if (cacheBytes.Length - nowIndex >= msgLength && msgLength != -1)  //判断8位后面的长度是否够解析{//解析消息体BaseMsg baseMsg = null;switch (msgID){case 1001:PlayerMsg msg = new PlayerMsg();msg.Reading(cacheBytes, nowIndex);baseMsg = msg;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){socket.Shutdown(SocketShutdown.Both);socket.Close();//线程也要一起关闭isConnected = false;}}private void OnDestroy(){Close();}}

注:理清楚逻辑

分包、黏包测试

先将分包、黏包的逻辑处理复制到 服务端

在NetMgr 中 先一个测试发送的方法

在Lesson7 中写测试逻辑

心跳消息

客户端主动断开连接

本节课涉及的代码量较多,需要配合视频

该动:

将 QuitMsg copy到服务器端

实现心跳消息

创建心跳消息 HeartMsg 类

NetMsg 定时发送心跳消息

将 HeartMsg 复制到 服务端

ClientSocket 中添加心跳消息相关逻辑

测试 把正常断开逻辑注释

改动:

异步

异步通信常用方法
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;public class Lesson12 : MonoBehaviour
{private byte[] resultBytes = new byte[1024];// Start is called before the first frame updatevoid Start(){#region 知识点一 异步方法和同步方法的区别//同步方法://方法中逻辑执行完毕后,再继续执行后面的方法//异步方法://方法中逻辑可能还没有执行完毕,就继续执行后面的内容//异步方法的本质//往往异步方法当中都会使用多线程执行某部分逻辑//因为我们不需要等待方法中逻辑执行完毕就可以继续执行下面的逻辑//注意:Unity中的协同程序中的某些异步方法,有的使用的是多线程有的使用的是迭代器分部执行//关于协同程序可以回顾Unity基础当中讲解协同程序原理的知识点#endregion#region 知识点二 举例说明异步方法原理//我们以一个异步倒计时方法举例//1.线程回调//CountDownAsync(5, () =>//{//    print("倒计时结束");//});//print("异步执行后的逻辑");//2.async 和 await 会等待线程执行完毕 继续执行后面的逻辑//相对第一种方式 可以让函数发步执行CountDownAsync(5);print("异步执行后的逻辑2");#endregion#region 知识点三 Socket TCP通信中的异步方法(Begin开头方法)//回调函数参数 IAsyncResult//AsyncState 调用异步方法时传入的参数 需要转换//AsyncWaitHandle 用于同步等待Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//服务器相关//BeginAccept//EndAcceptsocketTcp.BeginAccept(AcceptCallBack, socketTcp);   //监听连接是否成功//客户端相关//BeginConnect//EndConnectIPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9091);socketTcp.BeginConnect(ipPoint, (result) =>  //监听连接是否成功{Socket s = result.AsyncState as Socket;try{//socketTcp.EndConnect(result);  //两种写法都可以s.EndConnect(result);print("连接成功");}catch (SocketException e){print("连接出错" + e.SocketErrorCode + e.Message);}}, socketTcp);//服务器客户端通用//接收消息//BeginReceive//EndReceivesocketTcp.BeginReceive(resultBytes, 0, resultBytes.Length, SocketFlags.None, ReceiveCallBack, socketTcp);//发送消息//BeginSend//EndSendbyte[] bytes = Encoding.UTF8.GetBytes("山有木兮木有枝");socketTcp.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, (result) =>{try{socketTcp.EndSend(result);print("发送成功");}catch (SocketException e){print("发送错误" + e.SocketErrorCode + e.Message);}}, socketTcp);#endregion#region 知识点四 Socket TCP通信中的异步方法2 (Async结尾方法)//关键变量类型//SocketAsyncEventArgs//它会作为Async异步方法的传入值//我们需要通过它进行一些关键参数的赋值//服务器端//AcceptAsyncSocketAsyncEventArgs e = new SocketAsyncEventArgs();e.Completed += (socket, args) =>  //两个参数分别是 socket->socketTcp  args->e{//首先判断是否成功if (args.SocketError == SocketError.Success){//获取连入的客户端socketSocket clientSocket = args.AcceptSocket;(socket as Socket).AcceptAsync(args);}else{print("连入客户端失败" + args.SocketError);}};socketTcp.AcceptAsync(e);//客户端//ConnectAsyncSocketAsyncEventArgs e2 = new SocketAsyncEventArgs();e2.Completed += (socket, args) =>{if (args.SocketError == SocketError.Success){//连接成功}else{//连接失败print(args.SocketError);}};socketTcp.ConnectAsync(e2);//服务端和客户端//发送消息//SendAsyncSocketAsyncEventArgs e3 = new SocketAsyncEventArgs();byte[] bytes2 = Encoding.UTF8.GetBytes("山有木兮木有枝");e3.SetBuffer(bytes2, 0, bytes2.Length);   //发送和接收都是 用SetBuffere3.Completed += (socket, args) =>  //监听有没有发送成功{if(args.SocketError == SocketError.Success){//连接成功print("发送成功");}else{//连接失败print(args.SocketError);}};socketTcp.SendAsync(e3);//接收消息//ReceiveAsyncSocketAsyncEventArgs e4 = new SocketAsyncEventArgs();//设置接收数据的容器,偏移位置,容量e4.SetBuffer(new byte[1024 * 1024], 0, 1024 * 1024);//若是要处理数据 先得判断数据接收是否成功e4.Completed += (socket, args) =>{if (args.SocketError == SocketError.Success){//收取存储在容器当中的字节//Buffer是容器//BytesTransferred 是收取了多少字节Encoding.UTF8.GetString(args.Buffer, 0, args.BytesTransferred);args.SetBuffer(0, args.Buffer.Length);  //另一个重载//接收完消息 再接收下一条 (复用)(socket as Socket).ReceiveAsync(args);}else{}};socketTcp.ReceiveAsync(e4);#endregion#region 总结//C#中网络通信 异步方法中 主要提供了两种方案//1.Begin开头的API//内部开多线程,通过回调形式返回结果,需要和End相关方法 配合使用//2.Async结尾的API//内部开多线程,通过回调形式返回结果,依赖SocketAsyncEventArgs对象配合使用//可以让我们更加方便的进行操作#endregion}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);}}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);}}// Update is called once per framevoid Update(){}public void CountDownAsync(int second, UnityAction callBack){Thread t = new Thread(() =>{while (true){print(second);Thread.Sleep(1000);--second;if (second == 0)break;}callBack?.Invoke();});print("开始倒计时");t.Start();}public async void CountDownAsync(int second){print("倒计时开始");await Task.Run(() =>{while (true){print(second);Thread.Sleep(1000);--second;if (second == 0)break;}});print("倒计时结束");}
}
服务端

ClientSocket

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;namespace StudyTcpServerAsync
{class ClientSocket{public Socket socket;public int clientID;private static int CLIENT_BEGIN_ID = 1;private byte[] cacheBytes = new byte[1024];private int cacheNum = 0;public ClientSocket(Socket socket){this.clientID = CLIENT_BEGIN_ID++;this.socket = socket;//开始收消息this.socket.BeginReceive(cacheBytes, cacheNum, cacheBytes.Length, SocketFlags.None, ReceiveCallBack, this.socket);}private void ReceiveCallBack(IAsyncResult result){try{cacheNum = this.socket.EndReceive(result);//通过解析字符串去解析Console.WriteLine(Encoding.UTF8.GetString(cacheBytes, 0, cacheNum));//如果是连接状态再继续收消息//因为目前我们是以字符串的形式解析的 所以 解析完 就直接 从0又开始收cacheNum = 0;if (this.socket.Connected){this.socket.BeginReceive(cacheBytes, cacheNum, cacheBytes.Length, SocketFlags.None, ReceiveCallBack, this.socket);}else{Console.WriteLine("没有连接,不用在接收消息了");}}catch (SocketException e){Console.WriteLine("接收消息错误" + e.SocketErrorCode + e.Message);}}public void Send(string str){if (this.socket.Connected){byte[] bytes = Encoding.UTF8.GetBytes(str);this.socket.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, SendCallBack, null);}else{}}private void SendCallBack(IAsyncResult result){try{this.socket.EndSend(result);}catch (SocketException e){Console.WriteLine("发送失败" + e.SocketErrorCode + e.Message);}}}
}

ServerSocket

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;namespace StudyTcpServerAsync
{class ServerSocket{private Socket socket;private Dictionary<int, ClientSocket> clientDic = new Dictionary<int, ClientSocket>();public void Start(string ip, int potr, int num){socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), potr);try{socket.Bind(ipPoint);socket.Listen(num);//通过异步接收客户端接入socket.BeginAccept(AcceptCallBack, null);  //第二个参数原本是要传socket,但是这里可以直接得到所以就传null}catch (SocketException e){Console.WriteLine("启动服务器失败" + e.Message);}}private void AcceptCallBack(IAsyncResult result){try{//获取连入的客户端Socket clientSocket = socket.EndAccept(result);ClientSocket client = new ClientSocket(clientSocket);//记录客户端对象clientDic.Add(client.clientID, client);//继续去让别的客户端可以连入socket.BeginAccept(AcceptCallBack, null);}catch (Exception e){Console.WriteLine("客户端连入失败" + e.Message);}}public void Broadcast(string str){foreach (ClientSocket client in clientDic.Values){client.Send(str);}}}
}

主路口

客户端

NetAsyncMgr

using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;public class NetAsyncMgr : MonoBehaviour
{private static NetAsyncMgr instance;public static NetAsyncMgr Instance => instance;//和服务器进行连接的 Socketprivate Socket socket;//接收消息的 缓存容器private byte[] cacheBytes = new byte[1024 * 1024];private int cacheNum = 0;// Start is called before the first frame updatevoid Awake(){instance = this;//过场景不移除DontDestroyOnLoad(this.gameObject);}// Update is called once per framevoid Update(){}//连接服务器的代码public void Connect(string ip, int port){if (socket != null && socket.Connected)return;IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);SocketAsyncEventArgs args = new SocketAsyncEventArgs();args.RemoteEndPoint = ipPoint;   //告知 远端的服务器信息args.Completed += (socket, args) =>{if (args.SocketError == SocketError.Success){print("连接成功");//收消息SocketAsyncEventArgs receiveArgs = new SocketAsyncEventArgs();receiveArgs.SetBuffer(cacheBytes, 0, cacheBytes.Length);receiveArgs.Completed += ReceiveCallBack;this.socket.ReceiveAsync(receiveArgs);}else{print("连接失败" + args.SocketError);}};socket.ConnectAsync(args);}//收消息完成的回调函数private void ReceiveCallBack(object obj, SocketAsyncEventArgs args){if (args.SocketError == SocketError.Success){//解析消息 目前用的字符串规则print(Encoding.UTF8.GetString(args.Buffer, 0, args.BytesTransferred));//继续去接收消息args.SetBuffer(0, args.Buffer.Length);//继续异步收消息if (this.socket != null && this.socket.Connected)socket.ReceiveAsync(args);elseClose();}else{print("接收消息出错" + args.SocketError);//关闭客户端连接Close();}}public void Close(){if (socket != null){socket.Shutdown(SocketShutdown.Both);socket.Disconnect(false);socket.Close();socket = null;}}public void Send(string str){if(this.socket != null && this.socket.Connected){byte[] bytes = Encoding.UTF8.GetBytes(str);SocketAsyncEventArgs args = new SocketAsyncEventArgs();args.SetBuffer(bytes, 0, bytes.Length);args.Completed += (socket, args) =>{if (args.SocketError != SocketError.Success){print("发送消息失败" + args.SocketError);Close();}};socket.SendAsync(args);}else{Close();}}}

MainAsync

测试

服务端综合练习

自己练习

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace StudyTcpServerAsync
{class ClientSocket{public Socket socket;public int clientID;private static int CLIENT_BEGIN_ID = 1;private byte[] cacheBytes = new byte[1024];private int cacheNum = 0;//上一次收到消息的时间private long frontTime = -1;//超时时间private static int TIME_OUT_TIME = 10;public ClientSocket(Socket socket){this.clientID = CLIENT_BEGIN_ID++;this.socket = socket;//开始收消息this.socket.BeginReceive(cacheBytes, cacheNum, cacheBytes.Length, SocketFlags.None, ReceiveCallBack, this.socket);ThreadPool.QueueUserWorkItem(CheckTimeOut);}/// <summary>/// 间隔一段时间 检测一次超时 如果超时 就会主动断开该客户端的连接/// </summary>/// <param name="obj"></param>private void CheckTimeOut(object obj){while (this.socket != null && this.socket.Connected){if(frontTime != -1 &&DateTime.Now.Ticks / TimeSpan.TicksPerSecond - frontTime >= TIME_OUT_TIME){Program.serverSocket.CloseClientSocket(this);break;}Thread.Sleep(5000);}}private void ReceiveCallBack(IAsyncResult result){try{if (this.socket != null && this.socket.Connected){//消息成功int num = this.socket.EndReceive(result);//处理分包黏包HandleReceiveMsg(num);this.socket.BeginReceive(cacheBytes, cacheNum, cacheBytes.Length - cacheNum, SocketFlags.None, ReceiveCallBack, this.socket);}else{Console.WriteLine("没有连接,不用再收消息了");Program.serverSocket.CloseClientSocket(this);}//cacheNum = this.socket.EndReceive(result);通过解析字符串去解析//Console.WriteLine(Encoding.UTF8.GetString(cacheBytes, 0, cacheNum));如果是连接状态再继续收消息因为目前我们是以字符串的形式解析的 所以 解析完 就直接 从0又开始收//cacheNum = 0;//if (this.socket.Connected)//{//    this.socket.BeginReceive(cacheBytes, cacheNum, cacheBytes.Length, SocketFlags.None, ReceiveCallBack, this.socket);//}//else//{//    Console.WriteLine("没有连接,不用在接收消息了");//}}catch (SocketException e){Console.WriteLine("接收消息错误" + e.SocketErrorCode + e.Message);Program.serverSocket.CloseClientSocket(this);}}public void Send(BaseMsg msg){if (socket != null && this.socket.Connected){byte[] bytes = msg.Writing();this.socket.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, SendCallBack, null);}else{}}private void SendCallBack(IAsyncResult result){try{if (socket != null && socket.Connected)this.socket.EndSend(result);elseProgram.serverSocket.CloseClientSocket(this);}catch (SocketException e){Console.WriteLine("发送失败" + e.SocketErrorCode + e.Message);Program.serverSocket.CloseClientSocket(this);}}public void Close(){if (socket != null){socket.Shutdown(SocketShutdown.Both);socket.Close();socket = null;}}//处理分包黏包private void HandleReceiveMsg(int receiveNum){int msgID = 0;int msgLength = 0;int nowIndex = 0;//由于消息接收后是直接存储在 cacheBytes中的 所以不需要进行拷贝操作//收到消息的字节数量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;}if (baseMsg != null)ThreadPool.QueueUserWorkItem(MsgHandle, baseMsg);nowIndex += msgLength;if (nowIndex == cacheNum){cacheNum = 0;break;}}else{//如果不满足 证明有分包//那么我们需要把当前收到的内容 记录下来//等待下次接收到消息后 再做处理//如果进行了 id和长度的解析 但是 没有成功解析消息体 那么我们需要减去nowIndex 移动的位置if (msgLength != -1)nowIndex -= 8;//就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);cacheNum = cacheNum - nowIndex;break;}}}//消息处理private void MsgHandle(object obj){switch (obj){case PlayerMsg msg:PlayerMsg playerMsg = msg as PlayerMsg;Console.WriteLine(playerMsg.playerID);Console.WriteLine(playerMsg.playerData.name);Console.WriteLine(playerMsg.playerData.lev);Console.WriteLine(playerMsg.playerData.atk);break;case QuitMsg msg://收到断开连接消息 把自己添加到待移除的列表中Program.serverSocket.CloseClientSocket(this);break;case HeartMsg msg://收到心跳消息 记录收到消息的时间frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;Console.WriteLine("收到心跳消息");break;}}}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;namespace StudyTcpServerAsync
{class ServerSocket{private Socket socket;private Dictionary<int, ClientSocket> clientDic = new Dictionary<int, ClientSocket>();public void Start(string ip, int potr, int num){socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), potr);try{socket.Bind(ipPoint);socket.Listen(num);//通过异步接收客户端接入socket.BeginAccept(AcceptCallBack, null);  //第二个参数原本是要传socket,但是这里可以直接得到所以就传null}catch (Exception e){Console.WriteLine("启动服务器失败" + e.Message);}}private void AcceptCallBack(IAsyncResult result){try{//获取连入的客户端Socket clientSocket = socket.EndAccept(result);ClientSocket client = new ClientSocket(clientSocket);//记录客户端对象clientDic.Add(client.clientID, client);//继续去让别的客户端可以连入socket.BeginAccept(AcceptCallBack, null);}catch (Exception e){Console.WriteLine("客户端连入失败" + e.Message);}}//广播方法public void Broadcast(BaseMsg msg){foreach (ClientSocket client in clientDic.Values){client.Send(msg);}}//关闭客户端连接的 从字典中移除public void CloseClientSocket(ClientSocket socket){lock (clientDic){socket.Close();if (clientDic.ContainsKey(socket.clientID)){clientDic.Remove(socket.clientID);Console.WriteLine("客户端{0}主动断开连接", socket.clientID);}}}}
}
客户端综合练习

自个练习

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;public class NetAsyncMgr1 : MonoBehaviour
{private static NetAsyncMgr1 instance1;public static NetAsyncMgr1 Instance => instance1;//和服务器进行连接的 Socketprivate Socket socket;//接受消息用的 缓存容器private byte[] cacheBytes = new byte[1024 * 1024];private int cacheNum = 0;private Queue<BaseMsg> receiveQueue = new Queue<BaseMsg>();//发送心跳消息的间隔时间private int SEND_HEART_MSG_TIME = 2;private HeartMsg hearMsg = new HeartMsg();// Start is called before the first frame updatevoid Awake(){instance1 = this;//过场景不移除DontDestroyOnLoad(this.gameObject);//客户端循环定时给服务端发送心跳消息InvokeRepeating("SendHeartMsg", 0, SEND_HEART_MSG_TIME);}private void SendHeartMsg(){if (socket != null && socket.Connected)Send(hearMsg);}// Update is called once per framevoid Update(){if (receiveQueue.Count > 0){BaseMsg baseMsg = receiveQueue.Dequeue();switch (baseMsg){case PlayerMsg msg:print(msg.playerID);print(msg.playerData.name);print(msg.playerData.lev);print(msg.playerData.atk);break;}}}//连接服务器的代码public void Connect(string ip, int port){if (socket != null && socket.Connected)return;IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);SocketAsyncEventArgs args = new SocketAsyncEventArgs();args.RemoteEndPoint = ipPoint;args.Completed += (socket, args) =>{if(args.SocketError == SocketError.Success){print("连接成功");//收消息SocketAsyncEventArgs receiveArgs = new SocketAsyncEventArgs();receiveArgs.SetBuffer(cacheBytes, 0, cacheBytes.Length);receiveArgs.Completed += ReceiveCallBack;this.socket.ReceiveAsync(receiveArgs);}else{print("连接失败" + args.SocketError);}};socket.ConnectAsync(args);}//收消息完成的回调函数private void ReceiveCallBack(object obj, SocketAsyncEventArgs args){if(args.SocketError == SocketError.Success){HandleReceiveMsg(args.BytesTransferred);//继续去收消息args.SetBuffer(cacheNum, args.Buffer.Length - cacheNum);//继续异步收消息if (this.socket != null && this.socket.Connected)socket.ReceiveAsync(args);elseClose();}else{print("接受消息出错" + args.SocketError);//关闭客户端连接Close();}}public void Close(){if(socket != null){QuitMsg msg = new QuitMsg();socket.Send(msg.Writing());socket.Shutdown(SocketShutdown.Both);socket.Disconnect(false);socket.Close();socket = null;}}public void SendTest(byte[] bytes){SocketAsyncEventArgs args = new SocketAsyncEventArgs();args.SetBuffer(bytes, 0, bytes.Length);args.Completed += (socket, args) =>{if (args.SocketError != SocketError.Success){print("发送消息失败" + args.SocketError);Close();}};this.socket.SendAsync(args);}public void Send(BaseMsg msg){if(this.socket != null && this.socket.Connected){byte[] bytes = msg.Writing();SocketAsyncEventArgs args = new SocketAsyncEventArgs();args.SetBuffer(bytes, 0, bytes.Length);args.Completed += (socket, args) =>{if (args.SocketError != SocketError.Success){print("发送消息失败" + args.SocketError);Close();}};this.socket.SendAsync(args);}else{Close();}}//处理接受消息 分包、黏包问题的方法private void HandleReceiveMsg(int receiveNum){int msgID = 0;int msgLength = 0;int nowIndex = 0;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;}if (baseMsg != null)receiveQueue.Enqueue(baseMsg);nowIndex += msgLength;if (nowIndex == cacheNum){cacheNum = 0;break;}}else{if (msgLength != -1)nowIndex -= 8;//就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);cacheNum = cacheNum - nowIndex;break;}}}private void OnDestroy(){Close();}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;public class Lesson13 : 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((UnityEngine.Events.UnityAction)(() =>{PlayerMsg ms = new PlayerMsg();ms.playerID = 1011;ms.playerData = new PlayerData();ms.playerData.name = "Sunset的客户端";ms.playerData.atk = 66;ms.playerData.lev = 88;NetAsyncMgr1.Instance.Send(ms);}));//黏包测试btn1.onClick.AddListener((UnityEngine.Events.UnityAction)(() =>{PlayerMsg msg = new PlayerMsg();msg.playerID = 1001;msg.playerData = new PlayerData();msg.playerData.name = "Sunset01";msg.playerData.atk = 11;msg.playerData.lev = 111;PlayerMsg msg2 = new PlayerMsg();msg2.playerID = 1002;msg2.playerData = new PlayerData();msg2.playerData.name = "Sunset02";msg2.playerData.atk = 22;msg2.playerData.lev = 222;//黏包byte[] bytes = new byte[msg.GetByteNum() + msg2.GetByteNum()];msg.Writing().CopyTo(bytes, 0);msg2.Writing().CopyTo(bytes, msg.GetByteNum());NetAsyncMgr1.Instance.SendTest(bytes);}));//分包测试btn2.onClick.AddListener((UnityEngine.Events.UnityAction)(async () =>{PlayerMsg msg = new PlayerMsg();msg.playerID = 1003;msg.playerData = new PlayerData();msg.playerData.name = "Sunset03";msg.playerData.atk = 33;msg.playerData.lev = 333;byte[] bytes = msg.Writing();//分包byte[] bytes1 = new byte[10];byte[] bytes2 = new byte[bytes.Length - 10];//分成第一个包//将一个字节数组复制到另一个自己数组中//参数一:被copy的数组      参数二:从第几个位置开始copy//参数三:接收数组      参数四:开始位置        参数五:结束位置(copy了几位)Array.Copy(bytes, 0, bytes1, 0, 10);//第二个包Array.Copy(bytes, 10, bytes2, 0, bytes.Length - 10);//发送NetAsyncMgr1.Instance.SendTest(bytes1);//延迟500毫秒后再发送下一个包await Task.Delay(500);NetAsyncMgr1.Instance.SendTest(bytes2);}));//分包黏包测试btn3.onClick.AddListener((UnityEngine.Events.UnityAction)(async () =>{PlayerMsg msg = new PlayerMsg();msg.playerID = 1004;msg.playerData = new PlayerData();msg.playerData.name = "Sunset04";msg.playerData.atk = 44;msg.playerData.lev = 444;PlayerMsg msg2 = new PlayerMsg();msg2.playerID = 1005;msg2.playerData = new PlayerData();msg2.playerData.name = "Sunset05";msg2.playerData.atk = 55;msg2.playerData.lev = 555;byte[] bytes1 = msg.Writing(); //消息Abyte[] bytes2 = msg2.Writing(); //消息Bbyte[] bytes2_1 = new byte[10];byte[] bytes2_2 = new byte[bytes2.Length - 10];//将B 分包Array.Copy(bytes2, 0, bytes2_1, 0, 10);  //第一个包Array.Copy(bytes2, 10, bytes2_2, 0, bytes2.Length - 10);  //第二个包//消息A 和 消息B第一个包黏包byte[] bytesAB = new byte[bytes1.Length + bytes2_1.Length];bytes1.CopyTo(bytesAB, 0);bytes2_1.CopyTo(bytesAB, bytes1.Length);//发送NetAsyncMgr1.Instance.SendTest(bytesAB);//间隔 半秒钟发后一段await Task.Delay(500);NetAsyncMgr1.Instance.SendTest(bytes2_2);}));}// Update is called once per framevoid Update(){}
}

UDP通信

概述

同步

服务端和客户端

服务端综合练习

Client

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace StudyUdpServerExercise
{//它是用于记录和服务器通信过的客户端的IP和端口class Client{public IPEndPoint clientIPandPort;public string clientStrID;//上一次收到消息的时间public long frontTime = -1;public Client(string ip, int port){//规则和外面一样 记录唯一ID 通过 ip + port 拼接的形式clientStrID = ip + port;//就把客户端的信息记录下来clientIPandPort = new IPEndPoint(IPAddress.Parse(ip), port);}public void ReceiveMsg(byte[] bytes){//为了避免处理消息时 又 接收到了 其它消息 所以我们需要在这里之前 先把信息copy出来//处理消息和接收消息 用不同的容器 避免出现问题byte[] cacheBytes = new byte[512];bytes.CopyTo(cacheBytes, 0);//记录收到消息的 系统时间 单位为秒frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;ThreadPool.QueueUserWorkItem(ReceiveHandle, cacheBytes);}//多线程处理消息private void ReceiveHandle(object obj){try{//取出传进来的字节byte[] bytes = obj as byte[];int nowIndex = 0;//先处理IDint msgID = BitConverter.ToInt32(bytes, nowIndex);nowIndex += 4;//再处理 长度int msgLength = BitConverter.ToInt32(bytes, nowIndex);nowIndex += 4;//再解析消息体switch (msgID){case 1001:PlayerMsg playerMsg = new PlayerMsg();playerMsg.Reading(bytes, nowIndex);Console.WriteLine(playerMsg.playerID);Console.WriteLine(playerMsg.playerData.name);Console.WriteLine(playerMsg.playerData.atk);Console.WriteLine(playerMsg.playerData.lev);break;case 1003:QuitMsg quitMsg = new QuitMsg();//由于它没有消息体 所以都不用反序列化//quitMsg.Reading(bytes, nowIndex);//处理退出Program.serverSocket.RemoveClient(this.clientStrID);break;}}catch (Exception e){Console.WriteLine("处理消息时出错:" + e.Message);//如果出错 就不用记录这个客户端信息了Program.serverSocket.RemoveClient(this.clientStrID);}}}
}

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 StudyUdpServerExercise
{class ServerSocket{private Socket socket;//关闭循环的标识private bool isClose;//我们可以通过记录谁给我发了消息 把它的 ip和端口记录下来 这样就认为它是我的客户端//key用string:定义一个规则来获得唯一的 ID:IP和端口拼接private Dictionary<string, Client> clientDic = new Dictionary<string, Client>();public void Start(string ip, int port){IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);//声明一个用于UDP通信的Socketsocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);try{socket.Bind(ipPoint);isClose = false;//消息接收的处理ThreadPool.QueueUserWorkItem(ReceiveMsg);//定时检测超时线程ThreadPool.QueueUserWorkItem(CheckTimeOut);}catch (Exception e){Console.WriteLine("UDP开启出错" + e.Message);}}private void CheckTimeOut(object obj){long nowTime = 0;List<string> delList = new List<string>();while (true){//每隔30s检测一次 是否移除长时间没有接收到消息的客户端信息Thread.Sleep(30000);//得到当前系统时间nowTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;foreach (Client c in clientDic.Values){//超过10秒没有收到消息的 客户端信息 需要被移除if (nowTime - c.frontTime >= 10){delList.Add(c.clientStrID);}}//从待删除列表中移除 超时的客户端信息for (int i = 0; i < delList.Count; i++){RemoveClient(delList[i]);}delList.Clear();}}//收消息private void ReceiveMsg(object obj){//接收消息的容器byte[] bytes = new byte[512];//记录谁发的EndPoint ipPoint = new IPEndPoint(IPAddress.Any, 0);//用于拼接字符串 唯一ID:是由 IP + 端口构成的string strID = "";string ip;int port;while (!isClose){if (socket.Available > 0){lock(socket)socket.ReceiveFrom(bytes, ref ipPoint);//处理消息 最好不要在这直接处理 而是交给 客户端对象处理//收到消息时 我们可以判断 是不是记录了这个客户端信息 (ip和端口)//取出发送消息给我的 IP和端口ip = (ipPoint as IPEndPoint).Address.ToString();port = (ipPoint as IPEndPoint).Port;strID = ip + port;//拼接成一个唯一ID 这个是我们自己定义的规则//判断有没有记录这个客户端信息 如果有 用它直接处理消息if (clientDic.ContainsKey(strID)){clientDic[strID].ReceiveMsg(bytes);}else  //如果没有添加后再处理消息{clientDic.Add(strID, new Client(ip, port));clientDic[strID].ReceiveMsg(bytes);}}}}//指定发送一个消息给某个目标public void SendTo(BaseMsg msg, IPEndPoint ipPoint){try{lock (socket)socket.SendTo(msg.Writing(), ipPoint);}catch (SocketException s){Console.WriteLine("发送消息出问题 " + s.SocketErrorCode + s.Message);}catch (Exception e){Console.WriteLine("发送消息出问题(可能是序列化的问题)" + e.Message);}}//广播public void Broadcast(BaseMsg msg){//广播信息foreach (Client c in clientDic.Values){SendTo(msg, c.clientIPandPort);}}//关闭Socketpublic void Close(){if (socket != null){isClose = true;socket.Shutdown(SocketShutdown.Both);socket.Close();socket = null;}}public void RemoveClient(string clientID){if (clientDic.ContainsKey(clientID)){Console.WriteLine("客户端{0}被移除了" + clientDic[clientID].clientStrID);clientDic.Remove(clientID);}}}
}
客户端综合练习

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using UnityEngine;public class UdpNetMgr : MonoBehaviour
{private static UdpNetMgr instance;public static UdpNetMgr Instance => instance;private EndPoint serverIpPoint;private Socket socket;//客户端socket 是否关闭private bool isClose = true;//两个容器 队列//接收和发送消息的队列 在多线程里面可以操作private Queue<BaseMsg> sendQueue = new Queue<BaseMsg>();private Queue<BaseMsg> receiveQueue = new Queue<BaseMsg>();private byte[] cacheBytes = new byte[512];// Start is called before the first frame updatevoid Awake(){instance = this;//过场景不移除DontDestroyOnLoad(this.gameObject);}// Update is called once per framevoid Update(){//处理 receiveQueue 中的消息if (receiveQueue.Count > 0){switch (receiveQueue.Dequeue()){case PlayerMsg msg:print(msg.playerID);print(msg.playerData.name);print(msg.playerData.atk);print(msg.playerData.lev);break;}}}/// <summary>/// 启动客户端socket相关的方法/// </summary>/// <param name="ip">远端服务器的IP</param>/// <param name="port">远端服务器的端口</param>public void StartClient(string ip, int port){//如果当前是开启状态 就不用再开了if (!isClose)return;//先记录服务器地址,一会发消息时会使用serverIpPoint = new IPEndPoint(IPAddress.Parse(ip), port);IPEndPoint clientIpPort = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8082);try{socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);socket.Bind(clientIpPort);isClose = false;print("客户端网络启动");ThreadPool.QueueUserWorkItem(ReceiveMsg);ThreadPool.QueueUserWorkItem(SendMsg);}catch (System.Exception e){print("启动Socket出问题:" + e.Message);}}//接收消息的线程private void ReceiveMsg(object obj){EndPoint tempIpPoint = new IPEndPoint(IPAddress.Any, 0);int nowIndex;int msgID;int msgLength;while (!isClose){if (socket != null && socket.Available > 0){try{socket.ReceiveFrom(cacheBytes, ref tempIpPoint);//为了避免处理 非服务器发来的 骚扰消息if (!tempIpPoint.Equals(serverIpPoint))  //类型的原因 不能用 “==”比较{//如果发现 发消息给你的 不是服务器 那么证明是骚扰消息 就不用处理continue;}//处理服务器发来的消息nowIndex = 0;//解析IDmsgID = BitConverter.ToInt32(cacheBytes, nowIndex);nowIndex += 4;//解析长度msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);nowIndex += 4;//解析消息体BaseMsg msg = null;switch (msgID){case 1001:msg = new PlayerMsg();//反序列化消息体msg.Reading(cacheBytes, nowIndex);break;}if (msg != null)receiveQueue.Enqueue(msg);}catch (SocketException s){print("接收消息出错:" + s.SocketErrorCode + s.Message);}catch (Exception e){print("接收消息出错(非网络问题):" + e.Message);}}}}//实际发送消息的线程private void SendMsg(object obj){while (!isClose){if (socket != null && sendQueue.Count > 0){try{socket.SendTo(sendQueue.Dequeue().Writing(), serverIpPoint);}catch (SocketException e){print("发送消息出错:" + e.SocketErrorCode + e.Message);throw;}}}}//发送消息public void Send(BaseMsg msg){sendQueue.Enqueue(msg);}//关闭Socketpublic void Close(){if (socket != null){isClose = true;QuitMsg msg = new QuitMsg();//发送一个退出消息给服务器 让其移除记录socket.SendTo(msg.Writing(), serverIpPoint);socket.Shutdown(SocketShutdown.Both);socket.Close();socket = null;}}private void OnDestroy(){Close();}
}

异步

异步通信常用方法
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;public class Lesson17 : MonoBehaviour
{private byte[] cacheBytes = new byte[512];// Start is called before the first frame updatevoid Start(){#region 知识点一 Socket UDP通信中的异步方法//通过之前的学习,UDP用于的通信相关方法主要就是//SendTo 和 ReceiveForm//所以在讲解UDP异步通信时也主要是围绕着收发消息相关方法来讲解#endregion#region 知识点二 UDP通信中Begin相关异步方法Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);//BeginSendTobyte[] bytes = Encoding.UTF8.GetBytes("山有木兮木有枝");EndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8081);socket.BeginSendTo(bytes, 0, bytes.Length, SocketFlags.None, ipPoint, SendToOver, socket); //最后一个参数是作为SendToOver的参数传给obj//BeginReceiveFromsocket.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref ipPoint, ReceiveFromOver, (socket, ipPoint));#endregion#region 知识点三 UDP通信中 Async相关异步方法//sendToAsyncSocketAsyncEventArgs args = new SocketAsyncEventArgs();//设置要发送的数据args.SetBuffer(bytes, 0, bytes.Length);//设置完成事件args.Completed += SendToAsync;socket.SendToAsync(args);//ReceiveFromAsyncSocketAsyncEventArgs args2 = new SocketAsyncEventArgs();//这是设置接收消息的容器  (重载方法)args2.SetBuffer(cacheBytes, 0, cacheBytes.Length);args2.Completed += ReceiveFromAsync;socket.ReceiveFromAsync(args2);#endregion#region 总结//由于学习了TCP相关的知识点//所以UDP的相关内容的学习就变的简单了//他们异步通信的唯一的区别就是API的不同,使用规则都是一致的#endregion}private void SendToOver(IAsyncResult result){try{Socket s = result.AsyncState as Socket;s.EndSendTo(result);  //这样才算真正的完成了一次异步发送print("发送成功");}catch (SocketException s){print("发送失败:" + s.SocketErrorCode + s.Message);}}private void ReceiveFromOver(IAsyncResult result){try{(Socket s, EndPoint ipPoint) info = ((Socket, EndPoint))result.AsyncState;//返回值 就是接收了多少个 字节数int num = info.s.EndReceiveFrom(result, ref info.ipPoint);//处理消息//处理完消息 又继续接收消息info.s.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref info.ipPoint, ReceiveFromOver, info);}catch (SocketException s){print("接收消息出问题:" + s.SocketErrorCode + s.Message);}}private void SendToAsync(object s, SocketAsyncEventArgs args){if (args.SocketError == SocketError.Success){print("发送成功");}else{print("发送失败");}}private void ReceiveFromAsync(object s, SocketAsyncEventArgs args){if (args.SocketError == SocketError.Success){print("接收成功");//具体收了多少个字节//args.BytesTransferred//可以通过以下两种方式获取收到的字节数组内容//args.Buffer//cacheBytes//解析消息//继续接收Socket socket = s as Socket;//只需要设置 从第几个位置开始接收 能接多少args.SetBuffer(0, cacheBytes.Length);socket.ReceiveFromAsync(args);}else{print("接收失败");}}// Update is called once per framevoid Update(){}
}

服务器综合练习

自己练习

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 StudyUdpServerExercise2
{    class ServerSocketAsync{private Socket socket;//关闭循环的标识private bool isClose;private Dictionary<string, ClientAsync> clientDic = new Dictionary<string, ClientAsync>();private byte[] cacheBytes = new byte[512];public void Start(string ip, int port){EndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);//声明一个用于UDP通信的Socketsocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);try{socket.Bind(ipPoint);isClose = false;//接收消息//EndPoint clientIP = new IPEndPoint(IPAddress.Any, 0);socket.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref ipPoint, ReceiveFromOver, ipPoint);//定时检测超时线程ThreadPool.QueueUserWorkItem(CheckTimeOut);}catch (Exception e){Console.WriteLine("UDP开启错误:" + e.Message);}}private void CheckTimeOut(object obj){long nowTime = 0;List<string> delList = new List<string>();while (true){//每隔30s检测一次 是否移除长时间没有接收到消息的客户端信息Thread.Sleep(30000);//得到当前系统时间nowTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;foreach (ClientAsync c in clientDic.Values){//超过10秒没有收到消息的 客户端信息 需要被移除if (nowTime - c.frontTime >= 10){delList.Add(c.clientStrID);}}//从待删除列表中移除 超时的客户端信息for (int i = 0; i < delList.Count; i++){RemoveClient(delList[i]);}delList.Clear();}}private void ReceiveFromOver(IAsyncResult result){EndPoint clientPort = result.AsyncState as IPEndPoint;//返回值是收到多少个字节//int num = socket.EndReceiveFrom(result, ref clientPort);string ip = (clientPort as IPEndPoint).Address.ToString();int port = (clientPort as IPEndPoint).Port;string strID = ip + port;try{socket.EndReceiveFrom(result, ref clientPort);//进行消息处理if (clientDic.ContainsKey(strID)){clientDic[strID].ReceiveMsg(cacheBytes);}else{clientDic.Add(strID, new ClientAsync(ip, port));clientDic[strID].ReceiveMsg(cacheBytes);}//遗留 是否需要将 cacheBytes 置空socket.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref clientPort, ReceiveFromOver, clientPort);}catch (SocketException s){Console.WriteLine("消息处理出错:" + s.SocketErrorCode + s.Message);}}//指定发送一个消息给某个目标public void BeginSendTo(BaseMsg msg, IPEndPoint ipPoint){try{byte[] bytes = msg.Writing();socket.BeginSendTo(bytes, 0, bytes.Length, SocketFlags.None, ipPoint, BeginSendToOvet, socket);}catch (SocketException s){Console.WriteLine("发送消息出问题:" + s.SocketErrorCode + s.Message);}catch (Exception e){Console.WriteLine("发送消息出问题(非网络问题):" + e.Message);}}private void BeginSendToOvet(IAsyncResult result){try{socket.EndSendTo(result);}catch (SocketException s){Console.WriteLine("发送消息出错:" + s.SocketErrorCode + s.Message);}}//广播public void Broadcast(BaseMsg msg){//广播信息foreach (ClientAsync c in clientDic.Values){BeginSendTo(msg, c.clientIPandPort);}}//关闭Socketpublic void Close(){if (socket != null){isClose = true;socket.Shutdown(SocketShutdown.Both);socket.Close();socket = null;}}public void RemoveClient(string clientID){if (clientDic.ContainsKey(clientID)){Console.WriteLine("客户端{0}被移除了" + clientDic[clientID].clientStrID);clientDic.Remove(clientID);}}}
}
客户端综合练习
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using UnityEngine;public class UdpNetAsyncMgr : MonoBehaviour
{private static UdpNetAsyncMgr instance;public static UdpNetAsyncMgr Instance => instance;private EndPoint serverIpPoint;private Socket socket;//客户端socket 是否关闭private bool isClose = true;//两个容器 队列//接收和发送消息的队列 在多线程里面可以操作private Queue<BaseMsg> receiveQueue = new Queue<BaseMsg>();private byte[] cacheBytes = new byte[512];// Start is called before the first frame updatevoid Awake(){instance = this;//过场景不移除DontDestroyOnLoad(this.gameObject);}// Update is called once per framevoid Update(){if (receiveQueue.Count > 0){BaseMsg baseMsg = receiveQueue.Dequeue();switch (baseMsg){case PlayerMsg msg:print(msg.playerID);print(msg.playerData.name);print(msg.playerData.atk);print(msg.playerData.lev);break;}}}/// <summary>/// 启动客户端Socket 方法/// </summary>/// <param name="ip">远端服务器的IP</param>/// <param name="port">远端服务器的端口</param>public void StartClient(string ip, int port){//如果当前是开启状态 就不用再开了if (!isClose)return;//先记录服务器地址,一会发消息时会使用serverIpPoint = new IPEndPoint(IPAddress.Parse(ip), port);IPEndPoint clientIpPort = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9091);try{socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);socket.Bind(clientIpPort);isClose = false;//接收消息SocketAsyncEventArgs args = new SocketAsyncEventArgs();args.SetBuffer(cacheBytes, 0, cacheBytes.Length);args.RemoteEndPoint = new IPEndPoint(IPAddress.Any, 0);args.Completed += ReceiveFromAsync;socket.ReceiveFromAsync(args);print("客户端网络启动");}catch (System.Exception e){print("启动Socket出问题:" + e.Message);}}private void ReceiveFromAsync(object s, SocketAsyncEventArgs args){int nowIndex;int msgID;int msgLength;if (args.SocketError == SocketError.Success){try{//要是服务器发的才处理if (args.RemoteEndPoint.Equals(serverIpPoint)){//处理服务器发来的消息nowIndex = 0;//解析IDmsgID = BitConverter.ToInt32(args.Buffer, nowIndex);nowIndex += 4;//解析长度msgLength = BitConverter.ToInt32(args.Buffer, nowIndex);nowIndex += 4;//解析消息体BaseMsg msg = null;switch (msgID){case 1001:msg = new PlayerMsg();//反序列化消息体msg.Reading(args.Buffer, nowIndex);break;}if (msg != null)receiveQueue.Enqueue(msg);}//再次接收消息if (socket != null && !isClose){//只需要设置 从第几个位置开始接收 能接多少args.SetBuffer(0, cacheBytes.Length);socket.ReceiveFromAsync(args);}}catch (SocketException se){print("接收消息出错" + se.SocketErrorCode + se.Message);Close();}catch (Exception e){print("接收消息出错(可能是反序列化问题)" + e.Message);Close();}}else{print("接收失败" + args.SocketError);}}//发送消息public void Send(BaseMsg msg){try{if (socket != null && !isClose){SocketAsyncEventArgs args = new SocketAsyncEventArgs();byte[] bytes = msg.Writing();args.SetBuffer(bytes, 0, bytes.Length);//设置远端目标args.Completed += SendToCallBack;args.RemoteEndPoint = serverIpPoint;socket.SendToAsync(args);}}catch (SocketException s){print("发送消息出错" + s.SocketErrorCode + s.Message);}catch (Exception e){print("发送消息出错(可能是序列化问题)" + e.Message);}}private void SendToCallBack(object o, SocketAsyncEventArgs args){if (args.SocketError != SocketError.Success){print("发送消息失败" + args.SocketError);}}//关闭socketpublic void Close(){if (socket != null){isClose = true;QuitMsg msg = new QuitMsg();//发送一个退出消息给服务器 让其移除记录socket.SendTo(msg.Writing(), serverIpPoint);socket.Shutdown(SocketShutdown.Both);socket.Close();socket = null;}}private void OnDestroy(){Close();}
}

文件传输FTP

FTP工作原理

搭建FTP服务器

FTP关键类

上传文件

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;public class FtpMgr 
{private static FtpMgr instance = new FtpMgr();public static FtpMgr Instance => instance;//远程FTP服务器的地址private string FTP_PATH = "ftp://192.168.137.242/";//用户名和密码private string USER_NAME = "Sunset01";private string PASSWORD = "Sunset123";/// <summary>/// 上传文件到Ftp服务器(异步)/// </summary>/// <param name="fileName">FTP的文件名</param>/// <param name="localPath">本地文件路径</param>/// <param name="action">上传完毕后想要做什么的委托函数</param>public async void UploadFile(string fileName, string localPath, UnityAction action = null){await Task.Run(() =>{try{//通过一个线程执行这里面的逻辑 那么就不会影响主线程了//1.创建一个FTP连接FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;//2.进行一些设置//凭证 req.Proxy = null;NetworkCredential n = new NetworkCredential(USER_NAME, PASSWORD);req.Credentials = n;//是否操作结束后 关闭 控制连接req.KeepAlive = false;//传输类型req.UseBinary = true;//操作类型req.Method = WebRequestMethods.Ftp.UploadFile;//3.上传Stream upLoadStream = req.GetRequestStream();//开始上传using (FileStream fileStream = File.OpenRead(localPath)){byte[] bytes = new byte[1024];//返回值 为具体读取了多少字节int contenLength = fileStream.Read(bytes, 0, bytes.Length);// 有数据就上传while (contenLength != 0){//读多少就上传多少upLoadStream.Write(bytes, 0, contenLength);//继续从本地文件中读取contenLength = fileStream.Read(bytes, 0, bytes.Length);}//上传结束fileStream.Close();upLoadStream.Close();}Debug.Log("上传成功");}catch (Exception e){Debug.Log("上传出错:" + e.Message);}});//上传结束后 在外部做的事情action?.Invoke();}
}

测试:

下载文件

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;public class FtpMgr 
{private static FtpMgr instance = new FtpMgr();public static FtpMgr Instance => instance;//远程FTP服务器的地址private string FTP_PATH = "ftp://192.168.137.13/";//用户名和密码private string USER_NAME = "Sunset01";private string PASSWORD = "Sunset123";/// <summary>/// 上传文件到Ftp服务器(异步)/// </summary>/// <param name="fileName">FTP的文件名</param>/// <param name="localPath">本地文件路径</param>/// <param name="action">上传完毕后想要做什么的委托函数</param>public async void UploadFile(string fileName, string localPath, UnityAction action = null){await Task.Run(() =>{try{//通过一个线程执行这里面的逻辑 那么就不会影响主线程了//1.创建一个FTP连接FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;//2.进行一些设置//凭证 req.Proxy = null;NetworkCredential n = new NetworkCredential(USER_NAME, PASSWORD);req.Credentials = n;//是否操作结束后 关闭 控制连接req.KeepAlive = false;//传输类型req.UseBinary = true;//操作类型req.Method = WebRequestMethods.Ftp.UploadFile;//3.上传Stream upLoadStream = req.GetRequestStream();//开始上传using (FileStream fileStream = File.OpenRead(localPath)){byte[] bytes = new byte[1024];//返回值 为具体读取了多少字节int contenLength = fileStream.Read(bytes, 0, bytes.Length);// 有数据就上传while (contenLength != 0){//读多少就上传多少upLoadStream.Write(bytes, 0, contenLength);//继续从本地文件中读取contenLength = fileStream.Read(bytes, 0, bytes.Length);}//上传结束fileStream.Close();upLoadStream.Close();}Debug.Log("上传成功");}catch (Exception e){Debug.Log("上传出错:" + e.Message);}});//上传结束后 在外部做的事情action?.Invoke();}/// <summary>/// 下载文件从FTP服务器当中(异步)/// </summary>/// <param name="fileName">FTP上想要下载的文件名</param>/// <param name="localPath">存储的本地文件路径</param>/// <param name="action">下载完毕后想要做什么的委托函数</param>public async void DownLoadFile(string fileName, string localPath, UnityAction action = null){await Task.Run(() =>{try{//1.创建一个Ftp连接FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;//2.设置通信凭证req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);//是否操作后 关闭 控制连接req.KeepAlive = false;//3.设置操作命令req.Method = WebRequestMethods.Ftp.DownloadFile;//4.指定传输类型req.UseBinary = true;//设置代理为nullreq.Proxy = null;//5.得到用于下载的流对象FtpWebResponse res = req.GetResponse() as FtpWebResponse;Stream downLoadStream = res.GetResponseStream();//6.写入到本地文件中//Debug.Log(Application.persistentDataPath); //这里打印会报错,暂时不知道为什么using (FileStream fileStream = File.Create(localPath)){byte[] bytes = new byte[1024];//读取下载下来的数据流int contentLength = downLoadStream.Read(bytes, 0, bytes.Length);while (contentLength != 0){fileStream.Write(bytes, 0, contentLength);contentLength = downLoadStream.Read(bytes, 0, bytes.Length);}//下载结束 关闭流downLoadStream.Close();fileStream.Close();}Debug.Log("下载结束");}catch (Exception e){Debug.Log("下载出错:" + e.Message);}});//下载结束后执行的委托函数action?.Invoke();}}

其它操作

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;public class FtpMgr 
{private static FtpMgr instance = new FtpMgr();public static FtpMgr Instance => instance;//远程FTP服务器的地址private string FTP_PATH = "ftp://192.168.137.13/";//用户名和密码private string USER_NAME = "Sunset01";private string PASSWORD = "Sunset123";/// <summary>/// 上传文件到Ftp服务器(异步)/// </summary>/// <param name="fileName">FTP的文件名</param>/// <param name="localPath">本地文件路径</param>/// <param name="action">上传完毕后想要做什么的委托函数</param>public async void UploadFile(string fileName, string localPath, UnityAction action = null){await Task.Run(() =>{try{//通过一个线程执行这里面的逻辑 那么就不会影响主线程了//1.创建一个FTP连接FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;//2.进行一些设置//凭证 req.Proxy = null;NetworkCredential n = new NetworkCredential(USER_NAME, PASSWORD);req.Credentials = n;//是否操作结束后 关闭 控制连接req.KeepAlive = false;//传输类型req.UseBinary = true;//操作类型req.Method = WebRequestMethods.Ftp.UploadFile;//3.上传Stream upLoadStream = req.GetRequestStream();//开始上传using (FileStream fileStream = File.OpenRead(localPath)){byte[] bytes = new byte[1024];//返回值 为具体读取了多少字节int contenLength = fileStream.Read(bytes, 0, bytes.Length);// 有数据就上传while (contenLength != 0){//读多少就上传多少upLoadStream.Write(bytes, 0, contenLength);//继续从本地文件中读取contenLength = fileStream.Read(bytes, 0, bytes.Length);}//上传结束fileStream.Close();upLoadStream.Close();}Debug.Log("上传成功");}catch (Exception e){Debug.Log("上传出错:" + e.Message);}});//上传结束后 在外部做的事情action?.Invoke();}/// <summary>/// 下载文件从FTP服务器当中(异步)/// </summary>/// <param name="fileName">FTP上想要下载的文件名</param>/// <param name="localPath">存储的本地文件路径</param>/// <param name="action">下载完毕后想要做什么的委托函数</param>public async void DownLoadFile(string fileName, string localPath, UnityAction action = null){await Task.Run(() =>{try{//1.创建一个Ftp连接FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;//2.设置通信凭证req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);//是否操作后 关闭 控制连接req.KeepAlive = false;//3.设置操作命令req.Method = WebRequestMethods.Ftp.DownloadFile;//4.指定传输类型req.UseBinary = true;//设置代理为nullreq.Proxy = null;//5.得到用于下载的流对象FtpWebResponse res = req.GetResponse() as FtpWebResponse;Stream downLoadStream = res.GetResponseStream();//6.写入到本地文件中//Debug.Log(Application.persistentDataPath); //这里打印会报错,暂时不知道为什么using (FileStream fileStream = File.Create(localPath)){byte[] bytes = new byte[1024];//读取下载下来的数据流int contentLength = downLoadStream.Read(bytes, 0, bytes.Length);while (contentLength != 0){fileStream.Write(bytes, 0, contentLength);contentLength = downLoadStream.Read(bytes, 0, bytes.Length);}//下载结束 关闭流downLoadStream.Close();fileStream.Close();}res.Close();Debug.Log("下载成功");}catch (Exception e){Debug.Log("下载出错:" + e.Message);}});//下载结束后执行的委托函数action?.Invoke();}/// <summary>/// 移除指定的文件/// </summary>/// <param name="fileName">文件名</param>/// <param name="action">移除过后想要做什么的委托函数</param>public async void DeleteFile(string fileName, UnityAction<bool> action = null){await Task.Run(() =>{try{//1.创建一个Ftp连接FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;//2.设置凭证req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);//是否操作结束后 关闭 控制连接req.KeepAlive = false;//传输类型req.UseBinary = true;//操作类型req.Method = WebRequestMethods.Ftp.DeleteFile;//代理设置为空req.Proxy = null;//3.真正的删除FtpWebResponse res = req.GetResponse() as FtpWebResponse;res.Close();action?.Invoke(true);}catch (Exception e){Debug.Log("移除文件失败:" + e.Message);action?.Invoke(false);}});}/// <summary>/// 获取FTP服务器上某个文件的大小(单位 是 字节)/// </summary>/// <param name="fileName">文件名</param>/// <param name="action">获取成功后传递给外部 具体的大小</param>public async void GetFileSize(string fileName, UnityAction<long> action = null){await Task.Run(() =>{try{//1.创建一个Ftp连接FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;//2.设置凭证req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);//是否操作结束后 关闭 控制连接req.KeepAlive = false;//传输类型req.UseBinary = true;//操作类型req.Method = WebRequestMethods.Ftp.GetFileSize;//代理设置为空req.Proxy = null;//3.真正的 获取FtpWebResponse res = req.GetResponse() as FtpWebResponse;//把大小传递给外部action?.Invoke(res.ContentLength);res.Close();                               }catch (Exception e){Debug.Log("移除文件大小失败:" + e.Message);action?.Invoke(0);}});}/// <summary>/// 创建一个文件夹 在FTP服务器上/// </summary>/// <param name="directoryName">文件夹名字</param>/// <param name="action">创建完成后的回调</param>public async void CreateDirectory(string directoryName, UnityAction<bool> action = null){await Task.Run(() =>{try{//1.创建一个Ftp连接FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + directoryName)) as FtpWebRequest;//2.设置凭证req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);//是否操作结束后 关闭 控制连接req.KeepAlive = false;//传输类型req.UseBinary = true;//操作类型req.Method = WebRequestMethods.Ftp.MakeDirectory;//代理设置为空req.Proxy = null;//3.真正的 创建FtpWebResponse res = req.GetResponse() as FtpWebResponse;res.Close();action?.Invoke(true);}catch (Exception e){Debug.Log("创建文件夹失败:" + e.Message);action?.Invoke(false);}});}/// <summary>/// 获取所有文件名/// </summary>/// <param name="directoryName">文件夹路径</param>/// <param name="action">返回给外部使用的 文件名列表</param>public async void GetFileList(string directoryName, UnityAction<List<string>> action = null){await Task.Run(() =>{try{//1.创建一个Ftp连接FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + directoryName)) as FtpWebRequest;//2.设置凭证req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);//是否操作结束后 关闭 控制连接req.KeepAlive = false;//传输类型req.UseBinary = true;//操作类型req.Method = WebRequestMethods.Ftp.ListDirectory;//代理设置为空req.Proxy = null;//3.真正的 创建FtpWebResponse res = req.GetResponse() as FtpWebResponse;//把下载的信息流 转换成StreamReader对象 方便我们一行一行的读取信息Stream s = res.GetResponseStream();StreamReader streamReader = new StreamReader(s);//用于存储文件名的列表List<string> nameStrs = new List<string>();//一行一行的读取string line = streamReader.ReadLine();while (line != null){nameStrs.Add(line);line = streamReader.ReadLine();}res.Close();action?.Invoke(nameStrs);}catch (Exception e){Debug.Log("获取文件列表失败:" + e.Message);action?.Invoke(null);}});}}

超文本传输HTTP

HTTP工作原理

搭建HTTP服务器

C#的相关类

HTTP关键类

下载数据

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;public class HttpMgr 
{private static HttpMgr instance = new HttpMgr();public static HttpMgr Instance => instance;private string HTTP_PATH = "http://172.20.10.2/HTTP_Server/";/// <summary>/// 下载指定文件到本地指定路径中/// </summary>/// <param name="fileName">远程文件名</param>/// <param name="loaclPath">本地路径</param>/// <param name="action">下载结束后的回调函数</param>public async void DownLoad(string fileName, string loacFilePath, UnityAction<HttpStatusCode> action){HttpStatusCode result = HttpStatusCode.OK;await Task.Run(() =>{try{//判断文件是否存在 Head//1.创建HTTP连接对象HttpWebRequest req = WebRequest.Create(new Uri(HTTP_PATH + fileName)) as HttpWebRequest;//2.设置请求类型 和 其它相关参数req.Method = WebRequestMethods.Http.Head;req.Timeout = 2000;//3.发送请求,获取响应结果 HttpWebResponse 对象HttpWebResponse res = req.GetResponse() as HttpWebResponse;//存在才下载if (res.StatusCode == HttpStatusCode.OK){//先把上层的res close了res.Close();//下载//1.创建HTTP连接对象req = WebRequest.Create(new Uri(HTTP_PATH + fileName)) as HttpWebRequest;//2.设置请求类型 和 其它相关参数req.Method = WebRequestMethods.Http.Get;req.Timeout = 2000;//3.发送请求res = req.GetResponse() as HttpWebResponse;//4.存储数据到本地if (res.StatusCode == HttpStatusCode.OK){//存储数据using (FileStream fileStream = File.Create(loacFilePath)){Stream stream = res.GetResponseStream();byte[] bytes = new byte[4096];int contentLength = stream.Read(bytes, 0, bytes.Length);while (contentLength != 0){fileStream.Write(bytes, 0, contentLength);contentLength = stream.Read(bytes, 0, bytes.Length);}stream.Close();fileStream.Close();}result = HttpStatusCode.OK;}else{result = res.StatusCode;}res.Close();}else{result = res.StatusCode;}}catch (WebException w){result = HttpStatusCode.InternalServerError;Debug.Log("下载出错:" + w.Status + w.Message);}});action?.Invoke(result);}
}

上传数据

Post学前准备

上传文件

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using UnityEngine;public class Lesson28 : MonoBehaviour
{// Start is called before the first frame updatevoid Start(){#region 知识点一 上传文件到HTTP资源服务器需要遵守的规则//上传文件时内容的必备规则//  1.ContentType = "multipart/form-data;boundary=边界字符串";//  2.上传的数据必须按照格式写入流中//  --边界字符串//  Content-Disposition:form-data;name="字段名字,之后写入的文件2进制数据和该字段名对应";filename="传到服务器上使用的文件名"//  Content-Type:application/octet-strean(由于我们传2进制文件 所以这里使用2进制)//  空一行//  (这里直接写入传入的内容)//  --边界字符串--//  3.保证服务器允许上传//  4.写入流前需要先设置ContentLength内容长度#endregion#region 知识点二 上传文件//1.创建HttpWebRequest对象HttpWebRequest req = WebRequest.Create("http://172.20.10.2/HTTP_Server/") as HttpWebRequest;//2.相关设置(请求类型、内容类型、超时、身份验证等等)req.Method = WebRequestMethods.Http.Post;req.ContentType = "multipart/form-data;boundary=Sunset";req.Timeout = 500000;//设置凭证req.Credentials = new NetworkCredential("Sunset01", "Sunset123");req.PreAuthenticate = true; //先验证身份 再上传数据//3.按格式拼接字符串并且转为字节数组之后用于上传//3-1.文件数据前的头部信息//  --边界字符串//  Content-Disposition:form-data;name="字段名字,之后写入的文件2进制数据和该字段名对应";filename="传到服务器上使用的文件名"//  Content-Type:application/octet=strean(由于我们传2进制文件 所以这里使用2进制)//  空一行string head = "--Sunset\r\n" +"Content-Disposition:form-data;name=\"file\";filename=\"http上传的文件.jpg\"\r\n" +"Content-Type:application/octet-stream\r\n\r\n";//头部拼接字符串规则信息的字节数组byte[] headBytes = Encoding.UTF8.GetBytes(head);//3-2.结束的边界信息//  --边界字符串--byte[] endBytes = Encoding.UTF8.GetBytes("\r\n--Sunset--\r\n");//4.写入上传流  using (FileStream localFileStream = File.OpenRead(Application.streamingAssetsPath + "/test.png")){//4-1.设置上传长度//总长度 是前部分字符串 + 文件本身有多大 + 后部分边界字符串req.ContentLength = headBytes.Length + localFileStream.Length + endBytes.Length;//用于上传的流Stream upLoadStream = req.GetRequestStream();//4-2.先写入前部分头部信息upLoadStream.Write(headBytes, 0, headBytes.Length);//4-3.再写入文件数据byte[] bytes = new byte[2048];int contentLength = localFileStream.Read(bytes, 0, bytes.Length);while (contentLength != 0){upLoadStream.Write(bytes, 0, contentLength);contentLength = localFileStream.Read(bytes, 0, bytes.Length);}//4-4.再写入结束的边界信息upLoadStream.Write(endBytes, 0, endBytes.Length);upLoadStream.Close();localFileStream.Close();}//5.上传数据,获取响应HttpWebResponse res = req.GetResponse() as HttpWebResponse;if (res.StatusCode == HttpStatusCode.OK)print("上传通信成功");elseprint("上传失败:" + res.StatusCode);#endregion#region 总结//HTTP上传文件相对比较麻烦//需要按照指定的规则进行内容拼接达到上传文件的目的//其中相对重要的知识点是//s上传文件时的规则//  --边界字符串//  Content-Disposition:form-data;name="file";filename="传到服务器上使用的文件名"//  Content-Type:application/octet-strean(由于我们传2进制文件 所以这里使用2进制)//  空一行//  (这里直接写入传入的内容)//  --边界字符串--//关于其更多的规则,可以前往官网查看详细说明//关于ContentType更多内容可以前往//https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Type//关于媒体类型可以前往//https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types//关于Content -Disposition更多内容可以前往//https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition#endregion}// Update is called once per framevoid Update(){}
}

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;public class HttpMgr 
{private static HttpMgr instance = new HttpMgr();public static HttpMgr Instance => instance;private string HTTP_PATH = "http://172.20.10.2/HTTP_Server/";private string USER_NAME = "Sunset01";private string PASS_WORD = "Sunset123";/// <summary>/// 下载指定文件到本地指定路径中/// </summary>/// <param name="fileName">远程文件名</param>/// <param name="loaclPath">本地路径</param>/// <param name="action">下载结束后的回调函数</param>public async void DownLoad(string fileName, string loacFilePath, UnityAction<HttpStatusCode> action){HttpStatusCode result = HttpStatusCode.OK;await Task.Run(() =>{try{//判断文件是否存在 Head//1.创建HTTP连接对象HttpWebRequest req = WebRequest.Create(new Uri(HTTP_PATH + fileName)) as HttpWebRequest;//2.设置请求类型 和 其它相关参数req.Method = WebRequestMethods.Http.Head;req.Timeout = 2000;//3.发送请求,获取响应结果 HttpWebResponse 对象HttpWebResponse res = req.GetResponse() as HttpWebResponse;//存在才下载if (res.StatusCode == HttpStatusCode.OK){//先把上层的res close了res.Close();//下载//1.创建HTTP连接对象req = WebRequest.Create(new Uri(HTTP_PATH + fileName)) as HttpWebRequest;//2.设置请求类型 和 其它相关参数req.Method = WebRequestMethods.Http.Get;req.Timeout = 2000;//3.发送请求res = req.GetResponse() as HttpWebResponse;//4.存储数据到本地if (res.StatusCode == HttpStatusCode.OK){//存储数据using (FileStream fileStream = File.Create(loacFilePath)){Stream stream = res.GetResponseStream();byte[] bytes = new byte[4096];int contentLength = stream.Read(bytes, 0, bytes.Length);while (contentLength != 0){fileStream.Write(bytes, 0, contentLength);contentLength = stream.Read(bytes, 0, bytes.Length);}stream.Close();fileStream.Close();}result = HttpStatusCode.OK;}else{result = res.StatusCode;}res.Close();}else{result = res.StatusCode;}}catch (WebException w){result = HttpStatusCode.InternalServerError;Debug.Log("下载出错:" + w.Status + w.Message);}});action?.Invoke(result);}/// <summary>/// 上传文件/// </summary>/// <param name="fileName">传到远端服务器上的文件名</param>/// <param name="loacalFilePath">本地的文件路径</param>/// <param name="action">上传结束后的回调函数</param>public async void UpLoadFile(string fileName, string localFilePath, UnityAction<HttpStatusCode> action){HttpStatusCode result = HttpStatusCode.BadRequest;await Task.Run(() =>{try{//1.创建 HttpWebRequestHttpWebRequest req = WebRequest.Create(HTTP_PATH) as HttpWebRequest;//2.相关设置(请求类型、内容类型、超时、身份验证等等)req.Method = WebRequestMethods.Http.Post;req.ContentType = "multipart/form-data;boundary=Sunset";req.Timeout = 500000;//设置凭证req.Credentials = new NetworkCredential(USER_NAME, PASS_WORD);req.PreAuthenticate = true;  //先验证身份 再上传数据//3.按格式拼接字符串并且转为字节数组之后用于上传//3-1.文件数据前端头部信息string head = "--Sunset\r\n" +"Content-Disposition:form-data;name=\"file\";filename=\"{0}\"\r\n" +"Content-Type:application/octet-stream\r\n\r\n";//替换文件名head = string.Format(head, fileName);//头部拼接字符串规则信息的字节数组byte[] headBytes = Encoding.UTF8.GetBytes(head);//3-2.结束的边界信息// --边界字符串--byte[] endBytes = Encoding.UTF8.GetBytes("\r\n--Sunset--\r\n");//4.写入上传流using (FileStream localFileStream = File.OpenRead(localFilePath)){//4-1.设置上传长度req.ContentLength = headBytes.Length + localFileStream.Length + endBytes.Length;//用于上传的流Stream upLoadStream = req.GetRequestStream();//4-2.先写头部upLoadStream.Write(headBytes, 0, headBytes.Length);//4-3.再写文件数据byte[] bytes = new byte[4096];int contentLength = localFileStream.Read(bytes, 0, bytes.Length);while (contentLength != 0){upLoadStream.Write(bytes, 0, contentLength);contentLength = localFileStream.Read(bytes, 0, bytes.Length);}//4-4.再写入结束的边界信息upLoadStream.Write(endBytes, 0, endBytes.Length);localFileStream.Close();upLoadStream.Close();}//5.上传数据 获取响应HttpWebResponse res = req.GetResponse() as HttpWebResponse;//让外部去处理结束result = res.StatusCode;res.Close();}catch (WebException w){Debug.Log("上传出错:" + w.Status + w.Message);}});//要放在主线程处理,如果放上面的线程里可能会报错(无法访问主线程的部分内容)action?.Invoke(result);}}

测试

Unity的相关类

WWW类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;public class Lesson29 : MonoBehaviour
{public RawImage image;// Start is called before the first frame updatevoid Start(){#region 知识点一 WWW类的作用//WWw是Unity提供给我们简单的访问网页的类//我们可以通过该类下载和上传一些数据//在使用http协议时,默认的请求类型是Get,如果想要Post上传,需要配合下节课学习的WwWFrom类使用//它主要支持的协议//1. http:// 和https://超文本传输协议//2.ftp://文件传输协议(但仅限于匿名下载)//3. file://本地文件传输协议,可以使用该协议异步加载本地文件(PC、IOS、Android都支持)//我们本节课主要学习利用www来进行数据的下载或加载//注意://1.该类般配合 协同程序使用//2.该类在较新Unity版本中会提示过时,但是仍可以使用,新版本将其功能整合进了UnityWebRequest类(之后讲解)#endregion#region 知识点二 WWW类的常用方法和变量#region 常用方法//1.WWW:构造函数,用于创建一个WWW请求WWW www = new WWW("http://172.20.10.2/HTTP_Server/Test2.jpg");//2.GetAudioClip:从下载数据返回一个音效切片AudioClip对象//www.GetAudioClip();//3.LoadImageIntoTexture: 用下载数据中的图像来替换现有的一个Texture2D对象//Texture2D tex = new Texture2D(100, 100);//www.LoadImageIntoTexture(tex);//4.LoadFromCacheOrDownload: 从缓存加载AB包对象,如果该包不在缓存则自动下载存储到缓存中,一遍以后直接从本地缓存中加载//www.LoadFromCacheOrDownload("http://172.20.10.2/HTTP_Server/Test2.jpg", 1);#endregion#region 常用变量//1.assetBundle: 如果加载的数据是AB包,可以通过该变量直接获取加载结果//www.assetBundle();//2.audioClip:如果加载的数据是音效切片文件,可以通过该变量直接获取加载结果(老版本里,新版本会变成GetAudioClip)//www.GetAudioClip//3.bytes: 以字节数组的形式获取加载到的内容//www.bytes//4.bytesDownloaded: 过去已下载的字节数//www.bytesDownloaded//5.error:返回一个错误消息,如果下载期间出现错误,可以通过它获取错误信息//www.error != null//6.isDone:判断下载是否已经完成//www.isDone//7.movie:如果下载的视频,可以获取一个MovieTexture类型结果//www.GetMovieTexture()//8.progress:下载进度//www.progress//9.text:如果下载的数据是字符串,以字符串的形式返回内容//www.text//10.texture:如果下载的数据是图片,以Texture2D的形式返回加载结果//www.texture#endregion#endregion#region 知识点三 利用www类来异步下载或加载文件#region 1.下载HTTP服务器上的内容//StartCoroutine(DownLoadHttp());#endregion#region 2.下载FTP服务器上的内容(FTP服务器一定要支持匿名账号)//StartCoroutine(DownLoadFtp());#endregion#region 3.本地内容加载(一般移动平台加载数据都会使用该方式)StartCoroutine(DownLoadLocal());#endregion#endregion#region 总结//Unity中的WWW类比使用C#中的Http、FTP相关类更加的方便//建议大家使用Unity中为我们封装好的类来处理下载、加载相关逻辑#endregion}//协程 HTTP下载IEnumerator DownLoadHttp(){//1.创建www对象WWW www = new WWW("http://172.20.10.2/HTTP_Server/Test2.jpg");//2.就是等待加载结束//yield return www;while (!www.isDone){print(www.bytesDownloaded);  //下载了多少字节print(www.progress);  //下载进度yield return null;}print(www.bytesDownloaded);  print(www.progress);  //3.使用加载结束后的资源if (www.error == null){image.texture = www.texture;}else{print(www.error);}}//协程 FTP下载IEnumerator DownLoadFtp(){//1.创建www对象WWW www = new WWW("ftp://127.0.0.1/Test2.jpg");//2.就是等待加载结束//yield return www;while (!www.isDone){print(www.bytesDownloaded);  //下载了多少字节print(www.progress);  //下载进度yield return null;}print(www.bytesDownloaded);print(www.progress);//3.使用加载结束后的资源if (www.error == null){image.texture = www.texture;}else{print(www.error);}}//协程 本地下载IEnumerator DownLoadLocal(){//1.创建www对象WWW www = new WWW("file://" + Application.streamingAssetsPath + "/Test2.jpg");//2.就是等待加载结束//yield return www;while (!www.isDone){print(www.bytesDownloaded);  //下载了多少字节print(www.progress);  //下载进度yield return null;}print(www.bytesDownloaded);print(www.progress);//3.使用加载结束后的资源if (www.error == null){image.texture = www.texture;}else{print(www.error);}}// Update is called once per framevoid Update(){}
}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;public class NetWWWMgr : MonoBehaviour
{private static NetWWWMgr instance;public static NetWWWMgr Instance => instance;void Awake(){instance = this;DontDestroyOnLoad(this.gameObject);}/// <summary>/// 提供给外部加载资源用的方法/// </summary>/// <typeparam name="T">资源的类型</typeparam>/// <param name="path">资源的路径</param>/// <param name="action">加载结束后的回调函数 因为www是通过结合协同程序异步加载的 所以不能马上获取结果 需要回调获取</param>public void LoadRes<T>(string path, UnityAction<T> action) where T : class{StartCoroutine(LoadResAsync<T>(path, action));}private IEnumerator LoadResAsync<T>(string path, UnityAction<T> action) where T : class{//声明www对象 用于下载或加载WWW www = new WWW(path);//等待下载或加载结束(异步)yield return www;//如果没有错误 证明加载成功if (www.error == null){//根据T泛型的类型 决定使用哪种类型的资源 传递给外部if (typeof(T) == typeof(AssetBundle)){action?.Invoke(www.assetBundle as T);}else if(typeof(T) == typeof(Texture)){action?.Invoke(www.texture as T);}else if(typeof(T) == typeof(AudioClip)){action?.Invoke(www.GetAudioClip() as T);}else if(typeof(T) == typeof(string)){action?.Invoke(www.text as T);}else if(typeof(T) == typeof(byte[])){action?.Invoke(www.bytes as T);}//还可以写一些 自定义类型 可能需要将bytes 转换成对应的类型来使用}//如果错误 就提示别人else{Debug.LogError("www加载资源出错:" + www.error);}}}

测试

WWWForm类

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;public class NetWWWMgr : MonoBehaviour
{private static NetWWWMgr instance;public static NetWWWMgr Instance => instance;private string HTTP_SERVER_PATH = "http://172.20.10.2/HTTP_Server/";void Awake(){instance = this;DontDestroyOnLoad(this.gameObject);}/// <summary>/// 提供给外部加载资源用的方法/// </summary>/// <typeparam name="T">资源的类型</typeparam>/// <param name="path">资源的路径</param>/// <param name="action">加载结束后的回调函数 因为www是通过结合协同程序异步加载的 所以不能马上获取结果 需要回调获取</param>public void LoadRes<T>(string path, UnityAction<T> action) where T : class{StartCoroutine(LoadResAsync<T>(path, action));}private IEnumerator LoadResAsync<T>(string path, UnityAction<T> action) where T : class{//声明www对象 用于下载或加载WWW www = new WWW(path);//等待下载或加载结束(异步)yield return www;//如果没有错误 证明加载成功if (www.error == null){//根据T泛型的类型 决定使用哪种类型的资源 传递给外部if (typeof(T) == typeof(AssetBundle)){action?.Invoke(www.assetBundle as T);}else if(typeof(T) == typeof(Texture)){action?.Invoke(www.texture as T);}else if(typeof(T) == typeof(AudioClip)){action?.Invoke(www.GetAudioClip() as T);}else if(typeof(T) == typeof(string)){action?.Invoke(www.text as T);}else if(typeof(T) == typeof(byte[])){action?.Invoke(www.bytes as T);}//还可以写一些 自定义类型 可能需要将bytes 转换成对应的类型来使用}//如果错误 就提示别人else{Debug.LogError("www加载资源出错:" + www.error);}}public void SendMsg<T>(BaseMsg msg, UnityAction<T> action) where T : BaseMsg{StartCoroutine(SendMsgAsync<T>(msg, action));}private IEnumerator SendMsgAsync<T>(BaseMsg msg, UnityAction<T> action) where T : BaseMsg{//消息发送WWWForm data = new WWWForm();//准备要发送的消息数据data.AddBinaryData("Msg", msg.Writing());//发送 相关WWW www = new WWW(HTTP_SERVER_PATH, data);//我们也可以直接传递 2进制字节数组 只要和后端定好规则 怎么传递都是可以的//WWW www = new WWW("HTTP_SERVER_PATH", msg.Writing());//异步等待 发送结束 才会继续执行后面的代码yield return www;//发送完成后 收到响应//认为 后端发回来的内容 也是一个继承自Basemsg类的一个字节数组对象if (www.error == null){//先解析 ID和消息体长度int index = 0;int msgID = BitConverter.ToInt32(www.bytes, index);index += 4;int msgLength = BitConverter.ToInt32(www.bytes, index);index += 4;//反序列化 BaseMsg  (就先举例 1001)BaseMsg baseMsg = null;switch (msgID){case 1001:baseMsg = new PlayerMsg();baseMsg.Reading(www.bytes, index);break;}if (baseMsg != null)action?.Invoke(baseMsg as T);}else{Debug.LogError("发消息出问题:" + www.error);}}}
UnityWebRequest类

常用操作——获取数据
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;public class Lesson31 : MonoBehaviour
{public RawImage image;// Start is called before the first frame updatevoid Start(){#region 知识点一 UnityWebRequest 是什么?//UnityWebRequest 是一个Unity提供的一个模块化的系统类//用于构建HTTP请求和处理HTTP响应//它主要目标是让Unity游戏和Web服务端进行交互//它将之前WWW 的相关功能都集成在了其中//所以新版本中都建议选择使用UnityWebRequest类来代替www类//它在使用上和WWW很类似//主要的区别是UnityWebRequest把下载下来的数据处理单独提取出来了//我们可以根据自己的需求选择对应的数据处理对象来获取数据//注意://1.UnityWebRequest和WWW一样,需要配合协同程序使用//2.UnityWebRequest和WWW一样,支持http、ftp、file协议下载或加载资源//3.UnityWebRequest能够上传文件到HTTP资源服务器#endregion#region 知识点二 UnityWebRequest类的常用操作//1.使用Get请求获取文本或二进制数据//2.使用Get请求获取纹理数据//3.使用Get请求获取AB包数据//4.使用Post请求发送数据//5.使用Put请求上传数据#endregion#region 知识点三 Get获取操作//1.获取文本或2进制StartCoroutine(LoadText());//2.获取纹理StartCoroutine(LoadTexture());//3.获取AB包StartCoroutine(LoadAB());#endregion#region 总结//UnityWebRequest 使用上和www类相似//我们需要注意的是//1.获取文本或二进制数据时//使用UnityWebRequest.Get//2.获取纹理图片数据时//使用UnityWebRequestTexture.GetTexture//以及 DownloadHandlerTexture.GetContent//3.获取AB包数据时//使用UnityWebRequestAssetBundle.GetAssetBundle//以及 DownloadHandlerAs setBundle.GetContent#endregion}IEnumerator LoadText(){UnityWebRequest req = UnityWebRequest.Get("http://172.20.10.2/HTTP_Server/test.txt");//就会等待 服务器端响应后 断开连接后 再继续执行后面的内容yield return req.SendWebRequest();//如果处理成功 结果就是成功枚举if (req.result == UnityWebRequest.Result.Success){//文本 字符串print(req.downloadHandler.text);//字节数组byte[] bytes = req.downloadHandler.data;print("字节数组长度:" + bytes.Length);}else{print("获取失败:" + req.result + req.error + req.responseCode);}}IEnumerator LoadTexture(){//UnityWebRequest req = UnityWebRequestTexture.GetTexture("http://172.20.10.2/HTTP_Server/Test2.jpg");//UnityWebRequest req = UnityWebRequestTexture.GetTexture("ftp://127.0.0.1/Test2.jpg");UnityWebRequest req = UnityWebRequestTexture.GetTexture("file://" + Application.streamingAssetsPath + "/Test2.jpg");yield return req.SendWebRequest();if (req.result == UnityWebRequest.Result.Success){//两种方式//(req.downloadHandler as DownloadHandlerTexture).texture//DownloadHandlerTexture.GetContent(req)//image.texture = (req.downloadHandler as DownloadHandlerTexture).texture;image.texture = DownloadHandlerTexture.GetContent(req);}else{print("获取失败:" + req.error + req.result + req.responseCode);}}IEnumerator LoadAB(){UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle("http://172.20.10.2/HTTP_Server/lua");//若是要获取进度req.SendWebRequest();while (!req.isDone){print(req.downloadProgress);  //下载进度print(req.downloadedBytes);  //下载的字节数yield return null;}print(req.downloadProgress);  //下载进度print(req.downloadedBytes);  //下载的字节数//yield return req.SendWebRequest();if (req.result == UnityWebRequest.Result.Success){//AssetBundle ab = (req.downloadHandler as DownloadHandlerAssetBundle).assetBundle;AssetBundle ab = DownloadHandlerAssetBundle.GetContent(req);print(ab.name);}else{print("下载AB包失败:" + req.result + req.error + req.responseCode);}}// Update is called once per framevoid Update(){}
}
常用操作—上传数据
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;public class Lesson32 : MonoBehaviour
{// Start is called before the first frame updatevoid Start(){#region 知识点一 上传相关数据类//父接口//IMultipartFormSection//数据相关类都继承该接口//我们可以用父类装子类List<IMultipartFormSection> dataList = new List<IMultipartFormSection>();//子类数据//MultipartFormDataSection//1.二进制字节数组dataList.Add(new MultipartFormDataSection(Encoding.UTF8.GetBytes("山有木兮")));//2.字符串dataList.Add(new MultipartFormDataSection("木有枝"));//3.参数名,参数值(字节数组,字符串),编码类型,资源类型(常用)dataList.Add(new MultipartFormDataSection("Name", "Sunset", Encoding.UTF8, "application/..."));dataList.Add(new MultipartFormDataSection("Msg", new byte[1024], "application/..."));//MultipartFormFileSection//1.字节数组dataList.Add(new MultipartFormFileSection(File.ReadAllBytes(Application.streamingAssetsPath + "/Test2.jpg")));//2.文件名,字节数组(常用)dataList.Add(new MultipartFormFileSection("上传的文件.jpg", File.ReadAllBytes(Application.streamingAssetsPath + "/Test2.jpg")));//3.字符串数据,文件名(常用)dataList.Add(new MultipartFormFileSection("山有木兮", "test.txt"));//4.字符串数据,编码格式,文件名(常用)dataList.Add(new MultipartFormFileSection("山有木兮", Encoding.UTF8, "test.txt"));//5.表单名,字节数组,文件名,文件类型dataList.Add(new MultipartFormFileSection("file", new byte[1024], "test.txt", ""));//6.表单名,字符串数据,编码格式,文件名dataList.Add(new MultipartFormFileSection("file", "123123", Encoding.UTF8, "test.txt"));#endregion#region 知识点二 Post发送相关StartCoroutine(Upload());#endregion#region 知识点三 Put上传相关//注意:Put请求类型不是所有的web服务器都认,必须要服务器处理该请求类型那么才能有响应//StartCoroutine(UpLoadPut());#endregion#region 总结//我们可以利用Post上传数据或上传文件//Put主要用于上传文件,但是必须资源服务器支持Put请求类型//为了通用性,我们可以统一使用Post请求类型进行数据和资源的上传//它的使用和之前的WWW类似,只要前后端制定好规则就可以相互通信#endregion}IEnumerator Upload(){//准备上传的数据List<IMultipartFormSection> data = new List<IMultipartFormSection>();//键值对相关的 信息 字段数据data.Add(new MultipartFormDataSection("Name", "Sunset"));//PlayerMsg msg = new PlayerMsg();//data.Add(new MultipartFormDataSection("Msg", msg.Writing()));//添加一些文件 上传文件//传2进制文件data.Add(new MultipartFormFileSection("TestTest123.jpg", File.ReadAllBytes(Application.streamingAssetsPath + "/Test2.jpg")));//传文本文件data.Add(new MultipartFormFileSection("山有木兮", "Test123.txt"));UnityWebRequest req = UnityWebRequest.Post("http://172.20.10.2/HTTP_Server/", data);req.SendWebRequest();while (!req.isDone){print(req.uploadProgress);  //上传进度print(req.uploadedBytes);   //上传字节yield return null;}print(req.uploadProgress);  //上传进度print(req.uploadedBytes);   //上传字节if (req.result == UnityWebRequest.Result.Success){print("上传成功");//req.downloadHandler.data  //服务器传回的数据 可以进行处理 只是我们的服务器没有处理}elseprint("上传失败:" + req.result + req.error + req.responseCode);}IEnumerator UpLoadPut(){UnityWebRequest req = UnityWebRequest.Put("http://172.20.10.2/HTTP_Server/", File.ReadAllBytes(Application.streamingAssetsPath + "/Test2.jpg"));yield return req.SendWebRequest();if (req.result == UnityWebRequest.Result.Success){print("Put 上传成功");}elseprint("上传失败:" + req.result + req.error + req.responseCode);}// Update is called once per framevoid Update(){}
}

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;public class NetWWWMgr : MonoBehaviour
{private static NetWWWMgr instance;public static NetWWWMgr Instance => instance;private string HTTP_SERVER_PATH = "http://172.20.10.2/HTTP_Server/";void Awake(){instance = this;DontDestroyOnLoad(this.gameObject);}/// <summary>/// 提供给外部加载资源用的方法/// </summary>/// <typeparam name="T">资源的类型</typeparam>/// <param name="path">资源的路径</param>/// <param name="action">加载结束后的回调函数 因为www是通过结合协同程序异步加载的 所以不能马上获取结果 需要回调获取</param>public void LoadRes<T>(string path, UnityAction<T> action) where T : class{StartCoroutine(LoadResAsync<T>(path, action));}private IEnumerator LoadResAsync<T>(string path, UnityAction<T> action) where T : class{//声明www对象 用于下载或加载WWW www = new WWW(path);//等待下载或加载结束(异步)yield return www;//如果没有错误 证明加载成功if (www.error == null){//根据T泛型的类型 决定使用哪种类型的资源 传递给外部if (typeof(T) == typeof(AssetBundle)){action?.Invoke(www.assetBundle as T);}else if(typeof(T) == typeof(Texture)){action?.Invoke(www.texture as T);}else if(typeof(T) == typeof(AudioClip)){action?.Invoke(www.GetAudioClip() as T);}else if(typeof(T) == typeof(string)){action?.Invoke(www.text as T);}else if(typeof(T) == typeof(byte[])){action?.Invoke(www.bytes as T);}//还可以写一些 自定义类型 可能需要将bytes 转换成对应的类型来使用}//如果错误 就提示别人else{Debug.LogError("www加载资源出错:" + www.error);}}public void SendMsg<T>(BaseMsg msg, UnityAction<T> action) where T : BaseMsg{StartCoroutine(SendMsgAsync<T>(msg, action));}private IEnumerator SendMsgAsync<T>(BaseMsg msg, UnityAction<T> action) where T : BaseMsg{//消息发送WWWForm data = new WWWForm();//准备要发送的消息数据data.AddBinaryData("Msg", msg.Writing());//发送 相关WWW www = new WWW(HTTP_SERVER_PATH, data);//我们也可以直接传递 2进制字节数组 只要和后端定好规则 怎么传递都是可以的//WWW www = new WWW("HTTP_SERVER_PATH", msg.Writing());//异步等待 发送结束 才会继续执行后面的代码yield return www;//发送完成后 收到响应//认为 后端发回来的内容 也是一个继承自Basemsg类的一个字节数组对象if (www.error == null){//先解析 ID和消息体长度int index = 0;int msgID = BitConverter.ToInt32(www.bytes, index);index += 4;int msgLength = BitConverter.ToInt32(www.bytes, index);index += 4;//反序列化 BaseMsg  (就先举例 1001)BaseMsg baseMsg = null;switch (msgID){case 1001:baseMsg = new PlayerMsg();baseMsg.Reading(www.bytes, index);break;}if (baseMsg != null)action?.Invoke(baseMsg as T);}else{Debug.LogError("发消息出问题:" + www.error);}}/// <summary>/// 上传文件的方法/// </summary>/// <param name="fileName">上传上去的文件名</param>/// <param name="localPath">本地想要上传文件的路径</param>/// <param name="action">上传完成后的回调函数</param>public void UploadFile(string fileName, string localPath, UnityAction<UnityWebRequest.Result> action){StartCoroutine(UploadFileAsync(fileName, localPath, action));}private IEnumerator UploadFileAsync(string fileName, string localPath, UnityAction<UnityWebRequest.Result> action){//添加要上传文件的数据List<IMultipartFormSection> dataList = new List<IMultipartFormSection>();dataList.Add(new MultipartFormFileSection(fileName, File.ReadAllBytes(localPath)));UnityWebRequest req = UnityWebRequest.Post(HTTP_SERVER_PATH, dataList);yield return req.SendWebRequest();action?.Invoke(req.result);//如果不成功if (req.result != UnityWebRequest.Result.Success){Debug.LogWarning("上传出问题" + req.result + req.error + req.responseCode);}}}

测试

高级操作—获取数据
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;public class Lesson33 : MonoBehaviour
{public RawImage image;// Start is called before the first frame updatevoid Start(){#region 知识点一 高级操作是什么?//在常用操作中我们使用的是Unity为我们封装好的一些方法//我们可以方便的进行一些指定类型的数据获取//比如//下载数据时://1.文本和2进制//2.图片//3. AB包//如果我们想要获取其它类型的数据应该如何处理呢?//上传数据时://1.可以指定参数和值//2.可以上传文件//如果想要上传些基FHTTP规则的其它数据应该如何处理呢?//高级操作就是用来处理常用操作不能完成的需求的//它的核心思想就是: UnityWebRequest中可以将数据处理分离开//比如常规操作中我们用到的//DownloadHandlerTexture和DownloadHandlerAssetBundle两个类//就是用来将2进制字节数组转换成对应类型进行处理的//所以高级操作时指让你按照规则来实现更多的数据获取、上传等功能#endregion#region 知识点二 UnityWebRequest类的更多内容//目前已经学习到的内容//获取数据时讲解的内容//UnityWebRequest req = UnityWebRequest.Get("");//UnityWebRequest req = UnityWebRequestTexture.GetTexture("");//UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle("");//上传是讲解的//UnityWebRequest req = UnityWebRequest.Post();//UnityWebRequest req = UnityWebRequest.Put();//req.isDone//req.downloadProgress//req.downloadedBytes//req.uploadProgress//req.uploadedBytes//req.SendWebRequest();//更多内容//1.构造函数//UnityWebRequest req = new UnityWebRequest();//2.请求地址//req.url = "服务器地址";//3.请求类型//req.method = UnityWebRequest.kHttpVerbPOST;//4.进度//req.downloadProgress//req.uploadProgress//5.超时设置//req.timeout = 2000;//6.上传、下载的字节数//req.downloadedBytes//req.uploadedBeytes//7.重定向次数 设置为0表示不进行重定向 可以设置次数//req.redirectLimit = 10;//8.状态码、结果、错误内容//req.responseCode//req.result//req.error//9.下载、上传处理对象//req.downloadHandler//req.uploadHandler//更多内容//https://docs.unity.cn/cn/2020.3/ScriptReference/Networking.UnityWebRequest.htm1#endregion#region 知识点三 自定义获取数据DownloadHandler相关类//关键类://1.DownloadHandlerBuffer 用于简单的数据存储,得到对应的2进制数据//2.DownloadHandlerFile  用于下载文件并将文件保存到磁盘(内存占用少)//3.DownloadHandlerTexture  用于下载图像//4.DownloadHandlerAssetBundle  用于提取 AssetBundle//5.DownloadHandlerAudioClip  用于下载音频文件StartCoroutine(DownLoadTex());//StartCoroutine(DownLoadAB());//以上的这些类,其实就是Unity帮助我们实现好的,用于解析下载下来的数据的类//使用对应的类处理下载数据,他们就会在内部将下载的数据处理为对应的类型,方便我们使用//DownloadHandlerScript 是一个特殊类,就其本身而言,不会执行任何操作//但是,此类可由用户定义的类继承,此类接收来自 UnityWebRequest 系统的回调//然后可以使用这些回调在数据从网络到达时执行完全自定义的数据处理print(Application.persistentDataPath);StartCoroutine(DownLoadCustomHandler());#endregion#region 总结//我们可以自己设置UnityWebRequest 当中的下载处理对象//当设置后,下载数据后它会使用该对象中对于的函数处理数据//让我们更方便的获取我们想要的数据//方便我们对数据下载或获取进行拓展#endregion}IEnumerator DownLoadTex(){UnityWebRequest req = new UnityWebRequest("http://172.20.10.2/HTTP_Server/Test2.jpg",UnityWebRequest.kHttpVerbGET);//req.method = UnityWebRequest.kHttpVerbGET;  //也可以直接写到第二个参数里//1.DownloadHandlerBuffer//DownloadHandlerBuffer bufferHandler = new DownloadHandlerBuffer();//req.downloadHandler = bufferHandler;//2.DownloadHandlerFile//print(Application.persistentDataPath);//req.downloadHandler = new DownloadHandlerFile(Application.persistentDataPath + "/downloadFile.jpg");//3.DownloadHandlerTextureDownloadHandlerTexture textureHandler = new DownloadHandlerTexture();req.downloadHandler = textureHandler;yield return req.SendWebRequest();if (req.result == UnityWebRequest.Result.Success){//获取字节数组//bufferHandler.dataimage.texture = textureHandler.texture;}else{print("获取数据失败" + req.result + req.error + req.responseCode);}}IEnumerator DownLoadAB(){UnityWebRequest req = new UnityWebRequest("http://172.20.10.2/HTTP_Server/lua", UnityWebRequest.kHttpVerbGET);//4.DownloadHandlerAssetBundle//第二个参数 需要已知校检码 才能进行比较 检查完整性 如果不知道的话 只能传0 不进行完整性的检查//所以一般 只有进行AB包热更新时 服务器发送了对应的 文件列表中 包含了 验证码 才能进行验证DownloadHandlerAssetBundle headler = new DownloadHandlerAssetBundle(req.url, 0);req.downloadHandler = headler;yield return req.SendWebRequest();if (req.result == UnityWebRequest.Result.Success){AssetBundle ab = headler.assetBundle;print(ab.name);}else{print("获取数据失败" + req.result + req.error + req.responseCode);}}IEnumerator DownLoadAudioClip(){UnityWebRequest req = UnityWebRequestMultimedia.GetAudioClip("http://172.20.10.2/HTTP_Server/音效名.mp3",AudioType.MPEG);yield return req.SendWebRequest();if (req.result == UnityWebRequest.Result.Success){//5.DownloadHandlerAudioClipAudioClip a =  DownloadHandlerAudioClip.GetContent(req);}else{print("获取数据失败" + req.result + req.error + req.responseCode);}}IEnumerator DownLoadCustomHandler(){UnityWebRequest req = new UnityWebRequest("http://172.20.10.2/HTTP_Server/Test2.jpg", UnityWebRequest.kHttpVerbGET);//使用自定义的下载处理对象 来处理获取到的 2进制字节数组req.downloadHandler = new CustomDownLoadFileHandler(Application.persistentDataPath + "/CustomHandler.jpg");yield return req.SendWebRequest();if (req.result == UnityWebRequest.Result.Success){print("存储本地成功");}else{print("获取数据失败" + req.result + req.error + req.responseCode);}}// Update is called once per framevoid Update(){}
}public class CustomDownLoadFileHandler : DownloadHandlerScript
{//用于保存 本地存储时的路径private string savePath;//用于缓存收到的数据的容器private byte[] cacheBytes;//当前已收到的数据长度private int index = 0;public CustomDownLoadFileHandler() : base(){}public CustomDownLoadFileHandler(byte[] bytes) : base(bytes) {}public CustomDownLoadFileHandler(string path) : base(){savePath = path;}protected override byte[] GetData(){//返回字节数组return cacheBytes;}/// <summary>/// 从网络收到数据后 每帧会调用的方法 会自动调用的方法/// </summary>/// <param name="data"></param>/// <param name="dataLength"></param>/// <returns></returns>protected override bool ReceiveData(byte[] data, int dataLength){Debug.Log("收到数据长度:" + data.Length);Debug.Log("收到数据长度dataLength:" + dataLength);data.CopyTo(cacheBytes, index);index += dataLength;return true;}/// <summary>/// 从服务器收到 Content-Length 标头时 会自动调用的方法/// </summary>/// <param name="contentLength"></param>protected override void ReceiveContentLengthHeader(ulong contentLength){//base.ReceiveContentLengthHeader(contentLength);Debug.Log("收到数据长度:" + contentLength);//根据收到的标头 决定字节数组容器的大小cacheBytes = new byte[contentLength];}/// <summary>/// 当消息收完了 会自动调用的方法/// </summary>protected override void CompleteContent(){Debug.Log("消息收完");//把收到的字节数组 进行自定义处理 我们在这 处理成 存储到本地File.WriteAllBytes(savePath, cacheBytes);}}

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;public class DownLoadHandlerMsg : DownloadHandlerScript
{//我们最终想要的消息对象private BaseMsg msg;//用于装载收到的字节数组的private byte[] cacheBytes;private int index = 0;public DownLoadHandlerMsg() : base(){}public T GetMsg<T>() where T : BaseMsg{return msg as T;}protected override byte[] GetData(){return cacheBytes;}protected override bool ReceiveData(byte[] data, int dataLength){//将收到的数据 拷贝到容器中 到最后一起处理data.CopyTo(cacheBytes, index);index += dataLength;return true;}protected override void ReceiveContentLengthHeader(ulong contentLength){cacheBytes = new byte[contentLength];}protected override void CompleteContent(){//默认服务器下发的是继承BaseMsg的消息 那么我们在完成时解析它index = 0;int msgID = BitConverter.ToInt32(cacheBytes, index);index += 4;int msgLength = BitConverter.ToInt32(cacheBytes, index);index += 4;switch (msgID){case 1001:msg = new PlayerMsg();msg.Reading(cacheBytes, index);break;}if (msg == null)Debug.Log("对应ID" + msgID + "没有处理");elseDebug.Log("消息处理完毕");}}

测试

高级操作—上传数据

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;public class NetWWWMgr : MonoBehaviour
{private static NetWWWMgr instance;public static NetWWWMgr Instance => instance;private string HTTP_SERVER_PATH = "http://172.20.10.2/HTTP_Server/";void Awake(){instance = this;DontDestroyOnLoad(this.gameObject);}/// <summary>/// 提供给外部加载资源用的方法/// </summary>/// <typeparam name="T">资源的类型</typeparam>/// <param name="path">资源的路径</param>/// <param name="action">加载结束后的回调函数 因为www是通过结合协同程序异步加载的 所以不能马上获取结果 需要回调获取</param>public void LoadRes<T>(string path, UnityAction<T> action) where T : class{StartCoroutine(LoadResAsync<T>(path, action));}private IEnumerator LoadResAsync<T>(string path, UnityAction<T> action) where T : class{//声明www对象 用于下载或加载WWW www = new WWW(path);//等待下载或加载结束(异步)yield return www;//如果没有错误 证明加载成功if (www.error == null){//根据T泛型的类型 决定使用哪种类型的资源 传递给外部if (typeof(T) == typeof(AssetBundle)){action?.Invoke(www.assetBundle as T);}else if(typeof(T) == typeof(Texture)){action?.Invoke(www.texture as T);}else if(typeof(T) == typeof(AudioClip)){action?.Invoke(www.GetAudioClip() as T);}else if(typeof(T) == typeof(string)){action?.Invoke(www.text as T);}else if(typeof(T) == typeof(byte[])){action?.Invoke(www.bytes as T);}//还可以写一些 自定义类型 可能需要将bytes 转换成对应的类型来使用}//如果错误 就提示别人else{Debug.LogError("www加载资源出错:" + www.error);}}public void SendMsg<T>(BaseMsg msg, UnityAction<T> action) where T : BaseMsg{StartCoroutine(SendMsgAsync<T>(msg, action));}private IEnumerator SendMsgAsync<T>(BaseMsg msg, UnityAction<T> action) where T : BaseMsg{//消息发送WWWForm data = new WWWForm();//准备要发送的消息数据data.AddBinaryData("Msg", msg.Writing());//发送 相关WWW www = new WWW(HTTP_SERVER_PATH, data);//我们也可以直接传递 2进制字节数组 只要和后端定好规则 怎么传递都是可以的//WWW www = new WWW("HTTP_SERVER_PATH", msg.Writing());//异步等待 发送结束 才会继续执行后面的代码yield return www;//发送完成后 收到响应//认为 后端发回来的内容 也是一个继承自Basemsg类的一个字节数组对象if (www.error == null){//先解析 ID和消息体长度int index = 0;int msgID = BitConverter.ToInt32(www.bytes, index);index += 4;int msgLength = BitConverter.ToInt32(www.bytes, index);index += 4;//反序列化 BaseMsg  (就先举例 1001)BaseMsg baseMsg = null;switch (msgID){case 1001:baseMsg = new PlayerMsg();baseMsg.Reading(www.bytes, index);break;}if (baseMsg != null)action?.Invoke(baseMsg as T);}else{Debug.LogError("发消息出问题:" + www.error);}}/// <summary>/// 上传文件的方法/// </summary>/// <param name="fileName">上传上去的文件名</param>/// <param name="localPath">本地想要上传文件的路径</param>/// <param name="action">上传完成后的回调函数</param>public void UploadFile(string fileName, string localPath, UnityAction<UnityWebRequest.Result> action){StartCoroutine(UploadFileAsync(fileName, localPath, action));}private IEnumerator UploadFileAsync(string fileName, string localPath, UnityAction<UnityWebRequest.Result> action){//添加要上传文件的数据List<IMultipartFormSection> dataList = new List<IMultipartFormSection>();dataList.Add(new MultipartFormFileSection(fileName, File.ReadAllBytes(localPath)));UnityWebRequest req = UnityWebRequest.Post(HTTP_SERVER_PATH, dataList);yield return req.SendWebRequest();action?.Invoke(req.result);//如果不成功if (req.result != UnityWebRequest.Result.Success){Debug.LogWarning("上传出问题" + req.result + req.error + req.responseCode);}}/// <summary>/// 通过UnityWebRequest 去获取数据/// </summary>/// <typeparam name="T">byte[]、Texture、AssetBundle、AudioClip、object(自定义的 如果是object证明要保存到本地)</typeparam>/// <param name="path">远端或者本地数据路径 http ftp file</param>/// <param name="action">获取成功后的回调函数</param>/// <param name="localPath">如果是下载到本地 需要传第3个参数</param>/// <param name="type">如果下载 音效切片文件 需要传音效类型</param>public void UnityWebRequestLoad<T>(string path, UnityAction<T> action, string localPath = "", AudioType type = AudioType.MPEG) where T : class{StartCoroutine(UnityWebRequestLoadAsync<T>(path, action, localPath, type));}private IEnumerator UnityWebRequestLoadAsync<T>(string path, UnityAction<T> action, string localPath = "", AudioType type = AudioType.MPEG) where T : class{UnityWebRequest req = new UnityWebRequest(path, UnityWebRequest.kHttpVerbGET);if (typeof(T) == typeof(byte[]))req.downloadHandler = new DownloadHandlerBuffer();else if (typeof(T) == typeof(Texture))req.downloadHandler = new DownloadHandlerTexture();else if (typeof(T) == typeof(AssetBundle))req.downloadHandler = new DownloadHandlerAssetBundle(req.url, 0);else if (typeof(T) == typeof(object))req.downloadHandler = new DownloadHandlerFile(localPath);else if (typeof(T) == typeof(AudioClip)){req = UnityWebRequestMultimedia.GetAudioClip(path, type);}else  //如果出现没有的类型 就不用继续往下执行了{Debug.LogWarning("未知类型" + typeof(T));yield break;}yield return req.SendWebRequest();if (req.result == UnityWebRequest.Result.Success){if (typeof(T) == typeof(byte[]))action?.Invoke(req.downloadHandler.data as T);else if (typeof(T) == typeof(Texture)){//action?.Invoke((req.downloadHandler as DownloadHandlerTexture).texture as T);action?.Invoke(DownloadHandlerTexture.GetContent(req) as T);}else if (typeof(T) == typeof(AssetBundle)){action?.Invoke((req.downloadHandler as DownloadHandlerAssetBundle).assetBundle as T);action?.Invoke(DownloadHandlerAssetBundle.GetContent(req) as T);}else if (typeof(T) == typeof(object))action?.Invoke(null);else if (typeof(T) == typeof(AudioClip))action?.Invoke(DownloadHandlerAudioClip.GetContent(req) as T);}else{Debug.LogWarning("获取数据失败:" + req.error + req.result + req.responseCode);}}}

测试

消息处理

自定义协议生成工具

如何制作协议(消息)生产工具

协议(消息)配置

协议(消息)生成

生成枚举

using System.Collections;
using System.Collections.Generic;
using System.Xml;
using UnityEditor;
using UnityEngine;public class ProtocolTool 
{//配置文件所在路径private static string PROTO_INFO_PATH = Application.dataPath + "/Editor/ProtocolTool/ProtocolInfo.xml";private static GenerateCSharp generateCSharp = new GenerateCSharp();[MenuItem("ProtocolTool/生成C#脚本")]private static void GenerateCSharp(){//1.读取xml相关的信息XmlNodeList list = GetNode("enum");//2.根据这些信息 去拼接字符串 生成对应的脚本generateCSharp.GenerateEnum(list);//刷新编辑器界面 让我们可以看到生成的内容 不需要手动进行刷新AssetDatabase.Refresh();}[MenuItem("ProtocolTool/生成C++脚本")]private static void GenerateC(){Debug.Log("生成C++脚本");}[MenuItem("ProtocolTool/生成Java脚本")]private static void GenerateJava(){Debug.Log("生成Java脚本");}/// <summary>/// 获取指定名字的所有子节点 的 list/// </summary>/// <param name="nodeName"></param>/// <returns></returns>private static XmlNodeList GetNode(string nodeName){XmlDocument xml = new XmlDocument();xml.Load(PROTO_INFO_PATH);XmlNode root = xml.SelectSingleNode("messages");return root.SelectNodes(nodeName);}
}
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using UnityEngine;namespace GamePlayer
{public enum E_PLAYER_TYPE{MAIN = 1,OTHER,}
}public class GenerateCSharp 
{//协议保存路径private string SAVE_PATH = Application.dataPath + "/Scripts/Protocol/";//生成枚举public void GenerateEnum(XmlNodeList nodes){//生成枚举脚本的逻辑string namespaceStr = "";string enumNameStr = "";string fieldStr = "";foreach (XmlNode enumNode in nodes){//获取命名空间配置信息namespaceStr = enumNode.Attributes["namespace"].Value;//获取枚举名配置信息enumNameStr = enumNode.Attributes["name"].Value;//获取所有的字段节点 然后进行字符串拼接XmlNodeList enumFields = enumNode.SelectNodes("field");//清空上一次的数据fieldStr = "";foreach (XmlNode enumField in enumFields){fieldStr += "\t\t" + enumField.Attributes["name"].Value;if (enumField.InnerText != "")fieldStr += " = " + enumField.InnerText;fieldStr += ",\r\n";}//对所有可变的内容进行拼接string enumStr = $"namespace {namespaceStr}\r\n" + "{\r\n" + $"\tpublic enum {enumNameStr}\r\n" +"\t{\r\n" +$"{fieldStr}" + "\t}\r\n" + "}";//保存文件的路径string path = SAVE_PATH + namespaceStr + "/Enum/";//如果不存在该文件夹 则创建if (!Directory.Exists(path)){Directory.CreateDirectory(path);}//字符串保存 存储为枚举脚本文件//参数一:文件名//参数二:转换成string 的 数据File.WriteAllText(path + enumNameStr + ".cs", enumStr);}Debug.Log("枚举生成结束");}//生成数据结构类//生成消息类
}

生成数据结构类

生成成员变量
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using UnityEngine;namespace GamePlayer
{//public class PlayerData : BaseData//{//    int id;//    float atk;//    bool sex;//    long lev;//    int[] arrays;//    List<int> list;//    Dictionary<int, string> dic;//    public override int GetByteNum()//    {//        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 class GenerateCSharp 
{//协议保存路径private string SAVE_PATH = Application.dataPath + "/Scripts/Protocol/";//生成枚举public void GenerateEnum(XmlNodeList nodes){//生成枚举脚本的逻辑string namespaceStr = "";string enumNameStr = "";string fieldStr = "";foreach (XmlNode enumNode in nodes){//获取命名空间配置信息namespaceStr = enumNode.Attributes["namespace"].Value;//获取枚举名配置信息enumNameStr = enumNode.Attributes["name"].Value;//获取所有的字段节点 然后进行字符串拼接XmlNodeList enumFields = enumNode.SelectNodes("field");//清空上一次的数据fieldStr = "";foreach (XmlNode enumField in enumFields){fieldStr += "\t\t" + enumField.Attributes["name"].Value;if (enumField.InnerText != "")fieldStr += " = " + enumField.InnerText;fieldStr += ",\r\n";}//对所有可变的内容进行拼接string enumStr = $"namespace {namespaceStr}\r\n" + "{\r\n" + $"\tpublic enum {enumNameStr}\r\n" +"\t{\r\n" +$"{fieldStr}" + "\t}\r\n" + "}";//保存文件的路径string path = SAVE_PATH + namespaceStr + "/Enum/";//如果不存在该文件夹 则创建if (!Directory.Exists(path)){Directory.CreateDirectory(path);}//字符串保存 存储为枚举脚本文件//参数一:文件名//参数二:转换成string 的 数据File.WriteAllText(path + enumNameStr + ".cs", enumStr);}Debug.Log("枚举生成结束");}//生成数据结构类public void GenerateData(XmlNodeList nodes){string namespaceStr = "";string classNameStr = "";string fieldStr = "";foreach (XmlNode dataNode in nodes){//命名空间namespaceStr = dataNode.Attributes["namespace"].Value;//类名classNameStr = dataNode.Attributes["name"].Value;//读取所有字段节点XmlNodeList fields = dataNode.SelectNodes("field");//通过这个方法进行成员变量声明的拼接 返回拼接结果fieldStr = GetFieldStr(fields);string dataStr = "using System.Collections.Generic;\r\n" + $"namespace {namespaceStr}\r\n" +"{\r\n" +$"\tpublic class {classNameStr} : BaseData\r\n" +"\t{\r\n" +$"{fieldStr}" +"\t}\r\n" +"}";//保存为 脚本对象//保存文件的路径string path = SAVE_PATH + namespaceStr + "/Data/";//如果没有该路径 就创建if (!Directory.Exists(path))Directory.CreateDirectory(path);//字符串保存 存储为数据结构类脚本File.WriteAllText(path + classNameStr + ".cs", dataStr);}Debug.Log("数据结构类生成结束");}//生成消息类/// <summary>/// 获取成员变量声明内容/// </summary>/// <param name="fields"></param>/// <returns></returns>private string GetFieldStr(XmlNodeList fields){string fieldStr = "";foreach (XmlNode field in fields){//变量类型string type = field.Attributes["type"].Value;//变量名string fieldName = field.Attributes["name"].Value;if (type == "list"){string T = field.Attributes["T"].Value;fieldStr += "\t\tpublic " + "List<" + T + "> ";}else if (type == "array"){string data = field.Attributes["data"].Value;fieldStr += "\t\tpublic " + data + "[] ";}else if(type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;fieldStr += "\t\tpublic " + "Dictionary<" + Tkey + ", " + Tvalue + "> ";}else{fieldStr += "\t\tpublic " + type + " ";}fieldStr += fieldName + ";\r\n";}return fieldStr;}
}

生产GetBytesNum 函数
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using UnityEngine;namespace GamePlayer
{public enum E_Player_Type{Main,Other,}public class PlayerTest : BaseData{public List<int> list;public Dictionary<int, string> dic;public int[] arrays;public E_Player_Type type;public PlayerData player;public override int GetByteNum(){int num = 0;num += 4; //list.Countfor (int i = 0; i < list.Count; i++){num += 4;}num += 4; //dic.Countforeach (int key in dic.Keys){num += 4;//key所占的字节数num += 4;//value 字符串长度 占的字节数num += Encoding.UTF8.GetByteCount(dic[key]);}num += 4; //arrays.Length 数组长度for (int i = 0; i < arrays.Length; i++){num += 4;}num += 4; //枚举 用int来存num += player.GetByteNum(); //PlayerDatareturn num;}public override int Reading(byte[] bytes, int beginIndex = 0){throw new System.NotImplementedException();}public override byte[] Writing(){throw new System.NotImplementedException();}}
}public class GenerateCSharp 
{//协议保存路径private string SAVE_PATH = Application.dataPath + "/Scripts/Protocol/";//生成枚举public void GenerateEnum(XmlNodeList nodes){//生成枚举脚本的逻辑string namespaceStr = "";string enumNameStr = "";string fieldStr = "";foreach (XmlNode enumNode in nodes){//获取命名空间配置信息namespaceStr = enumNode.Attributes["namespace"].Value;//获取枚举名配置信息enumNameStr = enumNode.Attributes["name"].Value;//获取所有的字段节点 然后进行字符串拼接XmlNodeList enumFields = enumNode.SelectNodes("field");//清空上一次的数据fieldStr = "";foreach (XmlNode enumField in enumFields){fieldStr += "\t\t" + enumField.Attributes["name"].Value;if (enumField.InnerText != "")fieldStr += " = " + enumField.InnerText;fieldStr += ",\r\n";}//对所有可变的内容进行拼接string enumStr = $"namespace {namespaceStr}\r\n" + "{\r\n" + $"\tpublic enum {enumNameStr}\r\n" +"\t{\r\n" +$"{fieldStr}" + "\t}\r\n" + "}";//保存文件的路径string path = SAVE_PATH + namespaceStr + "/Enum/";//如果不存在该文件夹 则创建if (!Directory.Exists(path)){Directory.CreateDirectory(path);}//字符串保存 存储为枚举脚本文件//参数一:文件名//参数二:转换成string 的 数据File.WriteAllText(path + enumNameStr + ".cs", enumStr);}Debug.Log("枚举生成结束");}//生成数据结构类public void GenerateData(XmlNodeList nodes){string namespaceStr = "";string classNameStr = "";string fieldStr = "";string getBytesNumStr = "";foreach (XmlNode dataNode in nodes){//命名空间namespaceStr = dataNode.Attributes["namespace"].Value;//类名classNameStr = dataNode.Attributes["name"].Value;//读取所有字段节点XmlNodeList fields = dataNode.SelectNodes("field");//通过这个方法进行成员变量声明的拼接 返回拼接结果fieldStr = GetFieldStr(fields);//通过某个方法 对GetBytesNum函数中的字符串内容进行拼接 返回结果getBytesNumStr = GetGetBytesNumStr(fields);string dataStr = "using System.Collections.Generic;\r\n" +"using System.Text;\r\n\r\n" + $"namespace {namespaceStr}\r\n" +"{\r\n" +$"\tpublic class {classNameStr} : BaseData\r\n" +"\t{\r\n" +$"{fieldStr}\r\n" + "\t\tpublic override int GetByteNum()\r\n" + "\t\t{\r\n" + "\t\t\tint num = 0;\r\n" + $"{getBytesNumStr}" + "\t\t\treturn num;\r\n" + "\t\t}\r\n" +"\t}\r\n" +"}";//保存为 脚本对象//保存文件的路径string path = SAVE_PATH + namespaceStr + "/Data/";//如果没有该路径 就创建if (!Directory.Exists(path))Directory.CreateDirectory(path);//字符串保存 存储为数据结构类脚本File.WriteAllText(path + classNameStr + ".cs", dataStr);}Debug.Log("数据结构类生成结束");}//生成消息类/// <summary>/// 获取成员变量声明内容/// </summary>/// <param name="fields"></param>/// <returns></returns>private string GetFieldStr(XmlNodeList fields){string fieldStr = "";foreach (XmlNode field in fields){//变量类型string type = field.Attributes["type"].Value;//变量名string fieldName = field.Attributes["name"].Value;if (type == "list"){string T = field.Attributes["T"].Value;fieldStr += "\t\tpublic " + "List<" + T + "> ";}else if (type == "array"){string data = field.Attributes["data"].Value;fieldStr += "\t\tpublic " + data + "[] ";}else if(type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;fieldStr += "\t\tpublic " + "Dictionary<" + Tkey + ", " + Tvalue + "> ";}else if(type == "enum"){string data = field.Attributes["data"].Value;fieldStr += "\t\tpublic " + data + " ";}else{fieldStr += "\t\tpublic " + type + " ";}fieldStr += fieldName + ";\r\n";}return fieldStr;}private string GetGetBytesNumStr(XmlNodeList fields){string bytesNumStr = "";string type = "";string name = "";foreach (XmlNode field in fields){type = field.Attributes["type"].Value;name = field.Attributes["name"].Value;if (type == "list"){string T = field.Attributes["T"].Value;bytesNumStr += "\t\t\tnum += 2;\r\n"; // 2 是为了节约字节数 用一个short去存储数量信息bytesNumStr += "\t\t\tfor(int i = 0; i < " + name + ".Count; i++)\r\n";//这里使用的是 name + [i] 目的是获取 list当中的元素传入进行使用bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(T, name + "[i]") + ";\r\n";}else if (type == "array"){string data = field.Attributes["data"].Value;bytesNumStr += "\t\t\tnum += 2;\r\n";bytesNumStr += "\t\t\tfor(int i = 0; i < " + name + ".Length; i++)\r\n";bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(data, name + "[i]") + ";\r\n";}else if (type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;bytesNumStr += "\t\t\tnum += 2;\r\n";bytesNumStr += "\t\t\tforeach(" + Tkey + " key in " + name + ".Keys)\r\n";bytesNumStr += "\t\t\t{\r\n";bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(Tkey, "key") + ";\r\n";bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(Tvalue, name + "[key]") + ";\r\n";bytesNumStr += "\t\t\t}\r\n";}elsebytesNumStr += "\t\t\tnum += " + GetValueBytesNum(type, name) + ";\r\n";}return bytesNumStr;}private string GetValueBytesNum(string type, string name){//其它类型根据需求添加switch (type){case "int":case "float":case "enum":return "4";case "bool":case "byte":return "1";case "short":return "2";case "long":case "double":return "8";case "string":return "4 + Encoding.UTF8.GetByteCount(" + name + ")";default:return name + ".GetBytesNum()";}}}

生成Writing 函数
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using UnityEngine;namespace GamePlayer
{public enum E_Player_Type{Main,Other,}public class PlayerTest : BaseData{public List<int> list;public Dictionary<int, string> dic;public int[] arrays;public E_Player_Type type;public PlayerData player;public override int GetByteNum(){int num = 0;num += 4; //list.Countfor (int i = 0; i < list.Count; i++){num += 4;}num += 4; //dic.Countforeach (int key in dic.Keys){num += 4;//key所占的字节数num += 4;//value 字符串长度 占的字节数num += Encoding.UTF8.GetByteCount(dic[key]);}num += 4; //arrays.Length 数组长度for (int i = 0; i < arrays.Length; i++){num += 4;}num += 4; //枚举 用int来存num += player.GetByteNum(); //PlayerDatareturn num;}public override int Reading(byte[] bytes, int beginIndex = 0){throw new System.NotImplementedException();}public override byte[] Writing(){//固定内容int index = 0;byte[] bytes = new byte[GetByteNum()];//可变的 是根据成员变量来决定如何拼接的//存储 list的长度WriteShort(bytes, (short)list.Count, ref index);for (int i = 0; i < list.Count; i++){WriteInt(bytes, list[i], ref index);}//存储 dic//先长度WriteShort(bytes, (short)dic.Count, ref index);foreach (int key in dic.Keys){WriteInt(bytes, key, ref index);WriteString(bytes, dic[key], ref index);}//存储 数组WriteShort(bytes, (short)arrays.Length, ref index);for (int i = 0; i < arrays.Length; i++){WriteInt(bytes, arrays[i], ref index);}//存储 枚举 (我们用一个 int 来存储枚举)type 是一个枚举类型WriteInt(bytes, Convert.ToInt32(type), ref index);//存储 自定义数据结构类WriteData(bytes, player, ref index);//固定内容return bytes;}}
}public class GenerateCSharp 
{//协议保存路径private string SAVE_PATH = Application.dataPath + "/Scripts/Protocol/";//生成枚举public void GenerateEnum(XmlNodeList nodes){//生成枚举脚本的逻辑string namespaceStr = "";string enumNameStr = "";string fieldStr = "";foreach (XmlNode enumNode in nodes){//获取命名空间配置信息namespaceStr = enumNode.Attributes["namespace"].Value;//获取枚举名配置信息enumNameStr = enumNode.Attributes["name"].Value;//获取所有的字段节点 然后进行字符串拼接XmlNodeList enumFields = enumNode.SelectNodes("field");//清空上一次的数据fieldStr = "";foreach (XmlNode enumField in enumFields){fieldStr += "\t\t" + enumField.Attributes["name"].Value;if (enumField.InnerText != "")fieldStr += " = " + enumField.InnerText;fieldStr += ",\r\n";}//对所有可变的内容进行拼接string enumStr = $"namespace {namespaceStr}\r\n" + "{\r\n" + $"\tpublic enum {enumNameStr}\r\n" +"\t{\r\n" +$"{fieldStr}" + "\t}\r\n" + "}";//保存文件的路径string path = SAVE_PATH + namespaceStr + "/Enum/";//如果不存在该文件夹 则创建if (!Directory.Exists(path)){Directory.CreateDirectory(path);}//字符串保存 存储为枚举脚本文件//参数一:文件名//参数二:转换成string 的 数据File.WriteAllText(path + enumNameStr + ".cs", enumStr);}Debug.Log("枚举生成结束");}//生成数据结构类public void GenerateData(XmlNodeList nodes){string namespaceStr = "";string classNameStr = "";string fieldStr = "";//将 GetBytesNum方法 写成字符串string getBytesNumStr = "";// 将 Writing方法 写成字符串string writingStr = "";foreach (XmlNode dataNode in nodes){//命名空间namespaceStr = dataNode.Attributes["namespace"].Value;//类名classNameStr = dataNode.Attributes["name"].Value;//读取所有字段节点XmlNodeList fields = dataNode.SelectNodes("field");//通过这个方法进行成员变量声明的拼接 返回拼接结果fieldStr = GetFieldStr(fields);//通过某个方法 对GetBytesNum函数中的字符串内容进行拼接 返回结果getBytesNumStr = GetGetBytesNumStr(fields);//通过某个方法 对Writing函数中的字符串内容进行拼接 返回结果writingStr = GetWritingStr(fields);string dataStr = "using System.Collections.Generic;\r\n" +"using System.Text;\r\n" +"using System;\r\n\r\b" + $"namespace {namespaceStr}\r\n" +"{\r\n" +$"\tpublic class {classNameStr} : BaseData\r\n" +"\t{\r\n" +$"{fieldStr}\r\n" + "\t\tpublic override int GetByteNum()\r\n" + "\t\t{\r\n" + "\t\t\tint num = 0;\r\n" + $"{getBytesNumStr}" + "\t\t\treturn num;\r\n" + "\t\t}\r\n\r\n" + "\t\tpublic override byte[] Writing()\r\n" + "\t\t{\r\n" + "\t\t\tint index = 0;\r\n" + "\t\t\tbyte[] bytes = new byte[GetByteNum()];\r\n" + $"{writingStr}" + "\t\t\treturn bytes;\r\n" + "\t\t}\r\n\r\n" + "\t}\r\n" +"}";//保存为 脚本对象//保存文件的路径string path = SAVE_PATH + namespaceStr + "/Data/";//如果没有该路径 就创建if (!Directory.Exists(path))Directory.CreateDirectory(path);//字符串保存 存储为数据结构类脚本File.WriteAllText(path + classNameStr + ".cs", dataStr);}Debug.Log("数据结构类生成结束");}//生成消息类/// <summary>/// 获取成员变量声明内容/// </summary>/// <param name="fields"></param>/// <returns></returns>private string GetFieldStr(XmlNodeList fields){string fieldStr = "";foreach (XmlNode field in fields){//变量类型string type = field.Attributes["type"].Value;//变量名string fieldName = field.Attributes["name"].Value;if (type == "list"){string T = field.Attributes["T"].Value;fieldStr += "\t\tpublic " + "List<" + T + "> ";}else if (type == "array"){string data = field.Attributes["data"].Value;fieldStr += "\t\tpublic " + data + "[] ";}else if(type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;fieldStr += "\t\tpublic " + "Dictionary<" + Tkey + ", " + Tvalue + "> ";}else if(type == "enum"){string data = field.Attributes["data"].Value;fieldStr += "\t\tpublic " + data + " ";}else{fieldStr += "\t\tpublic " + type + " ";}fieldStr += fieldName + ";\r\n";}return fieldStr;}/// <summary>/// 拼接 GetBytesNum函数的方法/// </summary>/// <param name="fields"></param>/// <returns></returns>private string GetGetBytesNumStr(XmlNodeList fields){string bytesNumStr = "";string type = "";string name = "";foreach (XmlNode field in fields){type = field.Attributes["type"].Value;name = field.Attributes["name"].Value;if (type == "list"){string T = field.Attributes["T"].Value;bytesNumStr += "\t\t\tnum += 2;\r\n"; // 2 是为了节约字节数 用一个short去存储数量信息bytesNumStr += "\t\t\tfor(int i = 0; i < " + name + ".Count; i++)\r\n";//这里使用的是 name + [i] 目的是获取 list当中的元素传入进行使用bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(T, name + "[i]") + ";\r\n";}else if (type == "array"){string data = field.Attributes["data"].Value;bytesNumStr += "\t\t\tnum += 2;\r\n";bytesNumStr += "\t\t\tfor(int i = 0; i < " + name + ".Length; i++)\r\n";bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(data, name + "[i]") + ";\r\n";}else if (type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;bytesNumStr += "\t\t\tnum += 2;\r\n";bytesNumStr += "\t\t\tforeach(" + Tkey + " key in " + name + ".Keys)\r\n";bytesNumStr += "\t\t\t{\r\n";bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(Tkey, "key") + ";\r\n";bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(Tvalue, name + "[key]") + ";\r\n";bytesNumStr += "\t\t\t}\r\n";}elsebytesNumStr += "\t\t\tnum += " + GetValueBytesNum(type, name) + ";\r\n";}return bytesNumStr;}//获取指定类型的字节数private string GetValueBytesNum(string type, string name){//其它类型根据需求添加switch (type){case "int":case "float":case "enum":return "4";case "bool":case "byte":return "1";case "short":return "2";case "long":case "double":return "8";case "string":return "4 + Encoding.UTF8.GetByteCount(" + name + ")";default:return name + ".GetBytesNum()";}}/// <summary>/// 拼接 Writing函数的方法/// </summary>/// <param name="field"></param>/// <returns></returns>private string GetWritingStr(XmlNodeList fields){string writingStr = "";string type = "";string name = "";foreach (XmlNode field in fields){type = field.Attributes["type"].Value;name = field.Attributes["name"].Value;if (type == "list"){string T = field.Attributes["T"].Value;writingStr += "\t\t\tWriteShort(bytes, (short)" + name + ".Count, ref index);\r\n";writingStr += "\t\t\tfor(int i = 0; i < " + name + ".Count; i++)\r\n";writingStr += "\t\t\t\t" + GetFieldWritingStr(T, name + "[i]") + "\r\n";}else if(type == "array"){string data = field.Attributes["data"].Value;writingStr += "\t\t\tWriteShort(bytes, (short)" + name + ".Length, ref index);\r\n";writingStr += "\t\t\tfor(int i = 0; i < " + name + ".Length; i++)\r\n";writingStr += "\t\t\t\t" + GetFieldWritingStr(data, name + "[i]") + "\r\n";}else if (type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;writingStr += "\t\t\tWriteShort(bytes, (short)" + name + ".Count, ref index);\r\n";writingStr += "\t\t\tforeach(" + Tkey + " key in " + name + ".Keys)\r\n";writingStr += "\t\t\t{\r\n";writingStr += "\t\t\t\t" + GetFieldWritingStr(Tkey, "key") + "\r\n";writingStr += "\t\t\t\t" + GetFieldWritingStr(Tvalue, name + "[key]") + "\r\n";writingStr += "\t\t\t}\r\n";}else{writingStr += "\t\t\t" + GetFieldWritingStr(type, name) + "\r\n";}}return writingStr;}private string GetFieldWritingStr(string type, string name){switch (type){case "byte":return "WriteByte(bytes, " + name + ", ref index);";case "int":return "WriteInt(bytes, " + name + ", ref index);";case "short":return "WriteShort(bytes, " + name + ", ref index);";case "long":return "WriteLong(bytes, " + name + ", ref index);";case "float":return "WriteFloat(bytes, " + name + ", ref index);";case "bool":return "WriteBool(bytes, " + name + ", ref index);";case "string":return "WriteString(bytes, " + name + ", ref index);";case "enum":return "WriteInt(bytes, Convert.ToInt32(" + name + "), ref index);"; default:return "WriteData(bytes, " + name + ", ref index);";}}}

生成Reading函数
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using UnityEngine;namespace GamePlayer
{public enum E_Player_Type{Main,Other,}public class PlayerTest : BaseData{public List<int> list;public Dictionary<int, string> dic;public int[] arrays;public E_Player_Type type;public PlayerData player;public override int GetByteNum(){int num = 0;num += 4; //list.Countfor (int i = 0; i < list.Count; i++){num += 4;}num += 4; //dic.Countforeach (int key in dic.Keys){num += 4;//key所占的字节数num += 4;//value 字符串长度 占的字节数num += Encoding.UTF8.GetByteCount(dic[key]);}num += 4; //arrays.Length 数组长度for (int i = 0; i < arrays.Length; i++){num += 4;}num += 4; //枚举 用int来存num += player.GetByteNum(); //PlayerDatareturn num;}public override int Reading(byte[] bytes, int beginIndex = 0){int index = beginIndex;//反序列化 listlist = new List<int>();short listCount = ReadShort(bytes, ref index);            for (int i = 0; i < listCount; i++){list.Add(ReadInt(bytes, ref index));}//dicdic = new Dictionary<int, string>();short dicCount = ReadShort(bytes, ref index);            for (int i = 0; i < dicCount; i++){dic.Add(ReadInt(bytes, ref index), ReadString(bytes, ref index));}//arraysint arrayLength = ReadShort(bytes, ref index);arrays = new int[arrayLength];for (int i = 0; i < arrayLength; i++){arrays[i] = ReadInt(bytes, ref index);}//枚举type = (E_Player_Type)ReadInt(bytes, ref index);//自定义类型player = ReadData<PlayerData>(bytes, ref index);return index - beginIndex;}public override byte[] Writing(){//固定内容int index = 0;byte[] bytes = new byte[GetByteNum()];//可变的 是根据成员变量来决定如何拼接的//存储 list的长度WriteShort(bytes, (short)list.Count, ref index);for (int i = 0; i < list.Count; i++){WriteInt(bytes, list[i], ref index);}//存储 dic//先长度WriteShort(bytes, (short)dic.Count, ref index);foreach (int key in dic.Keys){WriteInt(bytes, key, ref index);WriteString(bytes, dic[key], ref index);}//存储 数组WriteShort(bytes, (short)arrays.Length, ref index);for (int i = 0; i < arrays.Length; i++){WriteInt(bytes, arrays[i], ref index);}//存储 枚举 (我们用一个 int 来存储枚举)type 是一个枚举类型WriteInt(bytes, Convert.ToInt32(type), ref index);//存储 自定义数据结构类WriteData(bytes, player, ref index);//固定内容return bytes;}}
}public class GenerateCSharp 
{//协议保存路径private string SAVE_PATH = Application.dataPath + "/Scripts/Protocol/";//生成枚举public void GenerateEnum(XmlNodeList nodes){//生成枚举脚本的逻辑string namespaceStr = "";string enumNameStr = "";string fieldStr = "";foreach (XmlNode enumNode in nodes){//获取命名空间配置信息namespaceStr = enumNode.Attributes["namespace"].Value;//获取枚举名配置信息enumNameStr = enumNode.Attributes["name"].Value;//获取所有的字段节点 然后进行字符串拼接XmlNodeList enumFields = enumNode.SelectNodes("field");//清空上一次的数据fieldStr = "";foreach (XmlNode enumField in enumFields){fieldStr += "\t\t" + enumField.Attributes["name"].Value;if (enumField.InnerText != "")fieldStr += " = " + enumField.InnerText;fieldStr += ",\r\n";}//对所有可变的内容进行拼接string enumStr = $"namespace {namespaceStr}\r\n" + "{\r\n" + $"\tpublic enum {enumNameStr}\r\n" +"\t{\r\n" +$"{fieldStr}" + "\t}\r\n" + "}";//保存文件的路径string path = SAVE_PATH + namespaceStr + "/Enum/";//如果不存在该文件夹 则创建if (!Directory.Exists(path)){Directory.CreateDirectory(path);}//字符串保存 存储为枚举脚本文件//参数一:文件名//参数二:转换成string 的 数据File.WriteAllText(path + enumNameStr + ".cs", enumStr);}Debug.Log("枚举生成结束");}//生成数据结构类public void GenerateData(XmlNodeList nodes){string namespaceStr = "";string classNameStr = "";string fieldStr = "";//将 GetBytesNum方法 写成字符串string getBytesNumStr = "";// 将 Writing方法 写成字符串string writingStr = "";//将 Reading方法 写成字符串string readingStr = "";foreach (XmlNode dataNode in nodes){//命名空间namespaceStr = dataNode.Attributes["namespace"].Value;//类名classNameStr = dataNode.Attributes["name"].Value;//读取所有字段节点XmlNodeList fields = dataNode.SelectNodes("field");//通过这个方法进行成员变量声明的拼接 返回拼接结果fieldStr = GetFieldStr(fields);//通过某个方法 对GetBytesNum函数中的字符串内容进行拼接 返回结果getBytesNumStr = GetGetBytesNumStr(fields);//通过某个方法 对Writing函数中的字符串内容进行拼接 返回结果writingStr = GetWritingStr(fields);//通过某个方法 对Reading函数中的字符串内容进行拼接 返回结果readingStr = GetReadingStr(fields);string dataStr = "using System.Collections.Generic;\r\n" +"using System.Text;\r\n" +"using System;\r\n\r\n" + $"namespace {namespaceStr}\r\n" +"{\r\n" +$"\tpublic class {classNameStr} : BaseData\r\n" +"\t{\r\n" +$"{fieldStr}\r\n" + "\t\tpublic override int GetByteNum()\r\n" + "\t\t{\r\n" + "\t\t\tint num = 0;\r\n" + $"{getBytesNumStr}" + "\t\t\treturn num;\r\n" + "\t\t}\r\n\r\n" + "\t\tpublic override byte[] Writing()\r\n" + "\t\t{\r\n" + "\t\t\tint index = 0;\r\n" + "\t\t\tbyte[] bytes = new byte[GetByteNum()];\r\n" + $"{writingStr}" + "\t\t\treturn bytes;\r\n" + "\t\t}\r\n\r\n" +"\t\tpublic override int Reading(byte[] bytes, int beginIndex = 0)\r\n" +"\t\t{\r\n" +"\t\t\tint index = beginIndex;\r\n" +                                        $"{readingStr}" +"\t\t\treturn index - beginIndex;\r\n" +"\t\t}\r\n\r\n" +"\t}\r\n" +"}";//保存为 脚本对象//保存文件的路径string path = SAVE_PATH + namespaceStr + "/Data/";//如果没有该路径 就创建if (!Directory.Exists(path))Directory.CreateDirectory(path);//字符串保存 存储为数据结构类脚本File.WriteAllText(path + classNameStr + ".cs", dataStr);}Debug.Log("数据结构类生成结束");}//生成消息类/// <summary>/// 获取成员变量声明内容/// </summary>/// <param name="fields"></param>/// <returns></returns>private string GetFieldStr(XmlNodeList fields){string fieldStr = "";foreach (XmlNode field in fields){//变量类型string type = field.Attributes["type"].Value;//变量名string fieldName = field.Attributes["name"].Value;if (type == "list"){string T = field.Attributes["T"].Value;fieldStr += "\t\tpublic " + "List<" + T + "> ";}else if (type == "array"){string data = field.Attributes["data"].Value;fieldStr += "\t\tpublic " + data + "[] ";}else if(type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;fieldStr += "\t\tpublic " + "Dictionary<" + Tkey + ", " + Tvalue + "> ";}else if(type == "enum"){string data = field.Attributes["data"].Value;fieldStr += "\t\tpublic " + data + " ";}else{fieldStr += "\t\tpublic " + type + " ";}fieldStr += fieldName + ";\r\n";}return fieldStr;}/// <summary>/// 拼接 GetBytesNum函数的方法/// </summary>/// <param name="fields"></param>/// <returns></returns>private string GetGetBytesNumStr(XmlNodeList fields){string bytesNumStr = "";string type = "";string name = "";foreach (XmlNode field in fields){type = field.Attributes["type"].Value;name = field.Attributes["name"].Value;if (type == "list"){string T = field.Attributes["T"].Value;bytesNumStr += "\t\t\tnum += 2;\r\n"; // 2 是为了节约字节数 用一个short去存储数量信息bytesNumStr += "\t\t\tfor(int i = 0; i < " + name + ".Count; i++)\r\n";//这里使用的是 name + [i] 目的是获取 list当中的元素传入进行使用bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(T, name + "[i]") + ";\r\n";}else if (type == "array"){string data = field.Attributes["data"].Value;bytesNumStr += "\t\t\tnum += 2;\r\n";bytesNumStr += "\t\t\tfor(int i = 0; i < " + name + ".Length; i++)\r\n";bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(data, name + "[i]") + ";\r\n";}else if (type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;bytesNumStr += "\t\t\tnum += 2;\r\n";bytesNumStr += "\t\t\tforeach(" + Tkey + " key in " + name + ".Keys)\r\n";bytesNumStr += "\t\t\t{\r\n";bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(Tkey, "key") + ";\r\n";bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(Tvalue, name + "[key]") + ";\r\n";bytesNumStr += "\t\t\t}\r\n";}elsebytesNumStr += "\t\t\tnum += " + GetValueBytesNum(type, name) + ";\r\n";}return bytesNumStr;}//获取指定类型的字节数private string GetValueBytesNum(string type, string name){//其它类型根据需求添加switch (type){case "int":case "float":case "enum":return "4";case "bool":case "byte":return "1";case "short":return "2";case "long":case "double":return "8";case "string":return "4 + Encoding.UTF8.GetByteCount(" + name + ")";default:return name + ".GetBytesNum()";}}/// <summary>/// 拼接 Writing函数的方法/// </summary>/// <param name="field"></param>/// <returns></returns>private string GetWritingStr(XmlNodeList fields){string writingStr = "";string type = "";string name = "";foreach (XmlNode field in fields){type = field.Attributes["type"].Value;name = field.Attributes["name"].Value;if (type == "list"){string T = field.Attributes["T"].Value;writingStr += "\t\t\tWriteShort(bytes, (short)" + name + ".Count, ref index);\r\n";writingStr += "\t\t\tfor(int i = 0; i < " + name + ".Count; i++)\r\n";writingStr += "\t\t\t\t" + GetFieldWritingStr(T, name + "[i]") + "\r\n";}else if(type == "array"){string data = field.Attributes["data"].Value;writingStr += "\t\t\tWriteShort(bytes, (short)" + name + ".Length, ref index);\r\n";writingStr += "\t\t\tfor(int i = 0; i < " + name + ".Length; i++)\r\n";writingStr += "\t\t\t\t" + GetFieldWritingStr(data, name + "[i]") + "\r\n";}else if (type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;writingStr += "\t\t\tWriteShort(bytes, (short)" + name + ".Count, ref index);\r\n";writingStr += "\t\t\tforeach(" + Tkey + " key in " + name + ".Keys)\r\n";writingStr += "\t\t\t{\r\n";writingStr += "\t\t\t\t" + GetFieldWritingStr(Tkey, "key") + "\r\n";writingStr += "\t\t\t\t" + GetFieldWritingStr(Tvalue, name + "[key]") + "\r\n";writingStr += "\t\t\t}\r\n";}else{writingStr += "\t\t\t" + GetFieldWritingStr(type, name) + "\r\n";}}return writingStr;}private string GetFieldWritingStr(string type, string name){switch (type){case "byte":return "WriteByte(bytes, " + name + ", ref index);";case "int":return "WriteInt(bytes, " + name + ", ref index);";case "short":return "WriteShort(bytes, " + name + ", ref index);";case "long":return "WriteLong(bytes, " + name + ", ref index);";case "float":return "WriteFloat(bytes, " + name + ", ref index);";case "bool":return "WriteBool(bytes, " + name + ", ref index);";case "string":return "WriteString(bytes, " + name + ", ref index);";case "enum":return "WriteInt(bytes, Convert.ToInt32(" + name + "), ref index);"; default:return "WriteData(bytes, " + name + ", ref index);";}}//private string GetReadingStr(XmlNodeList fields){string readingStr = "";string type = "";string name = "";foreach (XmlNode field in fields){type = field.Attributes["type"].Value;name = field.Attributes["name"].Value;if (type == "list"){string T = field.Attributes["T"].Value;readingStr += "\t\t\t" + name + " = new List<" + T + ">();\r\n";readingStr += "\t\t\tshort " + name + "Count = ReadShort(bytes, ref index);\r\n";readingStr += "\t\t\tfor(int i = 0; i < " + name + "Count; i++)\r\n";readingStr += "\t\t\t\t" + name + ".Add(" + GetFieldReadingStr(T) + ");\r\n";}else if (type == "array"){string data = field.Attributes["data"].Value;                readingStr += "\t\t\tshort " + name + "Length = ReadShort(bytes, ref index);\r\n";readingStr += "\t\t\t" + name + " = new " + data + "[" + name + "Length];\r\n";readingStr += "\t\t\tfor(int i = 0; i < " + name + "Length; i++)\r\n";readingStr += "\t\t\t\t" + name + "[i] = " + GetFieldReadingStr(data) + ";\r\n";}else if (type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;readingStr += "\t\t\t" + name + " = new Dictionary<" + Tkey + ", " + Tvalue + ">();\r\n";readingStr += "\t\t\tshort " + name + "Count = ReadShort(bytes, ref index);\r\n";readingStr += "\t\t\tfor(int i = 0; i < " + name + "Count; i++)\r\n";readingStr += "\t\t\t\t" + name + ".Add(" + GetFieldReadingStr(Tkey)+ ", " + GetFieldReadingStr(Tvalue) + ");\r\n";}else if (type == "enum"){string data = field.Attributes["data"].Value;readingStr += "\t\t\t" + name + " = (" + data + ")ReadInt(bytes, ref index);\r\n"; }elsereadingStr += "\t\t\t" + name + " = " + GetFieldReadingStr(type) + ";\r\n";}return readingStr;}private string GetFieldReadingStr(string type){switch (type){case "byte":return "ReadByte(bytes, ref index)";case "int":return "ReadInt(bytes, ref index)";case "short":return "ReadShort(bytes, ref index)";case "long":return "ReadLong(bytes, ref index)";case "bool":return "ReadBool(bytes, ref index)";case "float":return "ReadFloat(bytes, ref index)";case "string":return "ReadString(bytes, ref index)";default:return "ReadData<" + type + ">(bytes, ref index)";}}}

测试

练习题:生成消息类

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using UnityEngine;namespace GamePlayer
{public enum E_Player_Type{Main,Other,}public class PlayerTest : BaseData{public List<int> list;public Dictionary<int, string> dic;public int[] arrays;public E_Player_Type type;public PlayerData player;public override int GetByteNum(){int num = 0;num += 4; //list.Countfor (int i = 0; i < list.Count; i++){num += 4;}num += 4; //dic.Countforeach (int key in dic.Keys){num += 4;//key所占的字节数num += 4;//value 字符串长度 占的字节数num += Encoding.UTF8.GetByteCount(dic[key]);}num += 4; //arrays.Length 数组长度for (int i = 0; i < arrays.Length; i++){num += 4;}num += 4; //枚举 用int来存num += player.GetByteNum(); //PlayerDatareturn num;}public override int Reading(byte[] bytes, int beginIndex = 0){int index = beginIndex;//反序列化 listlist = new List<int>();short listCount = ReadShort(bytes, ref index);            for (int i = 0; i < listCount; i++){list.Add(ReadInt(bytes, ref index));}//dicdic = new Dictionary<int, string>();short dicCount = ReadShort(bytes, ref index);            for (int i = 0; i < dicCount; i++){dic.Add(ReadInt(bytes, ref index), ReadString(bytes, ref index));}//arraysint arrayLength = ReadShort(bytes, ref index);arrays = new int[arrayLength];for (int i = 0; i < arrayLength; i++){arrays[i] = ReadInt(bytes, ref index);}//枚举type = (E_Player_Type)ReadInt(bytes, ref index);//自定义类型player = ReadData<PlayerData>(bytes, ref index);return index - beginIndex;}public override byte[] Writing(){//固定内容int index = 0;byte[] bytes = new byte[GetByteNum()];//可变的 是根据成员变量来决定如何拼接的//存储 list的长度WriteShort(bytes, (short)list.Count, ref index);for (int i = 0; i < list.Count; i++){WriteInt(bytes, list[i], ref index);}//存储 dic//先长度WriteShort(bytes, (short)dic.Count, ref index);foreach (int key in dic.Keys){WriteInt(bytes, key, ref index);WriteString(bytes, dic[key], ref index);}//存储 数组WriteShort(bytes, (short)arrays.Length, ref index);for (int i = 0; i < arrays.Length; i++){WriteInt(bytes, arrays[i], ref index);}//存储 枚举 (我们用一个 int 来存储枚举)type 是一个枚举类型WriteInt(bytes, Convert.ToInt32(type), ref index);//存储 自定义数据结构类WriteData(bytes, player, ref index);//固定内容return bytes;}}
}public class GenerateCSharp 
{//协议保存路径private string SAVE_PATH = Application.dataPath + "/Scripts/Protocol/";//生成枚举public void GenerateEnum(XmlNodeList nodes){//生成枚举脚本的逻辑string namespaceStr = "";string enumNameStr = "";string fieldStr = "";foreach (XmlNode enumNode in nodes){//获取命名空间配置信息namespaceStr = enumNode.Attributes["namespace"].Value;//获取枚举名配置信息enumNameStr = enumNode.Attributes["name"].Value;//获取所有的字段节点 然后进行字符串拼接XmlNodeList enumFields = enumNode.SelectNodes("field");//清空上一次的数据fieldStr = "";foreach (XmlNode enumField in enumFields){fieldStr += "\t\t" + enumField.Attributes["name"].Value;if (enumField.InnerText != "")fieldStr += " = " + enumField.InnerText;fieldStr += ",\r\n";}//对所有可变的内容进行拼接string enumStr = $"namespace {namespaceStr}\r\n" + "{\r\n" + $"\tpublic enum {enumNameStr}\r\n" +"\t{\r\n" +$"{fieldStr}" + "\t}\r\n" + "}";//保存文件的路径string path = SAVE_PATH + namespaceStr + "/Enum/";//如果不存在该文件夹 则创建if (!Directory.Exists(path)){Directory.CreateDirectory(path);}//字符串保存 存储为枚举脚本文件//参数一:文件名//参数二:转换成string 的 数据File.WriteAllText(path + enumNameStr + ".cs", enumStr);}Debug.Log("枚举生成结束");}//生成数据结构类public void GenerateData(XmlNodeList nodes){string namespaceStr = "";string classNameStr = "";string fieldStr = "";//将 GetBytesNum方法 写成字符串string getByteNumStr = "";// 将 Writing方法 写成字符串string writingStr = "";//将 Reading方法 写成字符串string readingStr = "";foreach (XmlNode dataNode in nodes){//命名空间namespaceStr = dataNode.Attributes["namespace"].Value;//类名classNameStr = dataNode.Attributes["name"].Value;//读取所有字段节点XmlNodeList fields = dataNode.SelectNodes("field");//通过这个方法进行成员变量声明的拼接 返回拼接结果fieldStr = GetFieldStr(fields);//通过某个方法 对GetBytesNum函数中的字符串内容进行拼接 返回结果getByteNumStr = GetGetBytesNumStr(fields);//通过某个方法 对Writing函数中的字符串内容进行拼接 返回结果writingStr = GetWritingStr(fields);//通过某个方法 对Reading函数中的字符串内容进行拼接 返回结果readingStr = GetReadingStr(fields);string dataStr = "using System.Collections.Generic;\r\n" +"using System.Text;\r\n" +"using System;\r\n\r\n" + $"namespace {namespaceStr}\r\n" +"{\r\n" +$"\tpublic class {classNameStr} : BaseData\r\n" +"\t{\r\n" +$"{fieldStr}\r\n" + "\t\tpublic override int GetByteNum()\r\n" + "\t\t{\r\n" + "\t\t\tint num = 0;\r\n" + $"{getByteNumStr}" + "\t\t\treturn num;\r\n" + "\t\t}\r\n\r\n" + "\t\tpublic override byte[] Writing()\r\n" + "\t\t{\r\n" + "\t\t\tint index = 0;\r\n" + "\t\t\tbyte[] bytes = new byte[GetByteNum()];\r\n" + $"{writingStr}" + "\t\t\treturn bytes;\r\n" + "\t\t}\r\n\r\n" +"\t\tpublic override int Reading(byte[] bytes, int beginIndex = 0)\r\n" +"\t\t{\r\n" +"\t\t\tint index = beginIndex;\r\n" +                                        $"{readingStr}" +"\t\t\treturn index - beginIndex;\r\n" +"\t\t}\r\n\r\n" +"\t}\r\n" +"}";//保存为 脚本对象//保存文件的路径string path = SAVE_PATH + namespaceStr + "/Data/";//如果没有该路径 就创建if (!Directory.Exists(path))Directory.CreateDirectory(path);//字符串保存 存储为数据结构类脚本File.WriteAllText(path + classNameStr + ".cs", dataStr);}Debug.Log("数据结构类生成结束");}//生成消息类public void CenerateMsg(XmlNodeList nodes){string namespaceStr = "";string classNameStr = "";string idStr = "";string fieldStr = "";//将 GetBytesNum方法 写成字符串string getByteNumStr = "";// 将 Writing方法 写成字符串string writingStr = "";//将 Reading方法 写成字符串string readingStr = "";foreach (XmlNode dataNode in nodes){//命名空间namespaceStr = dataNode.Attributes["namespace"].Value;//类名classNameStr = dataNode.Attributes["name"].Value;//消息ididStr = dataNode.Attributes["id"].Value;//读取所有字段节点XmlNodeList fields = dataNode.SelectNodes("field");//通过这个方法进行成员变量声明的拼接 返回拼接结果fieldStr = GetFieldStr(fields);//通过某个方法 对GetBytesNum函数中的字符串内容进行拼接 返回结果getByteNumStr = GetGetBytesNumStr(fields);//通过某个方法 对Writing函数中的字符串内容进行拼接 返回结果writingStr = GetWritingStr(fields);//通过某个方法 对Reading函数中的字符串内容进行拼接 返回结果readingStr = GetReadingStr(fields);string dataStr = "using System.Collections.Generic;\r\n" +"using System.Text;\r\n" +"using System;\r\n\r\n" +$"namespace {namespaceStr}\r\n" +"{\r\n" +$"\tpublic class {classNameStr} : BaseMsg\r\n" +"\t{\r\n" +$"{fieldStr}\r\n" +"\t\tpublic override int GetByteNum()\r\n" +"\t\t{\r\n" +"\t\t\tint num = 8;\r\n" + //这个8代表的是 消息ID的4个字节 + 消息体长度的4个字节$"{getByteNumStr}" +"\t\t\treturn num;\r\n" +"\t\t}\r\n\r\n" +"\t\tpublic override byte[] Writing()\r\n" +"\t\t{\r\n" +"\t\t\tint index = 0;\r\n" +"\t\t\tbyte[] bytes = new byte[GetByteNum()];\r\n" +"\t\t\tWriteInt(bytes, GetID(), ref index);\r\n" + "\t\t\tWriteInt(bytes, bytes.Length - 8, ref index);\r\n" + $"{writingStr}" +"\t\t\treturn bytes;\r\n" +"\t\t}\r\n\r\n" +"\t\tpublic override int Reading(byte[] bytes, int beginIndex = 0)\r\n" +"\t\t{\r\n" +"\t\t\tint index = beginIndex;\r\n" +$"{readingStr}" +"\t\t\treturn index - beginIndex;\r\n" +"\t\t}\r\n\r\n" +"\t\tpublic override int GetID()\r\n" +"\t\t{\r\n" +"\t\t\treturn " + idStr + ";\r\n" +"\t\t}\r\n\r\n" +"\t}\r\n" +"}";//保存为 脚本对象//保存文件的路径string path = SAVE_PATH + namespaceStr + "/Msg/";//如果没有该路径 就创建if (!Directory.Exists(path))Directory.CreateDirectory(path);//字符串保存 存储为数据结构类脚本File.WriteAllText(path + classNameStr + ".cs", dataStr);}Debug.Log("消息类生成结束");}/// <summary>/// 获取成员变量声明内容/// </summary>/// <param name="fields"></param>/// <returns></returns>private string GetFieldStr(XmlNodeList fields){string fieldStr = "";foreach (XmlNode field in fields){//变量类型string type = field.Attributes["type"].Value;//变量名string fieldName = field.Attributes["name"].Value;if (type == "list"){string T = field.Attributes["T"].Value;fieldStr += "\t\tpublic " + "List<" + T + "> ";}else if (type == "array"){string data = field.Attributes["data"].Value;fieldStr += "\t\tpublic " + data + "[] ";}else if(type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;fieldStr += "\t\tpublic " + "Dictionary<" + Tkey + ", " + Tvalue + "> ";}else if(type == "enum"){string data = field.Attributes["data"].Value;fieldStr += "\t\tpublic " + data + " ";}else{fieldStr += "\t\tpublic " + type + " ";}fieldStr += fieldName + ";\r\n";}return fieldStr;}/// <summary>/// 拼接 GetBytesNum函数的方法/// </summary>/// <param name="fields"></param>/// <returns></returns>private string GetGetBytesNumStr(XmlNodeList fields){string bytesNumStr = "";string type = "";string name = "";foreach (XmlNode field in fields){type = field.Attributes["type"].Value;name = field.Attributes["name"].Value;if (type == "list"){string T = field.Attributes["T"].Value;bytesNumStr += "\t\t\tnum += 2;\r\n"; // 2 是为了节约字节数 用一个short去存储数量信息bytesNumStr += "\t\t\tfor(int i = 0; i < " + name + ".Count; i++)\r\n";//这里使用的是 name + [i] 目的是获取 list当中的元素传入进行使用bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(T, name + "[i]") + ";\r\n";}else if (type == "array"){string data = field.Attributes["data"].Value;bytesNumStr += "\t\t\tnum += 2;\r\n";bytesNumStr += "\t\t\tfor(int i = 0; i < " + name + ".Length; i++)\r\n";bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(data, name + "[i]") + ";\r\n";}else if (type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;bytesNumStr += "\t\t\tnum += 2;\r\n";bytesNumStr += "\t\t\tforeach(" + Tkey + " key in " + name + ".Keys)\r\n";bytesNumStr += "\t\t\t{\r\n";bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(Tkey, "key") + ";\r\n";bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(Tvalue, name + "[key]") + ";\r\n";bytesNumStr += "\t\t\t}\r\n";}elsebytesNumStr += "\t\t\tnum += " + GetValueBytesNum(type, name) + ";\r\n";}return bytesNumStr;}//获取指定类型的字节数private string GetValueBytesNum(string type, string name){//其它类型根据需求添加switch (type){case "int":case "float":case "enum":return "4";case "bool":case "byte":return "1";case "short":return "2";case "long":case "double":return "8";case "string":return "4 + Encoding.UTF8.GetByteCount(" + name + ")";default:return name + ".GetByteNum()";}}/// <summary>/// 拼接 Writing函数的方法/// </summary>/// <param name="field"></param>/// <returns></returns>private string GetWritingStr(XmlNodeList fields){string writingStr = "";string type = "";string name = "";foreach (XmlNode field in fields){type = field.Attributes["type"].Value;name = field.Attributes["name"].Value;if (type == "list"){string T = field.Attributes["T"].Value;writingStr += "\t\t\tWriteShort(bytes, (short)" + name + ".Count, ref index);\r\n";writingStr += "\t\t\tfor(int i = 0; i < " + name + ".Count; i++)\r\n";writingStr += "\t\t\t\t" + GetFieldWritingStr(T, name + "[i]") + "\r\n";}else if(type == "array"){string data = field.Attributes["data"].Value;writingStr += "\t\t\tWriteShort(bytes, (short)" + name + ".Length, ref index);\r\n";writingStr += "\t\t\tfor(int i = 0; i < " + name + ".Length; i++)\r\n";writingStr += "\t\t\t\t" + GetFieldWritingStr(data, name + "[i]") + "\r\n";}else if (type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;writingStr += "\t\t\tWriteShort(bytes, (short)" + name + ".Count, ref index);\r\n";writingStr += "\t\t\tforeach(" + Tkey + " key in " + name + ".Keys)\r\n";writingStr += "\t\t\t{\r\n";writingStr += "\t\t\t\t" + GetFieldWritingStr(Tkey, "key") + "\r\n";writingStr += "\t\t\t\t" + GetFieldWritingStr(Tvalue, name + "[key]") + "\r\n";writingStr += "\t\t\t}\r\n";}else{writingStr += "\t\t\t" + GetFieldWritingStr(type, name) + "\r\n";}}return writingStr;}private string GetFieldWritingStr(string type, string name){switch (type){case "byte":return "WriteByte(bytes, " + name + ", ref index);";case "int":return "WriteInt(bytes, " + name + ", ref index);";case "short":return "WriteShort(bytes, " + name + ", ref index);";case "long":return "WriteLong(bytes, " + name + ", ref index);";case "float":return "WriteFloat(bytes, " + name + ", ref index);";case "bool":return "WriteBool(bytes, " + name + ", ref index);";case "string":return "WriteString(bytes, " + name + ", ref index);";case "enum":return "WriteInt(bytes, Convert.ToInt32(" + name + "), ref index);"; default:return "WriteData(bytes, " + name + ", ref index);";}}//private string GetReadingStr(XmlNodeList fields){string readingStr = "";string type = "";string name = "";foreach (XmlNode field in fields){type = field.Attributes["type"].Value;name = field.Attributes["name"].Value;if (type == "list"){string T = field.Attributes["T"].Value;readingStr += "\t\t\t" + name + " = new List<" + T + ">();\r\n";readingStr += "\t\t\tshort " + name + "Count = ReadShort(bytes, ref index);\r\n";readingStr += "\t\t\tfor(int i = 0; i < " + name + "Count; i++)\r\n";readingStr += "\t\t\t\t" + name + ".Add(" + GetFieldReadingStr(T) + ");\r\n";}else if (type == "array"){string data = field.Attributes["data"].Value;                readingStr += "\t\t\tshort " + name + "Length = ReadShort(bytes, ref index);\r\n";readingStr += "\t\t\t" + name + " = new " + data + "[" + name + "Length];\r\n";readingStr += "\t\t\tfor(int i = 0; i < " + name + "Length; i++)\r\n";readingStr += "\t\t\t\t" + name + "[i] = " + GetFieldReadingStr(data) + ";\r\n";}else if (type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;readingStr += "\t\t\t" + name + " = new Dictionary<" + Tkey + ", " + Tvalue + ">();\r\n";readingStr += "\t\t\tshort " + name + "Count = ReadShort(bytes, ref index);\r\n";readingStr += "\t\t\tfor(int i = 0; i < " + name + "Count; i++)\r\n";readingStr += "\t\t\t\t" + name + ".Add(" + GetFieldReadingStr(Tkey)+ ", " + GetFieldReadingStr(Tvalue) + ");\r\n";}else if (type == "enum"){string data = field.Attributes["data"].Value;readingStr += "\t\t\t" + name + " = (" + data + ")ReadInt(bytes, ref index);\r\n"; }elsereadingStr += "\t\t\t" + name + " = " + GetFieldReadingStr(type) + ";\r\n";}return readingStr;}private string GetFieldReadingStr(string type){switch (type){case "byte":return "ReadByte(bytes, ref index)";case "int":return "ReadInt(bytes, ref index)";case "short":return "ReadShort(bytes, ref index)";case "long":return "ReadLong(bytes, ref index)";case "bool":return "ReadBool(bytes, ref index)";case "float":return "ReadFloat(bytes, ref index)";case "string":return "ReadString(bytes, ref index)";default:return "ReadData<" + type + ">(bytes, ref index)";}}}

第三方协议生成工具Protobuf

Protobuf

初识和准备Protobuf工具

配置规则

syntax = "proto3"; //决定了proto文档的版本号
//规则二:版本号//规则一:注释方式
//注释方式1
/*注释方式2*///规则11:导入定义
import "test2.proto";//规则三:命名空间
package GamePlayerTest; //这决定了命名空间//规则四:消息类
message TestMsg{//字段声明//规则五:成员类型 和唯一编号//浮点数// = 1 不代表默认值 而是代表唯一编号 方便我们进行序列化和反序列化的处理//required 必须赋值的字段required float testF = 1; //C# - float//optional 可以不赋值的字段optional double testD = 2; //C# - double//变长编码//所谓变长 就是会根据 数字的大小 来使用对应的字节数来存储 (字节数为:1 2 4)//Protobuf 帮助我们优化的部分 可以尽可能少的使用字节数 来存储内容int32 testInt32 = 3; //C# - int 它不太适用于来表示负数 请使用sint32//处理的字节数为 1,2,4,8int64 testInt64 = 4; //C# -long 它也不太适用于来表示负数 请使用sint64//更适应于表示负数类型的整数sint32 testSInt32 = 5; //C# - int 实用于表示负数的整数sint64 testSInt64 = 6; //C# - long 适用于表示负数的整数//无符号 变长参数uint32 testUInt = 7; //C# - uint 变长的编号uint64 testUInt = 8; //C# - ulong 变长的编号//固定字节数的类型fixed32 testFixed32 = 9; //C# - uint 它通常用来表示大于2的28次方的数,比uint32更有效 始终是4个字节fixed64 testFixed64 = 10; //C# - ulong 它通常用来表示大于2的56次方的数,比uint64更有效 始终是8个字节sfixed32 testSFixed32 = 11; //C# - int 始终4个字节sfixed64 testSFixed64 = 12; //C# - long 始终8个字节//其它类型bool testBool = 13; //C# - boolstring testStr = 14; //C# - stringbytes testBytes = 15; //C# - BytesString 字节字符串 (了解)//规则6 特殊标识//数组 Listrepeated int32 listInt = 16; //C# - 类型List<int>的使用//字典Dictionarymap<int32, string> testMap = 17; //C# - 类型Dictionary<int, string> 的使用//枚举成员变量的声明 需要唯一编码TestEnum testEnum = 18;//规则8 默认值//申明自定义类对象 需要唯一编码//默认值是nullTestMsg2 testMsg2 = 19;//规则9:允许嵌套//嵌套一个类在另一个类当中 相当于是内部类message TestMsg3{int32 testInt32 = 1; //在新的类中 唯一编号从1开始,不用接上一个类的}TestMsg3 testMsg3 = 20;//规则9:允许嵌套enum TestEnum2{NORMAL = 0; //第一个常量必须映射到0BOSS = 1;}TestEnum2 testEnum2 = 21;//int32 testInt32666 = 22;bool testBool666 = 23;GameSystemTest.HeartMsg testHeart = 24;//规则10 保留字段//告诉编译器 22 被占用 不准用户使用//之所以有这个功能 是为了在版本不匹配时 反序列化时 不会出现结构不统一//解析错误的问题reserved 22;reserved testInt32666;
}//规则7 枚举
//枚举的声明
enum TestEnum{NORMAL = 0; //第一个常量必须映射到0BOSS = 5;
}message TestMsg2{int32 testInt32 = 1; //在新的类中 唯一编号从1开始,不用接上一个类的
}

协议生成

using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using UnityEditor;
using UnityEngine;public class ProtobufTool 
{//协议配置文件所在路径//这个路径尽量是要和工程文件一起,可以放在 Assets同级目录下,但要求是这个路径不能有中文或特殊符号//我的这个工程路径有中文,所以用另一个文件代替。private static string PROTO_PATH = "D:\\Protobuf\\proto";  //转义字符 \\ -> 得\//协议生成可执行文件 protoc.exe 的路径private static string PROTOC_PATH = "D:\\Protobuf\\protoc.exe";//C#文件生成的路径private static string CSHARP_PATH = "D:\\Protobuf\\csharp";//C++文件生成的路径private static string CPP_PATH = "D:\\Protobuf\\cpp";//java文件生成的路径private static string JAVA_PATH = "D:\\Protobuf\\java";[MenuItem("ProtobufTool/生成C#代码")]private static void GenerateCSharp(){Generate("csharp_out", CSHARP_PATH);}[MenuItem("ProtobufTool/生成C++代码")]private static void GenerateCPP(){Generate("cpp_out", CPP_PATH);}[MenuItem("ProtobufTool/生成Java代码")]private static void GenerateJava(){Generate("java_out", JAVA_PATH);}//生成对应脚本的方法 private static void Generate(string outCmd, string outPath){//第一步:遍历对应协议配置文件夹 得到所有的配置文件DirectoryInfo directoryInfo = Directory.CreateDirectory(PROTO_PATH);//获取对应文件下所有文件信息FileInfo[] files = directoryInfo.GetFiles();//遍历所有的文件 为其生成协议脚本for (int i = 0; i < files.Length; i++){//后缀的判断 只有是 配置文件才能用于生成if (files[i].Extension == ".proto"){//第二步:根据文件内容 来生成对应的C#脚本 (需要使用C#当中的Process类)Process cmd = new Process();  //相当于执行cmd命令//protoc.exe 的路径  告诉cmd可执行文件的路径cmd.StartInfo.FileName = PROTOC_PATH;//命令cmd.StartInfo.Arguments = $"-I={PROTO_PATH} --{outCmd}={outPath} {files[i]}";//执行cmd.Start();//告诉外部 某一个文件 生成结束UnityEngine.Debug.Log(files[i] + "生成结束");}}UnityEngine.Debug.Log("所有内容生成结束");}
}

协议使用

using Google.Protobuf;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UnityEngine;public class NetTool 
{//序列化Protobuf生成的对象public static byte[] GetProtoBytes(IMessage msg){//涉及的知识点:拓展方法、里氏替换、接口 这些知识点 都在 C#相关的内容当中byte[] bytes = null;//基础写法 基于上节课学习的知识点//using (MemoryStream ms = new MemoryStream())//{//    msg.WriteTo(ms);//    bytes = ms.ToArray();//}//return bytes;//通过该拓展方法 就可以直接获取对应对象的 字节数组了return msg.ToByteArray();}/// <summary>/// 反序列化字节数组为Protobuf相关的对象/// </summary>/// <typeparam name="T">想要获取的消息类型</typeparam>/// <param name="bytes">对应的字节数组</param>/// <returns></returns>public static T GetProtoMsg<T>(byte[] bytes) where T : class, IMessage //T必须是个类并且继承IMessage{//涉及的知识点:泛型、反射 C#进阶//得到对应消息的类型 通过反射得到内部的静态成员 然后得到其中的 对应的方法//进行反序列化Type type = typeof(T);//通过反射 得到对应的 静态成员属性对象PropertyInfo pInfo = type.GetProperty("Parser");// 由于是静态的第一个参数传空,第二个参数因为没有参数也没有什么索引所以也传空 object parserObj =  pInfo.GetValue(null, null);//已经得到了对象 那么可以得到该对象中的 对应方法Type parserType = parserObj.GetType();//这是指定得到某个重载函数//第二个参数 new Type 代表一数组MethodInfo mInfo = parserType.GetMethod("ParseFrom", new Type[] { typeof(byte[]) });//调用对应的方法 反序列化为指定的对象// 相当于是 T(某个msg).Parser.ParseFrom(bytes)object msg =  mInfo.Invoke(parserObj, new object[] { bytes });return msg as T;}}

Protobuf-Net

其它

大小端模式

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using UnityEngine;public class Lesson44 : MonoBehaviour
{// Start is called before the first frame updatevoid Start(){#region 知识点一 什么是大小端模式//大端模式//是指数据的高字节保存在内存的低地址中//而数据的低字节保存在内存的高地址中//这样的存储模式有点儿类似于把数据当作字符串顺序处理//地址由小向大增加,数据从高位往低位放//符合人类的阅读习惯//小端模式//是指数据的高字节保存在内存的高地址中//而数据的低字节保存在内存的低地址中//举例说明//十六进制数据0x11223344//大端模式存储// 11  22  33  44// 0   1    2   3//低地址 ——> 高地址//小端模式存储// 44  33   22  11// 0   1    2   3//低地址 ——> 高地址#endregion#region 知识点二 为什么有大小端模式//大小端模式其实是计算机硬件的两种存储数据的方式//我们也可以称大小端模式为大小端字节序//对于我们来说,大端字节序阅读起来更加方便,为什么还要有小端字节序呢?//原因是,计算机电路先处理低位字节,效率比较高//计算机处理字节序的时候,不知道什么是高位字节,什么是低位字节//它只知道按顺序读取字节,先读第一个字节, 再读第二个字节//如果是大端字节序,先读到的就是高位字节,后读到的就是低位字节//小端字节序正好相反//因为计算机都是从低位开始的//所以,计算机的内部处理都是小端字节序//但是,我们人类的读写习惯还是大端字节序//所以,除了计算机的内部处理//其它场合几乎都是大端字节序,比如网络传输和文件存储//一般情况下,操作系统都是小端模式,而通讯协议都是大端模式//但是具体的模式,还是要根据硬件平台,开发语言来决定//主机不同,开发语言不同可能采用的大小端模式也会不一致#endregion#region 知识点三 大小端模式对于我们的影响//我们记住一句话://只有读取的时候,才必须区分大小端字节序,其它情况都不用考虑//因此对于我们来说,在网络传输当中我们传输的是字节数组//那么我们在收到字节数组进行解析时,就需要考虑大小端的问题//虽然TCP/IP协议规定了在网络上必须采用网络字节顺序(大 端模式)//但是具体传输时采用哪种模式,都是根据前后端语言、设备决定的//在进行网络通讯时,前后端语言不同时,可能会造成大小端不统一//一般情况下//C#和Java/Erlang/AS3 通讯需要进行大小端转换因为C#是小端模式Java/Erlang/AS3是大端模式//C#与C++通信不需要特殊处理他们都是小端模式#endregion#region 知识点四 大小端转换#region 1.判断是大小端哪种模式print("是否是小端模式:" + BitConverter.IsLittleEndian);#endregion#region 2.简单的转换API 只支持几种类型//转换为网络字节序 相当于就是转为大端模式//1.本机字节序转网络字节序int i = 99;byte[] bytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(i));//2.网络字节序转本机字节序int receI = BitConverter.ToInt32(bytes, 0);receI = IPAddress.NetworkToHostOrder(receI);#endregion#region 3.通用的转换方式//数组中的倒转API  —— Reverse//如果后端需要用到大端模式 那么我们进行判断//如果当前是小端模式 就进行一次 大小端转换if (BitConverter.IsLittleEndian){Array.Reverse(bytes);}#endregion#endregion#region 总结//大小端模式会根据主机硬件环境不同、语言不同而有所区别//当我们前后端是不同语言开发且运行在不同主机上时//前后端需要对大小端字节序定下统的规则//-般让前端迎合后端,因为字节序的转换也是会带来些许性能损耗的//网络游戏中要尽量减轻后端的负担//-般情况下//C#和Java/Erlang/AS3 通讯需要进行大小端转换前端C#从小变大//C#与C++通信不需要特殊处理//我们不用死记硬背和谁通讯要注意大小端模式//当开发时,发现后端收到的消息和前端发的不一样//在协议统一的情况下,往往就是因为大小端造成的//这时我们再转换模式即可//注意://Protobuf已经帮助我们解决了大小端问题//即使前后端语言不统//使用它也不用过多考虑字节序转换的问题#endregion}// Update is called once per framevoid Update(){}
}

消息加密

总结

注:实践项目放在另一篇文章了!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1559703.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

linux 搭建sentinel

1.下载 linux执行下面的命令下载包 wget https://github.com/alibaba/Sentinel/releases/download/1.8.6/sentinel-dashboard-1.8.6.jar2.启动 nohup java -Dserver.port9090 -Dcsp.sentinel.dashboard.serverlocalhost:9090 -Dproject.namesentinel-dashboard -jar sentin…

【免费可视化仪表盘】轻松实现静态/动态数据可视化图表—积木仪表盘

在当今信息爆炸的时代&#xff0c;如何从海量数据中快速提取关键信息&#xff0c;实现高效决策&#xff0c;成为了企业和个人面临的重大挑战。而积木仪表盘&#xff0c;就如同一位智慧的导航者&#xff0c;为你轻松开启数据可视化的精彩之旅。 代码下载 https://github.com/je…

elementui+vue 多行数据的合并单元格

多行的合并&#xff0c;可以参考&#xff0c;改改就能用 html <template><el-table :data"students" :span-method"objectSpanMethod"><el-table-column prop"grade" label"年级"></el-table-column><el-…

BMS-绝缘检测

一、为什么要进行绝缘检测 前言&#xff1a;BMS绝缘检测是指对电池组与车体之间的绝缘状态进行实时监测和检测。为了确保电池组与车体之间的绝缘性能良好&#xff0c;防止漏电和短路等安全隐患&#xff0c;BMS绝缘检测系统能够及时发现绝缘故障&#xff0c;并采取相应的措施进…

MySQL 联合索引底层存储结构及索引查找过程解读

前言 大家好&#xff0c;我是 Lorin &#xff0c;联合索引&#xff08;Composite Index&#xff09;又称复合索引&#xff0c;它包括两个或更多列。与单列索引不同&#xff0c;联合索引可以覆盖多个列&#xff0c;这有助于加速复杂查询和过滤条件的检索。联合索引的列顺序非常…

接口测试-day3-jmeter-2组件和元件

组件和元件&#xff1a; 组件&#xff1a;组件指的是jmeter里面任意一个可以使用的功能。比如说查看结果树或者是http请求 元件&#xff1a;元件指是提对组件的分类 组件的作用域&#xff1a;组件放的位置不一样生效也不一样。 作用域取决于组件的的层级结构并不取决于组件的…

GIS前端工程师岗位职责、技术要求和常见面试题

文章来源&#xff1a;https://blog.csdn.net/eqmaster/article/details/141891186 GIS 前端工程师负责运用前端技术实现地理信息系统的可视化交互界面&#xff0c;以提升用户对地理数据的操作体验和分析能力。 GIS 后端工程师岗位职责 界面开发 负责 GIS 应用的前端界面设计…

打造智能洗衣店:Spring Boot订单管理系统

1系统概述 1.1 研究背景 如今互联网高速发展&#xff0c;网络遍布全球&#xff0c;通过互联网发布的消息能快而方便的传播到世界每个角落&#xff0c;并且互联网上能传播的信息也很广&#xff0c;比如文字、图片、声音、视频等。从而&#xff0c;这种种好处使得互联网成了信息传…

Ubuntu安装nvidia显卡驱动

一、安装依赖 1、更新 sudo apt update sudo apt upgrade -y 2、基础工具 sudo apt install -y build-essential cmake 图形界面相关 sudo apt install -y lightdm 注:在弹出对话框选择"lightdm" 下载nvidia驱动&#xff1a; 进入如下网址&#xff1a;http…

机器人末端的负载辨识

关节处的摩擦力变小了&#xff0c;导致系统的参数辨识精度会变高&#xff0c;因为动力学方程中的摩擦力项占的比例会变小。 为什么要有一个负载的参数辨识&#xff0c;因为对于整个系统来说&#xff0c;除了负载哈&#xff0c;其他关节都是不变的&#xff0c;出厂时都设置好了&…

Java基础-知识点

文章目录 数据类型包装类型缓存池 String概述不可变的含义不可变的好处String、StringBuffer、StringBuilderString.intern() 运算参数传递float与double隐式类型转换switch 继承访问权限抽象类与接口super重写与重载**1. 重写(Override)****2. 重载(Overload)** Object类的通用…

FFMPEG数据封装格式、多媒体传输协议、音视频编解码器

FFMPEG堪称自由软件中最完备的一套多媒体支持库&#xff0c;它几乎实现了所有当下常见的数据封装格式、多媒体传输协议以及音视频编解码器&#xff0c;提供了录制、转换以及流化音视频的完整解决方案。 ffmpeg命令行参数解释 ffmpeg -i [输入文件名] [参数选项] -f [格式] [输出…

速通!腾讯发布《2024大模型十大趋势》

【写在前面】 腾讯发布的《2024大模型十大趋势》报告在2024世界人工智能大会上引起了广泛关注。该报告深入分析了人工智能领域的最新进展&#xff0c;特别是大模型技术在不同应用场景中的潜力和影响&#xff0c;并预测了未来人工智能的发展方向。 “大模型技术发展方向 大模型…

深入理解HTTP Cookie

&#x1f351;个人主页&#xff1a;Jupiter. &#x1f680; 所属专栏&#xff1a;Linux从入门到进阶 欢迎大家点赞收藏评论&#x1f60a; 目录 HTTP Cookie定义工作原理分类安全性用途 认识 cookie基本格式实验测试 cookie 当我们登录了B站过后&#xff0c;为什么下次访问B站就…

光伏电站灰尘监测系统的工作原理

型号&#xff1a;TH-HS1】光伏电站灰尘监测系统是一种专门用于监测光伏电站内部灰尘积累情况的系统&#xff0c;通过安装在太阳能电池板表面的传感器&#xff0c;实时收集电池板表面的灰尘信息&#xff0c;包括灰尘厚度、污染比、洁净比等&#xff0c;并将这些数据发送到中央处…

杨中科 ASP.NETCORE 异步编程二

一、不要用sleep() 如果想在异步方法中暂停一段时间&#xff0c;不要用Thread.sleep()&#xff0c;因为它会阻塞调用线程&#xff0c;而要用await.Task.Delay()。 举例: 下载一个网址,3秒后下载另一个 示例&#xff1a; sleep() 为了能直观看到效果&#xff0c;使用winfor…

【STM32开发之寄存器版】(八)-定时器的编码器接口模式

一、前言 1.1 编码器接口原理 编码器模式主要用于检测旋转编码器的转动方向和转动速度。旋转编码器一般输出两路相位相差90度的脉冲信号&#xff08;称为A相和B相&#xff09;&#xff0c;通过这两路信号&#xff0c;定时器可以判断编码器的旋转方向&#xff0c;并计数转动的脉…

新同事半天搭建了一套CRM系统,实力赢得老板青睐直接转正

我们都知道&#xff0c;搭建一套CRM系统&#xff0c;根据功能和数据的复杂性&#xff0c;一般需要2至4周才能完成。最近&#xff0c;我们团队新来了一位同事&#xff0c;之前做过产品&#xff0c;没写过代码。老板安排他试试能不能搭建一套CRM系统&#xff0c;主要用于市场部同…

【学术会议征稿】第五届应用力学与机械工程国际学术会议(ICAMME 2024)

第五届应用力学与机械工程国际学术会议&#xff08;ICAMME 2024&#xff09; 2024 5th International Conference on Applied Mechanics and Mechanical Engineering 在全球技术快速发展的背景下&#xff0c;应用力学和机械工程作为推动现代工业创新的根基&#xff0c;持续展…

解决html2canvas图片模糊不清,超出一页长截图问题

前言 最近需要做一个页面截图功能&#xff0c;类似QQ、微信截图功能&#xff0c;核心是将Html网页转换成图片格式&#xff0c;并且尽可能保证截图后图片样式和原网页一致。对比了一些第三方插件以及浏览器自带的API&#xff0c;最终选择使用JavaScript库‌&#xff1a;如html2…