指南:從 Hello World 到有狀態(tài)實(shí)時通信)
后端【免費(fèi)下載鏈接】ihp The fastest way to build type safe web apps. IHP is a new batteries-included web framework optimized for longterm productivity and programmer happiness項目地址https://gitcode.com/gh_mirrors/ih/ihp點(diǎn)擊查看免費(fèi)下載導(dǎo)讀IHP 對 WebSockets 提供一等公民first class支持WebSocket 服務(wù)端的入口與普通控制器高度相似同樣存放于Web/Controller/目錄、同樣通過 FrontController 掛載進(jìn)應(yīng)用路由。本文以HelloWorld示例為主線完整演示在 IHP 中創(chuàng)建 WebSocket 控制器、配置路由、從瀏覽器 JS 建立連接、雙向收發(fā)文本與 JSON 數(shù)據(jù)、利用狀態(tài)機(jī)管理連接生命周期、訪問當(dāng)前登錄用戶以及自定義解碼器、自定義路徑與 Ping 保活等進(jìn)階能力并輔以倉庫源碼級原理說明讓讀者既能照做也能理解底層機(jī)制。如果只是想用 WebSockets 推送 HTML/UI 更新頁面局部自動刷新IHP 提供了更高層的 Auto Refresh API詳見 Auto Refresh 指南本文不再展開。1. 了解 WebSocket 在 IHP 中的位置在 IHP 中WebSocket 服務(wù)端入口的寫法與普通 Controller 一脈相承同樣用一個自定義數(shù)據(jù)類型表示同樣放在Web/Controller/目錄下同樣被注冊進(jìn) FrontController 的路由列表中只是改用webSocketApp而非parseRoute。其背后的核心類型類是WSApp定義在 ihp/IHP/WebSocket.hsclass WSApp state where initialState :: state run :: IO () run pure () onPing :: IO () onPing pure () onClose :: IO () onClose pure () connectionOptions :: WebSocket.ConnectionOptions connectionOptions WebSocket.defaultConnectionOptions可見一個WSApp實(shí)例由四部分組成初始狀態(tài)initialState、主邏輯run、生命周期回調(diào)onPing/onClose以及可選的connectionOptions用于覆蓋握手/連接級選項例如啟用 permessage-deflate 壓縮注釋見源碼。整個框架構(gòu)建在 Haskell 生態(tài)的websockets庫之上import qualified Network.WebSockets as Websocket。2. 創(chuàng)建第一個 WebSocket 控制器2.1 定義控制器數(shù)據(jù)類型打開Web/Types.hs追加一個HelloWorldController類型data HelloWorldController HelloWorldController deriving (Eq, Show, Data)2.2 實(shí)現(xiàn) WSApp 實(shí)例新建文件Web/Controller/HelloWorld.hsmodule Web.Controller.HelloWorld where import Web.Controller.Prelude instance WSApp HelloWorldController where initialState HelloWorldController run do sendTextData (Hello World! :: Text)這里initialState給出連接建立時的初始狀態(tài)run是連接建立后立即執(zhí)行的入口sendTextData負(fù)責(zé)向客戶端發(fā)送文本消息。從源碼看run在 ihp/IHP/WebSocket.hs 中是帶?state、?request、?modelContext、?connection等隱式參數(shù)的IO ()默認(rèn)值為pure ()因此就算你不寫run連接也能正常建立。2.3 掛載路由打開Web/FrontController.hs先加入 importimport Web.Controller.HelloWorld然后把它掛載到controllers列表中instance FrontController WebApplication where controllers [ startPage StartPageAction -- Generator Marker , webSocketApp HelloWorldController ]注意這里使用的是webSocketApp而不是普通控制器所用的parseRoute。默認(rèn)路徑規(guī)則webSocketApp HelloWorldController會以類型名HelloWorldController作為訪問路徑見 ihp/IHP/RouterSupport.hs內(nèi)部通過Typeable.typeOf取出類型名并交給webSocketAppWithCustomPath。因此控制器可通過以下地址訪問ws://localhost:8000/HelloWorldController握手語義源碼依據(jù)webSocketAppWithCustomPath生成的控制器路由最終調(diào)用 ControllerSupport.hs 中的startWebSocketAppAndFailOnHTTP——它先用WebSockets.websocketsApp嘗試完成 WebSocket 握手HTTPGETUpgrade: websocket如果請求不是 WebSocket 升級請求則返回400 Bad Request響應(yīng)體為This endpoint is only available via a WebSocket。3. 從瀏覽器 JS 建立連接打開static/app.js追加以下代碼打開一條新連接var helloWorldController new WebSocket(ws://localhost:8000/HelloWorldController); helloWorldController.onopen function (event) { console.log(Connected); }; helloWorldController.onmessage function (event) { console.log(event.data); };在瀏覽器打開http://localhost:8000查看 JS 控制臺會依次看到Connected Hello World!Connected來自onopen回調(diào)Hello World!則是服務(wù)器在run里通過sendTextData推送過來的消息。4. 雙向收發(fā)消息4.1 接收文本并回送把Web/Controller/HelloWorld.hs改成先問用戶的名字再回送Hello $name!module Web.Controller.HelloWorld where import Web.Controller.Prelude instance WSApp HelloWorldController where initialState HelloWorldController run do name :: Text - receiveData sendTextData (Hello name !)receiveData用于讀取 JS 端發(fā)送過來的數(shù)據(jù)它會阻塞等待直到客戶端發(fā)來數(shù)據(jù)后才繼續(xù)執(zhí)行后續(xù)代碼源碼見 ihp/IHP/WebSocket.hs本質(zhì)是Websocket.receiveData ?connection類型約束為WebSocketsData a。JS 端對應(yīng)修改為var helloWorldController new WebSocket(ws://localhost:8000/HelloWorldController); helloWorldController.onopen function (event) { var name prompt(Your name?); helloWorldController.send(name); }; helloWorldController.onmessage function (event) { console.log(event.data); };連接就緒后瀏覽器會彈出輸入框詢問姓名send(name)把名字發(fā)到服務(wù)器服務(wù)器端receiveData拿到名字后回送問候語最終在onmessage里打印出來。4.2 發(fā)送 JSON框架提供了sendJSON輔助函數(shù)可以把 Haskell 數(shù)據(jù)結(jié)構(gòu)編碼成 JSON 字符串后發(fā)送module Web.Controller.HelloWorld where import Web.Controller.Prelude import Data.Aeson -- Needed for JSON functions instance WSApp HelloWorldController where initialState HelloWorldController run do name :: Text - receiveData sendJSON HelloWorldResponse { name } data HelloWorldResponse HelloWorldResponse { name :: Text } -- Encode HelloWorldResponse as { name: the name } -- See Aeson docs https://hackage.haskell.org/package/aeson-2.0.1.0/docs/Data-Aeson.html instance ToJSON HelloWorldResponse where toJSON HelloWorldResponse { name } object [name . name]底層實(shí)現(xiàn)sendJSON在 ihp/IHP/WebSocket.hs 中定義為sendTextData (Aeson.encode payload)即先用Data.Aeson編碼為字節(jié)串再走文本消息通道發(fā)出。本例中客戶端先發(fā)送名字隨后服務(wù)器會推送形如{name:客戶端之前發(fā)送的名字}的消息。4.3 接收其他數(shù)據(jù)類型receiveData不僅能收Text還能直接收Int、UUID、Bool等實(shí)現(xiàn)了WebSocketsData的類型myInt :: Int - receiveData myUUID :: UUID - receiveData myBool :: Bool - receiveData倉庫為UUID提供了現(xiàn)成的WebSocketsData實(shí)例ihp/IHP/WebSocket.hs按 ASCII 編碼的字節(jié)串解析 UUID。Auto Refresh 的會話 ID 正是通過receiveData UUID接收的見下文第 7 節(jié)。5. 管理有狀態(tài)連接5.1 用數(shù)據(jù)類型表達(dá)狀態(tài)機(jī)WebSocket 連接通常是長連接服務(wù)器端往往需要跨消息記住狀態(tài)。IHP 的做法是讓控制器數(shù)據(jù)類型本身成為狀態(tài)機(jī)。例如我們要在連接關(guān)閉時打印$name has left就需要在某個地方記住名字于是把數(shù)據(jù)類型改為data HelloWorldController WaitForName | NameEntered { name :: Text } deriving (Eq, Show, Data)WaitForName表示用戶尚未輸入名字的狀態(tài)NameEntered { name .. }記錄已輸入的名字。5.2 setState 與 getState把初始狀態(tài)改為WaitForNameinitialState WaitForName更新run在拿到名字后把狀態(tài)推進(jìn)到NameEnteredrun do name - receiveData Text setState NameEntered { name } sendTextData (Hello name !)setState NameEntered { name }把連接狀態(tài)從WaitForName切換到NameEntered。狀態(tài)的兩個原語操作定義在 ihp/IHP/WebSocket.hssetState :: (?state :: IORef state) state - IO () setState newState writeIORef ?state newState getState :: (?state :: IORef state) IO state getState readIORef ?state可以看到連接狀態(tài)本質(zhì)是一個IORef由startWSApp在連接建立時初始化ihp/IHP/WebSocket.hssetState/getState分別對應(yīng)writeIORef/readIORef。5.3 onClose 生命周期回調(diào)要處理連接關(guān)閉事件使用onClosemodule Web.Controller.HelloWorld where import Web.Controller.Prelude instance WSApp HelloWorldController where initialState WaitForName run do name - receiveData Text setState NameEntered { name } sendTextData (Hello name !) onClose do state - getState case state of WaitForName - pure () -- Do nothing in case the connection is closed before a name was entered NameEntered { name } - do let message :: Text name has left! putStrLn message用state - getState讀回當(dāng)前狀態(tài)再按狀態(tài)分別處理如果名字還沒輸入就關(guān)閉連接則什么都不做否則在運(yùn)行 IHP 服務(wù)器的終端里打印$name has left。連接生命周期源碼依據(jù)startWSAppihp/IHP/WebSocket.hs用withPingPong包裝run并通過Exception.finally保證無論正常結(jié)束還是異常退出都會執(zhí)行onClose其中ConnectionClosed與CloseRequest兩種異常被靜默處理其余異常會交給框架日志記錄。Good to know當(dāng)run函數(shù)執(zhí)行完畢時連接會自動關(guān)閉。因此在上面的例子中輸入名字、收到問候后連接隨即關(guān)閉... has left!會立刻打印出來。若希望連接一直保持到瀏覽器窗口關(guān)閉給run加上forever循環(huán)run do -- Loops until the connection is closed forever do name - receiveData Text setState NameEntered { name } sendTextData (Hello name !)如本節(jié)所示setStategetState兩個原語操作配合一個精心設(shè)計的數(shù)據(jù)結(jié)構(gòu)就能非常靈活地管理有狀態(tài)的 WebSocket 連接。6. 在 WebSocket 控制器中訪問當(dāng)前用戶用戶登錄后可以在 WebSocket 控制器里直接使用常規(guī)的認(rèn)證函數(shù)如currentUsermodule Web.Controller.HelloWorld where import Web.Controller.Prelude instance WSApp HelloWorldController where run do let name currentUser.name sendTextData (Hello name !)底層機(jī)制currentUser定義于 ihp/IHP/LoginSupport/Helper/Controller.hs其實(shí)現(xiàn)是“若未登錄則redirectToLogin否則返回currentUserOrNothing的值”。之所以能在 WebSocket 控制器里使用是因為 startWebSocketApp 在處理連接前就通過let ?modelContext requestModelContext ?request把請求上下文注入隱式參數(shù)currentUser正是從請求的 vault 中讀取認(rèn)證信息lookupAuthVault currentUserVaultKey ?request。也就是說會話 Cookie 在握手時隨 HTTP 請求一起到達(dá)框架據(jù)此還原登錄態(tài)連接建立后即可隨時訪問。7. 進(jìn)階技巧7.1 為自定義類型編寫解碼器receiveData通過WebSocketsData類型類解碼數(shù)據(jù)。要為自定義類型提供支持只需編寫對應(yīng)的實(shí)例。下面是倉庫中UUID解碼器的實(shí)現(xiàn)ihp/IHP/WebSocket.hs可作為模板import qualified Network.WebSockets as WS import qualified Data.UUID.V4 as UUID import qualified Data.UUID as UUID import qualified Data.Maybe as Maybe instance WS.WebSocketsData UUID where fromDataMessage (WS.Text byteString _) UUID.fromLazyASCIIBytes byteString | Maybe.fromJust fromDataMessage (WS.Binary byteString) UUID.fromLazyASCIIBytes byteString | Maybe.fromJust fromLazyByteString byteString UUID.fromLazyASCIIBytes byteString | Maybe.fromJust toLazyByteString UUID.toLazyASCIIBytes要點(diǎn)fromDataMessage負(fù)責(zé)把文本/二進(jìn)制消息解碼成你的類型fromLazyByteString負(fù)責(zé)把懶字節(jié)串解碼成你的類型toLazyByteString負(fù)責(zé)把你的類型編碼回字節(jié)串用于發(fā)送。7.2 自定義訪問路徑不想用類型名做路徑時可以用webSocketAppWithCustomPath覆蓋默認(rèn)路徑instance FrontController WebApplication where controllers [ startPage StartPageAction -- Generator Marker , webSocketAppWithCustomPath HelloWorldController my-ws ]這樣 WebSocket 服務(wù)就暴露在/my-ws下。其路由解析邏輯在 ihp/IHP/RouterSupport.hs先用 Attoparsec 匹配/加給定路徑匹配成功則以WS.initialState webSocketApp啟動 WebSocket 應(yīng)用。另外兩種相關(guān)形態(tài)源碼依據(jù)webSocketAppWithHTTPFallback/webSocketAppWithCustomPathAndHTTPFallbackihp/IHP/RouterSupport.hs需要同時滿足WSApp與Controller約束當(dāng)請求不是 WebSocket 升級時會回退到普通 HTTP action 處理用runActionWithNewContext執(zhí)行webSocketRouteihp/IHP/RouterSupport.hs基于 Trie 的注冊形態(tài)被[routes|...|]DSL 的WS關(guān)鍵字使用注冊在GET方法下WebSocket 握手本身就是 GET Upgrade。7.3 基于 routes DSL 聲明 WebSocket 路由除了在FrontController中手寫webSocketApp也可以在新版顯式路由 DSL 中用WS關(guān)鍵字注冊見 路由指南 WebSocket 路由一節(jié)[routes|webRoutes GET /posts PostsAction WS /chat ChatApp WS /datasync DataSyncController |] instance FrontController WebApplication where controllers webRoutes注意事項來自路由指南均為 v1 形態(tài)的限制WS行的右側(cè)是WSApp實(shí)例的類型名而不是 action 構(gòu)造器WS 路由只支持靜態(tài)路徑不支持{capture}或{splat}捕獲段WS 路由不讀取查詢參數(shù)?nameWS不能與 HTTP 方法寫在同一行如WS|GET會被拒絕WS 路由必須放在小寫命名的 binding 塊中該形態(tài)不生成HasPath實(shí)例如需在 JS 客戶端代碼里調(diào)用pathTo ChatApp需手動聲明instance HasPath ChatApp where pathTo _ /chat若需要 HTTP 回退形態(tài)仍請使用webSocketAppWithHTTPFallback等傳統(tǒng)方式兩種注冊方式可以在同一個controllers列表中共存。7.4 Ping 保活與 onPing默認(rèn)情況下服務(wù)器每 30 秒會向瀏覽器發(fā)送一次 Ping以確保連接仍然存活。你可以重寫onPing函數(shù)在每次 Ping 完成后執(zhí)行自定義代碼。真實(shí)用例Auto Refresh 的會話?;?。IHP 內(nèi)置的 Auto Refresh 正是基于 WebSocket 實(shí)現(xiàn)的ihp/IHP/AutoRefresh.hs頁面渲染時注入autoRefreshMeta元標(biāo)簽見 ihp/IHP/AutoRefresh/View.hs前端ihp-auto-refresh.js讀取其中的 session id 后連接/AutoRefreshWSAppihp/data/static/ihp-auto-refresh.js。其WSApp實(shí)例的onPing實(shí)現(xiàn)為onPing do now - getCurrentTime AutoRefreshActive { sessionId } - getState updateSession sessionId (\session - session { lastPing now })含義每次 Ping 完成就把當(dāng)前會話的lastPing刷新為當(dāng)前時間。Auto Refresh 的會話回收邏輯isSessionExpiredihp/IHP/AutoRefresh.hs規(guī)定超過 60 秒沒有 Ping 的會話視為過期并被 GC 清理。由于服務(wù)器每 30 秒 Ping 一次只要連接正常會話就永遠(yuǎn)不會被誤回收——這正是onPing的典型應(yīng)用場景。8. 小結(jié)與推薦閱讀本文覆蓋了 IHP WebSockets 的完整開發(fā)路徑能力關(guān)鍵 API / 寫法源碼位置控制器類型WSApp類型類ihp/IHP/WebSocket.hs掛載路由webSocketApp/webSocketAppWithCustomPathihp/IHP/RouterSupport.hs連接啟動startWebSocketAppihp/IHP/ControllerSupport.hs收發(fā)數(shù)據(jù)receiveData/sendTextData/sendJSONihp/IHP/WebSocket.hs狀態(tài)管理setState/getStateihp/IHP/WebSocket.hs生命周期onClose/onPingihp/IHP/WebSocket.hs自定義解碼WebSocketsData實(shí)例ihp/IHP/WebSocket.hs如果想繼續(xù)深入推薦閱讀Auto Refresh 指南基于 WebSocket 構(gòu)建的頁面自動刷新方案路由指南的 WebSocket 路由章節(jié)[routes|...|]DSL 中的WS關(guān)鍵字與限制RouterSupport.hswebSocketApp系列函數(shù)與webSocketRoute的完整實(shí)現(xiàn)AutoRefresh.hs一個真實(shí)、完整的WSApp實(shí)例參考含receiveData UUID、setState、onPing、onClose的聯(lián)合運(yùn)用。贊分享后端【免費(fèi)下載鏈接】ihp The fastest way to build type safe web apps. IHP is a new batteries-included web framework optimized for longterm productivity and programmer happiness項目地址https://gitcode.com/gh_mirrors/ih/ihp點(diǎn)擊查看免費(fèi)下載相關(guān)推薦終極Linux內(nèi)核模塊編程指南從Hello World到設(shè)備驅(qū)動開發(fā)實(shí)戰(zhàn)終極Linux內(nèi)核模塊編程指南從Hello World到設(shè)備驅(qū)動開發(fā)實(shí)戰(zhàn) LKMPGLinux Kernel Module Programming Guid文檔教程操作系統(tǒng)驅(qū)動開發(fā)從零部署DeepTutor讓AI學(xué)習(xí)伴侶真正懂你從零部署DeepTutor讓AI學(xué)習(xí)伴侶真正懂你 你收藏夾里那批必學(xué)資料是不是已經(jīng)吃灰半年了想系統(tǒng)學(xué)點(diǎn)東西卻被幾十個PDF和課程截圖堵在起點(diǎn)完全不人工智能AI 應(yīng)用AI Agent多智能體RAG教育后端前端PrusaSlicer 插件 API 實(shí)戰(zhàn)指南從 Hello World 到簽名分發(fā)PrusaSlicer 插件 API 實(shí)戰(zhàn)指南從 Hello World 到簽名分發(fā) 本指南基于 PrusaSlicer 倉庫中的 Plugin_API.md桌面應(yīng)用3D渲染上一篇KeyDive開發(fā)者指南如何擴(kuò)展支持新的Android設(shè)備與Widevine版本下一篇NSWindowStyles揭秘打造高顏值macOS應(yīng)用界面的核心技巧創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考