【学习总结】通用客户端网络模块

本文详细介绍了Unity3D网络游戏编程中的网络模块设计,包括NetManager类、ByteArray类、网络事件模块、连接服务端、关闭连接、协议编码解码以及心跳机制。NetManager作为核心类,负责数据的发送与接收,ByteArray优化了内存管理,网络事件模块处理连接状态,协议编码解码实现了高效的数据传输,心跳机制确保了连接的稳定。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

内容摘自最近继续看的《Unity3D网络游戏编程》

NetMannger和ByreArray综述

同前文,网络模块的核心静态类是NetManager,它将提供一些基础的网络方法。
NetManager基于异步Socket实现,异步Socket回调函数把收到的消息按顺序存入队列msgList中。Update方法依次读取消息,再跟几乎监听表和协议名,调用相应的处理方法。
网络模块分为两个部分,第一部分是框架部分framework。framework包含网络管理器NetManager、为提高运行效率使用的ByteArray缓冲区、以及协议基类MsgBase。第二部分是协议类,它定义了客户端和服务器通信的数据格式。
建立NetManager类,声明一些关键变量:
在这里插入图片描述
其中ByteArray类的实现方法也与前文相同:

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

//封装byte[],readIdx和length的类
public class ByteArray
{
    //默认大小
    const int DEFAULT_SIZE = 1024;
    //初始大小
    int initSize = 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;} }

    //构造函数1
    public ByteArray(int size = DEFAULT_SIZE)
    {
        bytes = new byte[size];
        capacity = size;
        initSize = size;
        readIdx = 0;
        writeIdx = 0;
    }
    //构造函数2
    public ByteArray(byte[] defaultBytes){
        bytes = defaultBytes;
        capacity = defaultBytes.Length;
        initSize = defaultBytes.Length; 
        readIdx = 0;
        writeIdx = defaultBytes.Length;
    }
    //重设尺寸,通过该方法控制缓冲区扩展
    public void ReSize(int size){
        if(size<length) return;
        if(size<initSize) return;
        int n = 1;
        while(n<size) n*=2;//以两倍扩展
        capacity = n;
        byte[] newBytes = new byte[capacity];
        Array.Copy(bytes,readIdx,newBytes,0,writeIdx-readIdx);
        bytes = newBytes;
        writeIdx = length;
        readIdx = 0;
    }
    //当数据量很少时可以通过移动数据的方式,而不用拓展
    public void CheckAndMoveBytes(){
        if(length < 8){
            MoveBytes();
        }
    }
    public void MoveBytes(){
        if(length > 0){
            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;
    }
    //读取INT16
    public Int16 ReadInt16(){
        if(length<2) return 0;
        Int16 ret = (Int16)((bytes[readIdx + 1]<<8) | bytes[readIdx]);
        readIdx += 2;
        CheckAndMoveBytes();
        return ret;
    }
    //读取Int32
    public Int32 ReadInt32(){
        if(length<4) return 0;
        Int32 ret = (Int32) ((bytes[readIdx +3]<<24)|
                            (bytes[readIdx +2]<<16)|
                            (bytes[readIdx +1]<<8) |
                            bytes[readIdx +0]);
        readIdx += 4;
        CheckAndMoveBytes();
        return ret;
    }
}

稍微回顾以下ByteArray类做了哪些事情:

  1. 配合发送队列Queue< ByteArray >使用,避免同时发送多条数据导致的混乱。在代码中判定当前数据发送是否发送完毕,若没有发送完毕则再次调用BeginSend发送数据,如果发送完毕则删除该条数据,继续发送写入队列后续的数据
  2. 为了在缓冲区剩余长度不足以装下所有的数据时自动扩展缓冲区
  3. 当读取指针读取到缓冲区末尾时,自动调用移动数据方法,从而节约每一次读取都要移动数据区的性能。这样做可以让该调用接近O(1)的复杂度

网络事件模块

