In the previous post, we saw how to support compression with permessage-deflate
extension for Rails ActionCable connections. Now that we are there, let’s go one step further and see how we can create a custom extension for WS frame compression.
As an example, I’ll demonstrate a way to compress JSON messages with messagepack. A note before starting, don’t expect huge savings if you are already using gzip compression using permessage-deflate
as messagepack savings can vary hugely based on the kind of data you are transmitting over the socket.
With that out of the way, let’s create our own permessage-messagepack
extension. First step, check out the documentation. We will need a ClientSession
that generates the offer for this custom protocol, a ServerSession
that generates a response accepting this custom offer, and a Session
that will process incoming and outgoing messages.
I’ll be using most of the boilerplate from permessage-messagepack
extension. The basic session class would look something like this:
module MessagePack
module PermessageMessagePack
class Session
VALID_PARAMS = [
"server_no_context_takeover",
"client_no_context_takeover"
]
def self.valid_params?(params)
return false unless params.keys.all? { |k| VALID_PARAMS.include?(k) }
return false if params.values.grep(Array).any?
if params.key?("server_no_context_takeover")
return false unless params["server_no_context_takeover"] == true
end
if params.key?("client_no_context_takeover")
return false unless params["client_no_context_takeover"] == true
end
true
end
def initialize(options)
@accept_no_context_takeover = options.fetch(:no_context_takeover, false)
end
end
end
end
Let’s get the easy bits out. Processing incoming and outgoing messages is going to be very straightforward since messagepack
has a well-developed ruby gem handling compression and uncompression. So, we can go forward and add our processing code to the Session
.
def process_incoming_message(message)
return message unless message.rsv2
suppress(Exception) do
message.data = Oj.dump(MessagePack.unpack(message.data))
end
message
end
def process_outgoing_message(message)
suppress(Exception) do
json = Oj.load(message.data)
message.data = MessagePack.pack(json)
message.rsv2 = true
end
message
end
The tricky bit here is understanding the rsv2
flag which needs to be set true when processing an outgoing message. This flag indicates to the receiver that the message has passed through this protocol. Similarly, when processing incoming message, we need to make sure this flag is true which would tell us that the message was compressed with this protocol. The WebSocket protocol has three flags, rsv1
, rsv2
and rsv3
for use for extensions. The rsv1
is already used by permessage-deflate
so we need to use the second one here.
Next up, generating offers from a ClientSession
(permessage_message_pack/client_session.rb
). Again most of it is boilerplate code from the other extension.
module MessagePack
module PermessageMessagePack
class ClientSession < Session
def generate_offer
offer = {}
if @accept_no_context_takeover
offer["client_no_context_takeover"] = true
end
offer
end
def activate(params)
return false unless ClientSession.valid_params?(params)
if @request_no_context_takeover && !params["server_no_context_takeover"]
return false
end
@own_context_takeover = !(@accept_no_context_takeover || params["client_no_context_takeover"])
@peer_context_takeover = !params["server_no_context_takeover"]
true
end
end
end
end
The final step for the extension, creating a server session.
module MessagePack
module PermessageMessagePack
class ServerSession < Session
def initialize(options, params)
super(options)
@params = params
end
def generate_response
response = {}
# https://tools.ietf.org/html/rfc7692#section-7.1.1.1
@own_context_takeover = !@accept_no_context_takeover &&
!@params["server_no_context_takeover"]
response["server_no_context_takeover"] = true unless @own_context_takeover
# https://tools.ietf.org/html/rfc7692#section-7.1.1.2
@peer_context_takeover = !@request_no_context_takeover &&
!@params["client_no_context_takeover"]
response["client_no_context_takeover"] = true unless @peer_context_takeover
response
end
end
end
end
With this out of the way, we can now go forward and create the extension that handles the creation of client and server sessions. All boilerplate code except that we now mark that this extension uses rsv2
field.
module MessagePack
module PermessageMessagePack
class Extension
def initialize(options = {})
@options = options
end
def create_client_session
ClientSession.new(@options || {})
end
def create_server_session(offers)
offers.each do |offer|
return ServerSession.new(@options || {}, offer) if ServerSession.valid_params?(offer)
end
nil
end
def name
"permessage-message-pack"
end
def type
"permessage"
end
def rsv1
false
end
def rsv2
true
end
def rsv3
false
end
end
end
end
Using the extension is as simple as adding it to the WebSocket driver. Please refer to the previous post for more details.
@driver.add_extension(::MessagePack::PermessageMessagePack::Extension.new(:no_context_takeover => true))