【C++接入大模型】WinHTTP类封装:实现对话式大模型接口访问
一、类设计概述
近期准备用C++做一些大预言模型方面的开发,先期计划实现C++调用公共的大模型Web接口,因为之前没做过C++的Web开发,经验少,所以对比了一些主流的框架,包括实际测试验证。以下是Windows平台下主流C++ HTTP库的对比分析:
库名称 | 内置支持 | 第三方依赖 | HTTPS支持 | API复杂度 | 跨平台 | 社区活跃度 | 典型应用场景 |
---|---|---|---|---|---|---|---|
WinHTTP | ✅ 是(Windows SDK) | 无(自动处理SSL/TLS) | ✅ 原生支持(无需OpenSSL) | 中等 | ❌ 仅Windows | 中等 | 系统级应用、快速集成、Windows专属开发 |
libcurl | ❌ 需手动集成 | 需OpenSSL/zlib等 | ✅ 依赖第三方库配置 | 高 | ✅ 是 | 高 | 跨平台项目、复杂协议需求 |
libhv | ❌ 需手动集成 | 无(内置SSL支持) | ✅ 需编译时启用 | 中等 | ✅ 是 | 中等 | 高性能网络服务、异步IO场景 |
Boost.Beast | ❌ 需Boost库 | 需OpenSSL | ✅ 需手动配置 | 高 | ✅ 是 | 高 | 现代C++项目、需要高度定制化 |
cpp-netlib | ❌ 需手动集成 | 需Boost/Asio等 | ✅ 需依赖项支持 | 高 | ✅ 是 | 低 | 传统企业级应用、遗留系统维护 |
Poco | ❌ 需手动集成 | 需OpenSSL | ✅ 需手动配置 | 中等 | ✅ 是 | 中等 | 综合性框架需求、企业级开发 |
Crow | ❌ 需手动集成 | 无(仅HTTP) | ❌ 无原生支持 | 低 | ✅ 是 | 低 | 轻量级Web服务、快速原型开发 |
转了一圈,最终选择了Windows原生WinHTTP API实现。
选型原因分析
-
内置支持与零配置优势
- WinHTTP直接集成于Windows系统,无需额外安装或配置OpenSSL等依赖,避免了证书链、动态库路径等复杂问题。对于Windows专属应用,可直接调用
winhttp.lib
,实现"开箱即用",也支持流式接口。
- WinHTTP直接集成于Windows系统,无需额外安装或配置OpenSSL等依赖,避免了证书链、动态库路径等复杂问题。对于Windows专属应用,可直接调用
-
简化HTTPS开发
- WinHTTP自动处理TLS/SSL握手和证书验证(通过
SECURITY_FLAG_IGNORE_*
标志控制),开发者无需手动管理加密套件或协议版本,显著降低HTTPS开发门槛。
- WinHTTP自动处理TLS/SSL握手和证书验证(通过
-
资源占用与性能平衡
- 相比libhv或Boost.Beast等跨平台库,WinHTTP在Windows环境下表现出更优的资源利用率,尤其适合对内存占用敏感的系统工具或后台服务。
-
API复杂度可控
- 虽然WinHTTP API较为底层,但通过封装(如您提供的
SimpleHttp
类)可屏蔽复杂性,同时保留对请求头、超时等关键参数的控制能力。
- 虽然WinHTTP API较为底层,但通过封装(如您提供的
-
企业级安全合规
- WinHTTP通过微软签名验证,符合Windows安全合规要求,适合金融、政务等对供应链安全敏感的场景。
SimpleHttp核心特性
SimpleHttp 是基于 Windows WinHTTP API 封装的 C++ HTTP 客户端类,支持同步/异步 HTTP/HTTPS 请求,提供流式数据处理能力。特性包括:
- 自适应HTTP/HTTPS协议
- 支持流式传输(Streaming)
- 线程安全的数据队列
- 异常安全的资源管理
- 符合C++20标准的现代语法
类接口说明
公共接口说明
1. 构造函数
SimpleHttp(const std::string& home, bool https = false);
- 功能:初始化 WinHTTP 会话,设置基地址和协议类型
- 参数:
home
:服务器基地址(如"api.example.com"
)https
:是否使用 HTTPS(默认false
)
- 异常:会话初始化失败时抛出
std::runtime_error
2. 析构函数
~SimpleHttp();
- 功能:释放所有资源,确保流式线程安全退出
- 行为:
- 终止流式接收线程(若有)
- 关闭所有 WinHTTP 句柄
3. GET 请求
int Get(const std::string& upath, std::string& resp, bool stream = false);
- 功能:发起 GET 请求
- 参数:
upath
:接口路径(自动拼接基地址)resp
:同步模式下存储完整响应stream
:是否启用流式处理(默认false
)
- 返回值:
0
:成功(流式请求立即返回)-1
:失败(异常抛出前返回)
- 异常:网络错误或 HTTP 状态码非 200 时抛出
std::runtime_error
4. POST 请求
int Post(const std::string& upath, const std::string& para, std::string& resp, bool stream = false);
- 功能:发起 POST 请求
- 参数:
upath
:接口路径para
:JSON 格式请求体resp
:同步模式下存储完整响应stream
:是否启用流式处理
- 返回值:同
Get()
方法 - 默认头:自动添加
Content-Type: application/json
(可覆盖)
5. 流式响应获取
int TryFetchResp(std::string& resp);
- 功能:轮询获取流式数据块
- 参数:
resp
存储当前数据块 - 返回值:
>0
:数据长度0
:流结束-1
:暂无数据
- 线程安全:通过互斥锁保护队列
6. 设置请求头
void SetHeaders(const std::unordered_map<std::string, std::string>& headers);
- 功能:批量设置请求头
- 参数:键值对映射(如
{ {"Authorization", "Bearer token"} }
) - 合并策略:相同头字段自动合并(
WINHTTP_ADDREQ_FLAG_COALESCE
)
异常处理
- 抛出类型:
std::runtime_error
- 典型场景:
- 网络连接失败(如 DNS 解析错误)
- 请求创建失败
- HTTP 状态码非 200
- 流式线程异常终止
流式处理工作流程
- 调用
Get()
/Post()
时设置stream = true
- 轮询调用
TryFetchResp()
获取数据 - 当返回
0
时终止循环
示例代码:
SimpleHttp client("https://api.example.com", true);
client.Get("/stream", response, true);
std::string chunk;
do {
int size = client.TryFetchResp(chunk);
if (size > 0) Process(chunk);
else std::this_thread::sleep_for(std::chrono::milliseconds(100));
} while (size != 0);
实现细节
- 线程管理:流式请求启动独立线程持续接收数据
- 资源释放:析构函数确保线程和句柄正确关闭
- 编码转换:内部使用 UTF-8 处理字符串(
wstring2s
/string2w
)
限制与注意事项
- 仅支持 Windows 平台(依赖 WinHTTP)
- 流式请求需手动管理生命周期
- HTTPS 默认忽略证书错误(生产环境需修改安全标志)
版本兼容性
- Windows SDK:最低支持 Windows 7/Server 2008 R2
- 编译器:需支持 C++17
二、核心函数深度分析
封装的类比较简单,支持GET和POST请求,支持流式接口,下面对封装的每个部分逐一展开说明。
1. 构造函数
SimpleHttp(const std::string& home, bool https = false)
实现要点:
- 会话初始化:通过
WinHttpOpen
创建持久化会话,设置用户代理为"SimpleHttp/1.0" - 超时控制:统一设置5秒超时(连接/发送/接收/空闲)
- 异常处理:会话创建失败时抛出
runtime_error
,包含系统错误码 - 协议识别:通过
_isHttps
标记自动区分协议类型
设计考量:
- 使用默认代理配置(
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY
)提高兼容性 - 零初始化参数(
WINHTTP_NO_PROXY_NAME
)避免冗余配置
2. 析构函数
~SimpleHttp()
资源管理:
- 线程安全终止:通过原子变量
_streamActive
控制流式线程退出 - 句柄清理:
- 顺序关闭请求→连接→会话句柄
- 使用RAII模式确保资源释放
异常防御:
joinable()
检查避免二次join导致崩溃- 线程终止前设置
_streamActive=false
防止死锁
3. Get请求
int Get(const std::string& upath, std::string& resp, bool stream = false)
工作流程:
- 调用
SendRequest
发起GET请求 - 根据stream参数选择同步/异步模式
- 同步模式直接返回完整响应
- 异步模式启动子线程持续接收数据
参数设计:
upath
:URL路径自动拼接基地址stream
:控制响应处理模式(内存缓冲/流式队列)
4. Post请求
int Post(const std::string& upath, const std::string& para, std::string& resp, bool stream = false)
增强特性:
- 自动设置Content-Type为application/json(未指定时)
- 支持任意格式POST数据(通过para参数)
- 数据长度自动计算(
postData.size()
)
安全机制:
- 使用
WINHTTP_FLAG_REFRESH
强制获取最新资源 - HTTPS请求自动忽略证书错误(测试环境适用)
5. 流式响应获取
int TryFetchResp(std::string& resp)
队列管理:
- 互斥锁保护的
_datas
队列 - 三态返回值设计:
>0
:有效数据长度0
:流结束标记-1
:暂无数据
性能优化:
- 队列首部数据优先出队(O(1)时间复杂度)
- 空队列时立即返回避免阻塞
6. 请求头设置
void SetHeaders(const std::unordered_map<std::string, std::string>& headers)
头处理机制:
- 使用
WINHTTP_ADDREQ_FLAG_COALESCE
自动合并重复头 - 支持批量添加/覆盖现有头
- 保留默认Content-Type设置
实现细节:
- 字符串转换使用UTF-8编码
- 头格式验证(自动添加冒号分隔符)
三、关键私有方法
1. 核心请求处理
int SendRequest(const std::wstring& method, ...)
多阶段处理:
- 连接建立:
WinHttpConnect
创建目标服务器连接 - 请求创建:
WinHttpOpenRequest
配置请求方法/路径 - 安全设置:HTTPS请求忽略证书错误
- 头注入:遍历
_headers
容器添加自定义头 - 数据发送:
WinHttpSendRequest
传输请求体 - 响应处理:同步模式直接读取,异步模式启动接收线程
异常处理:
- 每个WinAPI调用后立即检查返回值
- 使用结构化异常处理(SEH)捕获系统级错误
2. 流式接收器
void StreamReceiver(HINTERNET hConnect, HINTERNET hRequest)
并发设计:
- 使用原子变量
_streamActive
控制循环 - 独立线程持续调用
WinHttpReadData
- 数据块即时推入线程安全队列
资源管理:
- 函数退出时自动关闭连接/请求句柄
- 异常传播前确保资源释放
3. 字符串转换
std::string wstring2s(const std::wstring& wstr)
std::wstring string2w(const std::string& str)
编码转换:
- 基于Windows API的UTF-8转换
- 预分配目标缓冲区优化性能
- 空字符串快速路径
四、辅助函数解析
1. 全局替换函数
std::string ReplaceAll(std::string str, const std::string& from, const std::string& to)
实现特点:
- 原地修改(接收值拷贝)
- 处理重叠替换问题(pos递增策略)
- 空模式保护避免死循环
2. JSON内容提取
std::string ExtractContent(std::string& resp)
解析逻辑:
- 基于特征字符串定位(“content”:")
- 支持转义字符处理(\n, \r, ", \)
- 修改输入字符串实现流式解析
健壮性设计:
- 非规范JSON容错处理
- 内存安全(范围检查)
五、代码示例说明
1. 同步请求示例
SimpleHttp client("api.example.com", true);
std::string response;
client.Get("/data", response);
- 即时获取完整响应
- 适用于小型数据交互
2. 流式处理示例
client.Post("/stream", data, response, true);
while(int size = client.TryFetchResp(chunk)) {
if(size > 0) process(chunk);
}
- 实时处理大文件/持续数据流
- 减少内存占用(无需完整缓存)
六、问题解决
Q:析构时触发断点
- 原因:流式线程未正确终止
- 解决方案:
~SimpleHttp() { CancelStream(); // 新增统一清理 if(_hSession) WinHttpCloseHandle(_hSession); } void CancelStream() { if(_streamActive.exchange(false)) { WinHttpCloseHandle(_hRequest); // 强制终止 if(_worker.joinable()) _worker.join(); } }
Q:HTTPS证书错误
- 解决方案:生产环境应移除证书忽略配置
if(_isHttps) { DWORD dwFlags = SECURITY_FLAG_SECURE; // 严格验证 WinHttpSetOption(...); }
七、总结
本实现通过封装WinHTTP API,在保持高性能的同时提供了简洁的接口。通过线程安全的流式处理、健壮的错误恢复和现代化的C++特性,适用于需要精细控制HTTP通信的Windows应用场景。未来可通过连接池、异步回调等机制进一步提升性能,满足更复杂的企业级需求。
八、完整代码
#include <windows.h>
#include <winhttp.h>
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <unordered_map>
#include <tchar.h>
#include <mutex>
#include <atomic>
#include <thread>
#pragma comment(lib, "winhttp.lib")
#include <strsafe.h>
/// <summary>
/// 简单的HTTP类,自动支持http和https请求
/// 也支持流式处理,此时结果先缓存到队列,需主动调用TryFetchResp去轮询结果
/// </summary>
class SimpleHttp
{
public:
SimpleHttp(const std::string& home, bool https = false) :_strBaseUrl(home), _isHttps(https)
{
// 初始化WinHTTP会话 [[1]][[2]]
_hSession = WinHttpOpen(L"SimpleHttp/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS,
0);
if (!_hSession)
{
throw std::runtime_error("Failed to initialize WinHTTP session: " + GetLastError());
}
// 设置默认超时(5秒)
DWORD timeout = 5000;
WinHttpSetTimeouts(_hSession, timeout, timeout, timeout, timeout);
}
~SimpleHttp()
{
if (_worker.joinable())
{
_streamActive = false;
_worker.join();
}
if (_hSession) WinHttpCloseHandle(_hSession);
}
/// <summary>
/// 发起GET请求
/// </summary>
/// <param name="upath">接口名</param>
/// <param name="resp">非流式请求时接收应答数据</param>
/// <param name="stream">是否为流式请求</param>
/// <returns></returns>
int Get(const std::string& upath, std::string& resp, bool stream = false)
{
return SendRequest(L"GET", upath, "", resp, stream);
}
/// <summary>
///
/// </summary>
/// <param name="upath">接口名</param>
/// <param name="para">JSON请求参数</param>
/// <param name="resp">非流式请求时接收应答数据</param>
/// <param name="stream">是否为流式请求</param>
/// <returns></returns>
int Post(const std::string& upath, const std::string& para, std::string& resp, bool stream = false)
{
return SendRequest(L"POST", upath, para, resp, stream);
}
/// <summary>
/// 获取流式结果
/// </summary>
/// <param name="resp"></param>
/// <returns>返回本次数据包大小,等于0表示数据包接收完成,-1表示没数据</returns>
int TryFetchResp(std::string& resp)
{
std::lock_guard<std::mutex> guard(_mtxDatas);
if (_datas.empty())
{
return (_streamActive ? -1 : 0);
}
resp = _datas[0];
_datas.erase(_datas.begin());
return static_cast<int>(resp.size());
}
/// <summary>
/// 设置请求头
/// </summary>
/// <param name="headers"></param>
void SetHeaders(const std::unordered_map<std::string, std::string>& headers)
{
for (auto& e : headers)
{
_headers[e.first] = e.second;
}
}
protected:
/// <summary>
/// 写入接收数据
/// </summary>
/// <param name="dat"></param>
void Push(const std::string& dat)
{
std::lock_guard<std::mutex> guard(_mtxDatas);
if(!dat.empty()) _datas.push_back(dat);
}
private:
int SendRequest(const std::wstring& method, const std::string& upath, const std::string& postData, std::string& resp, bool stream)
{
DWORD dwSize = 0;
DWORD dwDownloaded = 0;
LPSTR pszOutBuffer;
BOOL bResults = FALSE;
HINTERNET hConnect = nullptr;
HINTERNET hRequest = nullptr;
try
{
// 创建连接
hConnect = WinHttpConnect(_hSession,
string2w(_strBaseUrl).c_str(),
INTERNET_DEFAULT_PORT,
0);
if (!hConnect) throw std::runtime_error("Connection failed");
// 创建请求
hRequest = WinHttpOpenRequest(hConnect,
method.c_str(),
string2w(upath).c_str(),
nullptr,
WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
(_isHttps ? (WINHTTP_FLAG_SECURE | WINHTTP_FLAG_REFRESH) : 0));
if (!hRequest) throw std::runtime_error("Request creation failed");
// 设置安全选项(忽略证书错误)
if (_isHttps)
{
DWORD dwFlags = SECURITY_FLAG_IGNORE_UNKNOWN_CA |
SECURITY_FLAG_IGNORE_CERT_DATE_INVALID;
WinHttpSetOption(hRequest, WINHTTP_OPTION_SECURITY_FLAGS, &dwFlags, sizeof(dwFlags));
}
// 设置请求头
SetHeaders(hRequest);
// 发送请求
bResults = WinHttpSendRequest(hRequest,
WINHTTP_NO_ADDITIONAL_HEADERS,
0,
(LPVOID)postData.c_str(),
(DWORD)postData.size(),
(DWORD)postData.size(),
0);
if (!bResults) throw std::runtime_error("Send request failed");
// 接收响应
if (!WinHttpReceiveResponse(hRequest, nullptr))
{
throw std::runtime_error("Receive response failed");
}
// 获取状态码 [[10]]
DWORD dwStatusCode = 0;
DWORD dwSize = sizeof(dwStatusCode);
WinHttpQueryHeaders(hRequest,
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX,
&dwStatusCode,
&dwSize,
WINHTTP_NO_HEADER_INDEX);
if (dwStatusCode != 200)
{
throw std::runtime_error("HTTP error: " + std::to_string(dwStatusCode));
}
// 流式子线程处理
if (stream)
{
_streamActive = true;
_worker = std::thread(&SimpleHttp::StreamReceiver, this, hConnect, hRequest);
return 0; // 立即返回
}
// 处理响应数据
std::string buffer;
do
{
dwSize = 0;
if (!WinHttpQueryDataAvailable(hRequest, &dwSize) || dwSize <= 0) break;
size_t nRecv = dwSize;
pszOutBuffer = new char[nRecv + 1];
ZeroMemory(pszOutBuffer, nRecv + 1);
if (WinHttpReadData(hRequest, (LPVOID)pszOutBuffer, dwSize, &dwDownloaded))
{
buffer.append(pszOutBuffer, dwDownloaded);
}
delete[] pszOutBuffer;
} while (dwSize > 0);
resp = buffer;
return 0;
}
catch (const std::exception& e)
{
if (hRequest) WinHttpCloseHandle(hRequest);
if (hConnect) WinHttpCloseHandle(hConnect);
throw std::runtime_error("WinHTTP Error: " + std::string(e.what()) +
" [ErrorCode: " + std::to_string(GetLastError()) + "]");
return -1;
}
}
/// <summary>
/// 流式接收应答
/// </summary>
void StreamReceiver(HINTERNET hConnect, HINTERNET hRequest)
{
DWORD dwSize = 0;
DWORD dwDownloaded = 0;
char* pszOutBuffer = nullptr;
std::string strErr;
try
{
while (_streamActive)
{
dwSize = 0;
if (!WinHttpQueryDataAvailable(hRequest, &dwSize) || dwSize == 0) break;
const size_t nRecv = dwSize;
pszOutBuffer = new char[nRecv + 1];
ZeroMemory(pszOutBuffer, nRecv + 1);
if (WinHttpReadData(hRequest, pszOutBuffer, dwSize, &dwDownloaded))
{
Push(std::string(pszOutBuffer, dwDownloaded));
}
delete[] pszOutBuffer;
}
}
catch (...)
{
strErr = "stream recv data error";
}
// 发送结束标记
_streamActive = false;
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
if (!strErr.empty())
{
throw std::runtime_error(strErr.c_str());
}
}
/// <summary>
/// 设置请求头
/// </summary>
void SetHeaders(HINTERNET hRequest)
{
// 创建请求后立即设置请求头
for (const auto& h : _headers)
{
std::wstring header = string2w(h.first + ": " + h.second);
if (!WinHttpAddRequestHeaders(hRequest,
header.c_str(),
static_cast<DWORD>(header.length()),
WINHTTP_ADDREQ_FLAG_COALESCE))
{
//throw std::runtime_error("Failed to set header: " + h.first + " [Error: " + std::to_string(GetLastError()) + "]");
}
}
}
// std::wstring → std::string (UTF-8)
std::string wstring2s(const std::wstring& wstr) {
if (wstr.empty()) return {};
int len = WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr);
std::string result(len, '\0');
WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), (LPSTR)result.data(), len, nullptr, nullptr);
return result;
}
// std::string (UTF-8) → std::wstring
std::wstring string2w(const std::string& str) {
if (str.empty()) return {};
int len = MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), nullptr, 0);
std::wstring result(len, L'\0');
MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), (LPWSTR)result.data(), len);
return result;
}
private:
// 是否https
bool _isHttps{ false };
// 基地址
std::string _strBaseUrl;
// 默认headers
std::unordered_map<std::string, std::string> _headers;
// 异步数据
std::vector<std::string> _datas;
std::mutex _mtxDatas;
// WinHttp对象
HINTERNET _hSession = nullptr;
// 流式操作子线程
std::atomic<bool> _streamActive{ false };
std::thread _worker;
};
std::string ReplaceAll(std::string str, const std::string& from, const std::string& to) {
if (from.empty()) return str; // 避免死循环[[2]]
size_t pos = 0;
while ((pos = str.find(from, pos)) != std::string::npos)
{
str.replace(pos, from.length(), to);
pos += to.length();
}
return str;
}
std::string ExtractContent(std::string& resp) {
std::vector<std::string> results;
std::string key = "\"content\":";
size_t pos = 0;
while ((pos = resp.find(key, pos)) != std::string::npos)
{
pos += key.length();
// 跳过空白和冒号
while (pos < resp.size() && (resp[pos] == ' ' || resp[pos] == ':')) pos++;
if (resp[pos] != '"') continue; // 非字符串值跳过
size_t start = ++pos;
std::string content;
// 处理转义字符和双引号
while (pos < resp.size())
{
char c = resp[pos++];
if (c == '"') break;
else if (c == '\\' && pos < resp.size())
{
switch (resp[pos++])
{
case 'n':
c = '\n';
break;
case '\r':
c = '\r';
break;
case '\\':
c = '\\';
break;
case '"':
c = '\"';
break;
default:
pos--;
break;
}
}
content += c;
}
resp = resp.substr(pos);
return content;
}
resp = "";
return resp;
}
int main()
{
int iii = 0;
std::cin >> iii;
// 请求参数
char para[4096] = "{\"stream\" : true,\"model\":\"deepseek-r1-distill-qwen-32b\",\"messages\":[{\"role\":\"user\",\"content\":\"Please help me write a C++ class for parsing XML, with user-friendly interfaces that support XPath and iterative data access.\"}]}";
// char para[4096] = "{\"stream\" : true,\"model\":\"deepseek-r1-distill-qwen-32b\",\"messages\":[{\"role\":\"user\",\"content\":\"The prime numbers within 10?\"}]}";
auto url = "https://cloud.infini-ai.com/maas/v1/chat/completions";
auto api_key = "Bearer sk-************";
auto model_name = "deepseek-r1-distill-qwen-32b";
SimpleHttp web("cloud.infini-ai.com", true);
web.SetHeaders({ { "Content-Type", "application/json" }, { "Authorization", "Bearer sk-daxdj5ksqdc6iuvn" } });
std::string resp;
web.Post("/maas/v1/chat/completions", para, resp, true);
int nRecv = 0;
do
{
nRecv = web.TryFetchResp(resp);
if (nRecv > 0)
{
while (!resp.empty())
{
std::cout << ExtractContent(resp);
}
}
else
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
} while (nRecv);
return 0;
}
运行结果: