二、需要自定义分隔的方案:二进制帧头 + 负载 当你希望在一条消息中承载多帧,或需要在多个消息中分片并重组,就要设计一个“帧头”(header),明确边界与元信息。
常见的自定义帧头字段:
magic/signature:固定字节标识,便于同步与校验,如 0x42 0x48 0x46 0x52 (“BHFR”)
version:协议版本
frameId:递增帧号(uint64)
timestamp:时间戳(uint64)
mimeLen:MIME 类型长度(如 image/jpeg)
payloadLen:本帧负载长度(uint32/uint64)
可选:CRC32、宽高、分片参数等
发送端将“帧头 + 负载”作为一个二进制消息发送;接收端按 DataView 读帧头,再按 payloadLen 读取负载。这也能实现可靠的“帧分隔”。
WPF 客户端解析示例:
private void ParseAndRenderFrames(ReadOnlySpan<byte> data)
{
int offset = 0;
while (offset + 24 <= data.Length) // 假设固定部分头 24 字节
{
// 读取 magic
if (!(data[offset] == 0x42 && data[offset + 1] == 0x48 && data[offset + 2] == 0x46 && data[offset + 3] == 0x52))
{
// 找不到同步字节,尝试跳过或重置
break;
}
offset += 4;
byte version = data[offset]; offset += 1;
// frameId (uint64 little-endian)
ulong frameId = BitConverter.ToUInt64(data.Slice(offset, 8)); offset += 8;
ulong ts = BitConverter.ToUInt64(data.Slice(offset, 8)); offset += 8;
byte mimeLen = data[offset]; offset += 1;
if (offset + mimeLen + 4 > data.Length) break;
string mime = Encoding.ASCII.GetString(data.Slice(offset, mimeLen)); offset += mimeLen;
uint payloadLen = BitConverter.ToUInt32(data.Slice(offset, 4)); offset += 4;
if (offset + payloadLen > data.Length) break; // 不完整,等待下次拼接
var payload = data.Slice(offset, (int)payloadLen).ToArray();
offset += (int)payloadLen;
// 解码该帧
RenderImage(payload);
}
}
注意:
如果一条 WS 消息内可能包含多帧,则在 ReceiveAsync 累积到 EndOfMessage 后,将整条消息送入 ParseAndRenderFrames 分解。
如果帧可能跨消息分片,就需要在 ReceiveLoop 里维持一个持续增长的缓冲,将每次收到的片段追加进去,只有当累计数据达到“完整帧”所需长度时才取出一帧,并把缓冲前移。可以用 MemoryStream 或环形缓冲。