unity和python相互之间通过socket通信来发送自定义数据包是一个利用unity构建场景和通过python来做数据处理的方式,能够有效的利用两种不同语言的优势。
我已经将对应的操作封装为对应的一个模块,SocketTools.cs,我们先来看一下具体的代码用法(这段代码是一个简单的将unity主相机渲染的图像经过编码处理后通过socket发送给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);
}
}
}
}
下面是对应的代码效果:
unity+python
下面将会针对一些技术细节做详细的说明,如果您对此实现的细节不感兴趣,您也可以直接下载我已经完成的项目代码:
接下来是详细的技术实现过程,总的来讲主要技术手段分为三个部分:
1. python和unity异步socket的实现
2. python和unity处理自定义数据包的拆分和合并的实现
3. python和unity两者socket通信的数据包认证实现
4. unity数据处理异步封装逻辑
1. python和unity异步socket的实现
a. python异步socket
由于此处python主要是作为服务端的,所以此处pytho代码主要是讲python作为socket tcp server来实现的。
首先是初始化服务器代码
# 初始化服务器
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))
然后是启动服务器监听代码,首先 start 开启一个新的线程用于监听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 异步socket
unity的异步socket相较而言会比较麻烦一点,因为它的异步状态每次都需要手动的通过代码去复位调整,下面将针对其过程做具体的说明。
关于整个流程我觉得用流程图会比较合适,下面是代码的一个简单的流程图:
其中各个地方的代码内容如下所示:
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#socket的代码。
2. python和unity处理自定义数据包的拆分和合并的实现
但是大家都知道,tcp是流式传输协议,特别是针对图像这类大的数据包是不能够很好的实现帧之间的拆分的,我们需要自定义协议的数据包头和数据包体来实现不定长的数据传输,接下来讲一下如何在python和c#中实现字节层面上的数据拆分和组合。
a. python
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
在unity的c#中,为了便于解析对应的数据帧头的内容,采用的是将对应的byte[]类型的变量直接转换为struct的方式,对应的两者相互转换的代码如下:
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;
}
其次就是一些简单的内存操作,由于c#封装的缘故,我们并不能像c++那样直接通过指针的方式访问内存数据,我们需要借助Buffer类,具体的用法如下所示:
//用于同时操作内存块中的数据
public static void BlockCopy (Array src, int srcOffset, Array dst, int dstOffset, int count);
它可以将src起始位置偏移srcOffset的数据截取count位复制到dst起始位置偏移dstOffset的位置后面count长度的内粗区域中。
如果要截取一个byte[]类型的变量指定范围的数据,需要用到如下的函数:
bytes.Skip(recvLenReal).Take(bytes.Length - recvLenReal).ToArray();
从头跳过recvLenReal的长度然后截取bytes.Length - recvLenReal长的数据并将其转换为byte[]类型的变量。
3. 数据帧封装逻辑
关于自定义数据包的实现主要分为两部分:数据头和数据主体。
数据包帧头的定义方式如下:
数据头 | 数据类型 | 数据主体长度 | ||||
两个字节的数据头用于进行协议的区分辨认;一个字节的数据类型用于标识后续的数据主体的数据类型,例如:图片或者文字或者其他的内容;四个字节的数据主体长度共同组成一个int类型的变量,可以指示后续的数据总长度。
其中该数据头在unity和python的实现方法如下图所示:
public struct DataHeader
{
public byte header1;
public byte header2;
public byte data_type;
public int data_len;
}
python部分的代码上面有展示,此处不再重复。
4. python和unity两者socket通信的数据包认证实现
关于数据包认证的代码,两者采用的是同一个逻辑此处仅提供对应的逻辑处理注释内容,只是在代码的实现上稍有不同。
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);
}
}
}
总结
此处仅仅提供对应的关键步骤的代码实现手段和原理讲解,如有错误欢迎大家指正!