网络管理器需要提供“连接成功” “连接失败”和“断开连接”三种事件的回调。使用枚举方法定义这三种事件。
在这里插入图片描述
使用委托和字典创建事件委托类型和事件监听列表

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

同时给监听列表写好添加和删除事件,需要注意的是在删除事件的时候,如果不使用Remove方法区移除事件,那么尽管对象的值为null,eventListener.ContainsKey还是会返回true。

    //添加事件监听
    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);
            }
        }
    }

监听完成之后,我们还需要将添加分发事件的方法FireEvent,保证将传递过来的字符串传递给回调方法。

    //分发事件
    private static void FireEvent(NetEvent netEvent,String err){
        if(eventListeners.ContainsKey(netEvent)){
            eventListeners[netEvent](err);
        }
    }

连接服务端

连接服务端的Connect方法应当接收两个参数,分别代表服务端的IP地址和端口。Connect方法的核心是调用BeginConnect发起连接,它还会将Socket参数的NoDelay设置为true,表示不使用Nagle算法
Nagle算法指的是:如果发送端欲多次发送包含少量字节的数据包时,发送端不会立马发送数据,而是积攒到了一定数量后再将其组成一个较大的数据包发送出去。
同时,需要一个状态量标识当前是否在连接,程序设置了isConnecting成员,它代表当前是否处于正在连接的状态,即调用BeginConnect但回调函数尚未返回的阶段。Connect会做出各种状态判断:当连接成功时,它会阻止再次连接;若Socket处于“连接中”,则不能再次发起连接,代码如下:

    //连接
    public static void Connect(string ip,int port)//两个参数分别为IP地址和端口号
    {
        //状态判断
        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);
    }

调用的InitState,其功能是重置缓冲区等成员变量。比如遇到这种情况:客户端连接服务端之后开始接收数据,一段事件后断开,断开时读取缓冲区readBuff可能还有未处理的数据。客户端调用Connect重新连接后,readBuff还有之前未处理的数据,很显然这不是我们需要的,所以需要重置缓冲区

    //初始化所有变量,为的是清理上一次可能留下的数据残留
    public static void InitState()
    {
        //Socket
        socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
        //接收缓冲区
        readBuff = new ByteArray();
        //写入队列
        writeQueue = new Queue<ByteArray>();
        //是否正在连接
        isConnecting = false;
    }

而Connect的回调函数则需要处理三个事项:

  1. 将可能抛出异常的代码放在try-catch中,用于捕获异常
  2. 连接不论失败或者成功,都需要调用FireEvent分发对应的事件
  3. 将标识“连接中”的变量isConnecting设置为False,因为它只标识正在连接中,连接成功之后会有Socket.Connected
    //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;
        }
        catch(SocketException ex){
            Debug.Log("Socket Connect fail" + ex.ToString());
            FireEvent(NetEvent.ConnectFail,ex.ToString());
            isConnecting = false;
        }
    }

关闭连接

当客户端需要关闭连接的时候,程序并不会直接关闭连接,而是判断写入队列是否还有数据,如果还有数据,会等待数据发送完毕再关闭连接。因此,我们也需要一个布尔值isClosing来标识程序是否处于关闭中的状态。也要记得在InitState中清空初始值。
关闭连接方法Close需要做以下判断:只有在连接建立后才能关闭,然后依据写入队列writeQueue的长度判断是否需要延迟关闭。如果需要延迟关闭,设置状态位isClosing,等待发送数据的回调函数去处理。否则,调用socket.close()关闭连接,再调用FireEvent(NetEvent.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,"");  
        }
    }

使用协议

使用协议一是为了传输的时候可以更加高效,二是为了开发人员方便操作,也方便查看协议内部有哪些变量。常见的协议有Json,protobuf等。而一个协议类的核心功能在于将一个协议对象转换成二进制数据(发送方),再把二进制数据转换成协议对象(接收方)
因此我们需要一个协议类来统一处理这些消息,一方面是为了实现处理消息的统一接口,另一个目的是方便实现Send方法。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MsgBase
{
    //协议名
    public string protoName = "";
}

