This proposed draft is only implemented by Iodine and hadn't seen external activity in a while.
Even though a number of other development teams (such as the teams for the Puma and Passenger server) mentioned that they plan to implements this draft, Iodine seems to be the only server currently implementing this draft and it is unlikely that this initiative will grow to become a community convention.
I still believe it's important to separate the Websocket server from the Websocket API used by application developers and frameworks, much like Rack did for HTTP. I hope that in the future a community convention for this separation of concerns can be achieved.
This is the proposed Websocket support extension for Rack servers.
Servers that publish Websocket support using the env['upgrade.websocket?'] value are assume by users to follow the requirements set in this document and thus should follow the requirements set in herein.
This document reserves the Rack env Hash keys of upgrade.websocket? and upgrade.websocket.
Websocket connection upgrade and handling is performed using a Websocket Callback Object.
The Websocket Callback Object should be a class (or an instance of such class) who's instances implement any of the following callbacks:
-
on_open()WILL be called once the upgrade had completed. -
on_message(data)(REQUIRED) WILL be called when incoming Websocket data is received.datawill be a String with an encoding of UTF-8 for text messages andbinaryencoding for non-text messages (as specified by the Websocket Protocol).The client MUST assume that the
dataString will be a recyclable buffer and that it's content will be corrupted the moment theon_messagecallback returns.Servers MAY, optionally, implement a recyclable buffer for the
on_messagecallback. However, this is optional and it is not required. -
on_ready()MAY be called when the state of the out-going socket buffer changes from full to not full (data can be sent to the socket). Ifhas_pending?returnstrue, theon_readycallback MUST be called once the buffer state changes. -
on_shutdown()MAY be called during the server's graceful shutdown process, before the connection is closed and in addition to theon_closefunction (which is called after the connection is closed. -
on_close()WILL be called after the connection was closed for whatever reason (socket errors, parsing errors, timeouts, client disconnection,closebeing called, etc'). -
on_open,on_ready,on_shutdownandon_closeshouldn't expect any arguments (arity == 0).
The following method names are reserved for the network implementation: write, close and has_pending?.
The server MUST extend the Websocket Callback Object's class using extend, so that the Websocket Callback Object inherits the following methods:
-
write(data)will attempt to send the data through the websocket connection.dataMUST be a String. Ifdatais UTF-8 encoded, the data will be sent as text. Ifdatais binary encoded it will be sent as non-text (as specified by the Websocket Protocol).writehas the same delivery promise asSocket#write(a successfulwritedoes not mean any of the data will reach the other side).writeshall returntrueon success andfalseif the websocket is closed.A server SHOULD document whether
writewill block or return immediately. It is RECOMMENDED that servers implement buffered IO, allowingwriteto return immediately when resources allow and block (or, possibly, disconnect) when the IO buffer is full. -
closecloses the connection once all the data in the outgoing queue was sent. Ifcloseis called while there is still data to be sent,closewill only take effect once the data was sent.closeshall always returnnil. -
has_pending?queries the state of the server's buffer for the specific connection (i.e., if the server has any data it is waiting to send through the socket).has_pending?, shall returntrueif the server has data waiting to be written to the socket and the server promises to call theon_readycallback once the buffer is empty and the socket is writable. Otherwise (i.e., if the server doesn't support theon_readycallback),has_pending?shall returnfalse.To clarify: implementing
has_pending?is semi-optional, meaning that a server may choose to always returnfalse, no matter the actual state of the socket's buffer.
The following keywords (both as method names and instance variable names) are reserved for the internal server implementation: _server_ws and conn_id.
-
The
_server_wsobject is private and shouldn't be accessed by the client. -
The
conn_idobject may be used as a connection ID for any functionality not specified herein.
Connection ping / pong, timeouts and network considerations should be implemented by the server. It is RECOMMENDED (but not required) that the server send pings to prevent connection timeouts and detect network failure.
Server settings MAY (not required) be provided to allow for customization and adaptation for different network environments or websocket extensions. It is RECOMMENDED that any settings be available as command line arguments and not incorporated into the application's logic.
The requirement that the server extends the class of the Websocket Callback Object (instead of the client application doing so explicitly) is designed to allow the client application to be more server agnostic.
-
Server: When an upgrade request is received, the server will set the
env['upgrade.websocket?']flag totrue, indicating that: 1. this specific request is upgradable; and 2. this server supports this specification. -
Client: When a client decides to upgrade a request, they will place a Websocket Callback Object (either a class or an instance) in the
env['upgrade.websocket']Hash key. -
Server: The server will review the
envHash before sending the response. If theenv['upgrade.websocket']was set, the server will perform the upgrade. -
Server: The server will send the correct response status and headers, as well as any headers present in the response object. The server will also perform any required housekeeping, such as closing the response body, if exists.
The response status provided by the response object shall be ignored and the correct response status shall be set by the server.
-
Server: Once the upgrade had completed, The server will add the required websocket/network functions to the callback handler or it's class (as aforementioned). If the callback handler is a Class object, the server will create a new instance of that class.
-
Server: The server will call the
on_opencallback.No other callbacks shall be called until the
on_opencallback had returned.Websocket messages shall be handled by the
on_messagecallback in the same order in which they arrive and theon_messagewill not be executed concurrently for the same connection.The
on_closecallback will not be called whileon_messageoron_opencallbacks are running.The
on_readycallback might be called concurrently with theon_messagecallback, allowing data to be sent even while other data is being processed. Multi-threading considerations may apply.
The biggest benefit from Iodine's Collection Extension is that it allows the creation of pub/sub plugins and other similar extensions that require access to all the connected Websockets - no nee for the plugin to ask "where's the list" or "add this code to on_open" or anything at all, truly "plug and play".
This extension should be easy enough to implement using a loop or an array within each process context. These methods do not cross process boundaries and they are meant to help implement more advanced features (they aren't a feature as much as an "access point").
Internally, this extension also allows iodine to manage connection memory and resource allocation while relieving developers from rewriting the same workflow of connection storing (on_open do ALL_WS << self; end ) and management. Knowing that the user doesn't keep Websocket object references, allows Iodine to safely optimize it's memory use.
-
Iodine::Websocket.each(class method) will run a block of code for each connected Websocket Callback Object belonging to the current process. This can be called from outside of a Websocket Callback Object as well.Iodine::Websocket.each {|ws| ws.write "You're connected to PID #{Process.pid}" }
-
Iodine::Websocket.defer(conn_id)Schedules a block of code to run for the specified connection at a later time, (if the connection is open) while preventing concurrent code from running for the same connection object.Iodine::Websocket.defer(self.conn_id) {|ws| ws.write "still open" }
-
#defer(instance method) Schedules a block of code to run for the specified connection at a later time, (if the connection is still open) while preventing concurrent code from running for the same connection object.defer { write "still open" }
-
Iodine::Websocket.count(instance method) Returns the number of active websocket connections (including connections that are in the process of closing down) belonging to the current process.write "#{Iodine::Websocket.count} clients sharing this process"
This extension separates the websocket Pub/Sub semantics from the Pub/Sub engine (i.e. Redis, MongoDB, etc') or the Server. If the Collection Extension is implemented, than this isn't necessarily a big step forward (the big step forward is what can be done with is).
I don't personally believe this has a chance of being adopted by other server implementors, but it's a very powerful tool that Iodine supports.
Including these semantics, in the form of the described API, allows servers and Pub/Sub engines to be optimized in different ways without distracting the application (or framework implementor with environmental details.
For example, Iodine includes two default Pub/Sub engines Iodine::PubSub::Engine::CLUSTER (the default) and Iodine::PubSub::Engine::SINGLE_PROCESS that implement a localized Pub/Sub engine within a process cluster.
The Websocket module (i.e. Iodine::Websocket) includes the following singleton methods:
-
Iodine::Websocket.default_pubsub=sets the default Pub/Sub engine. -
Iodine::Websocket.default_pubsubgets the default Pub/Sub engine.
Websocket Callback Objects inherit the following functions:
-
#subscribe(instance method) Subscribes to a channel using a specific "engine" (or the default engine). Returns a subscription object (success) ornil(error). Doesn't promise actual subscription, only that the subscription request was scheduled to be sent.Messages from the subscription are sent directly to the client unless an optional block is provided.
If an optional block is provided, it should accept the channel and message couplet (i.e.
{|channel, message| }) and call the websocketwritemanually.All subscriptions (including server side subscriptions) MUST be automatically canceled by the server when the websocket closes.
-
#subscription?(instance method) locates an existing subscription, if any. Returns a subscription object (success) ornil(error). -
#unsubscribe(instance method) cancel / stop a subscription. Safely ignoresnilsubscription objects. Returnsnil. Performance promise is similar tosubscribe- the event was scheduled.Notice: there's no need to call this function during the
on_closecallback, since the server will cancel all subscriptions automatically. -
#publish(instance method) publishes a message to engine's specified channel. Returnstrueon success orfalseon failure (i.e., engine error). Doesn't promise actual publishing, only that the message was scheduled to be published.
For example:
# client side subscription
s = subscribe(channel: "channel 1") # => subscription ID?
# client side unsubscribe
unsubscribe(s)
# client side subscription forcing binary encoding for Websocket protocol.
subscribe(channel: "channel 1", encoding: :binary) # => subscription ID?
# client side pattern subscription without saving reference
subscribe(pattern: "channel [0-9]") # => subscription ID?
# client side unsubscribe
unsubscribe( subscription?(pattern: "channel [0-9]"))
# server side anonymous block subscription
s = subscribe(channel: "channel ✨") do |channel, message|
puts message
end
# server side unsubscribe
unsubscribe(s)
# server side persistent block subscription without saving reference
block = proc {|channel, message| puts message }
subscribe({pattern: "*"}, &block)
# server side unsubscribe
unsubscribe subscription?({pattern: "*"}, &block)
# server side anonymous block subscription requires reference...
subscribe(pattern: "channel 5", force: text) do |channel, message|
puts message
end
# It's impossible to locate an anonymous block subscriptions!
subscription? (pattern: "channel 5", force: text) do |channel, message|
puts message
end
# => nil # ! can't locateServers supporting this extension aren't required to implement any Pub/Sub engines, but they MUST implement a bridge between the server and Pub/Sub Engines that follows the following semantics:
Engines MUST inherit from a server specific Engine class and implement the following three methods that will be called by the server when required (servers might implement an internal distribution layer and call these functions at their discretion):
-
subscribe(channel, is_pattern): Subscribes to the channel. Theis_patternflag sets the type of subscription. Returnstrue/false. -
unsubscribe(channel, is_pattern): Unsubscribes from the channel. Theis_patternflag sets the type of subscription. Returnstrue/false. -
publish(channel, msg, is_pattern): Publishes to the channel (or all channels matching the pattern). Returnstrue/false(some engines, such as Redis, might not support pattern publishing, they should simply returnfalse).
Engines inherit the following message from the server's Engine class and call it when messages were received:
distribute(channel, message, is_pattern = nil): Ifchannelis a channel pattern rather than a channel name, theis_patternshould be set totrueby the Engine. Engines that don't support pattern publishing, can simply ignore this flag.