资讯详情

DelphiWebsockets实战:为Delphi桌面应用实现WebSocket实时通信

📅 2026/10/13 13:58:30 | 华诺云谱 👁 阅读
DelphiWebsockets实战:为Delphi桌面应用实现WebSocket实时通信
简介DelphiWebsockets 是一份面向 Delphi 开发人员的 WebSocket 服务端与客户端实现资源适用于聊天、股票行情、在线协作等需要实时双向通信的应用场景尤其适合基于 VCL 构建高交互性服务的项目。压缩包共 32 个文件约 134KB核心为 22 个 Pascal 源文件和 2 个工程文件另含界面定义、配置与说明文档、单元测试及 WebSocket 客户端脚本还包含 .res、.gitattributes、.gitignore 等工程辅助文件覆盖 Indy 与 WebSocket4Delphi 等常用库的封装。源码完整展示了从初始化监听端口、响应 HTTP 升级请求并返回 101 状态码到帧解析、数据收发、错误处理与连接管理的流程可帮助开发者理解服务端的事件驱动机制、帧格式和握手细节并为排查连接断开、超时等问题提供参考。已有 418 人学习下载包内附带测试用例和演示工程便于在实际项目中快速验证与迁移。1. DelphiWebsockets 是什么给 Delphi 桌面端补上实时通信这一课DelphiWebsockets 是把 C 语言写的 libwebsocketslws封装成 Delphi 单元的开源库补上了 Delphi 在 WebSocket 客户端和服务端两头的能力。很多 Delphi 桌面项目会在同一处卡住设备状态要实时上大屏、工单要推给多个操作端、内部消息要秒到传统做法用 HTTP 轮询3 秒一刷延迟肉眼可见服务端还被无效请求打满。换 WebSocket 之后服务端主动推送延迟能压到毫秒级。DelphiWebsockets 的价值就是不让你去啃 RFC6455 里那些分片、掩码、握手细节也不用在 Indy 老代码里缝缝补补直接给出客户端与服务端两套 API。适合 Delphi 开发者做工业上位机、局域网即时通信、远程控制这类实时项目。2. 为什么绕不开 DelphiWebsockets三条路线对比与回调模型选 WebSocket 库之前先想清楚一件事你要的是客户端、服务端还是两头都要。Delphi 官方一直没有给出一套完整的 WebSocket 协议实现社区方案也各有取舍。有人觉得这不过是个通信协议找个例子抄一抄就行真做起来才会发现分片重组、掩码处理、心跳超时、TLS 握手这些细节堆在一起能把一个简单需求拖成两周的排障马拉松。2.1 Delphi 做 WebSocket 的三条路原生 Client、Indy 插件与 libwebsockets 封装先看三条路线的能力边界这里直接对比路线客户端服务端TLS/wss与 VCL 线程配合Delphi 自带 System.Net.WebSocketClient支持不支持支持默认校验较严异步事件需自行切主线程Indy 组件扩展可手写代码量大基于 TIdHTTPServer 手写升级依赖 OpenSSL配置繁琐回调跑在 Indy 线程DelphiWebsocketslibwebsockets 封装完整完整支持证书策略由 lws 控制事件在库内部线程需 Synchronize 切主线程先看 Delphi 自带的那条路。System.Net.WebSocketClient 作为纯客户端是能用的连 wss 也支持但第一它没有服务端实现你只能在「桌面程序连别人服务端」的场景下用它第二它的异步事件风格和 VCL 的窗口消息循环不搭每次收到消息都要自己做线程切换消息一多代码就散得到处都是。我见过有人用它在客户端收了数据直接写界面跑起来界面偶尔无响应查了半天才发现是回调线程的问题。再看 Indy 路线。Indy 在 Delphi 项目里存在感很强很多人第一反应就是拿 TIdHTTPServer 做 HTTP Upgrade 握手然后把它当长连接读帧。握手那一步确实不难算一下 Sec-WebSocket-Accept返回 101 就行。但真实项目里一旦涉及广播、分片、心跳、TLS 这些代码量和出问题的面会迅速膨胀而且 Indy 对 WebSocket 的支持从没成为一等公民出问题主要靠自己在社区翻帖子。这条路适合用来理解协议原理不适合作为交付方案。第三条就是 DelphiWebsockets。它把 libwebsockets 封装成 Delphi 单元lws 本身是 C 写的 WebSocket 实现服务端和客户端都齐全在嵌入式领域跑了很多年协议合规性和并发处理能力都比较可靠。Delphi 侧暴露的是连接、事件、发送这几个基本接口学习成本很低不需要懂 C。它的代价是要带动态库部署Windows 下是 libwebsockets.dll再加上配套的 OpenSSL 动态库。我的结论很直接要两头齐全、要 wss、要省心就选第三条后面所有内容都围绕它展开。2.2 回调模型与线程归属谁在什么线程通知你的代码用这个库之前必须先理解 lws 的运作模型。lws 内部是一个事件驱动循环所有 socket 上的可读、可写、断开事件都在这个循环里被检查通过回调函数上报给外层。DelphiWebsockets 做的事情是把这个 C 层回调翻译成 Delphi 事件连接建立时触发 OnOpen收到数据帧时触发 OnMessage连接断开时触发 OnClose出错时触发 OnError。翻译逻辑本身很薄但它决定了你所有代码的执行上下文。关键点在于这些事件全部发生在 lws 的线程上不经过 VCL 消息循环。也就是说你在 OnMessage 里写的代码不是在主线程跑的直接碰控件就是踩雷。这个模型还能解释另一个现象Send 方法通常只是把数据投进发送队列真正写 socket 要等 lws 循环里出现可写事件。所以 Send 返回成功不代表对端已经收到它只代表数据已入队高频发送大消息时队列积压才是你需要盯的指标。这个回调模型是后面所有坑的总根源。UI 操作要回主线程对象的创建和释放要避开事件回调跨线程访问会话集合要加锁——理解了这一条后面第 5 章那些翻车现场你就能一眼看穿。3. 编译 libwebsockets 并集成到 Delphi 工程「从 C 源码到 Delphi 能跑」的完整步骤用 DelphiWebsockets 的第一道坎不是写代码是把 lws 的 C 库编译出来并让 Delphi 工程正确找到它。很多人在这一步放弃了其实流程是固定的准备工具链、配 CMake 参数、编译出动态库、把 Delphi 单元路径和 DLL 放好。一次跑通之后换平台也就是重复一遍。3.1 环境准备CMake、C 编译器与 OpenSSL 依赖编译 lws 需要的工具是固定的三样CMake、一个 C 编译器、以及可选的 OpenSSL。CMake 版本不用太新手头能装到的最新版就行。编译器在 Windows 上推荐 Visual Studio 或者 MinGW-w64Linux 上用 gccmacOS 上用 clang。工具链的选择会影响后面 DLL 的运行时依赖建议你在一个项目里只用一套别今天 MinGW 明天 VS 混着来。如果只需要 ws:// 明文连接OpenSSL 可以不装编译时关掉 TLS 开关就行。但实际项目里 wss 几乎必定要上所以要提前准备 OpenSSL 的头文件和库。Windows 上我一般直接拿预编译的 OpenSSL 包不要自己用源码编 OpenSSL省下大量时间。这里有一个必须注意的原则OpenSSL 的位数必须和你的 Delphi 工程一致Delphi 编译出来是 32 位就配 32 位的 lws 和 32 位 OpenSSL是 64 位就全部用 64 位混搭会让运行时加载直接失败。3.2 编译 libwebsockets关键 CMake 参数与生成结果在 lws 源码根目录打开终端按下面的命令配置并编译。这里以 Windows MinGW-w64 为例Visual Studio 只要把生成器换成对应名称其他参数不变。# 在 libwebsockets 源码根目录执行以 Windows MinGW-w64 为例 cmake -B build -S . \ -G MinGW Makefiles \ -DLWS_WITH_SSLON \ -DLWS_WITH_CLIENTON \ -DLWS_WITH_SERVERON \ -DLWS_WITHOUT_TESTAPPSON \ -DLWS_WITH_STATICOFF \ -DLWS_WITH_SHAREDON \ -DCMAKE_BUILD_TYPERelease cmake --build build -j 8逐个看这些参数。LWS_WITH_SSL 是 TLS 总开关新版的 lws 把这个选项改名为 LWS_WITH_TLS你打开 CMakeLists.txt 确认一下实际选项名再传参。LWS_WITH_CLIENT 和 LWS_WITH_SERVER 分别控制编译出客户端和服务端能力DelphiWebsockets 两头都要用所以都打开。LWS_WITHOUT_TESTAPPS 用来去掉自带的测试程序能减少不少编译面。LWS_WITH_STATIC 设为 OFF、LWS_WITH_SHARED 设为 ON目的是只生成动态库因为 Delphi 工程走 DLL 部署更省事静态库在 Delphi 里链接 C 运行库反而容易引出符号冲突。编译失败的常见情况是 OpenSSL 找不到。这时在 CMake 命令里追加 -DOPENSSL_ROOT_DIR你的OpenSSL路径或者设置环境变量 CMAKE_PREFIX_PATH 指向 OpenSSL 安装目录。编译成功的产物集中在 build/binWindows 下是 libwebsockets.dllLinux 下是 .somacOS 下是 .dylib。另外记住OpenSSL 的动态库要在部署时一并发给目标机器Delphi 的 exe 自己不会带上它们。3.3 把动态库与 Delphi 单元接入工程搜索路径与启动初始化编译完成之后接入 Delphi 工程的步骤是固定的。先把仓库里 Delphi 侧的单元文件复制到工程目录下的 ThirdParty 文件夹这些文件一般集中在一个 source 目录里。然后在 IDE 的 Project Options 里找到 Delphi Compiler 页把 Search path 指向这个目录。接着把编译出的 DLL 和 OpenSSL 的 DLL 拷贝到 exe 输出目录也就是你的 Debug 或 Release 文件夹。最后打开仓库自带的 demo 工程试编译如果 demo 能编译能跑说明版本匹配集成就成功了一半。Delphi 侧启动时的最小初始化逻辑如下uses // 单元名以仓库实际文件为准可能是 DelphiWebsockets.pas 之类 DelphiWebsockets; procedure TMainForm.FormCreate(Sender: TObject); begin // 打开底层日志调试期建议用 LLL_WARN 或 LLL_ERR // 具体函数名和枚举以仓库 API 为准 LWS_SetLogLevel(LLL_ERR); // 客户端对象统一在主窗体创建避免在回调里反复 Create/Free FClient : TWebSocketClient.Create; FClient.OnMessage : HandleMessage; FClient.OnError : HandleError; end;这段初始化做了两件事设置 lws 日志级别这决定了出问题时底层能吐出多少排障信息创建客户端对象并挂事件。日志输出是后面排查问题的第一入口很多看似诡异的现象打开日志后一眼就能看到握手失败或连接被重置的原因。API 名和仓库不同时不要硬翻译以 demo 工程为准类名、事件名、枚举名在不同版本里确实有过调整。4. 跑通最小可用的客户端与服务端核心代码与必调参数集成通过之后下一步就是让代码真正跑起来。下面代码里的类名与方法名来自这个库的常见版本如果你拿到的版本不同以仓库里的 demo 工程为准做重命名。这一章的目标不是写完整个业务而是用最少的代码验证客户端和服务端都能独立收发消息。4.1 客户端最小示例连接、发送文本与接收事件先写客户端。一个最小可用的客户端要做四件事创建对象、挂事件、Connect、在 OnOpen 里发第一条消息。procedure TMainForm.ConnectClick(Sender: TObject); begin FClient.OnOpen : HandleOpen; FClient.OnMessage : HandleMessage; FClient.OnError : HandleError; FClient.OnClose : HandleClose; // Connect 是异步的连接结果通过 OnOpen / OnError 通知 // 参数按仓库 API 格式传支持完整 URL 或分传 Host 和 Port if FUseWSS then FClient.Connect(wss://192.168.1.50:9000) else FClient.Connect(ws://192.168.1.50:9000); end; procedure TMainForm.HandleOpen(Sender: TObject); begin // 连接成功这里执行发送上下文在库的内部线程 FClient.SendText(hello from delphi); end; procedure TMainForm.HandleMessage(Sender: TObject; const Msg: string); begin // 收到文本消息先切回主线程再更新 UI TThread.Queue(nil, procedure begin Memo1.Lines.Add(Msg); end); end;Connect 是异步的所以别在 Connect 调用之后立刻 Send这时连接大概率还没建立。连接成功的标志是 OnOpen 触发失败则走 OnError。OnOpen 里发第一条消息是常见的初始化方式可以避免「启动就发消息结果消息丢了」的问题。SendText 的返回值只代表数据已进入发送队列不代表对端已经收到这个语义在前面提过在高频发送时尤其要注意。OnMessage 的参数在不同版本里可能是 string也可能是一段 TBytes。如果你需要收二进制帧就按 TBytes 分支处理并在内部按 UTF-8 解码。中文消息如果直接当 AnsiString 显示十有八九是乱码先把字节流转成 UTF-8 字符串再显示。4.2 服务端最小示例监听端口、接收连接与广播服务端的写法更直接创建服务端对象、挂事件、Start 就完事。广播功能是自己遍历连接集合实现的库不会替你管理业务上的分组。procedure TMainForm.StartServerClick(Sender: TObject); begin FServer : TWebSocketServer.Create(nil); FServer.OnConnect : HandleServerConnect; FServer.OnMessage : HandleServerMessage; FServer.OnDisconnect : HandleServerDisconnect; // 监听本机所有网卡的 9000 端口 FServer.Start(9000); end; procedure TMainForm.BroadcastClick(Sender: TObject); var i: Integer; begin // 遍历当前连接集合逐个发送 for i : 0 to FServer.Clients.Count - 1 do FServer.Clients[i].SendText(broadcast at TimeToStr(Now)); end;Start 之后服务端在内部线程跑的也是 lws 的 service 循环每个新接入的连接触发 OnConnect断开时触发 OnDisconnect。Clients 集合是服务端的会话列表广播就是遍历它逐个发送。这里要注意并发如果消息回调所在线程和主线程同时访问 Clients 集合最好加临界区保护或者统一把集合操作切到主线程做否则遍历中途集合被修改会直接报越界。消息回调里通常带一个连接对象参数用来区分消息来自哪个客户端。业务上要做的会话映射就是拿这个对象当 key把用户信息、设备信息挂在它身上。这个技巧放到第 6 章细说。4.3 三个必调参数缓冲、心跳与 TLS 开关库暴露的配置项不算多但三个参数的效果最直接动手前先确认一遍。参数常见默认影响建议单条消息缓冲区数 KB 级别超过上限的消息会被分片或丢弃按单条消息最大长度的 1.5 倍设置心跳周期与超时lws 协议层默认值长时间静默连接被误判为死链局域网放宽公网按 30s/90s 调TLS 开关与证书策略随编译选项wss:// 能否正常握手调试期配置自签名证书或关闭校验缓冲区大小决定单条消息能装多大。如果你的业务要推送几百 KB 的 JSON 或文件块默认的几 KB 远远不够要找到对应属性调大否则消息会在 lws 层被分片或直接被丢弃。心跳方面lws 协议层会自己处理 ping/pongDelphi 侧通常不需要你手动回 pong但业务层的应用心跳还是建议自己实现比如服务端 15 秒没收到任何消息就主动断开该连接这样才能确认业务链路是活的。TLS 开关决定了你连的是 ws 还是 wss证书策略在前面的第 5.3 节会有详细排障顺序这里先记住调试期别在证书上死磕先确认 OpenSSL 版本一致再看证书策略。5. DelphiWebsockets 避坑指南线程、DLL 生命周期与 TLS 的五个翻车现场这一章的坑按出现频率排序每一条都是现象、原因、解决三段式。前两条是线程和生命周期问题后三条和协议细节、部署环境相关。它们之间没有必然顺序但在同一项目里踩中的概率极高。5.1 回调里更新 UI界面没反应甚至闪退现象在 OnMessage 里直接写 Memo1.Lines.Add运行起来界面卡死或者偶发崩溃日志看不出明显异常因为异常发生在库的内部线程。原因回调跑在 lws 线程不经过 VCL 消息循环而 VCL 控件不允许在主线程之外访问。解决所有 UI 更新都用 TThread.Queue 或 TThread.Synchronize 切回主线程。Queue 不会阻塞当前线程回调里推荐用 Queue。更稳当的做法是在回调里只记日志再通过自定义消息或定时器触发界面刷新把 UI 操作和网络回调彻底解耦。如果想进一步降低崩溃概率可以在主窗口收到 WM_APP 消息时集中刷新界面回调只 PostMessage。这样即使网络回调高频触发UI 刷新次数也能被你自己控制住不至于一秒刷新几百次把界面拖死。5.2 程序退出时崩溃在 lws 内部对象的释放顺序错了现象点了退出按钮主窗体关了进程还挂在后台或者崩溃在 libwebsockets 内部的 socket cleanup 代码里。原因客户端或服务端对象在内部线程还在跑的时候就被 Freelws 访问了已释放的内存。这是这类封装库最常见的程序退出崩溃几乎每次都栽在释放顺序上。解决退出流程按固定顺序走——先断开连接或停止服务再清空发送队列再释放对象。不要在事件回调里直接 Free 当前连接对象那是最典型的自毁操作lws 正在处理这个 socket你把它背后的对象释放了下一步就访问野指针。在 FormClose 里建议的顺序是先把待发数据清空然后调用 Disconnect 或 Stop等待片刻让内部线程退出最后再 Free 对象。如果库提供了 WaitFor 或 CloseWait 之类的方法优先用它们。实在没有就在 Stop 后加一个几十毫秒的延时给内部循环一个退出窗口。5.3 wss 握手失败OpenSSL 版本与证书策略双重坑现象ws:// 一切正常换成 wss:// 就报 handshake error 或证书错误客户端连不上日志里有 TLS 相关报错。原因通常是两个叠加。第一是随包带过去的 OpenSSL 动态库和编译 lws 时用的版本不一致符号版本对不上握手在 TLS 层就失败了第二是目标服务端用的是自签名证书lws 默认的证书校验策略过不了。解决排障顺序不能反。先确认 OpenSSL 的 DLL 与编译期一致写个小程序读取 DLL 版本信息对比再处理证书把自签名证书导入系统受信任的根证书存储或者按库的能力关掉客户端侧的证书校验。我见过有人调了一整天证书最后发现是 OpenSSL 混用白白浪费时间。如果库没有直接暴露关闭证书校验的开关另一个常用思路是在 lws 源码层把证书校验回调改成放行但这要求你会改 C 代码并重新编译 lws非必要不建议走这条路。5.4 收到的消息多几个字节或不完整文本帧、二进制帧与缓冲区边界现象某些消息前面多了几个乱码字节或者长消息被截断短消息正常。原因一端按文本帧发送另一端按二进制帧接收帧类型约定不一致或者单条消息超过缓冲区上限被 lws 分片后Delphi 侧只把第一段当作完整消息。解决先统一帧类型文本就都走 SendText二进制就都走 TBytes 接口不要混用。再把缓冲区调大到超过单条消息的长度上限避免触发分片。另外注意中文消息走文本帧时如果收到后直接按 AnsiString 处理会出现乱码要先按 UTF-8 解码再显示。判断帧类型的方法通常是看 OnMessage 的参数是 string 还是 TBytes也能通过消息帧的 opcode 区分看库里有没有暴露这个字段。5.5 连接数一多就被拒绝文件描述符与端口范围现象测试时 100 个连接都正常到 300 个时新连接全部失败已有连接也开始掉。原因进程级文件描述符上限不足或者 Windows 动态端口范围被耗尽也可能是服务端没有及时清理已断开的连接句柄不断累积。解决Linux 下先看 ulimit -n 的输出并调大连接数上限Windows 下用 netsh int ipv4 show dynamicport tcp 查看动态端口范围确认没有被占满。代码层面要确保 OnDisconnect 里把会话对象释放干净把连接从 Clients 集合移除避免断开连接的对象还挂在集合里占用句柄。这类问题最隐蔽的点是被移除的连接对象如果在别处还有引用析构会被延迟句柄会一直在涨。建议定期打印客户端数量和系统句柄数做对比涨了就是有泄漏。6. 进阶用法心跳保活、断线重连与连接对象管理6.1 应用层心跳与断线重连协议层的 ping/pong 只保证 TCP 链路活着业务链路死没死它管不着。我的做法是在业务层加心跳客户端每 5 秒发一条 {type:ping}服务端收到后原样回 {type:pong}服务端若连续 15 秒没收到任何消息就主动断开该连接。重连用指数退避首次等 1 秒失败后翻倍到 30 秒封顶。每次重连前先释放旧对象再新建客户端对象不要拿同一个对象反复 Connect状态残留会引起各种奇怪问题。6.2 会话与业务对象映射服务端回调里拿到的连接对象是抽象的业务上要知道它对应哪个用户、哪台设备。常见做法是把业务信息挂到连接对象的 Tag 或 Object 属性上OnConnect 里先不急着绑定等收到第一条鉴权消息后把 userId 写进 Tag后续所有消息按 Tag 分发。断开时读 Tag 做资源清理。这套映射要配合临界区使用因为库的内部线程和主线程可能同时访问同一个 Tag不加锁会在极端并发下丢数据。6.3 本地验证方法最快的验证工具是浏览器。本地起服务端监听 127.0.0.1:9000然后在浏览器开发者工具里执行new WebSocket(ws://127.0.0.1:9000)看 onopen 是否触发。再搭一个 echo 逻辑收到什么原样发回浏览器端就能完整验证双向链路。如果 echo 通了说明库的收发、帧解析、线程模型都没问题剩下的只是业务代码。我自己最早在这类库上翻车就是直接在回调里写了个 ShowMessage结果服务端一收消息整个界面卡死。后来养成了习惯凡是回调第一句先判断线程再谈业务。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