VC6.0 Socket编程实战:从Winsock初始化到粘包处理

发布时间:2026/10/24 2:36:43
VC6.0 Socket编程实战:从Winsock初始化到粘包处理 简介这份资源是面向VC6.0初学者的Socket编程入门示例包含一个可运行的服务器端与客户端配套工程帮助刚接触网络编程的开发者理解TCP通信的基本流程与代码组织方式。压缩包共27个文件约1.58MB以cpp源码、dsp/dsw工程文件、exe可执行程序为主另含pdb、obj、pch等编译中间文件与调试信息以及一份txt说明工程结构完整可直接在VC6.0中打开编译运行。资源已有855人学习下载说明其在入门场景中具有一定参考价值。读者可从中获得一套最小可用的Socket通信代码骨架对照服务器端与客户端两份工程观察连接建立、数据收发与工程配置的对应关系并借助现成可执行文件快速验证效果为后续深入网络编程打下基础。1. VC6.0 里那对 Socket 程序为什么老项目还在用它做联调底座接手一套十几年前的工控上位机时你会发现它既没有 CMake也没有 NuGet连std::thread都用不了但服务器和客户端之间照样要收发数据。这时候 VC6.0 Socket 服务端客户端这套最原始的 Winsock 代码反而是最省事的联调底座一个.dsw工程编译出 exe双击就能起服务另一台机器跑客户端连上去收发链路当场验证。它解决的不是“高并发”问题而是“在没有现代工具链的环境里把 TCP 通信跑通并且能讲清楚每一行在干什么”。适合两类人一是要维护或复刻老工控、老教学项目、老内网工具的从业者二是想搞明白阻塞 socket、WSAStartup、accept这些底层动作到底怎么串起来的新手。下面按“能编译、能连上、能排错”的顺序拆。2. 先把 Winsock 的启动与清理讲透两个函数决定程序能不能跑VC6.0 下的 Socket 编程不是包含一个头文件就完事它依赖ws2_32.lib这个导入库并且进程启动时必须先做 Winsock 版本协商。很多新手把代码抄过去编译过了一运行就返回10093未初始化问题几乎都出在这一步。2.1 工程配置链接库和头文件的正确挂法VC6.0 的工程设置藏在Project → Settings里和现在 IDE 的逻辑不一样必须手动挂库。常见做法是两步都做避免换机器后漏配// stdafx.h 或主 cpp 顶部顺序不要乱 #include winsock2.h // 必须早于 windows.h否则会撞定义 #include ws2tcpip.h // inet_pton / getaddrinfo 需要 #pragma comment(lib, ws2_32.lib) // 让链接器自动找库省去手动配置逻辑说明winsock2.h一定要放在windows.h之前VC6.0 自带的windows.h里含老版winsock.h顺序反了会出现大量重定义错误。#pragma comment是 MSVC 特有的写法作用等同于在工程设置里加ws2_32.lib好处是代码自带依赖换机器不容易翻车。参数上没什么可调的库名写死为ws2_32.lib注意不是wsock32.lib那是 1.1 版功能受限。如果坚持走工程设置路径是Project → Settings → Link → Object/library modules在末尾追加ws2_32.lib多个库用空格隔开。两种方式选一种即可同时配也不冲突。2.2 WSAStartup 与 WSACleanup版本协商的真实含义WSAStartup不是“初始化网络”这么笼统它做的是向系统申请一个指定版本的 Winsock 实现并把能力集写回你传入的结构体。服务端和客户端都要调用且必须在任何 socket 函数之前。WSADATA wsaData; WORD wVersion MAKEWORD(2, 2); // 请求 2.2 版 int nRet WSAStartup(wVersion, wsaData); if (nRet ! 0) { printf(WSAStartup failed: %d\n, nRet); return -1; } // 校验系统实际给的版本防止降级到 1.1 if (LOBYTE(wsaData.wVersion) ! 2 || HIBYTE(wsaData.wVersion) ! 2) { printf(Winsock version mismatch\n); WSACleanup(); return -1; } // ... 业务代码 ... WSACleanup(); // 进程退出前调用释放 Winsock 资源逻辑说明MAKEWORD(2,2)把主次版本压成一个 WORD低字节是主版本、高字节是次版本所以校验时用LOBYTE取主、HIBYTE取次。返回值nRet非零就是失败常见错误码10091版本不支持、10093未初始化通常是忘了调或调晚了。WSACleanup要和WSAStartup成对出现服务端长期运行时一般放在退出分支不要在每个连接里调否则会把整个进程的 Winsock 引用计数清掉。参数上唯一要改的是版本号老系统上如果 2.2 协商失败可以退到MAKEWORD(2,0)但功能会少一些。3. 服务端主循环bind、listen、accept 三步的参数字眼服务端的骨架就三件事把 socket 绑到地址端口、进入监听、循环接受连接。VC6.0 时代没有std::thread多客户端一般用“每连接一线程”或者干脆单连接阻塞处理选哪种取决于你的场景。3.1 创建、绑定与监听每个参数都别照抄SOCKET listenSock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listenSock INVALID_SOCKET) { printf(socket failed: %d\n, WSAGetLastError()); WSACleanup(); return -1; } // 允许端口复用避免重启时 bind 报 10048 BOOL bReuse TRUE; setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR, (const char*)bReuse, sizeof(bReuse)); sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_addr.s_addr htonl(INADDR_ANY); // 监听所有网卡 addr.sin_port htons(9527); // 端口按项目改 if (bind(listenSock, (sockaddr*)addr, sizeof(addr)) SOCKET_ERROR) { printf(bind failed: %d\n, WSAGetLastError()); closesocket(listenSock); WSACleanup(); return -1; } if (listen(listenSock, SOMAXCONN) SOCKET_ERROR) { printf(listen failed: %d\n, WSAGetLastError()); closesocket(listenSock); WSACleanup(); return -1; }逻辑说明socket三个参数分别是地址族、套接字类型、协议TCP 场景固定为AF_INET / SOCK_STREAM / IPPROTO_TCP第三个参数写 0 也能让系统推断但显式写更清楚。SO_REUSEADDR是血泪经验服务端调试时反复启停不设这个选项bind经常报10048地址已占用设了之后 TIME_WAIT 状态的端口也能复用。sin_addr.s_addr用INADDR_ANY表示监听本机所有网卡如果只想本机回环测试改成inet_addr(127.0.0.1)。端口用htons转网络字节序这是新手最容易漏的一步漏了端口会变成一个莫名其妙的数字。listen的第二个参数是等待队列长度SOMAXCONN让系统取最大值小项目直接用它就行。3.2 accept 循环与单连接处理阻塞模型下的取舍while (1) { sockaddr_in clientAddr; int addrLen sizeof(clientAddr); SOCKET clientSock accept(listenSock, (sockaddr*)clientAddr, addrLen); if (clientSock INVALID_SOCKET) { printf(accept failed: %d\n, WSAGetLastError()); continue; // 单次失败不退出继续等下一个 } printf(client connected: %s:%d\n, inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port)); char buf[1024]; int n recv(clientSock, buf, sizeof(buf) - 1, 0); if (n 0) { buf[n] \0; printf(recv: %s\n, buf); send(clientSock, ACK, 3, 0); // 回一个确认 } closesocket(clientSock); // 处理完就关简单但只支持串行 }逻辑说明accept返回的是一个新的已连接 socket和监听 socket 是两个东西处理完必须closesocket新 socket监听 socket 留着继续accept。addrLen是值-结果参数传入结构体大小返回实际填充大小必须初始化。recv的第四个参数 0 表示默认阻塞行为返回 0 代表对端正常关闭返回SOCKET_ERROR要用WSAGetLastError看具体原因。这段代码是串行模型一个客户端处理完才接下一个适合联调和小工具要支持多客户端常见做法是accept之后CreateThread起一个线程跑收发或者用select做非阻塞轮询VC6.0 下前者更直观。注意inet_ntoa返回的是静态缓冲区多线程里连续调用会互相覆盖调试打印够用正式逻辑里要拷出来。4. 客户端连接与收发连接超时和粘包是两个绕不开的点客户端比服务端简单但有两个坑几乎人人踩connect在阻塞模式下可能卡很久以及 TCP 是字节流、没有消息边界。4.1 connect 与阻塞超时别让界面假死SOCKET sock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in serverAddr; memset(serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family AF_INET; serverAddr.sin_port htons(9527); serverAddr.sin_addr.s_addr inet_addr(192.168.1.10); // 换成服务端 IP // 先设非阻塞connect 后立刻返回再用 select 等可写 u_long mode 1; ioctlsocket(sock, FIONBIO, mode); int ret connect(sock, (sockaddr*)serverAddr, sizeof(serverAddr)); if (ret SOCKET_ERROR) { int err WSAGetLastError(); if (err ! WSAEWOULDBLOCK) { // 不是“正在连接”就是真失败 printf(connect failed: %d\n, err); closesocket(sock); return -1; } fd_set writeSet; FD_ZERO(writeSet); FD_SET(sock, writeSet); timeval tv { 3, 0 }; // 最多等 3 秒 if (select(0, NULL, writeSet, NULL, tv) 0) { printf(connect timeout\n); closesocket(sock); return -1; } } mode 0; // 恢复阻塞模式后续收发按阻塞处理 ioctlsocket(sock, FIONBIO, mode);逻辑说明阻塞connect在目标不可达时可能等几十秒界面直接假死。常见做法是先ioctlsocket设非阻塞connect返回WSAEWOULDBLOCK属正常表示连接进行中再用select监听可写事件timeval控制超时。select第一个参数在 Windows 下被忽略传 0 即可。连接成功后把模式改回阻塞后面recv逻辑简单。参数上timeval的tv_sec按网络环境调内网 3 秒够跨网段可以放到 5 到 10 秒。inet_addr只接受点分十进制字符串传域名要先gethostbyname这是另一个话题。4.2 收发与粘包长度前缀是最省心的解法// 发送先发 4 字节长度网络字节序再发正文 const char* msg hello server; int len (int)strlen(msg); int netLen htonl(len); send(sock, (const char*)netLen, 4, 0); send(sock, msg, len, 0); // 接收先收满 4 字节长度再按长度收正文 int netLen2 0; int got 0; while (got 4) { int n recv(sock, ((char*)netLen2) got, 4 - got, 0); if (n 0) { /* 连接断开或出错跳出 */ break; } got n; } int bodyLen ntohl(netLen2); char* body new char[bodyLen 1]; got 0; while (got bodyLen) { int n recv(sock, body got, bodyLen - got, 0); if (n 0) break; got n; } body[bodyLen] \0;逻辑说明TCP 不保证一次send对应一次recv两次发送可能被合并一次发送也可能被拆开这就是粘包/拆包。长度前缀法的核心是“先定边界再读内容”4 字节长度用htonl转网络序接收端用ntohl转回来。两个while循环是关键recv返回值小于请求长度时必须继续读不能假设一次读满。参数上长度字段用 4 字节int能表达 2GB小项目够用如果协议要跨语言注意对端也用同样的字节序和字段宽度。new出来的body记得delete[]VC6.0 没有智能指针泄漏全靠自觉。5. 避坑与排查五个让程序“看着能跑其实不能用”的细节5.1 编译报 winsock 重定义现象一堆redefinition错误指向fd_set、timeval之类。原因windows.h先于winsock2.h被包含老winsock.h已经进来。解决把#include winsock2.h提到所有头文件最前面或者在stdafx.h里定义WIN32_LEAN_AND_MEAN再包含windows.h减少它带入的网络相关头。5.2 服务端起不来bind 返回 10048现象第一次运行正常关掉再开就bind failed: 10048。原因上一个进程的 socket 处于 TIME_WAIT端口没释放。解决加SO_REUSEADDR见 3.1或者换端口调试。注意SO_REUSEADDR在 Windows 上的语义和某些 Unix 不同它允许绑定到已被占用但处于 TIME_WAIT 的地址不要指望它能让你同时起两个监听同端口的进程。5.3 客户端连不上错误码 10061现象connect立刻返回10061连接被拒绝。原因服务端没起、端口写错、或者服务端只监听了127.0.0.1而客户端从另一台机器连。解决先在服务端机器上用netstat -an | findstr 9527看监听地址0.0.0.0:9527才是全网卡再看防火墙是否放行。VC6.0 项目本身不带防火墙配置这一步经常被忽略。5.4 收到数据但打印是乱码或截断现象recv返回正数打印出来后半截没了或者带乱码。原因没按协议长度读满或者发送端字符串没带结束符、接收端直接当 C 字符串用。解决按 4.2 的长度前缀循环读满接收缓冲区末尾手动补\0打印前确认bodyLen和实际内容一致。5.5 程序退出时崩溃在 WSACleanup现象关窗口时偶尔崩调用栈指向WSACleanup。原因还有 socket 没closesocket或者工作线程还在跑就清了 Winsock。解决退出顺序固定为“先停线程、再关所有 socket、最后WSACleanup”每个socket/accept返回的句柄都要有对应的closesocket用计数器或日志核对一遍。6. 进阶把这对程序改成能压测的联调工具原始代码跑通之后真正有价值的是让它能承受反复连接和异常断开用来验证对端行为。我一般会加三样东西连接计数、心跳、以及一个可配置的收发循环。改造点做法观察指标连接计数accept后原子自增断开自减当前连接数是否回落心跳客户端每 5 秒发一个 1 字节包服务端回 1 字节长时间空闲后是否被中间设备断开异常断开客户端closesocket前不发 FIN直接杀进程服务端recv返回 0 还是SOCKET_ERROR心跳这块有个细节VC6.0 没有std::chrono计时用GetTickCount()它返回毫秒但约 49 天回绕做长稳测试时要用差值比较而不是绝对值。异常断开测试能暴露一个常见误判——很多人把recv返回 0 和返回SOCKET_ERROR当成一回事实际上 0 是对端正常关闭SOCKET_ERROR才要查WSAGetLastError两者在日志里要分开记否则排障时根本分不清是谁先动的手。压测时把客户端循环起 200 个进程服务端用串行模型会明显排队这时候换成每连接一线程观察CreateThread失败返回和句柄泄漏。VC6.0 的调试器看线程不方便我习惯在accept和线程退出处各打一行带GetCurrentThreadId()的日志跑完对比数量。这套改造不追求性能数字追求的是“断开、重连、半包、超时”四种情况都能稳定复现联调时对端一有异常就能立刻定位到是协议层还是连接层。从那以后我每次拿到这类老 Socket 工程都强制先跑一遍“起服务、连、发、断、再连”的闭环再动业务代码——顺序反了后面所有排错都是在猜。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询