010. HttpServer 生产可用性修复
概述
用户反馈 HTTP 服务器运行数天后出现崩溃、无响应等故障。经四轮迭代修复,共发现 3 个致命级 + 5 个高危级 + 7 个中危级 = 15 个 BUG,已修复 12 个(BUG-1~10、BUG-15),缓解 2 个(BUG-11、BUG-12),未修复 1 个(BUG-13),功能缺失 1 个(BUG-14)。
用户的核心诉求是"HTTP 连一次内存涨一点点,永远不会下降,最终程序崩溃",四轮修复围绕该目标依次解决:致命崩溃→内存泄漏→空 Session 累积→As New 隐式引用→SSE 资源残留。
问题总览
| 编号 | 级别 | 问题 | 根因 | 影响 | 状态 |
|---|---|---|---|---|---|
| BUG-1 | 致命 | 连接池索引漂移 | Collection.Remove 导致后续索引前移 | 数据发错客户端、GPF 崩溃 | 已修复 |
| BUG-2 | 致命 | 僵尸连接无限累积 | 无空闲超时 + 循环引用链 | 内存持续增长 → OOM | 已修复 |
| BUG-3 | 致命 | 请求体无大小限制 | 无 Content-Length 上限 | 恶意请求撑爆内存 | 已修复 |
| BUG-4 | 高危 | recvBuffer 永不缩容 | 大请求后数组不释放 | 长连接隐性内存泄漏 | 已修复 |
| BUG-5 | 高危 | 循环引用链 | cHttpServer↔cClientCallback↔Response 互引 | COM 对象永不释放 | 已修复 |
| BUG-6 | 高危 | FindCRLFCRLF 返回 0 歧义 | 位置 0 与"未找到"混淆 | 特定请求永远无法解析 | 已修复 |
| BUG-7 | 高危 | 无最大连接数限制 | 服务端无防护 | 资源耗尽 | 已修复 |
| BUG-8 | 高危 | SendData 无错误处理 | 断开后发送抛异常 | 进程崩溃 | 已修复 |
| BUG-9 | 中危 | SSE.Entry 失败不清理连接池 | CloseSck 触发但 m_cConnPool 残留 | 连接池泄漏 | 已修复 |
| BUG-10 | 中危 | SSE.Data 共享可变状态 | 重入时数据错乱 | 数据丢失 | 已修复 |
| BUG-11 | 中危 | Session 清理概率 1% 过低 | 低流量时 Session 堆积 | 内存泄漏 | 已缓解(cTimer) |
| BUG-12 | 中危 | StopMe 销毁共享对象 | Router/SSE/Database 被设为 Nothing | 重启后配置丢失 | 已缓解 |
| BUG-13 | 中危 | 不支持 chunked 传输编码 | 未实现分块传输解析 | 特定客户端挂起 | 未修复 |
| BUG-14 | 中危 | 静态文件缓存不更新 | WebRoot 只在启动时扫描 | 运行时添加文件不可见 | 未修复 |
| BUG-15 | 中危 | SSE CloseClient 不关 Socket | 仅清理字典但不关 Socket | 连接残留 | 已修复 |
Round 1:致命/高危 BUG 修复
BUG-1: 连接池 Collection 索引漂移
症状: 运行一段时间后数据发错客户端,或访问已释放对象导致 GPF 崩溃。
根因: m_cConnPool 使用 Collection 存储连接,每个 cClientCallback.Index 记录自身在 Collection 中的位置。当某个连接断开执行 m_cConnPool.Remove Index 时,后续所有项的索引自动前移,但 oCallback.Index 仍是旧值。
连接池: [A(1), B(2), C(3), D(4)]
B 断开 → Remove 2
连接池: [A(1), C(2), D(3)] ← C 和 D 的索引变了
但 C.Index 仍为 2,D.Index 仍为 3
如果 A 断开 Remove 1,C 从 2→1,D 从 3→2
但 C.Index=2, D.Index=3 → m_cConnPool(3) 越界或取到错误对象!修复: Collection → Scripting.Dictionary,以 hSocket 为 Key。
' 修复前
Private m_cConnPool As New Collection ' 顺序索引
oCallback.Index = m_cConnPool.Count
m_cConnPool.Add oCallback
m_cConnPool.Remove Index
' 修复后
Private m_cConnPool As Scripting.Dictionary ' hSocket 为 Key
m_cConnPool.Add hSocket, oCallback
m_cConnPool.Remove hSocketBUG-2: 僵尸连接无限累积
症状: HTTP 服务器运行几天后内存持续增长,最终 OOM 崩溃。
根因: 客户端异常断开不发送 TCP FIN,Socket_CloseSck 事件永远不触发。循环引用链使 COM 引用计数不归零:
cHttpServer ← m_cConnPool ← cClientCallback ← Parent ──→ cHttpServer
cClientCallback ← Context ← Response ← fClient ──→ cClientCallback修复:
- 新增
LastActivity字段,每次数据到达时更新 - 新增
IdleTimeoutSeconds属性(默认 120 秒) - 新增
CleanupIdleConnections()方法,遍历连接池释放超时连接 - 集成
cTimer内置定时器,默认每 30 秒自动清理
BUG-3: 请求体无大小限制
根因: 服务器无脑接收数据,AppendToBuffer 不断 ReDim Preserve 扩大缓冲区。
修复:
- 新增
MaxRequestSize属性(默认 10MB) OnDataArrival中检查缓冲区 + 新数据是否超限ContainsCompleteRequest中检查 Content-Length 是否超限- 新增
State413响应
BUG-4: recvBuffer 永不缩容
根因: 处理完大请求后 recvBuffer() 数组保持已扩展的大小。
修复: RemoveFromBuffer 中,缓冲区清空时 Erase recvBuffer;容量超 64KB 且使用率低于 1/4 时 ReDim Preserve 缩容。
BUG-5: 循环引用链
修复: Release() 中彻底释放所有引用、Erase recvBuffer、重置 hSocket = 0。Class_Terminate 调用 StopMe。
BUG-6: FindCRLFCRLF 返回 0 歧义
根因: CRLFCRLF 出现在位置 0 时返回 0,未找到也返回 0。
修复: 未找到时返回 -1,ContainsCompleteRequest 和 ExtractOneRequest 同步更新。
BUG-7: 无最大连接数限制
修复: 新增 MaxConnections 属性(默认 1000),超限 Disconnect = True。
BUG-8: SendData 无错误处理
修复: SendBodyByte 和 SendHeader 加 On Error Resume Next,新增 m_HasSent 防止重复发送。
Round 2:内存泄漏显式清理
现象
Round 1 修复后,压测发现内存仍以 ~10MB/s 增长:10 连接、900 QPS,339 秒后内存涨到 1.8GB。
根因
ProcessHttpRequest 每次请求创建约 18+ COM 对象,请求完毕后仅靠 COM 引用计数等待回收,高并发下来不及:
- Response.fClient → oCallback 循环引用:
oCallback → Context → Response → fClient → oCallback - Request 内部大对象未及时清理:5 个 Dictionary + PathInfoList + 字节数组
- VB6 引用计数高频下回收滞后:900 QPS × 18 对象 = 16200 COM 对象/秒
修复
| 文件 | 修改 |
|---|---|
cHttpServer.cls | ProcessHttpRequest 所有退出点(State413、Options、正常出口、EH)前调用 Context.ReleaseRequest + Request.ClearRequest |
cHttpServerContext.cls | 新增 ReleaseRequest():断开 Response.fClient 循环引用、清理 Request/Response/Cookies/Session,保留 SSE/UserData/TimeUse |
cHttpServerRequest.cls | 新增 ClearRequest():RemoveAll 所有 Dictionary、Erase 字节数组;Class_Terminate 保险清理 |
cHttpServerResponse.cls | 新增 Class_Terminate:Set Client = Nothing、Header.RemoveAll、释放 JsonInst |
清理链路
ProcessHttpRequest 请求处理完毕
│
├─ Context.ReleaseRequest()
│ ├─ Response.fClient = Nothing ← 断开循环引用
│ ├─ Request.ClearRequest() ← 清空大对象
│ ├─ Set Request = Nothing
│ ├─ Set Response = Nothing
│ ├─ Cookies.Clear + Set Cookies = Nothing
│ ├─ Set Session = Nothing
│ └─ UserData.RemoveAll
│
└─ Set response = Nothing: Set Request = NothingRound 3:Session 累积 + 类级 As New 全量修复
现象
Round 2 修复后内存泄漏约 5-10MB/s。
根因分析
根因 1: Session 自动生成 sessionID 导致空 Session 大量累积
cHttpServerSession.sessionID Property Get 在 pvSessionID 为空时自动生成 GUID。ProcessHttpRequest 用 If Session.sessionID <> "" Then 判断是否存储——每个无 Cookie 的请求都会触发生成、条件恒 True、空 Session 永久存入 m_Sessions。
900 QPS × 60 秒 × 20 分钟 = 1,080,000 个空 Session,约 1-2GB。
根因 2: 类级 As New 隐式创建阻止显式释放
VB6 的 Dim X As New ClassName:Set X = Nothing 后下次访问自动重建,对象永远无法真正销毁。影响范围:
| 文件 | As New 字段数 |
|---|---|
cHttpServer.cls | 9 |
cSSE.cls | 7 |
cHttpServerRouter.cls | 8 |
cHttpServerSvr.cls | 4 |
cHttpServerSession.cls | 1 |
cHttpServerRouteBefore.cls | 1 |
cHttpServerRouterAfter.cls | 1 |
修复
核心修复:Session 累积
| 文件 | 修改 |
|---|---|
cHttpServerSession.cls | sessionID Property Get 不再自动生成 GUID,仅返回 pvSessionID;新增 HasID 属性;Data 改延迟创建(EnsureData);新增 Release();新增 Class_Terminate |
cHttpServer.cls | If Session.HasID Then 替代 If Session.sessionID <> "" Then;显式清理本地变量 |
类级 As New 全量转换
7 个文件全部改为 As + Class_Initialize 显式 Set = New + Class_Terminate 显式 Set = Nothing。
同时在 cSSE.cls 中修复 BUG-9(Entry 失败时清理已设置的 SSE 引用)。
清理链路(更新)
ProcessHttpRequest 请求处理完毕
│
├─ Context.ReleaseRequest()
│ ├─ Response.fClient = Nothing
│ ├─ Request.ClearRequest()
│ ├─ Set Request = Nothing
│ ├─ Set Response = Nothing
│ ├─ Cookies.Clear + Set Cookies = Nothing
│ ├─ Set Session = Nothing
│ └─ UserData.RemoveAll
│
├─ Set Session = Nothing ← 不再等 Sub 退出
├─ Set Cookies = Nothing
└─ Set response = Nothing: Set Request = NothingSession 新逻辑:只有 Session.HasID = True 时才会存入 m_Sessions、写入 Cookie、持久化。
Round 4:方法级 As New + SSE BUG-15/BUG-10
方法级 As New 局部变量全量转换
上一轮修复了类级字段,方法体内仍有 11 处 Dim X As New。As New 的自动重建机制在异常路径下可能阻止回收。
| 文件 | 修改数量 | 具体变更 |
|---|---|---|
cHttpServer.cls | 4 处 | CI, keysToClose, keysToRemove, sess → 全部显式 |
cHttpServerSession.cls | 4 处 | Serialize/Deserialize 中 Json, SessionData, DataCopy → 显式 |
cHttpServerResponse.cls | 1 处 | Json 方法中 Dic → 显式 |
cHttpServerCookies.cls | 1 处 | Cookie Property Get 中 CK → 显式 |
cHttpServerRouter.cls | 1 处 | Add 方法中 item → 显式 |
BUG-15: SSE CloseClient 不关闭 Socket
原实现问题:
- 通过
hSocket参数调用时(最常见方式),仅检查是否存在然后 GoTo 退出——不做任何清理 - 通过
ToUser().CloseClient调用时(GoSub Start 路径),清理字典但不关 Socket
后果:所有主动关闭的 SSE 客户端,Socket 永远不关闭,cClientCallback 永远不释放。
修复:重构为统一清理路径:
- 触发 OnClose 用户事件
- 清除 SSE 上下文引用
- 从 Clients 字典移除(防重入)
- 清理用户/分组映射
Client.Socket.CloseSck关闭 Socket,触发 cClientCallback 完整清理流程
BUG-10: SSE.Data 共享可变状态重入
原问题:Send 方法使用类级共享 Data 字典,SendData 可能触发 Winsock 消息泵导致重入。
修复:Send 编码前将 Data 快照到局部 SendData Dictionary,然后立即 Data.RemoveAll,编码发送使用安全副本。
修改文件汇总(全部四轮)
| 文件 | R1 | R2 | R3 | R4 | 变更概要 |
|---|---|---|---|---|---|
src\Socket\cClientCallback.cls | ● | Index→hSocket;LastActivity;Release 彻底清理;recvBuffer 缩容 | |||
src\HttpServer\cHttpServer.cls | ● | ● | ● | ● | Collection→Dictionary;6 个安全属性;FindCRLFCRLF -1;cTimer 集成;ReleaseRequest/ClearRequest;Session.HasID;13 处 As New→显式 |
src\HttpServer\cHttpServerResponse.cls | ● | ● | ● | SendData 错误保护;m_HasSent;State413;Class_Terminate;1 处 As New→显式 | |
src\HttpServer\cHttpServerContext.cls | ● | ● | ReleaseRequest();清理循环引用 | ||
src\HttpServer\cHttpServerRequest.cls | ● | ● | ClearRequest();Class_Terminate | ||
src\HttpServer\cHttpServerCookies.cls | ● | ● | 1 处 As New→显式 | ||
src\HttpServer\cHttpServerSession.cls | ● | ● | sessionID 不自动生成;HasID;EnsureData 延迟创建;Release();Class_Terminate;5 处 As New→显式 | ||
src\HttpServer\cHttpServerSvr.cls | ● | 4 处 As New→显式;Class_Initialize/Terminate | |||
src\HttpServer\cHttpServerRouter.cls | ● | ● | 8 处类级 + 1 处方法级 As New→显式;Class_Initialize/Terminate | ||
src\HttpServer\cHttpServerRouteBefore.cls | ● | 1 处 As New→显式;Class_Initialize/Terminate | |||
src\HttpServer\cHttpServerRouterAfter.cls | ● | 1 处 As New→显式;Class_Initialize/Terminate | |||
src\SSE\cSSE.cls | ● | ● | 7 处类级 As New→显式;BUG-9 Entry 失败清理;BUG-15 CloseClient 重构+关 Socket;BUG-10 Send 快照防重入;Class_Initialize/Terminate |
合计修改 12 个 .cls 文件
新增公共 API 汇总(全部四轮)
cHttpServer 属性/方法
| 属性/方法 | 类型 | 默认值 | 轮次 | 说明 |
|---|---|---|---|---|
MaxConnections | Long | 1000 | R1 | 最大并发连接数 |
MaxRequestSize | Long | 10485760 (10MB) | R1 | 最大请求体字节数,超限断开返回 413 |
IdleTimeoutSeconds | Long | 120 | R1 | 空闲连接超时秒数,超时自动关闭 |
ConnectionCount | Property Get | - | R1 | 当前活跃连接数(只读) |
CleanupIdleConnections() | Sub | - | R1 | 手动清理空闲连接 |
CleanupExpiredSessions() | Sub | - | R1 | 手动清理过期 Session |
CleanupTimerInterval | Property Get/Let | 30000 (30秒) | R1 | 内置清理定时器间隔(毫秒) |
State413() | Sub | - | R1 | 413 Payload Too Large 响应 |
cHttpServerSession 属性/方法
| 属性/方法 | 类型 | 轮次 | 说明 |
|---|---|---|---|
HasID | Property Get (Boolean) | R3 | 判断是否已有 sessionID(即有数据被写入) |
Release() | Friend Sub | R3 | 显式释放 Data Dictionary,清空 sessionID |
cHttpServerContext 方法
| 方法 | 轮次 | 说明 |
|---|---|---|
ReleaseRequest() | R2 | 断开 Response.fClient 循环引用,清理 Request/Response/Cookies/Session |
cHttpServerRequest 方法
| 方法 | 轮次 | 说明 |
|---|---|---|
ClearRequest() | R2 | RemoveAll 所有 Dictionary、Erase 字节数组 |
内置定时清理机制
服务器启动后自动创建 cTimer 定时器(依赖 ToolsTimer.bas 模块),默认每 30 秒触发一次:
CleanupIdleConnections— 清理超过IdleTimeoutSeconds的僵尸连接CleanupExpiredSessions— 清理过期的 Session
CleanupTimerInterval 可调整间隔,运行中修改自动重启生效。停止时定时器自动销毁。
As New 转换完整清单
此为四轮修复中规模最大的系统性改动,消除 VB6 As New 隐式自动重建导致对象永远无法真正释放的问题。
类级字段(Round 3)
| 文件 | 处数 |
|---|---|
cHttpServer.cls | 9 |
cSSE.cls | 7 |
cHttpServerRouter.cls | 8 |
cHttpServerSvr.cls | 4 |
cHttpServerSession.cls | 1 |
cHttpServerRouteBefore.cls | 1 |
cHttpServerRouterAfter.cls | 1 |
| 合计 | 31 |
方法级局部变量(Round 4)
| 文件 | 处数 |
|---|---|
cHttpServer.cls | 4 |
cHttpServerSession.cls | 4 |
cHttpServerResponse.cls | 1 |
cHttpServerCookies.cls | 1 |
cHttpServerRouter.cls | 1 |
| 合计 | 11 |
转换模式
' 修复前
Private m_Sessions As New Scripting.Dictionary
Dim Dic As New Scripting.Dictionary
' 修复后(类级)
Private m_Sessions As Scripting.Dictionary ' Class_Initialize 中 Set = New
' Class_Terminate 中 Set = Nothing
' 修复后(方法级)
Dim Dic As Scripting.Dictionary
Set Dic = New Scripting.Dictionary验证结果
四轮修复后,所有 HttpServer + SSE 共 15 个 .cls 文件中无代码级 As New 残留(仅注释中保留说明)。
性能影响评估
| 修复项 | 性能影响 | 轮次 |
|---|---|---|
| Collection → Dictionary | 查找 O(1),比 Collection.Item(index) 更快 | R1 |
| CleanupIdleConnections | 内置定时器每 30s + 手动,遍历 O(N) | R1 |
| 缓冲区缩容 | 仅容量 > 64KB 且使用率 < 25% 时触发 | R1 |
| MaxRequestSize 检查 | 每次数据到达一次 Long 比较 O(1) | R1 |
| MaxConnections 检查 | Dictionary.Count 读取 O(1) | R1 |
| SendData 错误保护 | On Error Resume Next 仅异常时有开销 | R1 |
| ReleaseRequest/ClearRequest | 请求末尾显式释放,减少 COM 回收压力 | R2 |
| Session.HasID 判断 | 布尔属性读取,O(1) | R3 |
| SSE Send 快照 | 复制 Data 到局部 Dict,O(N)(N = Data 条目数,通常 < 10) | R4 |
整体性能影响可忽略不计。
遗留问题
| 编号 | 问题 | 状态 | 说明 |
|---|---|---|---|
| BUG-11 | Session 清理概率 1% | 已缓解 | cTimer 定时器每 30s 调用 CleanupExpiredSessions,不再依赖随机概率 |
| BUG-12 | StopMe 销毁共享对象 | 已缓解 | Round 1 修复 StopMe 不再销毁共享对象,但重启后状态恢复仍需注意 |
| BUG-13 | 不支持 chunked 传输编码 | 未修复 | 需实现分块传输解析器,影响面较小 |
| BUG-14 | 静态文件缓存不更新 | 未修复 | WebRoot 字典启动时扫描一次,运行时添加文件需重启 |
| - | cWinSock.cls Collection.Exists | 未修复 | m_cClients 声明为 Collection 但调用 .Exists(),运行时必然报错 |
升级指南
最低升级要求: 替换以下 12 个 .cls 文件
src\Socket\cClientCallback.cls
src\HttpServer\cHttpServer.cls
src\HttpServer\cHttpServerResponse.cls
src\HttpServer\cHttpServerContext.cls
src\HttpServer\cHttpServerRequest.cls
src\HttpServer\cHttpServerCookies.cls
src\HttpServer\cHttpServerSession.cls
src\HttpServer\cHttpServerSvr.cls
src\HttpServer\cHttpServerRouter.cls
src\HttpServer\cHttpServerRouteBefore.cls
src\HttpServer\cHttpServerRouterAfter.cls
src\SSE\cSSE.cls项目依赖: cTimer 集成需要 ToolsTimer.bas 模块已包含在项目中
无需修改的部分: 以下文件经审计无需修改
cHttpServerApp.cls— 空类cHttpServerRouteItem.cls— 纯简单类型cHttpServerCookieAttr.cls— 纯简单类型cHttpServerClientInfo.cls— 纯简单类型cSSEContext.cls— 已有 Class_Terminate,无 As New
配置建议:
' 启动前配置(推荐生产环境值)
Server.MaxConnections = 500 ' 根据内存调整
Server.MaxRequestSize = 5242880 ' 5MB
Server.IdleTimeoutSeconds = 60 ' 1 分钟
Server.CleanupTimerInterval = 30000 ' 30 秒(默认值,可不动)修复日期: 2026-06-13 修复版本: v1.0.0.423 影响版本: 所有使用 Collection 连接池的历史版本
验证结果
2026-06-13 用户压测验证通过:四轮修复后,内存泄漏问题已全部消除,HttpServer 可正式用于生产环境。

定时回收器很健康了,,没有请求的时候,,内存被回收到 3.5MB,将近一半

四轮修复总览
| 轮次 | 重点 | 修改文件数 | 修复 BUG 数 |
|---|---|---|---|
| Round 1 | 致命/高危 BUG 修复 | 6 | BUG-1~8 |
| Round 2 | 内存泄漏显式清理 | 4 | ReleaseRequest/ClearRequest |
| Round 3 | Session 累积 + 类级 As New | 7 | Session HasID + 31 处 As New + BUG-9 |
| Round 4 | 方法级 As New + SSE BUG-15/10 | 6 | 11 处 As New + BUG-15 + BUG-10 |
| 合计 | 12 文件 | BUG-1~10, 15(12 个 BUG)+ 42 处 As New |