《Unity網(wǎng)絡(luò)游戲?qū)崙?zhàn)》Chapter7: 服務(wù)端網(wǎng)絡(luò)架構(gòu)

1、介紹

前面第六章完成了一個(gè)比較通用的客戶端網(wǎng)絡(luò)架構(gòu)驾锰,但是心跳機(jī)制沒有實(shí)現(xiàn)卸留。第七章會(huì)完成心跳機(jī)制,以一個(gè)在線記事本的案例實(shí)現(xiàn)一個(gè)通用的服務(wù)端程序椭豫。服務(wù)端依然采用Python2實(shí)現(xiàn)耻瑟。

2、客戶端

客戶端與第六章的代碼相差無幾赏酥,只是加了幾個(gè)協(xié)議喳整,完善了一下在線筆記本的界面。這里就不細(xì)寫裸扶。直接上界面和代碼:

客戶端界面:


image.png

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

ByteArray.cs

using System;

public class ByteArray
{
    const int default_size = 1024;
    // 初始緩沖區(qū)大小
    int initSize = 0;
    // 緩沖區(qū)
    public byte[] buffer;
    // 已發(fā)送的索引
    public int readIndex = 0;
    // 整個(gè)數(shù)據(jù)的長(zhǎng)度
    public int writeIndex = 0;
    // 緩沖區(qū)容量
    public int capacity = 0;
    // 緩沖區(qū)剩余空間
    public int Remain { get { return capacity - writeIndex; } }
    // 緩沖區(qū)中有效數(shù)據(jù)長(zhǎng)度
    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;
    }

    // 擴(kuò)容
    public void ReSize(int size)
    {
        if (size < Length || size < initSize)
            return;
        // 指數(shù)擴(kuò)容:從2的倍數(shù)中找一個(gè)比原來大的
        int n = 1;
        while (n < size)
            n *= 2;
        // 申請(qǐng)新數(shù)組并拷貝數(shù)據(jù)
        capacity = n;
        byte[] newbuffer = new byte[capacity];
        Array.Copy(buffer, readIndex, newbuffer, 0, Length);
        // buffer指向newbuffer
        buffer = newbuffer;
        writeIndex = Length;
        readIndex = 0;
    }

    // 向緩沖區(qū)寫數(shù)據(jù), count是要寫入緩沖區(qū)的字節(jié)數(shù)
    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;
    }

    // 讀取緩沖區(qū)中的數(shù)據(jù) 將緩沖區(qū)中的count字節(jié)數(shù)據(jù)讀取到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;
    }

    //檢查并移動(dòng)數(shù)據(jù)
    public void CheckAndMoveBytes()
    {
        if (Length < 128)
        {
            MoveBytes();
        }
    }

    //移動(dòng)數(shù)據(jù)
    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;
    }

    //打印緩沖區(qū)
    public override string ToString()
    {
        return BitConverter.ToString(buffer, readIndex, Length);
    }

    //打印調(diào)試信息
    public string Debug()
    {
        return string.Format("readIdx({0}) writeIdx({1}) bytes({2})",
            readIndex,
            writeIndex,
            BitConverter.ToString(buffer, 0, capacity)
        );
    }
}

LoginMsg.cs

//注冊(cè)
public class MsgRegister : MsgBase
{
    public MsgRegister() { protoName = "MsgRegister"; }
    //客戶端發(fā)
    public string username = "";
    public string password = "";
    //服務(wù)端回(0-成功框都,1-失敗)
    public int result = 0;
}


//登陸
public class MsgLogin : MsgBase
{
    public MsgLogin() { protoName = "MsgLogin"; }
    //客戶端發(fā)
    public string username = "";
    public string password = "";
    //服務(wù)端回(0-成功呵晨,1-失斘罕!)
    public int result = 0;
}


//踢下線(服務(wù)端推送)
public class MsgKick : MsgBase
{
    public MsgKick() { protoName = "MsgKick"; }
    //原因(0-其他人登陸同一賬號(hào))
    public int reason = 0;
}

//獲取記事本內(nèi)容
public class MsgGetText : MsgBase
{
    public MsgGetText() { protoName = "MsgGetText"; }
    //服務(wù)端回
    public string text = "";
}

//保存記事本內(nèi)容
public class MsgSaveText : MsgBase
{
    public MsgSaveText() { protoName = "MsgSaveText"; }
    //客戶端發(fā)
    public string text = "";
    //服務(wù)端回(0-成功 1-文字太長(zhǎng))
    public int result = 0;
}

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)
    {
        // 將消息轉(zhuǎn)換成Json格式
        string s = JsonUtility.ToJson(msgBase);
        // 轉(zhuǎn)換成字節(jié)數(shù)組
        return System.Text.Encoding.UTF8.GetBytes(s);
    }

    // 解碼
    public static MsgBase Decode(string protoName, byte[] bytes, int offset, int count)
    {
        // 將字節(jié)數(shù)組轉(zhuǎn)換為字符串
        string s = System.Text.Encoding.UTF8.GetString(bytes, offset, count);

        // 將字符串轉(zhuǎn)換成對(duì)應(yīng)協(xié)議名的Json格式
        MsgBase msgBase = (MsgBase)JsonUtility.FromJson(s, Type.GetType(protoName));

        return msgBase;
    }


    // 編碼協(xié)議名
    public static byte[] EncodeName(MsgBase msgBase)
    {
        // 協(xié)議名bytes和長(zhǎng)度
        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;
    }

    // 解碼協(xié)議名
    public static string DecodeName(byte[] bytes, int offset, out int count)
    {
        count = 0;

        // 判斷字節(jié)數(shù)是否大于數(shù)據(jù)長(zhǎng)度
        if (bytes.Length < headSize)
            return "";

        // 讀取協(xié)議名長(zhǎng)度
        int namelength = BitConverter.ToInt32(bytes, offset);

        // 判斷長(zhǎng)度是否足夠解析協(xié)議名
        if (bytes.Length < headSize + namelength)
            return "";

        // 解析
        count = headSize + namelength;

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

        return msgName;
    }
}

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<ByteArray> 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);
    // 監(jiān)聽列表 --- 連接服務(wù)器的監(jiān)聽列表
    static Dictionary<NetEvent, EventListener> eventListener = new Dictionary<NetEvent, EventListener>();

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

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

    // 消息列表
    static List<MsgBase> msgList = new List<MsgBase>();
    // 消息列表長(zhǎng)度 (用list.count不是可以嗎摸屠?)
    static int msgCount = 0;
    // 每一次Update處理的消息量
    readonly static int MAX_MESSAGE_FIRE = 10;
    // 消息委托
    public delegate void MsgListener(MsgBase msgBase);
    // 消息監(jiān)聽列表
    static Dictionary<string, MsgListener> msgListeners = new Dictionary<string, MsgListener>();

    // 添加消息監(jiān)聽
    public static void AddMsgListener(string msgName, MsgListener listener)
    {
        if (msgListeners.ContainsKey(msgName))
        {
            msgListeners[msgName] += listener;
        }
        else
        {
            msgListeners[msgName] = listener;
        }
    }

    // 刪除消息監(jiān)聽
    public static void RemoveMsgListener(string msgName, MsgListener listener)
    {
        if (msgListeners.ContainsKey(msgName))
            msgListeners[msgName] -= listener;

        if (msgListeners[msgName] == null)
            msgListeners.Remove(msgName);
    }

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

    public static void Connect(string ip, int port)
    {
        // 判斷當(dāng)前的連接狀態(tài)
        if (socket != null && socket.Connected)
        {
            Debug.Log("連接失敗谓罗,當(dāng)前已連接");
            return;
        }

        if (isConnecting)
        {
            Debug.Log("連接失敗,當(dāng)前正在連接中");
            return;
        }

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

        // 設(shè)置socket參數(shù)
        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;

            // 接收數(shù)據(jù)
            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<ByteArray>();

        isConnecting = false;

        isClosing = false;

        msgList = new List<MsgBase>();

        msgCount = 0;

        lastPingTime = Time.time;

        lastPongTime = Time.time;

        // 監(jiān)聽Pong
        if (!msgListeners.ContainsKey("MsgPong"))
            AddMsgListener("MsgPong", OnMsgPong);
    }

    //監(jiān)聽PONG協(xié)議: 收到MsgPong之后更新lastPongTime
    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)
            {
                // 服務(wù)器關(guān)閉客戶端的socket
                Close();
                return;
            }

            readBuffer.writeIndex += count;

            //處理消息
            OnReceiveData();

            //繼續(xù)接收數(shù)據(jù)
            if (readBuffer.Remain < 16)
            {
                readBuffer.MoveBytes();
                // 擴(kuò)容成原來的兩倍
                readBuffer.ReSize(readBuffer.capacity);
            }

            // 繼續(xù)接收消息
            socket.BeginReceive(readBuffer.buffer, readBuffer.writeIndex,
                        readBuffer.Remain, 0, ReceiveCallBack, socket);
        }
        catch(SocketException ex)
        {
            Debug.Log("接收消息錯(cuò)誤: " + ex.ToString());
        }
    }

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

        // 獲取消息長(zhǎng)度
        int bodyLength = BitConverter.ToInt32(readBuffer.buffer, readBuffer.readIndex);

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

        readBuffer.readIndex += headSize;

        // 解析協(xié)議名
        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;

        // 解析協(xié)議
        int bodyCount = bodyLength - nameCount;

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

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

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

    // 發(fā)送數(shù)據(jù)
    public static void Send(MsgBase msg)
    {
        if (socket == null || !socket.Connected)
            return;

        if (isConnecting)
            return;

        if (isClosing)
            return;

        // 數(shù)據(jù)編碼
        byte[] nameBytes = MsgBase.EncodeName(msg);
        byte[] bodyBytes = MsgBase.Encode(msg);

        int len = nameBytes.Length + bodyBytes.Length;

        byte[] lenBytes = BitConverter.GetBytes(len);

        // 統(tǒng)一小端編碼
        if (!BitConverter.IsLittleEndian)
            lenBytes.Reverse();

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

        // 寫入隊(duì)列
        ByteArray ba = new ByteArray(sendBytes);
        int count = 0;

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

        // 發(fā)送數(shù)據(jù)
        if (count == 1)
        {
            socket.BeginSend(sendBytes, 0, sendBytes.Length, SocketFlags.None, SendCallBack, socket);
        }

    }

    // send回調(diào)
    public static void SendCallBack(IAsyncResult ar)
    {
        Socket socket = (Socket)ar.AsyncState;

        // 狀態(tài)判斷
        if (socket == null || !socket.Connected)
            return;

        // 判斷是否發(fā)送完整
        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();
            }
        }

        // 發(fā)送不完整餐塘,繼續(xù)發(fā)送
        if (ba != null)
        {
            socket.BeginSend(ba.buffer, ba.readIndex, ba.Length, SocketFlags.None, SendCallBack, socket);
        }
        else if (isClosing)
        {
            // 安全關(guān)閉
            socket.Close();
        }
    }


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

        if (isConnecting)
            return;

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

        else
        {
            // 關(guān)閉socket
            socket.Close();
            // 調(diào)用關(guān)閉連接的委托
            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 時(shí)間
        if (Time.time - lastPongTime > pingInterval * 4)
            Close();
    }
}

NetWorkManager.cs

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

public class NetWorkManager : MonoBehaviour
{
    public InputField InputField_username;
    public InputField inputField_password;
    public InputField InputField_notepad;

    public Text text_LoginMsg;
    public Text text_SaveMsg;

    public bool isLogin = false;

    void Start()
    {
        NetManager.AddEventListener(NetManager.NetEvent.ConnectSucc, OnConnectSucc);
        NetManager.AddEventListener(NetManager.NetEvent.ConnectFail, OnConnectFail);
        NetManager.AddEventListener(NetManager.NetEvent.Close, OnClose);

        NetManager.AddMsgListener("MsgMove", OnMsgMove);
        NetManager.AddMsgListener("MsgRegister", OnMsgRegister);
        NetManager.AddMsgListener("MsgLogin", OnMsgLogin);
        NetManager.AddMsgListener("MsgGetText", OnMsgGetText);
        NetManager.AddMsgListener("MsgSaveText", OnMsgSaveText);
    }

    //連接成功回調(diào)
    void OnConnectSucc(string err)
    {
        Debug.Log("連接服務(wù)器成功");
        //TODO:進(jìn)入游戲
    }

    void OnConnectFail(string err)
    {
        Debug.Log("連接服務(wù)器失敗");
        //TODO:彈出提示框(連接失敗妥衣,請(qǐng)重試)
    }

    void OnClose(string err)
    {
        Debug.Log("連接斷開");
        //TODO:彈出提示框(網(wǎng)絡(luò)斷開)
        //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 OnButtonRegisterClick()
    {
        MsgRegister msg = new MsgRegister();

        msg.username = InputField_username.text;
        msg.password = inputField_password.text;

        NetManager.Send(msg);

    }

    public void OnButtonLoginClick()
    {
        if (isLogin)
        {
            Debug.Log("已經(jīng)登錄,不能重復(fù)登錄.");
            return;
        }

        MsgLogin msg = new MsgLogin();

        msg.username = InputField_username.text;
        msg.password = inputField_password.text;

        NetManager.Send(msg);
    }

    public void OnButtonGetTextClick()
    {
        if (!isLogin)
        {
            Debug.Log("沒有登錄,無法獲取文本");
            return;
        }

        MsgGetText msg = new MsgGetText();

        NetManager.Send(msg);
    }

    public void OnButtonSaveTextClick()
    {
        if (!isLogin)
        {
            Debug.Log("沒有登錄税手,無法保存");
            return;
        }

        MsgSaveText msg = new MsgSaveText();

        msg.text = InputField_notepad.text;

        NetManager.Send(msg);
    }

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

        // 打印坐標(biāo)
        Debug.Log("x: " + msgMove.x);
        Debug.Log("y: " + msgMove.y);
        Debug.Log("z: " + msgMove.z);
    }

