Unity BestHTTP 插件高级指南 - WebSocket 与实时通信

BestHTTP 不仅支持 HTTP 请求,还提供了完整的 WebSocket、Socket.IO、SignalR 和 Server-Sent Events 实现。本文档详细介绍这些实时通信协议的使用方法。


一、实时通信协议概述

1.1 协议对比

协议 特性 适用场景 连接方式
WebSocket 全双工通信,低延迟 实时游戏、聊天室 长连接
Socket.IO WebSocket 封装,自动降级 Node.js 后端兼容 长连接
SignalR 微软开发,自动协商 .NET 后端兼容 长连接
SSE 服务器推送,单向 消息通知、股票行情 长连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────────────────────────────────────┐
│ 实时通信协议架构对比 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ WebSocket │ │ Socket.IO │ │ SignalR │ │
│ │ (RFC 6455) │ │ (封装层) │ │ (抽象层) │ │
│ ├──────────────┤ ├──────────────┤ ├──────────────┤ │
│ │ 原生协议 │ │ 自动重连 │ │ 自动协商 │ │
│ │ 低延迟 │ │ 心跳检测 │ │ 多种传输 │ │
│ │ 二进制支持 │ │ 命名空间 │ │ Hub 模型 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └────────────────────┴────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ TCP 连接 │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

二、WebSocket 基础

2.1 引入命名空间

1
using BestHTTP.WebSocket;

2.2 创建 WebSocket 连接

1
2
3
4
5
6
7
8
9
10
11
12
// 创建 WebSocket
var webSocket = new WebSocket(new Uri("wss://echo.websocket.org"));

// 注册事件回调
webSocket.OnOpen += OnWebSocketOpen;
webSocket.OnMessage += OnMessageReceived;
webSocket.OnBinary += OnBinaryMessageReceived;
webSocket.OnClosed += OnWebSocketClosed;
webSocket.OnError += OnError;

// 打开连接
webSocket.Open();

2.3 WebSocket 事件详解

事件 触发时机 回调签名
OnOpen 连接成功建立 OnOpen(WebSocket ws)
OnMessage 收到文本消息 OnMessage(WebSocket ws, string message)
OnBinary 收到二进制消息 OnBinary(WebSocket ws, byte[] message)
OnClosed 连接关闭 OnClosed(WebSocket ws, UInt16 code, string message)
OnError 发生错误 OnError(WebSocket ws, Exception ex)
OnErrorDesc 详细错误信息 OnErrorDesc(WebSocket ws, string error)

2.4 事件处理代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 连接成功
void OnWebSocketOpen(WebSocket ws)
{
Debug.Log("WebSocket Open!");
// 此时 IsOpen 为 true,可以发送消息
}

// 收到文本消息
void OnMessageReceived(WebSocket ws, string message)
{
Debug.Log($"Text Message: {message}");
}

// 收到二进制消息
void OnBinaryMessageReceived(WebSocket ws, byte[] message)
{
Debug.Log($"Binary Message, Length: {message.Length}");
}

// 连接关闭
void OnWebSocketClosed(WebSocket ws, UInt16 code, string message)
{
Debug.Log($"WebSocket Closed! Code: {code}, Message: {message}");
}

// 发生错误
void OnError(WebSocket ws, Exception ex)
{
string errorMsg = string.Empty;
if (ws.InternalRequest.Response != null)
{
errorMsg = $"Status Code: {ws.InternalRequest.Response.StatusCode}, " +
$"Message: {ws.InternalRequest.Response.Message}";
}
Debug.LogError($"Error: {(ex != null ? ex.Message : "Unknown: " + errorMsg)}");
}

// 详细错误描述
void OnErrorDesc(WebSocket ws, string error)
{
Debug.LogError($"Error Description: {error}");
}

2.5 发送消息

1
2
3
4
5
6
7
8
// 在 OnOpen 之后发送消息
// 发送字符串
webSocket.Send("Message to the Server");

// 发送二进制数据
byte[] buffer = new byte[1024];
// ... 填充数据 ...
webSocket.Send(buffer);

2.6 心跳机制

1
2
3
4
5
// 启动 Ping 线程(心跳)
webSocket.StartPingThread = true;

