Comunicación de socket Unity + Python, paquete de datos personalizado

Unity y Python se comunican entre sí a través de sockets para enviar paquetes de datos personalizados. Esta es una forma de utilizar Unity para crear escenas y realizar procesamiento de datos a través de Python, que puede aprovechar eficazmente las ventajas de dos lenguajes diferentes.

He encapsulado la operación correspondiente en un módulo correspondiente, SocketTools.cs. Primero echemos un vistazo al uso del código específico (este código es un código simple que codifica la imagen renderizada por la cámara principal de Unity y la envía a Python a través del socket .Y recibir un código con el que Python procesa los resultados de la imagen y los muestra):

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using System.Text;
using System.Runtime.InteropServices;
using System.Linq;
using UnityEngine.UI;




public class HttpClient : MonoBehaviour
{
    public Camera cam;

    //关于相机的参数
    public RawImage rawImage;

    private float timeOld = 0;
    private RenderTexture cameraView = null;
    private Texture2D screenShot = null;
    private Texture2D texture = null;
    private byte[] image_recv_bytes = new byte[0];

    SocketTools tools = new SocketTools();
    private bool renderState = false;

    // Start is called before the first frame update
    void Start()
    {
        cameraView = new RenderTexture(Screen.width, Screen.height, 8);
        cameraView.enableRandomWrite = true;
        texture = new Texture2D(500, 400);
        Rigidbody rigidbody = GetComponent<Rigidbody>();
        rigidbody.angularVelocity = new Vector3(0,0.2f,0);

        timeOld = Time.time;
        tools.SocketInit();
        tools.SetCallBackFun(CallBackFun);
        tools.ConnectServer("127.0.0.1", 4444);
    }

    public void CallBackFun(SocketStatus socketStatus, System.Object obj)
    {
        switch (socketStatus)
        {
            case SocketStatus.eSocketConnecting:
                Debug.Log("正在连接服务器...");
                break;
            case SocketStatus.eSocketConnected:
                Debug.Log("服务器连接成功...");
                break;
            case SocketStatus.eSocketReceived:
                {
                    //Debug.Log("接收到消息:" + System.Text.Encoding.Default.GetString((byte[])obj));
                    image_recv_bytes = (byte[])obj;
                }
                break;
//             case SocketStatus.eSocketSending:
//                 Debug.Log("正在发送消息...");
//                 break;
//             case SocketStatus.eSocketSent:
//                 Debug.Log("消息已发送...");
//                 break;
            case SocketStatus.eSocketClosed:
                Debug.Log("服务器已断开...");
                break;
            default:
                break;
        }
    }

    // Update is called once per frame
    void Update()
    {
        if (tools.Connected && renderState)
        {
            renderState = false;
            if (null == screenShot)
            {
                screenShot = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false);
            }
            // 读取屏幕像素进行渲染
            cam.targetTexture = cameraView;
            cam.Render();
            RenderTexture.active = cameraView;
            screenShot.ReadPixels(new Rect(0, 0, cameraView.width, cameraView.height), 0, 0);
            screenShot.Apply();
            byte[] bytes = screenShot.EncodeToJPG();
//             //实例化一个文件流--->与写入文件相关联  
//             FileStream fs = new FileStream("test_unity.jpg", FileMode.Create);
//             //开始写入  
//             fs.Write(bytes, 0, bytes.Length);
//             //清空缓冲区、关闭流  
//             fs.Flush();
//             fs.Close();
//             return;
            cam.targetTexture = null;
            try
            {
                timeOld = Time.time;
                tools.SendToServer(tools.PackData(bytes, 1));
            }
            catch
            {
                if (!tools.Connected)
                {
                    tools.SocketClose();
                    Debug.Log("the server has been disconnected. ");
                }
            }
        }

        if (image_recv_bytes != null)
        {
            Debug.Log("图片大小:" + image_recv_bytes.Length + "bytes, 帧间延时:" + Time.deltaTime * 1000 + "ms, 网络时间延迟:" + ((Time.time - timeOld) * 1000).ToString() + "ms");
            texture.LoadImage(image_recv_bytes);
            image_recv_bytes = null;
            rawImage.texture = texture;
            renderState = true;
        }

