Skip to content

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 → Dictionary,以 hSocket 为 Key。

vb
' 修复前
Private m_cConnPool As New Collection          ' 顺序索引
oCallback.Index = m_cConnPool.Count
m_cConnPool.Add oCallback
m_cConnPool.Remove Index

' 修复后
Private m_cConnPool As Dictionary    ' hSocket 为 Key
m_cConnPool.Add hSocket, oCallback
m_cConnPool.Remove hSocket

BUG-2: 僵尸连接无限累积 ​

症状: HTTP 服务器运行几天后内存持续增长,最终 OOM 崩溃。

根因: 客户端异常断开不发送 TCP FIN,Socket_CloseSck 事件永远不触发。循环引用链使 COM 引用计数不归零:

cHttpServer ← m_cConnPool ← cClientCallback ← Parent ──→ cHttpServer
cClientCallback ← Context ← Response ← fClient ──→ cClientCallback

修复:

  1. 新增 LastActivity 字段,每次数据到达时更新
  2. 新增 IdleTimeoutSeconds 属性(默认 120 秒)
  3. 新增 CleanupIdleConnections() 方法,遍历连接池释放超时连接
  4. 集成 cTimer 内置定时器,默认每 30 秒自动清理

BUG-3: 请求体无大小限制 ​

根因: 服务器无脑接收数据,AppendToBuffer 不断 ReDim Preserve 扩大缓冲区。

修复:

  1. 新增 MaxRequestSize 属性(默认 10MB)
  2. OnDataArrival 中检查缓冲区 + 新数据是否超限
  3. ContainsCompleteRequest 中检查 Content-Length 是否超限
  4. 新增 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 引用计数等待回收,高并发下来不及:

  1. Response.fClient → oCallback 循环引用:oCallback → Context → Response → fClient → oCallback
  2. Request 内部大对象未及时清理:5 个 Dictionary + PathInfoList + 字节数组
  3. VB6 引用计数高频下回收滞后:900 QPS × 18 对象 = 16200 COM 对象/秒

修复 ​

