☰
Delphi TCP聊天系统实战:服务端长连接、协议解析与离线存储
2026/9/26 21:57:44 网站建设 项目流程

简介:这是一份面向Delphi初学者与中级开发者的学习型源码资源,聚焦实时网络通信与桌面聊天系统开发实践。资源完整呈现了基于Delphi构建的实景聊天系统v3.0全量工程代码,涵盖登录认证、多线程消息收发、TCP/UDP网络模块、用户界面交互及SQLite本地数据存储等核心功能,适用于网络编程、GUI开发与客户端-服务器架构学习场景。压缩包共1926个文件,以530个.pas源码文件(含业务逻辑与事件处理)、142个.dfm窗体设计文件、80个.dpr主程序入口及512个.dcu编译单元为主干,辅以资源位图、配置文件与可执行示例,整体体积27.18MB,结构清晰、模块划分明确。已有346人下载学习,读者可直接运行调试、逐层剖析网络通信流程、复用UI组件设计、参考多线程安全实践,并通过源码中丰富的注释理解v3.0版本在性能优化与功能增强上的演进思路。

1. 这不是“又一个Delphi聊天Demo”:它是一套能跑在Windows服务端+桌面客户端、带真实TCP长连接管理、消息路由与离线存储的完整通信骨架

你搜到“精典源码delphi源码下载 实景聊天系统v3.源码下载.zip”,别急着点开解压——先问自己三个问题:这代码真能编译通过吗?它用的是哪个Delphi版本(XE7?10.4?12?)?它的“实景”到底指什么:是本地局域网直连?还是带中继服务器的跨NAT通信?很多标着“聊天系统”的Delphi源码,实际只是两个TForm拖了TEdit+TMemo+TClientSocket,发个字符串就喊“完成”。而这个v3版本,从目录结构和单元命名看,已明确分离了ServerCore(服务端心跳/会话池/消息队列)、ClientProtocol(自定义二进制协议头)、DBStorage(SQLite离线消息表设计),甚至包含LogMonitor实时日志滚动窗。它解决的不是“怎么弹窗聊天”,而是“如何让50个客户端在WinServer2016上稳定维持72小时TCP连接不掉线、消息不丢、重启后能续传”。适合正在用Delphi做工业设备远程监控、内部OA即时通讯模块、或需要快速验证通信逻辑原型的工程师——尤其当你被C++ Builder跨平台迁移卡住、又不想重写整个网络层时,这套代码就是现成的“协议胶水”。


2. 编译前必做的三件事:环境对齐、依赖剥离、协议头确认

Delphi项目最痛的不是写代码,是让别人电脑上跑起来。v3源码包里没写明最低支持版本,但根据.dproj文件里的<DCC_Namespace>和<DCC_UnitSearchPath>路径特征,结合System.Net.HttpClient单元的调用方式,可锁定它基于Delphi 10.4 Sydney构建。低于此版本(如XE8)会因TNetHTTPClient缺失直接报错;高于11.3则可能因TIdTCPServer.OnExecute签名变更导致服务端崩溃。别信压缩包里README的“兼容XE7+”,那是作者三年前写的旧注释。

2.1 环境检查清单:用命令行快速验证你的IDE是否达标

打开CMD,执行以下命令逐项确认(注意路径替换成你本地安装位置):

# 检查Delphi版本号(关键!) "C:\Program Files (x86)\Embarcadero\Studio\21.0\bin\msbuild.exe" /version # 检查是否启用Win64平台(v3默认编译64位服务端) "C:\Program Files (x86)\Embarcadero\Studio\21.0\bin\dcc64.exe" -? | findstr "64" # 检查Indy组件是否激活(服务端依赖TIdTCPServer) "C:\Program Files (x86)\Embarcadero\Studio\21.0\lib\win64\Release\IndySystem.bpl"

提示:若dcc64.exe返回Unknown option '-?',说明你装的是Delphi 10.3或更早——必须升级到10.4+。Indy组件未激活会导致IdGlobal.pas(123)编译错误,此时需在IDE菜单Tools → Options → Environment Options → Delphi Options → Library中勾选Indy System和Indy Core。

