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ì)寫裸扶。直接上界面和代碼:
客戶端界面:
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格式的字符串蹈集。
4、結(jié)語
《Unity網(wǎng)絡(luò)游戲?qū)崙?zhàn)》這本書前面七章都比較簡(jiǎn)單雇初,后面8-12章是做一個(gè)坦克大戰(zhàn)游戲,比較難的部分就是同步算法的使用减响,其他相對(duì)都比較簡(jiǎn)單靖诗。
加油,好好學(xué)習(xí)天天向上支示!