《Unity3D网络游戏实战》第六章:通用客户端网络模块

本文详细描述了Unity中的网络模块设计,包括NetManager类的功能、异步Socket的使用、MsgBase基础类的作用以及心跳机制的实现,展示了如何通过这些组件进行网络通信和事件处理。
摘要由CSDN通过智能技术生成

一、网络模块设计

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

  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 《Unity3D网络游戏实战PDF》是一本非常重要的Unity3D开发书籍,它着重介绍了Unity3D游戏开发的网络部分。本书包含了网络游戏的核心概念、原理及实现方法,深入剖析了网络游戏的架构设计模式、游戏性能优化、安全防护等方面内容,并提供了实际的案例分析和设计思路,是网络游戏开发者的必备指南。 本书主要包括了以下内容:网络游戏的基础知识,如网络通信、协议、数据传输、数据整合等;Unity3D网络游戏的基本架构设计,如服务器端和客户端的架构设计、消息通信机制等;Unity3D游戏性能优化技巧,如消息压缩、消息缓存、负载均衡等;网络游戏的安全防护策略,如加密算法、防作弊、账号安全等;最后,作者还介绍了如何进行网络游戏的调试及问题排除。 《Unity3D网络游戏实战PDF》书籍内容深入浅出,适合初学者和中级开发者阅读。它提供了实用的方法和工具,帮助读者掌握网络游戏开发的技能,提高游戏开发的效率和质量。无论你是想开发网络游戏,还是想学习Unity3D游戏开发,本书都是不容错过的开发指南。 ### 回答2: unity3d网络游戏实战pdf是一本讲解使用Unity3D引擎开发网络游戏实战教程,内容涵盖了Unity3D的基础知识、网络编程与多人联机游戏设计。本书从基础开始,逐步讲解如何使用Unity3D引擎搭建网络游戏,包括如何进行多人联机游戏设计、实现网络通信,以及如何利用Unity3D引擎的特性实现游戏的界面设计和多人对战功能。本书不仅具有理论知识,更有大量实例和案例,可以帮助读者深刻了解Unity3D引擎的应用,提高网络游戏开发的技能和水平。这本书适合想要学习和掌握Unity3D引擎开发网络游戏的初、中级程序员、游戏开发者和爱好者阅读使用,可以帮助他们快速地了解并掌握开发网络游戏的流程和技巧。总之,如果你正在寻找一本全面且系统的Unity3D网络游戏开发教程,那么这本《unity3d网络游戏实战pdf》将是你的不二选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

buzhengli

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值