Unity WebSocket全平台极限兼容:浏览器禁掉原生 Socket 后,WebGL 到底怎么连服务器?

TCP、UDP 在 Windows、Android、iOS 上都很好理解,直接创建 Socket 就行。

但到了 WebGL,情况完全变了:代码最终运行在浏览器里,不能继续照搬原生 Socket 模型。

MyFramework 因此把 WebSocket 分成了两条底层实现:

bash 复制代码
Native平台
NetConnectWebSocket
    ↓
ClientWebSocket

WebGL
NetConnectWebSocketWebGL
    ↓
NativeWebSocket
    ↓
JavaScript WebSocket

但更有意思的是:底层虽然完全不同,上层 NetPacket、序列化、心跳、状态机和业务执行却仍然可以保持一致。

项目地址:

GitHub - ZHOURUIH/MyFramework: Unity 商用级别开发框架,经过了多年经验沉淀.一个在unity上使用的网络游戏客户端开发框架,为unity所有使用方式提供完善的封装和管理,只需要专注于游戏逻辑的编写 · GitHub

一、非 WebGL:直接使用 ClientWebSocket

普通平台的核心对象就是:

bash 复制代码
protected ClientWebSocket mWebSocket;

连接时:

bash 复制代码
mWebSocket = new();

foreach (var item in mHeader)
{
	mWebSocket.Options.SetRequestHeader(
		item.Key,
		item.Value);
}

await mWebSocket.ConnectAsync(
	new Uri(mURL),
	default);

所以:

bash 复制代码
Windows
Android
iOS
    ↓
.NET ClientWebSocket
    ↓
WebSocket服务器

连接完成以后统一进入:

bash 复制代码
CONNECTING
↓
CONNECTED

框架自己维护 NET_STATE,业务层不需要直接判断 WebSocketState


二、WebGL 为什么必须单独实现?

WebGL 版本不再使用:

bash 复制代码
System.Net.WebSockets.ClientWebSocket

而是:

bash 复制代码
protected WebSocket mWebSocket;

这个 WebSocket 来自框架中的 NativeWebSocket

在 WebGL 真机构建下,最终会调用:

bash 复制代码
[DllImport("__Internal")]
public static extern int WebSocketConnect(int instanceId);

[DllImport("__Internal")]
public static extern int WebSocketSend(
	int instanceId,
	byte[] dataPtr,
	int dataLength);

也就是:

bash 复制代码
C#
↓
WebAssembly
↓
JSLIB
↓
Browser WebSocket API

浏览器负责真正的网络连接。

这就是 WebGL 网络和原生平台最大的区别。


三、WebGL 接收数据靠事件,而不是 ReceiveAsync 循环

Native 版本使用:

bash 复制代码
await mWebSocket.ReceiveAsync(...);

持续等待服务器数据。

WebGL 版本则注册:

bash 复制代码
mWebSocket.OnOpen += ...
mWebSocket.OnError += ...
mWebSocket.OnClose += ...
mWebSocket.OnMessage += ...

真正收到数据时:

bash 复制代码
mWebSocket.OnMessage += (byte[] data) =>
{
	mInputBuffer.addData(
		data,
		data.Length);

	...
};

因此两边的底层模型是完全不同的:

bash 复制代码
Native
await ReceiveAsync()
↓
收到数据

WebGL
Browser收到数据
↓
JavaScript回调
↓
OnMessage

但下一层马上又被统一了。


四、不管数据从哪里来,都先进入 StreamBuffer

WebGL 的 OnMessage 收到数据以后:

bash 复制代码
mInputBuffer.addData(
	data,
	data.Length);

随后继续:

bash 复制代码
while (true)
{
	PARSE_RESULT result =
		preParsePacket(...);

	if (result != PARSE_RESULT.SUCCESS)
	{
		break;
	}

	mReceiveBuffer.Enqueue(...);

	mInputBuffer.removeData(
		0,
		index);
}

这和前面 TCP 的思路非常接近:

bash 复制代码
底层收到字节
↓
StreamBuffer
↓
preParsePacket
↓
拆出完整NetPacket
↓
ReceiveBuffer

所以 NativeWebGL 真正不同的只是:

数据怎么进入框架。

进入框架以后,处理链又重新统一了。


五、业务逻辑仍然只在 update() 中执行

WebGL 回调收到消息以后并不会直接:

bash 复制代码
packet.execute();

而是先:

bash 复制代码
mReceiveBuffer.Enqueue(...);

然后在主线程 update()

bash 复制代码
while (mReceiveBuffer.Count > 0)
{
	PacketReceiveInfo info =
		mReceiveBuffer.Dequeue();

	NetPacket packet =
		parsePacket(...);

	packet.execute();

	mNetPacketFactory.destroyPacket(packet);
}

最终仍然是:

bash 复制代码
浏览器网络事件
↓
ReceiveBuffer
================
Unity主线程
↓
反序列化
↓
NetPacket.execute()

