Phoenix Channels 客户端协议实战:手写 WebSocket 客户端接入 Phoenix 的完整指南
Phoenix Channels 客户端协议实战手写 WebSocket 客户端接入 Phoenix 的完整指南【免费下载链接】phoenixPeace of mind from prototype to production项目地址: https://gitcode.com/gh_mirrors/ph/phoenix本指南以 Phoenix 官方文档 Writing a Channels Client 为主线讲解如何不依赖官方phoenix.js通过原生 WebSocket或任意 WebSocket 客户端库手工实现 Phoenix Channels 客户端。文中将覆盖连接建立、V2 序列化消息格式、phx_join/phx_leave/heartbeat三大内建事件、phx_reply应答语义以及自定义handle_in事件的收发并对照本仓库源码WebSocket 传输层、V2 序列化器、Socket 消息分发 与 Channel 服务端逐层验证协议细节帮助读者完成从连不上到收发自如的完整实践。概述为什么需要理解 Channels 客户端协议Phoenix Channels 是构建在 WebSocket以及 LongPoll之上的双向消息复用层。一条 WebSocket 连接可以同时承载任意多个频道topic每个频道内部又按事件event划分消息类别。正因为 WebSocket 是双向的消息可以在任意时刻从任意一侧发起所以客户端通常采用回调callback驱动的模型注册好各类事件的处理函数然后被动等待入站消息。一个最基本的客户端必须满足两条约束至少加入一个 topic加入之后才能在该 topic 上发送和接收消息源码层面由 Phoenix.Socket 的handle_in/4保证未加入的 topic 会收到{:error, %{reason: unmatched topic}}应答。同一连接上可以加入任意多个 topic多路复用由 socket 层统一管理。官方仓库中已有多个语言实现的客户端库但如果你需要为自有平台嵌入式设备、命令行工具、游戏客户端、非 JavaScript 前端编写客户端或者只想用 wscat、浏览器 DevTools 等工具做协议级调试这份协议手册就非常有价值。官方参考实现是 assets/js/phoenix/channel.js 与 assets/js/phoenix/socket.js它们同样是本仓库的一部分可作为对照范本。建立连接从 Endpoint 的 socket 声明到 WebSocket Upgrade1. 找到 socket 声明与连接路径要连接 Phoenix Channels首先找到应用Endpoint模块中的socket声明。例如在 lib/phoenix/endpoint.ex 中定义的socket/3宏声明形如socket /mobile, MyAppWeb.MobileSocket那么初始 HTTP 请求路径就是[host]:[port]/mobile/websocket?vsn2.0.0路径由两部分拼接而成socket声明的路径前缀/mobile WebSocket 传输层的默认路径/websocket见 lib/phoenix/transports/websocket.ex 中default_config/0的path: /websocket。若服务端在socket声明中显式配置了websocket: [path: /...]则以配置为准。2.vsn参数与序列化器协商查询参数vsn2.0.0用于选择V2 JSON 序列化器。V2 序列化器是 Phoenix 内置的期望消息以JSON 数组list的形式出现其核心实现位于 lib/phoenix/socket/serializers/v2_json_serializer.ex入站decode!/2调用Phoenix.json_library().decode!/1把 JSON 文本解析为列表再填充进Phoenix.Socket.Message结构体出站encode!/1把Message/Reply编码为[join_ref, ref, topic, event, payload]这样的五元列表 JSON。序列化器的协商发生在 socket 连接阶段见 lib/phoenix/socket.ex 的negotiate_serializer/2服务端把请求参数中的vsn与serializer配置里每个序列化器声明的版本需求如{V2.JSONSerializer, ~ 2.0.0}做版本匹配。匹配失败时服务端会记录 warning 并拒绝连接。WebSocket 传输层默认配置为serializer: [{V1.JSONSerializer, ~ 1.0.0}, {V2.JSONSerializer, ~ 2.0.0}]也就是说vsn2.0.0选中 V2vsn1.0.0则可选中 V1。V1 序列化器lib/phoenix/socket/serializers/v1_json_serializer.ex的消息形态是 JSON对象map与 V2 的列表形态不同本指南默认使用 V2。官方 JavaScript 客户端在 assets/js/phoenix/constants.js 中默认发送DEFAULT_VSN 2.0.0。3. HTTP 升级与自定义握手WebSocket 连接始于一次普通的 HTTP 请求因此必须携带标准的HTTP Upgrade 头字段Connection: Upgrade、Upgrade: websocket、Sec-WebSocket-Key、Sec-WebSocket-Version等或者使用替你完成这些工作的 HTTP/WebSocket 客户端库——在 Elixir 生态中mint_web_socket就是这样的例子。服务端收到 GET 请求后由 Phoenix.Transports.WebSocket 经 Plug 管道处理并升级内部使用WebSockAdapter.upgrade/4。此外以下内容可能由应用的 socket 模块决定查询参数socket 模块的connect/3或connect/2会收到这些 params例如?tokenxxx、?user_idxxx用于认证与授权请求头若 WebSocket 配置启用了auth_tokenPhoenix 支持通过Sec-WebSocket-Protocol头传递base64url.bearer.phx.前缀的认证令牌见 lib/phoenix/transports/websocket.ex 的maybe_auth_token_from_header/2令牌前缀常量定义在同文件的auth_token_prefix base64url.bearer.phx.此时客户端也必须在请求中声明phoenix子协议Origin 校验默认check_origin: true服务端会校验Origin头是否与Endpoint.config(:url)[:host]匹配自定义允许来源、check_origin: :conn或 MFA 形式详见 lib/phoenix/endpoint.ex 中socket/3的文档。连接建立后connect/3返回{:ok, socket}或:error/{:error, reason}返回:error时服务端以 HTTP 403 拒绝lib/phoenix/transports/websocket.ex 的call/2WebSocket 握手失败。消息格式V2 序列化器的五元列表服务端与客户端之间所有的应用消息在 V2 JSON 序列化器下都遵循同一个通用格式[join_reference, message_reference, topic_name, event_name, payload]这也是 lib/phoenix/socket/serializers/v2_json_serializer.ex 中decode_text/1与encode!/1共同使用的结构。各字段语义如下字段选填/必填含义与规则join_reference必填phx_join时客户端自选的唯一值用于标识某一次加入。加入成功后后续发送到该频道的每条消息都应携带当前的join_reference服务端据此丢弃上一次加入产生的陈旧消息为向后兼容服务端也接受null。它同时充当服务端push主动推送消息的引用——例如新用户加入了聊天室这类不是对某条客户端消息的应答的推送。message_reference必填客户端自选的唯一值。服务端在应答reply中回显该值客户端据此知道应答对应的是哪条消息。topic_name必填必须是该 socket endpoint 已知的 topic即 socket 模块中channel宏声明的匹配模式且客户端必须先加入该 topic 才能在其上发消息。event_name必填必须匹配服务端频道模块中某个handle_in函数的第一个参数事件名。payload必填可序列化的值通常要求 JSON 可序列化具体取决于 Socket 的序列化器配置将作为该handle_in函数的第二个参数传入。从服务端看入站消息会被 lib/phoenix/socket.ex 的__in__/2解码为Phoenix.Socket.Message结构体其字段定义见 lib/phoenix/socket/message.extopic、event、payload、ref、join_ref再按 topic 分发到对应的 Channel 进程。Channel 进程收到普通事件后调用频道模块的handle_in(event, payload, socket)见 lib/phoenix/channel/server.ex 的handle_info/2。关于 payload 的边界V2 序列化器在encode!/1中要求出站 payload 必须是 map否则抛出ArgumentErrorexpected payload to be a map入站时 payload 则直接取自 JSON 数组第五个元素。若消息体是二进制数据{:binary, data}形式V2 序列化器会退化为二进制帧编码例如把Message编码为kind, join_ref_size, topic_size, event_size, ...的紧凑二进制头结构对应测试见 test/phoenix/socket/v2_json_serializer_test.exs。官方 JS 客户端的Serializerassets/js/phoenix/serializer.js也实现了同样的二进制编解码KINDS: {push: 0, reply: 1, broadcast: 2}。三个内建事件phx_join、phx_leave 与 heartbeat有三种事件被每一个 Phoenix 应用理解它们由 socket 层直接处理不会进入用户频道模块的handle_in见 lib/phoenix/socket.ex 中handle_in/4的各分支。1.phx_join加入频道phx_join用于加入一个频道。以加入miami:weather频道为例[0, 0, miami:weather, phx_join, {some: param}]其中join_reference为0message_reference为0payload{some: param}会作为认证参数传给频道模块的join/3。服务端处理流程lib/phoenix/socket.ex 的handle_in/4通过socket.handler.__channel__(topic)查找 topic 匹配的频道模块channel miami:weather, ...或channel miami:*, ...通配模式匹配成功则调用Phoenix.Channel.Server.join/4见 lib/phoenix/channel/server.ex启动频道进程并把 payload 作为auth_payload传给频道的join/3频道join/3返回{:ok, socket}/{:ok, reply, socket}/{:error, reply}结果以phx_reply事件回给客户端匹配失败则回{:error, %{reason: unmatched topic}}。需要留意的是服务端对每个传输连接最多同时加入的频道数有限制max_channels_per_transport默认 100超限会以%{reason: too many channels joined}拒绝加入lib/phoenix/socket.ex 中state.max_channels_per_transport的检查。2.phx_leave离开频道phx_leave用于离开频道。以离开miami:weather频道为例[0, 1, miami:weather, phx_leave, {}]服务端校验join_ref与当前已加入频道的 join 引用一致后把消息转给频道进程频道停止并以phx_reply应答:ok相关分支见 lib/phoenix/socket.ex 的handle_in/4与 lib/phoenix/channel/server.ex 对phx_leave的handle_info处理。若频道已不存在socket 层也会直接回一个:ok应答handle_in(nil, %{event: phx_leave, ...}, ...)分支保证客户端离开语义总是能得到确认。3.heartbeat维持连接heartbeat用于维持 WebSocket 连接存活[null, 2, phoenix, heartbeat, {}]注意这里join_reference为null心跳不属于任何频道topic_name固定为phoenix。服务端对phoenixtopic 的heartbeat事件会直接返回{:ok, %{}}应答而不启动任何频道lib/phoenix/socket.ex 的handle_in(_, %{ref: ref, topic: phoenix, event: heartbeat}, ...)分支。heartbeat仅在没有任何其他消息发送时才需要其作用是防止 Phoenix 因空闲超时关闭连接超时阈值:timeout配置在应用的Endpoint模块的 WebSocket 配置中lib/phoenix/transports/websocket.ex 的default_config/0为timeout: 60_000即默认 60 秒无消息会被判定超时。官方 JS 客户端默认每 30 秒heartbeatIntervalMs在空闲时发送一次心跳并以其作为探测断开的手段。服务端应答与主动推送理解 phx_reply应答reply与请求一一对应客户端发出的每条消息若服务端回复均以phx_reply事件回传其 JSON 形态为[join_ref, message_reference, topic_name, phx_reply, {status: ok, response: {...}}]这由 lib/phoenix/socket/serializers/v2_json_serializer.ex 的encode!(%Reply{})生成status与response被打包进 payload。客户端用message_reference字段把应答与之前发出的消息配对phx_join的应答还额外回显join_reference。状态值常见有ok与error例如加入未匹配 topic 会收到status: error, response: {reason: unmatched topic}。主动推送push服务端发起的消息服务端也可以不针对任何客户端消息主动向频道内的订阅者推送。这类消息使用当前连接的join_reference作为引用事件名是业务事件本身如new_msgpayload 为业务数据。典型触发路径是服务端调用Phoenix.Channel的broadcast/3或push/3见 lib/phoenix/channel/server.ex广播经 PubSub 分发后由频道的handle_out/3过滤并编码推送到传输层。服务端特殊的生命周期事件除phx_reply外socket 层还会发送两个特殊事件lib/phoenix/socket.ex 的encode_on_exit/4与encode_close/3phx_error频道进程崩溃、或尝试重复加入已加入频道等错误场景phx_close频道被优雅关闭如phx_leave完成或服务端主动关闭。健全的客户端应像官方 assets/js/phoenix/channel.js 那样监听这两个事件并触发对应的onClose/onError回调、执行自动重连逻辑。发送业务事件对接 handle_in除了上述三个内建事件其余允许的消息完全取决于 Phoenix 应用本身。假设服务端频道模块实现了如下handle_indef handle_in(report_emergency, payload, socket) do MyApp.Emergencies.report(payload) # or whatever {:reply, :ok, socket} end那么客户端可以发送[0, 3, miami:weather, report_emergency, {category: sharknado}]服务端会由 socket 层把消息解码为Message{topic: miami:weather, event: report_emergency, payload: %{category sharknado}, ref: 3, join_ref: 0}交给 lib/phoenix/channel/server.ex 的handle_info/2调用channel.handle_in(report_emergency, payload, socket){:reply, :ok, socket}被编码为phx_reply应答[0, 3, miami:weather, phx_reply, {status: ok, response: {}}]其中的3正是客户端原来的message_reference。handle_in/3支持多种返回值{:noreply, socket}、{:reply, status, socket}、{:reply, {status, response}, socket}、{:stop, reason, ...}等完整契约见 lib/phoenix/channel/server.ex 的handle_result/2错误提示。客户端在实现时应把事件名与服务端handle_in的第一个参数严格对齐并预期任何业务事件都可能收到phx_reply应答、也可能没有应答{:noreply, ...}时不回复。从零实现的最小客户端骨架把以上协议要素串起来一个最小可用的 V2 协议客户端伪代码示意与任何语言/库无关应包含connect(ws_url ?vsn2.0.0) on_open: send phx_join: [0, 0, miami:weather, phx_join, {some: param}] start heartbeat timer (仅当空闲时发送) on_message(raw): [join_ref, ref, topic, event, payload] JSON.parse(raw) if event phx_reply: 配对 ref 到挂起的请求join/push/leave分发 ok/error 回调 else if event phx_error: 触发该频道的错误处理与重连 else if event phx_close: 触发该频道的关闭处理 else: 分发到业务事件回调如 report_emergency 的推送 send business event: send [current_join_ref, unique_ref, miami:weather, report_emergency, {...}] ref 记入挂起表等待 phx_reply 配对工程化客户端还需考虑join_reference与message_reference的自增唯一性、未加入前禁止发消息官方 JS 客户端在 assets/js/phoenix/channel.js 中会抛错 tried to push ... before joining、网络中断后的自动重连与频道重加入、以及用join_reference丢弃上一次 join 的陈旧消息服务端正是依据 join_ref 一致性来忽略陈旧消息见 lib/phoenix/socket.ex 的handle_in/4对 join_ref 的校验注释。手动调试与验证建议本指南同样适用于用 WebSocket 客户端手工测试Phoenix Channels用wscat -c ws://localhost:4000/mobile/websocket?vsn2.0.0之类工具先验证握手与序列化器协商是否通过依次发送phx_join、业务事件、phx_leave、heartbeat观察 JSON 应答是否符合本文所述的phx_reply结构结合本仓库的单元测试 test/phoenix/socket/v2_json_serializer_test.exs 与集成测试 test/phoenix/integration/websocket_channels_test.exs 核对消息字节级编解码与完整收发链路若服务端 socket 模块在connect/3中要求额外 params如 token记得在 URL 或头部中携带并核对check_origin配置以免握手被 403 拒绝。掌握这套协议后无论是为自有语言编写 Phoenix Channels 客户端库还是手工驱动 WebSocket 做端到端调试都能做到所见即所信——因为你的客户端与官方phoenix.js走的是同一条 Phoenix.Socket 消息分发路径。【免费下载链接】phoenixPeace of mind from prototype to production项目地址: https://gitcode.com/gh_mirrors/ph/phoenix创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →