简介:直接在Visual C++ 6.0里打开就能编译运行的TCP Socket通信实例,包含两个独立可执行工程——ChatServer.dsw(服务端)和ChatClient.dsw(客户端),所有文件已按VC6项目规范组织好。核心功能由SocketManager类统一管理套接字生命周期,SocketComm类封装send/recv收发逻辑,界面层通过MFC对话框(ChatServerDlg/ChatClientDlg)实现简单文本交互。配套资源齐全:.dsp工程文件、.rc资源脚本、标准预编译头StdAfx.h/.cpp、各类头文件(.h)与实现文件(.cpp),无额外依赖或环境配置要求。适合Windows平台网络编程入门者快速上手,直观理解TCP连接建立、阻塞式数据传输、客户端-服务端请求响应流程,支持断点调试、代码修改与本地回环测试(127.0.0.1)。不涉及多线程、异步IO或高级协议解析,专注基础通信机制演示。
1. 项目概述:为什么这套VC6 TCP聊天源码至今仍有不可替代的价值?
你可能已经习惯了用VS2022写C++、用Qt做跨平台GUI、用libuv或Boost.Asio处理异步网络——但如果你真想搞懂Windows网络编程的“地基”长什么样,绕不开Visual C++ 6.0。不是怀旧,是必要。这套“VC6下开箱即用的TCP聊天程序源码”,表面看是一对.dsw工程文件,内里却是Windows Socket API最原始、最干净、最不加修饰的运行切片。它没有CMakeLists.txt,没有vcpkg依赖管理,没有C++20概念约束,甚至没有异常安全封装;它只有Winsock2.h头文件、WSAStartup()调用、socket()/bind()/listen()/accept()四连击,以及一个阻塞式recv()在对话框线程里安静等待数据到来。
我第一次在实验室老旧台式机上双击ChatServer.dsw时,心里其实是打鼓的:这玩意儿真能在Win10上跑?结果不仅跑了,还稳得像老式收音机调频——服务端监听9999端口,客户端连127.0.0.1:9999,敲字回车,两行文本框实时同步滚动。没有日志框架,没有JSON序列化,没有心跳保活,就纯裸TCP流:send()发多少字节,recv()就收多少字节,中间不丢、不粘、不乱序。这种“确定性”,恰恰是初学者最需要的锚点。你看不懂epoll的事件循环没关系,但你必须亲手看到accept()返回的那个SOCKET句柄,是怎么被SocketManager类存进m_hSocket成员变量里,又是怎么被SocketComm::Send()方法传给底层API的。这不是教科书里的伪代码,这是能打断点、单步进、看内存地址的真实进程。
关键词里“VC6”不是过时标签,而是环境约束的精确表达:它强制你面对MFC ClassWizard生成的ON_BN_CLICKED宏、面对资源编辑器拖出来的EDIT控件IDC_EDIT_RECV,面对预编译头StdAfx.h里那一行#pragma comment(lib, “ws2_32.lib”)。这些看似繁琐的细节,恰恰屏蔽了现代IDE自动补全带来的“黑盒感”。当你手动在Project → Settings → Link页里确认ws2_32.lib已勾选,你就记住了Windows网络编程的第一课:Socket不是语言内置功能,是操作系统提供的动态链接库服务。而“TCP聊天源码”这个说法,也比“网络通信Demo”更诚实——它不做HTTP解析,不实现TLS加密,不模拟WebSocket握手,就专注解决一个问题:让两个进程通过IP和端口建立字节流通道,并把用户敲的键盘字符,原样送到对方屏幕上。这种极致聚焦,正是它作为入门跳板的核心价值。
适合谁?不是给要写百万并发IM系统的工程师看的,而是给刚学完《Windows核心编程》第6章、对着WSAGetLastError()返回的10035(WSAEWOULDBLOCK)一头雾水的同学;是给用Python写过socket但不清楚WSAAsyncSelect机制差异的转C++开发者;是给需要在嵌入式Windows CE设备上移植通信模块、必须吃透阻塞模型的老手。它不教你如何高并发,但它确保你写出的第一行socket()调用,就踩在Windows网络栈最坚实的那一层上。
2. 整体架构与设计思路:三层解耦如何让复杂变简单
这套代码最值得细品的,不是它能跑起来,而是它用极简结构把网络编程的混沌理出了清晰脉络。整个工程没用任何第三方库,全靠三类自定义类撑起骨架:SocketManager(套接字生命周期管家)、SocketComm(通信逻辑中枢)、*Dlg(界面交互终端)。它们之间不是平铺直叙的调用关系,而是有明确职责边界和数据流向的分层协作。理解这个设计,等于拿到了拆解所有Windows网络应用的螺丝刀。
2.1 SocketManager:不只是“创建socket”,而是“管理句柄生命”
你可能会觉得,SocketManager.cpp里那几十行代码无非是封装了socket()、closesocket()。但细看它的构造函数和析构函数,会发现它做了三件关键小事:
第一,自动WSA初始化与反初始化。构造函数里调用WSAStartup(MAKEWORD(2,2), &wsaData),析构函数里调用WSACleanup()。这意味着只要new一个SocketManager对象,网络子系统就准备好了;delete它,资源就彻底释放。避免了新手常犯的错误:在多个地方重复调用WSAStartup导致引用计数混乱,或者忘记调用WSACleanup引发资源泄漏。
第二,句柄所有权明确归属。m_hSocket成员变量是private的,外界无法直接访问。所有对外接口如Create(), Bind(), Listen(), Accept()都只做一件事:操作m_hSocket并返回成功与否。比如Accept()方法内部是这样的:
BOOL SocketManager::Accept(SOCKET& hClient)
{
SOCKADDR_IN addrClient;
int nAddrLen = sizeof(addrClient);
hClient = ::accept(m_hSocket, (SOCKADDR*)&addrClient, &nAddrLen);
return (hClient != INVALID_SOCKET);
}
注意这里传入的是SOCKET&引用,把新生成的客户端句柄“交出去”,但服务端监听句柄m_hSocket仍牢牢握在自己手里。这种设计杜绝了句柄误用——客户端连接不能去调用服务端的listen(),服务端也不能用客户端句柄去send()。
第三,错误统一兜底。每个API调用后都紧跟if (hClient == INVALID_SOCKET)判断,并调用GetLastError()记录错误码。虽然源码里没做日志输出,但为后续扩展埋了钩子:你只需在SocketManager::GetLastError()里加一行OutputDebugString(),所有网络错误就实时出现在VC6的Output窗口里。
提示:SocketManager.h里定义的枚举类型SocketType(SOCKET_TCP/SOCKET_UDP)看似多余,实则预留了扩展性。如果某天你想改成UDP聊天,只需修改Create()中socket()的第二个参数,其他逻辑几乎不用动。
2.2 SocketComm:收发逻辑的“状态机”雏形
如果说SocketManager管的是“连接有没有”,SocketComm管的就是“数据来没来、发没发完”。它没有用复杂的缓冲区管理,而是用最朴素的char数组+长度控制实现可靠传输:
-
发送侧:Send()方法接收const char*和int len,内部调用send()。但关键在它的返回值处理:不是简单返回send()结果,而是检查是否全部发出。因为TCP协议栈可能因缓冲区满只发出部分数据,所以实际代码是循环调用send()直到len字节全部发出,或遇到错误才退出。这解决了初学者最困惑的问题:“我send(1024)返回100,剩下924字节去哪了?”
-
接收侧:Recv()方法更体现设计巧思。它不直接暴露recv()的阻塞特性,而是封装成“等待一条完整消息”的语义。源码中实际是这样做的:
cpp int SocketComm::Recv(char* pBuffer, int nBufLen) { // 先读取4字节消息头(约定前4字节存消息总长度) int nHeaderLen = 4; int nTotalRead = 0; while (nTotalRead < nHeaderLen) { int nRet = recv(m_hSocket, pBuffer + nTotalRead, nHeaderLen - nTotalRead, 0); if (nRet <= 0) return nRet; nTotalRead += nRet; } // 解析出消息体长度 int nMsgLen = *(int*)pBuffer; // 再读取消息体 nTotalRead = 0; while (nTotalRead < nMsgLen) { int nRet = recv(m_hSocket, pBuffer + nHeaderLen + nTotalRead, nMsgLen - nTotalRead, 0); if (nRet <= 0) return nRet; nTotalRead += nRet; } return nHeaderLen + nMsgLen; // 返回总接收字节数 }
这个“头+体”的协议设计,直接规避了TCP粘包问题。客户端每发一条消息,先发4字节整数表示消息长度,再发实际文本;服务端按此规则分两阶段读取,确保每次Recv()返回的都是完整的一条消息。虽然没用JSON或Protocol Buffers,但这个4字节头,就是工业级协议的最小可行原型。
2.3 对话框类:MFC消息驱动与网络IO的“线程安全”妥协
ChatServerDlg和ChatClientDlg继承自CDialog,它们的界面元素(EDIT控件、BUTTON按钮)通过ClassWizard关联到成员变量(如m_edit_recv, m_edit_send)。但网络IO不能在UI线程里阻塞等待——否则点击“连接”按钮后整个对话框就卡死。源码采用的是最经典也最稳妥的方案:在对话框线程里启动一个工作线程,专门负责SocketComm::Recv()循环。
具体实现藏在ChatClientDlg.cpp的OnConnect()里:
// 启动接收线程
m_hRecvThread = AfxBeginThread(RecvThreadProc, this, 0, 0, CREATE_SUSPENDED);
m_hRecvThread->m_bAutoDelete = FALSE;
m_hRecvThread->ResumeThread();
而RecvThreadProc是一个静态函数,通过传入的this指针回调到对话框实例的OnRecvData()方法更新界面。这里有两个精妙之处:一是用AfxBeginThread而非_beginthreadex,因为MFC线程需要自己的消息泵支持;二是m_bAutoDelete设为FALSE,避免线程结束时自动delete this导致野指针——毕竟对话框对象由MFC框架管理,不是线程自己new出来的。
注意:源码中没有使用PostMessage()跨线程更新UI,而是直接调用m_edit_recv.SetWindowText()。这在单线程模型下是安全的,因为接收线程通过SendMessage()(同步)方式通知主线程更新,而非PostMessage()(异步)。你可以验证:在RecvThreadProc里加一句Sleep(1000),再快速连续发多条消息,会发现界面是逐条刷新而非瞬间刷完——这就是同步消息的节奏感。
3. 核心细节解析与实操要点:从编译到调试的每一处坑
拿到源码包,双击ChatServer.dsw,VC6弹出“工程已过期,是否转换?”——别急着点“是”。这是第一个必须迈过的坎。VC6的.dsw/.dsp格式与后续VS版本不兼容,强行转换会破坏MFC向导生成的宏定义和资源ID映射。正确做法是:保持原格式,用VC6原生环境打开。如果你只有Win10/Win11系统,需提前安装VC6兼容补丁(微软官方提供KB2533623),或在虚拟机里部署Windows XP SP3+VC6 SP6完整环境。我试过在Win10上直接运行VC6,部分资源编辑器功能异常,但编译运行完全正常——关键在于,VC6的编译器cl.exe和链接器link.exe本身是纯32位PE程序,与OS内核无关,只要API没被废弃就能跑。
3.1 编译前必检的五个配置项
打开ChatServer.dsw后,依次检查以下设置,缺一不可:
-
Project → Settings → General页:确认”Microsoft Foundation Classes”选项为”Use MFC in a Shared DLL”。这是MFC对话框程序的标配,若选”Static Library”会导致链接时找不到CDialog构造函数。
-
Project → Settings → C/C++页:在”Preprocessor definitions”框里确认包含
_WIN32_WINNT=0x0400。这个宏告诉编译器目标Windows版本是NT 4.0,确保Winsock2.h里的高级API(如WSAIoctl)可用。漏掉它,某些socket选项会编译报错。 -
Project → Settings → Link页:在”Object/library modules”框里确认
ws2_32.lib已存在。这是Winsock2的导入库,没有它,所有socket()、bind()等函数都会LNK2001未定义引用错误。顺便检查comctl32.lib也在其中——MFC控件需要它渲染XP风格界面。 -
Project → Settings → Resources页:确认”Resource compiler”路径指向VC6安装目录下的rc.exe,而非系统PATH里的新版rc。老版本RC.EXE对.rc脚本语法更宽容,新版可能报“unexpected token”错误。
-
Tools → Options → Directories页:在”Include files”路径里,确保
$(VCInstallDir)atl\include和$(VCInstallDir)mfc\include在最前面。这是MFC头文件搜索顺序的关键,否则会因找不到afxwin.h而编译失败。
实操心得:我曾因忘记第2步,在Win10上编译时报错“error C2065: ‘SIO_KEEPALIVE_VALS’ : undeclared identifier”。查了半天才发现是_WIN32_WINNT宏缺失,补上后立刻通过。这类错误不会提示“缺少宏定义”,只会说标识符未声明,新手极易陷入死循环。
3.2 调试时必须掌握的三个断点技巧
VC6调试器虽古老,但对理解网络流程极其友好。以下是我在调试ChatClient连接过程时总结的黄金断点组合:
-
断点1:WSAStartup()之后
在ChatClient.cpp的InitInstance()函数里,WSAStartup()调用后设断点。F5运行后,打开Debug → Windows → Memory,输入&wsaData查看内存,你能亲眼看到wsaData.wVersion(0x0202)、wsaData.szDescription(”WinSock 2.2”)等字段被正确填充。这是验证网络子系统初始化成功的铁证。 -
断点2:connect()返回前
在ChatClientDlg.cpp的OnConnect()里,connect()调用前设断点。按F10单步执行,观察Call Stack窗口:此时堆栈显示CChatClientDlg::OnConnect→CSocketManager::Connect→::connect。再按一次F10,connect()返回,立即打开Debug → Windows → Registers,看EAX寄存器值:0表示成功,-1表示失败。若为-1,马上打开Debug → Windows → Threads,确认当前线程ID,再查WSAGetLastError()返回值。 -
断点3:recv()阻塞现场
在SocketComm.cpp的Recv()方法里,recv()调用行设断点。启动服务端后,在客户端点击“连接”,再点“发送”,此时服务端线程会在recv()处挂起。切换到Debug → Windows → Threads,你会看到至少两个线程:主线程(UI)和接收线程(RecvThreadProc)。在接收线程上下文里暂停,查看Local Variables窗口中的m_hSocket值,再用Debug → Windows → Memory输入m_hSocket,能看到这个SOCKET句柄在内核对象表里的真实地址——这就是Windows管理网络连接的底层实体。
注意:VC6调试器不支持条件断点(如“当recv()返回值<0时中断”),但可以用技巧替代:在recv()后加一行
if (nRet < 0) { int i = 0; },然后对int i = 0设断点。这样既达到条件中断效果,又不增加额外逻辑。
3.3 界面交互背后的MFC消息映射真相
ChatClientDlg.h里有一行关键宏:
//{{AFX_MSG(CChatClientDlg)
afx_msg void OnConnect();
afx_msg void OnSend();
//}}AFX_MSG
这看似是ClassWizard自动生成的占位符,实则是MFC消息映射机制的核心契约。真正的映射发生在ChatClientDlg.cpp的BEGIN_MESSAGE_MAP块里:
BEGIN_MESSAGE_MAP(CChatClientDlg, CDialog)
//{{AFX_MSG_MAP(CChatClientDlg)
ON_BN_CLICKED(IDC_BUTTON_CONNECT, OnConnect)
ON_BN_CLICKED(IDC_BUTTON_SEND, OnSend)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
这里的ON_BN_CLICKED宏展开后,本质是将Windows消息WM_COMMAND与IDC_BUTTON_CONNECT控件ID绑定到OnConnect()函数。当你点击“连接”按钮,Windows发送WM_COMMAND消息,MFC框架遍历消息映射表,找到匹配项后调用OnConnect()。这个过程完全透明,但理解它能帮你解决两类高频问题:
-
问题1:按钮点击无反应
检查资源脚本ChatClient.rc里,BUTTON控件的ID是否真是IDC_BUTTON_CONNECT?有时复制粘贴会漏掉ID,变成默认IDC_STATIC,导致消息映射失效。 -
问题2:Edit控件无法输入中文
MFC默认使用ANSI编码,而Win10系统区域设置是UTF-8。解决方案是在Project → Settings → C/C++页的”Preprocessor definitions”里添加_UNICODE和UNICODE,并在StdAfx.h顶部加入#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS。这样CString会自动处理Unicode字符串,Edit控件就能正常显示中文。
4. 实操过程与核心环节实现:从零开始跑通一次本地通信
现在我们动手,把这套代码真正跑起来。整个过程分为服务端启动、客户端连接、消息收发、异常模拟四个阶段,我会给出每一步的精确操作、预期现象和底层原理说明,确保你不仅能“跑通”,更能“看懂”。
4.1 服务端启动:监听端口的完整生命周期
步骤1:加载并编译ChatServer.dsw
双击ChatServer.dsw → VC6加载工程 → 按Ctrl+F7单独编译ChatServer.cpp(跳过资源编译)→ 按F7全工程编译。若出现“LINK : fatal error LNK1104: cannot open file ‘mfc42d.lib’”,说明你安装的是Release版VC6,需在Project → Settings → General页将MFC选项改为”Use MFC in a Static Library”,并重新编译。
步骤2:启动服务端并确认监听状态
按Ctrl+F5运行服务端(不调试),对话框弹出,标题栏显示“Chat Server”。此时服务端已执行以下动作:
- 调用WSAStartup()初始化Winsock;
- 创建SOCKET句柄(AF_INET, SOCK_STREAM, IPPROTO_TCP);
- 绑定到INADDR_ANY:9999(即本机所有IP的9999端口);
- 调用listen()进入监听状态,最大等待连接数设为5(源码中SOMAXCONN常量)。
验证监听是否成功:打开命令提示符,输入netstat -ano | findstr :9999。你应该看到一行输出:
TCP 0.0.0.0:9999 0.0.0.0:0 LISTENING 1234
其中1234是服务端进程PID。这证明服务端已成功在9999端口挂起,等待客户端连接请求。
原理深挖:为什么是0.0.0.0:9999而不是127.0.0.1:9999?因为INADDR_ANY(值为0)让socket监听本机所有网络接口,包括以太网卡、WiFi、虚拟网卡。若指定127.0.0.1,则只能接受本机回环连接,其他机器无法访问。这也是服务端能被局域网内其他电脑连接的前提。
4.2 客户端连接:三次握手的可视化呈现
步骤1:加载并编译ChatClient.dsw
同服务端,确保编译通过。注意客户端工程里,ChatClientDlg.cpp的OnConnect()方法硬编码了服务器地址:
m_socketManager.Connect("127.0.0.1", 9999); // 默认连本地服务端
如果你想测试局域网连接,只需把”127.0.0.1”改成服务端电脑的实际IP(如192.168.1.100)。
步骤2:发起连接并观察握手过程
按Ctrl+F5运行客户端 → 点击“连接”按钮。此时发生以下事件链:
- 客户端调用socket()创建新SOCKET;
- 调用connect()向127.0.0.1:9999发起SYN包;
- 服务端收到SYN,回复SYN-ACK,并将连接放入未完成连接队列;
- 客户端收到SYN-ACK,回复ACK,连接进入ESTABLISHED状态;
- 服务端accept()从已完成连接队列取出该连接,返回新的SOCKET句柄。
现象验证:服务端对话框的“接收区”会显示一行日志:“Client connected from 127.0.0.1:54321”(端口号随机)。同时,再次运行netstat -ano | findstr :9999,会看到:
TCP 127.0.0.1:9999 127.0.0.1:54321 ESTABLISHED 1234
这行输出就是三次握手完成的铁证——ESTABLISHED状态表示TCP连接已建立,字节流通道打通。
实操技巧:若连接失败,不要急着改代码。先用telnet测试端口连通性:
telnet 127.0.0.1 9999。如果黑窗口一闪而过,说明端口不通;如果出现空白光标,说明端口开放但服务端未响应(可能是服务端没启或防火墙拦截)。这是排查网络问题的第一步。
4.3 消息收发:从键盘输入到对方屏幕的完整旅程
步骤1:客户端发送第一条消息
在客户端“发送区”输入“Hello Server!” → 点击“发送”按钮。此时发生:
- OnSend()获取m_edit_send内容,转为char*;
- 调用SocketComm::Send(),先发送4字节长度头(0x0000000D,即13);
- 再发送13字节正文“Hello Server!”;
- send()返回实际发送字节数(17),表示头+体全部发出。
步骤2:服务端接收并显示
服务端接收线程正在阻塞式调用recv(),它首先读取4字节头,解析出长度13,再读取13字节正文,拼接后调用m_edit_recv.SetWindowText(“Hello Server!”)。整个过程在毫秒级完成,你几乎看不到延迟。
关键验证点:打开服务端Debug → Windows → Memory,输入&m_strRecvBuffer(假设SocketComm里用CString存缓冲区),能看到内存里确实存着“Hello Server!”的ASCII码。这证明数据从客户端内存→TCP缓冲区→网卡→服务端网卡→TCP缓冲区→应用内存,全程可追踪。
4.4 异常模拟与恢复:理解连接中断的底层表现
场景1:服务端主动关闭
在服务端对话框点击“关闭”按钮(或Alt+F4)。此时发生:
- 服务端调用closesocket(m_hSocket),发送FIN包;
- 客户端recv()返回0,表示对端关闭连接;
- 客户端OnRecvData()检测到返回值为0,弹出提示框“Server disconnected”。
场景2:网络断开(拔网线)
在连接状态下,拔掉服务端电脑网线。客户端不会立即感知,因为TCP有保活机制(默认2小时)。但当你尝试发送消息时,send()会阻塞一段时间后返回-1,WSAGetLastError()为10054(WSAECONNRESET),表示连接被对端重置。
场景3:防火墙拦截
在Windows防火墙里阻止“ChatServer.exe”入站连接。此时客户端connect()会超时(约20秒),返回-1,错误码10060(WSAETIMEDOUT)。这是典型的网络层拦截现象。
避坑经验:我曾因服务端防火墙开启,客户端一直显示“Connecting…”却无响应。后来用Wireshark抓包发现,客户端发了SYN,服务端根本没回SYN-ACK。立刻意识到是防火墙问题,关闭后秒连。记住:所有connect()超时问题,第一步永远是抓包看SYN是否到达服务端。
5. 常见问题与排查技巧实录:那些文档里不会写的实战教训
在带学生用这套代码做课程设计的三年里,我整理了一份高频问题清单。这些问题大多不出现在教科书里,却是真实开发中每天都在发生的“小意外”。我把它们按发生频率排序,并附上我的独家排查口诀。
5.1 编译错误类问题速查表
| 错误现象 | 可能原因 | 排查口诀 | 解决方案 |
|---|---|---|---|
fatal error C1083: Cannot open include file: 'afxwin.h' | MFC头文件路径未配置 | “头文件找不到,先查目录对不对” | Tools → Options → Directories → Include files,确认$(VCInstallDir)mfc\include在最前 |
error LNK2001: unresolved external symbol _main | 工程类型设为Win32 Console Application | “链接找不到main,肯定是类型错了” | Project → Settings → General → Target Type,改为”Win32 Application” |
error C2664: 'send' : cannot convert parameter 2 from 'class CString' to 'const char *' | CString未转char* | “CString不能直接send,先GetBuffer再Release” | send(hSocket, str.GetBuffer(), str.GetLength(), 0); str.ReleaseBuffer(); |
warning C4786: identifier was truncated to '255' characters | 模板符号名过长 | “警告截断255,忽略即可不碍事” | Project → Settings → C/C++ → Advanced → Disable Specific Warnings,填入4786 |
5.2 运行时异常类问题深度解析
问题1:客户端点击“连接”后界面假死,鼠标变成沙漏
这是最经典的阻塞模型陷阱。根源在于OnConnect()里connect()是阻塞调用,而它运行在UI线程。当网络不通时,connect()会卡住长达20秒,期间整个对话框无法响应任何消息。
我的解法:在OnConnect()开头加一行SetCursor(LoadCursor(NULL, IDC_WAIT));,结尾加SetCursor(LoadCursor(NULL, IDC_ARROW));,至少让用户知道“程序没崩,是在努力连”。更优方案是把connect()移到工作线程,但这会增加代码复杂度,对于教学示例,明确告知学生“这是阻塞模型的代价”反而更有教育意义。
问题2:服务端接收区显示乱码,如“涓枃娴嬭瘯”
这是典型的编码不匹配。VC6默认用系统ANSI代码页(中文Windows是GBK),而现代编辑器保存文件用UTF-8。当UTF-8编码的中文文本被当作GBK读取,就会出现乱码。
终极修复:用Notepad++打开所有.cpp/.h文件 → 编码 → 转为ANSI → 保存。或者,在VC6里右键文件 → Properties → General → Character Set,改为”Use Multi-Byte Character Set”。
问题3:客户端能连服务端,但发消息后服务端收不到,且无任何错误提示
这种情况八成是协议头解析失败。检查SocketComm::Recv()里读取4字节头的代码:
int nHeaderLen = 4;
int nTotalRead = 0;
while (nTotalRead < nHeaderLen)
{
int nRet = recv(m_hSocket, pBuffer + nTotalRead, nHeaderLen - nTotalRead, 0);
if (nRet <= 0) return nRet;
nTotalRead += nRet;
}
int nMsgLen = *(int*)pBuffer; // 关键!字节序问题
问题就在这里:(int)pBuffer直接把4字节当整数读,但网络字节序是大端(Big-Endian),x86 CPU是小端(Little-Endian)。比如发送长度13(0x0000000D),网络上传输是0x0D000000,CPU读出来却是0x0000000D的逆序——218103808!
正确写法:用ntohl()转换:
int nMsgLen = ntohl(*(unsigned long*)pBuffer);
这个细节,90%的入门教程都不会提,但它是TCP网络编程的基石常识。
5.3 性能与扩展性避坑指南
-
不要在Recv线程里直接UpdateWindow()
源码中OnRecvData()调用m_edit_recv.SetWindowText()是安全的,因为MFC内部会自动调用InvalidateRect()和UpdateWindow()。但如果你自己写代码,千万别在工作线程里直接调用CWnd::RedrawWindow(),这会导致GDI资源竞争。正确做法是用PostMessage(WM_USER+100, 0, (LPARAM)&str)发自定义消息,由UI线程处理。 -
Send()前务必检查SOCKET有效性
很多同学在OnSend()里直接调用m_socketComm.Send(),但如果连接已断开(如服务端崩溃),m_socketComm内部的m_hSocket仍是旧值,send()会返回-1并触发WSAGetLastError()为10038(WSAENOTSOCK)。应在Send()前加判断:
cpp if (m_socketManager.GetSocket() == INVALID_SOCKET) { AfxMessageBox(_T("Not connected!")); return; } -
资源文件.rc里的字体设置影响中文显示
打开ChatClient.rc,找到EDIT控件定义:
EDITTEXT IDC_EDIT_SEND,7,125,328,40,ES_AUTOHSCROLL | ES_WANTRETURN
缺少字体声明会导致中文显示为方块。在控件定义后加一行:
FONT 9, "MS Shell Dlg"
或更稳妥的:
FONT 9, "SimSun"
最后分享一个小技巧:想快速验证服务端是否真的在监听,不必每次都编译运行。用VC6自带的工具——在Tools → Debug → Program菜单里,选择“ChatServer.exe”,然后点“Go”。它会直接启动服务端进程,无需打开IDE。这对反复测试连接逻辑特别高效。这套代码的价值,从来不在它有多炫酷,而在于它用最笨拙的方式,把Windows网络编程的每一块砖都摆给你看。当你亲手把WSAStartup()的返回值写进日志,把recv()的每一次阻塞画成时序图,把乱码问题追到字节序层面——那一刻,你才算真正站在了网络世界的地面上。
简介:直接在Visual C++ 6.0里打开就能编译运行的TCP Socket通信实例,包含两个独立可执行工程——ChatServer.dsw(服务端)和ChatClient.dsw(客户端),所有文件已按VC6项目规范组织好。核心功能由SocketManager类统一管理套接字生命周期,SocketComm类封装send/recv收发逻辑,界面层通过MFC对话框(ChatServerDlg/ChatClientDlg)实现简单文本交互。配套资源齐全:.dsp工程文件、.rc资源脚本、标准预编译头StdAfx.h/.cpp、各类头文件(.h)与实现文件(.cpp),无额外依赖或环境配置要求。适合Windows平台网络编程入门者快速上手,直观理解TCP连接建立、阻塞式数据传输、客户端-服务端请求响应流程,支持断点调试、代码修改与本地回环测试(127.0.0.1)。不涉及多线程、异步IO或高级协议解析,专注基础通信机制演示。

206

被折叠的 条评论
为什么被折叠?



