Replacing JSON with MsgPack protocol in Phoenix WebSocket

Keywords: Programming socket github npm less

Why MsgPack?

  • MsgPack is lighter and less traffic than JSON
  • MsgPack's own date time type (always UTC)
  • MsgPack comes with binary data type (it's convenient to transfer a picture or something)
  • MsgPack supports custom types
  • The standards of MsgPack are open source and well documented

Phoenix.Socket.Serializer module

In the Phoenix framework, there is a behaviour module, Phoenix.Socket.Serializer (the earlier version is Phoenix.Transport.Serializer). What we need to do is to write a module to implement this behaviour.

Let's take a look at the Phoenix.Socket.Serializer module. from Official documents on hexdocs.pm As you can see, this module defines three callback s -- encode!/1, decode!/2, and fastlane!/1. Please refer to the document for specific types. I'd like to mention what fastlane is, because the official documents only mention one sentence about it

Phoenix Channels use a custom value to provide "fastlaning", allowing messages broadcast to thousands or even millions of users to be encoded once and written directly to sockets instead of being encoded per channel.

And the meaning of fastlane is not mentioned

In fact, if you understand Phoenix's WebSocket mechanism, you should know that there is an concept of intercept downlink events. If you don't block an event (the default), all messages of the event's downlink (server - > client) are serialized only once, and then the serialized IO data is pushed to each client. This method of serializing once and broadcasting to all clients is called fastlane. The callback serialized for fastlane is that fastlane!/1. In contrast, when you intercept an event, all the downlink messages need to be serialized separately for each client (which greatly affects performance). The callback serialized for this scenario is the encode!/1. Of course, reply is for a single client, so it's also encode!/1.

Realize self serializer

To save time, we use an existing MsgPack serialization / deserialization Library msgpax . The installation method is available on GitHub, so I won't talk about it here.

Next, write a module, MyAppWeb.MsgpaxSerializer. Where to put the file, please see the official to decide.

defmodule MyAppWeb.MsgpaxSerializer do
  # Explicitly declare that this module will implement all the callbacks of Serializer,
  # Convenient for dialyzer to check static code
  @behaviour Phoenix.Socket.Serializer
  
  alias Phoenix.Socket.{Message, Reply, Broadcast}
  
  @impl true
  def encode!(%Message{topic: topic, event: event, payload: payload, ref: ref, join_ref: join_ref}) do
    # Note the order of the contents of Msgpax.pack. This order must be consistent on all occasions!
    {:socket_push, :binary, Msgpax.pack!([topic, event, payload, ref, join_ref])}
  end
  
  def encode!(%Reply{topic: topic, status: status, payload: payload, ref: ref, join_ref: join_ref}) do
    # Note the order of the contents of Msgpax.pack. This order must be consistent on all occasions!
    {:socket_push, :binary, Msgpax.pack!([topic, status, payload, ref, join_ref])}
  end
  
  @impl true
  def decode!(raw_payload, _options) do
    # Do you see the order of data coming out of unpack? Same as above
    [topic, event, payload, ref, join_ref] = Msgpax.unpack!(raw_payload)
    %Message{
      topic: topic,
      event: event,
      payload: payload,
      ref: ref,
      join_ref: join_ref
    }
  end
  
  @impl true
  def fastlane!(%Broadcast{topic: topic, event: event, payload: payload}) do
    # Although there are no ref and join ref in the Broadcast structure, for the sake of safety, put two nil placeholders
    {:socket_push, :binary, Msgpax.pack!([topic, event, payload, nil, nil])}
  end
end

Server configuration

In order to use our own serializer globally, we need to modify the configuration. Open my app Web / endpoint.ex and make the following changes:

socket "/socket", MyAppWeb.UserSocket,
  # Websocket: true, < kill this line
  websocket: [serializer: [{MyAppWeb.MsgpaxSerializer, "~> 2.0.0"}]],  # Add this line
  longpoll: false

Note "~ > 2.0.0" in the configuration, which is used for "Protocol Version Negotiation" (my own name: D). When the client and the server shake hands, the client will bring the supported protocol version number (for example, vsn=2.0.0) in the query string of the URL. If the server does not support the version number, the handshake will fail.

Webpage code modification

In order for JavaScript to parse packages in MsgPack format, we need to introduce JS library @ msgpack/msgpack. How to add your front-end architecture? If you use npm, then

$ npm install --save @msgpack/msgpack

Find the file of the socket you configured for Phoenix (if you don't separate the front and back ends, the file of assets/js/socket.js) and make the following changes:

import {encode, decode} from '@msgpack/msgpack'

// Still that old saying, pay attention to the same order!
const serialize = ({topic, event, payload, ref, join_ref}, callback) => callback(encode([topic, event, payload, ref, join_ref]))

const deserialize = (rawMessage, callback) => {
  // Don't I repeat it?
  const [topic, event, payload, ref, join_ref] = decode(rawMessage)
  return callback({topic, event, payload, ref, join_ref})
}

let socket = new Socket("/socket", {
  ...,
  encode: serialize,  // Overwrite the original ws serialization mode
  decode: deserialize  // Overwrite the original ws deserialization mode
})

It's done! Enjoy your traffic saving trip:)

Appendix: entire agreement of MsgPack

https://github.com/msgpack/msgpack/blob/master/spec.md

Posted by shmoo525 on Mon, 27 Apr 2020 04:11:12 -0700