这样 WebGL 和 Native 平台的业务层完全不需要写两套逻辑。


六、发送也统一成"先排队,再真正发送"

业务层调用:

bash 复制代码
sendNetPacket(packet);

协议先完成序列化,然后进入:

bash 复制代码
mOutputBuffer.Enqueue(...);

Native 版本:

bash 复制代码
await mWebSocket.SendAsync(
	new ArraySegment<byte>(...),
	mMessageType,
	true,
	CancellationToken.None);

WebGL 版本:

bash 复制代码
await mWebSocket.Send(
	info.mData,
	info.mDataSize);

所以又变成:

bash 复制代码
业务层
↓
NetPacket
↓
序列化
↓
OutputBuffer
↓
平台相关Send

差异被封锁在最底层。


七、WebSocket 不只有 JSON

很多人一提 WebSocket 就默认:

bash 复制代码
JSON字符串

但 MyFramework 实际提供了不同实现。

例如:

bash 复制代码
NetConnectWebSocketByte
NetConnectWebSocketJson

WebGL:
NetConnectWebSocketWebGLByte
NetConnectWebSocketWebGLBit

也就是说 WebSocket 只是:

传输通道。

上层到底传:

bash 复制代码
JSON
普通Byte序列化
Bit压缩二进制

是另外一件事。

例如 NetConnectWebSocketByte 仍然会写入:

bash 复制代码
PacketSize
PacketType
Sequence
HasSign
FieldFlag
PacketBody

然后继续使用:

bash 复制代码
SerializerWrite
SerializerRead

所以前面讲过的网络协议体系,并不会因为换成 WebSocket 就全部推倒重来。


八、JSON WebSocket 则走另一条路线

NetConnectWebSocketJson 会把消息包装成:

bash 复制代码
WebSocketPacketBodyJson body = new(
	msgType,
	netPacket.writeContent(),
	timeStamp);

最后:

bash 复制代码
JsonConvert.SerializeObject(body)

消息结构更接近:

bash 复制代码
message_type
data
time

接收后再通过:

bash 复制代码
mPacketTypeStringList

找到对应消息类。

这类方案更适合:

bash 复制代码
调试方便
协议可读
对接第三方服务器
不极端追求带宽

而前面的 Bit / Byte 版本则更偏游戏内部协议。


九、心跳和连接状态也没有分裂

两套 WebSocket 都维护:

bash 复制代码
MyTimer mPingTimer;
Action mPingCallback;
NET_STATE mNetState;

连接成功:

bash 复制代码
CONNECTED
↓
启动PingTimer

定时:

bash 复制代码
if (mPingTimer.tickTimer(elapsedTime))
{
	mPingCallback.Invoke();
}

断线则统一转换成:

bash 复制代码
SERVER_CLOSE
NET_CLOSE
NONE

再通过网络状态命令通知上层。

因此业务系统面对的是:

bash 复制代码
NET_STATE

而不是:

bash 复制代码
这是ClientWebSocket错误?
还是Browser WebSocket错误?

十、真正的全平台网络抽象是什么

最终结构可以压缩成:

bash 复制代码
               NetPacket
                  ↓
           Serializer / JSON
                  ↓
             WebSocket层
            ↙           ↘
      Native              WebGL
        ↓                   ↓
ClientWebSocket        NativeWebSocket
        ↓                   ↓
     OS Socket          Browser JS API
            ↘           ↙
              Server

这套设计真正有价值的地方,不是"支持了 WebSocket"。

而是:

把平台差异压到了网络最底层。

上面的:

bash 复制代码
NetPacket
PacketType
序列化
心跳
连接状态
消息队列
主线程execute

仍然保持同一套模型。

所以当一个 Unity 项目需要同时跑:

bash 复制代码
Windows
Android
iOS
WebGL

时,业务代码不需要因为浏览器不能直接使用原生 Socket,就重新写一套网络系统。

底层可以完全不同,但上层绝不能跟着一起分裂。

相关推荐
SmalBox1 小时前
【高级着色器】Unity实现-彩虹泡泡着色器
unity3d·游戏开发·图形学
fujisheng66117 小时前
从 GetTypes() 到强类型 Route:FUI Source Generator 的设计演进
unity3d
SmalBox17 小时前
【动态着色】Unity实现3D扫描线
unity3d·游戏开发·图形学
SmalBox2 天前
【动态着色】Unity 实现消融效果
unity3d·游戏开发·图形学
_zhourui_h_2 天前
我以为 ECSList 已经够快了,直到我直接拿到了 Column
unity3d
SmalBox3 天前
【动态着色】Unity 实现全息投影着色器
unity3d·游戏开发·图形学
SmalBox4 天前
【动态着色】Unity 实现箭头图案
unity3d·游戏开发·图形学
SmalBox5 天前
【扭曲着色器】Unity实现-黑洞扭曲效果
unity3d·游戏开发·图形学
SmalBox6 天前
【扭曲着色器】Unity实现-冰纹理折射
unity3d·游戏开发·图形学