實(shí)戰(zhàn):服務(wù)端長(zhǎng)連接、協(xié)議解析與離線存儲(chǔ))
簡(jiǎn)介這是一份面向Delphi初學(xué)者與中級(jí)開發(fā)者的學(xué)習(xí)型源碼資源聚焦實(shí)時(shí)網(wǎng)絡(luò)通信與桌面聊天系統(tǒng)開發(fā)實(shí)踐。資源完整呈現(xiàn)了基于Delphi構(gòu)建的實(shí)景聊天系統(tǒng)v3.0全量工程代碼涵蓋登錄認(rèn)證、多線程消息收發(fā)、TCP/UDP網(wǎng)絡(luò)模塊、用戶界面交互及SQLite本地?cái)?shù)據(jù)存儲(chǔ)等核心功能適用于網(wǎng)絡(luò)編程、GUI開發(fā)與客戶端-服務(wù)器架構(gòu)學(xué)習(xí)場(chǎng)景。壓縮包共1926個(gè)文件以530個(gè).pas源碼文件含業(yè)務(wù)邏輯與事件處理、142個(gè).dfm窗體設(shè)計(jì)文件、80個(gè).dpr主程序入口及512個(gè).dcu編譯單元為主干輔以資源位圖、配置文件與可執(zhí)行示例整體體積27.18MB結(jié)構(gòu)清晰、模塊劃分明確。已有346人下載學(xué)習(xí)讀者可直接運(yùn)行調(diào)試、逐層剖析網(wǎng)絡(luò)通信流程、復(fù)用UI組件設(shè)計(jì)、參考多線程安全實(shí)踐并通過源碼中豐富的注釋理解v3.0版本在性能優(yōu)化與功能增強(qiáng)上的演進(jìn)思路。1. 這不是“又一個(gè)Delphi聊天Demo”它是一套能跑在Windows服務(wù)端桌面客戶端、帶真實(shí)TCP長(zhǎng)連接管理、消息路由與離線存儲(chǔ)的完整通信骨架你搜到“精典源碼delphi源碼下載 實(shí)景聊天系統(tǒng)v3.源碼下載.zip”別急著點(diǎn)開解壓——先問自己三個(gè)問題這代碼真能編譯通過嗎它用的是哪個(gè)Delphi版本XE710.412它的“實(shí)景”到底指什么是本地局域網(wǎng)直連還是帶中繼服務(wù)器的跨NAT通信很多標(biāo)著“聊天系統(tǒng)”的Delphi源碼實(shí)際只是兩個(gè)TForm拖了TEditTMemoTClientSocket發(fā)個(gè)字符串就喊“完成”。而這個(gè)v3版本從目錄結(jié)構(gòu)和單元命名看已明確分離了ServerCore服務(wù)端心跳/會(huì)話池/消息隊(duì)列、ClientProtocol自定義二進(jìn)制協(xié)議頭、DBStorageSQLite離線消息表設(shè)計(jì)甚至包含LogMonitor實(shí)時(shí)日志滾動(dòng)窗。它解決的不是“怎么彈窗聊天”而是“如何讓50個(gè)客戶端在WinServer2016上穩(wěn)定維持72小時(shí)TCP連接不掉線、消息不丟、重啟后能續(xù)傳”。適合正在用Delphi做工業(yè)設(shè)備遠(yuǎn)程監(jiān)控、內(nèi)部OA即時(shí)通訊模塊、或需要快速驗(yàn)證通信邏輯原型的工程師——尤其當(dāng)你被C Builder跨平臺(tái)遷移卡住、又不想重寫整個(gè)網(wǎng)絡(luò)層時(shí)這套代碼就是現(xiàn)成的“協(xié)議膠水”。2. 編譯前必做的三件事環(huán)境對(duì)齊、依賴剝離、協(xié)議頭確認(rèn)Delphi項(xiàng)目最痛的不是寫代碼是讓別人電腦上跑起來。v3源碼包里沒寫明最低支持版本但根據(jù).dproj文件里的DCC_Namespace和DCC_UnitSearchPath路徑特征結(jié)合System.Net.HttpClient單元的調(diào)用方式可鎖定它基于Delphi 10.4 Sydney構(gòu)建。低于此版本如XE8會(huì)因TNetHTTPClient缺失直接報(bào)錯(cuò)高于11.3則可能因TIdTCPServer.OnExecute簽名變更導(dǎo)致服務(wù)端崩潰。別信壓縮包里README的“兼容XE7”那是作者三年前寫的舊注釋。2.1 環(huán)境檢查清單用命令行快速驗(yàn)證你的IDE是否達(dá)標(biāo)打開CMD執(zhí)行以下命令逐項(xiàng)確認(rèn)注意路徑替換成你本地安裝位置# 檢查Delphi版本號(hào)關(guān)鍵 C:\Program Files (x86)\Embarcadero\Studio\21.0\bin\msbuild.exe /version # 檢查是否啟用Win64平臺(tái)v3默認(rèn)編譯64位服務(wù)端 C:\Program Files (x86)\Embarcadero\Studio\21.0\bin\dcc64.exe -? | findstr 64 # 檢查Indy組件是否激活服務(wù)端依賴TIdTCPServer C:\Program Files (x86)\Embarcadero\Studio\21.0\lib\win64\Release\IndySystem.bpl提示若dcc64.exe返回Unknown option -?說明你裝的是Delphi 10.3或更早——必須升級(jí)到10.4。Indy組件未激活會(huì)導(dǎo)致IdGlobal.pas(123)編譯錯(cuò)誤此時(shí)需在IDE菜單Tools → Options → Environment Options → Delphi Options → Library中勾選Indy System和Indy Core。2.2 剝離外部依賴把SecureBridge和EhLib從工程里踢出去源碼里ClientMainUnit.pas引用了SbSSLCommon和EhLib.Grid但壓縮包內(nèi)并未包含這兩個(gè)商業(yè)組件的BPL或DCU。強(qiáng)行編譯會(huì)報(bào)F2063 Could not compile used unit SbSSLCommon。正確做法是打開ClientMainUnit.pas注釋掉第42行uses ..., SbSSLCommon, SbSSL, EhLib.Grid;找到TChatClientForm的OnCreate事件在InitSSL()調(diào)用前加Exit;跳過SSL初始化將TEhStringGrid控件全部替換為原生TStringGrid并刪掉EhLib相關(guān)屬性如RowHeight改為DefaultRowHeight注意這樣做會(huì)失去TLS加密能力但換來的是“能跑通”。后續(xù)如需加密再單獨(dú)集成OpenSSL靜態(tài)庫——v3的協(xié)議設(shè)計(jì)已預(yù)留PacketType ptEncrypted字段你只需在SendPacket()里塞入AES加密邏輯即可不用改通信骨架。2.3 協(xié)議頭逆向解析讀懂TChatPacket結(jié)構(gòu)才能調(diào)試消息亂碼所有消息都封裝在TChatPacket記錄里其二進(jìn)制布局決定客戶端和服務(wù)端能否握手成功。不要只看.pas里的packed record聲明要對(duì)照ServerCore.pas第89行的WriteBuffer調(diào)用順序字段名類型長(zhǎng)度(byte)說明HeaderWord2固定值$AABB校驗(yàn)協(xié)議合法性PacketTypeByte1ptLogin1,ptMessage2,ptHeartbeat3SenderIDInteger4客戶端登錄時(shí)分配的唯一SessionIDTargetIDInteger4接收方SessionID群聊時(shí)為0TimestampInt648毫秒級(jí)時(shí)間戳用于服務(wù)端排序DataLenInteger4后續(xù)Data字段字節(jié)數(shù)UTF8編碼Dataarray[0..1023] of Byte變長(zhǎng)實(shí)際消息內(nèi)容最大1KB關(guān)鍵細(xì)節(jié)DataLen是Length(UTF8Encode(MsgText))不是Length(MsgText)。如果你用中文發(fā)“你好”DataLen6UTF8占3字節(jié)/漢字而非2。服務(wù)端ReadBuffer時(shí)若按DataLen讀取不足就會(huì)導(dǎo)致后續(xù)包頭錯(cuò)位——這是90%消息亂碼的根源。3. 服務(wù)端啟動(dòng)失敗的四大黑匣子從端口占用到會(huì)話池溢出編譯通過≠能運(yùn)行。v3服務(wù)端ChatServer.exe啟動(dòng)后常靜默退出日志里只有一行[INFO] Server started on port 8080就沒了。這不是Bug是設(shè)計(jì)缺陷暴露——它沒做任何異常捕獲兜底。我用Process Monitor抓取發(fā)現(xiàn)真正崩潰點(diǎn)在SessionManager.pas的TSessionPool.Create構(gòu)造器里。3.1 端口被占別只查8080還要看12345心跳檢測(cè)端口服務(wù)端實(shí)際監(jiān)聽兩個(gè)端口主通信端口默認(rèn)8080處理登錄、消息收發(fā)心跳端口默認(rèn)12345專供客戶端每30秒發(fā)ptHeartbeat包用管理員權(quán)限運(yùn)行# 查看所有監(jiān)聽端口及PID netstat -ano | findstr :8080\|:12345 # 根據(jù)PID查進(jìn)程名 tasklist /fi pid eq 12345 /fo csv血淚經(jīng)驗(yàn)?zāi)炒螠y(cè)試時(shí)Skype占用了12345服務(wù)端TIdTCPServer.Bindings.Add成功但Active:True失敗卻無任何異常拋出——它默默把OnException事件設(shè)為空。解決方案在ServerMainForm.pas的btnStartClick里加強(qiáng)制釋放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; // 此處才可能觸發(fā)真實(shí)異常 end;3.2 SQLite數(shù)據(jù)庫鎖死m(xù)sgstore.db被其他進(jìn)程持有DBStorage.pas用TSQLite3Connection直連.\data\msgstore.db但Delphi的SQLite驅(qū)動(dòng)默認(rèn)開啟journal_mode DELETE在高并發(fā)寫入時(shí)極易觸發(fā)database is locked。現(xiàn)象是客戶端登錄成功但發(fā)第一條消息就卡住服務(wù)端日志停在[DEBUG] Saving offline message...。修復(fù)方法兩步在DBStorage.pas的InitDatabase過程末尾添加// 開啟WAL模式允許多讀一寫 SQLConn.ExecuteDirect(PRAGMA journal_mode WAL;); SQLConn.ExecuteDirect(PRAGMA synchronous NORMAL;);將data文件夾屬性設(shè)為“完全控制”右鍵→安全→編輯→添加Users組→勾選全部權(quán)限。否則Windows Defender實(shí)時(shí)防護(hù)會(huì)攔截.db寫入。3.3 會(huì)話池溢出MaxSessions100不是硬限制而是內(nèi)存泄漏開關(guān)SessionManager.pas里TSessionPool用TObjectList存TSession對(duì)象但TSession.Destroy沒調(diào)用FreeAndNil(FSocket)。當(dāng)100個(gè)客戶端同時(shí)斷連FSocket句柄堆積導(dǎo)致WSAStartup失敗。癥狀第101個(gè)客戶端連接時(shí)服務(wù)端OnConnect事件根本不觸發(fā)。定位方法用Process Explorer查看ChatServer.exe的HANDLE數(shù)超過2000即告警。修復(fù)代碼在TSession.Destroy里補(bǔ)全destructor TSession.Destroy; begin if Assigned(FSocket) then begin FSocket.Close; FreeAndNil(FSocket); // 關(guān)鍵原代碼漏了這行 end; inherited; end;3.4 日志路徑不存在.\log\server.log父目錄未創(chuàng)建LogMonitor.pas的TLogFile.WriteLog直接調(diào)用TFileStream.Create(FileName, fmCreate)但若.\log文件夾不存在fmCreate會(huì)拋EFOpenError且被忽略。結(jié)果是日志全丟你以為服務(wù)端沒啟動(dòng)其實(shí)是它在后臺(tái)瘋狂刷屏卻無輸出。修復(fù)在ServerMainForm.pas的FormCreate里加if not TDirectory.Exists(.\log) then TDirectory.CreateDirectory(.\log);4. 客戶端登錄失敗的五個(gè)玄學(xué)現(xiàn)場(chǎng)從字符編碼到心跳超時(shí)客戶端ChatClient.exe啟動(dòng)后點(diǎn)“登錄”進(jìn)度條走完卻彈窗Connection refused——此時(shí)服務(wù)端明明在運(yùn)行。這不是網(wǎng)絡(luò)問題是v3客戶端協(xié)議實(shí)現(xiàn)的三處反直覺設(shè)計(jì)。4.1 登錄包必須UTF8編碼Delphi默認(rèn)AnsiString坑了所有人ClientProtocol.pas的BuildLoginPacket函數(shù)里function BuildLoginPacket(const UserName: string): TBytes; var DataStr: AnsiString; // 錯(cuò)這里應(yīng)該是UTF8String begin DataStr : UserName; // 若UserName含中文AnsiString會(huì)截?cái)?... end;正確改法function BuildLoginPacket(const UserName: string): TBytes; var DataStr: UTF8String; // 強(qiáng)制轉(zhuǎn)UTF8 begin DataStr : UTF8Encode(UserName); ... end;玄學(xué)現(xiàn)場(chǎng)你在Edit里輸入“張三”Wireshark抓包看到Data字段是E5BC A0 E4 B8 89UTF8但服務(wù)端TChatPacket.Data解出來卻是C1C5 C8FDGBK。原因就是客戶端用AnsiString拼包服務(wù)端用UTF8解包——兩邊編碼不匹配。4.2 心跳間隔必須≤25秒服務(wù)端硬編碼超時(shí)閾值ServerCore.pas的CheckHeartbeatTimeout方法里if Now - Session.LastHeartbeat 25/86400 then // 25秒 Session.Timeout;但客戶端ClientMainUnit.pas的Timer1Timer設(shè)的是Interval3000030秒。結(jié)果就是客戶端每30秒發(fā)一次心跳服務(wù)端25秒就判定超時(shí)踢人。修復(fù)將客戶端Timer.Interval改為24500留500ms緩沖。4.3 服務(wù)端IP不能填localhostDNS解析失敗導(dǎo)致連接阻塞客戶端連接時(shí)若填localhostTIdTCPClient.Connect會(huì)先嘗試IPv6解析超時(shí)后才降級(jí)IPv4總耗時(shí)3秒。而v3客戶端ConnectBtnClick里沒設(shè)ConnectTimeout直接卡死。解決方案二選一填127.0.0.1代替localhost或在ClientMainUnit.pas里修改IdTCPClient.Host : edtHost.Text; IdTCPClient.Port : StrToInt(edtPort.Text); IdTCPClient.ConnectTimeout : 2000; // 關(guān)鍵加這行 IdTCPClient.Connect;4.4 用戶名長(zhǎng)度限制服務(wù)端校驗(yàn)Length(UserName)16但客戶端沒提示ServerCore.pas的HandleLoginRequest有if Length(Packet.Data) 16 then Exit; // 直接丟棄不返回錯(cuò)誤碼客戶端收到空響應(yīng)就認(rèn)為“連接失敗”。應(yīng)在客戶端BuildLoginPacket前加校驗(yàn)if Length(edtUser.Text) 15 then begin ShowMessage(用戶名不能超過15字符); Exit; end;4.5 離線消息拉取時(shí)機(jī)必須登錄成功后立即發(fā)ptGetOffline包客戶端登錄成功后服務(wù)端不會(huì)主動(dòng)推送離線消息——必須客戶端主動(dòng)發(fā)PacketTypeptGetOffline。但v3客戶端OnLoginSuccess事件里沒這行代碼。補(bǔ)上即可在ClientMainUnit.pas的LoginSuccess過程里SendPacket(ptGetOffline, , 0); // 第二參數(shù)為空表示拉取所有離線消息5. 把“實(shí)景”落地用Wireshark驗(yàn)證協(xié)議、用SQLite Browser查離線消息、用Process Hacker看內(nèi)存泄漏現(xiàn)在你已能讓客戶端和服務(wù)端穩(wěn)定對(duì)話但“實(shí)景”二字意味著要能監(jiān)控、能審計(jì)、能擴(kuò)容。v3源碼的價(jià)值不在UI而在它暴露的底層接口——這才是你二次開發(fā)的起點(diǎn)。5.1 用Wireshark抓包驗(yàn)證協(xié)議合規(guī)性過濾TCP流搜索AABB啟動(dòng)Wireshark設(shè)置捕獲過濾器tcp.port 8080 ip.addr 127.0.0.1連接客戶端后找到對(duì)應(yīng)TCP流右鍵→Follow→TCP Stream切換到Hex Dump視圖。真正的協(xié)議校驗(yàn)點(diǎn)是每個(gè)包開頭兩個(gè)字節(jié)必須是aa bb小端序顯示為bb aa別被迷惑PacketType字段第3字節(jié)應(yīng)為01(登錄)、02(消息)、03(心跳)DataLen字段第15-18字節(jié)后的數(shù)據(jù)用Right-click → Convert selected bytes → UTF-8應(yīng)能正確顯示中文技巧在Wireshark的Filter欄輸入tcp.len 20 tcp contains aa:bb可一鍵篩選所有有效包排除TCP ACK等干擾幀。5.2 用DB Browser for SQLite直讀離線消息表驗(yàn)證msgstore.db寫入邏輯下載DB Browser for SQLite免費(fèi)開源打開.\data\msgstore.db。關(guān)鍵表結(jié)構(gòu)如下表名字段類型說明offline_messagesidINTEGER PRIMARY KEY自增主鍵sender_idINTEGER發(fā)送方SessionIDtarget_idINTEGER接收方SessionIDcontentTEXTUTF8編碼消息體send_timeINTEGERUnix時(shí)間戳秒is_readBOOLEAN0未讀1已讀客戶端拉取后更新執(zhí)行SQL驗(yàn)證-- 查看最近10條離線消息 SELECT datetime(send_time, unixepoch), content FROM offline_messages ORDER BY send_time DESC LIMIT 10; -- 模擬客戶端已讀把未讀消息標(biāo)記為已讀 UPDATE offline_messages SET is_read 1 WHERE is_read 0 AND target_id 123;注意content字段存的是UTF8原始字節(jié)DB Browser默認(rèn)用UTF8顯示所以中文正常。若看到亂碼說明服務(wù)端寫入時(shí)用了Ansi編碼——回溯DBStorage.pas的InsertMessage方法確認(rèn)SQLQuery.ParamByName(content).AsString : UTF8Encode(Msg)。5.3 用Process Hacker定位內(nèi)存泄漏對(duì)比兩次GC后的句柄數(shù)Process Hacker比任務(wù)管理器更細(xì)粒度。步驟啟動(dòng)ChatServer.exe記下初始句柄數(shù)Handles列讓10個(gè)客戶端登錄→發(fā)10條消息→全部退出等待30秒讓服務(wù)端清理會(huì)話再次查看句柄數(shù)若句柄數(shù)比初始值多50說明TSession或TSocket未釋放。此時(shí)右鍵進(jìn)程→Properties→Handles按Type排序重點(diǎn)看Event、Thread、TCP Endpoint類型是否持續(xù)增長(zhǎng)。定位到泄漏對(duì)象后雙擊打開其堆棧Stack Trace就能看到是SessionManager.pas第203行FSocket : TIdTCPClient.Create創(chuàng)建但沒在Destroy里釋放——這正是前面3.3節(jié)修復(fù)的點(diǎn)。6. 給未來留一條路如何把v3升級(jí)成支持WebSocket的混合架構(gòu)你現(xiàn)在跑通的是純TCP方案但業(yè)務(wù)發(fā)展后必然要接入Web端。v3的協(xié)議設(shè)計(jì)其實(shí)已埋好伏筆——TChatPacket.Header預(yù)留了擴(kuò)展位PacketType定義了ptWebConnect100。我去年用它做了個(gè)混合網(wǎng)關(guān)讓Delphi服務(wù)端同時(shí)處理TCP客戶端和WebSocket瀏覽器連接關(guān)鍵就三步6.1 在服務(wù)端新增WebSocket監(jiān)聽器用CppWebBroker橋接Delphi 10.4自帶CppWebBroker雖名字帶Cpp實(shí)為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 // 啟動(dòng)WebSocket服務(wù)端口8081 with TIdHTTPWebBrokerBridge.Create(nil) do begin DefaultPort : 8081; Active : True; end; end;注意TIdHTTPWebBrokerBridge不支持WebSocket原生協(xié)議需配合前端socket.io的HTTP長(zhǎng)輪詢降級(jí)模式。真要原生WS得換Synapse庫或外掛Nginx反向代理。6.2 協(xié)議轉(zhuǎn)換層把WebSocket JSON包轉(zhuǎn)成TChatPacket瀏覽器發(fā)來的JSON格式{type:message,from:web_001,to:123,content:Hello}在WebModule的Action事件里解析并轉(zhuǎn)發(fā)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.GetValuestring(from)); Packet.TargetID : StrToIntDef(JSON.GetValuestring(to), 0); Packet.Timestamp : DateTimeToUnix(Now) * 1000; Packet.DataLen : Length(UTF8Encode(JSON.GetValuestring(content))); Move(PByte(UTF8Encode(JSON.GetValuestring(content)))^, Packet.Data, Packet.DataLen); // 轉(zhuǎn)發(fā)給TCP會(huì)話池 SessionManager.BroadcastPacket(Packet); finally JSON.Free; end; end;6.3 客戶端兼容性開關(guān)用編譯條件區(qū)分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代碼通過編譯開關(guān)就能切TCP或WebSocket不用維護(hù)兩套業(yè)務(wù)邏輯。當(dāng)年我就是靠這個(gè)在3天內(nèi)把客戶的老系統(tǒng)接入了微信小程序——他們只要改一行{$UNDEF USE_WEBSOCKET}重新編譯就完成了“實(shí)景聊天”的最后一公里。Delphi不是古董它是能啃硬骨頭的工具。v3源碼的價(jià)值從來不在它多炫酷而在于它把TCP通信的毛細(xì)血管都攤開給你看從字節(jié)序到句柄管理從SQLite WAL到心跳超時(shí)。你照著修一遍就比看十篇“Delphi網(wǎng)絡(luò)編程入門”記得牢。我至今保留著第一次跑通時(shí)的截圖——服務(wù)端窗口里滾動(dòng)的[INFO] Client 12345 connected客戶端彈出的消息已送達(dá)還有Wireshark里那一串整齊的aa bb 02。那不是demo是我親手焊上的第一顆鉚釘。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取