// 设置 Ping 频率(默认 1000ms)
webSocket.PingFrequency = 1000;

💡 注意:Ping 消息会定期发送到服务器,服务器返回的 Pong 消息会自动处理。

2.7 关闭连接

1
2
// 关闭连接(无法重用已关闭的 WebSocket)
webSocket.Close();

三、Socket.IO 客户端

3.1 Socket.IO 概述

Socket.IO 是一个 WebSocket 封装库,提供:

  • 自动重连
  • 心跳检测
  • 命名空间
  • 房间管理
  • 事件驱动通信

3.2 引入命名空间

1
using BestHTTP.SocketIO;

3.3 创建 Socket.Manager

1
2
// 创建 Socket.IO 管理器
var manager = new SocketManager(new Uri("http://chat.socket.io/socket.io/"));

⚠️ 注意:URL 中的 /socket.io/ 路径很重要,这是 Socket.IO 服务器的默认监听路径。

3.4 连接命名空间

1
2
3
4
5
6
7
8
// 默认根命名空间
Socket root = manager.Socket;

// 自定义命名空间(两种方式等价)
Socket nsp = manager["/customNamespace"];
Socket nsp2 = manager.GetSocket("/customNamespace");

// 首次访问命名空间将启动连接

3.5 订阅事件

预定义事件 说明
connect 命名空间打开时发送
connecting 开始连接时发送
disconnect 传输断开时发送
reconnect 重连成功时发送
reconnecting 尝试重连时发送
reconnect_attempt 重连尝试时发送
reconnect_failed 重连失败时发送
error 服务器或内部错误时发送
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 订阅自定义事件
manager.Socket.On("login", OnLogin);
manager.Socket.On("new message", OnNewMessage);

void OnLogin(Socket socket, Packet packet, params object[] args)
{
// socket: 发送此事件的命名空间 Socket 对象
// packet: 包含内部分组数据,可访问二进制数据
// args: 解码后的参数数组
Debug.Log($"Login: {args[0]}");
}

// 订阅预定义事件
manager.Socket.On("connect", () => Debug.Log("Connected!"));
manager.Socket.On("disconnect", () => Debug.Log("Disconnected!"));

3.6 发送事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 发送携带参数的事件
manager.Socket.Emit("message", "userName", "message");

// 发送带回调的事件
manager.Socket.Emit("custom event", OnAckCallback, "param1", "param2");

void OnAckCallback(Socket socket, Packet originalPacket, params object[] args)
{
Debug.Log("Server acknowledged!");
}

// 发送确认回复
manager["/customNamespace"].On("customEvent", (socket, packet, args) =>
{
socket.EmitAck(packet, "Event", "Received", "Successfully");
});

3.7 二进制数据处理

发送二进制数据

1
2
3
// 方式一:直接传递字节数组(推荐)
byte[] data = new byte[10];
manager.Socket.Emit("eventWithBinary", "textual param", data);

接收二进制数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 方式一:使用 Attachments 属性(autoDecodePayload = true,默认)
socket.On("frame", OnFrame);
void OnFrame(Socket socket, Packet packet, params object[] args)
{
texture.LoadImage(packet.Attachments[0]);
}

// 方式二:不解码 Payload
socket.On("frame", OnFrame, autoDecodePayload: false);
void OnFrame(Socket socket, Packet packet, params object[] args)
{
texture.LoadImage(packet.Attachments[0]);
}

// 方式三:重建为索引
socket.On("frame", OnFrame, autoDecodePayload: false);
void OnFrame(Socket socket, Packet packet, params object[] args)
{
packet.ReconstructAttachmentAsIndex();
args = packet.Decode(socket.Manager.Encoder);
byte[] data = packet.Attachments[Convert.ToInt32(args[0])];
texture.LoadImage(data);
}

// 方式四:重建为 Base64
socket.On("frame", OnFrame, autoDecodePayload: false);
void OnFrame(Socket socket, Packet packet, params object[] args)
{
packet.ReconstructAttachmentAsBase64();
args = packet.Decode(socket.Manager.Encoder);
byte[] data = Convert.FromBase64String(args[0] as string);
texture.LoadImage(data);
}

3.8 事件管理

1
2
3
4
5
6
7
// 订阅一次性事件
socket.Once("connect", OnConnected);

// 取消订阅
socket.Off(); // 删除所有回调
socket.Off("connect"); // 删除 connect 事件的所有回调
socket.Off("connect", OnConnected); // 删除特定回调

3.9 SocketOptions 配置

选项 默认值 说明
Reconnection true 是否自动重连
ReconnectionAttempts int.MaxValue 重连尝试次数
ReconnectionDelay 1000ms 初始重连延迟
ReconnectionDelayMax 5000ms 最大重连延迟
RandomizationFactor 0.5 延迟随机因子
Timeout 20000ms 连接超时
AutoConnect true 是否自动连接
ConnectWith - 传输类型(Polling/WebSocket)

四、SignalR 客户端

4.1 SignalR 概述

SignalR 是微软开发的实时通信库,特点:

  • 自动协商传输方式
  • Hub 代理模型
  • 强类型支持
  • 连接状态管理

4.2 引入命名空间

1
using BestHTTP.SignalR;

4.3 创建 Connection

1
2
3
4
5
6
7
8
9
10
11
12
Uri uri = new Uri("http://besthttpsignalr.azurewebsites.net/raw-connection/");

// 无 Hub 连接
Connection connection = new Connection(uri);

// 带 Hub 名称的连接
Connection connection = new Connection(uri, "hub1", "hub2", "hubN");

// 使用 Hub 对象
Hub hub1 = new Hub("hub1");
Hub hub2 = new Hub("hub2");
Connection connection = new Connection(uri, hub1, hub2);

4.4 Connection 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 连接成功
connection.OnConnected += (conn) => Debug.Log("Connected!");

// 连接关闭
connection.OnClosed += (conn) => Debug.Log("Connection Closed");

// 发生错误
connection.OnError += (conn, err) => Debug.Log($"Error: {err}");

// 重连中
connection.OnReconnecting += (conn) => Debug.Log("Reconnecting...");

// 重连成功
connection.OnReconnected += (conn) => Debug.Log("Reconnected!");

// 状态改变
connection.OnStateChanged += (conn, oldState, newState) =>
Debug.Log($"{oldState} -> {newState}");

// 非 Hub 消息
connection.OnNonHubMessage += (conn, data) =>
Debug.Log($"Message: {data}");

// 请求预处理
connection.RequestPreparator = (conn, req, type) =>
req.Timeout = TimeSpan.FromSeconds(30);

4.5 发送非 Hub 消息

1
2
3
4
5
// 发送对象
connection.Send(new { Type = "Broadcast", Value = "Hello!" });

// 发送 JSON
connection.SendJson("{ Type: 'Broadcast', Value: 'Hello!' }");

4.6 Hub 操作

1
2
3
4
5
6
7
8
9
10
// 访问 Hub(两种方式)
Hub hub = connection[0];
Hub hub2 = connection["hubName"];

// 注册服务器可调用方法
connection["hubName"].On("joined", Joined);
void Joined(Hub hub, MethodCallMessage msg)
{
Debug.Log($"{msg.Arguments[0]} joined at {msg.Arguments[1]}");
}

4.7 调用服务器方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 方式一:即发即忘
connection["hubName"].Call("Ping");
connection["hubName"].Call("Message", "param1", "param2");

// 方式二:带成功回调
connection["hubName"].Call("GetValue", OnGetValueDone);
void OnGetValueDone(Hub hub, ClientMessage originalMessage, ResultMessage result)
{
Debug.Log($"Return value: {result.ReturnValue}");
}

// 方式三:带成功和失败回调
connection["hubName"].Call("GetValue", OnGetValueDone, OnGetValueFailed);
void OnGetValueFailed(Hub hub, ClientMessage originalMessage, FailureMessage error)
{
Debug.LogError($"Error: {error.ErrorMessage}");
}

// 方式四:带进度回调
connection["hubName"].Call("GetValue", OnDone, OnFailed, OnProgress);
void OnProgress(Hub hub, ClientMessage originalMessage, ProgressMessage progress)
{
Debug.Log($"Progress: {progress.Progress}%");
}

