分包协议与心跳机制
📦💓 解决 TCP 粘包/分包问题的内置协议,以及保持连接活跃的智能心跳
📖 目录
概述
TCP 是流式协议,没有消息边界——发送方连续发送的数据,接收方可能分多次收到(分包),也可能一次收到多条消息拼接的数据(粘包)。cWinsock 内置封包协议 cPacketProtocol 和智能心跳 cHeartbeat,自动解决这些问题:
- 封包协议:三种内置协议,自动封包/解包,保证每次收到的都是完整消息
- 心跳机制:内嵌定时器自动驱动,服务端超时检测 + 客户端智能保活
TCP 粘包与分包问题
什么是粘包/分包?
| 现象 | 术语 | 说明 |
|---|---|---|
| 一次 Send 的数据,分多次 Receive 到达 | 分包 | 发了 1000 字节,先收到 300,再收到 700 |
| 多次 Send 的数据,一次 Receive 全到 | 粘包 | 连续发了 3 条消息,一次收到拼接在一起的数据 |
| 上面两种混合出现 | 最常见 | 收到的数据既不完整又混着下一条的开头 |
现象图解
应用层发送:[Msg1][Msg2][Msg3]
↓ TCP 流式传输(无边界)
接收端可能收到:
情况1(分包): [Msg1前半] [Msg1后半+Msg2前半] [Msg2后半+Msg3]
情况2(粘包): [Msg1+Msg2] [Msg3]
情况3(混合): [Msg1前半] [Msg1后半+Msg2] [Msg3前半] [Msg3后半]
理想情况(少见):[Msg1] [Msg2] [Msg3]cPacketProtocol 的解决思路
在数据中定义明确的边界,将无界的字节流还原为有界的消息:
原始 TCP 字节流(无边界):
[Msg1前半][Msg1后半+Msg2前半][Msg2后半]
↓ cPacketProtocol.Decode()
完整消息:
[Msg1 完整] → 触发 MessageArrival
[Msg2 完整] → 触发 MessageArrival封包协议
三种协议对比
| 协议类型 | 原理 | 分包处理 | 粘包处理 | 优缺点 |
|---|---|---|---|---|
ppLengthHeader | 头部指明消息体长度 | 长度不够则缓存,等数据到齐再提取 | 长度够了就切一条,剩余继续解析 | 推荐。不依赖数据内容,不限消息长度 |
ppDelimiter | 分隔符标记消息结尾 | 未找到分隔符则缓存 | 找到分隔符就切一条,剩余继续找 | 简单,但分隔符不能出现在消息体中 |
ppFixedLength | 每条消息固定长度 | 不足定长则缓存 | 凑够定长就切一条 | 仅适用于定长消息场景 |
推荐方案:ppLengthHeader
4 字节小端长度头协议是最通用的选择:
- 不依赖数据内容中出现特殊字符(分隔符协议的硬伤)
- 不限制单条消息长度(定长协议的硬伤)
- 每条消息自带长度,接收端精确知道要读多少字节
ppDelimiter - 分隔符协议
使用指定的字符/字符串作为消息边界标记。
适用场景:文本协议、行式命令协议(如聊天、HTTP 头部)
' 配置
m_oServer.PacketProtocol = ppDelimiter
m_oServer.Delimiter = vbCrLf ' 换行分隔
' 发送自动追加分隔符
Client.SendData "Hello" ' 实际发送: "Hello" + vbCrLf
' 接收自动去除分隔符
Private Sub m_oServer_MessageArrival(Client As cWinsock, ByVal bytesTotal As Long)
Debug.Print "完整消息: " & Client.GetDataText() ' "Hello"
End Sub常用分隔符:
| 分隔符 | 常量 | 适用场景 |
|---|---|---|
\r\n | vbCrLf | 行式文本协议 |
\n | vbLf | Unix 风格行协议 |
\0 | vbNullChar | C 字符串风格 |
| 自定义字符串 | "<EOF>" | 自定义协议 |
注意:分隔符不能出现在消息体中,否则消息会被错误拆分。
ppFixedLength - 定长协议
每条消息固定长度,适用于已知长度的结构化数据。
适用场景:状态包、传感器数据、固定格式报文
' 配置
m_oServer.PacketProtocol = ppFixedLength
m_oServer.FixedLength = 256 ' 每条消息固定 256 字节
' 发送:不足 256 字节会补零,超过 256 字节会报错
Client.SendData myData注意:数据超长时会直接报错(不会静默截断)。
ppLengthHeader - 长度头协议(推荐)
在消息头部添加长度信息,最通用、最推荐的协议。
适用场景:二进制协议、变长消息、任何需要可靠传输的场景
' 配置
m_oServer.PacketProtocol = ppLengthHeader
m_oServer.HeaderBytes = 4 ' 4字节长度头(支持最大约4GB)
m_oServer.HeaderEndian = eeLittleEndian ' 小端序
' 发送自动加长度头
Client.SendData "Hello" ' 实际发送: [4字节长度=5] + "Hello"
' 接收自动剥离长度头
Private Sub m_oServer_MessageArrival(Client As cWinsock, ByVal bytesTotal As Long)
Dim baData() As Byte
baData = Client.GetDataByteArray() ' 100% 是一条完整消息
End SubHeaderBytes 选项:
| 值 | 类型 | 最大消息长度 | 说明 |
|---|---|---|---|
| 2 | Unsigned Integer | 65,535 字节(~64KB) | 小消息场景,节省带宽 |
| 4 | Unsigned Long | 2,147,483,647 字节(~2GB) | 通用场景,推荐 |
HeaderEndian 字节序:
| 值 | 说明 | 适用场景 |
|---|---|---|
eeLittleEndian | 小端序(默认) | x86/x64 平台内部通信 |
eeBigEndian | 大端序(网络字节序) | 与 Java/C 服务端通信 |
安全限制属性
防止恶意数据包耗尽内存,2026-06-09 新增:
MaxPacketSize
单包最大大小限制,防止恶意超大包声明耗尽内存。
Property Get MaxPacketSize() As Long
Property Let MaxPacketSize(ByVal Value As Long)- 默认值:1MB(1048576 字节)
- 作用:长度头协议解析时,如果声明的消息长度超过此值,直接报错丢弃
- 适用协议:
ppLengthHeader
' 调整最大包限制
m_oServer.MaxPacketSize = 524288 ' 512KB
' 新客户端自动继承此配置MaxBufferSize
缓冲区累积上限,防止大量不完整包慢慢吃内存。
Property Get MaxBufferSize() As Long
Property Let MaxBufferSize(ByVal Value As Long)- 默认值:4MB(4194304 字节)
- 作用:Decode 合并缓冲区前检查,超限报错
- 适用协议:所有协议
' 调整缓冲区上限
m_oServer.MaxBufferSize = 8388608 ' 8MB超限行为
| 超限类型 | 行为 |
|---|---|
单包超过 MaxPacketSize | 抛出明确错误信息,丢弃缓冲区 |
累积超过 MaxBufferSize | 抛出明确错误信息,丢弃缓冲区 |
| 数据超长(FixedLength) | 抛出错误(不静默截断) |
事件模型
协议模式与无协议模式的区别
| 模式 | 触发事件 | 说明 |
|---|---|---|
无协议(ppNone) | DataArrival | 原始字节流,可能不完整或粘连 |
| 有协议 | MessageArrival | 每次一定是完整的一条消息 |
关键规则:协议模式下只触发 MessageArrival,不触发 DataArrival,避免同一数据被两个事件重复读取。
MessageArrival 事件
Private Sub object_MessageArrival(Client As cWinsock, ByVal bytesTotal As Long)| 参数 | 类型 | 说明 |
|---|---|---|
Client | cWinsock | 接收消息的客户端对象 |
bytesTotal | Long | 完整消息的字节数 |
使用示例
' 设置长度头协议
m_oServer.PacketProtocol = ppLengthHeader
m_oServer.HeaderBytes = 4
m_oServer.HeaderEndian = eeLittleEndian
' 接收完整消息
Private Sub m_oServer_MessageArrival(Client As cWinsock, ByVal bytesTotal As Long)
' 此时缓冲区中已是完整消息,100% 完整
Dim sData As String
sData = Client.GetDataText()
Debug.Print "完整消息: " & sData
End Sub与 DataArrival 的区别
| 事件 | 触发时机 | 数据完整性 | 适用场景 |
|---|---|---|---|
DataArrival | 每次收到原始数据 | 可能是分片或粘包数据 | 无协议模式 |
MessageArrival | 协议解析出完整消息后 | 保证是一条完整消息 | 协议模式 |
智能心跳机制
cWinsock 内置 cHeartbeat 心跳管理器,内嵌 cTimer 自动驱动,无需外部定时器。
服务端:超时检测
服务端定期检查所有客户端的空闲时间,超时则自动断开僵尸连接。
' 配置
m_oServer.AutoHeartbeat = True
m_oServer.HeartbeatTimeout = 120 ' 2分钟无活动则超时
' 超时事件
Private Sub m_oServer_ClientTimeout(Client As cWinsock)
Debug.Print "客户端 " & Client.Tag & " 超时,已自动断开"
End Sub客户端:心跳保活
客户端定期发送心跳包,保持连接活跃。具有智能跳过机制——有数据收发时跳过心跳,节省带宽。
' 配置
m_oClient.AutoHeartbeat = True
m_oClient.HeartbeatInterval = 50 ' 50秒无活动则发心跳
' 心跳事件
Private Sub m_oClient_HeartbeatSent(Client As cWinsock)
Debug.Print "心跳已发送,空闲: " & Client.IdleSeconds & "秒"
End Sub自定义心跳包
' 默认心跳包为单字节 &H00,可自定义
Dim baHB(0 To 3) As Byte
baHB(0) = &H50 ' P
baHB(1) = &H49 ' I
baHB(2) = &H4E ' N
baHB(3) = &H47 ' G
m_oClient.HeartbeatData = baHB心跳与协议的一致性
心跳数据经过协议编码发送,不会污染协议状态机。即:
- 心跳包通过
SendData发送,会经过协议Encode - 接收端心跳数据经过协议
Decode - 心跳不会导致粘包/分包状态混乱
工作原理
- 心跳管理器内嵌
cTimer,每 10 秒触发一次 Tick - 服务端:检查所有客户端的
IdleSeconds,超时则触发ClientTimeout并自动断开 - 客户端:如果空闲超过
HeartbeatInterval则发送心跳包,有数据收发时智能跳过 - 每次收发数据自动重置
LastActivityTime
心跳相关属性
| 属性 | 类型 | 读写 | 说明 |
|---|---|---|---|
AutoHeartbeat | Boolean | 读写 | 启用/禁用自动心跳 |
HeartbeatTimeout | Long | 读写 | 服务端超时秒数(默认120) |
HeartbeatInterval | Long | 读写 | 客户端心跳间隔秒数(默认50) |
HeartbeatData | Byte() | 读写 | 心跳包内容(默认单字节0) |
IdleSeconds | Long | 只读 | 当前空闲秒数 |
UDP 协议支持
UDP 客户端同样支持分包协议。UDP 虽然是报文协议(天然有边界),但协议模式仍然可以用于:
- 自定义消息格式处理
- 与 TCP 端共享协议逻辑
- 利用安全限制属性
' UDP 服务端设置协议
m_oUdp.PacketProtocol = ppLengthHeader
m_oUdp.HeaderBytes = 4
m_oUdp.MaxPacketSize = 65536 ' UDP 单包通常不超过 64KB
' UDP 虚拟客户端自动继承协议配置
Private Sub m_oUdp_MessageArrival(Client As cWinsock, ByVal bytesTotal As Long)
Dim sData As String
sData = Client.GetDataText()
Debug.Print "UDP 完整消息: " & sData
End Sub完整示例
TCP 服务端 + 长度头协议 + 心跳
Private WithEvents m_oServer As cWinsock
Private Sub Form_Load()
Set m_oServer = New cWinsock
' 设置封包协议(推荐:长度头协议)
m_oServer.PacketProtocol = ppLengthHeader
m_oServer.HeaderBytes = 4
m_oServer.HeaderEndian = eeLittleEndian
m_oServer.MaxPacketSize = 1048576 ' 1MB
m_oServer.MaxBufferSize = 4194304 ' 4MB
' 设置心跳
m_oServer.AutoHeartbeat = True
m_oServer.HeartbeatTimeout = 120 ' 2分钟超时
' 启动服务器
m_oServer.Listen 8080
Debug.Print "服务器已启动"
End Sub
Private Sub m_oServer_ConnectionRequest(Client As cWinsock, ByRef DisConnect As Boolean)
Debug.Print "新客户端: " & Client.RemoteHostIP & ":" & Client.RemotePort
' 新客户端自动继承协议和心跳配置
End Sub
Private Sub m_oServer_MessageArrival(Client As cWinsock, ByVal bytesTotal As Long)
' 100% 是完整消息
Dim sData As String
sData = Client.GetDataText()
Debug.Print "[" & Client.Tag & "] " & sData
' 回显
Client.SendData "Echo: " & sData
End Sub
Private Sub m_oServer_ClientTimeout(Client As cWinsock)
Debug.Print "客户端超时: " & Client.Tag
End Sub
Private Sub m_oServer_CloseEvent(Client As cWinsock)
Debug.Print "客户端断开: " & Client.Tag
End Sub
Private Sub Form_Unload(Cancel As Integer)
On Error Resume Next
m_oServer.Close_
End SubTCP 客户端 + 长度头协议 + 心跳
Private WithEvents m_oClient As cWinsock
Private Sub Form_Load()
Set m_oClient = New cWinsock
' 设置封包协议(必须与服务端一致)
m_oClient.PacketProtocol = ppLengthHeader
m_oClient.HeaderBytes = 4
m_oClient.HeaderEndian = eeLittleEndian
' 设置心跳
m_oClient.AutoHeartbeat = True
m_oClient.HeartbeatInterval = 50 ' 50秒间隔
' 连接
m_oClient.Connect "127.0.0.1", 8080
End Sub
Private Sub m_oClient_Connect(Client As cWinsock)
Debug.Print "已连接"
Client.SendData "Hello, Server!"
End Sub
Private Sub m_oClient_MessageArrival(Client As cWinsock, ByVal bytesTotal As Long)
Dim sData As String
sData = Client.GetDataText()
Debug.Print "收到: " & sData
End Sub
Private Sub m_oClient_HeartbeatSent(Client As cWinsock)
Debug.Print "心跳已发送"
End Sub
Private Sub Form_Unload(Cancel As Integer)
On Error Resume Next
m_oClient.Close_
End Sub聊天服务端 + 分隔符协议
Private WithEvents m_oServer As cWinsock
Private Sub Form_Load()
Set m_oServer = New cWinsock
' 使用换行符作为消息分隔符
m_oServer.PacketProtocol = ppDelimiter
m_oServer.Delimiter = vbCrLf
' 心跳保活
m_oServer.AutoHeartbeat = True
m_oServer.HeartbeatTimeout = 180
m_oServer.Listen 9090
End Sub
Private Sub m_oServer_MessageArrival(Client As cWinsock, ByVal bytesTotal As Long)
Dim sMsg As String
sMsg = Client.GetDataText()
' 广播给所有客户端
Dim oClient As cWinsock
For Each oClient In m_oServer.Clients
If Not oClient Is Client Then
oClient.SendData "[" & Client.Tag & "] " & sMsg
End If
Next
End Sub最佳实践
1. 选择合适的协议类型
| 协议类型 | 适用场景 | 优缺点 |
|---|---|---|
ppLengthHeader | 二进制协议、变长消息 | 最通用,推荐 |
ppDelimiter | 文本协议(聊天、命令行式) | 简单直观,但数据中不能包含分隔符 |
ppFixedLength | 固定格式消息(状态包、传感器数据) | 解析最快,但不灵活 |
2. 协议模式下使用 MessageArrival
' ✅ 正确:协议模式下使用 MessageArrival
Private Sub m_oServer_MessageArrival(Client As cWinsock, ByVal bytesTotal As Long)
Dim sData As String
sData = Client.GetDataText() ' 保证是完整消息
End Sub
' ❌ 错误:协议模式下使用 DataArrival
' 协议模式下 DataArrival 不会触发3. 长度头协议的字节序
' 与 C/Java 服务端通信时用大端序
m_oServer.HeaderEndian = eeBigEndian
' 纯 VB6 内部通信可用小端序(默认)
m_oServer.HeaderEndian = eeLittleEndian4. 配置安全限制
' 根据业务需求调整安全限制
m_oServer.MaxPacketSize = 524288 ' 512KB,单包最大
m_oServer.MaxBufferSize = 8388608 ' 8MB,缓冲区最大5. 服务器配置协议在 Listen 之前
' ✅ 正确:Listen 之前设置协议
m_oServer.PacketProtocol = ppLengthHeader
m_oServer.HeaderBytes = 4
m_oServer.Listen 8080
' 新客户端自动继承服务器配置,创建独立协议实例6. 心跳与协议配合
心跳包走协议编码,不会污染协议状态机,无需手动过滤心跳。
常见问题
❓ 为什么要用封包协议?
TCP 是流式协议,没有消息边界。不使用协议时,DataArrival 可能收到不完整或粘连的数据,需要手动拼接/拆分,容易出错。使用封包协议后,MessageArrival 每次触发的都是一条完整消息,开发者无需关心底层字节流的分合。
❓ MaxPacketSize 和 MaxBufferSize 有什么区别?
MaxPacketSize:单条消息的最大长度,针对长度头协议中声明的消息体长度MaxBufferSize:接收缓冲区的累积上限,防止大量不完整包慢慢吃内存
❓ 协议模式下还会触发 DataArrival 吗?
不会。协议模式下只触发 MessageArrival,避免同一数据被两个事件重复读取。无协议模式下仍触发 DataArrival。
❓ 心跳包会影响协议解析吗?
不会。心跳数据经过协议编码发送和接收,不会污染协议状态机。
❓ UDP 需要用封包协议吗?
UDP 天然有消息边界,不需要封包协议来解决粘包问题。但 UDP 客户端同样支持协议模式,可用于统一消息格式、利用安全限制属性等场景。
❓ 2 字节长度头能传多大消息?
2 字节头可表示 0~65535(最大约 64KB)。超过 65535 字节会报错。建议大数据使用 4 字节头。
相关文档
| 文档 | 描述 |
|---|---|
| 属性参考 | 协议和心跳相关的属性详细说明 |
| 事件详解 | MessageArrival、ClientTimeout 等事件 |
| 方法参考 | GetDataText、GetDataByteArray 等方法 |
| TCP编程 | TCP 客户端和服务器编程指南 |
| 最佳实践 | 常见场景的解决方案和性能优化建议 |
最后更新: 2026-06-09