资讯动态

VC6.0 Socket编程实战:TCP服务器与客户端源码及避坑指南

发布时间:2026/10/24 2:37:13 来源:尧图企业网站定制
简介这份资源是面向VC6.0初学者的Socket编程入门示例包含一个可运行的服务器端与客户端配套工程帮助刚接触网络编程的开发者理解TCP通信的基本流程与代码组织方式。压缩包共27个文件约1.58MB以cpp源码、dsp/dsw工程文件、exe可执行程序为主另含pdb、obj、ilk等调试与编译中间文件以及一份txt说明工程结构完整可直接在VC6.0中打开编译运行。资源已有855人学习下载说明其在入门群体中具有一定参考价值。读者可从中获得一套现成的双端通信代码框架对照服务器端与客户端各自的工程配置、源码实现和调试产物快速弄清Socket创建、连接建立与数据收发的关键环节减少初次搭建环境时的摸索成本适合作为课程实验或自学练手的起点。1. VC6.0 里手搓 Socket 双端一份能直接编译的服务器加客户端源码包手上还有一堆 VC6.0 时代的老工控机、老检测设备要维护这类现场最尴尬的地方在于机器上只有 VC6.0没有现代 IDE也没有现成的网络库但需求偏偏是「让上位机把采集数据发出去、让另一台机器收回来」。这份 VC6.0 Socket 服务器加客户端源码包解决的就是这个场景——用最原始的 Winsock 接口在 VC6.0 里把 TCP 服务端和客户端各写一份能编译、能跑通、能改。它适合两类人一类是被老项目绑死、必须用 VC6.0 交差的从业者另一类是想搞明白 Socket 到底怎么从socket()一步步走到accept()和connect()的学习者。下面按「先立原理、再动手、最后说坑」的顺序拆开讲代码和参数都能直接抄。2. Winsock 初始化与工程配置VC6.0 下最容易翻车的第一步VC6.0 的 Socket 编程和现在差别最大的不是 API而是工程配置。现代 IDE 里#pragma comment(lib, ws2_32.lib)一行搞定VC6.0 虽然也认这行但很多人习惯去 Project Settings 里手动加库加错了路径或者加错了配置Debug 加了 Release 没加编译能过、链接报一堆unresolved external symbol。这一章把初始化和工程配置讲透后面两章的代码才有落脚点。2.1 为什么必须先调 WSAStartupWindows 下的 Socket 不是操作系统内核直接暴露的而是通过 Winsock DLL 提供。进程启动时这个 DLL 还没加载所以任何 Socket 调用之前必须先WSAStartup告诉系统「我要用哪个版本的 Winsock请把对应的 DLL 加载进来」。VC6.0 时代主流是 Winsock 2.2请求版本用MAKEWORD(2,2)。请求成功后进程退出前必须WSACleanup否则 DLL 引用计数不归零反复调试时会出现资源泄漏表现为跑几十次之后socket()开始返回INVALID_SOCKET这种玄学问题十有八九就是忘了清理。初始化代码我一般封装成一个函数服务端客户端共用#include winsock2.h #include stdio.h #pragma comment(lib, ws2_32.lib) // VC6.0 也认这行省去手动加库 // 返回值0 成功非 0 失败 int InitWinsock() { WSADATA wsaData; int ret WSAStartup(MAKEWORD(2, 2), wsaData); if (ret ! 0) { printf(WSAStartup failed: %d\n, ret); return ret; } // 校验实际协商到的版本防止系统只给了 1.1 if (LOBYTE(wsaData.wVersion) ! 2 || HIBYTE(wsaData.wVersion) ! 2) { printf(Winsock version mismatch\n); WSACleanup(); return -1; } return 0; }MAKEWORD(2,2)的低字节是主版本、高字节是次版本LOBYTE/HIBYTE就是取这两个字节做校验。很多示例代码省掉版本校验在只装了 Winsock 1.1 的老系统上会拿到一个「成功」但行为不一致的环境后面getaddrinfo之类的新 API 直接不可用。校验这一步不费事建议保留。2.2 VC6.0 工程里链接 ws2_32.lib 的两种方式第一种就是上面代码里的#pragma comment(lib, ws2_32.lib)编译器看到这行会自动把库塞进链接命令行最省心。第二种是菜单路径Project → Settings → Link 选项卡 → Object/library modules 里追加ws2_32.lib。第二种方式有个血泪经验VC6.0 的工程配置是分 Debug 和 Release 两套的你在 Debug 下加了库切到 Release 编译照样报链接错误因为 Release 的配置是独立的。所以要么两套都加要么干脆用#pragma让代码自己带库换机器换配置都不丢。还有一个 VC6.0 特有的坑新建工程时如果选了「Win32 Console Application」但没勾「A simple application」生成的是空工程没有main入口链接会报unresolved external symbol _main。这不是 Socket 的问题但新手经常误判成库没加对。确认工程里有main函数或者用「A simple application」模板起步。2.3 头文件顺序与 winsock2.h 冲突VC6.0 自带的老windows.h里可能间接包含winsock.h1.1 版本如果你又手动#include winsock2.h两个头文件的类型定义会打架报redefinition或conflicting types。标准做法是把winsock2.h放在windows.h之前或者定义WIN32_LEAN_AND_MEAN再包含windows.h把网络相关的旧头挡掉#define WIN32_LEAN_AND_MEAN #include windows.h #include winsock2.h顺序反了就会出现「明明代码没错却编译不过」的情况这类问题在 VC6.0 里特别常见因为它的头文件依赖关系比现代编译器松散报错位置经常指不到真正的原因。3. TCP 服务端实现从 socket 到 accept 的完整链路服务端的骨架是固定的五步socket()创建监听套接字、bind()绑定地址端口、listen()进入监听、accept()接受连接、recv()/send()收发数据。每一步的返回值都要判VC6.0 没有异常机制兜底一个INVALID_SOCKET没判后面全链路崩。这一章把服务端写完整参数逐个说清。3.1 创建、绑定与监听的三段式SOCKET CreateServer(int port) { SOCKET listenSock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listenSock INVALID_SOCKET) { printf(socket failed: %d\n, WSAGetLastError()); return INVALID_SOCKET; } // 允许端口复用避免重启服务端时 bind 报 10048 BOOL opt TRUE; setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR, (const char*)opt, sizeof(opt)); 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((u_short)port); if (bind(listenSock, (sockaddr*)addr, sizeof(addr)) SOCKET_ERROR) { printf(bind failed: %d\n, WSAGetLastError()); closesocket(listenSock); return INVALID_SOCKET; } if (listen(listenSock, SOMAXCONN) SOCKET_ERROR) { printf(listen failed: %d\n, WSAGetLastError()); closesocket(listenSock); return INVALID_SOCKET; } return listenSock; }AF_INET是 IPv4 地址族SOCK_STREAM是面向连接的字节流IPPROTO_TCP明确协议三个参数组合起来就是 TCP 套接字。INADDR_ANY表示不限网卡工控机上有多块网卡时这个最省事如果只想绑某块内网卡把htonl(INADDR_ANY)换成inet_addr(192.168.x.x)即可。htons/htonl是主机字节序转网络字节序端口和地址都必须转漏了会出现「bind 成功但连不上」的怪现象。SO_REUSEADDR这个选项在调试期价值极高——服务端崩了重启端口还处于 TIME_WAIT不加这个选项bind直接返回 10048加了就能立刻复用。3.2 accept 循环与单客户端收发void RunServer(SOCKET listenSock) { 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; while ((n recv(clientSock, buf, sizeof(buf) - 1, 0)) 0) { buf[n] \0; printf(recv %d bytes: %s\n, n, buf); send(clientSock, buf, n, 0); // 原样回显验证链路 } if (n 0) printf(client closed\n); else printf(recv error: %d\n, WSAGetLastError()); closesocket(clientSock); } }accept返回的是一个新的套接字专门对应这条连接监听套接字继续留着等下一个。recv返回 0 表示对端正常关闭返回SOCKET_ERROR才是出错这两种情况要分开处理混在一起会把正常断开误报成故障。buf留一个字节给\0sizeof(buf)-1就是这个道理。回显逻辑是为了验证链路通不通实际项目里把send换成业务处理即可。注意这个版本是单客户端串行处理一个客户端连着不放第二个连不上——这是有意的简化多客户端要用多线程或select放到最后一章说。4. TCP 客户端实现connect 超时与断线重连的处理客户端比服务端少一步bind和listen但多了connect的超时问题。默认的connect在目标不可达时会阻塞几十秒界面直接假死这在工控上位机里是致命的。这一章把客户端写完整重点讲connect超时怎么控、断线怎么重连。4.1 connect 与收发主循环SOCKET ConnectServer(const char* ip, int port) { SOCKET sock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sock INVALID_SOCKET) return INVALID_SOCKET; sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_port htons((u_short)port); addr.sin_addr.s_addr inet_addr(ip); // 仅支持点分十进制 if (connect(sock, (sockaddr*)addr, sizeof(addr)) SOCKET_ERROR) { printf(connect failed: %d\n, WSAGetLastError()); closesocket(sock); return INVALID_SOCKET; } return sock; }inet_addr只接受192.168.1.10这种点分十进制字符串传域名会返回INADDR_NONE需要域名解析得用gethostbyname。connect成功返回 0失败返回SOCKET_ERROR用WSAGetLastError()取具体错误码10061 是连接被拒绝服务端没开10060 是超时网络不通或防火墙拦了这两个码在现场排查时最常遇到记住能省不少时间。4.2 用非阻塞 connect 加 select 控制超时默认connect的超时由系统协议栈决定改不了。要自己控超时标准做法是把套接字设成非阻塞connect立刻返回WSAEWOULDBLOCK然后用select等可写事件等到就是连上了等不到就是超时SOCKET ConnectWithTimeout(const char* ip, int port, int timeoutMs) { SOCKET sock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sock INVALID_SOCKET) return INVALID_SOCKET; // 设为非阻塞 u_long mode 1; ioctlsocket(sock, FIONBIO, mode); sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_port htons((u_short)port); addr.sin_addr.s_addr inet_addr(ip); connect(sock, (sockaddr*)addr, sizeof(addr)); // 预期返回 WSAEWOULDBLOCK fd_set writeSet, errSet; FD_ZERO(writeSet); FD_SET(sock, writeSet); FD_ZERO(errSet); FD_SET(sock, errSet); timeval tv; tv.tv_sec timeoutMs / 1000; tv.tv_usec (timeoutMs % 1000) * 1000; int ret select(0, NULL, writeSet, errSet, tv); if (ret 0) { closesocket(sock); return INVALID_SOCKET; // 超时或出错 } if (FD_ISSET(sock, errSet)) { closesocket(sock); return INVALID_SOCKET; // 连接被拒 } // 恢复阻塞模式后续 recv/send 按阻塞用 mode 0; ioctlsocket(sock, FIONBIO, mode); return sock; }select的第一个参数在 Windows 下被忽略填 0 即可这是 Windows 和 Linux 的差异Linux 要填最大 fd1。writeSet可写说明连接建立成功errSet有事件说明连接失败两个集合都要检查只查可写会把「连接被拒」误判成成功。超时后必须closesocket否则套接字泄漏。连上之后把非阻塞改回阻塞后面收发逻辑就不用处理WSAEWOULDBLOCK了简单直接。4.3 断线检测与重连节奏客户端跑在现场网络抖动、服务端重启都会断线。recv返回 0 或SOCKET_ERROR就是断线信号重连不能死循环猛连会把服务端打挂。常见做法是退避重连第一次等 1 秒第二次 2 秒第三次 4 秒封顶 30 秒连上后重置计数。这个节奏在工控场景够用既不会长时间失联也不会在服务端故障时雪崩。int backoff 1000; while (1) { SOCKET s ConnectWithTimeout(192.168.1.10, 9000, 3000); if (s ! INVALID_SOCKET) { backoff 1000; // 连上就重置 DoSession(s); // 收发主循环返回即断线 closesocket(s); } Sleep(backoff); backoff (backoff * 2 30000) ? 30000 : backoff * 2; }Sleep的单位是毫秒VC6.0 里在windows.h中声明。DoSession里就是send/recv循环返回意味着连接断了外层负责重连。这套结构比「断了就 while 猛连」稳得多现场跑几天不掉线是常态。5. 避坑排查VC6.0 Socket 调试里最常见的五个翻车点这一章全是踩过的坑每条按「现象 → 原因 → 解决」写遇到对得上的直接照方抓药。5.1 编译过、链接报 unresolved external symbol现象代码里socket、bind全部标红未定义或者链接阶段报_socket12之类的符号找不到。原因ws2_32.lib没链上或者只在一个配置里链了。解决优先用#pragma comment(lib, ws2_32.lib)让代码自带库如果坚持手动加Debug 和 Release 两套配置都要加加完重新 Build 而不是只 Compile。5.2 bind 返回 10048现象服务端第一次跑正常关掉再启动就bind failed: 10048。原因上一次的连接还处于 TIME_WAIT端口没释放。解决bind之前加SO_REUSEADDR代码见 3.1如果还不行换端口验证是不是别的进程占着用netstat -ano | findstr 端口号查占用进程。5.3 客户端连不上但服务端显示在监听现象服务端listen成功客户端connect返回 10061 或 10060。原因10061 是服务端没在这个端口监听端口填错、绑了 127.0.0.1 只允许本机10060 是网络层不通防火墙、网段不对。解决先用telnet 目标IP 端口验证端口通不通不通查防火墙服务端绑INADDR_ANY而不是127.0.0.1否则跨机连不上。5.4 recv 收到乱码或半包现象发一段中文或长数据收到的内容缺一截或者乱码。原因TCP 是字节流没有消息边界recv一次返回多少取决于网络缓冲可能只收到半条消息中文按多字节存截断在半个汉字上就乱码。解决应用层自己定协议常见做法是「4 字节长度头 数据体」收方先收 4 字节算长度再循环收到够为止。别指望一次recv收全。5.5 程序退出后端口仍被占用现象进程已经关了netstat还能看到端口在 LISTENING。原因closesocket没调或者WSACleanup没调套接字句柄没释放。解决每个socket/accept返回的句柄都要配对closesocket程序退出前调WSACleanup。调试期频繁启停的程序尤其要注意漏一次就多占一个句柄跑久了资源耗尽。6. 从单连接走向多客户端select 模型与压测验证单客户端串行版本只适合验证链路真上现场至少要能同时接几台设备。VC6.0 没有 C11 的线程库多线程要用_beginthreadex但线程一多共享数据要加锁复杂度陡增。更轻的方案是select多路复用一个线程管所有套接字哪个可读处理哪个没有锁竞争代码量也可控。核心是把监听套接字和所有客户端套接字都塞进fd_setselect返回后遍历检查。fd_set readSet; FD_ZERO(readSet); FD_SET(listenSock, readSet); SOCKET maxSock listenSock; for (int i 0; i clientCount; i) { FD_SET(clients[i], readSet); if (clients[i] maxSock) maxSock clients[i]; } // Windows 下第一个参数忽略填 0 也行 int ret select(0, readSet, NULL, NULL, NULL); if (ret 0) { if (FD_ISSET(listenSock, readSet)) { // 有新连接accept 后加入 clients 数组 } for (int i 0; i clientCount; i) { if (FD_ISSET(clients[i], readSet)) { // 该客户端有数据recv 处理 } } }fd_set在 Winsock 里是个结构体数组默认最多 64 个套接字FD_SETSIZE够一般工控场景用要更多得自己改宏或者换 IOCP那是另一个量级的工作。select的最后一个参数传NULL表示无限等待实际项目里给个几百毫秒超时方便主循环做别的事。验证这套代码是否真的能扛多客户端我一般用两个手段一是写个简单的压测客户端开 20 个线程各连一次、各发 1000 条消息看服务端有没有丢连接、有没有内存涨二是用netstat -ano观察连接数是否和预期一致跑完关掉客户端再看连接是否正常回收。压测客户端本身也是用第 4 章的ConnectWithTimeout改的把重连逻辑去掉、循环发数据即可。有个细节值得单独说select返回后fd_set会被内核改写只保留有事件的套接字所以每轮循环前必须重新FD_ZERO和FD_SET不能复用上一轮的集合。这个坑我当年调了大半天现象是「第一轮正常第二轮开始漏事件」原因就是集合没重建。从那以后我每次写select循环都强制在循环体开头重建fd_set再检查事件这个习惯一直保留到现在。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