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) 越界或取到错误对象!

修复: CollectionScripting.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 Scripting.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 = 0Class_Terminate 调用 StopMe

BUG-6: FindCRLFCRLF 返回 0 歧义

根因: CRLFCRLF 出现在位置 0 时返回 0,未找到也返回 0。

修复: 未找到时返回 -1ContainsCompleteRequestExtractOneRequest 同步更新。

BUG-7: 无最大连接数限制

修复: 新增 MaxConnections 属性(默认 1000),超限 Disconnect = True

BUG-8: SendData 无错误处理

修复: SendBodyByteSendHeaderOn 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 为空时自动生成 GUIDProcessHttpRequestIf 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 ClassNameSet 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 NewAs 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.clsIndex→hSocket;LastActivity;Release 彻底清理;recvBuffer 缩容
src\HttpServer\cHttpServer.clsCollection→Dictionary;6 个安全属性;FindCRLFCRLF -1;cTimer 集成;ReleaseRequest/ClearRequest;Session.HasID;13 处 As New→显式
src\HttpServer\cHttpServerResponse.clsSendData 错误保护;m_HasSent;State413;Class_Terminate;1 处 As New→显式
src\HttpServer\cHttpServerContext.clsReleaseRequest();清理循环引用
src\HttpServer\cHttpServerRequest.clsClearRequest();Class_Terminate
src\HttpServer\cHttpServerCookies.cls1 处 As New→显式
src\HttpServer\cHttpServerSession.clssessionID 不自动生成;HasID;EnsureData 延迟创建;Release();Class_Terminate;5 处 As New→显式
src\HttpServer\cHttpServerSvr.cls4 处 As New→显式;Class_Initialize/Terminate
src\HttpServer\cHttpServerRouter.cls8 处类级 + 1 处方法级 As New→显式;Class_Initialize/Terminate
src\HttpServer\cHttpServerRouteBefore.cls1 处 As New→显式;Class_Initialize/Terminate
src\HttpServer\cHttpServerRouterAfter.cls1 处 As New→显式;Class_Initialize/Terminate
src\SSE\cSSE.cls7 处类级 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 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-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版权为微软公司所有