[学习笔记][Unity3D网络游戏实战]客户端基本网络框架

目标:一个客户端基本的网络框架

目录(框架的结构可以直接看目录吧)

网络模块设计:

1.主体框架部分NetManager

基本部分

网络事件

委托事件类型

监听列表

触发监听

具体网络事件的实现

1.Connet

2.Close 

3.Send(该事件需要先实现协议类) 

消息事件

监听列表

接收数据Receive

更新数据Update

2.协议类

个人理解结构

Json协议

协议格式

协议文件(消息类)

协议体/协议名的编码解码

3.ByteArray(消息的数据结构,提高效率)

尾声

网络模块设计:

1.主体框架部分NetManager

该部分将有关网络通讯部分的功能封装,方便后续调用。

主要利用观察者模式,让框架拥有松耦合,灵活性,可拓展性的特性。

整合了客户端连接服务器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("不存在此事件");
        }
    }
   
具体网络通讯功能的实现
1.Connet

具体实现:

#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
2.Close 
#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
3.Send(该事件需要先实现协议类) 

先进行条件检测,在对数据编码,组装长条,组装名字,组装消息体

#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
接收数据Receive

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
更新数据Update

得到的消息存储在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

2.协议类

管理协议编码与解码的功能。

个人理解:就是把对得到数据的从对象到byte数组转化,封装到此类中,方便调用,可以方便扩展更多不同消息协议。

结构:消息基类、继承消息的具体消息内容子类。

个人理解结构

消息的发送需要经历以下步骤

对象 <--json方法解析--> 字符串 <--编码/解码--> 比特数组

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;

    }
}

3.ByteArray(消息的数据结构,提高效率)

懒得写描述了哈哈,直接看实现吧

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协议。

你可能感兴趣的:(Socket网络编程,学习,笔记,网络)