        if (Input.GetKeyDown(KeyCode.Space))
        {
            if (tools.Connected)
            {
                tools.SocketClose();
                Debug.Log("socket closed. ");
            }
            else
            {
                tools.ConnectServer("127.0.0.1", 4444);
            }
        }

    }

}

El siguiente es el efecto de código correspondiente:

unidad+python

Algunos detalles técnicos se explicarán en detalle a continuación. Si no está interesado en los detalles de esta implementación, también puede descargar directamente el código del proyecto que completé:

Unity y Python implementan recursos de comunicación asincrónica a través del protocolo de socket personalizado: biblioteca CSDN

A continuación se detalla el proceso de implementación técnica. En términos generales, los principales medios técnicos se dividen en tres partes:

1. Implementación de socket asíncrono en python y unity

2. Implementación de dividir y fusionar paquetes de datos personalizados en Python y Unity

3. Implementación de autenticación de paquetes para comunicación de socket entre Python y Unity.

4. Lógica de encapsulación asíncrona de procesamiento de datos de Unity

1. Implementación de socket asíncrono en python y unity

A. socket asíncrono de Python

Dado que Python aquí se usa principalmente como servidor, el código de Python aquí se implementa principalmente usando Python como un servidor TCP de socket.

Primero está el código del servidor de inicialización.

# 初始化服务器
    def socket_init(self, host, port):
        # 1 创建服务端套接字对象
        self.tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # signal_update.emit(0.2, 1, 1, 'socket object created...')
        # 设置端口复用,使程序退出后端口马上释放
        self.tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
        # 2 绑定端口
        self.tcp_server.bind((host, port))
        # signal_update.emit(0.7, 1, 1, "socket bind successfully...")
        print('server port bind successfully, server host: {}, server port: {}...'.format(host, port))

Luego inicie el código de monitoreo del servidor, primero inicie un nuevo hilo para monitorear el estado del servidor TCP y luego implemente el código correspondiente en la función del hilo start_thread_fun, de la siguiente manera:

# 开始的线程函数,外部最好调用 start() 函数,不要调用此函数
    # 否则会阻塞
    def start_thread_fun(self):
        print("server_thread started. ")
        # 3 设置监听
        self.tcp_server.listen(self.max_connections)
        print('start to listen connections from client, max client count: {}'.format(
            self.max_connections))
        # 4 循环等待客户端连接请求(也就是最多可以同时有128个用户连接到服务器进行通信)
        while True:
            tcp_client_1, tcp_client_address = self.tcp_server.accept()
            self.tcp_clients.append(tcp_client_1)
            # 创建多线程对象
            thd = threading.Thread(target=self.client_process,
                                   args=(tcp_client_1, tcp_client_address), daemon=True)
            # 设置守护主线程  即如果主线程结束了 那子线程中也都销毁了  防止主线程无法退出
            thd.setDaemon(True)
            # 启动子线程对象
            thd.start()
            print("new client connected, client address: {}, total client count: {}".format(tcp_client_address, 1))

    # 启动服务器
    def start(self):
        self.start_thread = threading.Thread(target=self.start_thread_fun, daemon=True)
        self.start_thread.start()
        print("starting server_thread...")

Nota: El hilo principal no se puede terminar aquí; de lo contrario, todos los subprocesos se detendrán. Puede agregar time.sleep (100) al hilo principal para retrasar manualmente el tiempo de ejecución del hilo principal.

B. socket asíncrono de unidad

En comparación, el socket asincrónico de Unity es un poco más problemático, porque su estado asincrónico debe restablecerse y ajustarse manualmente mediante código cada vez. El proceso se explicará en detalle a continuación.

Con respecto a todo el proceso, creo que sería más apropiado utilizar un diagrama de flujo, aquí hay un diagrama de flujo simple del código:

 El contenido del código en cada lugar es el siguiente:

public void SocketInit()
    {
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    }

public void ConnectServer(string host, int port)
    {
        try
        {
            SocketAsyncEventArgs args = new SocketAsyncEventArgs();//创建连接参数对象
            this.endPoint = new IPEndPoint(IPAddress.Parse(host), port);
            args.RemoteEndPoint = this.endPoint;
            args.Completed += OnConnectedCompleted;//添加连接创建成功监听
            socket.ConnectAsync(args); //异步创建连接
        }
        catch (Exception e)
        {
            Debug.Log("服务器连接异常:" + e);
        }
    }

