
简介本资源是面向C Builder初学者与网络编程实践者的TCP通信入门示例聚焦客户端-服务器双向通信核心逻辑帮助开发者快速掌握基于VCL组件的网络应用开发流程。压缩包共47个文件含可执行程序exe、主窗体界面dfm、核心源码cpp/h、项目配置bdsproj、运行依赖库bpl/dll及调试符号文件tds/res整体1.91MB结构完整开箱即用。已有154人学习下载适合作为课堂实验、自学练手或项目原型参考。读者可直接运行服务端监听、启动客户端连接通过字符串收发直观理解三次握手、连接建立与数据交互全过程源码层次清晰Main.cpp与ClientServer.cpp分别封装服务端响应逻辑与客户端通信流程配合VCL控件事件驱动设计便于调试修改并拓展为聊天工具、远程控制等实际应用。1. BCBTCPClientServerDEMO一个被低估的 C Builder TCP 网络编程教学切口你可能在翻旧项目文档、维护遗留工业控制软件或刚接手一套用 C Builder 写的设备通信模块——突然看到BCBTCPClientServerDEMO.rar这个压缩包名心里一紧这玩意儿能跑吗Builder 还活着吗TCP 客户端/服务端在现代 C 工程里不是早该用 Boost.Asio 或 libuv 了吗但现实是全国仍有超 3000 家中小型自动化厂商、医疗设备集成商、PLC 上位机开发商其核心通信层仍运行着基于 C Builder 的 VCL TCP 组件如TClientSocket/TServerSocket且这些系统平均生命周期长达 12 年以上。这个 DEMO 不是古董标本而是真实产线里“改一行代码要测三天”的黑匣子入口。它用最朴素的 VCL 封装 原生 Win32 socket API 调用把 TCP 连接建立、数据收发、异常断连、多客户端管理等关键路径全摊开在.cpp和.dfm文件里。适合两类人一是需要快速读懂/修复老系统的工程师别再靠猜和试错二是想从零理解“GUI 框架如何与底层 socket 协同工作”的 C 初学者——它不抽象、不跨平台、不依赖第三方库所有逻辑都在 4 个.cpp文件里连WSAStartup()的调用时机都写得明明白白。提示本 DEMO 严格绑定 Windows 平台 C Builder 编译器非 MSVC不能直接用 VS 打开。若你手头只有 VS Code 或 CLion请先确认是否真需维护此类系统——否则建议跳过转向现代异步网络栈。2. 从解压到编译还原 BCBTCPClientServerDEMO 的最小可运行环境2.1 解压与目录结构解析看清四个核心文件的分工解压BCBTCPClientServerDEMO.rar后你会得到一个扁平目录包含以下关键文件无子文件夹Client.dpr/Client.cpp/ClientUnit.h/ClientUnit.cpp→ 客户端主程序及 UI 逻辑Server.dpr/Server.cpp/ServerUnit.h/ServerUnit.cpp→ 服务端主程序及 UI 逻辑Common.h→ 全局常量定义如DEFAULT_PORT 8080、数据包结构体TPacketHeaderREADME.txt→ 极简说明“运行 Server.exe再运行 Client.exe输入 IP 地址连接”注意.dpr是 C Builder 的项目描述文件Delphi-style等价于 VS 的.vcxproj但不可用 VS 直接加载。它声明了主窗体单元、依赖包VCL、Indy不本 DEMO 用原生 VCL Socket 组件、资源文件。真正编译入口是Client.cpp和Server.cpp中的WinMain函数。我们重点看ClientUnit.cpp开头几行#include vcl.h #pragma hdrstop #include ClientUnit.h #include Common.h //--------------------------------------------------------------------------- #pragma package(smart_init) #pragma resource *.dfm TForm1 *Form1; //--------------------------------------------------------------------------- __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { // 初始化 socket 组件TClientSocket *ClientSocket1; ClientSocket1-Address 127.0.0.1; // 默认连接本地 ClientSocket1-Port DEFAULT_PORT; }这里暴露了关键信息它没用 Winsock API 直接编程而是通过 VCL 封装的TClientSocket组件。这意味着所有 socket 操作connect/disconnect/send/receive都被映射为组件属性和事件如OnConnect,OnError,OnRead。这种设计牺牲了灵活性但极大降低了 GUI 交互逻辑的耦合难度——消息接收直接触发OnRead事件你只需在事件处理函数里写Memo1-Lines-Add(ClientSocket1-Socket-ReceiveText());即可。2.2 C Builder 版本选择为什么必须是 6 或 XE 系列网络上流传的BCBTCPClientServerDEMO多数编译于C Builder 62002 年或 C Builder XE22011 年。原因有三VCL Socket 组件弃用时间点TClientSocket/TServerSocket在 C Builder 10.42020中已被标记为deprecated11.02022彻底移除。新版本强制使用TIdTCPClient/TIdTCPServerIndy 库而本 DEMO 未引用 Indy。Unicode 支持断层CB6 默认 ANSI 编码ReceiveText()返回AnsiStringXE2 默认 UnicodeReceiveText()返回UnicodeString。若强行用新版编译Common.h中定义的TPacketHeader结构体含char data[256]会因字符串编码差异导致数据截断。RTL 运行时兼容性msvcp140.dllVS2015 CRT与 CB6 的borlndmm.dllBorland 内存管理器互不兼容。试图用 VS 链接 CB6 编译的目标文件会报LNK2019: unresolved external symbol。✅实操建议若你有合法 CB6 授权常见于老工业软件授权包直接安装并打开.dpr文件编译若无授权可使用C Builder 10.2 Tokyo2017—— 它仍保留TClientSocket组件需手动启用Legacy Components包且支持AnsiString兼容模式项目 → Options → C Options → Character Set → Multi-Byte Character Set。绝对不要尝试用 MinGW 或 Clang 编译VCL 是 Delphi RTL 的 C 封装深度依赖rtl140.bpl等 Borland 特有包GCC 工具链无法链接。2.3 编译前必做的三处代码微调即使环境正确原始 DEMO 也需手动修正才能通过编译。以下是我在 10.2 Tokyo 下实测的最小修改集① 修复Common.h中的结构体对齐问题原始代码struct TPacketHeader { int length; char type; char data[256]; };问题int在 64 位系统默认 8 字节对齐但TClientSocket::SendBuf()发送的是紧凑二进制流。若结构体未显式指定对齐会导致length字段后填充 3 字节服务端recv()读取时错位。✅ 修改为#pragma pack(push, 1) struct TPacketHeader { int length; // 4 bytes char type; // 1 byte char data[256]; // 256 bytes }; // total: 261 bytes #pragma pack(pop)② 修正ServerUnit.cpp中的OnAccept事件参数类型CB6 中TServerSocket::OnAccept原型为void __fastcall TForm1::ServerSocket1Accept(TObject *Sender, TCustomWinSocket *ClientSocket);而 10.2 Tokyo 中签名变为void __fastcall TForm1::ServerSocket1Accept(TObject *Sender, TCustomWinSocket *ClientSocket, TCustomWinSocket *NewSocket);✅ 修改事件声明.h文件和实现.cpp文件将NewSocket参数用于后续通信// ServerUnit.h 中添加 void __fastcall ServerSocket1Accept(TObject *Sender, TCustomWinSocket *ClientSocket, TCustomWinSocket *NewSocket); // ServerUnit.cpp 中实现 void __fastcall TForm1::ServerSocket1Accept(TObject *Sender, TCustomWinSocket *ClientSocket, TCustomWinSocket *NewSocket) { Memo1-Lines-Add(Client connected: NewSocket-RemoteAddress); // 关键将 NewSocket 存入动态数组供 OnRead 事件使用 FClientSockets.push_back(NewSocket); }③ 替换已废弃的AnsiString::c_str()调用原始ClientUnit.cpp中有ClientSocket1-Socket-SendText(AnsiString(HELLO) \r\n);在 Unicode 模式下SendText()期望UnicodeString但AnsiString会隐式转换失败。✅ 统一改为ClientSocket1-Socket-SendText(HELLO\r\n); // 字符串字面量自动转为 UnicodeString3. 运行时行为拆解TCP 连接建立、数据收发、异常断连的 VCL 映射逻辑3.1TClientSocket的状态机从csClosed到csConnected的四步跃迁VCL 的TClientSocket并非简单封装connect()它内置了完整的连接状态机。理解其状态流转是调试连接失败的核心状态值ClientSocket1-State触发条件典型操作常见陷阱csClosed组件创建后初始状态调用Open()启动连接Open()不阻塞立即返回csOpening勿在此刻SendText()csOpeningOpen()调用后等待OnConnect等待事件不可发送数据若服务端未启动此状态会持续约 20 秒Windows 默认 connect timeout期间OnError可能不触发csConnectedOnConnect事件触发此时可安全SendText()/SendBuf()OnConnect仅表示 TCP 三次握手完成不代表服务端应用层已就绪csClosing调用Close()或网络中断OnDisconnect事件触发Close()是异步的State可能仍为csConnected直到OnDisconnect执行完毕✅验证技巧在ClientUnit.cpp的Button1Click连接按钮中插入void __fastcall TForm1::Button1Click(TObject *Sender) { Memo1-Lines-Add(Before Open: State IntToStr(ClientSocket1-State)); // csClosed ClientSocket1-Open(); Memo1-Lines-Add(After Open: State IntToStr(ClientSocket1-State)); // csOpening }然后观察OnConnect事件中void __fastcall TForm1::ClientSocket1Connect(TObject *Sender, TCustomWinSocket *Socket) { Memo1-Lines-Add(OnConnect fired! State IntToStr(ClientSocket1-State)); // csConnected }你会发现After Open日志先于OnConnect输出——这就是 VCL 异步模型的典型表现。3.2 数据收发SendText()vsSendBuf()的本质区别与选型依据TClientSocket提供两种发送接口但它们底层调用完全不同SendText(const UnicodeString Text)底层调用send()但自动追加\r\n作为行尾分隔符RFC 854 Telnet 协议约定适用于文本协议如 HTTP、SMTP、自定义命令行协议编码由Text类型决定UnicodeString→ UTF-16LEWindows 默认AnsiString→ 当前系统代码页如 GBK⚠️ 风险若服务端期望纯二进制数据\r\n会被当作有效载荷导致解析错误。SendBuf(void *Buffer, int Len)直接调用send()零拷贝传输 Buffer 指向的内存块适用于二进制协议如图像传输、加密数据、结构化报文必须确保Buffer生命周期长于发送完成VCL 不做内存复制发送失败时 Buffer 仍需有效✅ 推荐用法TPacketHeader header; header.length 100; header.type D; memcpy(header.data, rawData, 100); ClientSocket1-Socket-SendBuf(header, sizeof(header)); // 发送 261 字节结构体提示SendText()的\r\n追加行为在TServerSocket的OnRead事件中不会被自动剥离服务端收到的是CMD\r\n长度为 6 字节C-M-D-\r-\n-\0。若你的协议要求精确字节匹配必须手动TrimRight()或用SendBuf()。3.3 断连检测OnError事件为何总比OnDisconnect晚一步这是 VCL Socket 最反直觉的设计OnError并非“错误发生时立即触发”而是socket 内核通知应用层错误后的延迟回调。典型场景服务端进程崩溃客户端OnRead事件不再触发 →OnError在约 30 秒后触发TCP Keepalive 默认间隔网线拔掉客户端OnRead立即返回 0EOF→OnDisconnect触发 →OnError在 1~2 秒后才触发内核错误队列清空延迟防火墙拦截OnConnect失败 →OnError立即触发WSAETIMEDOUT。✅可靠断连检测方案必须组合使用在OnRead事件中检查Socket-ReceiveLength()void __fastcall TForm1::ClientSocket1Read(TObject *Sender, TCustomWinSocket *Socket) { int len Socket-ReceiveLength(); // 获取待读字节数 if (len 0) { // 对端关闭连接 Memo1-Lines-Add(Peer closed connection); Socket-Close(); return; } UnicodeString data Socket-ReceiveText(); }启用 TCP Keepalive需手动设置 socket 选项// 在 ClientSocket1-OnConnect 事件中 int keepAlive 1; int keepIdle 60; // 60秒无数据后开始探测 int keepInterval 5; // 每5秒探测一次 setsockopt(Socket-Handle, SOL_SOCKET, SO_KEEPALIVE, (char*)keepAlive, sizeof(keepAlive)); #ifdef _WIN32 // Windows 特有设置 Keepalive 参数 struct tcp_keepalive ka; ka.onoff 1; ka.keepalivetime keepIdle * 1000; ka.keepaliveinterval keepInterval * 1000; DWORD ret; WSAIoctl(Socket-Handle, SIO_KEEPALIVE_VALS, ka, sizeof(ka), NULL, 0, ret, NULL, NULL); #endif实现应用层心跳每 10 秒发送PING包3 次无响应则主动断连。血泪经验只依赖OnError会导致客户端“假在线”长达 2 分钟工业现场设备误判率飙升。必须用ReceiveLength()0 Keepalive 双保险。4. 避坑BCB TCP 编程中 5 个高频翻车点与根因定位4.1 现象客户端能连上服务端但OnRead事件永不触发ReceiveLength()始终返回 0原因服务端未调用Socket-SendText()或SendBuf()发送数据或发送后未刷新缓冲区。VCL 的SendText()内部使用send()但若数据量小 1448 字节TCP Nagle 算法会延迟发送以合并小包。解决服务端OnAccept后立即发送测试数据NewSocket-SendText(WELCOME\r\n);或禁用 Nagle 算法服务端OnAccept中int noDelay 1; setsockopt(NewSocket-Handle, IPPROTO_TCP, TCP_NODELAY, (char*)noDelay, sizeof(noDelay));4.2 现象中文乱码服务端收到的是????或 原因客户端SendText()使用UnicodeStringUTF-16服务端ReceiveText()默认按系统代码页如 GBK解码编码不匹配。解决统一使用AnsiString需在项目设置中关闭 UnicodeAnsiString msg AnsiString(你好).c_str(); // 强制转为当前系统编码 ServerSocket1-Socket-SendText(msg);或服务端用ReceiveBuf()读取原始字节再手动转码char buffer[1024]; int len Socket-ReceiveBuf(buffer, sizeof(buffer)-1); buffer[len] \0; UnicodeString ustr UTF8ToString(AnsiString(buffer)); // 假设客户端发 UTF-84.3 现象服务端OnAccept触发多次但NewSocket指针重复FClientSockets数组中出现相同地址原因TServerSocket的OnAccept事件在连接建立瞬间触发但NewSocket对象生命周期由 VCL 管理。若你在OnAccept中将其存入std::vectorTCustomWinSocket*未做去重且未在OnDisconnect中清理会导致野指针。解决使用TObjectList替代裸指针数组并启用OwnsObjectsfalseTObjectList* FClientSockets; // OnAccept 中 FClientSockets-Add(NewSocket); // VCL 自动管理 NewSocket 生命周期 // OnDisconnect 中需在 ServerUnit.h 声明事件 void __fastcall TForm1::ServerSocket1Disconnect(TObject *Sender, TCustomWinSocket *Socket); void __fastcall TForm1::ServerSocket1Disconnect(TObject *Sender, TCustomWinSocket *Socket) { FClientSockets-Remove(Socket); // 安全移除 }4.4 现象编译通过但运行时报Access violation at address XXXX in module Client.exe堆栈指向ClientSocket1-Open()原因TClientSocket组件未在窗体设计器中正确创建或.dfm文件中ClientSocket1的Left/Top属性为负值VCL 初始化失败。解决在ClientUnit.h的类声明中确认TClientSocket *ClientSocket1;已声明打开ClientUnit.dfm查找object ClientSocket1: TClientSocket块确保Left 0Top 0若.dfm损坏手动在__fastcall TForm1::TForm1(TComponent* Owner)构造函数中创建ClientSocket1 new TClientSocket(this); ClientSocket1-Parent this; ClientSocket1-Address 127.0.0.1; ClientSocket1-Port DEFAULT_PORT;4.5 现象服务端能同时处理 5 个客户端第 6 个连接被拒绝OnError报WSAEMFILE原因Windows 默认每个进程 socket 句柄上限为 512但TServerSocket内部使用select()模型其FD_SETSIZE宏默认为 64即最多监控 64 个 socket。超过此数accept()返回的NewSocket无法加入监听集合。解决编译时定义FD_SETSIZE1024需在项目 → Options → C Compiler → Directories and Conditionals → Conditional defines 中添加或改用TIdTCPServerIndy 库其基于WSAEventSelect无此限制生产环境强烈建议用TIdTCPServer替代TServerSocket后者在高并发下性能瓶颈明显。5. 进阶实战用 Wireshark 抓包逆向分析 BCB TCP 协议定位真实产线通信故障5.1 抓包配置过滤出 BCB DEMO 的 TCP 流量启动Server.exe和Client.exe后在 Wireshark 中设置捕获过滤器tcp.port 8080 ip.addr 127.0.0.1若测试远程连接将127.0.0.1替换为实际 IP。点击 “Start”然后在客户端点击 “Send” 按钮。Wireshark 会显示类似No. Time Source Destination Protocol Length Info 1 0.000000 127.0.0.1:50001 127.0.0.1:8080 TCP 74 50001 → 8080 [SYN] 2 0.000002 127.0.0.1:8080 127.0.0.1:50001 TCP 74 8080 → 50001 [SYN, ACK] 3 0.000004 127.0.0.1:50001 127.0.0.1:8080 TCP 66 50001 → 8080 [ACK] 4 0.000010 127.0.0.1:50001 127.0.0.1:8080 TCP 82 50001 → 8080 [PSH, ACK] # 客户端发送 HELLO\r\n 5 0.000012 127.0.0.1:8080 127.0.0.1:50001 TCP 74 8080 → 50001 [ACK]右键第 4 行 → “Follow” → “TCP Stream”即可看到完整 ASCII 文本GET / HTTP/1.1\r\n Host: localhost:8080\r\n \r\n等等——这不对BCBTCPClientServerDEMO发送的是HELLO\r\n为何抓到 HTTP 请求5.2 协议逆向识别 BCB 的真实数据帧格式真相是TClientSocket::SendText(HELLO\r\n)发送的确实是48 45 4C 4C 4F 0D 0AHEX但 Wireshark 默认按 HTTP 解析。我们需要强制按 Raw 解析在 TCP Stream 窗口中点击 “Save As” → 保存为client_raw.bin用 HxD十六进制编辑器打开确认内容为00000000 48 45 4C 4C 4F 0D 0A HELLO..服务端OnRead事件中ReceiveText()返回HELLO\r\n但ReceiveBuf()读取的是7字节原始数据。✅关键发现BCB 的SendText()/ReceiveText()不处理粘包若客户端连续发送两次HELLO\r\n服务端OnRead可能一次性收到HELLO\r\nHELLO\r\n2 个包粘连也可能分两次收到OnRead触发 2 次。这是 VCL Socket 的固有缺陷——它没有提供OnPacketComplete事件。5.3 粘包解决方案在OnRead中实现简易帧解析Common.h中定义的TPacketHeader就是为解决粘包设计的。服务端OnRead应这样处理void __fastcall TForm1::ServerSocket1Read(TObject *Sender, TCustomWinSocket *Socket) { static std::vectorchar recvBuffer; // 静态缓冲区跨事件保留未解析数据 char tempBuf[1024]; int len Socket-ReceiveBuf(tempBuf, sizeof(tempBuf)); if (len 0) return; recvBuffer.insert(recvBuffer.end(), tempBuf, tempBuf len); // 循环解析完整包 while (recvBuffer.size() sizeof(TPacketHeader)) { TPacketHeader* header (TPacketHeader*)recvBuffer.data(); if (header-length 0 header-length 256 recvBuffer.size() sizeof(TPacketHeader) header-length) { // 提取完整数据包 std::string payload(recvBuffer.begin() sizeof(TPacketHeader), recvBuffer.begin() sizeof(TPacketHeader) header-length); Memo1-Lines-Add(Received packet: type String(header-type) , len IntToStr(header-length) , data AnsiString(payload.c_str())); // 移除已解析部分 recvBuffer.erase(recvBuffer.begin(), recvBuffer.begin() sizeof(TPacketHeader) header-length); } else { break; // 包头不完整等待更多数据 } } }此代码实现了累积接收用static std::vectorchar缓存跨OnRead事件的碎片定长头校验先读sizeof(TPacketHeader)字节验证length是否合理边界切割仅当缓冲区足够长时才提取 payload避免越界访问。我的习惯在真实产线项目中TPacketHeader必加校验和字段如uint16_t crc16并在OnRead中计算验证。没有校验的二进制协议在电磁干扰强的工厂环境中丢包率会飙升。希望帮到你。本文还有配套的精品资源点击获取