文件修改
cHttpServer.clsProcessHttpRequest 所有退出点(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 = Nothing

Round 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.cls9
cSSE.cls7
cHttpServerRouter.cls8
cHttpServerSvr.cls4
cHttpServerSession.cls1
cHttpServerRouteBefore.cls1
cHttpServerRouterAfter.cls1

修复 ​

核心修复:Session 累积 ​

文件修改
cHttpServerSession.clssessionID Property Get 不再自动生成 GUID,仅返回 pvSessionID;新增 HasID 属性;Data 改延迟创建(EnsureData);新增 Release();新增 Class_Terminate
cHttpServer.clsIf 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 = Nothing

Session 新逻辑:只有 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.cls4 处CI, keysToClose, keysToRemove, sess → 全部显式
cHttpServerSession.cls4 处Serialize/Deserialize 中 Json, SessionData, DataCopy → 显式
cHttpServerResponse.cls1 处Json 方法中 Dic → 显式
cHttpServerCookies.cls1 处Cookie Property Get 中 CK → 显式
cHttpServerRouter.cls1 处Add 方法中 item → 显式

BUG-15: SSE CloseClient 不关闭 Socket ​

原实现问题:

  • 通过 hSocket 参数调用时(最常见方式),仅检查是否存在然后 GoTo 退出——不做任何清理
  • 通过 ToUser().CloseClient 调用时(GoSub Start 路径),清理字典但不关 Socket

后果:所有主动关闭的 SSE 客户端,Socket 永远不关闭,cClientCallback 永远不释放。

修复:重构为统一清理路径:

  1. 触发 OnClose 用户事件
  2. 清除 SSE 上下文引用
  3. 从 Clients 字典移除(防重入)
  4. 清理用户/分组映射
  5. Client.Socket.CloseSck 关闭 Socket,触发 cClientCallback 完整清理流程

BUG-10: SSE.Data 共享可变状态重入 ​

原问题:Send 方法使用类级共享 Data 字典,SendData 可能触发 Winsock 消息泵导致重入。

修复:Send 编码前将 Data 快照到局部 SendData Dictionary,然后立即 Data.RemoveAll,编码发送使用安全副本。


修改文件汇总(全部四轮) ​

文件R1R2R3R4变更概要
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 属性/方法 ​

属性/方法类型默认值轮次说明
MaxConnectionsLong1000R1最大并发连接数
MaxRequestSizeLong10485760 (10MB)R1最大请求体字节数,超限断开返回 413
IdleTimeoutSecondsLong120R1空闲连接超时秒数,超时自动关闭
ConnectionCountProperty Get-R1当前活跃连接数(只读)
CleanupIdleConnections()Sub-R1手动清理空闲连接
CleanupExpiredSessions()Sub-R1手动清理过期 Session
CleanupTimerIntervalProperty Get/Let30000 (30秒)R1内置清理定时器间隔(毫秒)
State413()Sub-R1413 Payload Too Large 响应

cHttpServerSession 属性/方法 ​

属性/方法类型轮次说明
HasIDProperty Get (Boolean)R3判断是否已有 sessionID(即有数据被写入)
Release()Friend SubR3显式释放 Data Dictionary,清空 sessionID

cHttpServerContext 方法 ​

方法轮次说明
ReleaseRequest()R2断开 Response.fClient 循环引用,清理 Request/Response/Cookies/Session

cHttpServerRequest 方法 ​

方法轮次说明
ClearRequest()R2RemoveAll 所有 Dictionary、Erase 字节数组

内置定时清理机制 ​

服务器启动后自动创建 cTimer 定时器(依赖 ToolsTimer.bas 模块),默认每 30 秒触发一次:

  1. CleanupIdleConnections — 清理超过 IdleTimeoutSeconds 的僵尸连接
  2. CleanupExpiredSessions — 清理过期的 Session

CleanupTimerInterval 可调整间隔,运行中修改自动重启生效。停止时定时器自动销毁。


As New 转换完整清单 ​

此为四轮修复中规模最大的系统性改动,消除 VB6 As New 隐式自动重建导致对象永远无法真正释放的问题。

类级字段(Round 3) ​

文件处数
cHttpServer.cls9
cSSE.cls7
cHttpServerRouter.cls8
cHttpServerSvr.cls4
cHttpServerSession.cls1
cHttpServerRouteBefore.cls1
cHttpServerRouterAfter.cls1
合计31

方法级局部变量(Round 4) ​

文件处数
cHttpServer.cls4
cHttpServerSession.cls4
cHttpServerResponse.cls1
cHttpServerCookies.cls1
cHttpServerRouter.cls1
合计11

转换模式 ​

vb
' 修复前
Private m_Sessions As New Dictionary
Dim Dic As New Dictionary

' 修复后(类级)
Private m_Sessions As Dictionary    ' Class_Initialize 中 Set = New
                                               ' Class_Terminate 中 Set = Nothing

' 修复后(方法级)
Dim Dic As Dictionary
Set Dic = New 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-11Session 清理概率 1%已缓解cTimer 定时器每 30s 调用 CleanupExpiredSessions,不再依赖随机概率
BUG-12StopMe 销毁共享对象已缓解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

配置建议:

vb
' 启动前配置(推荐生产环境值)
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 修复6BUG-1~8
Round 2内存泄漏显式清理4ReleaseRequest/ClearRequest
Round 3Session 累积 + 类级 As New7Session HasID + 31 处 As New + BUG-9
Round 4方法级 As New + SSE BUG-15/10611 处 As New + BUG-15 + BUG-10
合计12 文件BUG-1~10, 15(12 个 BUG)+ 42 处 As New

VB6及其LOGO版权为微软公司所有