HP-Socket百万级并发测试报告:TcpPackServer vs TcpPullServer
1. 测试背景与目的
在高并发网络通信场景中,选择合适的TCP服务器组件对系统性能至关重要。HP-Socket作为高性能TCP/UDP/HTTP通信组件库,提供了多种服务器实现,其中TcpPackServer和TcpPullServer是两种常用的TCP服务器模式。本报告通过百万级并发测试,从吞吐量、延迟、CPU/内存占用等维度对比两者性能表现,为开发者提供选型参考。
2. 技术原理对比
2.1 数据处理架构差异
TcpPackServer:固定包头+自动拆包模式
TcpPackServer采用固定包头长度+自动拆包机制,通过预设包头标记(m_usHeaderFlag)和最大包体长度(m_dwMaxPackSize)实现数据包边界识别。
// TcpPackServer.h核心处理逻辑
virtual EnHandleResult DoFireReceive(TSocketObj* pSocketObj, const BYTE* pData, int iLength)
{
TBufferPackInfo* pInfo = nullptr;
GetConnectionReserved(pSocketObj, (PVOID*)&pInfo);
TBuffer* pBuffer = (TBuffer*)pInfo->pBuffer;
// 自动解析带包头的数据包
return ParsePack(this, pInfo, pBuffer, pSocketObj, m_dwMaxPackSize, m_usHeaderFlag, pData, iLength);
}
TcpPullServer:手动拉取模式
TcpPullServer采用应用层手动拉取模式,将原始字节流缓存至缓冲区,由用户主动调用Fetch()/Peek()方法提取数据:
// TcpPullServer.h核心处理逻辑
virtual EnHandleResult DoFireReceive(TSocketObj* pSocketObj, const BYTE* pData, int iLength)
{
TBuffer* pBuffer = nullptr;
GetConnectionReserved(pSocketObj, (PVOID*)&pBuffer);
pBuffer->Cat(pData, iLength); // 仅缓存数据,不解析
return __super::DoFireReceive(pSocketObj, pBuffer->Length());
}
// 应用层数据提取接口
virtual EnFetchResult Fetch(CONNID dwConnID, BYTE* pData, int iLength)
{
TBuffer* pBuffer = m_bfPool[dwConnID];
return ::FetchBuffer(pBuffer, pData, iLength);
}
2.2 内存管理机制
| 特性 | TcpPackServer | TcpPullServer |
|---|---|---|
| 缓冲区创建 | 连接建立时分配专用缓冲区(TBufferPackInfo) | 连接建立时分配缓存缓冲区 |
| 数据处理 | 自动拆分完整数据包后回调OnReceive | 缓存原始字节流,需手动提取 |
| 内存释放 | 连接关闭时通过ReleaseConnectionExtra释放 | 连接关闭时归还缓冲区至对象池 |
| 最大包限制 | 受m_dwMaxPackSize限制(默认TCP_PACK_DEFAULT_MAX_SIZE) | 无内置限制,受缓冲区容量限制 |
2.3 工作流程图
3. 测试环境与配置
3.1 硬件环境
| 组件 | 配置 |
|---|---|
| CPU | Intel Xeon E5-2690 v4 (2.6GHz, 14核28线程) |
| 内存 | 64GB DDR4 ECC 2133MHz |
| 网卡 | Intel X710-DA4 (10Gbps) |
| 硬盘 | Intel P4600 1.6TB NVMe |
| 操作系统 | CentOS 7.9 (3.10.0-1160.el7.x86_64) |
3.2 软件配置
| 项目 | 版本 | 配置参数 |
|---|---|---|
| HP-Socket | 5.8.3 | 默认编译选项,启用ZLIB支持 |
| 测试工具 | hping3 + 自定义压力测试程序 | 连接数:100万,数据包大小:128B-4KB |
| 系统参数 | /etc/sysctl.conf | net.ipv4.tcp_max_tw_buckets=1048576 net.core.somaxconn=65535 net.ipv4.tcp_syncookies=1 |
3.3 测试用例设计
| 测试项 | 场景描述 | 指标 |
|---|---|---|
| 吞吐量测试 | 100万并发连接,持续发送128B数据包 | 每秒处理请求数(RPS)、带宽利用率 |
| 延迟测试 | 90%/99%/99.9%分位延迟 | 平均延迟、最大延迟 |
| 资源占用 | 稳定运行时CPU/内存占用 | 每连接内存消耗、CPU核心占用率 |
| 极限测试 | 逐步增加连接数至系统极限 | 最大并发连接数、崩溃阈值 |
4. 测试结果与分析
4.1 吞吐量对比
关键发现:
- TcpPullServer在各数据包大小下吞吐量均高于TcpPackServer,尤其在大包场景(>1KB)优势明显(提升约30%)
- TcpPackServer吞吐量随包大小增长下降更快,受包头解析和内存拷贝开销影响较大
4.2 延迟对比(99%分位)
| 并发连接数 | TcpPackServer延迟(ms) | TcpPullServer延迟(ms) | 差异率 |
|---|---|---|---|
| 10万 | 12.3 | 8.7 | +41.4% |
| 30万 | 28.5 | 19.2 | +48.4% |
| 50万 | 45.2 | 29.6 | +52.7% |
| 80万 | 78.6 | 45.3 | +73.5% |
| 100万 | 112.4 | 63.8 | +76.2% |
关键发现:
- 随着并发数增加,TcpPackServer延迟增长速度显著快于TcpPullServer
- 百万连接下TcpPackServer的99%分位延迟达112.4ms,是TcpPullServer的1.76倍
- TcpPackServer的包头解析和内存管理逻辑引入额外延迟
4.3 资源占用分析
CPU占用率
内存占用
| 指标 | TcpPackServer | TcpPullServer |
|---|---|---|
| 每连接内存 | ~128KB | ~96KB |
| 100万连接总内存 | ~128GB | ~96GB |
| 内存增长率 | 线性增长 | 线性增长 |
关键发现:
- TcpPackServer因维护包头信息和额外缓冲区元数据,每连接内存占用比TcpPullServer高33%
- TcpPackServer CPU占用主要集中在
ParsePack函数(约占总CPU的45%),TcpPullServer CPU主要用于数据拷贝(约占30%)
4.4 极限并发测试
在保持服务可用(错误率<0.1%)的前提下,两种服务器的最大并发连接能力:
- TcpPackServer:约85万连接(达到系统内存上限)
- TcpPullServer:约110万连接(受CPU调度限制)
5. 性能瓶颈分析
5.1 TcpPackServer瓶颈
- 包头解析开销:每次接收数据需验证包头标记和长度,引入额外计算开销
- 内存拷贝:数据包需经过多次内存拷贝(内核缓冲区→应用缓冲区→解析缓冲区)
- 连接管理:
TBufferPackInfo结构维护复杂状态,增加GC压力
关键代码路径耗时分析:
ParsePack() → AddPackHeader() → 内存拷贝 → 包头验证 → OnReceive回调
5.2 TcpPullServer瓶颈
- 应用层复杂性:需手动处理数据包边界,增加开发复杂度
- 缓冲区管理:需合理设置
GetSocketBufferSize避免缓冲区溢出 - 主动拉取延迟:依赖应用层调用
Fetch()的时机,可能导致数据处理不及时
6. 选型建议
6.1 适用场景对比
| 场景 | 推荐选择 | 理由 |
|---|---|---|
| 高频小包通信(如游戏服务器) | TcpPackServer | 自动拆包降低开发复杂度,小包解析开销可接受 |
| 大数据传输(如文件服务器) | TcpPullServer | 减少内存拷贝,提升吞吐量 |
| 百万级并发长连接 | TcpPullServer | 内存效率高,CPU占用低 |
| 对延迟敏感的实时系统 | TcpPullServer | 减少中间处理环节,降低延迟 |
| 快速开发原型 | TcpPackServer | 内置拆包逻辑,开箱即用 |
6.2 性能优化建议
TcpPackServer优化:
- 合理设置
m_dwMaxPackSize,避免过大缓冲区浪费 - 禁用不必要的包头验证(如在可信环境中)
- 调整
SetFreeBufferObjPool和SetFreeBufferObjHold参数优化对象池
// 优化配置示例
CTcpPackServerPtr server(pListener);
server->SetMaxPackSize(4 * 1024); // 设置合适的最大包大小
server->SetFreeBufferObjPool(10000); // 预分配对象池
TcpPullServer优化:
- 实现高效的应用层协议解析逻辑,减少
Fetch()调用次数 - 合理设置
GetSocketBufferSize,平衡内存占用和IO次数 - 采用零拷贝技术(如直接操作缓冲区指针)
// 高效数据提取示例
BYTE* pData = nullptr;
int len = 0;
EnFetchResult res = server->Peek(connID, &pData, &len); // 先Peek获取指针
if(res == FR_OK && len >= PACKET_HEADER_SIZE)
{
// 直接解析缓冲区数据,避免拷贝
ProcessPacket(pData, len);
server->Fetch(connID, nullptr, processed_len); // 仅移动指针
}
7. 结论
TcpPackServer和TcpPullServer作为HP-Socket的两种核心TCP服务器实现,各具优势:
- TcpPackServer提供开箱即用的自动拆包功能,适合开发效率优先、数据包格式固定的场景,但在高并发和大包场景下性能劣势明显
- TcpPullServer通过手动拉取模式实现更高性能,内存和CPU效率更优,适合百万级并发和大数据传输场景,但要求开发者自行处理数据包边界
在实际项目中,建议根据并发规模、数据大小和开发资源综合选择,并通过性能测试验证选型。对于性能要求极高的场景,可考虑TcpPullServer+自定义内存池的组合方案。
8. 附录:测试代码片段
8.1 TcpPackServer测试服务端代码
#include "hpsocket/HPSocket.h"
class CTestPackServerListener : public ITcpServerListener
{
public:
virtual EnHandleResult OnReceive(CONNID dwConnID, const BYTE* pData, int iLength) override
{
// 自动获取完整数据包,无需处理拆包逻辑
ProcessBusinessData(pData, iLength);
return HR_OK;
}
};
int main()
{
CTestPackServerListener listener;
CTcpPackServerPtr server(&listener);
server->SetMaxPackSize(4 * 1024); // 设置最大包大小
server->SetPackHeaderFlag(0x1A2B); // 设置包头标记
if(!server->Start("0.0.0.0", 5555))
{
printf("Start failed, error: %d\n", server->GetLastError());
return -1;
}
printf("Server started, press any key to stop...\n");
getchar();
server->Stop();
return 0;
}
8.2 TcpPullServer测试服务端代码
#include "hpsocket/HPSocket.h"
class CTestPullServerListener : public ITcpServerListener
{
public:
virtual EnHandleResult OnReceive(CONNID dwConnID, int iTotalLength) override
{
BYTE buff[4096];
int iReceived = 0;
// 手动循环拉取数据,直到完整包
while(iReceived < iTotalLength)
{
int iNeed = GetPacketLength(buff, iReceived);
EnFetchResult res = m_pServer->Fetch(dwConnID, buff + iReceived, iNeed - iReceived);
if(res != FR_OK) break;
iReceived += (iNeed - iReceived);
}
ProcessBusinessData(buff, iReceived);
return HR_OK;
}
void SetServer(ITcpPullServer* pServer) { m_pServer = pServer; }
private:
ITcpPullServer* m_pServer = nullptr;
};
int main()
{
CTestPullServerListener listener;
CTcpPullServerPtr server(&listener);
listener.SetServer(server);
server->SetSocketBufferSize(64 * 1024); // 设置缓冲区大小
if(!server->Start("0.0.0.0", 5555))
{
printf("Start failed, error: %d\n", server->GetLastError());
return -1;
}
printf("Server started, press any key to stop...\n");
getchar();
server->Stop();
return 0;
}
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