    public void OnMsgRegister(MsgBase msgBase)
    {
        MsgRegister msg = (MsgRegister)msgBase;

        if (msg.result == 0)
            Debug.Log("注冊(cè)成功");
        else
            Debug.Log("注冊(cè)失敗");
    }

    public void OnMsgLogin(MsgBase msgBase)
    {
        MsgLogin msg = (MsgLogin)msgBase;

        if (msg.result == 0)
        {
            text_LoginMsg.text = "登錄成功";
            text_LoginMsg.color = Color.green;
            isLogin = true;
        }
        else
        {
            text_LoginMsg.text = "登錄成功";
            text_LoginMsg.color = Color.red;
            isLogin = false;
        }
            
    }

    public void OnMsgGetText(MsgBase msgBase)
    {
        MsgGetText msg = (MsgGetText)msgBase;

        InputField_notepad.text = msg.text;
    }

    public void OnMsgSaveText(MsgBase msgBase)
    {
        MsgSaveText msg = (MsgSaveText)msgBase;

        if (msg.result == 0)
        {
            text_SaveMsg.text = "保存成功";
            text_SaveMsg.color = Color.green;
        }
        else
        {
            text_SaveMsg.text = "保存失敗";
            text_SaveMsg.color = Color.red;
        }
    }

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

SyncMsg.cs


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

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

3蜂筹、服務(wù)端

服務(wù)端采用的數(shù)據(jù)庫為MySQL,服務(wù)端的代碼比較簡(jiǎn)單芦倒,操作都是接收消息艺挪,存入client_state對(duì)應(yīng)的緩沖區(qū),判斷數(shù)據(jù)長(zhǎng)度是否足夠解析一條數(shù)據(jù)兵扬,夠便處理消息麻裳,知道緩沖區(qū)數(shù)據(jù)不足一條消息。

加入PlayerManager類用于管理player器钟,編寫MySQLManager類用于對(duì)數(shù)據(jù)庫進(jìn)行操作津坑。

整個(gè)服務(wù)器的代碼邏輯比較簡(jiǎn)單,寫的時(shí)候想著采用多線程處理客戶端的消息傲霸,但是想著先實(shí)現(xiàn)一個(gè)簡(jiǎn)單的疆瑰,后續(xù)在修改,而完成之后事情也比較多昙啄,就不想改了穆役。以后有緣在修改吧。

這里就直接上代碼了:
ClientState.py

import Player
class ClientState(object):
    def __init__(self, socket, addr, time_span):
        self.socket = socket
        self.addr = addr
        self.buffer = ""
        self.last_ping_time = time_span
        self.player = Player.Player(self)

Message.py

import json as js

# message base class
class MsgBase(object):
    # init
    def __init__(self):
        self.protoName = "null"

    # transform the class member to dict
    def to_dict(self):
        return {'protoName': self.protoName}

    # encode msg_base to json str
    def encode(self):
        return js.dumps(self.to_dict())

    # decode json str to msg_base
    @staticmethod
    def decode(json_data):
        msg_base = MsgBase()
        data = js.loads(json_data)
        # assign member data
        msg_base.protoName = data['protoName']
        return msg_base

# move message
class MsgMove(MsgBase):
    def __init__(self):
        super(MsgMove, self).__init__()
        self.x = 0
        self.y = 0
        self.z = 0

    def to_dict(self):
        return {'protoName': self.protoName,
                'x': self.x,
                'y': self.y,
                'z': self.z}

    @staticmethod
    def decode(json_data):
        msg_move = MsgMove()
        data = js.loads(json_data)
        msg_move.protoName = data['protoName']
        msg_move.x = data['x']
        msg_move.y = data['y']
        msg_move.z = data['z']
        return msg_move

# attack message
class MsgAttack(MsgBase):
    def __init__(self):
        super(MsgAttack, self).__init__()
        self.protoName = 'MsgAttack'

# ping message
class MsgPing(MsgBase):
    def __init__(self):
        super(MsgPing, self).__init__()
        self.protoName = 'MsgPing'

# pong message, to response ping
class MsgPong(MsgBase):
    def __init__(self):
        super(MsgPong, self).__init__()
        self.protoName = 'MsgPong'

class MsgRegister(MsgBase):
    def __init__(self):
        super(MsgRegister, self).__init__()
        self.protoName = 'MsgRegister'
        self.username = ""
        self.password = ""
        self.result = 0

    def to_dict(self):
        return {'protoName': self.protoName,
                'username': self.username,
                'password': self.password,
                'result': self.result}

    @staticmethod
    def decode(json_data):
        msg_register = MsgRegister()
        data = js.loads(json_data)
        msg_register.protoName = data['protoName']
        msg_register.username = data['username']
        msg_register.password = data['password']
        msg_register.result = data['result']
        return msg_register

class MsgLogin(MsgBase):
    def __init__(self):
        super(MsgLogin, self).__init__()
        self.protoName = 'MsgLogin'
        self.username = ""
        self.password = ""
        self.result = 0

    def to_dict(self):
        return {'protoName': self.protoName,
                'username': self.username,
                'password': self.password,
                'result': self.result}

    @staticmethod
    def decode(json_data):
        msg_login = MsgLogin()
        data = js.loads(json_data)
        msg_login.protoName = data['protoName']
        msg_login.username = data['username']
        msg_login.password = data['password']
        msg_login.result = data['result']
        return msg_login

class MsgKick(MsgBase):
    def __init__(self):
        super(MsgKick, self).__init__()
        self.protoName = 'MsgKick'
        self.reason = 0

    def to_dict(self):
        return {'protoName': self.protoName,
                'reason': self.reason}

    @staticmethod
    def decode(json_data):
        msg_kick = MsgKick()
        data = js.loads(json_data)
        msg_kick.protoName = data['protoName']
        msg_kick.reason = data['reason']
        return msg_kick

class MsgGetText(MsgBase):
    def __init__(self):
        super(MsgGetText, self).__init__()
        self.protoName = 'MsgGetText'
        self.text = ""

    def to_dict(self):
        return {'protoName': self.protoName,
                'text': self.text}

    @staticmethod
    def decode(json_data):
        msg_get_text = MsgGetText()
        data = js.loads(json_data)
        msg_get_text.protoName = data['protoName']
        msg_get_text.text = data['text']
        return msg_get_text

class MsgSaveText(MsgBase):
    def __init__(self):
        super(MsgSaveText, self).__init__()
        self.protoName = 'MsgSaveText'
        self.text = ""
        self.result = 0

    def to_dict(self):
        return {'protoName': self.protoName,
                'text': self.text,
                'result': self.result}

    @staticmethod
    def decode(json_data):
        msg_save_text = MsgSaveText()
        data = js.loads(json_data)
        msg_save_text.protoName = data['protoName']
        msg_save_text.text = data['text']
        msg_save_text.result = data['result']
        return msg_save_text

MessageHandler.py

import Message
import time
import MySQLManager as dbm
import Player

class MessageHandler(object):
    def __init__(self, server, player_manager):
        self.events = {'MsgMove': self.move_response,
                       'MsgPing': self.ping_response,
                       'MsgRegister': self.register_response,
                       'MsgLogin': self.login_response,
                       'MsgGetText': self.get_text_response,
                       'MsgSaveText': self.save_text_response,
                       'UpdateDataBase': self.update_data_base}
        self.game_server = server
        self.pm = player_manager
        self.db = dbm.DataBaseManager()
        self.db.connect_mysql()

    def handle_msg(self, msg_name, json_msg, client_state):
        self.events[msg_name](json_msg, client_state)

    def move_response(self, json_msg, client_state):
        msg = Message.MsgMove.decode(json_msg)
        msg.x += 10
        msg.y += 10
        msg.z += 10
        self.game_server.send(client_state, msg)

    def ping_response(self, json_msg, client_state):
        msg = Message.MsgPing.decode(json_msg)
        # check msg ping
        if msg.protoName != 'MsgPing':
            print 'Receive Error MsgPing.'
            return
        # update client last ping time
        client_state.last_ping_time = time.time()
        # send MsgPong to client
        msg_pong = Message.MsgPong()
        self.game_server.send(client_state, msg_pong)

    def register_response(self, json_msg, client_state):
        msg_register = Message.MsgRegister.decode(json_msg)
        # register new account and player
        if self.db.register_new_user(msg_register.username, msg_register.password):
            if self.db.create_player(msg_register.username):
                msg_register.result = 0
            else:
                msg_register.result = 1
        else:
            msg_register.result = 1
        # send to client
        self.game_server.send(client_state, msg_register)

