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.
- 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:
-
- 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.
- sec_websocket_accept(+Info, -AcceptKey) is det[private]
- Compute the accept key as per 4.2.2., point 5.4
- 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.
- ws_receive(+WebSocket, -Message:dict) is det
- 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?
- 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.
- 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.
- 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]
- 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).
- ws_send(+WSStream) is det[private]
- Complete and send the WebSocket message. If the OpCode of the
message is
close
, close the stream.
- 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
).
- 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.
- to_opcode(+Spec, -OpCode:int) is det[private]
- Convert a specification of an opcode into the numeric opcode.
- ws_opcode(?Name, ?Code)[private]
- Define symbolic names for the WebSocket opcodes.
- 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.
- ws_receive(+WebSocket, -Message:dict) is det
- 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?