写在最前面:
我觉得现在做Hololens开发,整体的风气有一个很大问题,就是太浅,太上层,太依靠MRHoloTooklit,微软出的这一套SDK封装了很多功能,虽然好用,但是颇为上层,存在一些局限性,而且实现某些复杂功能时,会有额外的开销。Hololens终究是一款移动平台,性能的瓶颈还是挺明显的。
所以摆脱MRTK(Mixed Reality Toolkit-Unity),直接用unity的 UnityEngine.XR.WSA提供的API来开发应用是开发Hololensy的长久之计,最近我也在看MRTK的源码,奈何技术不到家,读起来磕磕绊绊的。
大多数开发者开发Hololens的通信功能是最先想到的是system.net.socket库里的socket,发布UWP的时候就可能出问题,因为UWP对system库不是完全的支持,很多方法或者类是没有定义的(这是一个很常见的发布UWP的报错)。本文用的system.net.socket里的SAEA系列,全称:SocketAsyncEvnetArgs,这是微软针对高并发而设计的一套API
SAEA是异步的socket参数,使用SAEA时需要注意三点:1.缓冲区 2.IP 3.完成后的回调,这三点是必要的,其次还有其他的SAEA参数,不是必要的,例如UserToken等,详细可查API。
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System;
using System.Text;
//这个脚本是hololens端的SocketUDP脚本,提供发送方法,初始化并开启接收方法
public class MyUdpClient : MonoBehaviour
{
Socket socket; //目标socket
//发送端口
EndPoint serverEnd;
IPEndPoint ipEnd;
//接收端口
IPEndPoint IPLocalPoint;
//发送用的socket异步参数
SocketAsyncEventArgs socketAsyceArgs;
//接收用的socket异步参数
SocketAsyncEventArgs reciveArgs;
//接收SAEA用来接收的缓冲区
byte[] reciveArgsBuffer;
//初始化
void InitSocket()
{
//定义连接的服务器ip和端口,可以是本机ip,局域网,互联网
ipEnd = new IPEndPoint(IPAddress.Parse("10.100.172.226"), 8001);
//初始化要接收的IP,IPAddress.Any表示接收所有IP地址发来的字节流
IPLocalPoint = new IPEndPoint(IPAddress.Any, 8002);
//初始化socket
socket = new Socket(IPLocalPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
//定义服务端
IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);
serverEnd = (EndPoint)sender;
//初始化发送用的SAEA
socketAsyceArgs = new SocketAsyncEventArgs();
//设置发送用的SAEA的IP
socketAsyceArgs.RemoteEndPoint = ipEnd;
//初始化接收用的SAEA的缓冲区,此处我设为10K
reciveArgsBuffer = new byte[1024 * 10];
//初始化接收SAEA
reciveArgs = new SocketAsyncEventArgs();
//设置接收SAEA的接收IP地址
reciveArgs.RemoteEndPoint = IPLocalPoint;
//因为SAEA系列API 是异步方法,所以设置好完成方法后的回调
reciveArgs.Completed += new EventHandler<SocketAsyncEventArgs>(CompletedRecive);
//设置接收缓冲区
reciveArgs.SetBuffer(reciveArgsBuffer, 0, reciveArgsBuffer.Length);
}
//异步方法完成后的complete时间
private void CompletedRecive(object sender, SocketAsyncEventArgs e)
{
//通过SAEA.LastOperation这个枚举来判断完成的是什么方法,对应不同的操作
switch (reciveArgs.LastOperation)
{
//因为reciveArgs是我专门用来接收的SAEA,所以这里只设置一个完成接收后用的方法
case SocketAsyncOperation.ReceiveFrom:
PocessReceiveFrom(e);
break;
}
}
//中转缓冲区,将数据拷贝出来给主线程用
byte[] tempBytes;
//用来通知主线程的参数
bool isOk=false;
//注意:处理这个方法是辅线程,不要用Unity的类,否则报错,将收到的字节流拷贝出来,通知主线程来处理
//接收完成后对应的处理方法
public void PocessReceiveFrom(SocketAsyncEventArgs e)
{
if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success)
{
//这里会造成内存垃圾以及内存碎片化,如果频繁的长时间的接收,建议做一个Byte池。
tempBytes = new byte[e.BytesTransferred]; //将数据拷贝出来保证可以复用
Array.Copy(e.Buffer, e.Offset, tempBytes, 0, tempBytes.Length);
//通知主线程
isOk = true;
}
}
/// <summary>
/// 异步发送消息方法
/// </summary>
/// <param name="bytes"></param>
public void AsyncSend(byte[] bytes)
{
//设置缓冲区,缓冲区里是发送的字节流
socketAsyceArgs.SetBuffer(bytes, 0, bytes.Length);
//Debug.Log("socket异步参数字节流长度 " + socketAsyceArgs.Buffer.Length);
bool bo = socket.SendToAsync(socketAsyceArgs);
if (!bo)
{
//在hololens上发现过一段时间scoket就不会发送数据,最后这样处理:判断SentToAsync方法失败后,就重新new一个SAEA,解决socket发送失败的问题
//注意初始化一个SAEA时,1.IP 2.缓冲区,3.完成后的回调事件 这三个都是必要的,
socketAsyceArgs = new SocketAsyncEventArgs();
socketAsyceArgs.RemoteEndPoint = ipEnd;
}
}
//初始化socket并测试一下
private void Start()
{
InitSocket();
TestSocekt();
}
//用来测试socket的方法,发送一个信息
void TestSocekt() {
int tempInt = 9999;
byte[] tempBytes;
tempBytes=BitConverter.GetBytes(tempInt);
AsyncSend(tempBytes);
}
private void Update()
{
if (isOk)
{
//对tempBytes进行处理
int temp= BitConverter.ToInt32(tempBytes, 0);
Debug.Log("接收socket,接收到了字节流,接收到的数字为 " + temp);
isOk = false;
}
}
//每隔一段时间就接受一下
private void FixedUpdate()
{
socket.ReceiveFromAsync(reciveArgs);
}
}
文中的接收模块和发送模块写在一起了。
SAEA系列是异步的,所以使用起来对于多线程需要一些了解。
一般的socket需求用上面的代码足够用的,由于上文中只有一个接收SAEA和一个发送SAEA,所以当一个SAEA在工作时,不要再让这个SAEA工作,(有高并发需求时,你需要一个SAEA池,里面放了很多SAEA,将空闲的SAEA拿来使用,微软希望开发者这么使用SAEA,来解决高并发)
后来发现在MixedRealTooklit里面有scoket组件,可以直接使用MRTK中Sharing文件夹中的组件,或者查看MRTK的源码,里面是用Windows.Networking和Task写的Socket,找了很长时间的SocketAPI,原来远在天边近在眼前,感叹当时怎么不好好看看MRTK!! 不过此时我已经用SAEA的方法写出来Hololens的sockt模块了~ 没有用MRTK的那一套。