4.8 Hub 基类继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SampleHub : Hub
{
public SampleHub() : base("SampleHub")
{
// 注册服务器可调用函数
base.On("ClientFunction", ClientFunctionImplementation);
}

private void ClientFunctionImplementation(Hub hub, MethodCallMessage msg)
{
// 处理服务器调用
}

public void ServerFunction(string argument)
{
base.Call("ServerFunction", argument);
}
}

// 使用
SampleHub sampleHub = new SampleHub();
Connection connection = new Connection(uri, sampleHub);

4.9 身份验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class HeaderAuthenticator : IAuthenticationProvider
{
public string User { get; private set; }
public string Roles { get; private set; }

public bool IsPreAuthRequired { get { return false; } }

public event OnAuthenticationSuccededDelegate OnAuthenticationSucceded;
public event OnAuthenticationFailedDelegate OnAuthenticationFailed;

public HeaderAuthenticator(string user, string roles)
{
this.User = user;
this.Roles = roles;
}

public void StartAuthentication() { }

public void PrepareRequest(HTTPRequest request, RequestTypes type)
{
request.SetHeader("username", this.User);
request.SetHeader("roles", this.Roles);
}
}

// 使用
connection.AuthenticationProvider = new HeaderAuthenticator("user1", "admin");

五、Server-Sent Events (SSE)

5.1 SSE 概述

SSE 是一种基于字符串的单向协议:

  • 数据从服务器流向客户端
  • 无法向服务器发送数据
  • 自动重连机制

5.2 引入命名空间

1
using BestHTTP.ServerSentEvents;

5.3 创建 EventSource

1
var eventSource = new EventSource(new Uri("http://server.com"));

5.4 EventSource 属性

属性 说明
Uri 连接端点
State 当前状态
ReconnectionTime 重连延迟(默认 2 秒)
LastEventId 最后收到的事件 ID
InternalRequest 内部 HTTP 请求

5.5 EventSource 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 连接成功
eventSource.OnOpen += OnEventSourceOpened;
void OnEventSourceOpened(EventSource source)
{
Debug.Log("EventSource Opened!");
}

// 收到消息
eventSource.OnMessage += OnEventSourceMessage;
void OnEventSourceMessage(EventSource source, Message msg)
{
Debug.Log($"Message: {msg.Data}");
}

// 发生错误
eventSource.OnError += OnEventSourceError;
void OnEventSourceError(EventSource source, string error)
{
Debug.LogError($"Error: {error}");
}

// 重连前
eventSource.OnRetry += OnEventSourceRetry;
bool OnEventSourceRetry(EventSource source)
{
// 返回 false 禁用重试
return true;
}

// 连接关闭
eventSource.OnClosed += OnEventSourceClosed;
void OnEventSourceClosed(EventSource source)
{
Debug.Log("EventSource Closed!");
}

// 状态改变
eventSource.OnStateChanged += OnEventSourceStateChanged;
void OnEventSourceStateChanged(EventSource source, States oldState, States newState)
{
Debug.Log($"{oldState} => {newState}");
}

5.6 订阅自定义事件

1
2
3
4
5
6
7
8
9
// 订阅命名事件
eventSource.On("userLogon", OnUserLoggedIn);
void OnUserLoggedIn(EventSource source, Message msg)
{
Debug.Log(msg.Data);
}

// 取消订阅
eventSource.Off("userLogon");

5.7 Message 属性

属性 说明
Id 事件 ID
Event 事件名称
Data 消息负载
Retry 重连延迟

六、简单示例

6.1 上传图片(表单方式)

1
2
3
var request = new HTTPRequest(new Uri("http://server.com"), HTTPMethods.Post, onFinished);
request.AddBinaryData("image", texture.EncodeToPNG(), "image.png");
request.Send();

6.2 上传图片(原始数据方式)

1
2
3
4
var request = new HTTPRequest(new Uri("http://server.com"), HTTPMethods.Post, onFinished);
request.SetHeader("Content-Type", "image/png");
request.RawData = texture.EncodeToPNG();
request.Send();

6.3 自定义请求头

1
2
3
4
var request = new HTTPRequest(new Uri("http://server.com"), HTTPMethods.Post, onFinished);
request.SetHeader("Content-Type", "application/json; charset=UTF-8");
request.RawData = UTF8Encoding.GetBytes(ToJson(data));
request.Send();

