简介:面向嵌入式网络开发者的WinCE TCP通信示例工程,基于Visual Studio 2005环境,围绕Winsock套接字编程演示连接建立、数据收发与异常关闭流程,适合需要理解WinCE网络编程基础、排查套接字调用细节的初中级开发者。压缩包共29个文件,约4.45MB,包含cpp与h源码、vcproj工程配置、rc资源脚本、sln解决方案及pdb、exe等编译调试产物,便于直接打开对照调试。已有315人学习,工程以PC端与WinCE端两套示例组织,将socket、connect、send、recv和closesocket等关键调用串成可运行流程,读者可观察地址与端口参数设置、缓冲区收发循环、错误处理写法,并参考ws2_32.lib链接依赖和_WIN32_WCE预处理器定义,快速迁移到自己的嵌入式网络模块。项目还保留调试符号与可执行文件,方便对照运行结果定位问题;源码把套接字创建、服务器地址填充、连接请求、循环收发和关闭清理分别展开,可帮助读者梳理WinCE网络程序的调用顺序与资源释放习惯。
1. 生产线上的老设备要接后台:先把 WinCE 的 TCP 通信边界摸清
手上还留着一批跑 Windows CE 的工控屏、手持终端、老式 HMI,后台早就换成 Linux 服务端了,中间这条链路还得靠 TCP 通信撑着。麻烦在于,WinCE 不是桌面 Windows:系统镜像由 Platform Builder 裁出来,Winsock 组件选没选、选的哪个版本、SDK 里的头文件和运行库对不对得上,任何一处不匹配,程序在开发板上能跑、到现场机器上就直接起不来。这份 WinCE 实现 TCP 通信的代码包,解决的正是这段最容易出事的路:把 socket 的建立、收发、断线重连、线程退出做成一套能复用的骨架。
它适合两类人。一类是刚接手 CE 老工程、手里只有 eVC 或 VS2005 的开发者,需要一份能编译、能对着改的最小可用实现;另一类是想把原来那个“一个线程死等 recv、界面卡成 PPT”的版本,改成能上生产的长连接。下面按“先摸清系统给了什么、再搭骨架、最后填坑”的顺序拆。
2. 摸清底牌:WinCE 上的 Winsock 到底给了什么
在桌面上写 socket,头文件和库是跟着编译器走的;在 WinCE 上,头文件跟着 SDK 走,运行库跟着设备上的 OS 镜像走。这两件事是分开的,所以“编译通过”和“设备上能跑”之间隔着一整个 OS 设计。绝大多数 WinCE TCP 通信的疑难杂症,根子都在这条缝里。
2.1 组件在 OS 设计里,不在你的代码里
WinCE 是组件化的,TCP/IP 协议栈和 Winsock 都是平台构建时按需选进去的目录项。如果做镜像的人只勾了最基础的网络支持,可能只有 Winsock 1.1,甚至只有 NDIS 驱动而没有上层 socket 接口。这时候你在 SDK 里对着winsock2.h写代码、调WSAStartup(MAKEWORD(2,2)),编译阶段一切正常,到设备上WSAStartup直接返回 10092(WSAVERNOTSUPPORTED),或者进程加载时因为找不到对应的 DLL 而直接退掉。
我一般的处理顺序是:先确认设备上“有没有”,再确认“是哪个版本”,最后才谈代码怎么写。判断方法很土但有效——跑一段探针程序,把WSAStartup的返回码和协商出来的版本号打出来。如果返回 0 但wVersion是 0x0101,说明这套镜像只给到 Winsock 1.1,那么getaddrinfo、WSAPoll这类 2.x 才有的 API 一律不要用。
2.2 一段最小探针:把版本和可用 API 一次问清
探针要足够小,小到能排除业务代码的干扰。下面这段直接放在WinMain里跑,先探版本,再探能不能创建 socket、能不能解析名字:
// probe.cpp —— 最小探针:确认这台 WinCE 上的 Winsock 给了什么 #include <windows.h> #include <winsock.h> // 老 SDK 只有 1.1 的头;有 ws2tcpip.h 的工程再换 #pragma comment(lib, "ws2.lib") // 只带 1.1 的 SDK 改成 "winsock.lib" static void Log(LPCTSTR s) { OutputDebugString(s); // 接了调试器就能在输出窗口看到 // 现场没有调试器时,改成写 \Storage Card\probe.txt 更省事 } int WINAPI WinMain(HINSTANCE, HINSTANCE, LPWSTR, int) { WSADATA wsa; int rc = WSAStartup(MAKEWORD(2, 2), &wsa); // 先按 2.2 申请 if (rc != 0) { // 10092 版本不支持 / 10091 网络子系统没就绪 / 10093 未初始化 TCHAR buf[128]; _stprintf(buf, TEXT("WSAStartup failed rc=%d"), rc); Log(buf); // 退一步按 1.1 再试一次,能成说明镜像只给了 1.1 rc = WSAStartup(MAKEWORD(1, 1), &wsa); if (rc != 0) return 1; } TCHAR buf[128]; _stprintf(buf, TEXT("ver=%x.%x high=%x.%x"), LOBYTE(wsa.wVersion), HIBYTE(wsa.wVersion), LOBYTE(wsa.wHighVersion), HIBYTE(wsa.wHighVersion)); Log(buf); SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (s == INVALID_SOCKET) { _stprintf(buf, TEXT("socket failed err=%d"), WSAGetLastError()); Log(buf); } else { closesocket(s); // 探针也要收尾,别养成漏关的习惯 } WSACleanup(); return 0; }这段代码的价值不在功能,在于它把“环境问题”和“业务问题”切开了。wVersion和wHighVersion两个字段要分开看:前者是协商结果,后者是这套栈支持到的上限。如果上限是 2.2 而你申请 2.2 却失败,问题通常在 OS 设计而不是代码。socket()那一步单独探,是因为它失败往往意味着协议栈没起来或者内存不足,跟版本没关系。
提示:探针程序的日志一定落到文件里。WinCE 设备在现场没有调试器可接,
OutputDebugString的输出等于扔掉。
2.3 头文件、库与字符集:三件常被忽略的小事
| 项目 | 老平台(eVC / CE 4.x 及更早) | 新平台(VS2005+ / CE 5.0、6.0) | 备注 |
|---|---|---|---|
| 头文件 | winsock.h | winsock2.h+ws2tcpip.h | 两套头不要混着 include,顺序冲突会报重定义 |
| 库 | winsock.lib | ws2.lib | 以 SDK 目录里实际存在的 lib 为准 |
| 字符集 | 多为多字节 | 默认 Unicode | 网络层收发一律走char,界面层再转 |
| 名字解析 | gethostbyname | 视组件而定,可能没有getaddrinfo | 拿不准就用inet_addr直接填 IP |
字符集这条得单独说。WinCE 工程默认定义_UNICODE,TCHAR展开成wchar_t,而 socket 的地址结构、inet_ntoa、recv的缓冲区全是char。我见过太多人在inet_ntoa外面套一层TEXT()宏,编译期悄无声息地过了(因为宏在 Unicode 下变成L""前缀,某些写法能凑巧编过),运行时指针越界。规矩定死:网络层只用char,日志和界面用TCHAR,两者之间靠MultiByteToWideChar转换,转换时显式指定代码页,不要依赖默认值。
3. 骨架:把 socket 生命周期拆成四段来写
一份能上生产的 WinCE TCP 通信代码,结构上跟桌面版本没本质区别,差别在每一段的超时控制和失败处理都必须显式写出来——WinCE 上很多默认行为跟桌面不一样,靠“应该会超时”这种假设一定会翻车。下面按服务端、客户端、收发、关闭四段拆。
3.1 服务端:bind / listen / accept 的三处参数
服务端最常见的问题是“端口被占”和“绑定到了错误的网卡”。WinCE 设备上常有不止一张网卡(板载以太网 + CF 无线网卡),INADDR_ANY会把两张都绑上,如果对端从另一张网卡的方向连过来,行为可能跟你预期不一致。稳一点的做法是把本机要监听的地址写进配置,明确绑哪一张。
SOCKET CreateListener(const char* bindIp, unsigned short port, int backlog) { SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (s == INVALID_SOCKET) return INVALID_SOCKET; BOOL reuse = TRUE; // 只解决 TIME_WAIT 期间重启服务端的重绑问题,不是端口复用 setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)); sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(port); // 端口必须转网络序 addr.sin_addr.s_addr = (bindIp && *bindIp) ? inet_addr(bindIp) : INADDR_ANY; if (bind(s, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { closesocket(s); // 失败路径也要关,别漏 return INVALID_SOCKET; } if (listen(s, backlog) == SOCKET_ERROR) { closesocket(s); return INVALID_SOCKET; } return s; }backlog在 WinCE 上不要给太大,老设备的 socket 句柄和内存都紧张,给 4 到 8 就够用,给 64 反而容易在压力下拿不到资源。htons这一步漏掉是经典错误:端口写成9000而不是htons(9000),在小端设备上实际监听的是0x2823,现象是“服务端起来了但客户端一直连不上”,用netstat在 PC 端看不到任何监听记录,很容易怀疑到防火墙上去。
accept 建议配select做超时,而不是裸阻塞。裸阻塞的 accept 在线程退出时没法及时响应停止标志:
for (;;) { fd_set rfds; FD_ZERO(&rfds); FD_SET(listener, &rfds); timeval tv = {0, 200000}; // 200ms 醒来一次,检查退出标志 int n = select(0, &rfds, NULL, NULL, &tv); // WinCE 上第一个参数被忽略,填 0 if (n == SOCKET_ERROR) break; if (n == 0) { if (g_bStop) break; continue; } sockaddr_in peer; int plen = sizeof(peer); SOCKET c = accept(listener, (sockaddr*)&peer, &plen); if (c == INVALID_SOCKET) continue; // 对端地址用 inet_ntoa 打日志时记得它是返回静态缓冲区,不可重入 }3.2 客户端:connect 的超时、非阻塞切换与重连
客户端的坑集中在connect。默认的阻塞connect在网络不通时要等系统默认超时才返回,WinCE 上这个时间可能长到几十秒,界面线程如果直接调,用户看到的就是“点了没反应”。正确做法是先切非阻塞、connect拿WSAEWOULDBLOCK、用select等可写、再用SO_ERROR取真实结果:
SOCKET ConnectWithTimeout(const char* ip, unsigned short port, int timeoutMs) { SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (s == INVALID_SOCKET) return INVALID_SOCKET; u_long nb = 1; ioctlsocket(s, FIONBIO, &nb); // 1 = 非阻塞 sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = inet_addr(ip); // 传域名就先 gethostbyname 再取地址 int rc = connect(s, (sockaddr*)&addr, sizeof(addr)); if (rc == SOCKET_ERROR) { if (WSAGetLastError() != WSAEWOULDBLOCK) { closesocket(s); return INVALID_SOCKET; } fd_set wfds; FD_ZERO(&wfds); FD_SET(s, &wfds); timeval tv; tv.tv_sec = timeoutMs / 1000; tv.tv_usec = (timeoutMs % 1000) * 1000; rc = select(0, NULL, &wfds, NULL, &tv); if (rc <= 0) { closesocket(s); return INVALID_SOCKET; } // 0 超时 / -1 出错 int err = 0, len = sizeof(err); getsockopt(s, SOL_SOCKET, SO_ERROR, (char*)&err, &len); if (err != 0) { closesocket(s); return INVALID_SOCKET; } } nb = 0; ioctlsocket(s, FIONBIO, &nb); // 连上后切回阻塞,收发逻辑更简单 return s; }这里有两个细节值得说。select返回可写不代表连接成功,必须再查SO_ERROR,因为连接被拒时 socket 同样会变成“可写”,只是内部错误码被置上了。另外切回阻塞这一步不能省,很多人在非阻塞 socket 上直接写recv的循环,忘了WSAEWOULDBLOCK也是“正常返回”,结果把“暂时没数据”当成了“对端关闭”,一半的“莫名断线”都是这么来的。
重连策略上,我不建议写成while(1) { connect(); Sleep(1000); }。指数退避更稳:1s、2s、4s、8s,封顶 30s,成功一次就清零。老设备的 CPU 和网络都不富裕,密集重连只会把本来能恢复的链路彻底压死。
3.3 收发:select 控超时,长度前缀控边界
TCP 是字节流,recv一次返回多少完全看协议栈心情。WinCE 的缓冲比桌面更小,现场又常是短报文高频发送,粘包和半包几乎必然出现。解决办法不是加Sleep,是定一个应用层帧格式:4 字节长度(网络序)+ 载荷。
bool SendAll(SOCKET s, const char* buf, int len) { int sent = 0; while (sent < len) { int n = send(s, buf + sent, len - sent, 0); if (n == SOCKET_ERROR) { int e = WSAGetLastError(); if (e == WSAEINTR) continue; // 被中断,重试 return false; // 其它错误一律按断链处理 } sent += n; // 部分发送是常态,必须累计 } return true; } bool RecvAll(SOCKET s, char* buf, int len, int timeoutMs) { int got = 0; while (got < len) { fd_set rfds; FD_ZERO(&rfds); FD_SET(s, &rfds); timeval tv; tv.tv_sec = timeoutMs / 1000; tv.tv_usec = (timeoutMs % 1000) * 1000; int rc = select(0, &rfds, NULL, NULL, &tv); if (rc == 0) return false; // 超时,由上层决定是否发心跳 if (rc == SOCKET_ERROR) return false; int n = recv(s, buf + got, len - got, 0); if (n == 0) return false; // 对端正常关闭 if (n == SOCKET_ERROR) { if (WSAGetLastError() == WSAEINTR) continue; return false; } got += n; } return true; }RecvAll只保证“凑满 len 字节或失败”,真正的分帧是在外面这层:
// 先读 4 字节头,按网络序解出长度,再读载荷 unsigned int netLen = 0; if (!RecvAll(s, (char*)&netLen, 4, 3000)) { /* 超时或断链 */ } unsigned int bodyLen = ntohl(netLen); if (bodyLen == 0 || bodyLen > 64 * 1024) { /* 长度非法,直接断链,防脏数据撑爆内存 */ }长度上限这道校验别省。老设备内存本来就小,一个被干扰坏掉的长度头解出个几百兆,new一下就把进程干掉了,日志里连个痕迹都不留。上限按业务最大报文来定,控制类报文 4KB 足够,带图片或文件块的放到 64KB。
4. 线程模型:阻塞 recv 绝不能待在消息泵里
WinCE 的 UI 线程同时负责消息循环,任何阻塞调用都会让窗口失去响应。WinCE 上没有桌面那种“无响应”灰屏提示,用户看到的就是彻底卡死,连关闭按钮都点不动,只能断电重启。所以网络收发必须挪到工作线程,这一点比在桌面上更刚性。
4.1 工作线程怎么写、怎么停
用CreateThread起线程,参数结构体传进去,线程内自己管 socket 的生死。停止标志用volatile BOOL加InterlockedExchange,别用普通的BOOL——编译器可能把它缓存在寄存器里,线程永远看不到变化,这种 bug 只在 Release 下出现,调试版一切正常。
struct NetCtx { HWND hWnd; // 用来 PostMessage 回界面线程 volatile LONG bStop; // 0 = 运行,1 = 请求退出 SOCKET sock; }; DWORD WINAPI NetThread(LPVOID param) { NetCtx* ctx = (NetCtx*)param; while (InterlockedCompareExchange(&ctx->bStop, 0, 0) == 0) { SOCKET s = ConnectWithTimeout("192.168.1.100", 9000, 3000); if (s == INVALID_SOCKET) { PostMessage(ctx->hWnd, WM_NET_STATE, 0, 0); // 通知界面"连接失败" for (int i = 0; i < 30 && !ctx->bStop; ++i) Sleep(1000); // 分片睡,能及时响应退出 continue; } ctx->sock = s; PostMessage(ctx->hWnd, WM_NET_STATE, 1, 0); // 收发循环,内部用 select 的超时周期性检查 bStop closesocket(s); ctx->sock = INVALID_SOCKET; } return 0; }注意那个“分片睡”的写法。Sleep(30000)一次性睡 30 秒,用户点退出就得等 30 秒,体验极差;拆成 30 次 1 秒,每轮检查一次标志,退出延迟就压到 1 秒以内。这是个很小的习惯,但在老设备上很值得坚持。
4.2 用 PostMessage 把数据递回界面线程
工作线程绝对不能直接碰控件。WinCE 上跨线程操作窗口句柄的后果比桌面更不可预测,轻则界面不刷新,重则直接崩。标准做法是自定义消息 + 堆上缓冲区:
#define WM_NET_DATA (WM_USER + 100) // lParam 指向堆上的数据块 #define WM_NET_STATE (WM_USER + 101) // wParam 0=断开 1=已连接 // 工作线程侧:拷贝一份数据,所有权交给界面线程 char* payload = new char[bodyLen]; memcpy(payload, recvBuf, bodyLen); if (!PostMessage(ctx->hWnd, WM_NET_DATA, bodyLen, (LPARAM)payload)) { delete[] payload; // 投递失败必须自己释放,否则就是内存泄漏 } // 界面线程侧:处理完立刻 delete case WM_NET_DATA: HandleFrame((const char*)lParam, (int)wParam); delete[] (char*)lParam; break;内存所有权这条链最容易断。PostMessage投递失败(窗口已销毁)时消息不会入队,缓冲区就没人管了;收到消息后忘记delete[],长时间运行下来内存会一直涨到设备扛不住。规矩是:谁分配谁负责投递成功,谁接收谁负责释放,投递失败的分支必须显式释放。
4.3 断线重连写成状态机,别写成 if-else 嵌套
连接状态就那么几个:未连接、连接中、已连接、退避等待。写成显式状态机,比一堆嵌套判断好维护得多,也方便在界面上显示当前状态。
| 状态 | 触发 | 动作 | 下一状态 |
|---|---|---|---|
| 未连接 | 启动 / 手动重连 | 置标志,启动连接 | 连接中 |
| 连接中 | connect 成功 | 启动心跳定时器 | 已连接 |
| 连接中 | connect 失败 | 退避计数 +1 | 退避等待 |
| 已连接 | recv 返回 0 或错误 | 关 socket,清定时器 | 退避等待 |
| 已连接 | 心跳超时(如 3 个周期无响应) | 主动关 socket | 退避等待 |
| 退避等待 | 退避时间到 | 退避计数清零或递增 | 连接中 |
心跳周期我一般给 10 到 15 秒,连续 3 个周期没有收到对端任何数据就判死。为什么不用SO_KEEPALIVE?WinCE 上这个选项的默认探测间隔很长(小时级),对现场那种“网线松了一下又插回去”的场景基本没用,必须自己做应用层心跳。
5. 避坑:WinCE TCP 最常翻车的五处
下面五条都是现场真出过的,按“现象 → 原因 → 解决”记,比看文档管用。
5.1 现象:开发板上跑得好好的,装到另一台设备上进程直接起不来
原因基本锁定在 Winsock 组件版本。开发板用的是调试镜像,做镜像的人把网络组件全勾上了;量产镜像为了省空间裁掉了 Winsock 2.2,只留 1.1。你的程序按 2.2 写的,WSAStartup返回 10092,或者进程加载时找不到对应的库文件。
解决分两步:先用 2.2 探针程序在那台设备上跑一遍,确认返回码;再决定是代码里降级到MAKEWORD(1,1)并去掉 2.x 专属 API,还是让做镜像的人把组件加回去。工程里最稳妥的写法是“先申请 2.2,失败再退 1.1”,退到 1.1 之后用一个全局标志记住,后面的代码路径避开getaddrinfo这类只在上限版本里存在的函数。
5.2 现象:日志里报文被截断,或者两条消息粘成一条,看起来像“丢包”
原因不是丢包,TCP 从来不保证消息边界。现场短报文高频发送,协议栈会把两条合并成一次recv返回,也会把一条拆成两次。很多人第一反应是加Sleep(50),那只是把概率降低,压力一大照样复现。
解决就是长度前缀分帧,把RecvAll读满 4 字节头、解析长度、再读满载荷,两步都不能少。顺带提一句,不要在recv里假设“一次就是一帧”,也不要用特殊字符做分隔符——业务数据里一旦出现同样的字节,解析立刻错位,而且这种错只在特定数据下才出现,极难复现。
5.3 现象:日志打印出来的中文全是乱码,或者编译报cannot convert类型错误
WinCE 工程默认_UNICODE,TCHAR是wchar_t,而inet_ntoa、recv缓冲区是char。混用的时候,_stprintf和sprintf、TEXT("...")和"..."之间一旦对不上,轻则乱码重则越界。
解决是把层次划清楚:网络层收发的数据一律char,编码按对端约定(UTF-8 或 GBK 都行,但要写进协议文档);界面和日志用TCHAR,中间统一走MultiByteToWideChar/WideCharToMultiByte,转换时显式传代码页,不要传CP_ACP图省事。另外inet_ntoa返回的是静态缓冲区,不可重入,多个线程同时打印会把地址串掉,需要保留就自己拷贝一份。
5.4 现象:设了SO_RCVTIMEO但recv还是卡死,或者选项完全不生效
WinCE 的 Winsock 对SO_RCVTIMEO/SO_SNDTIMEO的支持依版本和组件配置而定,有的镜像上直接被忽略,选项设了跟没设一样。这个问题特别隐蔽——代码在开发板上测着正常,换台设备就挂住。
解决是不依赖 socket 选项做超时,全部用select显式等可读。多一层select的代价很小,换来的是行为在每台设备上一致。如果业务需要更精细的控制,就切非阻塞 +select轮询,代价是 CPU 占用略高,老设备上要留意别把轮询间隔设成 0。
5.5 现象:退出程序时偶发卡死,或者第二次连接必失败
原因通常是关闭顺序错了:线程还在recv里等着,主线程已经把 socketclosesocket了,甚至已经WSACleanup。WinCE 上这种竞态比桌面更容易触发崩溃。
正确的收尾顺序固定成四步:先把bStop置 1;再用WaitForSingleObject等线程句柄,超时时间给够(比如 5 秒);线程真正退出后,再对每个 socket 调closesocket;最后全进程只调一次WSACleanup。WSACleanup调两次的后果是引用计数被减穿,后续所有 socket 操作都会失败,且报错信息完全不指向真正原因。
6. 联调:把问题切成两半,PC 端先做对端
WinCE 设备上能用的调试手段少得可怜,没有netstat、没有抓包工具,日志还得靠写文件。所以我的习惯是永远先在 PC 上搭一个对端,把链路问题和技术问题分开:PC 能连通说明网络和协议没问题,剩下的才在设备侧查。
6.1 一个够用的 PC 对端脚本
import socket, struct def serve(host="0.0.0.0", port=9000): srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((host, port)); srv.listen(4) print("listening", port) while True: conn, addr = srv.accept() print("from", addr) try: while True: head = conn.recv(4) # 先收长度头 if not head: break n = struct.unpack("!I", head)[0] # 网络序,和设备端 htonl 对齐 body = b"" while len(body) < n: body += conn.recv(n - len(body)) print("frame", n, body[:32]) conn.sendall(struct.pack("!I", 2) + b"OK") # 回一帧 finally: conn.close() serve()这段脚本的用处是验证帧格式对不对:如果设备端发来的n明显不合理,或者body一直收不满,问题一定在设备端的htonl、长度计算或者SendAll的累计逻辑上,跟网络无关。
6.2 日志里必须留这三个值
设备端日志记三样东西就够定位九成问题:每次recv的返回值和WSAGetLastError()、每次连接的建立与断开时刻、心跳的发送与回应时刻。有了这三样,配合 PC 端的netstat,能很快判断出是“根本没连上”“连上了但一个字节没发出去”还是“发出去但对面没回”。
我最深的一次教训,是花了整整两天排查一台设备“隔几小时断一次”的问题。最后发现是工作线程里recv成功后忘了清空接收缓冲区,数据一点点攒着,几个小时后内存不够,进程被系统干掉,重启后又恢复正常,表现得就像网络抖动。从那以后我每次写这类长连接,都强制在收发循环里加一个内存水位日志:每收 1000 帧打一次当前缓冲占用和进程可用内存,跑一晚上看曲线。希望帮到你。
本文还有配套的精品资源,点击获取