端安裝與實(shí)戰(zhàn)指南)
簡(jiǎn)介面向Delphi 12.3開發(fā)者的sgcWebSockets企業(yè)版WebSocket服務(wù)器控件壓縮包是一份面向企業(yè)級(jí)實(shí)時(shí)通信場(chǎng)景的專業(yè)組件資源適合正在構(gòu)建聊天室、行情推送系統(tǒng)、遠(yuǎn)程監(jiān)控平臺(tái)或需要WebSocket高并發(fā)服務(wù)端的開發(fā)團(tuán)隊(duì)使用。壓縮包整體約151.44MB內(nèi)附企業(yè)版核心組件、安裝配置文檔及多組可供調(diào)用的高級(jí)API能夠幫助開發(fā)者在Delphi中直接借助標(biāo)準(zhǔn)WebSocket協(xié)議完成雙向通信不再需要自行封裝底層數(shù)據(jù)幀。已有169人學(xué)習(xí)下載尤其適合熟悉Delphi卻希望快速補(bǔ)齊實(shí)時(shí)通信功能的中高級(jí)程序員。借助這份壓縮包可獲得帶身份驗(yàn)證、安全加密、負(fù)載均衡、高級(jí)路由等特性的現(xiàn)成功能模塊隨附的說明文件則具體講解了在Delphi 12.3環(huán)境下的安裝、服務(wù)器參數(shù)定制、安全選項(xiàng)調(diào)整以及服務(wù)端代碼編寫清單足以幫助開發(fā)者縮短從下載到部署的熟悉周期將更多精力集中在業(yè)務(wù)邏輯之上高效交付實(shí)時(shí)網(wǎng)絡(luò)應(yīng)用。1. 從 HTTP 輪詢到全雙工Delphi 12.3 里為什么值得換 WebSocket做實(shí)時(shí)監(jiān)控和行情推送老方案是幾秒一次 HTTP 輪詢延遲和服務(wù)器負(fù)載都大變化也要一個(gè)請(qǐng)求周期才能到達(dá)。WebSocket 在 TCP 上做全雙工通信握手上是 HTTP 升級(jí)之后服務(wù)端可以主動(dòng)推送瀏覽器端體驗(yàn)完全不同。sgcWebSockets Enterprise V2023.5 就是給 Delphi 12.3 準(zhǔn)備的服務(wù)端 WebSocket 控件。FS 代表帶完整源碼D12 對(duì)應(yīng) Delphi 12 的目標(biāo)版本。它把握手、連接管理、身份校驗(yàn)、心跳?;睢V播推送這些環(huán)節(jié)做成組件事件適合用 Delphi 寫桌面端和服務(wù)端、又要把數(shù)據(jù)實(shí)時(shí)推到網(wǎng)頁(yè)或移動(dòng)端的團(tuán)隊(duì)。2. 解壓即編譯sgcWebSockets Enterprise V2023.5 在 Delphi 12.3 的安裝路線安裝這類控件最怕的是版本名和 IDE 版本對(duì)不上。先看后綴FS 指 Full Source包內(nèi)會(huì)帶上 .pas 單元而不是只有 .dcuD12 表示這個(gè)包按 Delphi 12 系列編譯V2023.5 是 sgcWebSockets 的版本快照。壓縮包里通常還帶一個(gè)安裝說明.txt我一般先讀這份文件因?yàn)槔锩鏁?huì)寫清楚它測(cè)試過的 IDE 版本和依賴項(xiàng)比網(wǎng)上二手教程可靠。2.1 解壓后先做目錄規(guī)劃建議不要解壓到 Delphi 的安裝目錄而是放到獨(dú)立的第三方庫(kù)目錄例如D:\ThirdParty\sgcWebSockets_V2023.5。原因是后續(xù)升級(jí) IDE 或換版本時(shí)只需要改 Library 路徑不用動(dòng)系統(tǒng)目錄。解壓后應(yīng)該能看到sgcWebSocket.pas、sgcWebSocketServer.pas、sgcWebSocketClient.pas等服務(wù)端和客戶端的核心單元以及一組.dpk包文件。這里給一個(gè)常見的包文件命名習(xí)慣實(shí)際以你解壓出來的為準(zhǔn)文件后綴含義D12對(duì)應(yīng) Delphi 12包名里通常帶 D12 或 IDE 版本號(hào)Enterprise企業(yè)版包含 TLS/SSL、壓縮、高級(jí)認(rèn)證等增強(qiáng)功能FSFull Source附帶完整 .pas 源代碼安裝說明.txt官方安裝步驟務(wù)必先讀2.2 配置 Library 搜索路徑在 Delphi 12.3 菜單里打開Tools - Options - Language - Delphi - Library把D:\ThirdParty\sgcWebSockets_V2023.5和它的源碼子目錄加進(jìn) Library path。這里有一個(gè)常用技巧把源碼根目錄整個(gè)加進(jìn)去之后Delphi 會(huì)遞歸搜索 .pas 文件但有些版本只認(rèn)顯式目錄不見得識(shí)別子目錄。所以要對(duì)包裹結(jié)構(gòu)把src或sources目錄也手動(dòng)加進(jìn)去。加完路徑后可以用下面這段代碼在編譯期確認(rèn)組件單元被找得到省得等編譯到一半才報(bào)找不到sgcWebSocket{$IF not DECLARED(TsgcWebSocketServer)} {$MESSAGE ERROR sgcWebSockets source path not found} {$IFEND}這段代碼放在任一單元頂部即可。DECLARED是編譯器判定標(biāo)識(shí)符是否可見的指令找不到類型時(shí)直接中止編譯比鏈接期報(bào)錯(cuò)更好排查。2.3 用 IDE 編譯并注冊(cè)組件在項(xiàng)目管理器里打開對(duì)應(yīng) Delphi 12 的包.dpk文件名通常帶 D12 或 IDE 版本號(hào)右鍵執(zhí)行 Build如果提示缺少依賴優(yōu)先回頭檢查 Library 路徑而不是急著裝包。Build 成功后再打開帶 Design 前綴的包文件右鍵執(zhí)行 InstallDesign 頁(yè)面會(huì)多出一組sgcWebSocketServer、sgcWebSocketClient等圖標(biāo)。安裝完成后新建一個(gè) VCL 或 FMX 工程往窗體上拖一個(gè)TsgcWebSocketServer控件檢查 Object Inspector 里能看到 Port、BindIPs、Active 等屬性就說明安裝成功。注意如果你的 Delphi 裝的是 Community Edition同樣按 D12 的包編譯即可這控件不區(qū)分社區(qū)版還是專業(yè)版。如果想系統(tǒng)學(xué)一遍 Delphi 服務(wù)端開發(fā)我建議裝完后直接按 F12 追進(jìn)sgcWSServer.pas看連接狀態(tài)機(jī)怎么遷移比單看示例代碼收益高。提示Library 路徑加錯(cuò)是最常見的安裝失敗原因報(bào)錯(cuò)多集中在找不到 *.dcu 或 *.pas。先確認(rèn)目錄里有對(duì)應(yīng)的單元文件再檢查 IDE 的 Library 配置不要急著重裝包。3. 讓服務(wù)端跑起來TsgcWebSocketServer 的連接事件與消息收發(fā)安裝完成只是熱身真正要理解的是這套控件的事件模型。sgcWebSockets 把連接生命周期拆成了 OnConnect、OnMessage、OnDisconnect 幾個(gè)事件每個(gè)事件都拿到一個(gè)TsgcWSConnection對(duì)象。這個(gè)對(duì)象代表一條 WebSocket 連接發(fā)送、關(guān)閉、讀取 Header 這些操作都掛在它上面。事件回調(diào)運(yùn)行在 IO 線程里不是 VCL 主線程所以不能在事件里直接訪問窗體控件這是個(gè)新手很容易踩的坑。3.1 最小監(jiān)聽服務(wù)端口、綁定 IP 與握手在窗體上放一個(gè)按鈕和一個(gè)TsgcWebSocketServer代碼里配置監(jiān)聽端口和綁定地址procedure TForm1.btnStartServerClick(Sender: TObject); begin WebSocketServer1.Port : 8080; WebSocketServer1.BindIPs.Clear; WebSocketServer1.BindIPs.Add(0.0.0.0); WebSocketServer1.OnConnect : ServerConnect; WebSocketServer1.OnMessage : ServerMessage; WebSocketServer1.OnDisconnect : ServerDisconnect; WebSocketServer1.Active : True; end;Port用 8080 而不是 80是因?yàn)?80 容易被其他服務(wù)占用調(diào)試階段也要避開系統(tǒng)保留端口。BindIPs里0.0.0.0表示監(jiān)聽本機(jī)所有網(wǎng)卡部署時(shí)建議收斂到內(nèi)網(wǎng) IP避免把服務(wù)暴露到公網(wǎng)。Active : True會(huì)立刻完成監(jiān)聽端口初始化和握手前的 HTTP 監(jiān)聽準(zhǔn)備但并不代表已經(jīng)有客戶端連接。3.2 三個(gè)關(guān)鍵事件OnConnect / OnMessage / OnDisconnect下面是最常用的事件骨架事件簽名按常見版本寫如果你的庫(kù)里多了TextType之類的參數(shù)按實(shí)際聲明補(bǔ)齊即可procedure TForm1.ServerConnect(Sender: TObject; Connection: TsgcWSConnection); begin // 連接建立時(shí)給客戶端打個(gè)招呼 Connection.WriteData(welcome); end; procedure TForm1.ServerMessage(Sender: TObject; Connection: TsgcWSConnection; const Text: string); begin // 收到客戶端消息原樣返回 Connection.WriteData(Text); end; procedure TForm1.ServerDisconnect(Sender: TObject; Connection: TsgcWSConnection); begin // 清理和這個(gè)連接相關(guān)的狀態(tài) end;WriteData是連接對(duì)象上發(fā)送文本的方法sgcWebSockets 不同小版本里也出現(xiàn)過SendData、SendMessage這樣的同名方法以你源碼里TsgcWSConnection公開的方法為準(zhǔn)。OnConnect里可以讀握手請(qǐng)求OnDisconnect在連接被正常關(guān)閉或異常斷開時(shí)都會(huì)觸發(fā)適合清理會(huì)話。事件觸發(fā)時(shí)機(jī)典型用途OnConnectWebSocket 握手完成、連接進(jìn)入就緒狀態(tài)鑒權(quán)、寫入在線列表、發(fā)送初始化數(shù)據(jù)OnMessage收到完整的文本或二進(jìn)制幀處理業(yè)務(wù)請(qǐng)求、轉(zhuǎn)發(fā)消息OnDisconnect連接關(guān)閉包括異常斷開移除在線列表、結(jié)束會(huì)話OnErrorIO 層異常記錄日志、補(bǔ)償重連為什么異常斷開也要走 OnDisconnect因?yàn)?WebSocket 的關(guān)閉幀不一定每次都能收到網(wǎng)絡(luò)閃斷、對(duì)端進(jìn)程崩潰都不會(huì)正常發(fā)關(guān)閉幀。sgcWebSockets 在 TCP 層發(fā)現(xiàn)連接失效后最終還是會(huì)觸發(fā) OnDisconnect所以在線狀態(tài)清理放這里最穩(wěn)妥。3.3 從 Echo 到廣播維護(hù)在線列表并定向推送實(shí)際業(yè)務(wù)很少只做回聲。要做群發(fā)前先用TDictionary保存連接FConnections : TDictionarystring, TsgcWSConnection.Create; procedure TForm1.ServerConnect(Sender: TObject; Connection: TsgcWSConnection); begin FConnections.Add(Connection.Guid, Connection); Connection.WriteData(welcome); end; procedure TForm1.ServerDisconnect(Sender: TObject; Connection: TsgcWSConnection); begin FConnections.Remove(Connection.Guid); end; procedure TForm1.BroadcastAll(const AMessage: string); var LConn: TsgcWSConnection; begin for LConn in FConnections.Values do LConn.WriteData(AMessage); end;用Connection.Guid作為 Key是因?yàn)橥粋€(gè)客戶端斷開重連后會(huì)拿到新連接對(duì)象靠 IP 區(qū)分不靠譜。如果你安裝的版本里沒有Guid屬性用IntToHex(NativeInt(Connection))也能湊合但可讀性和穩(wěn)定性都差一些。廣播時(shí)如果想跳過某個(gè)連接就在循環(huán)里判斷LConn CurrentConnection。庫(kù)本身也提供Broadcast方法底層邏輯就是遍歷連接逐個(gè)寫但自帶實(shí)現(xiàn)的過濾條件少自己維護(hù)字典還有個(gè)好處可以在 OnDisconnect 后立刻移除失效連接避免向死連接寫數(shù)據(jù)觸發(fā)異常。注意TDictionary在System.Generics.Collections單元里VCL 和 FMX 工程都要手動(dòng)加上這個(gè) uses。事件回調(diào)發(fā)生在 IO 線程如果這里碰了上市窗體上的TLabel.Caption大概率會(huì)在運(yùn)行期收到線程安全錯(cuò)誤穩(wěn)妥做法是同步到主線程再更新界面。4. 企業(yè)級(jí)參數(shù)調(diào)優(yōu)心跳、認(rèn)證、反向代理與 1006 斷線真實(shí)環(huán)境里客戶端和服務(wù)器之間隔了交換機(jī)、防火墻、反向代理空閑連接很容易被中間設(shè)備回收。最常見的問題表現(xiàn)是連接靜置幾分鐘后用 WebSocket King 一測(cè)客戶端收到[websocket] onclose, code: 1006, reason:, reconnect: true。面試?yán)锶绻粏?1006 是什么記住它是異常關(guān)閉碼和正常的 1000 關(guān)閉碼相對(duì)。這種異常關(guān)閉表示連接在沒有收到關(guān)閉幀的情況下被切斷幾乎可以斷定是鏈路超時(shí)或服務(wù)端沒發(fā)心跳。4.1 心跳參數(shù)HeartBeatInterval 與 1006 的處理sgcWebSockets 的服務(wù)器組件上有心跳相關(guān)屬性常見的是HeartBeatInterval和HeartBeatTimeout。前者控制每隔多少毫秒發(fā)送一次 PING 幀后者控制等待 PONG 回來的最長(zhǎng)時(shí)間超出就判定連接失效并主動(dòng)關(guān)閉。經(jīng)驗(yàn)值我會(huì)設(shè)HeartBeatInterval : 3000030 秒一次心跳能穿過大多數(shù)防火墻的空閑超時(shí)閾值HeartBeatTimeout給 5000 到 10000避免網(wǎng)絡(luò)抖動(dòng)直接誤殺。場(chǎng)景HeartBeatIntervalHeartBeatTimeout說明普通內(nèi)網(wǎng)6000015000內(nèi)網(wǎng)丟包率低心跳不需要太快跨公網(wǎng)或經(jīng) nginx3000010000公網(wǎng)和代理設(shè)備容易掐空閑連接高并發(fā)長(zhǎng)連接4500010000心跳太頻繁會(huì)放大 IO 線程壓力收到 1006 后客戶端必須靠斷線重連邏輯恢復(fù)會(huì)話。服務(wù)端這邊能做的是在 OnDisconnect 里記錄時(shí)間和會(huì)話數(shù)據(jù)等客戶端重連回來時(shí)根據(jù)握手 Header 里的會(huì)話 ID 恢復(fù)現(xiàn)場(chǎng)而不是把消息直接丟掉。心跳參數(shù)往大調(diào)不是萬能藥如果中間設(shè)備的空閑超時(shí)是 15 秒那 60 秒一次心跳照樣會(huì)被掐反過來心跳太密又會(huì)在幾萬連接時(shí)明顯增加線程開銷所以先查一下網(wǎng)絡(luò)設(shè)備的 TCP idle timeout 再?gòu)南峦显O(shè)。4.2 用握手 Header 做身份校驗(yàn)對(duì)接 JWTWebSocket 也不建議裸奔。sgcWebSockets 的TsgcWSConnection上可以讀到客戶端握手時(shí)帶來的 HTTP Header比如Authorization: Bearer token。在 OnConnect 里校驗(yàn) token校驗(yàn)失敗就調(diào)用連接的關(guān)閉方法讓握手階段直接失敗function TForm1.ValidateToken(const AHeader: string): Boolean; begin Result : Copy(AHeader, 1, 7) Bearer ; end; procedure TForm1.ServerConnect(Sender: TObject; Connection: TsgcWSConnection); begin // 有些版本里屬性叫 Connection.Header按實(shí)際 TsgcWSConnection 聲明調(diào)整 if not ValidateToken(Connection.Headers[Authorization]) then begin Connection.Close; Exit; end; FConnections.Add(Connection.Guid, Connection); Connection.WriteData(welcome); end;Headers返回請(qǐng)求頭集合取不到對(duì)應(yīng)字段時(shí)返回空串。實(shí)際項(xiàng)目里建議在更早的OnConnecting或OnValidateAuthentication階段做避免無效連接消耗資源。簽名校驗(yàn)要做防篡改時(shí)間戳和隨機(jī)數(shù)也要進(jìn)去單純 Base64 解碼不等于安全。如果你用的是 JWT就按 RS256/HS256 驗(yàn)簽驗(yàn)簽通過后再把用戶 ID 存到一個(gè)業(yè)務(wù)字典里后續(xù) OnMessage 直接取不用重復(fù)解析 token。注意Connection.Close只是關(guān)閉當(dāng)前連接如果已經(jīng)寫入了在線字典緊接著要手動(dòng)移除否則下一次廣播會(huì)拿到一個(gè)已經(jīng)斷開的連接對(duì)象。4.3 nginx 反向代理與負(fù)載均衡下的 WebSocket 參數(shù)當(dāng)客戶端不直連 Delphi 服務(wù)端而是先走到 nginx 時(shí)代理層必須顯式升級(jí)連接。否則會(huì)看到服務(wù)端有握手但客戶端一直停在 Connecting。nginx 里對(duì)應(yīng) location 的配置location /ws/ { proxy_pass http://delphi_ws_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_read_timeout 60s; }proxy_pass指向上游 Delphi 服務(wù)器的 HTTP 端口比如一臺(tái)機(jī)器上開 8080。關(guān)鍵參數(shù)是Upgrade和Connection upgrade缺了它們 nginx 會(huì)把 WebSocket 當(dāng)成普通 HTTP 請(qǐng)求轉(zhuǎn)發(fā)握手永遠(yuǎn)完成不了。proxy_read_timeout要和心跳間隔匹配我一般設(shè)成心跳間隔加 10 秒否則 nginx 這邊的空閑超時(shí)比服務(wù)端心跳還短連接照樣會(huì)被切斷。負(fù)載均衡如果掛在多臺(tái) Delphi 服務(wù)之間注意 WebSocket 會(huì)話通常有粘性。因?yàn)榉?wù)端會(huì)話狀態(tài)在內(nèi)存里輪詢會(huì)把同一個(gè)用戶的連接分到不同機(jī)器。方案有兩個(gè)nginx 上按客戶端 IP 或自定義 Header 做ip_hash或者把會(huì)話數(shù)據(jù)外置到 Redis。sgcWebSockets 本身不自帶集群會(huì)話同步外置狀態(tài)是更干凈的思路。這類排錯(cuò)經(jīng)驗(yàn)通用Go 的 gin WebSocket 和 Java 的 Spring WebSocket 在 nginx 層配置幾乎一樣都是改這幾個(gè)proxy_set_header所以你在別的項(xiàng)目里踩過的坑可以直接搬過來。5. 實(shí)戰(zhàn)技巧把業(yè)務(wù)邏輯交給線程別在 OnMessage 里做重活連上幾十個(gè)客戶端后容易發(fā)現(xiàn)某個(gè)客戶端發(fā)來一個(gè)慢查詢其他客戶端推送全部卡住。原因就是 OnMessage 跑在 IO 線程里你在里面執(zhí)行數(shù)據(jù)庫(kù)查詢或遠(yuǎn)程調(diào)用等于把整個(gè)接收線程堵死。正確姿勢(shì)是 OnMessage 只做兩件事解析消息、把任務(wù)丟進(jìn)隊(duì)列業(yè)務(wù)線程處理完后再統(tǒng)一通過服務(wù)器組件推送。5.1 用 TThreadedQueue 解耦耗時(shí)任務(wù)我這里用TThreadedQueue做一個(gè)簡(jiǎn)單任務(wù)隊(duì)列。OnMessage 收到消息后把任務(wù)對(duì)象放進(jìn)去后臺(tái)線程取出執(zhí)行執(zhí)行完成后推送動(dòng)作本身是線程安全的可以直接調(diào)用服務(wù)器組件發(fā)送type TJob record ConnectionGuid: string; Payload: string; end; FQueue : TThreadedQueueTJob.Create(1000);后臺(tái)任務(wù)線程里用PopItem取出任務(wù)調(diào)用業(yè)務(wù)邏輯后從連接字典取出TsgcWSConnection再寫數(shù)據(jù)。注意不要讓隊(duì)列無限增長(zhǎng)Create的第一個(gè)參數(shù)是隊(duì)列容量默認(rèn) Push/Pop 超時(shí)是 0表示立即返回。我習(xí)慣容量設(shè) 1000處理不過來時(shí)寧愿阻塞接收也不把消息丟進(jìn)一個(gè)無界隊(duì)列把內(nèi)存寫爆。5.2 聯(lián)調(diào)驗(yàn)證用 WebSocket King 連本地服務(wù)寫完推送邏輯后用 WebSocket King 這類客戶端連ws://127.0.0.1:8080驗(yàn)證。連接成功后先看服務(wù)端有沒有觸發(fā) OnConnect再在客戶端發(fā)一條消息看回包確認(rèn)心跳時(shí)間點(diǎn)。如果從外部網(wǎng)絡(luò)連推薦用瀏覽器控制臺(tái)直接跑一行腳本const ws new WebSocket(ws://127.0.0.1:8080); ws.onmessage (e) console.log(e.data); ws.onclose (e) console.log(close, e.code, e.reason);服務(wù)端推送驗(yàn)證時(shí)在線狀態(tài)維護(hù)的準(zhǔn)確性最關(guān)鍵。我看過不少現(xiàn)場(chǎng)廣播時(shí)報(bào)錯(cuò)說找不到連接往下追是字典里保留了已斷開的連接。所以推送前至少要做一次連接狀態(tài)判斷或者直接 catch 發(fā)送異常后再在 OnDisconnect 里補(bǔ)一次清理。最穩(wěn)的一步是在 OnDisconnect 里調(diào)用FConnections.Remove前把返回值和 Count 打日志if not FConnections.Remove(Connection.Guid) then OutputDebugString(PChar(warn: missing connection Connection.Guid));調(diào)試階段這行日志能立刻暴露重復(fù)清理或連接未注冊(cè)的問題頻繁重連的場(chǎng)景下只看 warn 頻率就能判斷是心跳太慢還是代理超時(shí)太短不用再猜。本文還有配套的精品資源點(diǎn)擊獲取