ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

IHP WebSockets 开发指南:从 Hello World 到有状态实时通信

IHP WebSockets 开发指南:从 Hello World 到有状态实时通信 后端【免费下载链接】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点击查看免费下载导读IHP 对 WebSockets 提供一等公民first class支持WebSocket 服务端的入口与普通控制器高度相似同样存放于Web/Controller/目录、同样通过 FrontController 挂载进应用路由。本文以HelloWorld示例为主线完整演示在 IHP 中创建 WebSocket 控制器、配置路由、从浏览器 JS 建立连接、双向收发文本与 JSON 数据、利用状态机管理连接生命周期、访问当前登录用户以及自定义解码器、自定义路径与 Ping 保活等进阶能力并辅以仓库源码级原理说明让读者既能照做也能理解底层机制。如果只是想用 WebSockets 推送 HTML/UI 更新页面局部自动刷新IHP 提供了更高层的 Auto Refresh API详见 Auto Refresh 指南本文不再展开。1. 了解 WebSocket 在 IHP 中的位置在 IHP 中WebSocket 服务端入口的写法与普通 Controller 一脉相承同样用一个自定义数据类型表示同样放在Web/Controller/目录下同样被注册进 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实例由四部分组成初始状态initialState、主逻辑run、生命周期回调onPing/onClose以及可选的connectionOptions用于覆盖握手/连接级选项例如启用 permessage-deflate 压缩注释见源码。整个框架构建在 Haskell 生态的websockets库之上import qualified Network.WebSockets as Websocket。2. 创建第一个 WebSocket 控制器2.1 定义控制器数据类型打开Web/Types.hs追加一个HelloWorldController类型data HelloWorldController HelloWorldController deriving (Eq, Show, Data)2.2 实现 WSApp 实例新建文件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给出连接建立时的初始状态run是连接建立后立即执行的入口sendTextData负责向客户端发送文本消息。从源码看run在 ihp/IHP/WebSocket.hs 中是带?state、?request、?modelContext、?connection等隐式参数的IO ()默认值为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。默认路径规则webSocketApp HelloWorldController会以类型名HelloWorldController作为访问路径见 ihp/IHP/RouterSupport.hs内部通过Typeable.typeOf取出类型名并交给webSocketAppWithCustomPath。因此控制器可通过以下地址访问ws://localhost:8000/HelloWorldController握手语义源码依据webSocketAppWithCustomPath生成的控制器路由最终调用 ControllerSupport.hs 中的startWebSocketAppAndFailOnHTTP——它先用WebSockets.websocketsApp尝试完成 WebSocket 握手HTTPGETUpgrade: websocket如果请求不是 WebSocket 升级请求则返回400 Bad Request响应体为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回调Hello World!则是服务器在run里通过sendTextData推送过来的消息。4. 双向收发消息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 端发送过来的数据它会阻塞等待直到客户端发来数据后才继续执行后续代码源码见 ihp/IHP/WebSocket.hs本质是Websocket.receiveData ?connection类型约束为WebSocketsData a。JS 端对应修改为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)把名字发到服务器服务器端receiveData拿到名字后回送问候语最终在onmessage里打印出来。4.2 发送 JSON框架提供了sendJSON辅助函数可以把 Haskell 数据结构编码成 JSON 字符串后发送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]底层实现sendJSON在 ihp/IHP/WebSocket.hs 中定义为sendTextData (Aeson.encode payload)即先用Data.Aeson编码为字节串再走文本消息通道发出。本例中客户端先发送名字随后服务器会推送形如{name:客户端之前发送的名字}的消息。4.3 接收其他数据类型receiveData不仅能收Text还能直接收Int、UUID、Bool等实现了WebSocketsData的类型myInt :: Int - receiveData myUUID :: UUID - receiveData myBool :: Bool - receiveData仓库为UUID提供了现成的WebSocketsData实例ihp/IHP/WebSocket.hs按 ASCII 编码的字节串解析 UUID。Auto Refresh 的会话 ID 正是通过receiveData UUID接收的见下文第 7 节。5. 管理有状态连接5.1 用数据类型表达状态机WebSocket 连接通常是长连接服务器端往往需要跨消息记住状态。IHP 的做法是让控制器数据类型本身成为状态机。例如我们要在连接关闭时打印$name has left就需要在某个地方记住名字于是把数据类型改为data HelloWorldController WaitForName | NameEntered { name :: Text } deriving (Eq, Show, Data)WaitForName表示用户尚未输入名字的状态NameEntered { name .. }记录已输入的名字。5.2 setState 与 getState把初始状态改为WaitForNameinitialState WaitForName更新run在拿到名字后把状态推进到NameEnteredrun do name - receiveData Text setState NameEntered { name } sendTextData (Hello name !)setState NameEntered { name }把连接状态从WaitForName切换到NameEntered。状态的两个原语操作定义在 ihp/IHP/WebSocket.hssetState :: (?state :: IORef state) state - IO () setState newState writeIORef ?state newState getState :: (?state :: IORef state) IO state getState readIORef ?state可以看到连接状态本质是一个IORef由startWSApp在连接建立时初始化ihp/IHP/WebSocket.hssetState/getState分别对应writeIORef/readIORef。5.3 onClose 生命周期回调要处理连接关闭事件使用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读回当前状态再按状态分别处理如果名字还没输入就关闭连接则什么都不做否则在运行 IHP 服务器的终端里打印$name has left。连接生命周期源码依据startWSAppihp/IHP/WebSocket.hs用withPingPong包装run并通过Exception.finally保证无论正常结束还是异常退出都会执行onClose其中ConnectionClosed与CloseRequest两种异常被静默处理其余异常会交给框架日志记录。Good to know当run函数执行完毕时连接会自动关闭。因此在上面的例子中输入名字、收到问候后连接随即关闭... has left!会立刻打印出来。若希望连接一直保持到浏览器窗口关闭给run加上forever循环run do -- Loops until the connection is closed forever do name - receiveData Text setState NameEntered { name } sendTextData (Hello name !)如本节所示setStategetState两个原语操作配合一个精心设计的数据结构就能非常灵活地管理有状态的 WebSocket 连接。6. 在 WebSocket 控制器中访问当前用户用户登录后可以在 WebSocket 控制器里直接使用常规的认证函数如currentUsermodule Web.Controller.HelloWorld where import Web.Controller.Prelude instance WSApp HelloWorldController where run do let name currentUser.name sendTextData (Hello name !)底层机制currentUser定义于 ihp/IHP/LoginSupport/Helper/Controller.hs其实现是“若未登录则redirectToLogin否则返回currentUserOrNothing的值”。之所以能在 WebSocket 控制器里使用是因为 startWebSocketApp 在处理连接前就通过let ?modelContext requestModelContext ?request把请求上下文注入隐式参数currentUser正是从请求的 vault 中读取认证信息lookupAuthVault currentUserVaultKey ?request。也就是说会话 Cookie 在握手时随 HTTP 请求一起到达框架据此还原登录态连接建立后即可随时访问。7. 进阶技巧7.1 为自定义类型编写解码器receiveData通过WebSocketsData类型类解码数据。要为自定义类型提供支持只需编写对应的实例。下面是仓库中UUID解码器的实现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要点fromDataMessage负责把文本/二进制消息解码成你的类型fromLazyByteString负责把懒字节串解码成你的类型toLazyByteString负责把你的类型编码回字节串用于发送。7.2 自定义访问路径不想用类型名做路径时可以用webSocketAppWithCustomPath覆盖默认路径instance FrontController WebApplication where controllers [ startPage StartPageAction -- Generator Marker , webSocketAppWithCustomPath HelloWorldController my-ws ]这样 WebSocket 服务就暴露在/my-ws下。其路由解析逻辑在 ihp/IHP/RouterSupport.hs先用 Attoparsec 匹配/加给定路径匹配成功则以WS.initialState webSocketApp启动 WebSocket 应用。另外两种相关形态源码依据webSocketAppWithHTTPFallback/webSocketAppWithCustomPathAndHTTPFallbackihp/IHP/RouterSupport.hs需要同时满足WSApp与Controller约束当请求不是 WebSocket 升级时会回退到普通 HTTP action 处理用runActionWithNewContext执行webSocketRouteihp/IHP/RouterSupport.hs基于 Trie 的注册形态被[routes|...|]DSL 的WS关键字使用注册在GET方法下WebSocket 握手本身就是 GET Upgrade。7.3 基于 routes DSL 声明 WebSocket 路由除了在FrontController中手写webSocketApp也可以在新版显式路由 DSL 中用WS关键字注册见 路由指南 WebSocket 路由一节[routes|webRoutes GET /posts PostsAction WS /chat ChatApp WS /datasync DataSyncController |] instance FrontController WebApplication where controllers webRoutes注意事项来自路由指南均为 v1 形态的限制WS行的右侧是WSApp实例的类型名而不是 action 构造器WS 路由只支持静态路径不支持{capture}或{splat}捕获段WS 路由不读取查询参数?nameWS不能与 HTTP 方法写在同一行如WS|GET会被拒绝WS 路由必须放在小写命名的 binding 块中该形态不生成HasPath实例如需在 JS 客户端代码里调用pathTo ChatApp需手动声明instance HasPath ChatApp where pathTo _ /chat若需要 HTTP 回退形态仍请使用webSocketAppWithHTTPFallback等传统方式两种注册方式可以在同一个controllers列表中共存。7.4 Ping 保活与 onPing默认情况下服务器每 30 秒会向浏览器发送一次 Ping以确保连接仍然存活。你可以重写onPing函数在每次 Ping 完成后执行自定义代码。真实用例Auto Refresh 的会话保活。IHP 内置的 Auto Refresh 正是基于 WebSocket 实现的ihp/IHP/AutoRefresh.hs页面渲染时注入autoRefreshMeta元标签见 ihp/IHP/AutoRefresh/View.hs前端ihp-auto-refresh.js读取其中的 session id 后连接/AutoRefreshWSAppihp/data/static/ihp-auto-refresh.js。其WSApp实例的onPing实现为onPing do now - getCurrentTime AutoRefreshActive { sessionId } - getState updateSession sessionId (\session - session { lastPing now })含义每次 Ping 完成就把当前会话的lastPing刷新为当前时间。Auto Refresh 的会话回收逻辑isSessionExpiredihp/IHP/AutoRefresh.hs规定超过 60 秒没有 Ping 的会话视为过期并被 GC 清理。由于服务器每 30 秒 Ping 一次只要连接正常会话就永远不会被误回收——这正是onPing的典型应用场景。8. 小结与推荐阅读本文覆盖了 IHP WebSockets 的完整开发路径能力关键 API / 写法源码位置控制器类型WSApp类型类ihp/IHP/WebSocket.hs挂载路由webSocketApp/webSocketAppWithCustomPathihp/IHP/RouterSupport.hs连接启动startWebSocketAppihp/IHP/ControllerSupport.hs收发数据receiveData/sendTextData/sendJSONihp/IHP/WebSocket.hs状态管理setState/getStateihp/IHP/WebSocket.hs生命周期onClose/onPingihp/IHP/WebSocket.hs自定义解码WebSocketsData实例ihp/IHP/WebSocket.hs如果想继续深入推荐阅读Auto Refresh 指南基于 WebSocket 构建的页面自动刷新方案路由指南的 WebSocket 路由章节[routes|...|]DSL 中的WS关键字与限制RouterSupport.hswebSocketApp系列函数与webSocketRoute的完整实现AutoRefresh.hs一个真实、完整的WSApp实例参考含receiveData UUID、setState、onPing、onClose的联合运用。赞分享后端【免费下载链接】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点击查看免费下载相关推荐终极Linux内核模块编程指南从Hello World到设备驱动开发实战终极Linux内核模块编程指南从Hello World到设备驱动开发实战 LKMPGLinux Kernel Module Programming Guid文档教程操作系统驱动开发从零部署DeepTutor让AI学习伴侣真正懂你从零部署DeepTutor让AI学习伴侣真正懂你 你收藏夹里那批必学资料是不是已经吃灰半年了想系统学点东西却被几十个PDF和课程截图堵在起点完全不人工智能AI 应用AI Agent多智能体RAG教育后端前端PrusaSlicer 插件 API 实战指南从 Hello World 到签名分发PrusaSlicer 插件 API 实战指南从 Hello World 到签名分发 本指南基于 PrusaSlicer 仓库中的 Plugin_API.md桌面应用3D渲染上一篇KeyDive开发者指南如何扩展支持新的Android设备与Widevine版本下一篇NSWindowStyles揭秘打造高颜值macOS应用界面的核心技巧创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表