内容摘自最近继续看的《Unity3D网络游戏编程》
文章结构
NetMannger和ByreArray综述
同前文,网络模块的核心静态类是NetManager,它将提供一些基础的网络方法。
NetManager基于异步Socket实现,异步Socket回调函数把收到的消息按顺序存入队列msgList中。Update方法依次读取消息,再跟几乎监听表和协议名,调用相应的处理方法。
网络模块分为两个部分,第一部分是框架部分framework。framework包含网络管理器NetManager、为提高运行效率使用的ByteArray缓冲区、以及协议基类MsgBase。第二部分是协议类,它定义了客户端和服务器通信的数据格式。
建立NetManager类,声明一些关键变量:
其中ByteArray类的实现方法也与前文相同:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System;
//封装byte[],readIdx和length的类
public class ByteArray
{
//默认大小
const int DEFAULT_SIZE = 1024;
//初始大小
int initSize = 0;
//缓冲区
public byte[] bytes;
//读写位置
public int readIdx = 0;
public int writeIdx = 0;//指向缓冲区字符末尾
//容量
private int capacity = 0;
//剩余空间
public int remain{ get {return capacity-writeIdx; } }
//数据长度
public int length{ get {return writeIdx - readIdx;} }
//构造函数1
public ByteArray(int size = DEFAULT_SIZE)
{
bytes = new byte[size];
capacity = size;
initSize = size;
readIdx = 0;
writeIdx = 0;
}
//构造函数2
public ByteArray(byte[] defaultBytes){
bytes = defaultBytes;
capacity = defaultBytes.Length;
initSize = defaultBytes.Length;
readIdx = 0;
writeIdx = defaultBytes.Length;
}
//重设尺寸,通过该方法控制缓冲区扩展
public void ReSize(int size){
if(size<length) return;
if(size<initSize) return;
int n = 1;
while(n<size) n*=2;//以两倍扩展
capacity = n;
byte[] newBytes = new byte[capacity];
Array.Copy(bytes,readIdx,newBytes,0,writeIdx-readIdx);
bytes = newBytes;
writeIdx = length;
readIdx = 0;
}
//当数据量很少时可以通过移动数据的方式,而不用拓展
public void CheckAndMoveBytes(){
if(length < 8){
MoveBytes();
}
}
public void MoveBytes(){
if(length > 0){
Array.Copy(bytes,readIdx,bytes,0,length);
}
writeIdx = length;
readIdx = 0;
}
//写入数据
public int Write(byte[] bs,int offset,int count){
if(remain < count){
ReSize(length + count);
}
Array.Copy(bs,offset,bytes,writeIdx,count);
writeIdx += count;
return count;
}
//读取数据
public int Read(byte[] bs,int offset,int count){
count = Math.Min(count,length);
Array.Copy(bytes,readIdx,bs,offset,count);
readIdx += count;
CheckAndMoveBytes();
return count;
}
//读取INT16
public Int16 ReadInt16(){
if(length<2) return 0;
Int16 ret = (Int16)((bytes[readIdx + 1]<<8) | bytes[readIdx]);
readIdx += 2;
CheckAndMoveBytes();
return ret;
}
//读取Int32
public Int32 ReadInt32(){
if(length<4) return 0;
Int32 ret = (Int32) ((bytes[readIdx +3]<<24)|
(bytes[readIdx +2]<<16)|
(bytes[readIdx +1]<<8) |
bytes[readIdx +0]);
readIdx += 4;
CheckAndMoveBytes();
return ret;
}
}
稍微回顾以下ByteArray类做了哪些事情:
- 配合发送队列Queue< ByteArray >使用,避免同时发送多条数据导致的混乱。在代码中判定当前数据发送是否发送完毕,若没有发送完毕则再次调用BeginSend发送数据,如果发送完毕则删除该条数据,继续发送写入队列后续的数据
- 为了在缓冲区剩余长度不足以装下所有的数据时自动扩展缓冲区
- 当读取指针读取到缓冲区末尾时,自动调用移动数据方法,从而节约每一次读取都要移动数据区的性能。这样做可以让该调用接近O(1)的复杂度
网络事件模块
网络管理器需要提供“连接成功” “连接失败”和“断开连接”三种事件的回调。使用枚举方法定义这三种事件。
使用委托和字典创建事件委托类型和事件监听列表
//事件委托类型
public delegate void EventListener(String err);
//事件监听列表
public static Dictionary<NetEvent,EventListener> eventListeners = new Dictionary<NetEvent, EventListener>();
同时给监听列表写好添加和删除事件,需要注意的是在删除事件的时候,如果不使用Remove方法区移除事件,那么尽管对象的值为null,eventListener.ContainsKey还是会返回true。
//添加事件监听
public static void AddEventListener(NetEvent netEvent,EventListener listener){
//添加事件
if(eventListeners.ContainsKey(netEvent)){
eventListeners[netEvent] += listener;
}
//新增事件
else{
eventListeners[netEvent] = listener;
}
}
//删除事件监听
public static void RemoveEventListener(NetEvent netEvent,EventListener listener){
if(eventListeners.ContainsKey(netEvent)){
eventListeners[netEvent] -= listener;
//删除
if(eventListeners[netEvent] == null){
eventListeners.Remove(netEvent);
}
}
}
监听完成之后,我们还需要将添加分发事件的方法FireEvent,保证将传递过来的字符串传递给回调方法。
//分发事件
private static void FireEvent(NetEvent netEvent,String err){
if(eventListeners.ContainsKey(netEvent)){
eventListeners[netEvent](err);
}
}
连接服务端
连接服务端的Connect方法应当接收两个参数,分别代表服务端的IP地址和端口。Connect方法的核心是调用BeginConnect发起连接,它还会将Socket参数的NoDelay设置为true,表示不使用Nagle算法
Nagle算法指的是:如果发送端欲多次发送包含少量字节的数据包时,发送端不会立马发送数据,而是积攒到了一定数量后再将其组成一个较大的数据包发送出去。
同时,需要一个状态量标识当前是否在连接,程序设置了isConnecting成员,它代表当前是否处于正在连接的状态,即调用BeginConnect但回调函数尚未返回的阶段。Connect会做出各种状态判断:当连接成功时,它会阻止再次连接;若Socket处于“连接中”,则不能再次发起连接,代码如下:
//连接
public static void Connect(string ip,int port)//两个参数分别为IP地址和端口号
{
//状态判断
if(socket != null && socket.Connected){
Debug.Log("Connect fail,already connected");
return;
}
if(isConnecting){
Debug.Log("Connect fail,isConnecting");
return;
}
//初始化变量
InitState();
//参数设置
socket.NoDelay = true;
//Connect
isConnecting = true;
socket.BeginConnect(ip,port,ConnectCallback,socket);
}
调用的InitState,其功能是重置缓冲区等成员变量。比如遇到这种情况:客户端连接服务端之后开始接收数据,一段事件后断开,断开时读取缓冲区readBuff可能还有未处理的数据。客户端调用Connect重新连接后,readBuff还有之前未处理的数据,很显然这不是我们需要的,所以需要重置缓冲区
//初始化所有变量,为的是清理上一次可能留下的数据残留
public static void InitState()
{
//Socket
socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
//接收缓冲区
readBuff = new ByteArray();
//写入队列
writeQueue = new Queue<ByteArray>();
//是否正在连接
isConnecting = false;
}
而Connect的回调函数则需要处理三个事项:
- 将可能抛出异常的代码放在try-catch中,用于捕获异常
- 连接不论失败或者成功,都需要调用FireEvent分发对应的事件
- 将标识“连接中”的变量isConnecting设置为False,因为它只标识正在连接中,连接成功之后会有Socket.Connected
//Connect回调
private static void ConnectCallback(IAsyncResult ar)
{
try{
Socket socket = (Socket)ar.AsyncState;
socket.EndConnect(ar);
Debug.Log("Socket Connect Succ");
FireEvent(NetEvent.ConnectSucc,"");
isConnecting = false;
}
catch(SocketException ex){
Debug.Log("Socket Connect fail" + ex.ToString());
FireEvent(NetEvent.ConnectFail,ex.ToString());
isConnecting = false;
}
}
关闭连接
当客户端需要关闭连接的时候,程序并不会直接关闭连接,而是判断写入队列是否还有数据,如果还有数据,会等待数据发送完毕再关闭连接。因此,我们也需要一个布尔值isClosing来标识程序是否处于关闭中的状态。也要记得在InitState中清空初始值。
关闭连接方法Close需要做以下判断:只有在连接建立后才能关闭,然后依据写入队列writeQueue的长度判断是否需要延迟关闭。如果需要延迟关闭,设置状态位isClosing,等待发送数据的回调函数去处理。否则,调用socket.close()关闭连接,再调用FireEvent(NetEvent.Close,"")分发连接关闭的事件
//关闭连接
public static void Close()
{
//状态判断
if(socket==null || !socket.Connected){
return;
}
if(isConnecting){
return;
}
//还有数据在发送
if(writeQueue.Count > 0){
isClosing = true;
}
//没有数据需要继续发送
else{
socket.Close();//关闭连接
FireEvent(NetEvent.Close,"");
}
}
使用协议
使用协议一是为了传输的时候可以更加高效,二是为了开发人员方便操作,也方便查看协议内部有哪些变量。常见的协议有Json,protobuf等。而一个协议类的核心功能在于将一个协议对象转换成二进制数据(发送方),再把二进制数据转换成协议对象(接收方)
因此我们需要一个协议类来统一处理这些消息,一方面是为了实现处理消息的统一接口,另一个目的是方便实现Send方法。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MsgBase
{
//协议名
public string protoName = "";
}
之后的所有协议继承这个类即可,比如添加一个协议文件BattleMsg,再里面定义MsgMove和MsgAttack两个类
public class MsgMove : MsgBase
{
public MsgMove(){protoName = "MsgMove";}
public int x = 0;
public int y = 0;
public int z = 0;
}
public class MsgAttack : MsgBase
{
public MsgAttack(){protoName = "MsgAttack";}
public string desc = "127.0.0.1:6543";
}
协议体的编码和解码
在MsgBase中添加静态方法Encode和Decode,后续只要解析出Json字符串所在的缓冲区位置和协议名,再使用“MsgBase.Decode(协议名,缓冲区,起始位置,长度)”即可获得协议对象,编码方法Encode包含一个协议体对象参数,程序使用JsonUtility.ToJson将协议体转化成字符串,再使用“System.Text.Encoding.UTF8.GetString”将字符串转化成byte数组。解码方法会先使用“System.Text.Encoding.UTF8.GetString”将Byte数组中的部分数据解析成字符串,再使用JsonUtility.FromJson将字符串还原成指定类型的协议对象。程序使用“Type.GetTypr(protoName)”获取协议名对应的类型。
//编码
public static byte[] Encode(MsgBase msgBase){
string s = JsonUtility.ToJson(msgBase);
return System.Text.Encoding.UTF8.GetBytes(s);
}
//解码
public static MsgBase Decode(string protoName,byte[] bytes,int offset,int count){
string s = System.Text.Encoding.UTF8.GetString(bytes,offset,count);
MsgBase msgBase = (MsgBase)JsonUtility.FromJson(s,Type.GetType(protoName));
return msgBase;
}
}
协议名的编码解码
我们在消息格式中也附带了协议名长度这一信息量,长度为两个字节。所以对协议名的解析主要是围绕这个长度信息来完成的
//编码协议名(2字节长度+字符串)
public static byte[] EncodeName(MsgBase msgBase)
{
//名字bytes和长度
byte[] nameBytes = System.Text.Encoding.UTF8.GetBytes(msgBase.protoName);
Int16 len = (Int16)nameBytes.Length;
//申请bytes数值
byte[] bytes = new byte[2+len];
//组装2字节的长度信息
bytes[0] = (byte)(len%256);
bytes[1] = (byte)(len/256);
//组装名字bytes
Array.Copy(nameBytes,0,bytes,2,len);
return bytes;
}
//解码协议名(2字节长度+字符串)
public static string DecodeName(byte[] bytes,int offset,out int count)
{
count = 0;
//必须大于两个字节
if(offset + 2 > bytes.Length){
return "";
}
//读取长度
Int16 len = (Int16)((bytes[offset+1] <<8)|bytes[offset]);
if(len<=0){
return "";
}
//长度必须足够
if(offset + 2 + len > bytes.Length){
return "";
}
//解析
count = 2 + len;
string name = System.Text.Encoding.UTF8.GetString(bytes,offset+2,len);
return name;
}
发送数据
Send方法
NetManager类中另一个重要的方法就是发送数据,上文中的编码和解码都是为了发送而准备的。
Send方法需要完成的事情有:
- 进行一系列的状态判断,判断是否可以发送数据(当前是否连接,是否正在连接,是否已经关闭)
- 编码组装数据。虽然我们有了编码和解码方法,但是还需要将其协议名和协议体组装到一起,并且加上长度信息,复制到最终的发送缓冲中才算完成任务。
- 将其添加到发送队列之中,并且当发送队列长度为一的时候进行发送
public static void Send(MsgBase msg)
{
//状态判断
if(socket==null || !socket.Connected){
return;
}
if(isConnecting){
return;
}
if(isClosing){
return;
}
//数据编码
byte[] nameBytes = MsgBase.EncodeName(msg);
byte[] bodyBytes = MsgBase.Encode(msg);
int len = nameBytes.Length + bodyBytes.Length;
byte[] sendBytes = new byte[len + 2];
//组装长度,封装长度信息
sendBytes[0] = (byte)(len%256);
sendBytes[1] = (byte)(len/256);
//组装名字
Array.Copy(nameBytes,0,sendBytes,2,nameBytes.Length);
//组装消息体
Array.Copy(bodyBytes,0,sendBytes,2+nameBytes.Length,bodyBytes.Length);
//写入队列
ByteArray ba = new ByteArray(sendBytes);
int count = 0;//writeQueue的长度
lock(writeQueue){//加锁,避免异步线程错误
writeQueue.Enqueue(ba);
count = writeQueue.Count;
}
//send
if(count == 1){
socket.BeginSend(sendBytes,0,sendBytes.Length,0,SendCallback,socket);
}
}
SendCallback
回调方法主要做的事情是判断写入队列是否还有数据,如果写入队列不为空,它会继续调用BeginSend发送数据。
//Send回调
public static void SendCallback(IAsyncResult ar){
//获取state,EndSend的处理
Socket socket = (Socket)ar.AsyncState;
//状态判断
if(socket==null||!socket.Connected){
return;
}
//EndSend
int count = socket.EndSend(ar);
//获取写入队列的第一条数据
ByteArray ba;
lock(writeQueue){
ba = writeQueue.First();
}
//完整发送
ba.readIdx += count;
if(ba.length == 0){
lock(writeQueue){
writeQueue.Dequeue();
ba = writeQueue.First();
}
}
//继续发送
if(ba!=null){
socket.BeginSend(ba.bytes,ba.readIdx,ba.length,0,SendCallback,socket);
}
//正在关闭
else if(isClosing){
socket.Close();
}
}
消息事件
消息事件就是用来处理不同协议的回调方法,与网络事件比较类似,只不过它是通过协议名称去分发消息的
//消息委托类型
public delegate void MsgListener(MsgBase msgBase);
//消息监听列表
private static Dictionary<string,MsgListener> msgListeners = new Dictionary<string, MsgListener>();
//添加消息监听
public static void AddMsgListener(string msgName,MsgListener listener){
//添加
if(msgListeners.ContainsKey(msgName)){
msgListeners[msgName] += listener;
}
//新增
else{
msgListeners[msgName] = listener;
}
}
//删除消息监听
public static void RemoveMsgListener(string msgName,MsgListener listener){
if(msgListeners.ContainsKey(msgName)){
msgListeners[msgName] -= listener;
//删除
if(msgListeners[msgName] ==null){
msgListeners.Remove(msgName);
}
}
}
//分发消息
private static void FireMsg(string msgName,MsgBase msgBase){
if(msgListeners.ContainsKey(msgName)){
msgListeners[msgName](msgBase);
}
}
接收数据
接收数据的过程和前篇介绍的过程类似:回调函数ReceiveCallback会将消息存放到消息列表msgList中,主线程Update会读取消息列表,再一条一条进行处理。这里还将做出一些改进:
- 定义MAX_MESSAGE_FIRE指示每一帧处理多少条信息,控制处理
- 添加粘包半包、大小端判断等处理
- 使用Json协议,使得后续开发时,无需关注协议的格式
//消息列表
static List<MsgBase> msgList = new List<MsgBase>();
//消息列表长度
static int msgCount = 0;
//每一次Update处理的信息量,readonly代表只读属性
readonly static int MAX_MESSAGE_FIRE = 10;
同时要记得在初始化函数中将相关变量清理,同时需要在异步连接方法成功后开启异步接受方法
//开启接收
socket.BeginReceive(readBuff.bytes,readBuff.writeIdx,readBuff.remain,0,ReceiveCallback,socket);
异步接收回调ReceiveCallback
ReceiveCallback会判断是否成功接收到数据,如果收到FIN信号,就会断开连接,如果收到正常的数据,那么他就会更新缓冲区的writeIdx,再调用OnReceiveData处理消息,OnReceiveData会解析协议,并且把协议对象放置到消息列表msgList中,在ReceiveCallback的最后,它将再次调用BeginReceive,并且开启下一轮的数据接收
//Receive回调
public static void ReceiveCallback(IAsyncResult ar){
try{
Socket socket = (Socket) ar.AsyncState;
//获取接收数据长度
int count = socket.EndReceive(ar);
if(count == 0){
Close();
return;
}
readBuff.writeIdx += count;
//处理二进制消息
OnReceiveData();
//继续接收数据
if(readBuff.remain<8){
readBuff.MoveBytes();
readBuff.ReSize(readBuff.length*2);
}
socket.BeginReceive(readBuff.bytes,readBuff.writeIdx,readBuff.remain,0,ReceiveCallback,socket);
}
catch(SocketException ex){
Debug.Log("Socket Receive fail" + ex.ToString());
}
}
OnReceiveData
OnReceiveData有两个功能。其一是根据协议的前两个字节判断是否接收到一条完整的协议。如果接收到完整协议,便解析它。如果没有接收完整协议,则退出等待下一波消息。其二是解析协议,解析出协议对象,然后通过“msgList.Add(msgBase)”将协议对象添加到协议列表中。添加到协议列表之前,程序使用“lock(msgList)”锁住了消息列表,这是因为OnReceiveData在子线程将数据写入消息队列,而Update在主线程读取消息队列,为了避免线程冲突,对msgList的操作都需要加锁。
//数据处理
public static void OnReceiveData(){
//消息长度
if(readBuff.length <= 2){
return;
}
//获取消息体长度
int readIdx = readBuff.readIdx;
byte[] bytes = readBuff.bytes;
Int16 bodyLength = (Int16)((bytes[readIdx+1]<<8) | bytes[readIdx]);
if(readBuff.length < bodyLength + 2){
return;
}
readBuff.readIdx += 2;
//解析协议名
int nameCount = 0;
string protoName = MsgBase.DecodeName(readBuff.bytes,readBuff.readIdx,out nameCount);
if(protoName ==""){
Debug.Log("OnReceiveData MsgBase.DecodeName fail");
return;
}
readBuff.readIdx += nameCount;
//解析协议体
int bodyCount = bodyLength - nameCount;
MsgBase msgBase = MsgBase.Decode(protoName,readBuff.bytes,readBuff.readIdx,bodyCount);
readBuff.readIdx += bodyCount;
readBuff.CheckAndMoveBytes();
//添加到消息队列
lock(msgList){
msgList.Add(msgBase);
}
msgCount++;
//继续读取消息
if(readBuff.length > 2){
OnReceiveData();
}
}
Update
Update需要做如下的处理:
- 现根据msgCount是否为0判断需不需要处理消息。如果msgCount为0,说明消息列表可能为空(因为多线程冲突所以是可能为空),则不需要乡下运行,提高程序运行效率。另一种做法是使用lock再判断,但是这样会增加主线程的等待时间,所以一般不使用锁
- 程序会使用for循环来做消息读取,并且循环次数小于每帧设定的消息处理数量最大值MAX_MESSAGE_FIRE。在每一次循环中,程序都会所著msgList,然后取出它的第一条数据,最后调用FireMsg分发消息
//Update方法
public static void Update() {
MsgUpdate();
}
//更新消息
public static void MsgUpdate(){
//初步判断,提升效率
if(msgCount == 0){
return;
}
//重复处理消息
for(int i = 0;i<MAX_MESSAGE_FIRE;i++){
//获取第一条消息
MsgBase msgBase = null;
lock(msgList){
if(msgList.Count > 0){
msgBase = msgList[0];
msgList.RemoveAt(0);
msgCount--;
}
}
//分发消息
if(msgBase != null){
FireMsg(msgBase.protoName,msgBase);
}
//没有消息了
else{
break;
}
}
}
心跳机制
心跳机制是确保服务端资源能正常释放的机制。如果玩家拿着手机进入没有信号的山区,或者有人拿着剪刀剪断网线,都会导致链路不通。如果超过一定的时长,则需要服务端和客户端主动断开连接以节约资源。具体来说就是,客户端会定时发送PING协议给服务端,服务端收到之后会回复PONG协议。如果客户端很长时间都没有接收到PONG协议,则说明链路可能不通畅,这时客户端这边可以主动释放Socket资源。这其实对服务端更重要一些,因为服务端空余的Socket的资源可以支撑更多的玩家同时游玩。
在Proto下建立SysMsg文件,在文件里新建MsgPing和MsgPong两个类,同时在NetMannager中声明相关变量,同时要记得在初始化方法中重置。
//是否启用心跳机制
public static bool isUsePing = true;
//心跳间隔时间
public static int pintInterval = 30;
//上一次发送Ping的时间
static float lastPingTime = 0;
//上一次收到PONG的时间
static float lastPongTime = 0;
发送Ping协议
在NetManager中编写发送MsgPing协议的PingUpdate方法,并在Update中调用它。PingUpdate应当实现下列三种功能:
- 根据isUsePing判断是否使用心跳机制,如果没有开启,则直接跳过
- 判断当前时间与上一次发送MsgPing协议的时间间隔,如果超过指定时间,则调用Send(msgPing)向服务端发送MsgPing协议
- 判断当前时间与上一次接收MsgPong协议的时间间隔,如果超过指定时间,则调用Close关闭连接
//心跳机制PingUpdate
private static void PingUpdate(){
//是否启用
if(!isUsePing){
return;
}
//发送Ping
if(Time.time - lastPingTime > pintInterval){
MsgPing msgPing = new MsgPing();
Send(msgPing);
lastPingTime = Time.time;
}
//检测PONG时间
if(Time.time - lastPongTime > pintInterval*4){
Close();
}
}
核心类完整代码
NetMannager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net.Sockets;
using System;
using System.Linq;
public class NetManager : MonoBehaviour
{
//该枚举用于区分监听方法
public enum NetEvent
{
ConnectSucc = 1,
ConnectFail = 2,
Close = 3,
}
//是否正在连接的布尔值
static bool isConnecting = false;
//是否处于"关闭中"
static bool isClosing = false;
//定义套接字
static Socket socket;
//接收缓冲区
static ByteArray readBuff;
//写入队列
static Queue<ByteArray> writeQueue;
//是否启用心跳机制
public static bool isUsePing = true;
//心跳间隔时间
public static int pintInterval = 30;
//上一次发送Ping的时间
static float lastPingTime = 0;
//上一次收到PONG的时间
static float lastPongTime = 0;
//消息列表
static List<MsgBase> msgList = new List<MsgBase>();
//消息列表长度
static int msgCount = 0;
//每一次Update处理的信息量,readonly代表只读属性
readonly static int MAX_MESSAGE_FIRE = 10;
//事件委托类型
public delegate void EventListener(String err);
//事件监听列表
private static Dictionary<NetEvent,EventListener> eventListeners = new Dictionary<NetEvent, EventListener>();
//添加事件监听
public static void AddEventListener(NetEvent netEvent,EventListener listener){
//添加事件
if(eventListeners.ContainsKey(netEvent)){
eventListeners[netEvent] += listener;
}
//新增事件
else{
eventListeners[netEvent] = listener;
}
}
//删除事件监听
public static void RemoveEventListener(NetEvent netEvent,EventListener listener){
if(eventListeners.ContainsKey(netEvent)){
eventListeners[netEvent] -= listener;
//删除
if(eventListeners[netEvent] == null){
eventListeners.Remove(netEvent);
}
}
}
//分发事件
private static void FireEvent(NetEvent netEvent,String err){
if(eventListeners.ContainsKey(netEvent)){
eventListeners[netEvent](err);
}
}
//消息委托类型
public delegate void MsgListener(MsgBase msgBase);
//消息监听列表
private static Dictionary<string,MsgListener> msgListeners = new Dictionary<string, MsgListener>();
//添加消息监听
public static void AddMsgListener(string msgName,MsgListener listener){
//添加
if(msgListeners.ContainsKey(msgName)){
msgListeners[msgName] += listener;
}
//新增
else{
msgListeners[msgName] = listener;
}
}
//删除消息监听
public static void RemoveMsgListener(string msgName,MsgListener listener){
if(msgListeners.ContainsKey(msgName)){
msgListeners[msgName] -= listener;
//删除
if(msgListeners[msgName] ==null){
msgListeners.Remove(msgName);
}
}
}
//分发消息
private static void FireMsg(string msgName,MsgBase msgBase){
if(msgListeners.ContainsKey(msgName)){
msgListeners[msgName](msgBase);
}
}
//连接
public static void Connect(string ip,int port)//两个参数分别为IP地址和端口号
{
//状态判断
if(socket != null && socket.Connected){
Debug.Log("Connect fail,already connected");
return;
}
if(isConnecting){
Debug.Log("Connect fail,isConnecting");
return;
}
//初始化变量
InitState();
//参数设置
socket.NoDelay = true;
//Connect
isConnecting = true;
socket.BeginConnect(ip,port,ConnectCallback,socket);
}
//关闭连接
public static void Close()
{
//状态判断
if(socket==null || !socket.Connected){
return;
}
if(isConnecting){
return;
}
//还有数据在发送
if(writeQueue.Count > 0){
isClosing = true;
}
//没有数据需要继续发送
else{
socket.Close();//关闭连接
FireEvent(NetEvent.Close,"");
}
}
//初始化所有变量,为的是清理上一次可能留下的数据残留
public static void InitState()
{
//Socket
socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
//接收缓冲区
readBuff = new ByteArray();
//写入队列
writeQueue = new Queue<ByteArray>();
//是否正在连接
isConnecting = false;
//是否处于关闭中
isClosing = false;
//清空消息列表
msgList = new List<MsgBase>();
msgCount = 0;
//重置心跳机制的记录时间
lastPingTime = Time.time;
lastPongTime = Time.time;
//监听PONG协议
if(!msgListeners.ContainsKey("MsgPong")){
AddMsgListener("MsgPong",OnMsgPong);
}
}
//监听Pong协议,更新lastPongTime方法
private static void OnMsgPong(MsgBase msgBase){
lastPongTime = Time.time;
}
//Connect回调
private static void ConnectCallback(IAsyncResult ar)
{
try{
Socket socket = (Socket)ar.AsyncState;
socket.EndConnect(ar);
Debug.Log("Socket Connect Succ");
FireEvent(NetEvent.ConnectSucc,"");
isConnecting = false;
//开启接收
socket.BeginReceive(readBuff.bytes,readBuff.writeIdx,readBuff.remain,0,ReceiveCallback,socket);
}
catch(SocketException ex){
Debug.Log("Socket Connect fail" + ex.ToString());
FireEvent(NetEvent.ConnectFail,ex.ToString());
isConnecting = false;
}
}
//发送数据
public static void Send(MsgBase msg)
{
//状态判断
if(socket==null || !socket.Connected){
return;
}
if(isConnecting){
return;
}
if(isClosing){
return;
}
//数据编码
byte[] nameBytes = MsgBase.EncodeName(msg);
byte[] bodyBytes = MsgBase.Encode(msg);
int len = nameBytes.Length + bodyBytes.Length;
byte[] sendBytes = new byte[len + 2];
//组装长度,封装长度信息
sendBytes[0] = (byte)(len%256);
sendBytes[1] = (byte)(len/256);
//组装名字
Array.Copy(nameBytes,0,sendBytes,2,nameBytes.Length);
//组装消息体
Array.Copy(bodyBytes,0,sendBytes,2+nameBytes.Length,bodyBytes.Length);
//写入队列
ByteArray ba = new ByteArray(sendBytes);
int count = 0;//writeQueue的长度
lock(writeQueue){//加锁,避免异步线程错误
writeQueue.Enqueue(ba);
count = writeQueue.Count;
}
//send
if(count == 1){
socket.BeginSend(sendBytes,0,sendBytes.Length,0,SendCallback,socket);
}
}
//Send回调
public static void SendCallback(IAsyncResult ar){
//获取state,EndSend的处理
Socket socket = (Socket)ar.AsyncState;
//状态判断
if(socket==null||!socket.Connected){
return;
}
//EndSend
int count = socket.EndSend(ar);
//获取写入队列的第一条数据
ByteArray ba;
lock(writeQueue){
ba = writeQueue.First();
}
//完整发送
ba.readIdx += count;
if(ba.length == 0){
lock(writeQueue){
writeQueue.Dequeue();
ba = writeQueue.First();
}
}
//继续发送
if(ba!=null){
socket.BeginSend(ba.bytes,ba.readIdx,ba.length,0,SendCallback,socket);
}
//正在关闭
else if(isClosing){
socket.Close();
}
}
//Receive回调
public static void ReceiveCallback(IAsyncResult ar){
try{
Socket socket = (Socket) ar.AsyncState;
//获取接收数据长度
int count = socket.EndReceive(ar);
if(count == 0){
Close();
return;
}
readBuff.writeIdx += count;
//处理二进制消息
OnReceiveData();
//继续接收数据
if(readBuff.remain<8){
readBuff.MoveBytes();
readBuff.ReSize(readBuff.length*2);
}
socket.BeginReceive(readBuff.bytes,readBuff.writeIdx,readBuff.remain,0,ReceiveCallback,socket);
}
catch(SocketException ex){
Debug.Log("Socket Receive fail" + ex.ToString());
}
}
//数据处理
public static void OnReceiveData(){
//消息长度
if(readBuff.length <= 2){
return;
}
//获取消息体长度
int readIdx = readBuff.readIdx;
byte[] bytes = readBuff.bytes;
Int16 bodyLength = (Int16)((bytes[readIdx+1]<<8) | bytes[readIdx]);
if(readBuff.length < bodyLength + 2){
return;
}
readBuff.readIdx += 2;
//解析协议名
int nameCount = 0;
string protoName = MsgBase.DecodeName(readBuff.bytes,readBuff.readIdx,out nameCount);
if(protoName ==""){
Debug.Log("OnReceiveData MsgBase.DecodeName fail");
return;
}
readBuff.readIdx += nameCount;
//解析协议体
int bodyCount = bodyLength - nameCount;
MsgBase msgBase = MsgBase.Decode(protoName,readBuff.bytes,readBuff.readIdx,bodyCount);
readBuff.readIdx += bodyCount;
readBuff.CheckAndMoveBytes();
//添加到消息队列
lock(msgList){
msgList.Add(msgBase);
}
msgCount++;
//继续读取消息
if(readBuff.length > 2){
OnReceiveData();
}
}
//Update方法
public static void Update() {
MsgUpdate();
PingUpdate();
}
//更新消息
public static void MsgUpdate(){
//初步判断,提升效率
if(msgCount == 0){
return;
}
//重复处理消息
for(int i = 0;i<MAX_MESSAGE_FIRE;i++){
//获取第一条消息
MsgBase msgBase = null;
lock(msgList){
if(msgList.Count > 0){
msgBase = msgList[0];
msgList.RemoveAt(0);
msgCount--;
}
}
//分发消息
if(msgBase != null){
FireMsg(msgBase.protoName,msgBase);
}
//没有消息了
else{
break;
}
}
}
//心跳机制PingUpdate
private static void PingUpdate(){
//是否启用
if(!isUsePing){
return;
}
//发送Ping
if(Time.time - lastPingTime > pintInterval){
MsgPing msgPing = new MsgPing();
Send(msgPing);
lastPingTime = Time.time;
}
//检测PONG时间
if(Time.time - lastPongTime > pintInterval*4){
Close();
}
}
}
MsgBase
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class MsgBase
{
//协议名
public string protoName = "";
//编码
public static byte[] Encode(MsgBase msgBase){
string s = JsonUtility.ToJson(msgBase);
return System.Text.Encoding.UTF8.GetBytes(s);
}
//解码
public static MsgBase Decode(string protoName,byte[] bytes,int offset,int count){
string s = System.Text.Encoding.UTF8.GetString(bytes,offset,count);
MsgBase msgBase = (MsgBase)JsonUtility.FromJson(s,Type.GetType(protoName));
return msgBase;
}
//编码协议名(2字节长度+字符串)
public static byte[] EncodeName(MsgBase msgBase)
{
//名字bytes和长度
byte[] nameBytes = System.Text.Encoding.UTF8.GetBytes(msgBase.protoName);
Int16 len = (Int16)nameBytes.Length;
//申请bytes数值
byte[] bytes = new byte[2+len];
//组装2字节的长度信息
bytes[0] = (byte)(len%256);
bytes[1] = (byte)(len/256);
//组装名字bytes
Array.Copy(nameBytes,0,bytes,2,len);
return bytes;
}
//解码协议名(2字节长度+字符串)
public static string DecodeName(byte[] bytes,int offset,out int count)
{
count = 0;
//必须大于两个字节
if(offset + 2 > bytes.Length){
return "";
}
//读取长度
Int16 len = (Int16)((bytes[offset+1] <<8)|bytes[offset]);
if(len<=0){
return "";
}
//长度必须足够
if(offset + 2 + len > bytes.Length){
return "";
}
//解析
count = 2 + len;
string name = System.Text.Encoding.UTF8.GetString(bytes,offset+2,len);
return name;
}
}
ByteArray
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System;
//封装byte[],readIdx和length的类
public class ByteArray
{
//默认大小
const int DEFAULT_SIZE = 1024;
//初始大小
int initSize = 0;
//缓冲区
public byte[] bytes;
//读写位置
public int readIdx = 0;
public int writeIdx = 0;//指向缓冲区字符末尾
//容量
private int capacity = 0;
//剩余空间
public int remain{ get {return capacity-writeIdx; } }
//数据长度
public int length{ get {return writeIdx - readIdx;} }
//构造函数1
public ByteArray(int size = DEFAULT_SIZE)
{
bytes = new byte[size];
capacity = size;
initSize = size;
readIdx = 0;
writeIdx = 0;
}
//构造函数2
public ByteArray(byte[] defaultBytes){
bytes = defaultBytes;
capacity = defaultBytes.Length;
initSize = defaultBytes.Length;
readIdx = 0;
writeIdx = defaultBytes.Length;
}
//重设尺寸,通过该方法控制缓冲区扩展
public void ReSize(int size){
if(size<length) return;
if(size<initSize) return;
int n = 1;
while(n<size) n*=2;//以两倍扩展
capacity = n;
byte[] newBytes = new byte[capacity];
Array.Copy(bytes,readIdx,newBytes,0,writeIdx-readIdx);
bytes = newBytes;
writeIdx = length;
readIdx = 0;
}
//当数据量很少时可以通过移动数据的方式,而不用拓展
public void CheckAndMoveBytes(){
if(length < 8){
MoveBytes();
}
}
public void MoveBytes(){
if(length > 0){
Array.Copy(bytes,readIdx,bytes,0,length);
}
writeIdx = length;
readIdx = 0;
}
//写入数据
public int Write(byte[] bs,int offset,int count){
if(remain < count){
ReSize(length + count);
}
Array.Copy(bs,offset,bytes,writeIdx,count);
writeIdx += count;
return count;
}
//读取数据
public int Read(byte[] bs,int offset,int count){
count = Math.Min(count,length);
Array.Copy(bytes,readIdx,bs,offset,count);
readIdx += count;
CheckAndMoveBytes();
return count;
}
//读取INT16
public Int16 ReadInt16(){
if(length<2) return 0;
Int16 ret = (Int16)((bytes[readIdx + 1]<<8) | bytes[readIdx]);
readIdx += 2;
CheckAndMoveBytes();
return ret;
}
//读取Int32
public Int32 ReadInt32(){
if(length<4) return 0;
Int32 ret = (Int32) ((bytes[readIdx +3]<<24)|
(bytes[readIdx +2]<<16)|
(bytes[readIdx +1]<<8) |
bytes[readIdx +0]);
readIdx += 4;
CheckAndMoveBytes();
return ret;
}
}
test
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class test : MonoBehaviour
{
//开始
void Start()
{
NetManager.AddEventListener(NetManager.NetEvent.ConnectSucc,OnConnectSucc);
NetManager.AddEventListener(NetManager.NetEvent.ConnectFail,OnConnectFail);
NetManager.AddEventListener(NetManager.NetEvent.Close,OnConnectClose);
NetManager.AddMsgListener("MsgMove",OnMsgMove);
}
//玩家点击连接按钮
public void OnConnectClick(){
NetManager.Connect("127.0.0.1",8888);
}
//玩家点击断开连接按钮
public void OnCloseClick(){
NetManager.Close();
}
//玩家点击移动按钮
public void OnMoveClick(){
MsgMove msg = new MsgMove();
msg.x = 120;
msg.y = 123;
msg.z = -6;
NetManager.Send(msg);
}
//回调函数
//连接成功回调
void OnConnectSucc(string err)
{
Debug.Log("OnConnectSucc");
}
//连接失败回调
void OnConnectFail(string err)
{
Debug.Log("OnConnectFail" + err);
}
//关闭连接
void OnConnectClose(string err)
{
Debug.Log("OnConnectClose");
}
//收到MsgMove协议
public void OnMsgMove(MsgBase msgBase){
MsgMove msg = (MsgMove)msgBase;
//消息处理
Debug.Log("OnMsgMove msg.x = " + msg.x);
Debug.Log("OnMsgMove msg.y = " + msg.y);
Debug.Log("OnMsgMove msg.z = " + msg.z);
}
//update
public void Update(){
NetManager.Update();
}
}