之后的所有协议继承这个类即可,比如添加一个协议文件BattleMsg,再里面定义MsgMove和MsgAttack两个类

public class MsgMove : MsgBase
{
    public MsgMove(){protoName = "MsgMove";}

    public int x = 0;
    public int y = 0;
    public int z = 0;
}

public class MsgAttack : MsgBase
{
    public MsgAttack(){protoName = "MsgAttack";}
    public string desc = "127.0.0.1:6543";
}

协议体的编码和解码

在MsgBase中添加静态方法Encode和Decode,后续只要解析出Json字符串所在的缓冲区位置和协议名,再使用“MsgBase.Decode(协议名,缓冲区,起始位置,长度)”即可获得协议对象,编码方法Encode包含一个协议体对象参数,程序使用JsonUtility.ToJson将协议体转化成字符串,再使用“System.Text.Encoding.UTF8.GetString”将字符串转化成byte数组。解码方法会先使用“System.Text.Encoding.UTF8.GetString”将Byte数组中的部分数据解析成字符串,再使用JsonUtility.FromJson将字符串还原成指定类型的协议对象。程序使用“Type.GetTypr(protoName)”获取协议名对应的类型。

    //编码
    public static byte[] Encode(MsgBase msgBase){
        string s = JsonUtility.ToJson(msgBase);
        return System.Text.Encoding.UTF8.GetBytes(s);
    }
    //解码
    public static MsgBase Decode(string protoName,byte[] bytes,int offset,int count){
        string s = System.Text.Encoding.UTF8.GetString(bytes,offset,count);
        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;
    }
    //解码协议名(2字节长度+字符串)
    public static string DecodeName(byte[] bytes,int offset,out int count)
    {
        count = 0;
        //必须大于两个字节
        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 = System.Text.Encoding.UTF8.GetString(bytes,offset+2,len);
        return name;
    }

发送数据

Send方法

NetManager类中另一个重要的方法就是发送数据,上文中的编码和解码都是为了发送而准备的。
Send方法需要完成的事情有:

  1. 进行一系列的状态判断,判断是否可以发送数据(当前是否连接,是否正在连接,是否已经关闭)
  2. 编码组装数据。虽然我们有了编码和解码方法,但是还需要将其协议名和协议体组装到一起,并且加上长度信息,复制到最终的发送缓冲中才算完成任务。
  3. 将其添加到发送队列之中,并且当发送队列长度为一的时候进行发送
    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[len + 2];
        //组装长度,封装长度信息
        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;//writeQueue的长度
        lock(writeQueue){//加锁,避免异步线程错误
            writeQueue.Enqueue(ba);
            count = writeQueue.Count;
        }
        //send
        if(count == 1){
            socket.BeginSend(sendBytes,0,sendBytes.Length,0,SendCallback,socket);
        }
    }

SendCallback

回调方法主要做的事情是判断写入队列是否还有数据,如果写入队列不为空,它会继续调用BeginSend发送数据。

    //Send回调
    public static void SendCallback(IAsyncResult ar){
        //获取state,EndSend的处理
        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();
        }
    }

消息事件

消息事件就是用来处理不同协议的回调方法,与网络事件比较类似,只不过它是通过协议名称去分发消息的

    //消息委托类型
    public delegate void MsgListener(MsgBase msgBase);
    //消息监听列表
    private static Dictionary<string,MsgListener> msgListeners = new Dictionary<string, MsgListener>();
    //添加消息监听
    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);
        }
    }

接收数据

接收数据的过程和前篇介绍的过程类似:回调函数ReceiveCallback会将消息存放到消息列表msgList中,主线程Update会读取消息列表,再一条一条进行处理。这里还将做出一些改进:

  1. 定义MAX_MESSAGE_FIRE指示每一帧处理多少条信息,控制处理
  2. 添加粘包半包、大小端判断等处理
  3. 使用Json协议,使得后续开发时,无需关注协议的格式
    //消息列表
    static List<MsgBase> msgList = new List<MsgBase>();
    //消息列表长度
    static int msgCount = 0;
    //每一次Update处理的信息量,readonly代表只读属性
    readonly static int MAX_MESSAGE_FIRE = 10;