6.4 显示下载进度

1
2
3
4
5
6
7
8
9
var request = new HTTPRequest(new Uri("http://server.com/largefile"), (req, resp) =>
{
Debug.Log("Finished!");
});
request.OnProgress += (req, downloaded, length) =>
{
Debug.Log($"Progress: {downloaded / (float)length:P2}");
};
request.Send();

6.5 中止请求

1
2
3
4
5
6
7
var request = new HTTPRequest(new Uri(address), (req, resp) =>
{
Debug.Log($"State: {req.State}"); // 应为 Aborted
});
request.Send();
// 稍后中止
request.Abort();

6.6 可恢复下载(分块下载)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
private const int ChunkSize = 1024 * 1024; // 1 MB
private string saveTo = "downloaded.bin";

void StartDownload(string url)
{
// 先发 Head 请求检查是否支持范围请求
var headRequest = new HTTPRequest(new Uri(url), HTTPMethods.Head, (request, response) =>
{
if (response == null)
Debug.LogError("Server unreachable!");
else if (response.HasHeaderWithValue("accept-ranges", "none"))
Debug.LogError("Server doesn't support Range header!");
else
DownloadCallback(request, response);
});

int startPos = PlayerPrefs.GetInt("LastDownloadPosition", 0);
headRequest.SetRangeHeader(startPos, startPos + ChunkSize);
headRequest.DisableCache = true;
headRequest.Send();
}

void DownloadCallback(HTTPRequest request, HTTPResponse response)
{
if (response == null) return;

var range = response.GetRange();
if (range == null || !range.IsValid) return;

if (request.MethodType != HTTPMethods.Head)
{
// 保存数据块
string path = Path.Combine(Application.temporaryCachePath, saveTo);
using (FileStream fs = new FileStream(path, FileMode.Append))
fs.Write(response.Data, 0, response.Data.Length);

PlayerPrefs.SetInt("LastDownloadPosition", range.LastBytePos);

if (range.LastBytePos == range.ContentLength - 1)
{
Debug.Log("Download finished!");
return;
}
}

// 请求下一块
int nextPos = (request.MethodType != HTTPMethods.Head)
? range.LastBytePos + 1
: PlayerPrefs.GetInt("LastDownloadPosition", 0);

var downloadRequest = new HTTPRequest(request.Uri, HTTPMethods.Get, true, DownloadCallback);
downloadRequest.SetRangeHeader(nextPos, nextPos + ChunkSize);
downloadRequest.DisableCache = true;
downloadRequest.Send();
}

七、高级配置

7.1 禁用特定功能

通过编译符号可以禁用特定功能以减小包大小:

定义 作用
BESTHTTP_DISABLE_COOKIES 禁用 Cookie 功能
BESTHTTP_DISABLE_CACHING 禁用缓存功能
BESTHTTP_DISABLE_SERVERSENT_EVENTS 禁用 SSE
BESTHTTP_DISABLE_WEBSOCKET 禁用 WebSocket
BESTHTTP_DISABLE_SIGNALR 禁用 SignalR
BESTHTTP_DISABLE_SOCKETIO 禁用 Socket.IO

7.2 支持的平台

平台 支持情况
WebGL
iOS
Android
Windows Phone 10
WinRT / Metro
Windows / Linux / Mac Standalone

7.3 SSL 处理

1
2
// 使用备用 SSL 处理器(针对特殊证书)
request.UseAlternateSSL = true;

⚠️ 注意:大多数情况下默认 SSL 处理即可,仅在遇到证书问题时使用备用方案。


八、总结

协议 核心特性 适用场景 复杂度
WebSocket 全双工、低延迟 实时游戏、高频交易 ⭐⭐
Socket.IO 自动重连、房间 聊天应用、社交游戏 ⭐⭐⭐
SignalR Hub 模型、协商传输 .NET 后端应用 ⭐⭐⭐⭐
SSE 单向推送、简单实现 消息通知、数据订阅

💡 最佳实践

  • 优先使用加密连接(WSS)
  • 实现心跳和断线重连
  • 添加消息确认机制
  • 做好错误处理和日志记录
  • 根据场景选择合适的协议

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1487842110@qq.com