Delphi集成sgcWebSockets:从服务端到客户端的WebSocket实战指南
2026/9/23 2:55:52 网站建设 项目流程

简介:sgcWebSockets-Enterprise-V2023.5-FS是一份面向企业级应用场景的WebSocket服务器软件包,主要服务于需要大规模双向实时通信的中大型系统开发者。WebSocket在单个TCP连接上提供全双工通信信道,相比HTTP轮询或服务器发送事件,延迟更低、资源占用更可控,因此这套资源非常适合在线游戏、实时数据分析看板、金融交易终端、即时聊天服务等对响应速度要求苛刻的场合。企业版本在标准功能之外,进一步加入更精细的权限控制、负载均衡、集群支持、加密通信、用户认证、访问控制列表、日志记录、监控报告以及企业现有系统的API集成等能力,可帮助技术团队快速构建兼具高并发、高可用与高安全性的实时通信层。资源以7z压缩包发布,压缩后体积约66.64MB,便于传输与本地部署。目前已有145人学习使用,无论是中高级后端工程师进行二次开发,还是系统架构师评估实时通信选型,都能从中获得一套完整的企业级WebSocket服务端参考方案。

1. sgcWebSockets-Enterprise-V2023.5-FS.7z 是什么,为什么 Delphi 项目需要单独维护一套 WebSocket 通道

不少团队在接到 WebSocket 需求时,第一反应是上 Python、Node 或者 Java 的现成库,等做完了才发现桌面客户端是 Delphi 写的,业务核心逻辑全在 VCL 里,跨语言调一轮反而多出一层维护成本。sgcWebSockets 就是解决这个问题的:它是 Delphi / C++Builder 环境下的 WebSocket 实现,Enterprise 版带完整源码,V2023.5 是 2023 年的版本快照,FS 后缀通常表示 Full Source,.7z 是压缩包格式。装上它之后,服务端和客户端都能在同一个 IDE、同一套代码里写完,编出来是一个原生 exe,不依赖 Node 运行时,也不需要在客户端机器上再部署一套 Python 环境。

这篇东西面向的是已经在用 Delphi 做业务、又需要接入 WebSocket 的工程师。如果你只是写个一次性测试脚本,用 Postman 连接就够了,不需要上组件库;但如果你要在生产环境里做长连接、做心跳保活、做 WSS 加密,甚至要同时起几百个客户端连接,就需要一个能在 Delphi 里直接调用的库。我会按安装、服务端、客户端、参数调优、排错这条路径讲,每一步都给可复现的代码和配置,重点放在那些文档里写得含糊、实际一跑就翻车的地方。

2. 把 sgcWebSockets V2023.5 装进 IDE:版本匹配、源码路径与编译顺序

2.1 安装前先确认 IDE 版本与库版本的对应关系

sgcWebSockets 的版本号是独立于 Delphi 版本号的,V2023.5 指的是 2023 年 5 月左右的发布快照,它支持的 IDE 范围会在安装文档里列出来。常见的做法是先看sgcWebSockets.inc里的条件编译定义,里面会写明针对不同 Delphi 版本启用了哪些特性。假设你在 Delphi 10.4 或 11 上安装,一般不需要额外打补丁;如果你还在用 Delphi 7 或者 XE 系列的老古董,那就要确认这个版本是否还保留了对老编译器的兼容分支,很多新版库已经默认只支持 Unicode 版本的 IDE。

在动手解压之前,建议先做两件事。第一,路径不要带空格和中文,C:\Libs\sgcWebSocketsC:\My Files\第三方库\sgc...稳得多,因为老的.dcu路径解析遇到空格容易出邪门问题。第二,关掉 IDE 再解压,不要在 Delphi 运行状态下覆盖文件,不然.bpl.dcu文件被文件锁占用,后面编译报错你会以为是自己代码写错了。

2.2 Windows 下源码安装的四个步骤

解压之后,目录里通常会看到PackagesSourceExamples三个核心目录。Packages下面按 IDE 版本分了子目录,例如D10_4D11之类。安装步骤如下:

# 假设 7z 包已解压到 C:\Libs\sgcWebSockets # 1. 用命令行进入对应 IDE 版本的包目录,先编译运行期包,再编译设计期包 cd C:\Libs\sgcWebSockets\Packages\D10_4 # 2. 用 msbuild 编译,或者直接在 IDE 中打开 .dpk 文件 # 常见做法:先打开 sgcws_runtime.dpk,右键 Compile # 再打开 sgcws_design.dpk,右键 Install

如果你习惯命令行方式,Delphi 自带的 msbuild 可以直接用来编译包。要注意的是,必须在 IDE 的 Tools > Options > Library 里把Source目录加到 Library Path,否则编译时会提示找不到sgcWebSocket.pas。装完设计期包之后,工具栏上会出现 sgcWebSockets 的分页,里面有TsgcWebSocketServerTsgcWebSocketClient两个主要组件。

2.3 安装完成后必须做的版本一致性检查

安装本身不复杂,复杂的是装完之后老项目引用到了旧的.dcu。如果你之前装过别的 WebSocket 库,或者同一个库装过旧版本,很容易出现.dcu冲突。检查方法是:新建一个空工程,放一个TsgcWebSocketServer到窗体上,然后查看sgcWebSocket.pas的单元版本。如果编译报错说找不到某个依赖单元,优先怀疑是 Library Path 里旧版本目录排在前面。

检查项正确状态错误状态处理方式
Library Path 顺序sgcWebSockets 的 Source 在前旧库路径排更前面调整路径顺序
<Delphi>\Lib下的旧 dcu无 sgc 开头的 dcu存在 sgc 相关 dcu删除或移走
设计期包状态sgc 组件出现在面板组件面板看不到重新 Install 设计期包
运行时包exe 目录有 sgcWebSockets 对应 bpl缺少 bpl把 bpl 复制到 exe 目录

提示:如果编译时出现File not found: sgcWebSocket.dcu,几乎都是 Library Path 没配好,跟代码本身无关。先检查路径,再重编译,不要急着改代码。

第 2 章的核心就一句话:版本对应关系决定了你能不能装上,路径顺序决定了你装的是不是这个版本。这一步没做干净,后面所有代码跑起来都可能表现诡异。

3. 用 TsgcWebSocketServer 搭一个最小 WebSocket 服务端并讲清回调线程模型

3.1 最小启动代码:不拖组件,纯代码创建

用组件库最怕的就是教程非要拖控件,生产环境里通常需要动态创建,因为端口、绑定地址、TLS 开关往往来自配置中心。下面给一段纯代码创建服务端的做法,适合放在FormCreate或者一个独立的TServerController类里:

procedure TMainForm.StartServer; var LServer: TsgcWebSocketHTTPServer; begin LServer := TsgcWebSocketHTTPServer.Create(nil); try LServer.Port := 8080; LServer.BindAddress := '0.0.0.0'; // 同一个端口上既处理 HTTP 又处理 WebSocket 升级 LServer.AllowUndefinedHost := True; LServer.OnConnect := HandleConnect; LServer.OnMessage := HandleMessage; LServer.OnDisconnect := HandleDisconnect; LServer.Active := True; FServer := LServer; // 保存引用,防止被 GC except LServer.Free; raise; end; end;

TsgcWebSocketHTTPServerTsgcWebSocketServer的区别在于前者可以同时响应普通 HTTP 请求和 WebSocket 升级请求。如果你用 nginx 做反向代理,通常会选后者,因为代理层已经把 HTTP 头处理掉了;如果你要直接暴露端口给客户端,用前者更省事。BindAddress设为0.0.0.0表示监听所有网卡,内网联调用起来方便,但也意味着外部能直接访问,生产环境要用防火墙限制。

3.2 OnMessage 不是主线程,别在里面碰 VCL 控件

新手最容易翻车的地方:在OnMessage回调里直接写Memo1.Lines.Add(...),结果界面上偶尔有输出、偶尔崩溃,还夹着 Access Violation。原因是 sgcWebSockets 默认用内部线程池收包,回调线程不是主线程。VCL 控件只能在主线程操作,跨线程碰控件轻则显示错乱,重则直接挂掉。

procedure TMainForm.HandleMessage(aSender: TsgcWSBaseConnection; const aText: string); begin // 错误写法:直接访问 VCL // Memo1.Lines.Add(aText); // 正确写法:投递到主线程 TThread.Queue(nil, procedure begin Memo1.Lines.Add(aText); end); end;

TThread.Queue是异步投递,回调线程不会等主线程处理完,吞吐量高;如果后续逻辑强依赖主线程处理完的结果,用TThread.Synchronize,但会阻塞当前回调线程。多客户端同时发消息时,Synchronize 会排队,卡顿会连锁放大。我的习惯是:回调里只做数据解析和入队,真正复杂的业务逻辑丢到自己的线程池里跑。

3.3 subprotocol 协商与连接握手的关键参数

WebSocket 握手阶段有一个 subprotocol 协商机制,客户端会在Sec-WebSocket-Protocol头里带上自己想用的协议名,服务端要在OnHandshake或通过属性配置选择一个返回。sgcWebSockets 里通常用Authentication事件来处理这些头信息。举一个常见场景:客户端带了Sec-WebSocket-Protocol: chat, video,服务端如果支持chat,响应里返回Sec-WebSocket-Protocol: chat,连接才算建立成功。

// 在 OnHandshake 里检查客户端请求头 procedure TMainForm.HandleHandshake(aSender: TsgcWSBaseConnection); var LProtocol: string; begin LProtocol := aSender.Request.Headers.Values['Sec-WebSocket-Protocol']; if Pos('chat', LProtocol) > 0 then aSender.Response.Headers.Values['Sec-WebSocket-Protocol'] := 'chat' else aSender.Close; // 不接受不支持的 subprotocol,直接断开 end;

这里有个容易忽略的细节:aSender.Close是主动断开,调用时机在握手响应发送之前,所以客户端会看到连接失败而不是握手成功后才关闭。有些场景你想让连接建立起来但不用任何 subprotocol,那就不用在响应里回这个头。

3.4 心跳保活:用 PING/PONG 而非自己发业务心跳

WebSocket 协议自带PINGPONG控制帧,sgcWebSockets 提供了心跳机制,可以设置服务端每隔一段时间主动发 PING,客户端在协议层自动回 PONG。不要再自己约定一种{"type":"heartbeat"}业务消息,浪费带宽不说,还需要业务层解析逻辑。

参数位置推荐值说明
HeartBeatInterval服务端属性3000030 秒发一次 PING,单位毫秒
HeartBeatTimeout服务端属性1000010 秒内收不到 PONG 判定超时
MaxPayloadLength服务端属性1024*1024单条消息最大 1MB,防止内存被打爆
MaxConnections服务端属性00 表示不限制,生产环境按资源定

HeartBeatTimeout要比HeartBeatInterval小,例如 30 秒发一次 Ping、10 秒等不到 Pong 就判定连接死掉。反过来会导致 TCP 层已经断开很久了,业务层还不自知。

第 3 章的落点在于:服务端能不能扛住并发,主要看你是否理解回调线程模型和心跳探测机制。组件帮你做了协议层的封包拆包,但线程调度和连接生命周期判断需要自己设计。

4. 用 TsgcWebSocketClient 实现客户端连接、WSS 配置与断线重连

4.1 最小客户端代码与连接状态回调

客户端和服务端在组件的使用上是对称的,差别在于TsgcWebSocketClient是主动发起连接的一方。下面是一段最小实现,包含连接、发消息、断开三件事:

procedure TMainForm.ConnectToServer; begin FClient := TsgcWebSocketClient.Create(nil); FClient.Host := '192.168.1.100'; FClient.Port := 8080; FClient.Tls := False; // 走 ws:// 协议 FClient.OnMessage := HandleClientMessage; FClient.OnConnect := HandleClientConnect; FClient.OnDisconnect := HandleClientDisconnect; // 设置握手超时:单位毫秒 FClient.Timeout := 5000; FClient.Connect; end;

Timeout参数很多人不设置,默认值可能长达 30 秒或更长。如果目标服务器 IP 不可达,TCP 连接会卡在 SYN 重传上,用户点了连接按钮半天没反应。设成 5 秒是合理的业务体验底线,但要注意:超时之后组件内部会自动触发OnDisconnect,不要重复调用Disconnect,否则会抛异常。

4.2 WSS 连接与 TLS 证书校验的两个注意点

走 WSS 时需要先把Tls设为True,然后要处理证书验证。生产环境用正规 CA 证书,直接默认校验就行;内网测试用自签名证书,默认校验会失败。

// 自签名证书场景:允许忽略证书链错误 FClient.Tls := True; // sgcWebSockets 的 TLS 层基于 OpenSSL,所以要先初始化 InitOpenSSL; // 通过 CertFile / KeyFile 指定客户端证书(可选) // FClient.SSL.CertFile := 'client.crt'; // 如果服务端证书是自签名,关掉严格校验 FClient.SSL.VerifyMode := [];

