首页 / 资讯中心 / 文章详情

VC6.0 Socket编程实战:从C/S骨架到二次开发改造

VC6.0 Socket编程实战:从C/S骨架到二次开发改造 ★ FEATURED ARTICLE
简介这份资源是面向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 工程服务器和客户端两个 exe一个负责收现场设备的数据一个负责把数据推到监控界面。机器不能换系统编译器也不能升级只能在这套老环境里改。这类需求其实不少见教学实验、老设备维护、内网小工具VC6.0 加 Winsock 的组合依然是能跑就行的最优解。这份资源给的就是一套完整的服务器加客户端 Socket 编程示例服务端负责监听端口、接受连接、收发数据客户端负责发起连接、发送请求、接收回包。适合两类人一是刚学网络编程、想找一个能直接编译运行的 C/S 骨架的新手二是手里有老工程、需要一份干净的 Socket 参考实现来对照改造的熟手。下面按「能干什么、怎么跑、怎么改」的顺序拆开讲。2. 工程结构与 Winsock 初始化两个 exe 是怎么搭起来的2.1 目录与文件构成这类 VC6.0 的 Socket 示例工程结构通常很朴素一个工作区.dsw下挂两个工程.dsp分别编译出服务端和客户端。常见做法是SocketDemo/ ├── Server/ │ ├── Server.dsp # 服务端工程文件 │ ├── Server.cpp # 主流程初始化、监听、accept、收发 │ ├── Server.h │ └── StdAfx.cpp/.h # 预编译头 ├── Client/ │ ├── Client.dsp # 客户端工程文件 │ ├── Client.cpp # 主流程初始化、connect、收发 │ ├── Client.h │ └── StdAfx.cpp/.h └── SocketDemo.dsw # 工作区同时包含两个工程服务端和客户端各自独立编译互不依赖这样调试时可以同时开两个进程一个监听一个连接链路最直观。工程里一般还会带一个ws2_32.lib的链接配置这是 Winsock 2 的导入库少了它链接阶段会报一堆unresolved external symbol。2.2 Winsock 的启动与清理Windows 下用 Socket 必须先调WSAStartup这是整个程序的第一道门槛也是最容易被新手忽略的一步。典型写法#include winsock2.h #pragma comment(lib, ws2_32.lib) WSADATA wsaData; int ret WSAStartup(MAKEWORD(2, 2), wsaData); if (ret ! 0) { printf(WSAStartup failed: %d\n, ret); return -1; } // ... 后续 socket 操作 WSACleanup(); // 程序退出前必须调用MAKEWORD(2, 2)表示请求 Winsock 2.2 版本wsaData回填实际协商到的版本信息。WSAStartup失败一般意味着系统网络组件异常或者版本不匹配返回值可以直接查错误码。WSACleanup要和WSAStartup成对出现虽然进程退出时系统会兜底但在长期运行的服务端里不清理会累积资源句柄属于血泪经验级别的细节。提示VC6.0 默认的windows.h里可能已经间接包含了旧版winsock.h如果同时包含winsock2.h会冲突。常见做法是把#include winsock2.h放在所有头文件最前面或者在工程设置里定义WIN32_LEAN_AND_MEAN。2.3 服务端的 socket 生命周期服务端的核心是四步创建 socket、绑定地址、监听、接受连接。代码骨架大致如下SOCKET listenSock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listenSock INVALID_SOCKET) { printf(socket failed: %d\n, WSAGetLastError()); WSACleanup(); return -1; } 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(8888); // 端口号按需改 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; }AF_INET是 IPv4 地址族SOCK_STREAM是面向连接的流式套接字IPPROTO_TCP明确走 TCP 协议。INADDR_ANY表示绑定本机所有网卡地址如果只想监听某个内网 IP把htonl(INADDR_ANY)换成inet_addr(192.168.x.x)即可。htons(8888)把主机字节序转成网络字节序端口号选 1024 以上避免权限问题。listen的第二个参数是等待队列长度SOMAXCONN让系统取最大值小规模场景够用。接受连接用accept它会阻塞直到有客户端连进来sockaddr_in clientAddr; int clientLen sizeof(clientAddr); SOCKET clientSock accept(listenSock, (sockaddr*)clientAddr, clientLen); if (clientSock INVALID_SOCKET) { printf(accept failed: %d\n, WSAGetLastError()); // 继续循环或退出视业务而定 } printf(client connected: %s:%d\n, inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));accept返回一个新的 socket 专门和这个客户端通信原来的listenSock继续监听。inet_ntoa把网络字节序的 IP 转成点分字符串ntohs把端口转回主机序这两个函数在打印日志时很常用。注意inet_ntoa返回的是静态缓冲区多线程下不能直接共享单线程示例无所谓。2.4 客户端的连接流程客户端比服务端少一步监听核心是connectSOCKET sock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sock INVALID_SOCKET) { /* 错误处理 */ } sockaddr_in serverAddr; memset(serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family AF_INET; serverAddr.sin_port htons(8888); serverAddr.sin_addr.s_addr inet_addr(127.0.0.1); // 服务端 IP if (connect(sock, (sockaddr*)serverAddr, sizeof(serverAddr)) SOCKET_ERROR) { printf(connect failed: %d\n, WSAGetLastError()); closesocket(sock); WSACleanup(); return -1; }inet_addr把点分字符串转成网络字节序整数本机测试用127.0.0.1跨机测试换成服务端实际 IP。connect失败最常见的原因是服务端没启动、端口写错、或者防火墙拦了WSAGetLastError()返回10061连接被拒绝基本就是服务端没在监听。3. 收发数据与阻塞模型一次请求从发出到回包的完整链路3.1 send 和 recv 的基本用法TCP 是字节流没有消息边界send和recv的返回值必须认真对待。服务端收到连接后典型循环是char buf[1024]; while (true) { int n recv(clientSock, buf, sizeof(buf) - 1, 0); if (n 0) { printf(client closed\n); break; // 对端正常关闭 } else if (n SOCKET_ERROR) { printf(recv failed: %d\n, WSAGetLastError()); break; } buf[n] \0; printf(recv: %s\n, buf); const char* reply server received; send(clientSock, reply, (int)strlen(reply), 0); } closesocket(clientSock);recv返回 0 表示对端调用了closesocket或shutdown这是正常关闭信号不是错误。返回SOCKET_ERROR才是真错误。sizeof(buf) - 1留一个字节给\0避免打印越界。send的返回值是实际发送的字节数小数据量一般一次发完大数据量要循环发这是后面二次开发要重点处理的地方。客户端对应地发一条收一条const char* msg hello server; send(sock, msg, (int)strlen(msg), 0); char buf[1024]; int n recv(sock, buf, sizeof(buf) - 1, 0); if (n 0) { buf[n] \0; printf(reply: %s\n, buf); }3.2 阻塞模型的含义与局限默认创建的 socket 是阻塞模式accept、connect、recv、send在条件不满足时都会卡住当前线程。这套示例工程基本都跑在阻塞模型上优点是代码直白、逻辑线性缺点是服务端一次只能服务一个客户端——accept返回后进入收发循环第二个客户端连进来只能排队等。教学和小工具场景够用但要做多客户端就得改。常见做法有三种一是每接受一个连接就CreateThread开一个线程专门处理该客户端二是用select做 I/O 多路复用单线程管理多个 socket三是改成非阻塞 socket 加轮询。VC6.0 环境下第一种最省事第二种最稳。下面给一个线程版的骨架DWORD WINAPI ClientThread(LPVOID param) { SOCKET clientSock (SOCKET)param; char buf[1024]; while (true) { int n recv(clientSock, buf, sizeof(buf) - 1, 0); if (n 0) break; buf[n] \0; send(clientSock, buf, n, 0); // 原样回显 } closesocket(clientSock); return 0; } // accept 循环里 SOCKET clientSock accept(listenSock, NULL, NULL); if (clientSock ! INVALID_SOCKET) { CreateThread(NULL, 0, ClientThread, (LPVOID)clientSock, 0, NULL); }CreateThread的第三个参数是线程函数第四个参数把客户端 socket 传进去。注意 socket 句柄转LPVOID再转回来在 32 位下没问题64 位下要小心不过 VC6.0 只有 32 位不用纠结。线程函数里recv返回 0 或错误就退出循环并关闭 socket避免句柄泄漏。3.3 粘包与消息边界TCP 不保证一次recv对应一次send客户端连发两条消息服务端可能一次收到两条拼在一起也可能一条分两次收到这就是粘包。示例工程如果只做单条短消息演示通常不处理一旦上真实业务必须自己定边界。常见做法是「长度前缀 内容」// 发送先发 4 字节长度再发内容 int len (int)strlen(payload); send(sock, (char*)len, 4, 0); send(sock, payload, len, 0); // 接收先收 4 字节长度再按长度收内容 int len 0; recv(sock, (char*)len, 4, MSG_WAITALL); char* body new char[len 1]; recv(sock, body, len, MSG_WAITALL); body[len] \0;MSG_WAITALL让recv尽量收满指定字节数再返回减少循环次数但它不是万能的连接异常时仍可能提前返回所以严格实现还要检查返回值。长度字段本身也有字节序问题跨平台通信要统一用htonl/ntohl本机自测可以偷懒。注意send返回的字节数可能小于请求长度尤其是发送大块数据时。正确做法是循环发送直到全部发完示例里为了简洁常常省略二次开发时务必补上。4. 编译运行与联调VC6.0 下把两个 exe 跑起来4.1 工程配置要点VC6.0 打开.dsw工作区后先确认两个工程的链接设置里都有ws2_32.lib。路径是 Project → Settings → Link 选项卡 → Object/library modules追加ws2_32.lib。如果代码里用了#pragma comment(lib, ws2_32.lib)这一步可以省但老工程经常两者都没有链接报错就是从这里来的。编译顺序上服务端和客户端互不依赖可以分别 Build。Build 菜单选 Set Active Project 切换当前工程或者直接 F7 全部编译。输出目录默认在工程下的Debug或Release两个 exe 分别生成在各自目录里。4.2 本机联调步骤联调最省事的方式是本机回环先运行服务端 exe看到「listening on 8888」之类的日志说明绑定成功。再运行客户端 exe连接127.0.0.1:8888。服务端打印客户端 IP 和端口客户端发送消息服务端回包两边日志对得上就通了。如果服务端启动就报bind failed: 10048说明端口被占用换个端口或者用netstat -ano | findstr 8888查一下谁占着。如果客户端报connect failed: 10061先确认服务端在跑、端口一致、没有防火墙拦截。跨机测试时把客户端里的127.0.0.1换成服务端内网 IP服务端绑定INADDR_ANY就能接受外部连接。4.3 调试技巧VC6.0 的调试器虽然老但断点、单步、查看变量都够用。联调时常见做法是两个工程都设断点先启动服务端到accept前再启动客户端到connect后然后服务端放行观察accept返回值。WSAGetLastError()的值可以在 Watch 窗口直接看比打印日志快。日志方面示例工程多用printf控制台窗口一闪而过是常见翻车点。解决办法是在main末尾加system(pause)或者在工程设置里把子系统改成 Console。MFC 对话框版的工程则用MessageBox或OutputDebugString配合 DebugView 看输出。5. 避坑与常见问题那些年 Socket 联调翻过的车5.1 链接报错 unresolved external symbol现象编译通过链接阶段报__imp__WSAStartup8之类的未解析符号。原因没有链接ws2_32.lib。解决在工程 Link 设置里加库或者代码里加#pragma comment(lib, ws2_32.lib)。还有一种情况是头文件包含了旧版winsock.h函数声明对不上把winsock2.h提到最前面即可。5.2 bind 失败错误码 10048现象服务端启动即退出日志显示bind failed: 10048。原因端口已被其他进程占用或者上一次运行的服务端没退干净socket 处于TIME_WAIT。解决换端口或者等几十秒让系统回收开发阶段可以在bind前设SO_REUSEADDRint opt 1; setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR, (char*)opt, sizeof(opt));SO_REUSEADDR允许绑定处于TIME_WAIT的地址调试时很实用生产环境要不要开看具体场景。5.3 客户端连不上但服务端在跑现象服务端日志正常客户端connect返回 10061 或超时。原因IP 或端口写错、防火墙拦截、服务端绑定的是127.0.0.1而不是INADDR_ANY。解决逐项核对先用telnet 服务端IP 端口测连通性再看服务端绑定地址。跨网段时还要确认路由可达。5.4 recv 返回 0 被当成错误现象客户端正常关闭后服务端日志刷一堆错误或者死循环。原因把recv返回 0 当成了SOCKET_ERROR处理。解决明确区分0 是对端正常关闭应该break出循环并closesocket只有SOCKET_ERROR才查WSAGetLastError()。这个坑几乎每个新手都会踩一次。5.5 中文乱码现象发送中文接收端显示乱码。原因VC6.0 默认多字节字符集源码文件编码和运行环境不一致或者发送接收两端编码不统一。解决统一用 GBK 或者统一转 UTF-8 再发跨平台通信建议在协议层约定编码别依赖系统默认。6. 二次开发改造从示例骨架到能用的 C/S 工具拿到这套示例直接上业务通常不够用得按实际需求补几块。第一块是消息协议把裸字符串换成「长度前缀 命令字 载荷」的结构命令字区分登录、心跳、数据上报载荷按业务定义这样服务端才能路由。第二块是多客户端管理用线程或者select把accept循环和收发解耦维护一个客户端列表断线时清理。第三块是异常处理send/recv全部改成循环收发处理部分发送和超时给 socket 设SO_RCVTIMEO避免永久阻塞。改造时我一般先写一个最小的协议头结构体两端共用#pragma pack(push, 1) struct MsgHeader { int magic; // 魔数校验用 int cmd; // 命令字 int bodyLen; // 载荷长度网络字节序 }; #pragma pack(pop)#pragma pack(1)保证结构体不被编译器填充两端内存布局一致。发送时先htonl各字段再send接收时先收固定长度头再按bodyLen收载荷。验证方法很直接写一个循环发一万条随机长度消息的压测客户端服务端逐条校验magic和长度跑通不崩就算协议层稳了。还有一个容易忽略的点是资源释放顺序先shutdown再closesocket最后WSACleanup。shutdown会通知对端「我不再发了」对端能及时感知比直接closesocket温和。服务端退出时把监听 socket 和所有客户端 socket 都关掉别指望进程退出兜底长期运行的工具重启一次漏一批句柄迟早出事。从那以后我每次改这类老 Socket 工程都强制先跑一遍「启动服务端 → 连三个客户端 → 逐个断开 → 再连一个」的回归流程确认没有句柄泄漏和错误码误判才动业务代码。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站