尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Sails WebSockets 实时通信指南:sails.io.js 客户端、sails.sockets 服务端与 Resourceful PubSub 深度解析

Sails WebSockets 实时通信指南:sails.io.js 客户端、sails.sockets 服务端与 Resourceful PubSub 深度解析 Sails WebSockets 实时通信指南sails.io.js 客户端、sails.sockets 服务端与 Resourceful PubSub 深度解析【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sailsSails 是一个基于 Node.js 的实时RealtimeMVC 框架其实时能力全部建立在 WebSockets 之上。本文以 WebSockets 参考文档 为骨架系统梳理 Sails 实时通信的完整技术栈底层以 Socket.IO 为引擎、以sails.sockets.*为服务端统一推送接口、以sails.io.js为浏览器/Node.js 客户端 SDK并以Resourceful PubSubRPS提供模型级的发布/订阅抽象。读完本文你将掌握从客户端发虚拟请求、到服务端房间广播、再到模型数据变更实时推送的完整实战链路并能理解其背后的源码实现原理。1. Sails 实时通信架构总览Sails 使用 Socket.IO 作为实时通信的底层引擎。每一个 Sails 应用都会持有一个 Socket.IO 实例以sails.io的形式暴露出来。但框架并不推荐直接操作sails.io——绝大多数 Socket.IO 功能都被包装成了更安全、更便捷的sails.sockets.*方法。整个实时通信体系由四大部分组成对应官方参考文档的四个入口组成职责仓库内文档入口Realtime 概念实时通信的总体设计思想与适用场景docs/concepts/Realtime/Realtime.mdSocket Clientsails.io.js客户端向服务端发送虚拟请求并监听事件docs/reference/websockets/sails.io.js/sails.io.js.mdsails.sockets服务端向已连接的 socket 客户端实时推送消息docs/reference/websockets/sails.sockets/sails.sockets.mdResourceful PubSub基于模型Model的发布/订阅用于数据变更实时同步docs/reference/websockets/resourceful-pubsub/resourceful-pubsub.md关键架构事实见 websockets.mdsails.io是每个 Sails 应用自带的 Socket.IO 实例而sails.sockets是对 Socket.IO 功能的便捷且安全包装层。从源码看lib/hooks/pubsub/index.js 实现的 Resourceful PubSub 方法底层也正是调用sails.sockets.broadcast()、sails.sockets.join()、sails.sockets.leave()这些基础方法详见第 4 节。2. 客户端通信sails.io.js的引入与配置sails.io.js是 Sails 官方提供的 Socket 客户端 SDK一个轻量级的浏览器库也支持在 Node.js 服务端使用。它本质上是 Socket.IO 客户端之上的一层封装目的是让通过 WebSocket 与 Sails 后端收发消息这件事变得尽可能简单。其核心设计目标是提供一套类似 Ajax 的接口.get()、.post()、.put()、.delete()以及.patch()。这意味着在浏览器里执行io.socket.post(/user)与向同一路由发送一个 HTTP POST 请求在 Sails 应用内部走的是完全相同的路由解析链路——只是底层传输换成了 WebSocket。这个机制得益于 Sails 的虚拟请求解释器virtual request interpreter客户端发出 Socket.IO 消息时使用保留的事件名Sails 收到后将其解释为模拟 HTTP 请求再按应用的路由与蓝图配置分发到对应的 policies、controllers 等参见 sails.io.js.md 的 FAQ。2.1 浏览器中的基本用法在浏览器中只需通过script标签引入即可。新建 Sails 应用默认会把该库放到assets/js/dependencies目录!-- 这段代码会引入 Sails 应用默认捆绑的 sails.io.js 库。 捆绑版本同时内嵌了压缩过的 Socket.io 客户端代码。 脚本加载后经过一个事件循环 tick会自动创建一个 eager急切连接的 socket 开始连接 除非你通过配置关闭该行为。 -- script typetext/javascript src/js/dependencies/sails.io.js/script引入后即可在后续的内联或外部脚本中直接使用全局变量io.socket。该库同时存在一些社区项目实现的 iOS、Android、Windows Phone 原生客户端但其官方 SDK 本体是 JavaScript 编写的同样可用于服务端。2.2 Node.js 中的基本用法要在 Node.js 脚本中使用 Sails socket 客户端需要同时安装并requiresails.io.js与socket.io-client两个库// 用 socket.io-client 模块初始化 sails.io.js 库 // 初始化时会自动创建并连接一个 socket 作为 io.socket除非配置关闭。 var io require(sails.io.js)( require(socket.io-client) );2.3 配置方式一HTML 属性在浏览器中最简单的配置方式是直接在script标签上书写 HTML 属性。官方支持四个最常用的配置项autoConnect、environment、headers和urlscript src/js/dependencies/sails.io.js autoConnectfalse environmentproduction headers{ x-csrf-token: % typeof _csrf ! undefined ? _csrf : % } /script上例的效果是禁用急切连接的 socket、将客户端环境强制设为production减少日志输出、并设置一个会在每个 socket 请求中携带的x-csrf-token头除非被单独覆盖。使用 HTML 属性配置时有两个要点复合值必须 JSON 编码像headers这种字典类型需要用一对单引号包裹整个值因为其键名与字符串值必须用双引号示例中正是外单内双的写法。支持data-前缀任何 HTML 属性配置都可以写成data-autoConnect、data-environment、data-headers、data-url形式以兼容对非标准 HTML 属性支持不佳的浏览器。若同一配置同时给出标准属性与data-前缀属性后者优先。注意若你使用默认的 Grunt 资源管道它会自动注入 script 标签需要把sails.io.js从pipeline.js中移除改为显式书写script标签引入否则该配置方式不生效。2.4 配置方式二编程式修改io.sails自 Sails v0.12.x 起HTML 属性只能配置最基础的选项其余选项必须通过编程方式操作io.sails对象。HTML 属性方案本质上只是这种编程式方案的一种快捷写法。sails.io.js在页面加载后会等待一个事件循环周期再自动连接 socket前提是io.sails.autoConnect为开启状态这正是为了留出时间让你修改io.sails上的配置。为了保证配置在连接前被读取设置属性的代码应紧跟在引入sails.io.js的script标签之后script src/js/dependencies/sails.io.js/script script typetext/javascript io.sails.url https://myapp.com; /script !-- ...其他脚本... --默认情况下socket 客户端总是连接当前服务该脚本的服务器上例则会让急切连接的 socket 改为尝试跨域连接运行在https://myapp.com的 Sails 服务器。注意同样地若使用默认 Grunt 资源管道建议把sails.io.js从pipeline.js中排除并显式添加script标签。这样才能保证内联配置脚本如上面的io.sails.url ...在 socket 客户端之后立即执行先于急切连接的 socket 开始连接。2.5io.sails的常用配置项io.sails是sails.io.js库及其创建的所有 socket 的全局配置中心详见 io.sails.md属性类型默认值说明autoConnectbooleantrue为true时库加载后等待一个事件循环周期自动创建并连接一个SailsSocket到io.sails.url指定的地址。浏览器中该 socket 暴露为io.socketNode.js 脚本中则挂到初始化变量如io的socket属性上reconnectionbooleanfalse为true时socket 若意外断开非.disconnect()主动断开会自动持续尝试重连environmentstring—设置客户端日志详细程度development输出完整日志production输出最少日志urlstring与io.sails.url一致socket 连接或将要连接的目标 URLtransportsarray与io.sails.transports一致socket 尝试使用的传输方式按顺序尝试且允许升级。例如同时列出polling与websocket会先建立长轮询连接再由服务端尝试升级为 websocket 连接。该值应与 Sails 应用中的sails.config.sockets.transports保持一致headersdictionary与io.sails.headers一致该 socket 每次请求默认携带的请求头字典可通过.request()的headers选项覆盖除autoConnect、reconnection、environment外io.sails的其余属性在创建新 socket无论是急切连接的 socket 还是io.sails.connect()手动创建的时作为默认值使用。若io.sails.autoConnect为false或需要建立多个 socket 连接则需通过io.sails.connect([url], [options])手动创建——两个参数均可选未提供时回退到io.sails上的对应属性值。2.6io.socket自动创建的客户端 socket在浏览器中sails.io.js加载后即创建全局的SailsSocket实例io.socket并在等待一个事件循环周期后尝试连接服务器。在连接建立之前你就可以开始使用它的属性和方法——所有请求与事件绑定会被排队待连接建立后自动重放详见 io.socket.md。io.socket同样受全局io.sails配置影响因此在连接前的那个事件循环周期内修改配置即可改变其行为script typetext/javascript src/js/dependencies/sails.io.js/script script typetext/javascript io.sails.url http://somesailsapp.com; /script3. 虚拟请求io.socket.*方法全解io.socket提供与 HTTP 动词一一对应的虚拟请求方法。所有方法签名统一为io.socket.verb(url, data, callback)回调参数为(resData, jwres)resDataSails 服务器响应中发送的数据等价于jwres.body。jwresJSON WebSocket Response 对象包含headers字典、bodyJSON和statusCode数字。方法用途data 处理方式io.socket.get()发送虚拟 GET 请求data 会被 URL 编码并追加到url保留 url 中已有的 query 参数io.socket.post()发送虚拟 POST 请求data 会被 JSON 编码并作为虚拟 HTTP bodyio.socket.put()发送虚拟 PUT 请求同上io.socket.patch()发送虚拟 PATCH 请求同上io.socket.delete()发送虚拟 DELETE 请求data 会被 URL 编码并追加到url保留已有 query 参数3.1 各方法的签名与示例GETurl为必填 stringdata与callback可选io.socket.get(url, data, function (resData, jwres){ // ... });script io.socket.get(/users/9, function (resData) { // resData {id:9, name: Timmy Mendez} }); /scriptPOST / PUT / PATCHdata为可选 JSON会被 JSON 编码进虚拟请求体script io.socket.post(/users, { name: Timmy Mendez }, function (resData, jwRes) { jwRes.statusCode; // 200 }); /scriptscript io.socket.put(/users/9, { occupation: psychic }, function (resData, jwr) { resData.statusCode; // 200 }); /scriptDELETEdata会被 URL 编码进 query stringscript io.socket.delete(/users/9, function (resData) { resData; // {id:9, name: Timmy Mendez, occupation: psychic} }); /script所有方法都支持与任意路由通信——蓝图路由、自定义路由、/下的一切路径均可通过 socket 请求访问。3.2 底层方法io.socket.request()当需要更细粒度地控制请求头、参数、方法时可以使用io.socket.request()。它与io.socket.get()等的关系可以类比 jQuery 中$.ajax与$.get的关系——前者是后者更底层的实现基础。io.socket.request(options, function (resData, jwres)){ // ... // jwres.headers // jwres.statusCode // jwres.body resData // ... });options支持以下键选项类型说明methodstringHTTP 请求方法如GETurlstring目标 URL 路径如/checkoutdatajson可选请求数据会被 JSON 编码并作为虚拟 HTTP bodyheadersdictionary可选作为虚拟请求头发送的字符串字典完整示例携带自定义 CSRF 头io.socket.request({ method: get, url: /user/3/friends, data: { limit: 15 }, headers: { x-csrf-token: ji4brixbiub3 } }, function (resData, jwres) { if (jwres.error) { console.log(jwres.statusCode); // e.g. 403 return; } console.log(jwres.statusCode); // e.g. 200 });若想为所有发出的请求统一设置默认头则应在io.sails.headers上配置。4. 服务端推送sails.sockets.*方法全解sails.sockets提供了面向服务端的实时通信接口用于主动向已连接的 socket 客户端推送事件与数据——无需等待客户端的 HTTP 请求。这些方法对客户端来源一视同仁无论是浏览器标签页、iOS 应用还是物联网设备只要 socket 已连接即可推送详见 sails.sockets.md。4.1 方法速查表方法说明.addRoomMembersToRooms()将某个房间的所有成员订阅到一个或多个其他房间.blast()向连接到服务器的所有socket 广播消息.broadcast()向某个房间内的所有socket 广播消息.getId()从传入的 socket 请求req中解析出 socket ID.join()将某个 socket 订阅到一个房间.leave()将某个 socket 从房间退订.leaveAll()将某房间所有成员从该房间及其当前订阅的所有其他房间退订但保留与每个 socket ID 同名的自动房间.removeRoomMembersFromRooms()将某房间的所有成员从一个或多个其他房间退订4.2 与sails.io的关系这些方法基于内置的 Socket.IO 实例实现即sails.io。但官方强烈建议几乎永远不要直接使用sails.io而应调用sails.sockets.*方法。原因在于sails.sockets封装了房间管理、多服务器协调等细节更安全也更符合 Sails 的约定优于配置哲学。对于某些用例还可以使用更高层抽象的Resourceful PubSub 方法下一节——它们正是 Sails 内置 blueprint API 所使用的推送机制。部分sails.sockets方法在 Sails v0.12 中已被弃用或出于性能/可扩展性原因被更高效的别名取代详见 docs/upgrading/To0.12.md 迁移指南。5. Resourceful PubSubRPS模型级发布/订阅对于重度依赖实时通信的应用如点对点聊天、社交网络手动发送和监听 socket 事件很快会变得难以管理。Sails 通过引入resourceful PubSub发布/订阅模式来平滑这部分复杂度应用中的每个模型都自动装配了 RPS 方法提供一套约定俗成、以数据为中心的接口用于广播通知与将 socket 订阅到单个数据库记录的通知上详见 resourceful-pubsub.md。5.1 三个核心方法与sails.sockets的映射RPS 共暴露三个方法理解它们最简单的方式是先理解sails.sockets.*——因为每个 RPS 方法本质上都是某个更简单的sails.sockets.*方法加上上下文语境的包装RPS 方法底层对应文档.publish()sails.sockets.broadcast()publish.md.subscribe()sails.sockets.join()subscribe.md.unsubscribe()sails.sockets.leave()unsubscribe.md两者最大的区别在于抽象层级RPS 方法在幕后替你选定房间名并根据模型的 identity 推断出约定俗成的事件名从而统一了应用中 socket 通信的接口约定——这些约定包括房间命名、消息数据结构、事件命名等。源码印证在 lib/hooks/pubsub/index.js 中可以看到这个映射关系的实现。该 hook 通过getPubsubMethods()为所有模型装配subscribe、unsubscribe、publish及_room、_publishCreate、_publishUpdate、_publishDestroy等私有方法第 146-161 行附近其中publish(ids, data, req)内部对每个 id 计算房间名后调用sails.sockets.broadcast(room, self.identity, data, socketToOmit)subscribe(req, ids)内部逐个调用sails.sockets.join(socket, self._room(id))unsubscribe(req, ids)内部逐个调用sails.sockets.leave(socket, self._room(id))。从源码结构看RPS 确实只是给sails.sockets基础方法套上模型身份上下文的更高层封装印证了文档的表述。5.2 使用sails.io.js监听 RPS 事件在客户端监听 RPS 事件非常简单事件名就是模型 identity默认是模型名的小写形式除非在模型文件中手动配置了 identity参见 model-settings.mdio.socket.on(model identity, function (msg) { });5.3 完整示例从订阅到收到推送假设应用中有一个名为User的模型带一个name属性。第一步客户端添加事件监听监听user事件io.socket.on(user, function(msg){ console.log(msg); })此后客户端收到的、事件名为user的 socket 通知都会被打印到控制台。但注意只有先把客户端 socket 订阅到具体的User记录上才会真正收到消息。第二步通过蓝图 API 自动订阅。如果应用启用了 blueprint API订阅非常简单Find蓝图动作如果是通过 socket 请求访问的除了返回数据外还会自动调用User.subscribe()。例如客户端发送 socket GET 请求io.socket.get(/user, function(resData) { console.log(resData); });这会命中Find蓝图动作返回当前用户列表。如果是普通 HTTP 请求如jQuery.get(/user)到这里就结束了但因为这是socket 请求服务器额外将当前客户端 socket 订阅到了这些返回的用户记录上今后对这些记录的.publish()调用都会推送给它。源码印证在 lib/hooks/blueprints/actions/find.js 第 44 行可以看到Model.subscribe(req, _.pluck(matchingRecords, Model.primaryKey))的调用——Find 蓝图动作在返回匹配记录的同时确实把请求 socket 订阅到了这些记录与文档描述完全一致。第三步触发发布。.publish()可以在任何地方执行——由 socket 请求、AJAX 请求甚至命令行 cURL 触发的 controller action也可以来自自定义 helper 或命令行脚本这是.subscribe()所不具备的灵活性。继续上面的例子再打开一个浏览器窗口访问/user/create?namejoe原始窗口的控制台会看到如下消息{ verb: created, id: 1, data: { id: 1, name: joe, createdAt: 2014-08-01T05:50:19.855Z updatedAt: 2014-08-01T05:50:19.855Z } }这就是由Create蓝图动作广播出来的字典对象。在 blueprint API 场景下该数据格式是标准化的而在你自己的代码中.publish()可以广播任何你想要的数据。5.4 何时用 RPS、何时用sails.socketsRPS 方法专门服务于一个或多个 UI 监听 socket 事件以与后端保持同步的场景。如果不符合你的用例或者难以抉择直接调用sails.sockets.broadcast()、sails.sockets.join()、sails.sockets.leave()也是完全正确的做法。两种方式可以在同一应用中共存甚至混用。测试佐证仓库中的集成测试 hook.pubsub.modelEvents.subscribers.test.js 与 hook.pubsub.modelEvents.noSubscribers.test.js 分别验证了有订阅者时收到模型事件与无订阅者时不推送两种行为可作为理解 RPS 消息流转的参考。6. 常见问题与最佳实践6.1 可以用在任何前端框架中吗可以。Sails socket 客户端能与 Angular、React、Ember、Backbone、Knockout、jQuery 等任何前端框架配合使用——它只是封装了 Socket.IO 客户端与具体 UI 框架无关。6.2 必须使用sails.io.js吗不是必须的。构建基于浏览器的实时/聊天功能时它非常有用但如果你在构建原生应用或纯 API无任何 UI它和assets/目录下的大多数样板文件一样可能并不需要。与 Sails 中其他样板文件一样socket 客户端完全可选——删除assets/js/dependencies/sails.io.js即可移除。6.3 如何阻止 socket 绑定当前浏览器会话默认情况下socket 连接会通过初始握手时发送的cookie头与当前浏览器会话如果有关联。若要关闭该行为在 socket 连接前给query属性加上nosessiontruescript src/js/dependencies/sails.io.js/script script typetext/javascriptio.sails.querynosessiontrue;/script6.4 可以绕过该客户端直接使用 Socket.IO 吗技术上可以绕过虚拟请求解释器直接与 Socket.IO 通信但官方不推荐——这会破坏框架约定优于配置的哲学。sails.io.js是无侵入式的它包装原生 Socket.IO 客户端暴露更高级的 API借助 Sails 的虚拟请求解释器发送模拟 HTTP 请求。这样做让后端代码可复用性更高、降低了 WebSockets/Socket.IO 新手的入门门槛也让应用更易于理解。在极少数情况下例如与直接使用 Socket.IO 的遗留前端兼容绕开虚拟请求解释器是必须的。如果确实需要可以在后端用sails.io访问原始 Socket.IO 实例。但请仅在具备丰富 Socket.IO 实战经验、且已通读 sockets hook 内部实现特别是其 Redis 集成后再走这条路。6.5 实践建议汇总配置先行若需调整io.socket的连接目标或默认头务必把配置脚本紧跟在sails.io.js的script标签之后赶在 eager socket 连接前执行。注意transports一致性客户端io.sails.transports应与服务端sails.config.sockets.transports保持一致避免握手失败。优先高层抽象服务端推送优先用sails.sockets.*而非裸sails.io涉及模型数据变更时优先用 RPS 方法获得统一的房间/事件命名约定。默认头 vs 单次头全局请求头用io.sails.headers设置单次请求的定制头用io.socket.request()的headers选项。测试先行参考仓库中的 hook.pubsub.modelEvents.subscribers.test.js 等测试验证你的实时推送逻辑在有/无订阅者两种场景下的表现。7. 延伸阅读实时概念总览docs/concepts/Realtime/Realtime.md服务端实时事件、docs/concepts/Realtime/On the server.md、docs/concepts/Realtime/On the client.md、docs/concepts/Realtime/Multi-server environments.md多服务器环境客户端 SDK 各方法详情io.socket.get、io.socket.post、io.socket.put、io.socket.patch、io.socket.delete、io.socket.request、io.socket.on、io.socket.offsails.sockets各方法详情broadcast、join、leave、blast 等Resourceful PubSub 详情publish、subscribe、unsubscribe、get-room-name蓝图 APIRPS 的默认使用者docs/reference/blueprint-api/blueprint-api.md、Find、Create底层实现源码lib/hooks/pubsub/index.jsRPS 实现、lib/hooks/blueprints/actions/find.jsFind 蓝图自动订阅、lib/hooks/blueprints/actions/update.jsUpdate 蓝图发布变更【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表