指南:從 Hello World 到有狀態(tài)實(shí)時(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項(xiàng)目地址https://gitcode.com/gh_mirrors/ih/ihp點(diǎn)擊查看免費(fèi)下載導(dǎo)讀IHP 對(duì) 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 ?;畹冗M(jìn)階能力并輔以倉庫源碼級(jí)原理說明讓讀者既能照做也能理解底層機(jī)制。如果只是想用 WebSockets 推送 HTML/UI 更新頁面局部自動(dòng)刷新IHP 提供了更高層的 Auto Refresh API詳見 Auto Refresh 指南本文不再展開。1. 了解 WebSocket 在 IHP 中的位置在 IHP 中WebSocket 服務(wù)端入口的寫法與普通 Controller 一脈相承同樣用一個(gè)自定義數(shù)據(jù)類型表示同樣放在Web/Controller/目錄下同樣被注冊(cè)進(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可見一個(gè)WSApp實(shí)例由四部分組成初始狀態(tài)initialState、主邏輯run、生命周期回調(diào)onPing/onClose以及可選的connectionOptions用于覆蓋握手/連接級(jí)選項(xiàng)例如啟用 permessage-deflate 壓縮注釋見源碼。整個(gè)框架構(gòu)建在 Haskell 生態(tài)的websockets庫之上import qualified Network.WebSockets as Websocket。2. 創(chuàng)建第一個(gè) WebSocket 控制器2.1 定義控制器數(shù)據(jù)類型打開Web/Types.hs追加一個(gè)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給出連接建立時(shí)的初始狀態(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會(huì)以類型名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如果請(qǐng)求不是 WebSocket 升級(jí)請(qǐng)求則返回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 控制臺(tái)會(huì)依次看到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ù)它會(huì)阻塞等待直到客戶端發(fā)來數(shù)據(jù)后才繼續(xù)執(zhí)行后續(xù)代碼源碼見 ihp/IHP/WebSocket.hs本質(zhì)是Websocket.receiveData ?connection類型約束為WebSocketsData a。JS 端對(duì)應(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); };連接就緒后瀏覽器會(huì)彈出輸入框詢問姓名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ù)器會(huì)推送形如{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 的會(huì)話 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)閉時(shí)打印$name has left就需要在某個(gè)地方記住名字于是把數(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)的兩個(gè)原語操作定義在 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ì)是一個(gè)IORef由startWSApp在連接建立時(shí)初始化ihp/IHP/WebSocket.hssetState/getState分別對(duì)應(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é)束還是異常退出都會(huì)執(zhí)行onClose其中ConnectionClosed與CloseRequest兩種異常被靜默處理其余異常會(huì)交給框架日志記錄。Good to know當(dāng)run函數(shù)執(zhí)行完畢時(shí)連接會(huì)自動(dòng)關(guān)閉。因此在上面的例子中輸入名字、收到問候后連接隨即關(guān)閉... has left!會(huì)立刻打印出來。若希望連接一直保持到瀏覽器窗口關(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兩個(gè)原語操作配合一個(gè)精心設(shè)計(jì)的數(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 控制器里使用是因?yàn)?startWebSocketApp 在處理連接前就通過let ?modelContext requestModelContext ?request把請(qǐng)求上下文注入隱式參數(shù)currentUser正是從請(qǐng)求的 vault 中讀取認(rèn)證信息lookupAuthVault currentUserVaultKey ?request。也就是說會(huì)話 Cookie 在握手時(shí)隨 HTTP 請(qǐng)求一起到達(dá)框架據(jù)此還原登錄態(tài)連接建立后即可隨時(shí)訪問。7. 進(jìn)階技巧7.1 為自定義類型編寫解碼器receiveData通過WebSocketsData類型類解碼數(shù)據(jù)。要為自定義類型提供支持只需編寫對(duì)應(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 自定義訪問路徑不想用類型名做路徑時(shí)可以用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啟動(dòng) WebSocket 應(yīng)用。另外兩種相關(guān)形態(tài)源碼依據(jù)webSocketAppWithHTTPFallback/webSocketAppWithCustomPathAndHTTPFallbackihp/IHP/RouterSupport.hs需要同時(shí)滿足WSApp與Controller約束當(dāng)請(qǐng)求不是 WebSocket 升級(jí)時(shí)會(huì)回退到普通 HTTP action 處理用runActionWithNewContext執(zhí)行webSocketRouteihp/IHP/RouterSupport.hs基于 Trie 的注冊(cè)形態(tài)被[routes|...|]DSL 的WS關(guān)鍵字使用注冊(cè)在GET方法下WebSocket 握手本身就是 GET Upgrade。7.3 基于 routes DSL 聲明 WebSocket 路由除了在FrontController中手寫webSocketApp也可以在新版顯式路由 DSL 中用WS關(guān)鍵字注冊(cè)見 路由指南 WebSocket 路由一節(jié)[routes|webRoutes GET /posts PostsAction WS /chat ChatApp WS /datasync DataSyncController |] instance FrontController WebApplication where controllers webRoutes注意事項(xiàng)來自路由指南均為 v1 形態(tài)的限制WS行的右側(cè)是WSApp實(shí)例的類型名而不是 action 構(gòu)造器WS 路由只支持靜態(tài)路徑不支持{capture}或{splat}捕獲段WS 路由不讀取查詢參數(shù)?nameWS不能與 HTTP 方法寫在同一行如WS|GET會(huì)被拒絕WS 路由必須放在小寫命名的 binding 塊中該形態(tài)不生成HasPath實(shí)例如需在 JS 客戶端代碼里調(diào)用pathTo ChatApp需手動(dòng)聲明instance HasPath ChatApp where pathTo _ /chat若需要 HTTP 回退形態(tài)仍請(qǐng)使用webSocketAppWithHTTPFallback等傳統(tǒng)方式兩種注冊(cè)方式可以在同一個(gè)controllers列表中共存。7.4 Ping 保活與 onPing默認(rèn)情況下服務(wù)器每 30 秒會(huì)向?yàn)g覽器發(fā)送一次 Ping以確保連接仍然存活。你可以重寫onPing函數(shù)在每次 Ping 完成后執(zhí)行自定義代碼。真實(shí)用例Auto Refresh 的會(huì)話?;?。IHP 內(nèi)置的 Auto Refresh 正是基于 WebSocket 實(shí)現(xiàn)的ihp/IHP/AutoRefresh.hs頁面渲染時(shí)注入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)前會(huì)話的lastPing刷新為當(dāng)前時(shí)間。Auto Refresh 的會(huì)話回收邏輯isSessionExpiredihp/IHP/AutoRefresh.hs規(guī)定超過 60 秒沒有 Ping 的會(huì)話視為過期并被 GC 清理。由于服務(wù)器每 30 秒 Ping 一次只要連接正常會(huì)話就永遠(yuǎn)不會(huì)被誤回收——這正是onPing的典型應(yīng)用場(chǎng)景。8. 小結(jié)與推薦閱讀本文覆蓋了 IHP WebSockets 的完整開發(fā)路徑能力關(guān)鍵 API / 寫法源碼位置控制器類型WSApp類型類ihp/IHP/WebSocket.hs掛載路由webSocketApp/webSocketAppWithCustomPathihp/IHP/RouterSupport.hs連接啟動(dòng)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)建的頁面自動(dòng)刷新方案路由指南的 WebSocket 路由章節(jié)[routes|...|]DSL 中的WS關(guān)鍵字與限制RouterSupport.hswebSocketApp系列函數(shù)與webSocketRoute的完整實(shí)現(xiàn)AutoRefresh.hs一個(gè)真實(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項(xiàng)目地址https://gitcode.com/gh_mirrors/ih/ihp點(diǎn)擊查看免費(fèi)下載相關(guān)推薦終極Linux內(nèi)核模塊編程指南從Hello World到設(shè)備驅(qū)動(dòng)開發(fā)實(shí)戰(zhàn)終極Linux內(nèi)核模塊編程指南從Hello World到設(shè)備驅(qū)動(dòng)開發(fā)實(shí)戰(zhàn) LKMPGLinux Kernel Module Programming Guid文檔教程操作系統(tǒng)驅(qū)動(dòng)開發(fā)從零部署DeepTutor讓AI學(xué)習(xí)伴侶真正懂你從零部署DeepTutor讓AI學(xué)習(xí)伴侶真正懂你 你收藏夾里那批必學(xué)資料是不是已經(jīng)吃灰半年了想系統(tǒng)學(xué)點(diǎn)東西卻被幾十個(gè)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),僅供參考