《Unity网络游戏实战》Chapter6: 客户端网络架构

1、介绍

《Unity网络游戏实战》第六章介绍的一个通用的客户端网络架构。与前面章节中客户端的网络模块不同的地方有:
(1)编写协议类MsgBase,协议的编码解码采用json协议,简化打包协议和解析协议的接口。
(2)区别开连接服务器事件和处理服务器消息的事件,分别处理。
(3)实现心跳机制,判断客户端是否掉线。

2、客户端代码

MsgBase.cs

using UnityEngine;
using System;
using System.Linq;

public class MsgBase
{
    public string protoName = "null";

    public static int headSize = 4;

    // 编码
    public static byte[] Encode(MsgBase msgBase)
    {
        // 将消息转换成Json格式
        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);

        // 将字符串转换成对应协议名的Json格式
        MsgBase msgBase = (MsgBase)JsonUtility.FromJson(s, Type.GetType(protoName));

        return msgBase;
    }


    // 编码协议名
    public static byte[] EncodeName(MsgBase msgBase)
    {
        // 协议名bytes和长度
        byte[] nameBytes = System.Text.Encoding.UTF8.GetBytes(msgBase.protoName);
        int length = nameBytes.Length;

        // 组装
        byte[] lenBytes = BitConverter.GetBytes(length);

        if(!BitConverter.IsLittleEndian)
        {
            lenBytes.Reverse();
        }

        byte[] retBytes = lenBytes.Concat(nameBytes).ToArray();

        return retBytes;
    }

    // 解码协议名
    public static string DecodeName(byte[] bytes, int offset, out int count)
    {
        count = 0;

        // 判断字节数是否大于数据长度
        if (bytes.Length < headSize)
            return "";

        // 读取协议名长度
        int namelength = BitConverter.ToInt32(bytes, offset);

        // 判断长度是否足够解析协议名
        if (bytes.Length < headSize + namelength)
            return "";

        // 解析
        count = headSize + namelength;

        string msgName = System.Text.Encoding.UTF8.GetString(bytes, offset + headSize, namelength);

        return msgName;
    }
}

SyncMsg.cs


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

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

BattleMsg.cs


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 = NetManager.GetDesc();
}

NetWorkManager.cs

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

public class NetWorkManager : MonoBehaviour
{
    void Start()
    {
        NetManager.AddEventListener(NetManager.NetEvent.ConnectSucc, OnConnectSucc);
        NetManager.AddEventListener(NetManager.NetEvent.ConnectFail, OnConnectFail);
        NetManager.AddEventListener(NetManager.NetEvent.Close, OnClose);

        NetManager.AddMsgListener("MsgMove", OnMsgMove);
    }

    //连接成功回调
    void OnConnectSucc(string err)
    {
        Debug.Log("连接服务器成功");
        //TODO:进入游戏
    }

    void OnConnectFail(string err)
    {
        Debug.Log("连接服务器失败");
        //TODO:弹出提示框(连接失败,请重试)
    }

    void OnClose(string err)
    {
        Debug.Log("连接断开");
        //TODO:弹出提示框(网络断开)
        //TODO:弹出按钮(重新连接)
    }

    public void OnButtonConnectClick()
    {
        NetManager.Connect("127.0.0.1", 8888);
    }

    public void OnButtonCloseClick()
    {
        NetManager.Close();
    }

    public void OnButtonMoveClick()
    {
        MsgMove msg = new MsgMove();

        msg.x = 12;
        msg.y = 10;
        msg.z = 7;

        NetManager.Send(msg);
    }

    public void OnMsgMove(MsgBase msg)
    {
        MsgMove msgMove = (MsgMove)msg;

        // 打印坐标
        Debug.Log("x: " + msgMove.x);
        Debug.Log("y: " + msgMove.y);
        Debug.Log("z: " + msgMove.z);
    }

    private void Update()
    {
        NetManager.HandleMsg();
        NetManager.PingMsg();
    }
}