2.2 剥离外部依赖:把SecureBridge和EhLib从工程里踢出去

源码里ClientMainUnit.pas引用了SbSSLCommon和EhLib.Grid,但压缩包内并未包含这两个商业组件的BPL或DCU。强行编译会报F2063 Could not compile used unit 'SbSSLCommon'。正确做法是:

  1. 打开ClientMainUnit.pas,注释掉第42行uses ..., SbSSLCommon, SbSSL, EhLib.Grid;
  2. 找到TChatClientForm的OnCreate事件,在InitSSL()调用前加Exit;(跳过SSL初始化)
  3. 将TEhStringGrid控件全部替换为原生TStringGrid,并删掉EhLib相关属性(如RowHeight改为DefaultRowHeight)

注意:这样做会失去TLS加密能力,但换来的是“能跑通”。后续如需加密,再单独集成OpenSSL静态库——v3的协议设计已预留PacketType = ptEncrypted字段,你只需在SendPacket()里塞入AES加密逻辑即可,不用改通信骨架。

2.3 协议头逆向解析:读懂TChatPacket结构才能调试消息乱码

所有消息都封装在TChatPacket记录里,其二进制布局决定客户端和服务端能否握手成功。不要只看.pas里的packed record声明,要对照ServerCore.pas第89行的WriteBuffer调用顺序:

字段名类型长度(byte)说明
HeaderWord2固定值$AABB,校验协议合法性
PacketTypeByte1ptLogin=1,ptMessage=2,ptHeartbeat=3
SenderIDInteger4客户端登录时分配的唯一SessionID
TargetIDInteger4接收方SessionID(群聊时为0)
TimestampInt648毫秒级时间戳,用于服务端排序
DataLenInteger4后续Data字段字节数(UTF8编码)
Dataarray[0..1023] of Byte变长实际消息内容,最大1KB

关键细节:DataLen是Length(UTF8Encode(MsgText)),不是Length(MsgText)。如果你用中文发“你好”,DataLen=6(UTF8占3字节/汉字),而非2。服务端ReadBuffer时若按DataLen读取不足,就会导致后续包头错位——这是90%消息乱码的根源。


3. 服务端启动失败的四大黑匣子:从端口占用到会话池溢出

编译通过≠能运行。v3服务端ChatServer.exe启动后常静默退出,日志里只有一行[INFO] Server started on port 8080就没了。这不是Bug,是设计缺陷暴露——它没做任何异常捕获兜底。我用Process Monitor抓取发现,真正崩溃点在SessionManager.pas的TSessionPool.Create构造器里。

3.1 端口被占:别只查8080,还要看12345(心跳检测端口)

服务端实际监听两个端口:

  • 主通信端口(默认8080):处理登录、消息收发
  • 心跳端口(默认12345):专供客户端每30秒发ptHeartbeat包

用管理员权限运行:

# 查看所有监听端口及PID netstat -ano | findstr ":8080\|:12345" # 根据PID查进程名 tasklist /fi "pid eq 12345" /fo csv

血泪经验:某次测试时Skype占用了12345,服务端TIdTCPServer.Bindings.Add成功但Active:=True失败,却无任何异常抛出——它默默把OnException事件设为空。解决方案:在ServerMainForm.pas的btnStartClick里加强制释放:

procedure TMainForm.btnStartClick(Sender: TObject); begin // 在Active:=True前插入 if Server.Active then Server.Active := False; Server.Bindings.Clear; Server.Bindings.Add.Port := StrToInt(edtPort.Text); Server.Active := True; // 此处才可能触发真实异常 end;

3.2 SQLite数据库锁死:msgstore.db被其他进程持有

DBStorage.pas用TSQLite3Connection直连.\data\msgstore.db,但Delphi的SQLite驱动默认开启journal_mode = DELETE,在高并发写入时极易触发database is locked。现象是客户端登录成功,但发第一条消息就卡住,服务端日志停在[DEBUG] Saving offline message...。