private void OnConnectedCompleted(object sender, SocketAsyncEventArgs args)
    {
        try
        {   ///连接创建成功监听处理
            if (args.SocketError == SocketError.Success)
            {
                StartReceiveMessage(); //启动接收消息

            }
        }
        catch (Exception e)
        {
            Debug.Log("开启接收数据异常" + e);
        }

    }


    private void StartReceiveMessage(int bufferSize = 40960)
    {
        //启动接收消息
        SocketAsyncEventArgs receiveArgs = new SocketAsyncEventArgs();
        //设置接收消息的缓存大小,正式项目中可以放在配置 文件中
        byte[] buffer = new byte[bufferSize];
        //设置接收缓存
        receiveArgs.SetBuffer(buffer, 0, buffer.Length);
        receiveArgs.RemoteEndPoint = this.endPoint;
        receiveArgs.Completed += OnReceiveCompleted; //接收成功
        socket.ReceiveAsync(receiveArgs);//开始异步接收监听
    }

public void OnReceiveCompleted(object sender, SocketAsyncEventArgs args)
    {
        try
        {
            //Debug.Log("网络接收成功线程:" + Thread.CurrentThread.ManagedThreadId.ToString());

            if (args.SocketError == SocketError.Success && args.BytesTransferred > 0)
            {
                //创建读取数据的缓存
                byte[] bytes = new byte[args.BytesTransferred];
                //将数据复制到缓存中
                Buffer.BlockCopy(args.Buffer, 0, bytes, 0, args.BytesTransferred);
                Debug.Log(bytes);
                //再次启动接收数据监听,接收下次的数据。
                StartReceiveMessage();
            }
        }
        catch (Exception e)
        {
            Debug.Log("接收数据异常:" + e);
        }
    }

public void SendToServer(byte[] data)
    {
        try
        {
            //创建发送参数
            SocketAsyncEventArgs sendEventArgs = new SocketAsyncEventArgs();
            sendEventArgs.RemoteEndPoint = endPoint;
            //设置要发送的数据
            sendEventArgs.SetBuffer(data, 0, data.Length);
            sendEventArgs.Completed += OnSendCompleted;
            //异步发送数据
            socket.SendAsync(sendEventArgs);

        }
        catch (Exception e)
        {
            Debug.Log("发送数据异常:" + e);
        }
    }

    public void OnSendCompleted(object sender, SocketAsyncEventArgs args)
    {
        if (args.SocketError == SocketError.Success)
        {
            Debug.Log("send ok");
        }
    }

public void SocketClose()
    {
        socket.Close();
    }

El código contiene algunos de los códigos de socket C# asíncronos más simples.

2. Implementación de dividir y fusionar paquetes de datos personalizados en Python y Unity

Pero todo el mundo sabe que TCP es un protocolo de transmisión por secuencias. Especialmente para paquetes de datos grandes, como imágenes, no se puede dividir bien entre cuadros. Necesitamos personalizar el encabezado del paquete de datos y el cuerpo del paquete de datos del protocolo. Para lograr datos de longitud variable transmisión, hablemos sobre cómo dividir y combinar datos a nivel de bytes en Python y C#.

a. pitón

Python utiliza principalmente las funciones struct.pack y struct.unpack para dividir y combinar datos. El código específico se muestra a continuación:

    # 判断当前的数据头对不对,如果正确返回解析结果
    def process_protocol(self, header):
        header_unpack = struct.unpack(self.header_format, header)
        if header_unpack[0] == self.header_bytes:
            return True, header_unpack
        else:
            return False, None

    def pack_data(self, data, data_type):
        if type(data) is str:
            data = data.encode()
        data_pack = struct.pack(self.header_format, self.header_bytes, data_type, len(data))
        # print("datalen:{}".format(len(data)))
        return data_pack+data


# 部分相关变量定义
self.header_bytes = b'\x0a\x0b'  # 占用两个字节
self.header_format = "2ssi"


b. unidad

En C# de Unity, para facilitar el análisis del contenido del encabezado del marco de datos correspondiente, el método utilizado es convertir directamente la variable de tipo byte [] correspondiente en una estructura. El código correspondiente para convertir los dos entre sí es el siguiente sigue:

public static object BytesToStruct(byte[] buf, int len, Type type)
    {
        object rtn;
        IntPtr buffer = Marshal.AllocHGlobal(len);
        Marshal.Copy(buf, 0, buffer, len);
        rtn = Marshal.PtrToStructure(buffer, type);
        Marshal.FreeHGlobal(buffer);
        return rtn;
    }

public static byte[] StructToBytes(object structObj)
    {
        //得到结构体的大小
        int size = Marshal.SizeOf(structObj);
        //创建byte数组
        byte[] bytes = new byte[size];
        //分配结构体大小的内存空间
        IntPtr structPtr = Marshal.AllocHGlobal(size);
        //将结构体拷到分配好的内存空间
        Marshal.StructureToPtr(structObj, structPtr, false);
        //从内存空间拷到byte数组
        Marshal.Copy(structPtr, bytes, 0, size);
        //释放内存空间
        Marshal.FreeHGlobal(structPtr);
        //返回byte数组
        return bytes;
    }

El segundo son algunas operaciones de memoria simples. Debido a la encapsulación de C#, no podemos acceder a los datos de la memoria directamente a través de punteros como C++. Necesitamos usar la clase Buffer. El uso específico es el siguiente:

//用于同时操作内存块中的数据
public static void BlockCopy (Array src, int srcOffset, Array dst, int dstOffset, int count);

Puede copiar los bits de conteo de interceptación de datos del desplazamiento de la posición inicial de src srcOffset al área gruesa interna de la longitud de conteo detrás del desplazamiento de la posición inicial de dst dstOffset.

Si desea interceptar los datos en el rango especificado de una variable de tipo byte[], debe utilizar la siguiente función:

bytes.Skip(recvLenReal).Take(bytes.Length - recvLenReal).ToArray();

Omita la longitud de recvLenReal desde el principio y luego intercepte bytes.Length: datos largos de recvLenReal y conviértalos en una variable de tipo byte [].

3. Lógica de encapsulación de marcos de datos

La implementación de paquetes de datos personalizados se divide principalmente en dos partes: encabezado de datos y cuerpo de datos.

El encabezado del paquete se define de la siguiente manera:

encabezado de datos tipo de datos Longitud del interesado
             

El encabezado de datos de dos bytes se usa para distinguir el protocolo; el tipo de datos de un byte se usa para identificar el tipo de datos del cuerpo de datos posterior, como: imágenes o texto u otro contenido; la longitud del cuerpo de datos de cuatro bytes juntos Forman una variable de tipo int, que puede indicar la longitud total de los datos posteriores.

El método de implementación de este encabezado de datos en Unity y Python se muestra en la siguiente figura:

public struct DataHeader
{
    public byte header1;
    public byte header2;
    public byte data_type;
    public int data_len;
    
}

La parte del código de Python se muestra arriba y no se repetirá aquí.

4. Implementación de autenticación de paquetes para comunicación de socket entre Python y Unity.

