环境
Unity版本
vs版本: 2022
python版本: Python 3.12.5
文章末尾附上python服务端的代码[仅提供运行后台主要的代码,其他我自己的业务代码就不用提供了, 对项目没影响 ]
unity
-
点开资源包管理页面
-
搜索 json 找到Newtonsoft Json 包并下载, 导入到unity项目里面
-
新建一个空物体到场景里面, 以及新建一个用于编写连接代码的脚本
剩下的内容见代码:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using UnityEngine;
using Newtonsoft.Json;
using System.Threading.Tasks;
/// <summary>
/// 定义消息通信的枚举类型
/// </summary>
public enum SocketState
{
Ping = 1,
Login = 2,
Exit = 3,
Msg = 4,
Shop = 5,
UserPool = 6,
}
public class WSSocket : MonoBehaviour
{
// 将连接设为静态暴露出去
public static WSSocket Instance;
// 连接对象
ClientWebSocket ws;
CancellationToken ct;
// 以下声明仅我自己的Demo用到
// 是否已连接
bool isConn = false;
// 是否在尝试重连
bool isReconed = false;
// 为每一次连接生成一个客户端的身份ID
string clientID;
// 事件
Action<MsgModel> action;
private void Awake()
{
Instance = this;
}
void Start()
{
clientID = Guid.NewGuid().ToString();
ClientCon();
}
/// <summary>
/// 应用退出时需要断开进程
/// </summary>
private void OnApplicationQuit()
{
CloseCon();
}
async private void CloseCon()
{
isConn = false;
if (ws != null)
{
if (!isReconed)
{
print("客户端退出");
}
await SendMsgToServiceAsync(SocketState.Exit, "{}");
await Task.Delay(1000);
ws?.Dispose();
ws = null;
}
}
public async void ClientCon()
{
try
{
ws = new ClientWebSocket();
ct = new CancellationToken();
Uri url = new("ws://127.0.0.1:8888");
await ws.ConnectAsync(url, ct);
// 连接之后,给服务器发送一个ping
await this.SendMsgToServiceAsync(SocketState.Ping, $"客户端:[{clientID}] 建立了连接");
isConn = true;
isReconed = false;
// 开始接收服务器推送的消息
while (true)
{
var result = new byte[1024];
await ws.ReceiveAsync(new ArraySegment<byte>(result), ct);//接受数据
try
{
// 将收到的字节数据进行处理, 里面可能包含了一些没用的数据
var strMsg = Encoding.UTF8.GetString(result, 0, result.Length);
if (strMsg.Contains("\0"))
{
strMsg = strMsg.Replace("\0", ""); // 替换所有的 null 字符
}
// 将字节数据转换为字符串-- 此方法在下面定义
byte[] bytes = HexStringToBytes(strMsg);
string jsonString = Encoding.UTF8.GetString(bytes);
// 使用 NewsoftJson的方法把解析出来的字符串转换为指定类型的数据 , 这里的 MsgModel 类定义在当前代码的底部,需要注意的是我的后台是python写的,转格式的时候里面的 双引号变成了单引号, 可能会在这里转换失败
MsgModel jsonObject = JsonConvert.DeserializeObject<MsgModel>($"{jsonString.Replace("'", "\"")}");
// 如果类型是 ping, 那就不执行. 并 告诉服务器, 还存活着
if (jsonObject.Type == (int)SocketState.Ping)
{
// 通知服务端,我还在,后面的 1 随便写, 没有实际意义
await this.SendMsgToServiceAsync(SocketState.Ping, $"1");
continue;
}
// 在其他脚本调用此连接的时候,如果传入了 一个回调的事件.那就收到消息之后把消息发给这个事件
if (action != null)
{
action.Invoke(jsonObject as MsgModel);
}
else
{
Debug.Log(strMsg);
}
}
catch (Exception ex)
{
print($"处理服务器消息出错:{ex.Message}");
}
}
}
catch (Exception ex)
{
print($"监听服务器出错:{ex.Message}");
CloseCon();
if (ex.Message.ToString().Equals("Unable to connect to the remote server") && !isReconed)
{
isReconed = true;
GameTipManager.Instace.PushMsg("正在尝试重连服务器");
StartCoroutine(OnReconectWS());
}
}
}
/// <summary>
/// 当连接断开时,尝试每5秒重新连接一次
/// </summary>
/// <returns></returns>
IEnumerator OnReconectWS()
{
while (!isConn)
{
GameTipManager.Instace.PushMsg("正在尝试重连服务器");
ClientCon();
yield return new WaitForSeconds(5);
}
}
/// <summary>
/// 给服务器推送数据
/// </summary>
/// <param name="msg"></param>
/// <returns></returns>
public async Task SendMsgToServiceAsync(SocketState type, string msg)
{
if (ws == null)
{
GameTipManager.Instace.PushMsg("服务器连接已断开");
return;
}
MsgBodyModel msgBody = new MsgBodyModel();
msgBody.msg = msg;
// 构造消息对象
MsgModel msgObj = new(type, msgBody);
// 将消息对象转换为字符串并编码为字节数组
byte[] messageBytes = Encoding.UTF8.GetBytes(StringToHexString(msgObj.ToString()));
try
{
// 发送数据
await ws.SendAsync(new ArraySegment<byte>(messageBytes), WebSocketMessageType.Text, true, ct);
}
catch (WebSocketException ex)
{
// 捕获 WebSocket 相关的异常
Console.WriteLine($"WebSocketException: {ex.Message}");
}
catch (OperationCanceledException ex)
{
// 捕获操作被取消的异常
Console.WriteLine($"OperationCanceledException: {ex.Message}");
}
catch (Exception ex)
{
// 捕获其他异常
Console.WriteLine($"Exception: {ex.Message}");
}
}
/// <summary>
/// 设置事件,接收ws服务器返回的内容
/// </summary>
/// <param name="action"></param>
public void SetAction(Action<MsgModel> action)
{
this.action = action;
}
/// <summary>
/// 16进制转义为字符串
/// </summary>
/// <param name="hex"></param>
/// <returns></returns>
private byte[] HexStringToBytes(string hex)
{
int length = hex.Length;
byte[] bytes = new byte[length / 2];
for (int i = 0; i < length; i += 2)
{
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
}
return bytes;
}
/// <summary>
/// 将字符串转换为 16 进制表示的字符串
/// </summary>
/// <param name="str">要转换的字符串</param>
/// <returns>16 进制表示的字符串</returns>
private string StringToHexString(string str)
{
// 将字符串转换为字节数组
byte[] bytes = Encoding.UTF8.GetBytes(str);
// 使用 StringBuilder 来高效地构建 16 进制字符串
System.Text.StringBuilder hex = new System.Text.StringBuilder(bytes.Length * 2);
// 将每个字节转换为 16 进制字符串
foreach (byte b in bytes)
{
hex.AppendFormat("{0:x2}", b);
}
return hex.ToString();
}
}
/// <summary>
/// 消息数据的结构, 把收到的消息转换为这个类的结构
/// </summary>
public class MsgModel
{
public string Size { get; set; }
public int Type { get; set; }
public MsgBodyModel Body { get; set; }
public string End { get; set; }
public MsgModel() { }
/// <summary>
/// 这个主要是用来给服务器发送消息的时候结构体封装
/// </summary>
/// <param name="type"></param>
/// <param name="msg"></param>
public MsgModel(SocketState type, MsgBodyModel msg)
{
Size = msg.msg.Length.ToString();
Type = (int)type;
Body = msg;
End = "#$#";
}
/// <summary>
/// 确保有这个入口. 否则转换的时候会报错
/// </summary>
/// <param name="size"></param>
/// <param name="type"></param>
/// <param name="body"></param>
/// <param name="end"></param>
public MsgModel(string size, int type, MsgBodyModel body, string end)
{
Size = size;
Type = type;
Body = body;
End = end;
}
public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
}
/// <summary>
/// 消息里面的内容主体结构,
/// </summary>
public class MsgBodyModel
{
public string mid { get; set; } // 消息id
public string msg { get; set; } // 消息内容
public DateTime time { get; set; } // 消息时间
public int code { get; set; } // 消息返回代码
public List<string> data { get; set; } // 消息额外体
public MsgBodyModel()
{
this.mid = Guid.NewGuid().ToString();
this.msg = string.Empty;
this.time = DateTime.Now;
this.code = -1;
this.data = new List<string>();
}
}
python后台结构
server.py 入口
# @time 2024/08/13 09:47:28
# @author Eval
# @description ws服务端
import asyncio
import websockets
import json
from lib.Logger import ws_log_info, ws_log_error, ws_log_warn
from lib.HandlerEventMsg import ClientMsgRecv
class Services:
def __init__(self) -> None:
self.conf = {}
self.__load_config()
self.client = ClientMsgRecv()
def __load_config(self):
with open("./conf/config.ini", "r") as f:
data = f.read()
dataList = data.split("\n")
for con in dataList:
if con.startswith("#") or con.startswith("//"):
continue
[key, value] = con.replace(" ", "").split("=")
self.conf[key] = str(value)
ws_log_info("初始化全局配置完成")
def start_con(self) -> None:
ws_log_info(f"======WS服务已启动======")
ip_addr = self.conf.get("IP_ADDR")
ip_port = self.conf.get("IP_PORT")
ws_log_info(f"监听端口{ip_addr}:{ip_port}", True)
server = websockets.serve(self.server_run, ip_addr, int(ip_port))
asyncio.get_event_loop().run_until_complete(server)
asyncio.get_event_loop().run_forever()
async def server_recv(self, websocket: websockets.WebSocketServerProtocol):
""" 开始接收数据 """
while True:
try:
recv_text = await websocket.recv()
# 解析对象
msgObj = json.loads(bytes.fromhex(recv_text).decode("utf-8").replace("'", "\""))
await self.client.recv_msg(websocket, msgObj)
except websockets.exceptions.ConnectionClosedOK:
ws_log_error(f"[已关闭连接]客户端断开")
break
except Exception as e:
ws_log_error(f"[已关闭连接]出现错误:{e}")
break
# async def server_ping(self):
# while True:
# try:
# await self.client.send_ping()
# await asyncio.sleep(1)
# except websockets.exceptions.ConnectionClosedOK:
# ws_log_error(f"[已关闭连接]客户端断开")
# break
# except Exception as e:
# ws_log_error(f"[已关闭连接]出现错误:{e}")
# break
# 握手并且接收数据
async def server_run(self, websocket: websockets.WebSocketServerProtocol):
ws_log_info(f"有客户端进来了,{websocket.id}")
# ping_task = asyncio.create_task(self.server_ping())
try:
await self.server_recv(websocket)
finally:
pass
# ping_task.cancel()
# await ping_task
if __name__ == '__main__':
try:
Services().start_con()
except KeyboardInterrupt:
ws_log_warn("主动结束服务器")
config.ini配置的内容
IP_ADDR = 0.0.0.0
IP_PORT = 8888
EventHandler.py 消息分发器
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
@Project :PVZ_Service
@File :EventHandler.py
@IDE :PyCharm
@Author :eval--Eval
@Date :2024/8/18 下午8:16
@Describe: 事件分发控制器
"""
import websockets
from lib.MsgStructure import MsgState, MsgModelState
from typing import Dict
class EventHandler:
def __init__(self):
self.__handlerDict: Dict[MsgState, list] = {}
def register_handler(self, event_type, handler):
"""注册事件处理程序"""
if event_type not in self.__handlerDict:
self.__handlerDict[event_type] = []
self.__handlerDict[event_type].append(handler)
async def handle_event(self, msg_model: MsgModelState, ws: websockets.WebSocketServerProtocol):
"""处理事件"""
if msg_model.Type in self.__handlerDict:
for handler in self.__handlerDict[msg_model.Type]:
await handler(ws, msg_model)
break
HandlerEventMsg.py 事件处理
# @time 2024/08/13 11:20:00
# @author Eval
# @description 客户端消息接收处理器
import websockets
from lib.Logger import ws_log_warn, ws_log_info, ws_log_error
from lib.ConnectionPool import ConnectionPool
from lib.MsgStructure import get_msg_structure, MsgState, MsgModel, MsgModelState
from lib.EventHandler import EventHandler
from Services.UserService import UserService
class ClientMsgRecv:
def __init__(self) -> None:
# 初始化连接池
self.userCon = ConnectionPool()
# 初始化事件分发器
self.event_handler = EventHandler()
# 初始化用户服务类
self.user_service = UserService(self.userCon)
# 匿名连接池--登录之后移除这个连接
self.connect_list: [str, websockets.WebSocketServerProtocol] = {}
self.__reg_event()
def __reg_event(self):
"""注册事件"""
# self.event_handler.register_handler(MsgState.Ping.value, self.__client_ping)
self.event_handler.register_handler(MsgState.Login.value, self.user_service.Login)
self.event_handler.register_handler(MsgState.Register.value, self.user_service.UserRegister)
self.event_handler.register_handler(MsgState.Exit.value, self.__client_exit)
self.event_handler.register_handler(MsgState.UserPool.value, self.user_service.GetUserList)
self.event_handler.register_handler(MsgState.Msg.value, self.__client_broadcast)
# async def send_ping(self):
# """不断的给所有连接推送ping"""
# await self.userCon.send_msg_by_all("ping", MsgState.Ping)
# for con in self.connect_list:
# if self.connect_list[str(con)]["ping_count"] > self.connect_list[str(con)]["max_ping"]:
# self.close_none_con(self.connect_list[str(con)]["ws"])
# continue
# await self.connect_list[str(con)]["ws"].send(get_msg_structure("ping", MsgState.Ping))
# self.connect_list[str(con)]["ping_count"] += 1 # 每次都会加 1,
# async def __client_ping(self, *args):
# """ping请求"""
# ws: websockets.WebSocketServerProtocol = args[0]
# msgModel: MsgModelState = args[1]
# if self.connect_list.get(str(ws.id)):
# self.connect_list[str(ws.id)]["ping_count"] = 0
async def __client_exit(self, *args):
"""客户端发起退出请求"""
ws: websockets.WebSocketServerProtocol = args[0]
await ws.close()
if self.userCon.ws_exist(ws):
await self.userCon.remove(ws)
async def __client_broadcast(self, *args):
"""广播消息"""
ws: websockets.WebSocketServerProtocol = args[0]
msgModel: MsgModelState = args[1]
if not self.userCon.ws_exist(ws):
await ws.send(get_msg_structure({"msg": '请先登录!'}, MsgState.Msg))
return
ws_log_info(f"推送广播消息:{msgModel.Body}")
await self.userCon.send_msg_by_all(msgModel.Body)
async def recv_msg(self, ws: websockets.WebSocketServerProtocol, msg: MsgModel):
"""处理消息"""
# 是否在匿名连接池中 , 是否也不在已经连接的连接池中
if not self.connect_list.get(str(ws.id)) and not self.userCon.ws_exist(ws):
self.connect_list[str(ws.id)] = {
"ws": ws,
"ping_count": 0,
"max_ping": 5
}
try:
msgModel = MsgModelState(msg)
await self.event_handler.handle_event(msgModel, ws)
except websockets.ConnectionClosed as e:
ws_log_warn(f"客户端断开连接.{e}")
except Exception as e:
ws_log_error(f"处理消息出错:{e}")
MsgStructure.py 消息结构
# @time 2024/08/13 11:47:19
# @author Eval
# @description 定义消息传输模型
import json
import uuid
from datetime import datetime
from enum import Enum
class MsgModelState:
"""通信模型"""
def __init__(self, msg):
self.Size = 0
self.Type = -1
self.Body = {}
self.End = ""
self.__load(msg)
def __load(self, msg: dict):
for k in msg:
match k:
case "Size":
self.Size = int(msg[k])
case "Type":
self.Type = int(msg[k])
case "Body":
self.Body = msg[k]
case "End":
self.End = str(msg[k])
class MsgModel(Enum):
"""消息通信结构"""
Size: 0
Type: -1
Body: ""
End: ""
class MsgState(Enum):
"""消息类型"""
Ping = 1
Login = 2
Exit = 3
Msg = 4
Shop = 5
UserPool = 6
Register = 7
def get_msg_structure(msg: dict, msg_type: MsgState) -> str:
"""转义消息结构"""
# 确保每条消息都有一个ID, 方便后续拓展
if msg.get("mid") is None:
msg["mid"] = str(uuid.uuid4())
if msg.get("time") is None:
msg["time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if msg.get("code") is None:
msg["code"] = "-1"
if msg.get("data") is None:
msg["data"] = ""
if msg.get("msg") is None:
msg["msg"] = ""
return str({
"Size": len(str(msg)),
"Type": msg_type.value,
"Body": msg,
"End": "#$#"
}).encode().hex()
测试客户端
这是我用来测试连接的python客户端
import asyncio
import json
from enum import Enum
from typing import Dict
import websockets
import concurrent.futures
IP_ADDR = "127.0.0.1"
IP_PORT = "8888"
class MsgState(Enum):
"""消息类型"""
Ping = 1
Login = 2
Exit = 3
Msg = 4
Shop = 5
UserPool = 6
Register = 7
async def client_ping(websocket):
while True:
await SendMsg(websocket, MsgState.Ping, "ping")
# recv_text = await websocket.recv()
await asyncio.sleep(1)
async def client_send(websocket):
loop = asyncio.get_event_loop()
print("""
操作类型:
login: 登录
exit: 退出
msg: 推送消息
user: 在线列表
register: 注册账号
""")
with concurrent.futures.ThreadPoolExecutor() as pool:
while True:
input_type = await loop.run_in_executor(pool, input, "请输入操作类型:")
if input_type == "exit":
print('"exit", bye!')
await SendMsg(websocket, MsgState.Exit, "")
await websocket.close(reason="exit")
exit()
elif input_type == "login":
input_name = await loop.run_in_executor(pool, input, "请输入用户名:")
input_pwd = await loop.run_in_executor(pool, input, "请输入密码:")
param = {"username": input_name, "password": input_pwd}
await SendMsg(websocket, MsgState.Login, param)
elif input_type == "register":
input_name = await loop.run_in_executor(pool, input, "请输入用户名:")
input_pwd = await loop.run_in_executor(pool, input, "请输入密码:")
param = {"username": input_name, "password": input_pwd}
await SendMsg(websocket, MsgState.Register, param)
elif input_type == "msg":
input_msg = await loop.run_in_executor(pool, input, "请输入消息:")
await SendMsg(websocket, MsgState.Msg, input_msg)
elif input_type == "user":
await SendMsg(websocket, MsgState.UserPool, "")
else:
print(f"---------未知指令:{input_type}")
continue
# await asyncio.sleep(10)
async def SendMsg(ws: websockets.WebSocketClientProtocol, _type: MsgState, msg: str | dict):
msgObj = {
"Size": len(str(msg)),
"Type": _type.value,
"Body": msg,
"End": "#$#"
}
await ws.send(str(msgObj).encode().hex())
async def Recv(ws: websockets):
while True:
try:
recv_byte = await ws.recv()
recv_data = bytes.fromhex(recv_byte).decode("utf-8")
data_json: Dict = json.loads(recv_data.replace("'", "\""))
if data_json.get("Type") == MsgState.Exit.value:
print('服务器通知下线')
await ws.close(reason="exit")
print(f"服务器返回:{recv_data}")
await asyncio.sleep(0.5)
except Exception as e:
print(e)
break
async def client_run():
ipaddress = IP_ADDR + ":" + IP_PORT
uri = "ws://" + ipaddress
async with websockets.connect(uri) as websocket:
# 启动 ping 协程
ping_task = asyncio.create_task(client_ping(websocket))
recv_task = asyncio.create_task(Recv(websocket))
try:
await client_send(websocket)
finally:
ping_task.cancel()
recv_task.cancel()
await ping_task # 确保 ping_task 完成
await recv_task # 确保 recv_task 完成
if __name__ == '__main__':
print("====== client main begin ======")
try:
asyncio.run(client_run())
except Exception as e:
print(e)