同时要记得在初始化函数中将相关变量清理,同时需要在异步连接方法成功后开启异步接受方法

//开启接收
            socket.BeginReceive(readBuff.bytes,readBuff.writeIdx,readBuff.remain,0,ReceiveCallback,socket);

异步接收回调ReceiveCallback

ReceiveCallback会判断是否成功接收到数据,如果收到FIN信号,就会断开连接,如果收到正常的数据,那么他就会更新缓冲区的writeIdx,再调用OnReceiveData处理消息,OnReceiveData会解析协议,并且把协议对象放置到消息列表msgList中,在ReceiveCallback的最后,它将再次调用BeginReceive,并且开启下一轮的数据接收

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

OnReceiveData

OnReceiveData有两个功能。其一是根据协议的前两个字节判断是否接收到一条完整的协议。如果接收到完整协议,便解析它。如果没有接收完整协议,则退出等待下一波消息。其二是解析协议,解析出协议对象,然后通过“msgList.Add(msgBase)”将协议对象添加到协议列表中。添加到协议列表之前,程序使用“lock(msgList)”锁住了消息列表,这是因为OnReceiveData在子线程将数据写入消息队列,而Update在主线程读取消息队列,为了避免线程冲突,对msgList的操作都需要加锁。

    //数据处理
    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();
        }
    }

Update

Update需要做如下的处理:

  1. 现根据msgCount是否为0判断需不需要处理消息。如果msgCount为0,说明消息列表可能为空(因为多线程冲突所以是可能为空),则不需要乡下运行,提高程序运行效率。另一种做法是使用lock再判断,但是这样会增加主线程的等待时间,所以一般不使用锁
  2. 程序会使用for循环来做消息读取,并且循环次数小于每帧设定的消息处理数量最大值MAX_MESSAGE_FIRE。在每一次循环中,程序都会所著msgList,然后取出它的第一条数据,最后调用FireMsg分发消息
    //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(msgList){
                if(msgList.Count > 0){
                    msgBase = msgList[0];
                    msgList.RemoveAt(0);
                    msgCount--;
                }
            }
            //分发消息
            if(msgBase != null){
                FireMsg(msgBase.protoName,msgBase);
            }
            //没有消息了
            else{
                break;
            }
        }
    }

心跳机制

心跳机制是确保服务端资源能正常释放的机制。如果玩家拿着手机进入没有信号的山区,或者有人拿着剪刀剪断网线,都会导致链路不通。如果超过一定的时长,则需要服务端和客户端主动断开连接以节约资源。具体来说就是,客户端会定时发送PING协议给服务端,服务端收到之后会回复PONG协议。如果客户端很长时间都没有接收到PONG协议,则说明链路可能不通畅,这时客户端这边可以主动释放Socket资源。这其实对服务端更重要一些,因为服务端空余的Socket的资源可以支撑更多的玩家同时游玩。
在Proto下建立SysMsg文件,在文件里新建MsgPing和MsgPong两个类,同时在NetMannager中声明相关变量,同时要记得在初始化方法中重置。

    //是否启用心跳机制
    public static bool isUsePing = true;
    //心跳间隔时间
    public static int pintInterval = 30;
    //上一次发送Ping的时间
    static float lastPingTime = 0;
    //上一次收到PONG的时间
    static float lastPongTime = 0;

发送Ping协议

在NetManager中编写发送MsgPing协议的PingUpdate方法,并在Update中调用它。PingUpdate应当实现下列三种功能:

  1. 根据isUsePing判断是否使用心跳机制,如果没有开启,则直接跳过
  2. 判断当前时间与上一次发送MsgPing协议的时间间隔,如果超过指定时间,则调用Send(msgPing)向服务端发送MsgPing协议
  3. 判断当前时间与上一次接收MsgPong协议的时间间隔,如果超过指定时间,则调用Close关闭连接
    //心跳机制PingUpdate
    private static void PingUpdate(){
        //是否启用
        if(!isUsePing){
            return;
        }
        //发送Ping
        if(Time.time - lastPingTime > pintInterval){
            MsgPing msgPing = new MsgPing();
            Send(msgPing);
            lastPingTime = Time.time;
        }
        //检测PONG时间
        if(Time.time - lastPongTime > pintInterval*4){
            Close();
        }
    }

核心类完整代码

NetMannager

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

public class NetManager : MonoBehaviour
{
    //该枚举用于区分监听方法
    public enum NetEvent
    {
        ConnectSucc = 1,
        ConnectFail = 2,
        Close = 3,
    }

    //是否正在连接的布尔值
    static bool isConnecting = false;

    //是否处于"关闭中"
    static bool isClosing = false;

    //定义套接字
    static Socket socket;
    //接收缓冲区
    static ByteArray readBuff;
    //写入队列
    static Queue<ByteArray> writeQueue;

    //是否启用心跳机制
    public static bool isUsePing = true;
    //心跳间隔时间
    public static int pintInterval = 30;
    //上一次发送Ping的时间
    static float lastPingTime = 0;
    //上一次收到PONG的时间
    static float lastPongTime = 0;

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

    //事件委托类型
    public delegate void EventListener(String err);
    //事件监听列表
    private static Dictionary<NetEvent,EventListener> eventListeners = new Dictionary<NetEvent, EventListener>();
    
    //添加事件监听
    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);
        }
    }

    //消息委托类型
    public delegate void MsgListener(MsgBase msgBase);
    //消息监听列表
    private static Dictionary<string,MsgListener> msgListeners = new Dictionary<string, MsgListener>();
    //添加消息监听
    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);
        }
    }


    //连接
    public static void Connect(string ip,int port)//两个参数分别为IP地址和端口号
    {
        //状态判断
        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);
    }

    //关闭连接
    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 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;
        //重置心跳机制的记录时间
        lastPingTime = Time.time;
        lastPongTime = Time.time;
        //监听PONG协议
        if(!msgListeners.ContainsKey("MsgPong")){
            AddMsgListener("MsgPong",OnMsgPong);
        }
    }

    //监听Pong协议,更新lastPongTime方法
    private static void OnMsgPong(MsgBase msgBase){
        lastPongTime = Time.time;
    }

    //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);
        }
        catch(SocketException ex){
            Debug.Log("Socket Connect fail" + ex.ToString());
            FireEvent(NetEvent.ConnectFail,ex.ToString());
            isConnecting = false;
        }
    }

    //发送数据
    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[len + 2];
        //组装长度,封装长度信息
        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;//writeQueue的长度
        lock(writeQueue){//加锁,避免异步线程错误
            writeQueue.Enqueue(ba);
            count = writeQueue.Count;
        }
        //send
        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
        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();
        }
    }

    //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(){
        //消息长度
        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();
        }
    }

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

    //心跳机制PingUpdate
    private static void PingUpdate(){
        //是否启用
        if(!isUsePing){
            return;
        }
        //发送Ping
        if(Time.time - lastPingTime > pintInterval){
            MsgPing msgPing = new MsgPing();
            Send(msgPing);
            lastPingTime = Time.time;
        }
        //检测PONG时间
        if(Time.time - lastPongTime > pintInterval*4){
            Close();
        }
    }
}

MsgBase

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class MsgBase
{
    //协议名
    public string protoName = "";
    //编码
    public static byte[] Encode(MsgBase msgBase){
        string s = JsonUtility.ToJson(msgBase);
        return System.Text.Encoding.UTF8.GetBytes(s);
    }
    //解码
    public static MsgBase Decode(string protoName,byte[] bytes,int offset,int count){
        string s = System.Text.Encoding.UTF8.GetString(bytes,offset,count);
        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;
    }
    //解码协议名(2字节长度+字符串)
    public static string DecodeName(byte[] bytes,int offset,out int count)
    {
        count = 0;
        //必须大于两个字节
        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 = System.Text.Encoding.UTF8.GetString(bytes,offset+2,len);
        return name;
    }
}

