一、网络模块设计
1、对外接口
网络模块的核心是静态类NetManager,它对外提供了Connect、AddListener、Send等多个方法,它提供如下方法。
- NetManager.Connect("127.0.0.1",8888) 连接服务端
- NetManager.Close() 关闭连接
- NetManager.Send(msgMove) 发送消息,其中的参数msgMove为协议对象,NetManager会自动把它转换成二进制数据
- NetManager.Update() 需外部调用,用于驱动NetManager
- NetManager.AddMsgListener("MsgMove",OnMsgMove); 添加消息事件,如果收到MsgMove协议,便调用OnMsgMove方法。
- NetManager.AddEventListener(NetManager.NetEvent.ConnectSucc,OnConnectSucc);监听网络事件。一共会有3种监听事件:解决成功、连接失败、连接关闭
2、内部设计
NetManager基于异步Socket实现。异步socket回调函数把收到的消息按顺序存入消息队列msgList中,Update方法依次读取消息,再根据监听表和协议名,调用相应的处理方法。
网络模块分为两个部分。第一部分是框架部分framework。framework包含网络管理器NetManager、为提高运行效率使用的ByteArray缓冲区(前面已经实现了)、以及协议基类MsgBase(后面会详细介绍)。第二部分是协议类。它定义了客户端和服务端通信的数据格式,例如前面出现的移动协议MsgMove、攻击协议MsgAttack会定义在BattleMsg中。
二、MsgBase
所有的协议类都继承MsgBase。由于每个协议都含有协议名,在MsgBase中定义了代表协议名的字符串protoName。定义MsgBase还为了实现处理消息的统一接口,形如“OnMove(MsgBase msgBase)”,接口参数类型为MsgBase,用户只需使用形如“MsgMove msgMove=(MsgMove)msgBase”的语句即可得到真正的协议对象。另一个目的是方便实现Send方法。后续我们会给NetManager添加Send(MsgBase msgBase)函数,基于类的多态性,它可以发送具体的协议类,如“NetManager.Send(msgMove)”
using System;
using UnityEngine;
public class MsgBase
{
//协议名
public string protoName = "";
//编码,使用JsonUtility将MagBase编码成Json字符串
public static byte[] Encode(MsgBase msgBase)
{
string s = JsonUtility.ToJson(msgBase);
//把字符串转换成byte数组,用于后面给服务器端发送信息
return System.Text.Encoding.UTF8.GetBytes(s);
}
//解码,使用JsonUtility将Json字符串解码成MsgBase
public static MsgBase Decode(string protoName,byte[] bytes, int offset, int count)
{
//先把Byte数组转换成字符串
string s = System.Text.Encoding.UTF8.GetString(bytes, offset, count);
//使用JsonUtility.FromJson把字符串转换成具体的MsgBase实例
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;//这个byte包含 协议名字长度+协议名字,用于后面给服务器端发送消息
}
//解码协议名(2字节长度+字符串)
public static string DecodeName(byte[] bytes, int offset, out int count)
{
count = 0;
//必须大于2字节,因为有两个字节是存储协议名长度的
if (offset + 2 > bytes.Length)
return "";
//读取长度,手动计算出长度
Int16 len = (Int16)((bytes[offset + 1] << 8) | bytes[offset]);
//长度必须足够
if (offset + 2 + len > bytes.Length)
return "";
//解析
count = 2 + len;
string name = System.Text.Encoding.UTF8.GetString(bytes, offset + 2, len);
return name;
}
}
三、ByteArray
我前面一篇文章有
四、NetManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net.Sockets;
using System;
using System.Linq;
public static class NetManager
{
//定义套接字
static Socket socket;
//接收缓冲区,用于接收服务器端消息
static ByteArray readBuff;
//写入队列,用于存储所有要给服务器发送但还没发送的消息
static Queue<ByteArray> writeQueue;
//是否正在连接
static bool isConnecting = false;
//是否正在关闭
static bool isClosing = false;
//事件枚举类型,成功、失败、关闭
public enum NetEvent
{ ConnectSucc = 1,ConnectFail = 2,Close = 3, }
//定义事件委托类型
public delegate void EventListener(string err);
//事件监听列表
private static Dictionary<NetEvent, EventListener> eventListeners = new Dictionary<NetEvent, EventListener>();
[Header("消息处理")]
//消息列表,存储所有接收到的消息
static List<MsgBase> msgList = new List<MsgBase>();
//消息列表长度
//虽然也可以使用msgList.Length获取消息长度,但由于主线程(Update)和其他线程(ReceiveCallback)
//可能在同一时间操作msgList,为避免操作msgList引发的冲突,可定义msgCount来减少对msgList的操作次数。
static int msgCount = 0;
//Unity每一帧执行一次Update,一般每秒会执行30到60帧,如果没有下面这个变量,那么秒最多只能处理60条消息。
//每一次Update处理的消息量,定义只读变量MAX_MESSAGE_FIRE,指示每一帧处理多少条消息
readonly static int MAX_MESSAGE_FIRE = 10;
//定义消息委托类型
public delegate void MsgListener(MsgBase msgBase);
//消息监听列表
private static Dictionary<string, MsgListener> msgListeners = new Dictionary<string, MsgListener>();
[Header("心跳机制")]
//是否启用心跳
public static bool isUsePing = true;
//心跳间隔时间
public static int pingInterval = 30;
//上一次发送PING的时间
static float lastPingTime = 0;
//上一次收到PONG的时间
static float lastPongTime = 0;
//添加事件监听
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);
}
}
//初始化状态
private 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;
//上一次发送PING的时间
lastPingTime = Time.time;
//上一次收到PONG的时间
lastPongTime = Time.time;
//监听PONG协议
if (!msgListeners.ContainsKey("MsgPong"))
{
AddMsgListener("MsgPong", OnMsgPong);
}
}
//连接
public static void Connect(string ip, int port)
{
//状态判断
if (socket != null && socket.Connected)
{
Debug.Log("Connect fail, already connected!");
return;
}
if (isConnecting)
{
Debug.Log("Connect fail, isConnecting");
return;
}
//初始化成员
InitState();
//参数设置
socket.NoDelay = true;//将Socket参数NoDelay设置为true,表明不使用Nagle算法
//!Nagle算法要是避免发送小的数据包,要求TCP连接上最多只能有一个未被确认的小分组,在该分组的确认到达之前不能发送其他的小分组。
//也就是说他可能会把几段消息合成一个发送出去来减少消耗,减少网络中传输大量小数据包,
//但对于实时要求很高的游戏开发来说是显而易见需要禁掉的
//Connect
isConnecting = true;
socket.BeginConnect(ip, port, ConnectCallback, socket);//回调
}
//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);//Receive回调
}
catch (SocketException ex)
{
Debug.Log("Socket Connect fail " + ex.ToString());
FireEvent(NetEvent.ConnectFail, ex.ToString());//执行连接失败监听事件
isConnecting = false;
}
}
//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()
{
//消息长度,最少也要有长度信息也就是2字节
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)
return;
readBuff.readIdx += 2;
//解析协议名
int nameCount = 0;//名字所占用的字节数,通过下面的MsgBase.DecodeName函数获得
string protoName = MsgBase.DecodeName(readBuff.bytes,readBuff.readIdx, out nameCount);
Debug.Log("接收到:" + protoName);
if (protoName == "")//为空说明处理失败
{
Debug.Log("OnReceiveData MsgBase.DecodeName fail");
return;
}
//名字已经读取完了,所以readIdx也要后移
readBuff.readIdx += nameCount;
//解析协议体
int bodyCount = bodyLength - nameCount;//减去名字所占用的字节数,剩下的就是可以转换成MsgBase的数据了
MsgBase msgBase = MsgBase.Decode(protoName,readBuff.bytes, readBuff.readIdx, bodyCount);
readBuff.readIdx += bodyCount;//读完了对应的readIdx也要后移
readBuff.CheckAndMoveBytes();//检查并移动数据,避免readBuff过快的满,腾出更多的剩余空间
//添加到消息队列
lock (msgList)
{
msgList.Add(msgBase);
}
msgCount++;
//继续读取消息
if (readBuff.length > 2)
{
OnReceiveData();
}
}
//关闭连接:为了“完整地发送数据”,客户端关闭连接时,程序并不会直接关闭连接,而是判断写入队列是否还有数据,如果还有数据,会等待数据发送完毕再关闭连接。
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 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[2 + len];
//组装长度
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);
//最后得到的消息体结构就是:总长度+协议名长度+协议名+协议类的jason字符串
//写入队列writeQueue
ByteArray ba = new ByteArray(sendBytes);
int count = 0; //writeQueue的长度
lock (writeQueue)
{
writeQueue.Enqueue(ba);
count = writeQueue.Count;
}
//send,只有在写入队列里的数量为1的时候,才会调用BeginSend发送数据
//这样做的目的是控制发送的数据,不同时发送多条数据,导致混乱
//count == 1说明当前并没有正在发送的消息,因为这个1就是刚刚加进去的
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:结束挂起的异步发送,如果成功,则将返回向 Socket 发送的字节数;否则会返回无效 Socket 错误。
int count = socket.EndSend(ar);
//获取写入队列第一条数据
ByteArray ba;
lock (writeQueue)
{
ba = writeQueue.First();
}
//完整发送
ba.readIdx += count;
if (ba.length == 0)
{
//ba.length == 0说明当前的这个ByteArray的所有数据已经发送完毕了
//然后就获取队列里的下一条数据。如果下一条数据不为空,则下面会继续调用BeginSend发送数据
//这里不需要判断writeQueue.Count是否为1,因为同一时间只会处理发送一条消息,也就是这个ba,
//此时ba已经发送完毕,可以直接获取下一条:ba = writeQueue.First();
lock (writeQueue)
{
writeQueue.Dequeue();
ba = writeQueue.First();
}
}
//如果前面ba.length != 0那就会继续发送ba剩余的数据,
//继续发送
if (ba != null)
{
socket.BeginSend(ba.bytes, ba.readIdx, ba.length,
0, SendCallback, socket);
}
//正在关闭
else if (isClosing)
{
//ba == null说明写入队列里面的消息已经发送完毕
socket.Close();
}
}
//添加消息监听
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);
}
}
//Update
public static void Update()
{
MsgUpdate();
PingUpdate();
}
//更新消息,用于处理消息队列里面的消息
public static void MsgUpdate()
{
//初步判断,提升效率
if (msgCount == 0)//没有消息之间return
{
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;
}
}
}
//心跳机制
//游戏程序一般都会自行实现心跳机制。具体来说就是,客户端会定时(如30秒)给服务端发送PING协议,
//服务端收到后会回应PONG协议。正常情况下,客户端每隔一段时间(如30秒)
//必然会收到服务端的PONG协议(就算网络不通畅,最慢120秒也总该收到了吧)。
//如果客户端很长时间(如120秒)没有收到PONG协议,很大概率是网络不通畅或服务端挂掉,客户端程序可以释放Socket资源。
//发送PING协议
private static void PingUpdate()
{
//是否启用
if (!isUsePing)
{
return;
}
//发送PING
if (Time.time - lastPingTime > pingInterval)
{
MsgPing msgPing = new MsgPing();
Send(msgPing);
lastPingTime = Time.time;
}
//检测PONG时间
if (Time.time - lastPongTime > pingInterval * 4)
{
Close();
}
}
//监听PONG协议
private static void OnMsgPong(MsgBase msgBase)
{
lastPongTime = Time.time;
}
}