Skip to content

Instantly share code, notes, and snippets.

@Morgul
Last active August 29, 2015 14:00
Show Gist options
  • Save Morgul/11074637 to your computer and use it in GitHub Desktop.
Save Morgul/11074637 to your computer and use it in GitHub Desktop.
Proposed websocket-based client/server protocol
// Example client interface
var socket = SocketFramework.connect("http://localhost:3000");
// Listen for the some event
socket.on('some event', function(data) {
console.log('got data:', data);
});
// Listen for an event that expects a reply
socket.on('reply event', function(data, callback) {
callback({ foo: "bar" });
});
// Send an event
socket.emit('foo');
// Send an event with data
socket.emit('bar', { foo: "bar" });
%% -----------------------------------------------------------------------------
%% @doc An example message handler for the server side, in erlang.
-module(example_handler).
-import("unisocket.hrl").
-behavior(unisocket_handler).
-record(state, {
foo :: any()
}).
-export([handle_message/3]).
%% -----------------------------------------------------------------------------
%% unisocket_handler
%% -----------------------------------------------------------------------------
%% @doc Handle a simple message.
handle_message(test, Message, State) ->
lager:info("Got the 'test' message!"),
{noreply, State};
%% @doc This is a message that expects a reply. We reply by returning the tuple
%% `{reply, Data, State}'.
handle_message(expects_reply, Message, State) ->
% Build our reply
Reply = [{foo, bar}],
{reply, Reply, State};
%% @doc We can handle event names with spaces, or capital letters; This is
%% important, since javascript developers could be evil, and disrespect our
%% atoms.
handle_message('Event Name With Spaces', Message, State) ->
lager:info("Got the message with spaces!"),
{noreply, State};
%% @doc This is a message that expects a reply. We reply by returning the tuple
%% `{reply, Data, State}'.
handle_message(expects_reply, Message, State) ->
% Build our reply
Reply = [{foo, bar}],
{reply, Reply, State};
%% @doc The catch-all handler.
-spec handle_message(Name :: atom(), Message :: term(), State :: any()) ->
{noreply, State :: any()} | {reply, Reply :: term(), State :: any()}.
handle_message(Name, Message, State) ->
lager:warning("Unhandled message: ~p, ~p", [Name, Message]),
{noreply, State}.

Websocket Framework proposal

Websockets, while they're cool as hell, have the problem of being low enough level that we need to wrap them up in order to avoid rewriting code all over the place. Libraries, such as Socket.io are useful, but implementing socket.io in other languages is not very straight forward, and seem to be fraught with difficulty, as evidenced by the lack of feature-comparable non-node.js server side libraries that exist.

So, I want to write my own. This doesn't have any fall-back support; instead it is tailored to websockets. Here's a list of targeted features:

  • Event-based
  • Replies
  • Channels (ex: '/chat', '/foobar', etc)

Message Protocol

Since this is WebSocket based, we simply need a nice JSON structure for handling all of this.

{
    "name": "..." // The name of the event.
    "channel": "..." // Either '/', '', or undefined for messages on the root channel, or '/channel-name' for channeled messages.
    "replyWith": "..." // (optional) Indicates that this message expects a reply.
    "replyTo": "..." // (optional) Indicated that this is a reply message.
    "data": [ ... ] // (optional) Any additional data being sent with the message.
}

This format is used for both client to server and server to client messages.

name

The name of the event. It is determined by the user on either the client or server. It is a string, and can contain any valid unicode characters.

channel

The name of the channel the message is being sent over. It is either '/', '' (empty string), or undefined if the message is intended for the root channel, other wise it is any valid slug. (See here.)

replyWith

An identifier generated by the client, only when a reply is desired. (Hence, this is optional.) The identifier is sent back to the client by the server when it is responding. (During a response, replyTo is populated with this value, and name and channel are also preserved.) It is up to the client to ensure that it can match the identifier of the reply to the originating message. Generally, this means the identifier must be unique across the set of all messages waiting on a reply. It is a string, and can contain any valid unicode characters.

replyTo

An identifier originally sent by the client, to the server. This indicates the message is a reply. It is a string, and can contain any valid unicode characters.

data

A list of additional JSON-able objects to be sent with the request. This is always a list, or undefined. The objects contained within must be able to be serialized as json.

Connection

When a client connects, first the WebSocket connection is finished. Then, the client sends a 'connect' event on the '$control' channel. Other than name and channel, this message is also expected to contain a replyWith field. The server then replies with a message containing a data list with a single configuration object. The client is expected to use this as the configuration for the session.

Channels

Messages sent on a channel are automatically filtered by both the client and the server. This is intended to allow users to handle only channels they are interested in.

If a message is sent without a channel specified (or on the '/' channel), it is assumed to be the root channel.

The '$control' channel is a special channel reserved for special messages, such as connection.

Replies

Events that expect a reply must send the replyWith field. That field contains an identifier generated by the sender. The receiver is expect to send a reply with the same name, channel, and the replyTo field populated with the identifier from the replWith field of the originating message. This reply is always expected, even if the reply is empty. The timeout on this is 30 seconds (this is configurable on the server side). If there is a timeout, the sender is expected to generate an error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment