Extensions
This guide explains how to use protocol-websocket
for implementing a websocket client and server using extensions.
Per-message Deflate
WebSockets have a mechanism for implementing extensions. At the time of writing, the only published extension is permessage-deflate
for per-message compression. It operates on complete messages rather than individual frames.
Clients and servers can negotiate a set of extensions to use. The server can accept or reject these extensions. The client can then instantiate the extensions and apply them to the connection. More specifically, clients need to define a set of extensions they want to support:
require 'protocol/websocket'
require 'protocol/websocket/extensions'
client_extensions = Protocol::WebSocket::Extensions::Client.new([
[Protocol::WebSocket::Extension::Compression, {}]
])
offer_headers = []
client_extensions.offer do |header|
offer_headers << header.join(';')
end
offer_headers # => ["permessage-deflate;client_max_window_bits"]
This is transmitted to the server via the Sec-WebSocket-Extensions
header. The server processes this and returns a subset of accepted extensions. The client receives a list of accepted extensions and instantiates them:
server_extensions = Protocol::WebSocket::Extensions::Server.new([
[Protocol::WebSocket::Extension::Compression, {}]
])
accepted_headers = []
server_extensions.accept(offer_headers) do |header|
accepted_headers << header.join(';')
end
accepted_headers # => ["permessage-deflate;client_max_window_bits=15"]
client_extensions.accept(accepted_headers)
We can check the extensions are accepted:
server_extensions.accepted
# => [[Protocol::WebSocket::Extension::Compression, {:client_max_window_bits=>15}]]
client_extensions.accepted
# => [[Protocol::WebSocket::Extension::Compression, {:client_max_window_bits=>15}]]
Once the extensions are negotiated, they can be applied to the connection:
require 'protocol/websocket/connection'
require 'socket'
sockets = Socket.pair(Socket::PF_UNIX, Socket::SOCK_STREAM)
client = Protocol::WebSocket::Connection.new(Protocol::WebSocket::Framer.new(sockets.first))
server = Protocol::WebSocket::Connection.new(Protocol::WebSocket::Framer.new(sockets.last))
client_extensions.apply(client)
server_extensions.apply(server)
# We can see that the appropriate wrappers have been added to the connections:
client.reader.class # => Protocol::WebSocket::Extension::Compression::Inflate
client.writer.class # => Protocol::WebSocket::Extension::Compression::Deflate
server.reader.class # => Protocol::WebSocket::Extension::Compression::Inflate
server.writer.class # => Protocol::WebSocket::Extension::Compression::Deflate
client.send_text("Hello World")
# => #<Protocol::WebSocket::TextFrame:0x000000011d555460 @finished=true, @flags=4, @length=13, @mask=nil, @opcode=1, @payload="\xF2H\xCD\xC9\xC9W\b\xCF/\xCAI\x01\x00">
server.read
# => #<Protocol::WebSocket::TextMessage:0x000000011e1e5248 @buffer="Hello World">
It's possible to disable compression on a per-message basis:
client.send_text("Hello World", compress: false)
# => #<Protocol::WebSocket::TextFrame:0x00000001028945b0 @finished=true, @flags=0, @length=11, @mask=nil, @opcode=1, @payload="Hello World">
server.read
# => #<Protocol::WebSocket::TextMessage:0x000000011e77eb50 @buffer="Hello World">