Unity + Python ソケット通信、カスタム データ パッケージ

Unity と Python はソケットを介して相互に通信し、カスタム データ パケットを送信します。これは、Unity を使用してシーンを構築し、Python を通じてデータ処理を実行する方法であり、2 つの異なる言語の利点を効果的に活用できます。

対応する操作を対応するモジュール SocketTools.cs にカプセル化しました。最初に特定のコードの使用法を見てみましょう (このコードは、Unity のメイン カメラによってレンダリングされた画像をエンコードし、ソケット経由で Python に送信する単純なコードです)そして、Python が画像結果を処理して表示するコードを受け取ります):

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);
            }
        }

    }

}

対応するコード効果は次のとおりです。

ユニティ+Python

いくつかの技術的な詳細については、以下で詳しく説明します。この実装の詳細に興味がない場合は、私が完成したプロジェクト コードを直接ダウンロードすることもできます。

Unity と Python はカスタム ソケット プロトコルを介して非同期通信リソースを実装します - CSDN ライブラリ

次に、詳細な技術的実装プロセスですが、一般的に、主な技術的手段は次の 3 つの部分に分かれています。

1. PythonとUnityでの非同期ソケットの実装

2. PythonとUnityでのカスタムデータパケットの分割とマージの実装

3. PythonとUnity間のソケット通信におけるパケット認証の実装

4. Unity データ処理の非同期カプセル化ロジック

1. PythonとUnityでの非同期ソケットの実装

a. Python 非同期ソケット

ここでの Python は主にサーバーとして使用されるため、ここでの Python コードは主に Python をソケット TCP サーバーとして使用して実装されます。

最初は初期化サーバーのコードです

# 初始化服务器
    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))

次に、サーバー監視コードを開始します。まず、TCP サーバーのステータスを監視する新しいスレッドを開始し、次に、次のように、対応するコードを start_thread_fun スレッド関数に実装します。

# 开始的线程函数,外部最好调用 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...")

注: ここでメイン スレッドを終了することはできません。そうしないと、すべてのサブスレッドが停止します。time.sleep(100) をメイン スレッドに追加して、メイン スレッドの実行時間を手動で遅らせることができます。

b. Unity 非同期ソケット

Unity の非同期ソケットは、非同期ステータスを毎回コードで手動でリセットおよび調整する必要があるため、それに比べて少し面倒ですが、そのプロセスについては後で詳しく説明します。

全体のプロセスに関しては、フローチャートを使用する方が適切だと思います。コードの簡単なフローチャートは次のとおりです。

 それぞれの場所のコード内容は次のとおりです。

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();
    }

このコードには、最も単純な非同期 C# ソケット コードの一部が含まれています。

2. PythonとUnityでのカスタムデータパケットの分割とマージの実装

しかし、TCP がストリーミング伝送プロトコルであることは誰もが知っています。特に画像などの大きなデータ パケットの場合、フレーム間でうまく分割することができません。プロトコルのデータ パケット ヘッダーとデータ パケット本体をカスタマイズする必要があります。可変長データを実現するには、送信については、Python と C# でバイト レベルでデータを分割および結合する方法について話しましょう。

a.パイソン

Python は主に struct.pack 関数と struct.unpack 関数を使用してデータを分割および結合します。具体的なコードを以下に示します。

    # 判断当前的数据头对不对,如果正确返回解析结果
    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. 団結

UnityのC#では、対応するデータフレームヘッダーの内容を解析しやすくするために、対応するbyte[]型の変数を直接構造体に変換する方法がとられており、両者を相互に変換する対応コードは以下の通りです。以下に続きます:

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;
    }

2 番目は、いくつかの単純なメモリ操作です。C# のカプセル化により、C++ のようなポインタを介してメモリ データに直接アクセスできません。Buffer クラスを使用する必要があります。具体的な使用方法は次のとおりです。

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

src 開始位置オフセット srcOffset のデータ傍受カウント ビットを、dst 開始位置オフセット dstOffset より後ろのカウント長の内側の太い領域にコピーできます。

byte[] 型変数の指定範囲のデータをインターセプトする場合は、次の関数を使用する必要があります。

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

recvLenReal の長さを先頭からスキップし、bytes.Length - recvLenReal の長いデータをインターセプトして、byte[] 型の変数に変換します。

3. データフレームのカプセル化ロジック

カスタム データ パケットの実装は、主にデータ ヘッダーとデータ本体の 2 つの部分に分かれています。

パケットヘッダーは次のように定義されます。

データヘッダー データの種類 データ主体の長さ
             

2 バイトのデータ ヘッダーはプロトコルを区別するために使用されます。1 バイトのデータ タイプは、後続のデータ本体のデータ タイプ (写真、テキスト、その他のコンテンツなど) を識別するために使用されます。4 バイトのデータ本体の長さは一緒にこれらは int 型変数を形成し、後続のデータの合計長を示すことができます。

UnityとPythonでのこのデータヘッダーの実装方法は以下の図のようになります。

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

コードの Python 部分は上に示されているため、ここでは繰り返しません。

4. PythonとUnity間のソケット通信におけるパケット認証の実装

データ パケット認証コードに関しては、どちらも同じロジックを使用しています。ここでは、対応するロジック処理のアノテーションのみを示しますが、コードの実装には若干の違いがあります。

    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);
            }
        }
        
    }

要約する

ここでは、コードの実装方法と、対応する主要な手順の原則の説明のみを提供します。間違いがある場合は、修正してください。

おすすめ

転載: blog.csdn.net/weixin_47232366/article/details/131536238