InitOpenSSL这一步非常关键,很多人在 Delphi 里跑 WSS 报TLS handshake failed,四处排查发现是 OpenSSL 的 DLL 没加载。注意libssl-3-x64.dlllibcrypto-3-x64.dll要放在 exe 同目录,而且 32 位和 64 位版本不能混用。VerifyMode属性设成空集合表示不校验证书链,平时开发方便,生产环境不要这么干。

4.3 断线重连的参数设计:指数退避而不是固定 1 秒

客户端断线重连是最容易被做糙的环节。固定 1 秒重连一次,服务端重启时客户端全变成发疯的扫描器,每秒钟打一遍端口。指数退避是常见解法,起始 1 秒,最大 30 秒,每次翻倍并加一点随机抖动。

procedure TMainForm.HandleClientDisconnect(aSender: TsgcWSBaseConnection); begin // 延迟重连:避免服务端刚启动时流量风暴 FReconnectCount := FReconnectCount + 1; FReconnectDelay := Min(30000, 1000 * (1 shl FReconnectCount)); // 用 TTimer 或 TThread 延迟执行重连 TThread.CreateAnonymousThread( procedure begin Sleep(FReconnectDelay); TThread.Synchronize(nil, procedure begin FClient.Connect; end); end ).Start; end;

1 shl FReconnectCount是 2 的 N 次方,第 1 次 2 秒、第 2 次 4 秒、第 3 次 8 秒,加Min做上限保护。重连成功后在OnConnect里把FReconnectCount清零。这个方案的核心是:连接失败不是异常,而是常态,系统的稳定性靠的是失败后的行为可控。

4.4 用 Postman 做客户端调试,反向验证服务端逻辑

sgcWebSockets 的服务端写好之后,不建议直接用自己写的客户端调试,因为两边都是新代码,出错了分不清是谁的问题。Postman 从 2023 年开始支持 WebSocket 连接,可以直接当调试客户端用。输入ws://192.168.1.100:8080,点 Connect,然后手动发 JSON 消息,观察服务端返回。这一步能快速验证服务端OnConnectOnMessage回调是否触发。

注意 Postman 的 WebSocket 客户端默认不带 subprotocol,如果你的服务端强制要求 subprotocol 协商,连接会被拒绝。这种情况先去服务端把 subprotocol 判断做成可选的,等联调通了再收紧。Postman 也支持设置 header,Sec-WebSocket-Protocol: chat可以在连接时手动加上。

第 4 章的要点概括起来就是:客户端不是把Connect调通就完事了,WSS 证书、握手超时、断线重连这三个点决定了这个客户端能不能在生产环境里稳定运行一整周不挂。

5. 内存管理、回调顺序与并发控制的进阶陷阱

5.1 OnMessage 里的大消息处理:分片消息与内存占用

WebSocket 协议允许消息分片,大消息会被拆成多个 Frame 传输。sgcWebSockets 默认会在内部把分片重组完后才触发OnMessage,这事本身不用你操心,但要注意MaxPayloadLength。这个限制决定了单条消息能传多大。

如果业务里有大数据传输,比如上传日志文件,建议走消息分片而不是把MaxPayloadLength调到极大值。一个 100MB 的消息,在组件内部重组时要占用连续内存缓冲区,Delphi 的默认内存管理器在这种场景下容易产生地址空间碎片。常见做法是:客户端先把数据切块,每块 64KB 或 256KB,通过统一的消息带上序号,服务端收全后合并落盘。这种方案不仅绕过MaxPayloadLength限制,还能做断点续传。

5.2 消息并发顺序:同一个连接上的消息顺序不能保证吗

WebSocket 协议本身保证消息在单一 TCP 连接上的完整顺序,服务端收到消息 A再收到消息 BOnMessage的触发顺序也是 A 然后 B。问题出在回调线程池:如果服务端配置了多线程接收,两条消息可能被分到不同线程处理,你的业务代码如果依赖顺序,就会出问题。

sgcWebSockets 里每个连接可以有独立的接收线程,也可以共享线程池。共享线程池节省资源,但多个连接的消息间没有顺序保证;单个连接的消息顺序由 TCP 保证,前提是这个连接的处理没有被线程池打散。如果业务里强依赖顺序,处理方案是在业务层加自增序号和乱序缓冲,而不是在组件层调线程模型。

5.3 连接对象生命周期:不要用全局变量缓存连接对象