ByteArray

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

//封装byte[],readIdx和length的类
public class ByteArray
{
    //默认大小
    const int DEFAULT_SIZE = 1024;
    //初始大小
    int initSize = 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;} }

    //构造函数1
    public ByteArray(int size = DEFAULT_SIZE)
    {
        bytes = new byte[size];
        capacity = size;
        initSize = size;
        readIdx = 0;
        writeIdx = 0;
    }
    //构造函数2
    public ByteArray(byte[] defaultBytes){
        bytes = defaultBytes;
        capacity = defaultBytes.Length;
        initSize = defaultBytes.Length; 
        readIdx = 0;
        writeIdx = defaultBytes.Length;
    }
    //重设尺寸,通过该方法控制缓冲区扩展
    public void ReSize(int size){
        if(size<length) return;
        if(size<initSize) return;
        int n = 1;
        while(n<size) n*=2;//以两倍扩展
        capacity = n;
        byte[] newBytes = new byte[capacity];
        Array.Copy(bytes,readIdx,newBytes,0,writeIdx-readIdx);
        bytes = newBytes;
        writeIdx = length;
        readIdx = 0;
    }
    //当数据量很少时可以通过移动数据的方式,而不用拓展
    public void CheckAndMoveBytes(){
        if(length < 8){
            MoveBytes();
        }
    }
    public void MoveBytes(){
        if(length > 0){
            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;
    }
    //读取INT16
    public Int16 ReadInt16(){
        if(length<2) return 0;
        Int16 ret = (Int16)((bytes[readIdx + 1]<<8) | bytes[readIdx]);
        readIdx += 2;
        CheckAndMoveBytes();
        return ret;
    }
    //读取Int32
    public Int32 ReadInt32(){
        if(length<4) return 0;
        Int32 ret = (Int32) ((bytes[readIdx +3]<<24)|
                            (bytes[readIdx +2]<<16)|
                            (bytes[readIdx +1]<<8) |
                            bytes[readIdx +0]);
        readIdx += 4;
        CheckAndMoveBytes();
        return ret;
    }
}

test

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class test : MonoBehaviour
{
    //开始
    void Start()
    {
        NetManager.AddEventListener(NetManager.NetEvent.ConnectSucc,OnConnectSucc);
        NetManager.AddEventListener(NetManager.NetEvent.ConnectFail,OnConnectFail);
        NetManager.AddEventListener(NetManager.NetEvent.Close,OnConnectClose);
        NetManager.AddMsgListener("MsgMove",OnMsgMove);
    }

    //玩家点击连接按钮
    public void OnConnectClick(){
        NetManager.Connect("127.0.0.1",8888);
    }

    //玩家点击断开连接按钮
    public void OnCloseClick(){
        NetManager.Close();
    }
    //玩家点击移动按钮
    public void OnMoveClick(){
        MsgMove msg = new MsgMove();
        msg.x = 120;
        msg.y = 123;
        msg.z = -6;
        NetManager.Send(msg);
    }
    
    //回调函数
    //连接成功回调
    void OnConnectSucc(string err)
    {
        Debug.Log("OnConnectSucc");
    }
    //连接失败回调
    void OnConnectFail(string err)
    {
        Debug.Log("OnConnectFail" + err);
    }
    //关闭连接
    void OnConnectClose(string err)
    {
        Debug.Log("OnConnectClose");
    }

    //收到MsgMove协议
    public void OnMsgMove(MsgBase msgBase){
        MsgMove msg = (MsgMove)msgBase;
        //消息处理
        Debug.Log("OnMsgMove msg.x = " + msg.x);
        Debug.Log("OnMsgMove msg.y = " + msg.y);
        Debug.Log("OnMsgMove msg.z = " + msg.z);
    }

    //update
    public void Update(){
        NetManager.Update();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值