NetManager.cs

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

    static Queue writeQueue;

    static bool isConnecting = false;

    static bool isClosing = false;

    static int headSize = 4;

    public static bool isUsePing = true;

    public static int pingInterval = 3;

    static float lastPingTime = 0;

    static float lastPongTime = 0;


    public enum NetEvent
    {
        ConnectSucc = 1,
        ConnectFail = 2,
        Close = 3
    }

    // 事件委托
    public delegate void EventListener(string err);
    // 监听列表 --- 连接服务器的监听列表
    static Dictionary eventListener = new Dictionary();

    public static string GetDesc()
    {
        if (socket == null)
            return "";

        return socket.LocalEndPoint.ToString();
    }
    
    public static void AddEventListener(NetEvent netEvent, EventListener listener)
    {
        // 如果有则添加事件,否则新增事件
        if (eventListener.ContainsKey(netEvent))
            eventListener[netEvent] += listener;
        else
            eventListener[netEvent] = listener;
    }

    public static void RemoveEventListener(NetEvent netEvent, EventListener listener)
    {
        if (eventListener.ContainsKey(netEvent))
            eventListener[netEvent] -= listener;

        if (eventListener[netEvent] == null)
            eventListener.Remove(netEvent);
    }

    // 分发事件
    public static void FireEvent(NetEvent netEvent, string err)
    {
        if (eventListener.ContainsKey(netEvent))
            eventListener[netEvent](err);
    }

    // 消息列表
    static List msgList = new List();
    // 消息列表长度 (用list.count不是可以吗?)
    static int msgCount = 0;
    // 每一次Update处理的消息量
    readonly static int MAX_MESSAGE_FIRE = 10;
    // 消息委托
    public delegate void MsgListener(MsgBase msgBase);
    // 消息监听列表
    static Dictionary msgListeners = new Dictionary();

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

    // 分发消息
    public static void FireMsg(string msgName, MsgBase msgBase)
    {
        if (msgListeners.ContainsKey(msgName))
            msgListeners[msgName](msgBase);
    }

    public static void Connect(string ip, int port)
    {
        // 判断当前的连接状态
        if (socket != null && socket.Connected)
        {
            Debug.Log("连接失败,当前已连接");
            return;
        }

        if (isConnecting)
        {
            Debug.Log("连接失败,当前正在连接中");
            return;
        }

        // 初始化所有成员
        InitState();

        // 设置socket参数
        socket.NoDelay = true;
        isConnecting = true;
        socket.BeginConnect(ip, port, ConnectCallBack, socket);
    }

    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(readBuffer.buffer, readBuffer.writeIndex,
                                    readBuffer.Remain, 0, ReceiveCallBack, socket);
        }
        catch (SocketException ex)
        {
            Debug.Log("连接失败: " + ex.ToString());
            FireEvent(NetEvent.ConnectFail, ex.ToString());
            isConnecting = false;
        }
    }

    static void InitState()
    {
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        readBuffer = new ByteArray();

        writeQueue = new Queue();

        isConnecting = false;

        isClosing = false;

        msgList = new List();

        msgCount = 0;

        lastPingTime = Time.time;

        lastPongTime = Time.time;

        // 监听Pong
        if (!msgListeners.ContainsKey("MsgPong"))
            AddMsgListener("MsgPong", OnMsgPong);
    }

    //监听PONG协议: 简单实现,在第七章会具体实现心跳机制
    private static void OnMsgPong(MsgBase msgBase)
    {
        lastPongTime = Time.time;
    }

    public static void ReceiveCallBack(IAsyncResult ar)
    {
        try
        {
            Socket sokcet = (Socket)ar.AsyncState;

            int count = socket.EndReceive(ar);

            if (count == 0)
            {
                // 服务器关闭客户端的socket
                Close();
                return;
            }

            readBuffer.writeIndex += count;

            //处理消息
            OnReceiveData();

            //继续接收数据
            if (readBuffer.Remain < 16)
            {
                readBuffer.MoveBytes();
                // 扩容成原来的两倍
                readBuffer.ReSize(readBuffer.capacity);
            }

            // 继续接收消息
            socket.BeginReceive(readBuffer.buffer, readBuffer.writeIndex,
                        readBuffer.Remain, 0, ReceiveCallBack, socket);
        }
        catch(SocketException ex)
        {
            Debug.Log("接收消息错误: " + ex.ToString());
        }
    }

    public static void OnReceiveData()
    {
        if (readBuffer.Length < headSize)
            return;

        // 获取消息长度
        int bodyLength = BitConverter.ToInt32(readBuffer.buffer, readBuffer.readIndex);

        if (readBuffer.Length < bodyLength + headSize)
            return;

        readBuffer.readIndex += headSize;

        // 解析协议名
        int nameCount = 0;
        string protoName = MsgBase.DecodeName(readBuffer.buffer, readBuffer.readIndex, out nameCount);

        if (protoName == "")
        {
            Debug.Log("OnReceiveData MsgBase.DecodeName failed.");
            return;
        }

        readBuffer.readIndex += nameCount;

        // 解析协议
        int bodyCount = bodyLength - nameCount;

        MsgBase msgBase = MsgBase.Decode(protoName, readBuffer.buffer, readBuffer.readIndex, bodyCount);
        
        readBuffer.readIndex += bodyCount;
        readBuffer.CheckAndMoveBytes();

        // 加入消息队列
        lock(msgList)
        {
            msgList.Add(msgBase);
            msgCount++;
        }

        if (readBuffer.Length > headSize)
            OnReceiveData();
    }

    // 发送数据
    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[] lenBytes = BitConverter.GetBytes(len);

        // 统一小端编码
        if (!BitConverter.IsLittleEndian)
            lenBytes.Reverse();

        byte[] sendBytes1 = lenBytes.Concat(nameBytes).ToArray();
        byte[] sendBytes = sendBytes1.Concat(bodyBytes).ToArray();

        // 写入队列
        ByteArray ba = new ByteArray(sendBytes);
        int count = 0;

        lock(writeQueue)
        {
            writeQueue.Enqueue(ba);
            count = writeQueue.Count;
        }

        // 发送数据
        if (count == 1)
        {
            socket.BeginSend(sendBytes, 0, sendBytes.Length, SocketFlags.None, SendCallBack, socket);
        }

    }

    // send回调
    public static void SendCallBack(IAsyncResult ar)
    {
        Socket socket = (Socket)ar.AsyncState;

        // 状态判断
        if (socket == null || !socket.Connected)
            return;

        // 判断是否发送完整
        int count = socket.EndSend(ar);

        ByteArray ba;
        lock(writeQueue)
        {
            ba = writeQueue.First();
        }

        ba.readIndex += count;
        if(ba.Length == 0)
        {
            lock(writeQueue)
            {
                writeQueue.Dequeue();
                ba = writeQueue.First();
            }
        }

        // 发送不完整,继续发送
        if (ba != null)
        {
            socket.BeginSend(ba.buffer, ba.readIndex, ba.Length, SocketFlags.None, SendCallBack, socket);
        }
        else if (isClosing)
        {
            // 安全关闭
            socket.Close();
        }
    }


    public static void Close()
    {
        // 安全close socket
        if (socket == null || !socket.Connected)
            return;

        if (isConnecting)
            return;

        if (writeQueue.Count > 0)
            isClosing = true;

        else
        {
            // 关闭socket
            socket.Close();
            // 调用关闭连接的委托
            FireEvent(NetEvent.Close, "");
        }
    }

    public static void HandleMsg()
    {
        if (msgCount == 0)
            return;

        // 每一帧处理十条消息
        for (int i = 0; i < MAX_MESSAGE_FIRE; i++)
        {
            MsgBase msg = null;

            lock (msgList)
            {
                if (msgList.Count > 0)
                {
                    msg = msgList[0];
                    msgList.RemoveAt(0);
                    msgCount--;
                }
            }

            if (msg != null)
                FireMsg(msg.protoName, msg);
            else
                break;
        }
    }

    public static void PingMsg()
    {
        if (!isUsePing)
            return;

        if (Time.time - lastPingTime > pingInterval)
        {
            MsgPing msgPing = new MsgPing();

            Send(msgPing);

            lastPingTime = Time.time;
        }

        // Pong 时间
        if (Time.time - lastPongTime > pingInterval * 4)
            Close();
    }
}