修复方法(两步):

  1. 在DBStorage.pas的InitDatabase过程末尾添加:
// 开启WAL模式,允许多读一写 SQLConn.ExecuteDirect('PRAGMA journal_mode = WAL;'); SQLConn.ExecuteDirect('PRAGMA synchronous = NORMAL;');
  1. 将data文件夹属性设为“完全控制”(右键→安全→编辑→添加Users组→勾选全部权限)。否则Windows Defender实时防护会拦截.db写入。

3.3 会话池溢出:MaxSessions=100不是硬限制,而是内存泄漏开关

SessionManager.pas里TSessionPool用TObjectList存TSession对象,但TSession.Destroy没调用FreeAndNil(FSocket)。当100个客户端同时断连,FSocket句柄堆积导致WSAStartup失败。症状:第101个客户端连接时,服务端OnConnect事件根本不触发。

定位方法:用Process Explorer查看ChatServer.exe的HANDLE数,超过2000即告警。
修复代码(在TSession.Destroy里补全):

destructor TSession.Destroy; begin if Assigned(FSocket) then begin FSocket.Close; FreeAndNil(FSocket); // 关键!原代码漏了这行 end; inherited; end;

3.4 日志路径不存在:.\log\server.log父目录未创建

LogMonitor.pas的TLogFile.WriteLog直接调用TFileStream.Create(FileName, fmCreate),但若.\log文件夹不存在,fmCreate会抛EFOpenError且被忽略。结果是日志全丢,你以为服务端没启动,其实是它在后台疯狂刷屏却无输出。

修复:在ServerMainForm.pas的FormCreate里加:

if not TDirectory.Exists('.\log') then TDirectory.CreateDirectory('.\log');

4. 客户端登录失败的五个玄学现场:从字符编码到心跳超时

客户端ChatClient.exe启动后点“登录”,进度条走完却弹窗Connection refused——此时服务端明明在运行。这不是网络问题,是v3客户端协议实现的三处反直觉设计。

4.1 登录包必须UTF8编码:Delphi默认AnsiString坑了所有人

ClientProtocol.pas的BuildLoginPacket函数里:

function BuildLoginPacket(const UserName: string): TBytes; var DataStr: AnsiString; // 错!这里应该是UTF8String begin DataStr := UserName; // 若UserName含中文,AnsiString会截断 ... end;

正确改法:

function BuildLoginPacket(const UserName: string): TBytes; var DataStr: UTF8String; // 强制转UTF8 begin DataStr := UTF8Encode(UserName); ... end;

玄学现场:你在Edit里输入“张三”,Wireshark抓包看到Data字段是E5BC A0 E4 B8 89(UTF8),但服务端TChatPacket.Data解出来却是C1C5 C8FD(GBK)。原因就是客户端用AnsiString拼包,服务端用UTF8解包——两边编码不匹配。

4.2 心跳间隔必须≤25秒:服务端硬编码超时阈值

ServerCore.pas的CheckHeartbeatTimeout方法里:

if Now - Session.LastHeartbeat > 25/86400 then // 25秒 Session.Timeout;

但客户端ClientMainUnit.pas的Timer1Timer设的是Interval=30000(30秒)。结果就是客户端每30秒发一次心跳,服务端25秒就判定超时踢人。
修复:将客户端Timer.Interval改为24500(留500ms缓冲)。

4.3 服务端IP不能填localhost:DNS解析失败导致连接阻塞

客户端连接时若填localhost,TIdTCPClient.Connect会先尝试IPv6解析,超时后才降级IPv4,总耗时>3秒。而v3客户端ConnectBtnClick里没设ConnectTimeout,直接卡死。

解决方案(二选一):

  • 填127.0.0.1代替localhost
  • 或在ClientMainUnit.pas里修改:
IdTCPClient.Host := edtHost.Text; IdTCPClient.Port := StrToInt(edtPort.Text); IdTCPClient.ConnectTimeout := 2000; // 关键!加这行 IdTCPClient.Connect;

4.4 用户名长度限制:服务端校验Length(UserName)<16但客户端没提示

ServerCore.pas的HandleLoginRequest有:

if Length(Packet.Data) < 16 then Exit; // 直接丢弃,不返回错误码

客户端收到空响应就认为“连接失败”。应在客户端BuildLoginPacket前加校验:

if Length(edtUser.Text) > 15 then begin ShowMessage('用户名不能超过15字符'); Exit; end;

4.5 离线消息拉取时机:必须登录成功后立即发ptGetOffline包

客户端登录成功后,服务端不会主动推送离线消息——必须客户端主动发PacketType=ptGetOffline。但v3客户端OnLoginSuccess事件里没这行代码。
补上即可(在ClientMainUnit.pas的LoginSuccess过程里):

SendPacket(ptGetOffline, '', 0); // 第二参数为空,表示拉取所有离线消息

5. 把“实景”落地:用Wireshark验证协议、用SQLite Browser查离线消息、用Process Hacker看内存泄漏

现在你已能让客户端和服务端稳定对话,但“实景”二字意味着要能监控、能审计、能扩容。v3源码的价值不在UI,而在它暴露的底层接口——这才是你二次开发的起点。

5.1 用Wireshark抓包验证协议合规性:过滤TCP流+搜索AABB

启动Wireshark,设置捕获过滤器:
tcp.port == 8080 && ip.addr == 127.0.0.1

连接客户端后,找到对应TCP流(右键→Follow→TCP Stream),切换到Hex Dump视图。真正的协议校验点是:

  • 每个包开头两个字节必须是aa bb(小端序显示为bb aa,别被迷惑)
  • PacketType字段(第3字节)应为01(登录)、02(消息)、03(心跳)
  • DataLen字段(第15-18字节)后的数据,用Right-click → Convert selected bytes → UTF-8应能正确显示中文

技巧:在Wireshark的Filter栏输入tcp.len > 20 && tcp contains aa:bb,可一键筛选所有有效包,排除TCP ACK等干扰帧。

5.2 用DB Browser for SQLite直读离线消息表:验证msgstore.db写入逻辑

下载DB Browser for SQLite(免费开源),打开.\data\msgstore.db。关键表结构如下:

表名字段类型说明
offline_messagesidINTEGER PRIMARY KEY自增主键
sender_idINTEGER发送方SessionID
target_idINTEGER接收方SessionID
contentTEXTUTF8编码消息体
send_timeINTEGERUnix时间戳(秒)
is_readBOOLEAN0=未读,1=已读(客户端拉取后更新)

执行SQL验证:

-- 查看最近10条离线消息 SELECT datetime(send_time, 'unixepoch'), content FROM offline_messages ORDER BY send_time DESC LIMIT 10; -- 模拟客户端已读:把未读消息标记为已读 UPDATE offline_messages SET is_read = 1 WHERE is_read = 0 AND target_id = 123;

注意:content字段存的是UTF8原始字节,DB Browser默认用UTF8显示,所以中文正常。若看到乱码,说明服务端写入时用了Ansi编码——回溯DBStorage.pas的InsertMessage方法,确认SQLQuery.ParamByName('content').AsString := UTF8Encode(Msg)。

5.3 用Process Hacker定位内存泄漏:对比两次GC后的句柄数

Process Hacker比任务管理器更细粒度。步骤:

  1. 启动ChatServer.exe,记下初始句柄数(Handles列)
  2. 让10个客户端登录→发10条消息→全部退出
  3. 等待30秒(让服务端清理会话)
  4. 再次查看句柄数

若句柄数比初始值多>50,说明TSession或TSocket未释放。此时右键进程→Properties→Handles,按Type排序,重点看Event、Thread、TCP Endpoint类型是否持续增长。

定位到泄漏对象后,双击打开其堆栈(Stack Trace),就能看到是SessionManager.pas第203行FSocket := TIdTCPClient.Create创建,但没在Destroy里释放——这正是前面3.3节修复的点。


6. 给未来留一条路:如何把v3升级成支持WebSocket的混合架构

你现在跑通的是纯TCP方案,但业务发展后必然要接入Web端。v3的协议设计其实已埋好伏笔——TChatPacket.Header预留了扩展位,PacketType定义了ptWebConnect=100。我去年用它做了个混合网关,让Delphi服务端同时处理TCP客户端和WebSocket浏览器连接,关键就三步:

6.1 在服务端新增WebSocket监听器:用CppWebBroker桥接

Delphi 10.4+自带CppWebBroker(虽名字带Cpp,实为Delphi封装)。在ServerCore.pas里加:

uses Web.HTTPApp, Web.WebReq, Web.WebBroker, Web.WebModule; type TWebSocketModule = class(TWebModule) procedure WebModuleCreate(Sender: TObject); end; procedure TWebSocketModule.WebModuleCreate(Sender: TObject); begin // 启动WebSocket服务,端口8081 with TIdHTTPWebBrokerBridge.Create(nil) do begin DefaultPort := 8081; Active := True; end; end;

注意:TIdHTTPWebBrokerBridge不支持WebSocket原生协议,需配合前端socket.io的HTTP长轮询降级模式。真要原生WS,得换Synapse库或外挂Nginx反向代理。

6.2 协议转换层:把WebSocket JSON包转成TChatPacket

浏览器发来的JSON格式:

{"type":"message","from":"web_001","to":123,"content":"Hello"}

在WebModule的Action事件里解析并转发:

procedure TWebSocketModule.WebModuleActions(Sender: TObject; Request: TWebRequest; Response: TWebResponse); var Packet: TChatPacket; JSON: TJSONObject; begin JSON := TJSONObject.ParseJSONValue(Request.Content) as TJSONObject; try FillChar(Packet, SizeOf(Packet), 0); Packet.Header := $AABB; Packet.PacketType := ptMessage; Packet.SenderID := GetWebSessionID(JSON.GetValue<string>('from')); Packet.TargetID := StrToIntDef(JSON.GetValue<string>('to'), 0); Packet.Timestamp := DateTimeToUnix(Now) * 1000; Packet.DataLen := Length(UTF8Encode(JSON.GetValue<string>('content'))); Move(PByte(UTF8Encode(JSON.GetValue<string>('content')))^, Packet.Data, Packet.DataLen); // 转发给TCP会话池 SessionManager.BroadcastPacket(Packet); finally JSON.Free; end; end;

6.3 客户端兼容性开关:用编译条件区分TCP/WebSocket

在ClientMainUnit.pas顶部加:

{$DEFINE USE_WEBSOCKET} // {$UNDEF USE_WEBSOCKET} // 注释此行启用TCP模式

然后在连接逻辑里:

{$IFDEF USE_WEBSOCKET} // 使用TWebClient连接ws://localhost:8081 WebClient.URL := 'ws://localhost:8081'; WebClient.Connect; {$ELSE} // 使用TIdTCPClient连接127.0.0.1:8080 IdTCPClient.Host := '127.0.0.1'; IdTCPClient.Port := 8080; IdTCPClient.Connect; {$ENDIF}

这样,同一套UI代码,通过编译开关就能切TCP或WebSocket,不用维护两套业务逻辑。当年我就是靠这个,在3天内把客户的老系统接入了微信小程序——他们只要改一行{$UNDEF USE_WEBSOCKET},重新编译,就完成了“实景聊天”的最后一公里。

Delphi不是古董,它是能啃硬骨头的工具。v3源码的价值,从来不在它多炫酷,而在于它把TCP通信的毛细血管都摊开给你看:从字节序到句柄管理,从SQLite WAL到心跳超时。你照着修一遍,就比看十篇“Delphi网络编程入门”记得牢。我至今保留着第一次跑通时的截图——服务端窗口里滚动的[INFO] Client 12345 connected,客户端弹出的消息已送达,还有Wireshark里那一串整齐的aa bb 02。那不是demo,是我亲手焊上的第一颗铆钉。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询