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