ByteArray.cs

using System;

public class ByteArray
{
    const int default_size = 1024;
    // 初始缓冲区大小
    int initSize = 0;
    // 缓冲区
    public byte[] buffer;
    // 已发送的索引
    public int readIndex = 0;
    // 整个数据的长度
    public int writeIndex = 0;
    // 缓冲区容量
    public int capacity = 0;
    // 缓冲区剩余空间
    public int Remain { get { return capacity - writeIndex; } }
    // 缓冲区中有效数据长度
    public int Length { get { return writeIndex - readIndex; } }

    public ByteArray(int size = default_size)
    {
        buffer = new byte[size];

        capacity = size;

        initSize = size;

        readIndex = 0;

        writeIndex = 0;
    }


    public ByteArray(byte[] defaultBytes)
    {
        buffer = defaultBytes;

        readIndex = 0;

        writeIndex = defaultBytes.Length;

        capacity = defaultBytes.Length;

        initSize = defaultBytes.Length;
    }

    // 扩容
    public void ReSize(int size)
    {
        if (size < Length || size < initSize)
            return;
        // 指数扩容:从2的倍数中找一个比原来大的
        int n = 1;
        while (n < size)
            n *= 2;
        // 申请新数组并拷贝数据
        capacity = n;
        byte[] newbuffer = new byte[capacity];
        Array.Copy(buffer, readIndex, newbuffer, 0, Length);
        // buffer指向newbuffer
        buffer = newbuffer;
        writeIndex = Length;
        readIndex = 0;
    }

