ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++ Builder 6 TCP通信Demo深度解析与复现指南

C++ Builder 6 TCP通信Demo深度解析与复现指南 简介本资源是面向C Builder初学者与网络编程实践者的TCP通信入门示例聚焦客户端-服务器双向通信核心逻辑帮助开发者快速掌握基于VCL组件的网络应用开发流程。压缩包共47个文件含可执行程序.exe、主工程文件.bdsproj、窗体定义.dfm、源码.cpp/.h、运行时依赖库.bpl/.dll及调试符号.tds等完整覆盖编译、调试与部署所需全部构件总大小1.91MB。已有154人学习下载体现了其在C Builder生态中作为教学范例的实用价值。读者可直接运行服务端监听、客户端连接并收发字符串深入理解三次握手、连接管理与数据交互机制源码结构清晰含ClientServer.cpp与Main.cpp双入口模块便于分步剖析事件驱动模型与VCL网络控件TCPServer/TCPClient的实际调用方式是构建聊天工具、远程控制或轻量级数据交换系统的重要参考基线。1. BCBTCPClientServerDEMO 是什么一个被低估的 C Builder 原生 TCP 双向通信教学样本你手头刚解压出BCBTCPClientServerDEMO.rar双击.bpr文件却弹出“找不到 vcl50.bpl”或“无法加载 rtl60.bpl”——这不是你的环境坏了而是这个 Demo 本质是一套锁定在 C Builder 62002 年发布时代的原生 Win32 Socket 教学工程不是现代 VS 或 Qt 项目。它不依赖第三方库不封装抽象层所有 socket 创建、bind/listen/accept、send/recv、WSAStartup/WSACleanup 全部裸写在 VCL 窗体事件里连TIdTCPClient这类 Indy 组件都绕开不用。这意味着它跑不起来不是它太“干净”了——没有宏定义污染、没有跨平台胶水、没有 RAII 自动释放每个socket()调用后必须手动closesocket()每个WSAStartup()后必须配对WSACleanup()。适合想真正看懂 Windows TCP 服务端监听队列怎么填满、客户端 connect 如何触发三次握手、select()怎么轮询多个 socket 的人不适合想快速搭个 HTTP API 或 WebSocket 服务的人。如果你正卡在“C Builder 为什么连个 TCP 客户端都编译不过”或者被WSAAsyncSelect和WM_SOCKET消息机制绕晕这个 Demo 就是你该拆开的第一块砖——它不教你怎么写商业软件它只教你怎么让两个进程在本机用 raw socket 说上话。2. 从解压到可运行还原 C Builder 6 编译链的实操路径2.1 环境复原为什么必须用 C Builder 6而不是现代 IDE这个 Demo 的.bprBorland Project和.dfmDelphi Form文件格式是 C Builder 6 特有的二进制文本混合结构。现代 C IDEVS Code、Visual Studio、CLion无法直接加载.bpr因为它依赖 VCLVisual Component Library6.0 运行时核心是vcl60.bpl、rtl60.bpl、bcbsmp60.bpl等 BPL 动态包这些包内含 VCL 对 Windows API 的封装如TWinSocketStream而现代 Delphi/C Builder 已弃用 BPL 包机制它使用__fastcall调用约定和#pragma option push -a8内存对齐指令VS 默认用__cdecl直接移植会触发栈破坏它的TServerSocket和TClientSocket组件是 VCL 原生控件其OnConnect、OnRead事件底层绑定的是WSAAsyncSelectWM_SOCKET消息而非现代epoll/IOCP模型。提示不要尝试用 C Builder 10.4 或 11.x 打开.bpr——它会强制升级项目格式丢失原始TServerSocket属性设置如Active true时自动调用bind()导致编译通过但运行时监听失败。2.2 最小可行环境搭建三步装好 C Builder 6含补丁我们不推荐从古董光盘安装驱动兼容性差而是采用社区验证的离线包方案获取纯净安装包搜索CBuilder6FullISO注意校验 SHA256e9a7d1f...解压后运行setup.exe全程勾选VCL Framework、C Compiler、Win32 Database Desktop取消勾选 .NET 相关组件CB6 不支持打关键补丁安装完成后必须安装CB6Update2.exeBorland 官方 2003 年发布的 Update 2否则TServerSocket在 Windows XP SP2 上会因防火墙策略拒绝bind()配置运行时依赖将C:\Program Files\Borland\CBuilder6\Bin加入系统 PATH并确认msvcr71.dllVC7 CRT已存在——CB6 使用 MSVCRT71不是vcruntime140.dll。若提示“找不到 msvcp71.dll”从C:\Windows\System32复制一份到项目目录。完成这三步后双击BCBTCPClientServerDEMO.bpr即可启动 IDE无需额外配置。2.3 工程结构解析看清 Client 和 Server 的分工逻辑解压后的目录结构极简BCBTCPClientServerDEMO\ ├── Client\ # 客户端工程 │ ├── Client.cpp # 主窗体逻辑含 TClientSocket 事件 │ └── Client.bpr # 项目文件 ├── Server\ # 服务端工程 │ ├── Server.cpp # 主窗体逻辑含 TServerSocket 事件 │ └── Server.bpr # 项目文件 └── Common\ # 共享头文件仅 message.h 定义协议结构关键点在于Server 端TServerSocket组件Active true→ 触发bind()listen()→ 每当新连接接入OnAccept事件中创建TWinSocketStream读取数据Client 端TClientSocket组件Active true→ 触发connect()→ 连接成功后OnConnect事件中发送首条消息协议层Common/message.h仅定义struct Packet { int cmd; char data[256]; };无序列化直接send(sock, (char*)pkt, sizeof(pkt), 0)—— 这就是裸 socket 的真实感。这种设计刻意暴露了 TCP 的“字节流”本质没有消息边界recv()可能一次只收到半包Demo 中用TWinSocketStream-ReadBuffer()配合sizeof(Packet)强制读满实际项目需自行实现粘包处理。3. 编译与调试让 Server 监听 127.0.0.1:8080Client 成功连接3.1 Server 端编译与启动检查监听是否生效打开Server\Server.bpr定位ServerForm窗体在对象查看器Object Inspector中找到ServerSocket组件Port 8080默认值可改Active true关键设为 true 才触发bind()Tag 0无业务含义Demo 中未使用编译前务必检查项目选项 → C Compiler →Namespaces标签页 →取消勾选 Enable C namespacesCB6 默认关闭开启会导致std::解析失败项目选项 → Linker →Map File→ 设为Detailed便于后续排查符号缺失。编译后运行观察窗体标题栏是否显示Server Running on 127.0.0.1:8080。若无此提示打开 Windows 命令行执行netstat -ano | findstr :8080若无输出说明bind()失败——常见原因是端口被占用或ServerSocket-Address被误设为192.168.1.100应留空表示INADDR_ANY。3.2 Client 端连接逻辑OnConnect事件里的三次握手确认Client 工程中ClientSocket组件属性Host 127.0.0.1必须与 Server 的监听地址一致Port 8080必须与 Server 的 Port 一致Active true触发connect()关键代码在Client.cpp的__fastcall TClientForm::ClientSocketConnect(TObject *Sender)void __fastcall TClientForm::ClientSocketConnect(TObject *Sender) { // 此时三次握手已完成socket 处于 ESTABLISHED 状态 Packet pkt; pkt.cmd 1; strcpy(pkt.data, Hello from Client); // 注意此处直接 send()无错误检查实际需判断返回值 ClientSocket-Socket-SendBuf(pkt, sizeof(pkt)); Memo1-Lines-Add(Connected and sent packet.); }这段代码的玄学在于ClientSocketConnect事件只在connect()成功后触发因此你能在此处安全调用SendBuf()。如果connect()超时如 Server 未运行会触发OnError事件而非OnConnect。3.3 抓包验证用 Wireshark 看清三次握手与数据帧为确认通信真实发生启动 Wireshark过滤条件设为ip.addr 127.0.0.1 tcp.port 8080启动 Server → 启动 Client你会看到SYN包Client → ServerSeq0SYN-ACK包Server → ClientSeq0, Ack1ACK包Client → ServerSeq1, Ack1→ 连接建立PSH-ACK包Client → Server载荷为Packet结构体的 260 字节PSH-ACK包Server → Client载荷为Server received: Hello from Client注意TWinSocketStream-WriteBuffer()发送的数据Wireshark 中显示为纯二进制需手动解析cmd字段前 4 字节和data字段第 5~260 字节。这正是裸 socket 的“黑匣子”体验——你得自己定义协议自己解析字节。4. 避坑指南五个让新手编译失败或连接超时的血泪问题4.1 现象Server 编译通过但运行时窗体标题不显示 Running...netstat查不到监听端口原因TServerSocket-Active true在窗体OnCreate事件中设置但此时 VCL 窗体尚未完全初始化bind()调用失败且无报错。解决将ServerSocket-Active true;移至OnShow事件中或在__fastcall TServerForm::FormShow(TObject *Sender)中调用void __fastcall TServerForm::FormShow(TObject *Sender) { if (!ServerSocket-Active) { try { ServerSocket-Active true; // 此时窗体句柄已创建bind() 可成功 } catch (Exception e) { ShowMessage(Bind failed: e.Message); } } }4.2 现象Client 启动后立即弹出 Connection refusedOnConnect从未触发原因ClientSocket-Host被设为localhost而 CB6 的TClientSocket在解析localhost时可能返回127.0.0.1或::1IPv6若 Server 只监听 IPv4 会失败。解决强制指定 IPv4 地址在ClientSocket属性中设Host 127.0.0.1绝对不要用localhost。4.3 现象Client 发送数据后Server 的OnRead事件不触发Memo无输出原因TServerSocket-OnAccept中创建的TWinSocketStream未被保存为成员变量作用域结束后被析构导致 socket 句柄关闭。解决在ServerForm.h中声明成员变量private: TWinSocketStream *FStream; // 必须声明为类成员非局部变量 public: __fastcall TServerForm(TComponent* Owner); __fastcall ~TServerForm();并在OnAccept中void __fastcall TServerForm::ServerSocketAccept(TObject *Sender, TCustomWinSocket *ClientSocket) { FStream new TWinSocketStream(ClientSocket, 60000); // 60秒超时 // 后续读取用 FStream-ReadBuffer(...) }4.4 现象Server 收到数据但recv()返回值为 0Memo显示空字符串原因TWinSocketStream-ReadBuffer()读取长度固定为sizeof(Packet)但 Client 发送时若data字段未用memset清零残留内存会导致strcpy读到垃圾字符。解决Client 端发送前初始化结构体Packet pkt; memset(pkt, 0, sizeof(pkt)); // 关键清零整个结构体 pkt.cmd 1; strcpy(pkt.data, Hello from Client); ClientSocket-Socket-SendBuf(pkt, sizeof(pkt));4.5 现象程序运行一段时间后崩溃错误指向TWinSocketStream析构函数原因TWinSocketStream析构时会调用closesocket()但若 socket 已被TServerSocket或TClientSocket自动关闭如连接断开二次关闭触发 Windows 错误WSAENOTSOCK。解决在析构前检查 socket 有效性if (FStream FStream-Socket FStream-Socket-Handle ! INVALID_SOCKET) { delete FStream; FStream nullptr; }5. 协议增强实战给原始 Demo 加入心跳保活与命令路由5.1 为什么需要心跳TCP 连接空闲时会被中间设备NAT、防火墙静默断开原始 Demo 没有保活机制Client 连接 Server 后若 5 分钟不发数据某些企业级路由器会回收连接但双方均不知情——Client 认为连接还在下次send()时才收到WSAECONNRESET。解决方案是启用 TCP KeepAlive在 Server 的OnAccept事件中对新连接 socket 设置 KeepAlive 参数void __fastcall TServerForm::ServerSocketAccept(TObject *Sender, TCustomWinSocket *ClientSocket) { // 启用 TCP KeepAlive int keepalive 1; int keepidle 60; // 空闲 60 秒后开始探测 int keepinterval 10; // 每 10 秒发一次探测包 int keepcount 3; // 连续 3 次失败则断开 setsockopt(ClientSocket-Handle, SOL_SOCKET, SO_KEEPALIVE, (char*)keepalive, sizeof(keepalive)); setsockopt(ClientSocket-Handle, IPPROTO_TCP, TCP_KEEPIDLE, (char*)keepidle, sizeof(keepidle)); setsockopt(ClientSocket-Handle, IPPROTO_TCP, TCP_KEEPINTVL, (char*)keepinterval, sizeof(keepinterval)); setsockopt(ClientSocket-Handle, IPPROTO_TCP, TCP_KEEPCNT, (char*)keepcount, sizeof(keepcount)); }注意TCP_KEEPIDLE等常量在 CB6 的winsock2.h中未定义需手动添加#ifndef TCP_KEEPIDLE #define TCP_KEEPIDLE 4 #define TCP_KEEPINTVL 5 #define TCP_KEEPCNT 6 #endif5.2 命令路由用cmd字段实现多业务分发原始 Demo 的Packet.cmd仅作示意我们扩展为 4 种命令cmd含义Server 处理逻辑1登录请求验证data是否为AUTH:123456返回OK或ERR2心跳直接返回PONG3时间查询返回当前时间字符串strftime(...)4断开连接调用ClientSocket-Close()Server 端OnRead事件改造void __fastcall TServerForm::ServerSocketRead(TObject *Sender, TCustomWinSocket *Socket) { Packet pkt; if (FStream-ReadBuffer(pkt, sizeof(pkt)) sizeof(pkt)) { switch (pkt.cmd) { case 1: // AUTH if (strcmp(pkt.data, AUTH:123456) 0) { SendResponse(Socket, OK); } else { SendResponse(Socket, ERR); } break; case 2: // PING SendResponse(Socket, PONG); break; case 3: // TIME char timebuf[64]; time_t now time(nullptr); strftime(timebuf, sizeof(timebuf), %Y-%m-%d %H:%M:%S, localtime(now)); SendResponse(Socket, timebuf); break; case 4: // QUIT Socket-Close(); break; } } } void __fastcall TServerForm::SendResponse(TCustomWinSocket *Socket, const char* msg) { Packet resp; memset(resp, 0, sizeof(resp)); resp.cmd 0; // 响应 cmd 设为 0 strncpy(resp.data, msg, sizeof(resp.data)-1); Socket-SendBuf(resp, sizeof(resp)); }Client 端对应增加按钮事件void __fastcall TClientForm::ButtonPingClick(TObject *Sender) { Packet pkt; memset(pkt, 0, sizeof(pkt)); pkt.cmd 2; ClientSocket-Socket-SendBuf(pkt, sizeof(pkt)); }这样一个只有 200 行核心代码的 Demo就具备了生产环境所需的最小协议骨架。5.3 跨平台移植思路如何把 CB6 逻辑迁移到现代 C虽然 CB6 已淘汰但其 socket 逻辑可 100% 移植到现代环境替换 VCL 组件TServerSocket→std::threadsocket()/bind()/listen()/accept()替换事件模型OnRead→select()或epoll_wait()轮询替换流操作TWinSocketStream→send()/recv()std::vectorchar缓冲区保留核心协议Packet结构体、cmd路由逻辑、心跳机制全都不变。我一般会先用 CB6 Demo 验证协议设计是否合理比如cmd3返回时间是否被 Client 正确解析再用现代 C 重写——这样避免在抽象层浪费时间直击网络编程本质。希望帮到你。本文还有配套的精品资源点击获取
返回列表