Unity3D网络游戏实战——通用客户端模块

前言

书中说的是搭建一套商业级的客户端网络模块,一次搭建长期使用。
本章主要是完善大乱斗游戏中的网络模块,解决粘包分包、完整发送数据、心跳机制、事件分发等功能

6.1网络模块设计

核心是静态类NetManager,它对外提供了一系列方法

  • NetManager.Connect(ip,port)连接服务端
  • NetManager.Close()
  • NetManager.Send(msgMove)发送消息,参数为协议对象,NetManager会自动把它转换成二进制数据
  • NetManager.Update()需外部调用,用于驱动NetManager
  • NetManager.AddMsgListener(“MsgMove”,OnMsgMove)添加消息时间,收到MsgMove协议,就调用OnMsgMove方法
  • NetManager.AddEventListener(NetManager.NetEvent.ConnectSucc,OnConnectSucc),监听网络事件,该语句表示成功调用OnConnetSucc方法

6.1.2内部设计

NetManager基于异步Socket实现,异步socket回调函数把收到的消息按顺序存入消息队列msgList中。Update依次读取信息,根据监听表和协议名,调用相应方法
网络模块分为两个部分。
第一部分:框架模块包含NetManager、ByteArray、MsgBase
第二部分:协议类定义了客户端和服务端通信的数据格式,BattleMsg、SysMsg等

6.1.3code

NetManager的代码,基本都有注释。挑一些重点出来说

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net.Sockets;
using System;
using System.Linq;

public static class NetManager
{
    //不用协议名区分监听方法,用NetEvent来区分监听方法
    public enum NetEvent
    {
        //提供连接成功、失败、断开的回调
        ConnectSucc = 1,
        ConnectFail = 2,
        Close = 3,
    }
    //定义套接字
    static Socket socket;
    //接收缓冲区
    static ByteArray readBuff;
    //写入队列
    static Queue<ByteArray> writeQueue;

    //事件委托类型
    public delegate void EventListener(string err);
    //事件监听列表
    private static Dictionary<NetEvent, EventListener> eventListeners = new Dictionary<NetEvent, EventListener>();

    //消息委托类型,和上面的不一样,上面是用来监听事件的,这里是用来监听消息调用对应的回调函数的
    //和网络事件的添加监听删除监听一样,两者共同组成了NetManager的监听结构
    public delegate void MsgListener(MsgBase msgBase);
    //消息监听列表
    private static Dictionary<string, MsgListener> msgListeners = new Dictionary<string, MsgListener>();

    //消息列表
    static List<MsgBase> msgList = new List<MsgBase>();
    //消息列表长度
    static int msgCount = 0;
    //每一次Update处理的消息量
    readonly static int MAX_MESSAGE_FIRE = 10;

    //是否正在连接
    static bool isConnecting = false;

    //是否正在关闭
    static bool isClosing = false;

    //是否启用心跳
    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))
        {
            //value是委托,可以绑定相同类型的多个函数,后面这个参数就是和该委托类型相同的函数对象
            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 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)
    {
        //用协议名找到对应回调,传入msgBase作为参数调用
        if (msgListeners.ContainsKey(msgName))
        {
            msgListeners[msgName](msgBase);
        }
    }

    //连接
    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;
        //Connect
        isConnecting = true;
        socket.BeginConnect(ip, port, ConnectCallback, socket);
    }

    //初始化状态
    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;

        //消息列表,每次update取出固定长度消息处理
        msgList = new List<MsgBase>();
        //消息列表长度
        msgCount = 0;

        //上一次发送PING的时间
        lastPingTime = Time.time;
        //上一次收到PONG的时间
        lastPingTime = Time.time;
        //监听PONG协议
        if (!msgListeners.ContainsKey("MsgPong"))
        {
            AddMsgListener("MsgPong", OnMsgPong);
        }
    }

    //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;//连接成功了
            //开始接收,将网络数据读取到读缓冲区readBuff中
            socket.BeginReceive(readBuff.bytes, readBuff.readIdx, readBuff.remain, 0, ReceiveCallback, socket);
        }
        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 = (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();
        }
    }

    //关闭连接
    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);
        //写入队列
        ByteArray ba = new ByteArray(sendBytes);
        int count = 0;//write queue的长度
        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 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();
        }
    }

    //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;
            }
        }
    }

    //发送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)
    {
        //收到pong之后回调此函数更新lastPongTime
        lastPongTime = Time.time;
    }
}

Connect

定义isConnecting来判断是否处于正在连接状态,也就是调用BeginConnect但回调函数尚未返回的阶段。
Connect会做出各种判断,连接成功时,阻止再次连接;如果连接中,则不能再次发起连接。

在InitState中药重置缓冲区等成员变量,因为断开网络的时候读缓冲区readBuff可能还有未处理的数据,所以要在每次连接前,重置缓冲区。

Close

程序关闭连接时,会先判断写入队列writeQueue是否还有数据,如果还有数据,会等待数据发送完毕再关闭连接。
如果需要延迟关闭,就等待发送数据的回调函数去处理,否则就关闭连接,并用FireEvent分发连接关闭的事件。

6.5Json协议

6.5.1为什么会有协议类

之前我们是用字符串协议,但是要经过参数解析,非常麻烦。所以直接定义协议类,也就是编写两个接口后可以和二进制数据、字符串相互转换就很方便。
协议类的核心在于
编码:把一个协议对象转换成二进制数据
解码:把二进制数据转换成协议对象

本节主要使用Json协议,将协议对象转换成"{“x”:100,“y”:200,“z”:300}"的字符串。

6.5.2使用JsonUtility

Unity提供Json辅助类JsonUtility,通过ToJson和FromJson可以实现Json协议的编码和解码
也可以用FromJsonOverWrite(s,msgMove),直接给sting s赋值。

6.5.3协议格式

消息长度(2字节)协议名长度(2字节)协议名(由协议名长度确定)协议体

6.5.4协议文件

全部继承自MsgBase,所以协议都需要的就是协议名。

6.6发送数据

6.6.1Send

成功连接后服务器后,才能发送数据数据。

6.6.2SendCallback

此回调会判断写入队列是否还有数据,如果写入队列不为空,它会继续调用BeginSend发送数据。

6.7消息事件

给不同的协议添加不同的回调方法,比如给MsgMove协议添加OnMsgMove方法的监听等…
此处的消息监听列表也是字典,key、val分别是字符串和委托,再给他们加上添加监听和删除监听的函数即可。

6.8接收数据

回调函数ReceiveCallback会将消息存放到消息msgList,主线程Update读取消息列表,每次处理多条数据
且有处理粘包半包、大小端处理等判断
使用Json协议
用msgCount来减少对msgList的操作次数
OnReceiveData和Update位于不同线程,防止冲突,对msgList的操作需要加锁

6.9心跳机制

客户端定时(30s)发送PING协议,服务端回应PONG。很长时间没有收到PONG,客户端就释放Socket。
其实对于客户端来说,释放不释放关系不大,毕竟只有一个Socket,但是对于维护数以万计的连接的服务器来说关心就很大。

6.10Protobuf协议

6.10.1什么是Protobuf

Protobuf是google发布的一套协议,特点是编码后的数据量小,可以节省带宽。
Json的编码流程是编写协议类后,通过编码将协议对象转换成byte数组。
Protobuf流程是,编写描述文件.proto,根据第三方工具转成协议类,再通过编码为byte数组。
比Json麻烦,所以先不用!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值