服务端场景里,你通常需要保存所有活跃连接,方便后续往某个客户端推消息。很多人用TList<TsgcWSBaseConnection>存连接指针,然后发现客户端断开后列表里的指针悬空了。正确做法是监听OnDisconnect回调,在里面同步移除连接引用,或者在TsgcWSBaseConnection的子类里维护状态,用组件自己的类来扩展数据。

type TMyConnection = class(TsgcWSBaseConnection) public UserID: string; LastActive: TDateTime; end;

然后通过OnConnect里可以拿到连接对象并转为TMyConnection,往连接类上挂业务属性。这种扩展方式比用TList加关联字典更简洁,因为断开时组件自己释放连接对象,不需要你额外清理,只要不在释放后访问即可。

常见误用后果建议
全局变量保存连接引用悬空指针访问崩溃用连接对象自己的属性存业务数据
OnMessage 里直接处理耗时逻辑阻塞当前连接的下一条消息接收丢到独立线程池
Disconnect 后再调用 Send异常或静默失败Send 前用Connection.Active判断
修改MaxPayloadLength到 100MB内存峰值过高应用层分片传输

5.4 用日志定位并发问题:每个连接打点时要带连接标识

OnConnectOnDisconnect里打日志时,只打一条字符串是排不了并发问题的。要输出连接对象的内存地址或内部 ID,因为多个连接的回调交错时,没有标识的日志根本对不上号。

procedure TMainForm.HandleConnect(aSender: TsgcWSBaseConnection); begin Log(Format('Connection established: %p, remote: %s', [Pointer(aSender), aSender.RemoteAddr])); end;

%p格式化指针地址,能区分是哪个连接实例。再把日志写到一个循环缓冲区里,崩溃后导出,能看到最后几秒的上下文。这个技巧在排查“某个客户端连接被莫名断开”时特别有效。

第 5 章要传达的不是某个具体参数,而是一个观念:WebSocket 库本身把协议细节吞掉了,但是回调时机、对象生命周期、内存峰值这些边界条件它吞不掉,排错方向都指向这些边界。

6. 用 OnMessage 回调做二阶段分发,把耗时逻辑从 WebSocket 线程中剥出去

在实际项目里,服务端收到一条消息后往往要做数据库查询、调用内部 HTTP 服务,或者触发耗时计算。如果直接在OnMessage里做这些事,组件负责该连接的接收线程会被卡住,影响这个连接上的后续消息,极端情况下心跳也会因线程阻塞而超时,服务端主动断开连接。

常见的解法是自己做二阶段分发:第一阶段,OnMessage只做快速解析,把解析后的数据放进线程安全队列;第二阶段,工作线程池从队列里取任务执行真正业务逻辑,执行完后通过TThread.Queue回到主线程或直接把结果推给连接。下面给一个轻量实现,不引入第三方队列库,用TThreadedQueue<T>即可:

type TMessageTask = record Conn: TsgcWSBaseConnection; Data: string; end; procedure TMainForm.HandleMessage(aSender: TsgcWSBaseConnection; const aText: string); var LTask: TMessageTask; begin // 快速路径:只做解析和入队 LTask.Conn := aSender; LTask.Data := aText; FQueue.PushItem(LTask); end; procedure TMainForm.WorkerLoop; var LTask: TMessageTask; LResult: string; begin while not FTerminated do begin if FQueue.PopItem(LTask) = wrSignaled then begin // 耗时逻辑在这里执行,不阻塞 WebSocket 接收线程 LResult := DoHeavyWork(LTask.Data); // 发送结果时注意连接可能已断开 if LTask.Conn.Active then LTask.Conn.Write(LResult); end; end; end;

入队的Data是字符串拷贝,大消息会带来一次内存复制开销,但换来的收益是接收线程不会被业务逻辑卡死,心跳和后续消息照常处理。发送结果时要用LTask.Conn.Active判断连接状态,因为工作线程处理期间客户端可能已经断开,Write一个无效连接会触发异常。如果要在Write里处理异常,包一层try/catch并记录错误即可。

这个二阶段分发方案再往前走一步,就是把队列里的任务按连接 ID 做分组,保证同一个连接的任务按顺序执行,不同连接的任务并行处理。用 TDictionary 加锁,或者用多个TThreadedQueue,每个连接一个队列,实现起来都不复杂。考虑到TsgcWebSocketServer本身就支持 TLS、限流、心跳等机制,你在业务层处理消息时其实不需要再关心协议细节,只需要关心一件事:别把它自己的工作线程拖死,剩下的就都稳了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询