Con respecto al código de autenticación del paquete de datos, ambos usan la misma lógica. Aquí solo se proporcionan las anotaciones de procesamiento lógico correspondientes, pero existen ligeras diferencias en la implementación del código.

    def process_raw_data(self, recv_data):
        '''
        关于操作:
            本函数应该具有递归功能,否则无法处理复杂任务
        关于消息接收的逻辑处理:
        1. 首先判断当前是否已经接收过帧头 (self.frame_info.data_type is not None)
            接收过:
                根据帧头的数据长度接收对应的数据体内容
            没接收过:
            判断当前接收的数据长度是否满足帧头的长度
                满足:尝试解析
                    解析失败:正常传输数据
                    解析成功:如果有其他的数据,继续接收处理后续的数据
                不满足:将本次数据输出,丢弃此次的数据 !!!
        '''
        # 如果已经接收过数据头,直接继续接收内容
        if self.frame_info.data_type is not None:
            # 首先计算剩余的数据包长度
            recv_len_left = self.frame_info.data_all_length - self.frame_info.data_recv_length
            # 然后计算本次可以接收的数据长度,选取数据长度和剩余接收长度的最小值
            recv_len_real = min(recv_len_left, len(recv_data))
            self.frame_info.data_buffer += recv_data[:recv_len_real]
            # 更新对应的接收的数据长度
            self.frame_info.data_recv_length = len(self.frame_info.data_buffer)
            # 判断当前是否已经接受完本帧的内容
            if self.frame_info.data_recv_length >= self.frame_info.data_all_length:
                # 根据回调函数返回对应的内容
                if self.callback_fun is not None:
                    self.callback_fun(self.frame_info.data_buffer)
                # 从剩余的数据中尝试检索出对应的数据头
                # 首先更新 recv_data 的数据的内容
                # print(self.frame_info.data_buffer)
                self.frame_info.reset()
                recv_data = recv_data[recv_len_real:len(recv_data)]
                if len(recv_data) != 0:
                    self.process_raw_data(recv_data)
            else:
                return
            # 从剩余的数据中尝试解析数据头
        else:
            if len(recv_data) >= self.header_length:
                ret = self.process_protocol(recv_data[:self.header_length])
                if ret[0]:
                    # 打印出协议对应的内容
                    # print(ret[1])
                    self.frame_info.set(ret[1][1], ret[1][2])
                    # 此处还得继续判断当前是否转换完了,如果没有的话需要继续转换接收到的内容
                    recv_data = recv_data[self.header_length:len(recv_data)]
                    if len(recv_data) != 0:
                        self.process_raw_data(recv_data)
                else:
                    print(recv_data)
            else:
                print(recv_data)
    private void ProcessRawData(ref byte[] bytes)
    {
//         关于操作:
//             本函数应该具有递归功能,否则无法处理复杂任务
//         关于消息接收的逻辑处理:
//         1.首先判断当前是否已经接收过帧头(self.frame_info.data_type is not None)
//             接收过:
//                 根据帧头的数据长度接收对应的数据体内容
//             没接收过:
//             判断当前接收的数据长度是否满足帧头的长度
//                 满足:尝试解析
//                     解析失败:正常传输数据
//                     解析成功:如果有其他的数据,继续接收处理后续的数据
//                 不满足:将本次数据输出,丢弃此次的数据!!!
        
        if (frameInfo.sPyDataTest.data_type != 0)
        {
            int recvLenLeft = frameInfo.sPyDataTest.data_len - frameInfo.data_recv_len;
            int recvLenReal = Math.Min(recvLenLeft, bytes.Length);
            frameInfo.AddBufferData(bytes.Skip(0).Take(recvLenReal).ToArray());
            frameInfo.data_recv_len = frameInfo.GetBufferLength();
            if (frameInfo.data_recv_len >= frameInfo.sPyDataTest.data_len)
            {
                //Debug.Log("接收成功!");
                socketStatus = SocketStatus.eSocketReceived;
                if (callBackFun != null)
                {
                    callBackFun(socketStatus, frameInfo.data_buffer);
                }
                frameInfo.Reset();
                bytes = bytes.Skip(recvLenReal).Take(bytes.Length - recvLenReal).ToArray();
                if (bytes.Length != 0)
                {
                    ProcessRawData(ref bytes);
                }
            }
            else
            {
                return;
            }
        }
        else
        {
            if (recvBuffer != null)
            {
                byte[] newBytes = new byte[recvBuffer.Length + bytes.Length];
                Buffer.BlockCopy(recvBuffer, 0, newBytes, 0, recvBuffer.Length);
                Buffer.BlockCopy(bytes, 0, newBytes, recvBuffer.Length, bytes.Length);
                bytes = newBytes;
                recvBuffer = null;
            }

            if (bytes.Length >= frameInfo.StructLength)
            {
                bool ret = true;
                ret = frameInfo.IsHeaderMatch(bytes.Take(frameInfo.StructLength).ToArray());
                if (ret == true)
                {
                    //Debug.Log(frameInfo.sPyDataTest);
                    frameInfo.Set(frameInfo.sPyDataTest.data_type, frameInfo.sPyDataTest.data_len);
                    bytes = bytes.Skip(frameInfo.StructLength).Take(bytes.Length - frameInfo.StructLength).ToArray();
                    if (bytes.Length != 0)
                    {
                        ProcessRawData(ref bytes);
                    }
                }
                else
                {

                    Debug.Log(bytes);
                }
            }
            else
            {
                recvBuffer = bytes;
                Debug.Log(bytes);
            }
        }
        
    }

Resumir

Aquí solo proporcionamos métodos de implementación de código y explicaciones de principios para los pasos clave correspondientes. Si hay algún error, ¡corríjame!

Supongo que te gusta

Origin blog.csdn.net/weixin_47232366/article/details/131536238
Recomendado
Clasificación