Did you know ... Search Documentation:
websocket.pl -- WebSocket support
PublicShow source

WebSocket is a lightweight message oriented protocol on top of TCP/IP streams. It is typically used as an upgrade of an HTTP connection to provide bi-directional communication, but can also be used in isolation over arbitrary (Prolog) streams.

The SWI-Prolog interface is based on streams and provides ws_open/3 to create a websocket stream from any Prolog stream. Typically, both an input and output stream are wrapped and then combined into a single object using stream_pair/3.

The high-level interface provides http_upgrade_to_websocket/3 to realise a websocket inside the HTTP server infrastructure and http_open_websocket/3 as a layer over http_open/3 to realise a client connection. After establishing a connection, ws_send/2 and ws_receive/2 can be used to send and receive messages. The predicate ws_close/3 is provided to perform the closing handshake and dispose of the stream objects.

See also
- RFC 6455, http://tools.ietf.org/html/rfc6455
To be done
- Deal with protocol extensions.
Source http_open_websocket(+URL, -WebSocket, +Options) is det
Establish a client websocket connection. This predicate calls http_open/3 with additional headers to negotiate a websocket connection. In addition to the options processed by http_open/3, the following options are recognised:
subprotocols(+List)
List of subprotocols that are acceptable. The selected protocol is available as ws_property(WebSocket, subprotocol(Protocol).

Note that clients often provide an Origin header and some servers require this field. See RFC 6455 for details. By default this predicate does not set Origin. It may be set using the request_header option of http_open/3, e.g. by passing this in the Options list:

request_header('Origin' = 'https://www.swi-prolog.org')

The following example exchanges a message with the html5rocks.websocket.org echo service:

?- URL = 'ws://html5rocks.websocket.org/echo',
   http_open_websocket(URL, WS, []),
   ws_send(WS, text('Hello World!')),
   ws_receive(WS, Reply),
   ws_close(WS, 1000, "Goodbye").
URL = 'ws://html5rocks.websocket.org/echo',
WS = <stream>(0xe4a440,0xe4a610),
Reply = websocket{data:"Hello World!", opcode:text}.
Arguments:
WebSocket- is a stream pair (see stream_pair/3)
Source http_upgrade_to_websocket(:Goal, +Options, +Request)
Create a websocket connection running call(Goal, WebSocket), where WebSocket is a socket-pair. Options:
guarded(+Boolean)
If true (default), guard the execution of Goal and close the websocket on both normal and abnormal termination of Goal. If false, Goal itself is responsible for the created websocket if Goal succeeds. The websocket is closed if Goal fails or raises an exception. This can be used to create a single thread that manages multiple websockets using I/O multiplexing. See library(http/hub).
subprotocols(+List)
List of acceptable subprotocols.
timeout(+TimeOut)
Timeout to apply to the input stream. Default is infinite.

Note that the Request argument is the last for cooperation with http_handler/3. A simple echo server that can be accessed at =/ws/= can be implemented as:

:- use_module(library(http/websocket)).
:- use_module(library(http/thread_httpd)).
:- use_module(library(http/http_dispatch)).

:- http_handler(root(ws),
                http_upgrade_to_websocket(echo, []),
                [spawn([])]).

echo(WebSocket) :-
    ws_receive(WebSocket, Message),
    (   Message.opcode == close
    ->  true
    ;   ws_send(WebSocket, Message),
        echo(WebSocket)
    ).
throws
- switching_protocols(Goal, Options). The recovery from this exception causes the HTTP infrastructure to call call(Goal, WebSocket).
See also
- http_switch_protocol/2.
Source sec_websocket_accept(+Info, -AcceptKey) is det[private]
Compute the accept key as per 4.2.2., point 5.4
Source ws_send(+WebSocket, +Message) is det
Send a message over a websocket. The following terms are allowed for Message:
text(+Text)
Send a text message. Text is serialized using write/1.
binary(+Content)
As text(+Text), but all character codes produced by Content must be in the range [0..255]. Typically, Content will be an atom or string holding binary data.
prolog(+Term)
Send a Prolog term as a text message. Text is serialized using write_canonical/1.
json(+JSON)
Send the Prolog representation of a JSON term using json_write_dict/2.
string(+Text)
Same as text(+Text), provided for consistency.
close(+Code, +Text)
Send a close message. Code is 1000 for normal close. See websocket documentation for other values.
Dict
A dict that minimally contains an opcode key. Other keys used are:
format:Format
Serialization format used for Message.data. Format is one of string, prolog or json. See ws_receive/3.
data:Term
If this key is present, it is serialized according to Message.format. Otherwise it is serialized using write/1, which implies that string and atoms are just sent verbatim.

Note that ws_start_message/3 does not unlock the stream. This is done by ws_send/1. This implies that multiple threads can use ws_send/2 and the messages are properly serialized.

To be done
- Provide serialization details using options.
Source ws_receive(+WebSocket, -Message:dict) is det
Source ws_receive(+WebSocket, -Message:dict, +Options) is det
Receive the next message from WebSocket. Message is a dict containing the following keys:
opcode:OpCode
OpCode of the message. This is an atom for known opcodes and an integer for unknown ones. If the peer closed the stream, OpCode is bound to close and data to the atom end_of_file.
data:String
The data, represented as a string. This field is always present. String is the empty string if there is no data in the message.
rsv:RSV
Present if the WebSocket RSV header is not 0. RSV is an integer in the range [1..7].

If ping message is received and WebSocket is a stream pair, ws_receive/1 replies with a pong and waits for the next message.

The predicate ws_receive/3 processes the following options:

format(+Format)
Defines how text messages are parsed. Format is one of
string
Data is returned as a Prolog string (default)
json
Data is parsed using json_read_dict/3, which also receives Options.
prolog
Data is parsed using read_term/3, which also receives Options.
To be done
- Add a hook to allow for more data formats?
Source read_text_data(+Format, +WsStream, -Dict, +Options) is det[private]
Read a websocket message into a dict websocket{opcode:OpCode, data:Data}, where Data is parsed according to Format.
Source ws_close(+WebSocket:stream_pair, +Code, +Data) is det
Close a WebSocket connection by sending a close message if this was not already sent and wait for the close reply.
Arguments:
Code- is the numerical code indicating the close status. This is 16-bit integer. The codes are defined in section 7.4.1. Defined Status Codes of RFC6455. Notably, 1000 indicates a normal closure.
Data- is currently interpreted as text.
Errors
- websocket_error(unexpected_message, Reply) if the other side did not send a close message in reply.
Source ws_open(+Stream, -WSStream, +Options) is det
Turn a raw TCP/IP (or any other binary stream) into a websocket stream. Stream can be an input stream, output stream or a stream pair. Options includes
mode +Mode
One of server or client. If client, messages are sent as masked.
buffer_size(+Count)
Send partial messages for each Count bytes or when flushing the output. The default is to buffer the entire message before it is sent.
close_parent(+Boolean)
If true (default), closing WSStream also closes Stream.
subprotocol(+Protocol)
Set the subprotocol property of WsStream. This value can be retrieved using ws_property/2. Protocol is an atom. See also the subprotocols option of http_open_websocket/3 and http_upgrade_to_websocket/3.

A typical sequence to turn a pair of streams into a WebSocket is here:

    ...,
    Options = [mode(server), subprotocol(chat)],
    ws_open(Input, WsInput, Options),
    ws_open(Output, WsOutput, Options),
    stream_pair(WebSocket, WsInput, WsOutput).
 ws_start_message(+WSStream, +OpCode) is det[private]
Source ws_start_message(+WSStream, +OpCode, +RSV) is det[private]
Prepare for sending a new message. OpCode is one of text, binary, close, ping or pong. RSV is reserved for extensions. After this call, the application usually writes data to WSStream and uses ws_send/1 to complete the message. Depending on OpCode, the stream is switched to binary (for OpCode is binary) or text using utf8 encoding (all other OpCode values). For example, to a JSON message can be send using:
ws_send_json(WSStream, JSON) :-
   ws_start_message(WSStream, text),
   json_write(WSStream, JSON),
   ws_send(WSStream).
Source ws_send(+WSStream) is det[private]
Complete and send the WebSocket message. If the OpCode of the message is close, close the stream.
Source ws_read_header(+WSStream, -OpCode, -RSV) is det[private]
Read the header of the WebSocket next message. After this call, WSStream is switched to the appropriate encoding and reading from the stream will signal end-of-file at the end of the message. Note that this end-of-file does not invalidate WSStream. Reading may perform various tasks on the background:
  • If the message has Fin is false, it will wait for an additional message.
  • If a ping is received, it will reply with a pong on the matching output stream.
  • If a pong is received, it will be ignored.
  • If a close is received and a partial message is read, it generates an exception (TBD: which?). If no partial message is received, it unified OpCode with close and replies with a close message.

If not all data has been read for the previous message, it will first read the remainder of the message. This input is silently discarded. This allows for trailing white space after proper text messages such as JSON, Prolog or XML terms. For example, to read a JSON message, use:

ws_read_json(WSStream, JSON) :-
    ws_read_header(WSStream, OpCode, RSV),
    (   OpCode == text,
        RSV == 0
    ->  json_read(WSStream, JSON)
    ;   OpCode == close
    ->  JSON = end_of_file
    ).
Source ws_property(+WebSocket, ?Property) is nondet
True if Property is a property WebSocket. Defined properties are:
subprotocol(Protocol)
Protocol is the negotiated subprotocol. This is typically set as a property of the websocket by ws_open/3.
Source to_opcode(+Spec, -OpCode:int) is det[private]
Convert a specification of an opcode into the numeric opcode.
Source ws_opcode(?Name, ?Code)[private]
Define symbolic names for the WebSocket opcodes.
Source ws_mask(-Mask)
Produce a good random number of the mask of a client message.

Re-exported predicates

The following predicates are exported from this file while their implementation is defined in imported modules or non-module files loaded by this module.

Source ws_receive(+WebSocket, -Message:dict) is det
Source ws_receive(+WebSocket, -Message:dict, +Options) is det
Receive the next message from WebSocket. Message is a dict containing the following keys:
opcode:OpCode
OpCode of the message. This is an atom for known opcodes and an integer for unknown ones. If the peer closed the stream, OpCode is bound to close and data to the atom end_of_file.
data:String
The data, represented as a string. This field is always present. String is the empty string if there is no data in the message.
rsv:RSV
Present if the WebSocket RSV header is not 0. RSV is an integer in the range [1..7].

If ping message is received and WebSocket is a stream pair, ws_receive/1 replies with a pong and waits for the next message.

The predicate ws_receive/3 processes the following options:

format(+Format)
Defines how text messages are parsed. Format is one of
string
Data is returned as a Prolog string (default)
json
Data is parsed using json_read_dict/3, which also receives Options.
prolog
Data is parsed using read_term/3, which also receives Options.
To be done
- Add a hook to allow for more data formats?