unity + python 使用websocket 实现简单联机

环境

Unity版本在这里插入图片描述

vs版本: 2022

python版本: Python 3.12.5

文章末尾附上python服务端的代码[仅提供运行后台主要的代码,其他我自己的业务代码就不用提供了, 对项目没影响 ]

unity

  1. 点开资源包管理页面
    在这里插入图片描述

  2. 搜索 json 找到Newtonsoft Json 包并下载, 导入到unity项目里面
    在这里插入图片描述

  3. 新建一个空物体到场景里面, 以及新建一个用于编写连接代码的脚本
    在这里插入图片描述

剩下的内容见代码:

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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值