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;
}
}
客户端界面就是简单的几个按钮,用于测试网络模块:
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、运行结果
客户端能够正确解析服务器发来的消息。然后客户端每个3秒给服务器发送一个ping协议维持心跳,但是服务器还没实现ping和pong协议,因此过一段时间之后客户端就会断开连接。正常运行。