概述
基础知识
网络开发必备理论
网络基本概念
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);
}
// 存储 short
protected void WriteShort(byte[] bytes, short value, ref int index)
{
BitConverter.GetBytes(value).CopyTo(bytes, index);
index += sizeof(short);
}
// 存储 long
protected void WriteLong(byte[] bytes, long value, ref int index)
{
BitConverter.GetBytes(value).CopyTo(bytes, index);
index += sizeof(long);
}
//存储 float
protected void WriteFloat(byte[] bytes, float value, ref int index)
{
BitConverter.GetBytes(value).CopyTo(bytes, index);
index += sizeof(float);
}
//存储 byte
protected void WriteByte(byte[] bytes, byte value, ref int index)
{
bytes[index] = value;
index += 1;
}
//存储 bool
protected 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);
}
// 存储 short
protected void WriteShort(byte[] bytes, short value, ref int index)
{
BitConverter.GetBytes(value).CopyTo(bytes, index);
index += sizeof(short);
}
// 存储 long
protected void WriteLong(byte[] bytes, long value, ref int index)
{
BitConverter.GetBytes(value).CopyTo(bytes, index);
index += sizeof(long);
}
//存储 float
protected void WriteFloat(byte[] bytes, float value, ref int index)
{
BitConverter.GetBytes(value).CopyTo(bytes, index);
index += sizeof(float);
}
//存储 byte
protected void WriteByte(byte[] bytes, byte value, ref int index)
{
bytes[index] = value;
index += 1;
}
//存储 bool
protected 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;
}
// 都 short
protected short ReadShort(byte[] bytes, ref int index)
{
short value = BitConverter.ToInt16(bytes, index);
index += sizeof(short);
return value;
}
//读 long
protected long ReadLong(byte[] bytes, ref int index)
{
long value = BitConverter.ToInt64(bytes, index);
index += sizeof(long);
return value;
}
//读 float
protected float ReadFloat(byte[] bytes, ref int index)
{
float value = BitConverter.ToSingle(bytes, index);
index += sizeof(float);
return value;
}
//读 byte
protected byte ReadByte(byte[] bytes, ref int index)
{
byte value = bytes[index];
index += 1;
return value;
}
//读 bool
protected bool ReadBool(byte[] bytes, ref int index)
{
bool value = BitConverter.ToBoolean(bytes, index);
index += sizeof(bool);
return value;
}
//读 string
protected string ReadString(byte[] bytes, ref int index)
{
//先读取长度
int length = ReadInt(bytes, ref index);
//再读取string
string value = Encoding.UTF8.GetString(bytes, index, length);
index += length;
return value;
}
//读 自定义类
protected T ReadData<T>(byte[] bytes, ref int index) where T:BaseData,new()
{
T value = new T();
index += value.Reading(bytes, index);
return value;
}
#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
{
//服务端 Socket
public Socket socket;
//客户端连接的所有Socket
public 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;
//客户端Socket
private 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 frame
void 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("服务器拒绝连接");
else
print("连接失败" + 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;
//客户端Socket
private 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 frame
void 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("服务器拒绝连接");
else
print("连接失败" + 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 和 长度
{
//解析ID
msgID = 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 update
void 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
//EndAccept
socketTcp.BeginAccept(AcceptCallBack, socketTcp); //监听连接是否成功
//客户端相关
//BeginConnect
//EndConnect
IPEndPoint 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
//EndReceive
socketTcp.BeginReceive(resultBytes, 0, resultBytes.Length, SocketFlags.None, ReceiveCallBack, socketTcp);
//发送消息
//BeginSend
//EndSend
byte[] 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异步方法的传入值
//我们需要通过它进行一些关键参数的赋值
//服务器端
//AcceptAsync
SocketAsyncEventArgs e = new SocketAsyncEventArgs();
e.Completed += (socket, args) => //两个参数分别是 socket->socketTcp args->e
{
//首先判断是否成功
if (args.SocketError == SocketError.Success)
{
//获取连入的客户端socket
Socket clientSocket = args.AcceptSocket;
(socket as Socket).AcceptAsync(args);
}
else
{
print("连入客户端失败" + args.SocketError);
}
};
socketTcp.AcceptAsync(e);
//客户端
//ConnectAsync
SocketAsyncEventArgs e2 = new SocketAsyncEventArgs();
e2.Completed += (socket, args) =>
{
if (args.SocketError == SocketError.Success)
{
//连接成功
}
else
{
//连接失败
print(args.SocketError);
}
};
socketTcp.ConnectAsync(e2);
//服务端和客户端
//发送消息
//SendAsync
SocketAsyncEventArgs e3 = new SocketAsyncEventArgs();
byte[] bytes2 = Encoding.UTF8.GetBytes("山有木兮木有枝");
e3.SetBuffer(bytes2, 0, bytes2.Length); //发送和接收都是 用SetBuffer
e3.Completed += (socket, args) => //监听有没有发送成功
{
if(args.SocketError == SocketError.Success)
{
//连接成功
print("发送成功");
}
else
{
//连接失败
print(args.SocketError);
}
};
socketTcp.SendAsync(e3);
//接收消息
//ReceiveAsync
SocketAsyncEventArgs 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 就可以得到连入的客户端Socket
Socket 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 frame
void 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;
//和服务器进行连接的 Socket
private Socket socket;
//接收消息的 缓存容器
private byte[] cacheBytes = new byte[1024 * 1024];
private int cacheNum = 0;
// Start is called before the first frame update
void Awake()
{
instance = this;
//过场景不移除
DontDestroyOnLoad(this.gameObject);
}
// Update is called once per frame
void 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);
else
Close();
}
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);
else
Program.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)
{
//解析ID
msgID = 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;
//和服务器进行连接的 Socket
private 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 update
void 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 frame
void 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);
else
Close();
}
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)
{
//解析ID
msgID = 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 update
void 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(); //消息A
byte[] bytes2 = msg2.Writing(); //消息B
byte[] 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 frame
void 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;
//先处理ID
int 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通信的Socket
socket = 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);
}
}
//关闭Socket
public 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 update
void Awake()
{
instance = this;
//过场景不移除
DontDestroyOnLoad(this.gameObject);
}
// Update is called once per frame
void 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;
//解析ID
msgID = 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);
}
//关闭Socket
public 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 update
void Start()
{
#region 知识点一 Socket UDP通信中的异步方法
//通过之前的学习,UDP用于的通信相关方法主要就是
//SendTo 和 ReceiveForm
//所以在讲解UDP异步通信时也主要是围绕着收发消息相关方法来讲解
#endregion
#region 知识点二 UDP通信中Begin相关异步方法
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
//BeginSendTo
byte[] 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
//BeginReceiveFrom
socket.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref ipPoint, ReceiveFromOver, (socket, ipPoint));
#endregion
#region 知识点三 UDP通信中 Async相关异步方法
//sendToAsync
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
//设置要发送的数据
args.SetBuffer(bytes, 0, bytes.Length);
//设置完成事件
args.Completed += SendToAsync;
socket.SendToAsync(args);
//ReceiveFromAsync
SocketAsyncEventArgs 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 frame
void 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通信的Socket
socket = 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);
}
}
//关闭Socket
public 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 update
void Awake()
{
instance = this;
//过场景不移除
DontDestroyOnLoad(this.gameObject);
}
// Update is called once per frame
void 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;
//解析ID
msgID = 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);
}
}
//关闭socket
public 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;
//设置代理为null
req.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;
//设置代理为null
req.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 update
void 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("上传通信成功");
else
print("上传失败:" + 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 frame
void 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.创建 HttpWebRequest
HttpWebRequest 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 update
void 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 frame
void 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 update
void 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 frame
void 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 update
void 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 //服务器传回的数据 可以进行处理 只是我们的服务器没有处理
}
else
print("上传失败:" + 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 上传成功");
}
else
print("上传失败:" + req.result + req.error + req.responseCode);
}
// Update is called once per frame
void 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 update
void 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.DownloadHandlerTexture
DownloadHandlerTexture textureHandler = new DownloadHandlerTexture();
req.downloadHandler = textureHandler;
yield return req.SendWebRequest();
if (req.result == UnityWebRequest.Result.Success)
{
//获取字节数组
//bufferHandler.data
image.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.DownloadHandlerAudioClip
AudioClip 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 frame
void 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 + "没有处理");
else
Debug.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.Count
for (int i = 0; i < list.Count; i++)
{
num += 4;
}
num += 4; //dic.Count
foreach (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(); //PlayerData
return 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";
}
else
bytesNumStr += "\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.Count
for (int i = 0; i < list.Count; i++)
{
num += 4;
}
num += 4; //dic.Count
foreach (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(); //PlayerData
return 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";
}
else
bytesNumStr += "\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.Count
for (int i = 0; i < list.Count; i++)
{
num += 4;
}
num += 4; //dic.Count
foreach (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(); //PlayerData
return num;
}
public override int Reading(byte[] bytes, int beginIndex = 0)
{
int index = beginIndex;
//反序列化 list
list = new List<int>();
short listCount = ReadShort(bytes, ref index);
for (int i = 0; i < listCount; i++)
{
list.Add(ReadInt(bytes, ref index));
}
//dic
dic = 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));
}
//arrays
int 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";
}
else
bytesNumStr += "\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";
}
else
readingStr += "\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.Count
for (int i = 0; i < list.Count; i++)
{
num += 4;
}
num += 4; //dic.Count
foreach (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(); //PlayerData
return num;
}
public override int Reading(byte[] bytes, int beginIndex = 0)
{
int index = beginIndex;
//反序列化 list
list = new List<int>();
short listCount = ReadShort(bytes, ref index);
for (int i = 0; i < listCount; i++)
{
list.Add(ReadInt(bytes, ref index));
}
//dic
dic = 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));
}
//arrays
int 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;
//消息id
idStr = 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";
}
else
bytesNumStr += "\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";
}
else
readingStr += "\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,8
int64 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# - bool
string testStr = 14; //C# - string
bytes testBytes = 15; //C# - BytesString 字节字符串 (了解)
//规则6 特殊标识
//数组 List
repeated int32 listInt = 16; //C# - 类型List<int>的使用
//字典Dictionary
map<int32, string> testMap = 17; //C# - 类型Dictionary<int, string> 的使用
//枚举成员变量的声明 需要唯一编码
TestEnum testEnum = 18;
//规则8 默认值
//申明自定义类对象 需要唯一编码
//默认值是null
TestMsg2 testMsg2 = 19;
//规则9:允许嵌套
//嵌套一个类在另一个类当中 相当于是内部类
message TestMsg3{
int32 testInt32 = 1; //在新的类中 唯一编号从1开始,不用接上一个类的
}
TestMsg3 testMsg3 = 20;
//规则9:允许嵌套
enum TestEnum2{
NORMAL = 0; //第一个常量必须映射到0
BOSS = 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; //第一个常量必须映射到0
BOSS = 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 update
void 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 frame
void Update()
{
}
}
消息加密
总结
注:实践项目放在另一篇文章了!