尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

VB6/VBA实现WebSocket完整指南:从握手到断线重连

VB6/VBA实现WebSocket完整指南:从握手到断线重连 简介这是一份面向VB开发者的WebSocket通信实例源码覆盖客户端与服务端两端实现可用于学习如何在经典VB环境中集成WebSocket协议、完成握手连接、数据帧收发与消息推送等关键操作。代码结构清晰注释与模块划分便于新手对照理解也有一定经验的开发者可直接移植或二次扩展。无论刚接触网络编程还是已有VB基础但未曾深入WebSocket都可以通过这套源码快速建立完整认知。压缩包整体约476KB暂未单独列出文件清单和类型明细核心内容为可直接运行的VB工程源码。目前已有754人学习/下载适合需要快速上手VB网络编程或希望在自己的桌面应用中嵌入WebSocket能力的技术人员。通过这份源码读者可以理清客户端与服务端的交互流程掌握协议解析思路并为后续集成即时通信、远程控制等功能打下基础。1. VB 的 WebSocket 缺口实例程序为什么必须同时给客户端和服务端源码VB6/VBA 环境里没有官方 WebSocket 组件这是老项目改造时最先撞上的墙。网上能搜到的所谓 VB WebSocket 实例程序源码本质上都是用 WinSock 控件手写 HTTP Upgrade 握手和帧解析代码而不是引用一个现成的 DLL。同时给客户端和服务端源码不是作者想凑文件数而是 WebSocket 握手机制天然要求两端对齐服务端算错Sec-WebSocket-Accept值客户端连接必然被 1006 关闭客户端发送业务帧时不按协议加掩码服务端按协议就要主动断开。只拿一边代码看这类问题永远排不出来。下面把标题里这套源码拆成服务端、客户端、帧协议、断线重连四块来讲适合手头有 VB6/VBA 老程序、需要在工控界面或 Access 前端里接入实时推送的工程师。2. 搭服务端用 VB 写 WebSocket 服务端的最小可运行骨架2.1 WebSocket 握手不是普通 Web 请求先理解 101 状态码WebSocket 会话的起点是一个普通 HTTP GET 请求但请求头里带着Upgrade: websocket。服务端如果同意切换协议只能返回101 Switching Protocols并带上Sec-WebSocket-Accept响应头。这个头不是随便填的它是客户端Sec-WebSocket-Key拼接固定 GUID 后做 SHA-1 再 Base64 编码的结果。固定 GUID 是258EAFA5-E914-47DA-95CA-C5AB0DC85B11这是协议规范里写死的字符串一个字节都不能改。VB6 的 WinSock 控件拿到的数据是原始字节流。客户端发来的握手请求长这样GET /chat HTTP/1.1 Host: 192.168.1.10:8080 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ Sec-WebSocket-Version: 13 Origin: http://192.168.1.10注意Sec-WebSocket-Version必须是 13这是目前唯一被广泛支持且 RFC 6455 定稿的版本。如果客户端发来的是 8 或 12通常是老库或者某些嵌入式设备服务端可以返回426 Upgrade Required并在Sec-WebSocket-Version头里告诉对方支持的版本。服务端要做的第一件事是判断收到的数据里有没有\r\n\r\n这个头部结束标记收到完整头部后再解析而不是收到第一包就直接处理。为什么强调这一点因为 TCP 是流式协议一次DataArrival事件里可能只到了半个包也可能同时到了完整握手加第一条业务帧。如果没做缓冲处理会出现握手成功但第一条消息丢失的诡异现象。先收完整头部再解析再进入帧循环这是服务端源码里最重要的一段结构。2.2 服务端源码WinSock 控件数组与握手响应VB6 里做服务端一般用一个 WinSock 控件数组。索引 0 作为监听 socketAccept后新连接的索引动态添加。请求头解析和响应写在DataArrival里关键代码如下 模块级变量保存每个连接尚未处理完的字节 Private buffers() As String Private Sub Winsock1_DataArrival(ByVal Index As Integer, ByVal bytesTotal As Long) Dim data As String Winsock1(Index).GetData data, vbString buffers(Index) buffers(Index) data 检查是否收到完整的 HTTP 头部结束标记 Dim headerEnd As Long headerEnd InStr(1, buffers(Index), vbCrLf vbCrLf, vbTextCompare) If headerEnd 0 Then Exit Sub 还没收全等下一包 Dim header As String header Left$(buffers(Index), headerEnd - 1) buffers(Index) Mid$(buffers(Index), headerEnd 4) 剩余字节留给帧解析 If InStr(1, header, Upgrade: websocket, vbTextCompare) 0 Then Call SendHandshakeResponse(Index, header) Else Winsock1(Index).SendData HTTP/1.1 400 Bad Request vbCrLf vbCrLf End If End Subbuffers(Index)是每个连接独立的缓冲区用InStr找\r\n\r\n是最常见的判断方式。找到后把头部从缓冲区里切掉剩下的字节就是 WebSocket 帧数据。后续所有业务数据都要走帧解析流程不能再用字符串拼接方式处理。用vbString取数据在纯 ASCII 场景没问题但如果消息里有中文帧解析阶段必须按字节处理这一点在第 4 章展开。握手响应的核心是算Sec-WebSocket-Accept。VB6 没有内置 SHA-1常见做法是内嵌一个纯 VB 的 SHA-1 实现或者调用系统里的 CAPICOM 组件。代码示意如下Private Sub SendHandshakeResponse(ByVal Index As Integer, ByVal reqHeader As String) Dim webKey As String webKey ExtractHeader(reqHeader, Sec-WebSocket-Key) Dim accept As String accept Base64Encode(SHA1(webKey 258EAFA5-E914-47DA-95CA-C5AB0DC85B11)) Dim resp As String resp HTTP/1.1 101 Switching Protocols vbCrLf _ Upgrade: websocket vbCrLf _ Connection: Upgrade vbCrLf _ Sec-WebSocket-Accept: accept vbCrLf vbCrLf Winsock1(Index).SendData resp End SubExtractHeader是个按行解析Key: Value的小函数注意 VB6 里字符串拼接用而不是收到中文或二进制时有时会被当算术运算符。SHA1函数如果自己写建议用标准 64 轮循环实现。网上流传的很多 VB6 SHA1 代码对小写 key 和长文本有 bug验证方法很简单用 RFC 6455 规范附录里的示例 keydGhlIHNhbXBsZSBub25jZQ期望的 accept 值是s3pPLMBiTxaQ9kYGzzhZRbKxOo。算不出这个值说明 SHA1 实现有问题先别往下写帧解析。提示CAPICOM 依赖系统组件Win7 之后默认不带跨机器部署容易翻车。推荐优先用纯 VB 实现或者用自己封装的 COM 组件避免实例程序拷到别的机器上握手直接失败。2.3 用命令行验证服务端curl 和浏览器二选一写完全部接收逻辑后第一个验证动作不是写客户端而是用现成工具测试。Windows 10 自带 curl可以直接发握手包curl -i -N -H Connection: Upgrade -H Upgrade: websocket -H Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ -H Sec-WebSocket-Version: 13 http://127.0.0.1:8080/chat服务端返回HTTP/1.1 101就说明握手通过。也可以用浏览器控制台直接执行new WebSocket(ws://127.0.0.1:8080/chat)来测。如果控制台出现onclose code 1006说明握手响应里的Sec-WebSocket-Accept算错了或者服务端发完响应就把连接关掉了。1006 这个关闭码比较特殊它是客户端在未收到正常关闭帧的情况下给出的错误码看到它优先查握手响应内容而不是查业务逻辑。如果你想做更完整的服务端接口测试可以给 JMeter 装 websocket sampler 插件写一个字节级 Echo 场景比手写客户端省事。服务端到这里只完成了骨架还差帧接收、Ping/Pong 回应和主动关闭这三个功能放到第 4 章和第 5 章。因为客户端源码里同样要处理帧把帧解析单独成章比在两边代码里重复粘贴更利于排错。3. 写客户端VB 客户端建立连接的握手与收发循环3.1 客户端握手的两个容易忽略的细节客户端握手请求和服务端的区别在于必须自己生成Sec-WebSocket-Key并且要校验服务端返回的Sec-WebSocket-Accept。Key 的生成规则是取 16 字节随机数然后做 Base64。VB6 的Rnd函数默认是线性同余生成器直接用Rnd拼 16 字节会有规律性虽然握手不会被拒绝但协议要求 Key 是随机性较强的值建议用系统 API 或高精度计时器取随机种子。第二个细节是客户端必须校验响应里的Sec-WebSocket-Accept。校验方法很简单把客户端发送的Sec-WebSocket-Key拼上固定 GUID做 SHA-1 和 Base64与服务端返回的值逐字符比较。不校验的后果是如果服务端返回的是错误响应比如配置成普通 HTTP GET客户端无法立刻感知等到发送业务帧后才发现连接被关闭报错信息还特别隐晦。3.2 客户端源码发握手、读响应、进事件循环VB6 客户端通常开一个 WinSock 连接到服务端发送握手请求后等待数据到达。代码骨架Private Sub ConnectWebSocket(ByVal url As String) Winsock1.Close Winsock1.Connect 192.168.1.10, 8080 Connect 事件里发送握手 End Sub Private Sub Winsock1_Connect() Dim secKey As String secKey GenerateRandomKey() 16字节随机数做Base64 gSecKey secKey 存起来等校验 Dim req As String req GET /chat HTTP/1.1 vbCrLf _ Host: 192.168.1.10:8080 vbCrLf _ Upgrade: websocket vbCrLf _ Connection: Upgrade vbCrLf _ Sec-WebSocket-Key: secKey vbCrLf _ Sec-WebSocket-Version: 13 vbCrLf vbCrLf Winsock1.SendData req End Sub然后是数据接收逻辑Private Sub Winsock1_DataArrival(ByVal bytesTotal As Long) Dim data As String Winsock1.GetData data, vbString gRecvBuf gRecvBuf data If Not gHandshakeDone Then Dim endPos As Long endPos InStr(1, gRecvBuf, vbCrLf vbCrLf, vbTextCompare) If endPos 0 Then Exit Sub Dim respHeader As String respHeader Left$(gRecvBuf, endPos - 1) gRecvBuf Mid$(gRecvBuf, endPos 4) If InStr(1, respHeader, 101 , vbTextCompare) 0 Then Call HandleHandshakeError(respHeader) 服务端没同意升级 Exit Sub End If Dim expectAccept As String expectAccept Base64Encode(SHA1(gSecKey 258EAFA5-E914-47DA-95CA-C5AB0DC85B11)) Dim gotAccept As String gotAccept ExtractHeader(respHeader, Sec-WebSocket-Accept) If expectAccept gotAccept Then Call HandleAcceptMismatch() 校验失败记录日志后断开 Exit Sub End If gHandshakeDone True End If Call ParseFrameData() 第4章实现 End Sub客户端这段代码的关键在于gRecvBuf这个模块级缓冲区和gHandshakeDone这个状态变量。握手响应和业务帧可能在同一次数据到达时一起被收到所以从数据里切掉响应头之后剩余部分不能丢必须立刻进入帧解析。另一个关键点是事件驱动模式VB6 的 WinSock 是异步的发完握手之后不能在一个函数里同步等响应所有后续流程都在DataArrival事件里推进。3.3 H5 能连而打包后连不上三个排查方向热搜里有一个很典型的场景网页调试时 WebSocket 连接正常打包成 App 后连不上。在 VB 场景里对应的现象是VB 写的客户端能连本地服务端但内嵌浏览器组件比如 WebView 或 IE 控件里的页面去连时失败。排查顺序按三个方向来。第一检查Origin头。服务端如果做了跨域校验App 打包后的包名或壳配置会和 H5 环境不同Origin变成null服务端需要决定放行还是拒绝。第二检查网络安全配置很多打包框架默认只允许 https连 ws 明文会被安全策略拦截。第三检查User-Agent部分老服务端按 UA 做兼容分支VB 内置的 WebBrowser 控件 UA 和现代浏览器差异很大。这三个方向里最容易踩的是第一个因为服务端代码里如果写了强校验H5 调试时配置过没问题但打包后直接 403。4. 帧协议与粘包客户端服务端都要过的数据关4.1 帧头前 2 个字节包含的信息WebSocket 帧的最小单位是 2 字节。第一个字节的最高 1 位是 FIN表示这是否为分片消息的最后一片低 4 位是 opcode决定这帧是什么类型1 是文本2 是二进制8 是关闭9 是 Ping10 是 Pong。第二个字节的最高 1 位是 MASK服务端发往客户端的帧必须不掩码客户端发往服务端的帧必须掩码。剩下 7 位是负载长度。两端源码要对帧结构完全一致才能正常工作。下面是帧头解析时每一部分对应的含义位段长度含义FIN1 bit1 表示最后一片分片RSV1-33 bit扩展协商用无扩展必须为 0opcode4 bit1 文本2 二进制8 关闭9 Ping10 PongMASK1 bit客户端发服务端必须为 1Payload len7 bit0-125 直接是长度126 表示后面 2 字节是长度127 表示后面 8 字节是长度Masking-key4 字节MASK1 时存在用于解掩码Payload data变长负载数据面试里常考的边界是 126 和 127 的判定负载长度在 0 到 125 之间时直接用一个字节表示。如果是 126真正的长度在紧接着的 2 字节无符号 16 位整数里。如果是 127在接下来 8 字节无符号 64 位整数里。VB6 处理这组数字时要小心字节序网络字节序是大端收到 2 字节长度时要用(b2 * 256) b3这种方式还原不能直接把两个字节转字符串再拼接。4.2 粘包与缓冲缓冲区累积法的实现服务端和客户端都面对同一个问题一次DataArrival里可能包含多帧也可能只包含半帧。处理粘包的标准做法是单连接单缓冲区数据到达先拼入缓冲区然后在一个循环里反复尝试解帧直到缓冲区剩余长度不够一个完整的帧头。VB6 里的循环解帧实现Private Sub ParseFrameData() Dim tmp As String tmp gRecvBuf While Len(tmp) 2 Dim b0 As Byte, b1 As Byte b0 Asc(Mid$(tmp, 1, 1)) b1 Asc(Mid$(tmp, 2, 1)) Dim fin As Boolean Dim opcode As Byte Dim masked As Boolean Dim payloadLen As Long fin (b0 And H80) 0 opcode b0 And HF masked (b1 And H80) 0 payloadLen b1 And H7F Dim headerLen As Long headerLen 2 If payloadLen 126 Then If Len(tmp) 4 Then Exit While payloadLen (Asc(Mid$(tmp, 3, 1)) * 256) Asc(Mid$(tmp, 4, 1)) headerLen 4 ElseIf payloadLen 127 Then If Len(tmp) 10 Then Exit While 这里只取低 4 字节超长负载在 VB6 里一般用不到 payloadLen (Asc(Mid$(tmp, 7, 1)) * 16777216) _ (Asc(Mid$(tmp, 8, 1)) * 65536) _ (Asc(Mid$(tmp, 9, 1)) * 256) Asc(Mid$(tmp, 10, 1)) headerLen 10 End If Dim maskKey As String If masked Then If Len(tmp) headerLen 4 Then Exit While maskKey Mid$(tmp, headerLen 1, 4) headerLen headerLen 4 End If If Len(tmp) headerLen payloadLen Then Exit While Dim payload As String payload Mid$(tmp, headerLen 1, payloadLen) If masked Then payload ApplyMask(payload, maskKey) 处理完整帧opcode 和 FIN 一起传过去 Call DispatchFrame(fin, opcode, payload) 从缓冲区中移除已处理的字节 tmp Mid$(tmp, headerLen payloadLen 1) Wend gRecvBuf tmp End Sub这段代码的标准流程是先读帧头再判断长度是否够不够就跳出循环等下一包。为什么Exit While之前不清空缓冲区因为半帧数据必须保留下次DataArrival到达时和新数据拼接否则消息就残了。工作现场最常见的问题是只判断缓冲区长度大于帧头就开工少算了掩码 key 和扩展长度字段的长度导致帧解析偶发失败。上面用headerLen累加的方式能把这些边界问题全部屏蔽掉。DispatchFrame里对 opcode 做分发1 是文本消息2 是二进制8 是关闭帧9 是 Ping此时要回 Pong10 是 Pong 响应。对关闭帧正确处理是把收到的 payload 原样带回发一个新的关闭帧给对方然后再断开连接。4.3 掩码处理服务端不收掩码客户端必须加掩码第 4.1 节专门强调过服务端到客户端不掩码客户端到服务端必须掩码。这意味着服务端解析客户端帧时masked一定是 1maskKey必须被读取客户端解析服务端帧时masked一定是 0。如果服务端发现一个masked 0的帧来自客户端按协议要求必须关闭连接这就是很多嵌入式自定义实现用字符串拼接发消息直接断线的原因。掩码算法很简单把 payload 按字节和 maskKey 的 4 个字节做异或第一个 payload 字节异或 maskKey[0]第二个异或 maskKey[1]第五个又异或 maskKey[0]。VB6 的字符串转字节数组成本较高如果只处理文本消息可以对每个字符用Asc取值再异或。性能瓶颈通常在频繁的字符串复制建议把接收缓冲区改成字节数组避免中文乱码。第 2 章的代码里用了vbString取数据这要求数据到达时 WinSock 内部做了转换。提示通过vbString收到中文字符会受本地代码页影响出现乱码。生产环境建议改成GetData data, vbArray vbByte把缓冲区整体当字节数组处理再做 UTF-8 解码。客户端和服务端源码要同时改否则只有一边改会直接收到乱码。5. 断线重连与心跳把实例程序改成能上生产的三个细节5.1 服务端定时 Ping客户端回 Pong生产环境最重要的机制是探测死连接。网络断开时TCP 层可能不会立即通知应用服务端定期发 Ping 帧客户端收到后必须回 Pong两端才能确认链路是活的。VB6 里在服务端用一个 Timer 控件每 30 秒对每个连接发送一个 Ping 帧Private Sub Timer1_Timer() Dim i As Integer For i 1 To Winsock1.UBound If Winsock1(i).State sckConnected Then Dim frame(1) As Byte frame(0) H89 FIN1, opcode9 Ping frame(1) H0 负载长度 0 Winsock1(i).SendData frame End If Next i End Sub客户端收到 Ping 帧后把第一字节改成H8A即 FIN1 且 opcode10 Pong原样发回去。这个帧没有负载两字节就够。客户端同时要记一个「最后收到数据的时间」超过两个心跳周期没收到任何帧就主动重连。5.2 断线重连指数退避与状态重置客户端断线后常见做法是加一个状态标志和一个重试计数器按 1 秒、2 秒、4 秒的指数退避重连。VB6 里用 Timer 实现重连控制每次失败重试次数加一达到上限后停止并弹出提示。重连前必须重置gHandshakeDone False清空接收缓冲区否则下一次握手成功后会混入上一次会话的旧数据帧解析会错位。这个重置动作最容易忘忘了之后的现象是重连偶尔成功、偶尔收发乱码且只在长时间运行后出现。重连间隔建议加上随机抖动避免多个客户端同时断线后在同一秒发起重连把服务端打崩。抖动量取当前间隔的百分之二十左右即可。5.3 正常关闭发送关闭帧再断开服务端主动关闭时要发送 opcode8 的关闭帧带上状态码比如 1000 表示正常关闭1001 表示服务端即将重启1008 表示策略拒绝然后等客户端回关闭帧或超时才真正Close。工作现场看到 1006 的另一个高发场景是定位不了到底是哪边主动断的。建议双端各自打印日志服务端记状态码和触发位置客户端记收到关闭帧的时间两边日志一对比谁没发关闭帧直接暴露。5.4 验证链路写一个 Echo 自测脚本验证整条链路是否可上生产最终手段是写一个小的自测客户端在服务端窗口里打印每一次收发的字节。把日志里的帧头二进制打印出来对照第 4.1 节的位结构逐位检查。文本消息、中文消息、超过 125 字节的消息、超过 64K 的消息各发一次确认 126 和 127 两种长度扩展都覆盖到。最后拔掉网线模拟断网看心跳能否在 30 秒内发现死连接并触发重连。把日志里的帧头二进制打印出来对比上面的位结构多数问题会在十分钟内浮出来。本文还有配套的精品资源点击获取
返回列表