== ws-client
[[toc:]]
=== Description
An implementation of the client side of the WebSocket Protocol
([[https://www.rfc-editor.org/rfc/rfc6455.html|RFC6455]]), including
the {{permessage-deflate}} extension
([[https://www.rfc-editor.org/rfc/rfc7692.html|RFC7692]]). It passes
all of the [[https://github.com/crossbario/autobahn-testsuite|Autobahn
Testsuite]] compliance tests.
=== Requirements
* [[srfi-1]]
* [[foreigners]]
* [[openssl]]
* [[uri-common]]
* [[intarweb]]
* [[base64]]
* [[simple-sha1]]
External dependencies:
* [[https://www.zlib.net/|zlib]]
Uses [[tcp6]] if it is imported together with this extension.
=== API
====== Establishing a WebSocket connection
ws-connection
(ws-connect STRING #!optional (list WS-EXTENSION ...)) -> WS-CONNECTION
The state of a WebSocket connection is stored in a record of type
{{ws-connection}}.
The procedure {{ws-connect}} establishes a WebSocket connection to a
server. The argument {{STRING}} should be a valid
[[https://www.rfc-editor.org/rfc/rfc6455.html#section-3|WebSocket
URI]].
To use WebSockets over TLS, supply a URI with scheme {{wss}}.
The procedure {{ws-connect}} optionally accepts a list of extensions
the client hopes to use. The only extension currently supported by
this library is {{permessage-deflate}}; see the
[[#the-permessage-deflateextension|relevant section]].
====== Sending and receiving messages
ws-message
(message-type WS-MESSAGE) -> SYMBOL
(message-data* WS-MESSAGE) -> U8VECTOR
(message-frames WS-MESSAGE) -> U8VECTOR
WebSocket messages are represented with the record type {{ws-message}}.
For a message {{m}}, The procedure {{(message-type m)}} yields the
type of {{m}} as specified by the opcode of its first frame, which is
either {{'text}} or {{'binary}}.
If {{m}} is a message obtained with {{recv-message}},
{{(message-frames m)}} is a list of frames from which {{m}} has been
assembled, after any per-frame transforms defined by extensions have
been applied. In the absence of extensions which apply per-message
transforms, {{(message-data* m)}} is the concatenation of the contents
of each {{(frame-payload-data f)}} for {{f}} in {{(message-frames
m)}}.
(message-size WS-MESSAGE) -> INTEGER
Equivalent to {{(u8vector-length (message-data* WS-MESSAGE))}}.
(message-data WS-MESSAGE) -> STRING or BLOB
The procedure {{(message-data m)}} yields {{(message-data* m)}}
converted to the corresponding type depending on {{(message-type m)}}.
(recv-message WS-CONNECTION) -> WS-MESSAGE or FALSE
Receives a message from the server. The procedure
quietly processes any control frames encountered while assembling the
message.
If it encounters a {{connection-close}} frame, it closes the
connection and returns {{#f}} instead. Once this has happened, the
application should not invoke {{recv-message}} again on the same
connection.
(send-message WS-CONNECTION WS-MESSAGE)
Sends a message to the server. Currently only useful
for echoing received messages; see instead {{send-text-message}} and
{{send-binary-message}} below.
(send-text-message WS-CONNECTION STRING)
(send-binary-message WS-CONNECTION BLOB)
Sends a text (resp. binary) message to (resp. from) the server.
(recv-message-loop WS-CONNECTION HANDLER)
Receives messages from the server and calls the procedure {{HANDLER}}
on each received message, until the connection is closed. The
procedure responds to (or sends, e.g. in the case of closure due to a
protocol error) connection close frames so that the connection is
closed cleanly.
====== Closing a WebSocket connection
(ws-close WS-CONNECTION SYMBOL)
Sends a frame with optype {{'connection-close}} to the server, with
payload the close code corresponding to the reason given by {{SYMBOL}}
(see {{reason->close-code}}).
Once the application has called {{ws-close}} with a connection, it
should not send any further data through the connection, though it may
still receive messages until the server sends a connection close frame
in return.
==== Low-level interface
An interface is provided for when an application wishes to interact
with the connection on the level of individual WebSocket frames.
Care should be taken if these procedures are used in conjunction with
the message-level interface: for example, if the first frame of a
fragmented message has been consumed using {{recv-frame}}, invoking
{{recv-message}} results in an error on the next frame since there is
nothing to continue.
ws-frame
(frame-fin WS-FRAME) -> BOOLEAN
(frame-rsv WS-FRAME) -> INTEGER
(frame-opcode WS-FRAME) -> INTEGER
(frame-optype WS-FRAME) -> SYMBOL
(frame-mask? WS-FRAME) -> BOOLEAN
(frame-payload-length WS-FRAME) -> INTEGER
(frame-payload-data WS-FRAME) -> U8VECTOR
WebSocket messages are represented with the record type {{ws-frame}}.
For a frame {{f}}:
* {{(frame-fin f)}} is {{#t}} iff the {{FIN}} bit is set.
* {{(frame-rsv f)}} are the three {{RSV}} bits interpreted as an
integer. For example, if exactly the {{RSV2}} and {{RSV3}} bits are
set, {{(frame-rsv f)}} is {{3}}.
* {{(frame-opcode f)}} (resp. {{(frame-optype f)}} is the opcode
(resp. optype) of {{f}}.
* {{(frame-mask? f)}} is {{#t}} iff the {{MASK}} bit is set. This
should be {{#t}} for every outbound frame and {{#f}} for every frame
received. A {{ws-frame}} record retains no information about the
masking key; masking/unmasking is handled quietly by the procedures
{{send-frame}} and {{recv-frame}}.
* The first {{(frame-payload-length f)}} bytes of the vector
{{(frame-payload-data f)}} is the payload of {{f}}. Note that
{{(frame-payload-length f)}} is ''not'' guaranteed to be the same as
{{(u8vector-length (frame-payload-data f))}}.
(frame-rsv-bit WS-FRAME N) -> BOOLEAN
Is {{#t}} iff the {{bitwise-and}} of {{N}} with the result of
{{frame-rsv}} is nonzero. For example, {{(frame-rsv-bit f 4)}} is
{{#t}} iff the {{RSV1}} bit is set on the frame {{f}}.
(recv-frame WS-CONNECTION) -> WS-FRAME
(send-frame WS-CONNECTION WS-FRAME)
Receives (resp. sends) a frame from (resp. to) the server.
(opcode->optype INTEGER) -> SYMBOL
(optype->opcode SYMBOL) -> INTEGER
Maps between opcodes and optypes:
opcode | optype |
{{#x0}} | {{'continuation}} |
{{#x1}} | {{'text}} |
{{#x2}} | {{'binary}} |
{{#x8}} | {{'connection-close}} |
{{#x9}} | {{'ping}} |
{{#xa}} | {{'pong}} |
Signals a composite condition of kind {{'websocket 'exn}} if an
unrecognised optype or opcode is supplied. The application is expected
to redefine this procedure if it wishes to make use of reserved
opcodes.
(reason->close-code SYMBOL) -> U8VECTOR
(close-code->reason INTEGER) -> SYMBOL
Maps between reasons for closing a connection and the corresponding close codes:
code (integer) | code (u8vector) | reason |
{{1000}} | {{#u8(3 232)}} | {{'normal-closure}} |
{{1001}} | {{#u8(3 233)}} | {{'going-away}} |
{{1002}} | {{#u8(3 234)}} | {{'protocol-error}} |
{{1003}} | {{#u8(3 235)}} | {{'unsupported-data}} |
{{1005}} | {{#u8(3 237)}} | {{'no-status-rcvd}} |
{{1006}} | {{#u8(3 238)}} | {{'abnormal-closure}} |
{{1007}} | {{#u8(3 239)}} | {{'invalid-frame-payload-data}} |
{{1008}} | {{#u8(3 240)}} | {{'policy-violation}} |
{{1009}} | {{#u8(3 241)}} | {{'message-too-big}} |
{{1010}} | {{#u8(3 242)}} | {{'mandatory-ext}} |
{{1011}} | {{#u8(3 243)}} | {{'internal-server-error}} |
{{1015}} | {{#u8(3 247)}} | {{'tls-handshake}} |
The procedure {{reason->close-code}} returns the close code as a
{{u8vector}} rather than an integer; this is so that the value can be
included into a frame payload without conversion.
Signals a composite condition of kind {{'websocket 'exn}} if an
unrecognised close code or reason is supplied. The application is
expected to redefine this procedure if it wishes to make use of
reserved close codes. Note that not all of the close codes listed here
are expected to occur in a connection close frame.
==== The {{permessage-deflate}} extension
This library provides an implementation of the {{permessage-deflate}}
extension, using {{zlib}} for compression/decompression.
(permessage-deflate PARAMETERS) -> WS-EXTENSION
To offer to use this extension during the opening handshake, supply
{{(list (permessage-deflate PARAMETERS))}} as an optional argument to
{{ws-connect}}.
The argument {{PARAMETERS}} should be a list in which each item is an
{{alist}}, each of whose items is in turn a pairs of strings
{{(PARAMETER . VALUE)}} or a pair of form {{(PARAMETER . #t)}}. These
specify the parameters to be advertised during the opening handshake.
For example, to ask the server to use an LZ77 sliding window of length
no greater than 1024, but to fall back to {{permessage-deflate}}
without this parameter if the server does not support it, one might
specify
'((("server_max_window_bits" . "10")) ())
so that the {{Sec-WebSocket-Extensions}} header in the client opening
handshake will contain the string
permessage-deflate;server_max_window_bits=10,permessage-deflate
==== Handling exceptions
When a procedure encounters a situation which should result in the
WebSocket connection being failed, it signals a composite condition of
kind {{'websocket 'fail}}, with the following properties:
; reason : a symbol representing the reason for failing the connection, and
; message : a string describing the exception.
The application may choose to catch and handle these exceptions, such
as by calling {{ws-close}} with the corresponding arguments (see
{{reason->close-code}}). The procedure {{recv-message-loop}} does
this automatically.
For cases where it does not make sense to attempt to close the
connection (for example if the underlying TCP connection fails, or if
an error results from user input instead of a problem with data
received from the server) a condition of kind {{'websocket 'exn}} is
signalled instead, with the following properties:
; message : a string describing the exception.
=== Examples
A client which connects to {{localhost}} port {{9001}} without TLS,
and echoes back every text message it receives from the server:
(import ws-client)
(let ((conn (ws-connect "ws://localhost:9001")))
(recv-message-loop conn
(lambda (m)
(if (eq? 'text (message-type m))
(send-text-message conn (message-data m))))))
See the {{examples}} folder in the source repository for more
examples.
=== Author
[[/users/loh-ka-tsun|Lo̍h Ka-tsùn]]
=== Repository
[[https://github.com/lohkatsun/ws-client-egg]]
=== License
BSD
=== Version History
; 0.2:
* 0.2.1 Added unit test, replaced some foreign dependencies
* 0.2.0 Initial release