    // 向缓冲区写数据, count是要写入缓冲区的字节数
    public int Write(byte[] bs, int offset, int count)
    {
        if (Remain < count)
            ReSize(Length + count);

        Array.Copy(bs, offset, buffer, writeIndex, count);
        writeIndex += count;
        return count;
    }

    // 读取缓冲区中的数据 将缓冲区中的count字节数据读取到bs中
    public int Read(byte[] bs, int offset, int count)
    {
        count = Math.Min(count, Length);
        Array.Copy(buffer, readIndex, bs, offset, count);
        readIndex += count;
        CheckAndMoveBytes();
        return count;
    }

    //检查并移动数据
    public void CheckAndMoveBytes()
    {
        if (Length < 128)
        {
            MoveBytes();
        }
    }

    //移动数据
    public void MoveBytes()
    {
        Array.Copy(buffer, readIndex, buffer, 0, Length);
        writeIndex = Length;
        readIndex = 0;
    }

    public short ReadInt16()
    {
        if (Length < 2)
            return 0;

        short ret = BitConverter.ToInt16(buffer, readIndex);
        readIndex += 2;
        CheckAndMoveBytes();
        return ret;
    }

    public int ReadInt32()
    {
        if (Length < 4)
            return 0;

        int ret = BitConverter.ToInt32(buffer, readIndex);
        readIndex += 4;
        CheckAndMoveBytes();
        return ret;
    }
}

客户端界面就是简单的几个按钮,用于测试网络模块:


image.png

3、服务端代码

服务器代码与之前的代码相比改动不大,因为服务端没有实现PONG协议,比较简单。
server.py

# Chapter 6 Server code.
import socket
import time
import struct

def parse_msg_length(head):
    # get message body length
    length = 0
    for i in range(len(head)):
        length += ord(head[i]) * 256 ** i
    print "length: ", length
    return length

def handle_message(read_buffer, socket):
    # check buffer > head size
    buffer_count = len(read_buffer)
    if buffer_count < head_size:
        return False, 0
    # check buffer > one whole data
    msg_body_size = parse_msg_length(read_buffer[0:head_size])
    if buffer_count < head_size + msg_body_size:
        return False, 0
    # handle message
    data = read_buffer[2 * head_size:msg_body_size + head_size]
    # socket.send(data)
    print 'data:', data
    return True, msg_body_size

# server socket
ip = "127.0.0.1"
port = 8888
server_address = (ip, port)
buffer_size = 1024
head_size = 4
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(server_address)
server.listen(1)
read_buffer = ""

print "Waiting for Connection."
while True:
    connection, client_address = server.accept()
    print "client connected: ", client_address
    while True:
        try:
            msg_body = "MsgMove{\"protoName\":\"MsgMove\",\"x\":12,\"y\":10,\"z\":7}"
            pack_format = 'I' + 'I' + str(len(msg_body)) + 's'
            send_data = (len(msg_body) + head_size, len('MsgMove'), msg_body)
            send_bytes = struct.pack(pack_format, *send_data)
            connection.send(send_bytes)
            # time.sleep(3)
            receive_data = connection.recv(buffer_size)
            if receive_data:
                read_buffer += receive_data
                result, length = handle_message(read_buffer, connection)
                if result:
                    read_buffer = read_buffer[head_size + length:]
                else:
                    pass
                print 'read_buffer: ', read_buffer
            else:
                connection.close()
                break
        except socket.error:
            connection.close()

4、运行结果

image.png

image.png

客户端能够正确解析服务器发来的消息。然后客户端每个3秒给服务器发送一个ping协议维持心跳,但是服务器还没实现ping和pong协议,因此过一段时间之后客户端就会断开连接。正常运行。

你可能感兴趣的:(《Unity网络游戏实战》Chapter6: 客户端网络架构)