    def login_response(self, json_msg, client_state):
        msg_login = Message.MsgLogin.decode(json_msg)
        # check username and password
        if not self.db.check_password(msg_login.username, msg_login.password):
            # username or password error
            msg_login.result = 1
            self.game_server.send(client_state, msg_login)
            return
        # if the client is online
        if self.pm.is_online(msg_login.username):
            # kick the player offline
            pre_player = self.pm.get_player(msg_login.username)
            msg_kick = Message.MsgKick()
            self.game_server.send(pre_player.client_state, msg_kick)
            # close, but not update the data in database
            self.game_server.connect_sockets.remove(pre_player.client_state.socket)
            self.game_server.client_states.pop(pre_player.client_state.socket)
            pre_player.client_state.socket.close()
            self.pm.remove_player(msg_login.username)
        # get player data
        player_data = self.db.get_player_data(msg_login.username)
        if not player_data:
            # query failed
            msg_login.result = 1
            self.game_server.send(client_state, msg_login)
            return
        # add new player
        new_player = Player.Player(client_state)
        new_player.username = msg_login.username
        new_player.player_data = player_data
        self.pm.add_player(new_player.username, new_player)
        client_state.player = new_player
        # return msg to client
        msg_login.result = 0
        self.game_server.send(client_state, msg_login)

    def get_text_response(self, json_msg, client_state):
        msg_get_text = Message.MsgGetText.decode(json_msg)
        player = client_state.player
        msg_get_text.text = player.player_data.text
        self.game_server.send(client_state, msg_get_text)

    def save_text_response(self, json_msg, client_state):
        msg_save_text = Message.MsgSaveText.decode(json_msg)
        player = client_state.player
        player.player_data.text = msg_save_text.text
        if self.update_data_base(client_state):
            msg_save_text.result = 0
        else:
            msg_save_text.result = 1
        self.game_server.send(client_state, msg_save_text)

    def update_data_base(self, client_state):
        # update database when player quit
        player_data = client_state.player.player_data.to_json()
        # update database
        try:
            self.db.update_player_data(client_state.player.username, player_data)
            return True
        except Exception:
            return False

MySQLManager.py

import MySQLdb as mysql
import re
import Player

class DataBaseManager(object):
    def __init__(self):
        self.db = None
        self.cursor = None

    def connect_mysql(self):
        try:
            self.db = mysql.connect('127.0.0.1', 'root', 'xxxxxx', 'base', charset='utf8', autocommit=True)
            self.cursor = self.db.cursor()
            print '[DataBase] Connect MySQL Successfully.'
        except Exception:
            print '[DataBase] Connect MySQL Failed!'

    def is_safe_string(self, s):
        # search the special char, like ';', '*' and so on
        pattern = r"[-|;|,|\/|\(|\)|\[|\]|\}|\{|%|@|\*|!|\']"
        # use the re.search
        result = re.search(pattern, s)
        if result:
            return False
        else:
            return True

    def is_account_exist(self, username):
        # check
        if self.db is None:
            print '[DataBase] DataBase not connected.'
            return
        # check the string, if the string is not safe, return
        if not self.is_safe_string(username):
            print '[DataBase] The username is illegal!'
            return
        # get the sql
        sql = 'select * from account where username=\"{0}\"'.format(username)
        # query
        try:
            self.cursor.execute(sql)
            query_result = self.cursor.fetchone()
        except Exception:
            print '[DataBase] query account failed!'
            return False
        # if the username not exist, the query result is none
        if query_result:
            return True
        else:
            return False

    def register_new_user(self, username, password):
        # check
        if not self.is_safe_string(username):
            print '[DataBase] The username is illegal!'
            return False
        if not self.is_safe_string(password):
            print '[DataBase] The password is illegal!'
            return False
        if self.is_account_exist(username):
            print '[DataBase] The username already exist!'
            return False
        # create a new account
        try:
            sql = 'insert into account values(\"{0}\", \"{1}\")'.format(username, password)
            self.cursor.execute(sql)
            return True
        except Exception:
            print '[DataBase] create account failed!'
            return False

    def create_player(self, username):
        # check
        if not self.is_safe_string(username):
            print '[DataBase] The username is illegal!'
            return False
        # write the player data
        player_data = Player.PlayerData()
        data = player_data.to_json()
        try:
            sql = 'insert into player values(\"{0}\", \"{1}\")'.format(username, data.replace('"', '\\"'))
            self.cursor.execute(sql)
            return True
        except Exception:
            print '[DataBase] create player failed!'
            return False

