多年回答支持工单后,同样的少数问题占了"我的 WebSocket 连接行为奇怪"报告的绝大多数。没有一个是库中的错误——所有都是配置错误,一旦您知道它们存在,需要五分钟即可修复。本文列出了最常见的十个,包括症状、原因以及解决每个问题的单行代码。
如果您从这个列表中只阅读一个错误,请将其作为 #2。阻塞 OnMessage 事件比所有其他类别加起来都负责更多的"库很慢"工单,并且在扩展到几百个并发客户端之前它都是不可见的。在发布前修复它,省去深夜生产调试会话。
错误 1:WatchDog 重连已禁用
症状:客户端在启动时连接,运行良好数小时,然后在 Wi-Fi 故障、VPN 重新连接或服务器重启后默默停止接收消息。
原因:WatchDog 属性默认关闭。库不假定您想要自动重连——一些应用希望快速失败行为。大多数则不。
oClient.WatchDog.Enabled := True; oClient.WatchDog.Interval := 10; // try every 10 seconds oClient.WatchDog.Attempts := 0; // 0 = unlimited
发后不理。将其与 OnDisconnect 日志记录结合起来,以便您可以在现场看到重连发生的频率。对于用户在网络之间漫游的移动或笔记本电脑部署,还应连接 OS 网络更改通知(.NET 上的 SystemEvents.NetworkAvailabilityChanged,Windows-Delphi 上的 WMI 路由)以触发立即重连,而不是等待下一个间隔滴答。
错误 2:阻塞 OnMessage 事件
症状:负载下吞吐量崩溃。服务器感觉"卡住"。CPU 低,但消息堆积。
原因:OnMessage 在 I/O 工作线程上运行。如果您在其中调用慢速数据库、外部 HTTP API 或长时间运行的解析器,您将饿死共享该工作器的每个其他连接。
// BAD: blocks the worker
procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
oDb.Query('INSERT INTO events VALUES(?)', [Text]); // 50 ms
oHttp.Get('https://billing/track?event=' + Text); // 200 ms
end;
// GOOD: hand off to a worker queue
procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
oWorkQueue.Push(TWorkItem.Create(Connection.Guid, Text));
end;
错误 3:忽略 HeartBeat
症状:幽灵连接累积。Connections.Count 不断增长,即使您知道客户端已死亡。
原因:没有应用层心跳,OS TCP keepalive 计时器在约 2 小时后触发。到那时,您有数万个吃内存的僵尸套接字。
oServer.HeartBeat.Enabled := True; oServer.HeartBeat.Interval := 30; oServer.HeartBeat.Timeout := 90;
错误 4:不匹配的 TLS 版本
症状:客户端收到"handshake failed"或"SSL_ERROR_SYSCALL"。连接在开发机器上工作,但在企业代理后面的生产中失败。
原因:客户端 SSL 选项默认为 TLS 1.0–1.2 以实现向后兼容性;现代服务器仅要求 TLS 1.2/1.3。或者 OpenSSL DLL 旧了。
oClient.TLSOptions.Version := tlsTLSv1_2; // or tlsTLSv1_3 oClient.TLSOptions.OpenSSL_Options.LibPath := ExtractFilePath(ParamStr(0)); // Bundle the latest libcrypto-3.dll / libssl-3.dll with your installer.
错误 5:不处理分片消息
症状:服务器接收大 JSON 文档的第一部分,您的解析器失败,连接被关闭。
原因:默认情况下,sgcWebSockets 为您重新组装分片,并仅在完整负载时触发 OnMessage。但如果您出于内存原因设置了 ReadOptions.FragmentMode := frgPartial,您必须重新组装。
// Default (recommended for most apps)
oServer.ReadOptions.FragmentMode := frgComplete;
// If you opted into partial delivery, reassemble manually
procedure TForm1.OnFragment(Connection: TsgcWSConnection;
const aPartial: TBytes; aIsFinal: Boolean);
begin
Buffers[Connection.Guid].Append(aPartial);
if aIsFinal then
Process(Buffers[Connection.Guid].ToBytes);
end;
错误 6:在 GUI 线程中使用同步 API
症状:连接时 UI 冻结数秒,或在慢链接上调用 WriteData 时。
原因:主线程上的阻塞调用。始终在 VCL/FMX 应用程序中使用异步模式,或从工作线程调用。
// BAD: blocks the UI
oClient.Active := True;
// GOOD: connect asynchronously
TTask.Run(procedure
begin
oClient.Active := True;
end);
// Or use the event-driven API
oClient.OnConnect := DoConnected;
oClient.Connect; // returns immediately
错误 7:忘记选择子协议
症状:连接成功,但对等方拒绝每个帧,或以与预期不同的格式响应。
原因:许多 WebSocket 服务器(MQTT-over-WS、STOMP、GraphQL-WS、Phoenix)在握手中需要特定的子协议。没有它,服务器默认为不同的协议或丢弃您。
// MQTT over WebSocket
oClient.Specifications.Hixie76 := False;
oClient.Specifications.RFC6455 := True;
oClient.HeadersRequest.Add('Sec-WebSocket-Protocol: mqttv3.1');
// GraphQL over WS
oClient.HeadersRequest.Add('Sec-WebSocket-Protocol: graphql-ws');
错误 8:缓冲区大小过小(或过大)
症状:推送许多小消息的服务器上 CPU 高,或推送大消息的服务器上内存不足。
原因:默认发送/接收缓冲区大小适合一般使用。调整它们以适应您的流量形状。
// Small messages (chat, telemetry): smaller buffers reduce per-conn memory oServer.IOHandler.RecvBufferSize := 4096; oServer.IOHandler.SendBufferSize := 4096; // Big messages (file transfer, media): larger buffers reduce syscalls oServer.IOHandler.RecvBufferSize := 65536; oServer.IOHandler.SendBufferSize := 65536;
错误 9:公共服务器上没有 Origin 检查
症状:安全审计标记"任何网站都可以通过用户的浏览器连接到您的 WebSocket"。
原因:WebSocket 协议不强制执行同源。您的服务器必须验证 Origin 标头。
procedure TForm1.ServerBeforeConnect(Connection: TsgcWSConnection;
var Continue: Boolean);
var
vOrigin: string;
begin
vOrigin := Connection.Headers.Values['Origin'];
Continue := (vOrigin = 'https://app.example.com')
or (vOrigin = 'https://admin.example.com');
end;
错误 10:在 OnMessage 中记录敏感数据
症状:审计员在日志文件中找到 API 密钥、JWT 或 PII。合规问题。
原因:OnMessage 中简单的 LogMemo.Lines.Add(Text) 将每个负载永远写入磁盘。
procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
// Hash / redact before logging
LogMemo.Lines.Add(Format('[%s] %d bytes (sha=%s)',
[Connection.Guid, Length(Text), ShortHash(Text)]));
Process(Text);
end;
奖励:不读取 History.txt
每个版本都附带一个 history.txt,列出自 2013 年以来的每个更改、修复和重大注释。在每次升级后花五分钟浏览它,可以节省稍后数小时的"为什么这个停止工作"调试。
奖励 2:跨项目混合组件版本
Delphi 开发者有时将单个 .pas 从较新的 sgcWebSockets 版本复制到较旧的项目中,"就这一个文件"。这一直有效,直到无效——该文件依赖于两个版本前更改的类型,链接器神秘地失败或,更糟,链接但在运行时崩溃。始终一起升级整个库。复制一个文件节省的 30 秒不值得下游某些东西损坏时四小时的调试。
奖励 3:将 WebSocket 视为发后不理
WebSocket 不是消息队列。它是 TCP 上的双向字节流。如果网络在消息中途断开,帧丢失且永远不会自动重新传递。对于业务关键消息,您必须在其上添加自己的确认协议——通常是每条消息的 UUID、来自接收者的显式 ACK 以及超时后发送者的重发。跳过这一层对"用户正在输入"通知没问题,对"用户为购物车付款"是致命的。
奖励 4:让 Memo 渗漏内存
症状:调试窗体上的诊断 memo 是几小时流量后 OOM 您的客户端的第一件事。您责备 sgcWebSockets;sgcWebSockets 是无辜的。
原因:一个 TMemo 保留每行添加的内容。每秒 100 行就是每小时 360,000 行。每行分配一个字符串。VCL 在每个 WM_PAINT 上渲染数千个不可见行。您的进程停止运行,而库没有做错任何事。
// Cap the diagnostic memo
procedure TForm1.LogLine(const aText: string);
const
cMaxLines = 500;
begin
TThread.Queue(nil, procedure
begin
while Memo1.Lines.Count > cMaxLines do
Memo1.Lines.Delete(0);
Memo1.Lines.Add(aText);
end);
end;
更好:通过 LoggerPro 记录到滚动文件,并仅将最后 200 行镜像到 memo 中以进行视觉调试。生产代码永远不应该从网络线程写入 UI 控件。
奖励 5:不在 Application.Terminate 之前关闭服务器
症状:在应用关闭时,进程挂起 30 秒,或 OS 在客户端日志中报告未处理的异常,因为连接被不优雅地拆除。
原因:服务器析构函数向每个连接发送关闭帧并等待 OS 释放监听端口。如果您在 oServer.Active := False 之前调用 Application.Terminate,连接在握手中途死亡,OS 端口保持在 TIME_WAIT 中,阻止快速重启。
procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
if oServer.Active then
begin
oServer.Broadcast('{"event":"shutdown","reconnect_after":5}');
Sleep(200); // let frames flush
oServer.Active := False; // graceful close
end;
end;
对于控制台服务器,在 Windows 上挂接 SetConsoleCtrlHandler 或在 Linux 上挂接 SIGTERM,并运行相同的关闭序列。将其与您的服务管理器中的 HUP/重启循环配对,您就有零丢弃连接的部署。
模式背后的模式
这些错误中的大多数都有一个共同根源:假设网络是可靠的。它不是。半开 TCP 连接会发生。移动网络掉线。企业代理破坏 TLS。Wi-Fi 漫游。服务器重启。云负载均衡器在 60 秒空闲后终止连接。一个不在这些条件下生存的 WebSocket 应用没有完成——它是一个快乐路径演示。好消息:该库为每个这些场景公开一个控件。坏消息:出于向后兼容性,它们中的大多数默认关闭。花两小时阅读文档的 WatchDog、HeartBeat、Reconnect 和 TLS 选项页面是您将购买的最便宜的保险。
第二阶模式:尊重 I/O 线程。任何超过一毫秒的事情——数据库查询、文件 I/O、外部 HTTP 调用、长字符串上的正则表达式、100 KB 负载上的 JSON 解析——都属于工作线程,而不是 OnMessage。将此规则绝对化。三个月后初级开发者将违反它;包含"OnMessage 中无阻塞"的代码审查清单会在发布前捕捉到它。
接下来去哪
如果您正在调优高流量服务器,接下来阅读 sgcWebSockets 性能调优。库的新手?从开始使用中心开始,五分钟内连接您的第一个 WebSocket。