github.com/anycable/anycable-go@v1.5.1/docs/binary_formats.md (about) 1 # Binary messaging formats 2 3 <p class="pro-badge-header"></p> 4 5 AnyCable Pro allows you to use Msgpack or Protobufs instead of JSON to serialize incoming and outgoing data. Using binary formats bring the following benefits: faster (de)serialization and less data passing through network (see comparisons below). 6 7 ## Msgpack 8 9 ### Usage 10 11 In order to initiate Msgpack-encoded connection, a client MUST use `"actioncable-v1-msgpack"` or `"actioncable-v1-ext-msgpack"` subprotocol during the connection. 12 13 A client MUST encode outgoing and incoming messages using Msgpack. 14 15 ### Using Msgpack with AnyCable JS client 16 17 [AnyCable JavaScript client][anycable-client] supports Msgpack out-of-the-box: 18 19 ```js 20 // cable.js 21 import { createCable } from '@anycable/web' 22 import { MsgpackEncoder } from '@anycable/msgpack-encoder' 23 24 export default createCable({protocol: 'actioncable-v1-msgpack', encoder: new MsgpackEncoder()}) 25 26 // or for the extended Action Cable protocol 27 // export default createCable({protocol: 'actioncable-v1-ext-msgpack', encoder: new MsgpackEncoder()}) 28 ``` 29 30 ### Action Cable JavaScript client patch 31 32 Here is how you can patch the built-in Action Cable JavaScript client library to support Msgpack: 33 34 ```js 35 import { createConsumer, logger, adapters, INTERNAL } from "@rails/actioncable"; 36 // Make sure you added msgpack library to your frontend bundle: 37 // 38 // yarn add @ygoe/msgpack 39 // 40 import msgpack from "@ygoe/msgpack"; 41 42 let consumer; 43 44 // This is an application specific function to create an Action Cable consumer. 45 // Use it everywhere you need to connect to Action Cable. 46 export const createCable = () => { 47 if (!consumer) { 48 consumer = createConsumer(); 49 // Extend the connection object (see extensions code below) 50 Object.assign(consumer.connection, connectionExtension); 51 Object.assign(consumer.connection.events, connectionEventsExtension); 52 } 53 54 return consumer; 55 } 56 57 // Msgpack support 58 // Patches this file: https://github.com/rails/rails/blob/main/actioncable/app/javascript/action_cable/connection.js 59 60 // Replace JSON protocol with msgpack 61 const supportedProtocols = [ 62 "actioncable-v1-msgpack" 63 ] 64 65 const protocols = supportedProtocols 66 const { message_types } = INTERNAL 67 68 const connectionExtension = { 69 // We have to override the `open` function, since we MUST provide custom WS sub-protocol 70 open() { 71 if (this.isActive()) { 72 logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`) 73 return false 74 } else { 75 logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`) 76 if (this.webSocket) { this.uninstallEventHandlers() } 77 this.webSocket = new adapters.WebSocket(this.consumer.url, protocols) 78 this.webSocket.binaryType = "arraybuffer" 79 this.installEventHandlers() 80 this.monitor.start() 81 return true 82 } 83 }, 84 isProtocolSupported() { 85 return supportedProtocols[0] == this.getProtocol() 86 }, 87 send(data) { 88 if (this.isOpen()) { 89 const encoded = msgpack.encode(data); 90 this.webSocket.send(encoded) 91 return true 92 } else { 93 return false 94 } 95 } 96 } 97 98 // Incoming messages are handled by the connection.events.message function. 99 // There is no way to patch it, so, we have to copy-paste :( 100 const connectionEventsExtension = { 101 message(event) { 102 if (!this.isProtocolSupported()) { return } 103 const {identifier, message, reason, reconnect, type} = msgpack.decode(new Uint8Array(event.data)) 104 switch (type) { 105 case message_types.welcome: 106 this.monitor.recordConnect() 107 return this.subscriptions.reload() 108 case message_types.disconnect: 109 logger.log(`Disconnecting. Reason: ${reason}`) 110 return this.close({allowReconnect: reconnect}) 111 case message_types.ping: 112 return this.monitor.recordPing() 113 case message_types.confirmation: 114 return this.subscriptions.notify(identifier, "connected") 115 case message_types.rejection: 116 return this.subscriptions.reject(identifier) 117 default: 118 return this.subscriptions.notify(identifier, "received", message) 119 } 120 }, 121 }; 122 ``` 123 124 > See the [demo](https://github.com/anycable/anycable_rails_demo/pull/17) of using Msgpack in a Rails project with AnyCable Rack server. 125 126 ## Protobuf 127 128 We squeeze a bit more space by using Protocol Buffers. AnyCable uses the following schema: 129 130 ```proto 131 syntax = "proto3"; 132 133 package action_cable; 134 135 enum Type { 136 no_type = 0; 137 welcome = 1; 138 disconnect = 2; 139 ping = 3; 140 confirm_subscription = 4; 141 reject_subscription = 5; 142 confirm_history = 6; 143 reject_history = 7; 144 } 145 146 enum Command { 147 unknown_command = 0; 148 subscribe = 1; 149 unsubscribe = 2; 150 message = 3; 151 history = 4; 152 pong = 5; 153 } 154 155 message StreamHistoryRequest { 156 string epoch = 2; 157 int64 offset = 3; 158 } 159 160 message HistoryRequest { 161 int64 since = 1; 162 map<string, StreamHistoryRequest> streams = 2; 163 } 164 165 message Message { 166 Type type = 1; 167 Command command = 2; 168 string identifier = 3; 169 // Data is a JSON encoded string. 170 // This is by Action Cable protocol design. 171 string data = 4; 172 // Message has no structure. 173 // We use Msgpack to encode/decode it. 174 bytes message = 5; 175 string reason = 6; 176 bool reconnect = 7; 177 HistoryRequest history = 8; 178 } 179 180 message Reply { 181 Type type = 1; 182 string identifier = 2; 183 bytes message = 3; 184 string reason = 4; 185 bool reconnect = 5; 186 string stream_id = 6; 187 string epoch = 7; 188 int64 offset = 8; 189 string sid = 9; 190 bool restored = 10; 191 repeated string restored_ids = 11; 192 } 193 ``` 194 195 When using the standard Action Cable protocol (v1), both incoming and outgoing messages are encoded as `action_cable.Message` type. When using the extended version, incoming messages are encoded as `action_cable.Reply` type. 196 197 Note that `Message.message` field and `Reply.message` have the `bytes` type. This field carries the information sent from a server to clients, which could be of any form. We Msgpack to encode/decode this data. Thus, AnyCable Protobuf protocol is actually a mix of Protobufs and Msgpack. 198 199 ### Using Protobuf with AnyCable JS client 200 201 [AnyCable JavaScript client][anycable-client] supports Protobuf encoding out-of-the-box: 202 203 ```js 204 // cable.js 205 import { createCable } from '@anycable/web' 206 import { ProtobufEncoder } from '@anycable/protobuf-encoder' 207 208 export default createCable({protocol: 'actioncable-v1-protobuf', encoder: new ProtobufEncoder()}) 209 ``` 210 211 > See the [demo](https://github.com/anycable/anycable_rails_demo/pull/24) of using Protobuf encoder in a Rails project with AnyCable JS client. 212 213 To use Protobuf with the extended Action Cable protocol, use the following configuration: 214 215 ```js 216 // cable.js 217 import { createCable } from '@anycable/web' 218 import { ProtobufEncoderV2 } from '@anycable/protobuf-encoder' 219 220 export default createCable({protocol: 'actioncable-v1-ext-protobuf', encoder: new ProtobufEncoderV2()}) 221 ``` 222 223 ## Formats comparison 224 225 Here is the in/out traffic comparison: 226 227 | Encoder | Sent | Rcvd | 228 |----------|------|-------| 229 | protobuf | 315.32MB | 327.1KB | 230 | msgpack | 339.58MB | 473.6KB | 231 | json | 502.45MB | 571.8KB | 232 233 The data above were captured while running a [websocket-bench][] benchmark with the following parameters: 234 235 ```sh 236 websocket-bench broadcast ws://0.0.0.0:8080/cable —server-type=actioncable —origin http://0.0.0.0 —sample-size 100 —step-size 1000 —total-steps 5 —steps-delay 2 —wait-broadcasts=5 —payload-padding=100 237 ``` 238 239 **NOTE:** The numbers above depend on the messages structure. Binary formats are more efficient for _objects_ (JSON-like) and less efficient when you broadcast long strings (e.g., HTML fragments). 240 241 Here is the encode/decode speed comparison: 242 243 | Encoder | Decode (ns/op) | Encode (ns/op) | 244 |--------|------|-------| 245 | protobuf (base) | 425 | 1153 | 246 | msgpack (base) | 676 | 1512 | 247 | json (base) | 1386 | 1266 | 248 |||| 249 | protobuf (long) | 479 | 2370 | 250 | msgpack (long) | 763 | 2506 | 251 | json (long) | 2457 | 2319 | 252 253 Where base payload is: 254 255 ```json 256 { 257 "command": "message", 258 "identifier": "{\"channel\":\"test_channel\",\"channelId\":\"23\"}", 259 "data": "hello world" 260 } 261 ``` 262 263 And the long one is: 264 265 ```json 266 { 267 "command": "message", 268 // x10 means repeat string 10 times 269 "identifier": "{\"channel\":\"test_channel..(x10)\",\"channelId\":\"123..(x10)\"}", 270 // message is the base message from above 271 "message": { 272 "command": "message", 273 "identifier": "{\"channel\":\"test_channel\",\"channelId\":\"23\"}", 274 "data": "hello world" 275 } 276 } 277 ``` 278 279 [websocket-bench]: https://github.com/anycable/websocket-bench 280 [anycable-client]: https://github.com/anycable/anycable-client