    def check_password(self, username, password):
        # check
        if not self.is_safe_string(username):
            print '[DataBase] The username is illegal!'
            return False
        if not self.is_safe_string(password):
            print '[DataBase] The password is illegal!'
            return False
        # query
        sql = 'select password from account where username=\"{0}\"'.format(username)
        # get player password
        try:
            self.cursor.execute(sql)
            data = self.cursor.fetchone()
        except Exception:
            print '[DataBase] get user password failed!'
            return False
        # check the password
        if data:
            # username exist
            if str(data[0]) == password:
                return True
            else:
                print '[DataBase] Password Error.'
                return False
        else:
            print '[DataBase] Username not exist.'
            return False

    def get_player_data(self, username):
        # check
        if not self.is_safe_string(username):
            print '[DataBase] The username is illegal!'
            return False
        sql = 'select data from player where username=\"{0}\"'.format(username)
        try:
            self.cursor.execute(sql)
            # data is a tuple
            data = self.cursor.fetchone()
            return Player.PlayerData.decode(str(data[0]))
        except Exception:
            print '[DataBase] get player data failed!'
            return False

    def update_player_data(self, username, player_data):
        sql = 'update player set data=\"{0}\" where username=\"{1}\"'.format(player_data.replace('"', '\\"'), username)
        try:
            self.cursor.execute(sql)
            return True
        except Exception:
            print 'update player data failed!'
            return False

Player.py

import json
# the data save in mysql
class PlayerData(object):
    def __init__(self):
        self.coin = 0
        self.text = 'new text'

    def to_json(self):
        data = {'coin': self.coin,
                'text': self.text}
        return json.dumps(data)

    @staticmethod
    def decode(json_data):
        player_data = PlayerData()
        data = json.loads(json_data)
        player_data.coin = data['coin']
        player_data.text = data['text']
        return player_data

class Player(object):
    def __init__(self, cs):
        self.username = ""
        self.client_state = cs
        self.x = 0
        self.y = 0
        self.z = 0
        self.player_data = PlayerData()

class PlayerManager(object):
    def __init__(self):
        self.players = {}

    def is_online(self, username):
        if username in self.players:
            return True
        else:
            return False

    def get_player(self, username):
        return self.players[username]

    def add_player(self, username, player):
        self.players[username] = player

    def remove_player(self, username):
        self.players.pop(username)

Server.py

import socket
import select
import ClientState as cs
import MessageHandler as mh
import time
import struct
import Player

class TcpServer(object):
    def __init__(self, ip, port):
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.ip = ip
        self.port = port
        self.buffer_size = 1024
        self.header_size = 4
        self.read_buffer = ""
        self.connect_sockets = []
        self.client_states = {}
        self.player_manager = Player.PlayerManager()
        self.handler = mh.MessageHandler(self, self.player_manager)
        self.pingInterval = 3
        self.client_drops_time = self.pingInterval * 4

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

    # data: DataLength(4Bytes) + MessageNameLength(4Bytes) + MessageName + DataBody
    def handle_message(self, client_state):
        readIndex = 0
        read_buffer = client_state.buffer
        # check buffer > head size
        buffer_count = len(read_buffer)
        if buffer_count < self.header_size:
            return False, 0

        # check buffer > one whole data
        msg_body_size = self.parse_msg_length(read_buffer[readIndex:readIndex + self.header_size])
        if buffer_count < self.header_size + msg_body_size:
            return False, 0

        # move read index
        readIndex += self.header_size
        # parse Message Name length
        msg_name_len = self.parse_msg_length(read_buffer[readIndex:readIndex + self.header_size])

        # move read index
        readIndex += self.header_size
        # get the message name
        msg_name = read_buffer[readIndex:readIndex + msg_name_len]

        # move read index
        readIndex += msg_name_len
        # get the message body
        data_body = read_buffer[readIndex:msg_body_size + self.header_size]

        # handler message
        self.handler.handle_msg(msg_name, data_body, client_state)

        # update the client read buffer
        client_state.buffer = client_state.buffer[self.header_size + msg_body_size:]
        # self.client_states[socket].buffer = self.client_states[socket].buffer[self.header_size + msg_body_size:]

        # continue handle message
        if len(client_state.buffer) > self.header_size:
            self.handle_message(client_state)

    def send(self, cs, msg):
        # send msg to client...Build the json msg
        # msg: DataLength(4Bytes) + MessageNameLength(4Bytes) + MessageName + MessageBody
        msg_body = msg.encode()
        msg_name = msg.protoName
        msg_name_length = len(msg_name)
        data = msg_name + msg_body
        data_length = len(data) + self.header_size
        pack_format = 'I' + 'I' + str(len(data)) + 's'
        # json str is unicode, need to transform to str
        send_data = (data_length, msg_name_length, str(data))
        send_bytes = struct.pack(pack_format, *send_data)
        try:
            # send data to client socket. do the complete send data later
            cs.socket.send(send_bytes)
        except socket.error:
            print cs.addr, 'socket close on send!'

    def check_ping(self):
        now = time.time()
        # check all client ping time
        for cs in self.client_states.values():
            if now - cs.last_ping_time > self.client_drops_time:
                # close client
                print '[GameServer] close client:', cs.addr
                cs.socket.close()
                self.connect_sockets.remove(cs.socket)
                self.client_states.pop(cs.socket)
                self.player_manager.remove_player(cs.player.username)

    def receive(self, read_fds):
        for fd in read_fds:
            try:
                if fd is self.server:
                    # client connect
                    client_socket, client_addr = self.server.accept()
                    print '[GameServer] ', client_addr, 'connected'
                    self.connect_sockets.append(client_socket)
                    self.client_states[client_socket] = cs.ClientState(client_socket, client_addr, time.time())
                else:
                    # client send message to server
                    receive_data = fd.recv(self.buffer_size)
                    if receive_data:
                        # add data to client read buffer
                        self.client_states[fd].buffer += receive_data
                        self.handle_message(self.client_states[fd])
                    else:
                        # client quit
                        self.player_manager.remove_player(self.client_states[fd].player.username)
                        self.connect_sockets.remove(fd)
                        self.client_states.pop(fd)
                        fd.close()
            except socket.error:
                self.player_manager.remove_player(self.client_states[fd].player.username)
                if fd in self.connect_sockets:
                    self.connect_sockets.remove(fd)
                if fd in self.client_states:
                    self.client_states.pop(fd)
                fd.close()

    def run(self):
        self.server.bind((self.ip, self.port))
        self.server.listen(5)
        self.connect_sockets.append(self.server)
        print "[GameServer] GameServer Start."
        while True:
            read_fds, write_fds, error_fds = select.select(self.connect_sockets, [], [], 1)
            # time.sleep(3)
            self.receive(read_fds)
            # check client last ping time
            self.check_ping()

# run server
server = TcpServer("127.0.0.1", 8888)
server.run()

MySQL數(shù)據(jù)庫中創(chuàng)建兩個(gè)表梳凛,一個(gè)是account耿币,一個(gè)是player。
account存放的是玩家的username和password韧拒,使用text或char都行淹接。

player中存放的是player的username和data,data的類型是text叭莫,存放的是json格式的字符串蹈集。


image.png

4、結(jié)語

《Unity網(wǎng)絡(luò)游戲?qū)崙?zhàn)》這本書前面七章都比較簡(jiǎn)單雇初,后面8-12章是做一個(gè)坦克大戰(zhàn)游戲,比較難的部分就是同步算法的使用减响,其他相對(duì)都比較簡(jiǎn)單靖诗。

加油,好好學(xué)習(xí)天天向上支示!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末刊橘,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子颂鸿,更是在濱河造成了極大的恐慌促绵,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異败晴,居然都是意外死亡浓冒,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門尖坤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來稳懒,“玉大人,你說我怎么就攤上這事慢味〕“穑” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵纯路,是天一觀的道長(zhǎng)或油。 經(jīng)常有香客問我,道長(zhǎng)驰唬,這世上最難降的妖魔是什么装哆? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮定嗓,結(jié)果婚禮上蜕琴,老公的妹妹穿的比我還像新娘。我一直安慰自己宵溅,他們只是感情好凌简,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著恃逻,像睡著了一般雏搂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上寇损,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天凸郑,我揣著相機(jī)與錄音,去河邊找鬼矛市。 笑死芙沥,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的浊吏。 我是一名探鬼主播而昨,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼找田!你這毒婦竟也來了歌憨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤墩衙,失蹤者是張志新(化名)和其女友劉穎务嫡,沒想到半個(gè)月后甲抖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡心铃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年准谚,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片于个。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡氛魁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出厅篓,到底是詐尸還是另有隱情秀存,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布羽氮,位于F島的核電站或链,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏档押。R本人自食惡果不足惜澳盐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望令宿。 院中可真熱鬧叼耙,春花似錦、人聲如沸粒没。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽癞松。三九已至爽撒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間响蓉,已是汗流浹背硕勿。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留枫甲,地道東北人源武。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像言秸,于是被迫代替她去往敵國和親软能。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345