目录(框架的结构可以直接看目录吧)
网络模块设计:
1.主体框架部分NetManager
基本部分
网络事件
委托事件类型
监听列表
触发监听
具体网络事件的实现
1.Connet
2.Close
3.Send(该事件需要先实现协议类)
消息事件
监听列表
接收数据Receive
更新数据Update
2.协议类
个人理解结构
Json协议
协议格式
协议文件(消息类)
协议体/协议名的编码解码
3.ByteArray(消息的数据结构,提高效率)
尾声
该部分将有关网络通讯部分的功能封装,方便后续调用。
主要利用观察者模式,让框架拥有松耦合,灵活性,可拓展性的特性。
整合了客户端连接服务器Connect,关闭与服务器的连接Close,想服务器发送消息Send等基本网络通讯功能。
下面是具体实现逻辑
Socket:连接套接字
readBuff:接受消息的缓冲区
writeQueue:发送消息的写入队列
isClosing :记录是否连接
private static Socket socket;
//接收缓存区
private static ByteArray readBuff;
//写入队列
private static Queue writeQueue;
private static bool isClosing = false;
这些属性是NetManager的基本字段,十分重要。是实现网络通讯的基础。
处理基本的网络事件,如Connect,Close,Send。
使用观察者模式,也就是采用事件驱动模式。
在这种模式中,存在一个发布者(Subject)和多个订阅者(Observers),订阅者会注册(监听)某些事件,当事件发生时,所有订阅该事件的订阅者都会被通知并触发相应的处理逻辑。
首先利用一个枚举类型NetEvent,存储常用的网络事件名称,该类型作为监听器的key值,用来触发对应事件,方便后续调用。
(其实当然也可以用其他类型,来定义监听器的key值,比如直接用string注册,但是不方便调用,可能会出错)
public enum NetEvent
{
ConnectSucc = 1,
ConnectFail = 2,
Close = 3,
}
1.定义委托类型EventListener(参数为string),用于触发多个回调方法。
作为监听者的模板,在事件触发的时候,监听该事件的监听者被会触发。
2.定义一个事件列表,为字典类型
3.实现添加事件监听者与删除事件监听者的方法。
其中的if/else逻辑是:
如果这个事件不存在就创建一个事件,并为其添加监听者。如果已经存在,就在原事件上再添加监听者。
具体源代码:
#region 事件处理
//事件委托类型
public delegate void EventListener(string err);
//事件监听列表
private static Dictionary
eventListeners = new Dictionary();
//添加事件监听
public static void AddEventListener(NetEvent netEvent, EventListener listener)
{
if (eventListeners.ContainsKey(netEvent))
{
eventListeners[netEvent] += listener;
}
else
{
eventListeners[netEvent] = listener;
}
}
//删除事件监听
public static void RemoveEventListenner(NetEvent netEvent, EventListener listener)
{
if (eventListeners.ContainsKey(netEvent))
{
eventListeners[netEvent] -= listener;
}
else
{
eventListeners.Remove(netEvent);
}
}
#endregion
方法需要一个网络事件类型,和一个字符串(用于作为被触发的监听器的参数)
//分发(触发)事件
public static void FireEvent(NetEvent netEvent, string err)
{
if (eventListeners.ContainsKey(netEvent))
{
eventListeners[netEvent](err);
}
else
{
throw new Exception("不存在此事件");
}
}
具体实现:
#region Connect
private static bool isConnecting = false;
public static void Connect(string ip, int port)
{
//状态判断
if (socket != null && socket.Connected)
{
Debug.Log("Connect fail,is already connected");
}
if (isConnecting)
{
Debug.Log("Connect fail is connecting");
}
InitState();
socket.NoDelay = true;
socket.BeginConnect(ip, port,ConnectCallback, socket);
}
private static void ConnectCallback(IAsyncResult ar)
{
try
{
Socket socket = ar.AsyncState as Socket;
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;
}
}
//初始化状态
private static void InitState()
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
readBuff = new ByteArray();
writeQueue = new Queue();
isConnecting = false;
isClosing = false;
}
#endregion
#region 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,"");
}
}
#endregion
先进行条件检测,在对数据编码,组装长条,组装名字,组装消息体
#region 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[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);
//写入队列
ByteArray ba = new ByteArray(sendBytes);
int count = 0;
lock (writeQueue)
{
writeQueue.Enqueue(ba);
count = writeQueue.Count;
}
//send
if (count == 1)
{
socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket);
}
}
public static void SendCallback(IAsyncResult ar)
{
//获取state EndSend处理
Socket socket = ar.AsyncState as Socket;
//状态判断
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(9);
}
}
#endregion
根据协议名称去分发信息
可以实现
1.每次update处理多条数据、
2.处理粘包半包、大小端判断、
3.使用json协议
一个用来记录并在合适的时候根据提供的string与MsgBase来调用方法的委托回调,与上文的网络事件类似。
1.定义委托类型
2.定义一个事件列表,为字典类型,用于记录每种网络事件对应的回调方法
3.实现添加事件与删除事件
#region 消息事件
//消息委托类型
public delegate void MsgListener(MsgBase msgBase);
//消息监听列表
public static Dictionary MsgListeners = new Dictionary();
public static void AddMsgListener(string msgName, MsgListener msgListener)
{
//添加
if (MsgListeners.ContainsKey(msgName))
{
MsgListeners[msgName] += msgListener;
}
//新增
else
{
MsgListeners[msgName] = msgListener;
}
}
public static void RemoveMsgListener(string msgName, MsgListener msgListener)
{
if (MsgListeners.ContainsKey(msgName))
{
MsgListeners[msgName] -= msgListener;
//删除
if (MsgListeners[msgName] == null)
{
MsgListeners.Remove(msgName);
}
}
}
public static void FireMsg(string msgName, MsgBase msgBase)
{
if (MsgListeners.ContainsKey(msgName))
{
MsgListeners[msgName](msgBase);
}
}
#endregion
在connect中启动Receive来接收数据,使用消息队列记录消息
2.处理粘包半包、大小端判断、
3.使用json协议
#region 接收数据
//消息列表
private static List msgList = new List();
//消息列表长度
private static int msgCount = 0;
//每一次update处理的消息量
private readonly static int MAX_MESSAGE_FIRE = 10;
//初始化状态
private static void InitMsgListState()
{
msgList = new List();
msgCount = 0;
}
//Receive回调 在Connet中调用
public static void ReceiveCallBack(IAsyncResult ar)
{
try
{
Socket socket = ar.AsyncState as Socket;
//获取接收数据长度
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(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("Fail Decode protoName");
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();
}
}
#endregion
得到的消息存储在msgList后,需要不断更新判断,是否需要处理并处理消息。
需要在另一个带有monobehavior的类中,使用Update去调用NetManager.Update启动消息处理。
1.每次update处理多条数据、
#region 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 (msgBase)
{
if (msgList.Count>0)
{
msgBase = msgList[0];
msgList.RemoveAt(0);
msgCount--;
}
}
//分发消息
if (msgBase != null)
{
FireMsg(msgBase.protoName,msgBase);
}
//没有消息了
else
{
break;
}
}
}
#endregion
管理协议编码与解码的功能。
个人理解:就是把对得到数据的从对象到byte数组转化,封装到此类中,方便调用,可以方便扩展更多不同消息协议。
结构:消息基类、继承消息的具体消息内容子类。
消息的发送需要经历以下步骤
对象 <--json方法解析--> 字符串 <--编码/解码--> 比特数组
利用unity提供的JsonUtility来实现json协议的编码解码,
使用长度信息法,在消息前面加上2字节的长度,并接上协议名与协议体。防止粘包与半包
通过协议文件创建协议基类,实现统一接口,方便处理协议。(当作json编码解码的参数,提供转化标准)
以MsgBase为基类,派生出各种不同协议。
public class MsgMove : MsgBase
{
public MsgMove()
{
protoName = "MsgMove";
}
public int x = 0;
public int y = 0;
public int z = 0;
}
处理正常获得的数据的编码与解码
下述是完整的消息基类的代码实现
public class MsgBase
{
//协议名
public string protoName = " ";
//编码 对象->字符->bytes
public static byte[] Encode(MsgBase msgBase)
{
string s = JsonUtility.ToJson(msgBase);
return Encoding.UTF8.GetBytes(s); //以UTF8的格式(transform to 8 bit)转化为比特格式
}
//解码
public static MsgBase Decode(string protoName, byte[] bytes, int offset, int count)
{
string s = Encoding.UTF8.GetString(bytes,offset,count);
MsgBase msgBase = (MsgBase)JsonUtility.FromJson(s, Type.GetType(protoName));
return msgBase;
}
//bytes->字符->对象
public static byte[] EncodeName(MsgBase msgBase)
{
//名字与bytes长度
byte[] nameBytes = 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;
}
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 (len <= 0) return "";
//长度必须足够
if (offset + 2 + len > bytes.Length) return " ";
//解析
count = 2 + len;
string name = Encoding.UTF8.GetString(bytes, offset + 2, len);
return name;
}
}
懒得写描述了哈哈,直接看实现吧
public class ByteArray : MonoBehaviour
{
//默认大小
private const int DEFAULT_SIZE = 1024;
//初始大小
private int iniSize = 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; }
}
//构造函数
public ByteArray(int size = DEFAULT_SIZE)
{
bytes = new byte[size];
capacity = size;
iniSize = size;
readIdx = 0;
writeIdx = 0;
}
//构造函数
public ByteArray(byte[] defaultBytes)
{
bytes = defaultBytes;
readIdx = 0;
writeIdx = defaultBytes.Length;
}
public void ReSize(int size)
{
if(size0)
{
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;
}
}
上述是我对《Unity3D网络游戏实战》第三版第六章的个人笔记,用来梳理以下我对客户端网络模块结构的认识,更详细的可以自己买一本来学习。
后面还有一些模块没写如心跳机制,Protobuf协议。