RFC 6455 Web  Sockets for Racket
1 Introduction
2 Changes
3 Synopsis
4 License

RFC 6455 WebSockets for Racket

Tony Garnock-Jones <tonygarnockjones@gmail.com>

1 Introduction

This package, rfc6455, provides RFC 6455 compatible WebSockets server and client interfaces for Racket, building on Racket’s web-server collection.

Besides support for RFC 6455, the final WebSockets standard, the package also incorporates code supporting the earlier, draft hybi-00 proposal, because several common older browsers still use this protocol variant.

Wikipedia has a section on browser support for WebSocket protocol variations. In 2013, at the time this manual was written, it stated that "all the latest browsers except Android browser support the latest specification (RFC 6455) of the WebSocket protocol." For up-to-date, detailed information on browser support for WebSockets, see caniuse.com.

This package has been developed against

2 Changes

Version 2.1.0 of this library introduces ws-conn-close-status and ws-conn-close-reason for retrieving information from close frames sent by remote peers.

Version 2.0.0 of this library introduces a new interface to streaming message reception, ws-recv-stream, and makes a breaking change to the way ws-conn? values work as synchronizable events. The synchronization result of such an event is now a received message, where previously it was an uninteresting value. See the (new) procedure ws-recv-evt.

In addition, prior to version 2.0.0, the default #:payload-type for ws-recv was actually 'text, even though the documentation claimed that it was 'auto. From version 2.0.0, the default is 'auto in both the documentation and the implementation.

Version 1.0.1 of this library is the last version with the old #:stream? interface to streaming receives. It also has an interface bug: when (1) using a ws-conn? as an synchronizable event, (2) with an event-handler that calls ws-recv, (3) connected to a client that occasionally sends websocket "ping" frames, the call to ws-recv may get stuck waiting for the next non-"ping" frame, leading to a kind of temporary stall on the thread invoking sync. This bug was the chief motivation for the changes of version 2.0.0.

3 Synopsis

Using the legacy net/websocket-compatible interface:

(require net/rfc6455)
(ws-serve #:port 8081 (lambda (c s) (ws-send! c "Hello world!")))

Using an interface that supports URL path matching and WebSocket subprotocol selection:

(require net/rfc6455)
(ws-serve* #:port 8081
            ["/test" ; the URL path (regular expression)
             [(subprotocol) ; if client requests subprotocol "subprotocol"
              (lambda (c) (ws-send! c "You requested a subprotocol"))]
             [(#f) ; if client did not request any subprotocol
              (lambda (c) (ws-send! c "You didn't explicitly request a subprotocol"))]]))

Creating a client connection:

(require net/rfc6455)
(require net/url)
(define c (ws-connect (string->url "ws://localhost:8081/")))
(ws-send! c "Hello world!")

4 License

All the code in this package is licensed under the LGPL, version 3.0 or any later version. Each source file has a brief copyright and licensing notice attached, but see the licence text itself for full details.

The only exceptions to the above are the files marked "public domain" in the net/rfc6455/examples directory. They are intended to be examples of usage of the package for people to build on without concern for licensing minutiae.


 (require net/rfc6455) package: rfc6455

The interface is based on the net/websocket API from older versions of Racket, with some extensions and differences.


(ws-url? x)  boolean?

  x : any/c
Returns true if and only if x is a url? and has a url-scheme equal to "ws" or "wss".


(wss-url? x)  boolean?

  x : any/c
Returns true if and only if x is a url? and has a url-scheme equal to "wss".


(ws-conn? x)  boolean?

  x : any/c
Returns #t if and only if x is a WebSocket connection as defined by this package.

#t iff the connected peer supports sending and receiving fragmented/streamed messages. Currently, RFC 6455-compliant peers support fragmentation, while hybi-00 peers do not.


(ws-conn-supports-payload-type? c    
  payload-type)  boolean?
  c : ws-conn?
  payload-type : symbol?
#t iff the connected peer supports the requested payload type. RFC 6455-compliant peers support types 'text and 'binary, while hybi-00 peers support only 'text.

#t iff the status and/or reason values are communicated to the remote peer on connection close (ws-close!). RFC 6455 includes space in the wire protocol for these values, while hybi-00 does not.


(ws-conn-closed? c)  boolean?

  c : ws-conn?
Returns #t if the given connection has been closed, and #f otherwise.


(ws-conn-close-status c)  (or/c #f number?)

  c : ws-conn?


(ws-conn-close-reason c)  (or/c #f string?)

  c : ws-conn?
When ws-conn-closed? returns #t, these procedures will respectively retrieve the "status code" and "reason" text from the close frame sent by the remote peer that caused the connection shutdown. If no such information is available, they will return #f. Only RFC 6455 peers send information in their close frames; hybi-00 connections will always yield #f from these procedures.


(ws-connect u    
  [#:headers headers    
  #:protocol protocol])  ws-conn?
  u : (or/c ws-url? wss-url?)
  headers : (listof header?) = '()
  protocol : (or/c 'rfc6455 'hybi00) = 'rfc6455
Connect to the given WebSockets server, via TLS if (wss-url? u). Supplies headers as additional headers in the WebSocket handshake request. A protocol variant other than the RFC 6455 standard can be chosen by supplying a value for the #:protocol parameter.


(ws-serve conn-dispatch    
  #:conn-headers conn-headers ...)  (-> void)
  conn-dispatch : (-> ws-conn? request? void)
  conn-headers : 
(or/c (-> bytes? (listof header?) request?
          (values (listof header?) any/c))
      (-> bytes? (listof header?)
          (values (listof header?) any/c)))
This is a convenience entry point, largely directly compatible with ws-serve from the built-in websocket support in older versions of Racket, including all its arguments besides those shown. If #:conn-headers is supplied, then it is inspected to see whether it takes two or three arguments, and is called appropriately.


(ws-serve* service-mapper ...)  (-> void)

  service-mapper : 
(-> url? (-> (or/c symbol? #f) (or/c #f
                                     (-> ws-conn? void))))

Like ws-serve, except uses the given service-mapper to decide how to handle an incoming request. See ws-service-mapper.


(ws-service-mapper [uri-regexp [(protocol ...) function-expr] ...] ...)

protocol = symbol
  | #f
Macro that expands to an expression suitable for use as a service-mapper with ws-serve*.

Each uri-regexp is matched against an incoming request’s URL in turn until one matches. Then,

The function-exprs must evaluate to connection handler procedures, each taking a ws-conn? as their only argument.


(ws-send! c    
  [#:final-fragment? final-fragment?    
  #:payload-type payload-type    
  #:flush? flush?])  void?
  c : ws-conn?
  payload : (or/c string? bytes? input-port?)
  final-fragment? : boolean? = #t
  payload-type : (or/c 'continuation 'text 'binary) = 'text
  flush? : boolean? = #t
Sends a message to the remote peer.

(Note: Only RFC 6455 peers support fragmentation and non-text payloads. Attempts to use these features with hybi-00 peers will signal an error. See ws-conn-supports-fragmentation? and ws-conn-supports-payload-type?.)

If payload is a string, it is converted to bytes using string->bytes/utf-8 before transmission. If it is an input-port, it is read from and streamed using multiple WebSockets message fragments to the peer until it yields eof (see also rfc6455-stream-buffer-size).

If flush? is false, the buffers of the underlying connection socket output-ports are not flushed after sending the message. Otherwise (i.e. by default), they are flushed.

If payload-type is 'text or 'binary, the appropriate WebSockets content type bit is set upon transmission.

Fragmented messages can be sent using this procedure.

For single-fragment (unfragmented) messages, the defaults are fine: a plain (ws-send! c payload) is enough. Here is an example of a multi-fragment message:

(ws-send! c #"first" #:final-fragment? #f)
(ws-send! c #"second" #:final-fragment? #f #:payload-type 'continuation)
(ws-send! c #"third" #:final-fragment? #t #:payload-type 'continuation)


(ws-recv c [#:payload-type payload-type])

  (or/c eof-object? string? bytes?)
  c : ws-conn?
  payload-type : (or/c 'auto 'text 'binary) = 'auto
Receives a message from the remote peer.

(Note: Only RFC 6455 peers support binary payloads. Attempts to use 'binary payload-type with hybi-00 peers will signal an error. See ws-conn-supports-payload-type?.)

Returns either a string or a bytes, depending on payload-type. If a specific 'text or 'binary payload type is requested, the corresponding result type is returned, or if 'auto is requested, the message’s own text/binary indicator bit is used to decide which to return. If eof occurs mid-message, fragments so far received are discarded and eof is returned.

Multi-fragment messages are transparently reassembled into a single string or bytes.


(ws-recv-evt c [#:payload-type payload-type])  evt?

  c : ws-conn?
  payload-type : (or/c 'auto 'text 'binary) = 'auto
Produces a synchronizable event that effectively gives an asynchronous version of ws-recv. The synchronization result of the event is the received message (either a string or a bytes, just the same as the way ws-recv works) or eof.

The same caveats regarding payload-type apply here as for ws-recv.

A ws-conn? value may be used as if it were an event produced by ws-recv-evt: using c alone as an event is equivalent to using (ws-recv-evt c).


(ws-recv-stream c)  input-port?

  c : ws-conn?
Receives a message from the remote peer, making the contents of the message available through an input port.

(Note: Only RFC 6455 peers support streaming. Attempts to use this procedure with hybi-00 peers will signal an error. See ws-conn-supports-fragmentation?.)

Returns an input port from which the bytes or characters making up the message can be read. An end-of-file from the resulting input port is ambiguous: it does not separate the end of the message being read from the end of the connection itself. Use ws-conn-closed? to disambiguate, though beware of time-of-check-to-time-of-use issues.

Multi-fragment messages are transparently reassembled. Fragment boundaries are not preserved when reading from the returned input port.


(ws-close! c    
  [#:status status    
  #:reason reason])  void?
  c : ws-conn?
  status : integer? = 1000
  reason : string? = ""
Closes a connection. Has no effect if the connection is already closed. The status code and reason are supplied to the remote peer in the close frame.

(Note: hybi-00 peers do not have room in their wire protocol for the status and reason codes. See ws-conn-signals-status-on-close?.)

Used when streaming the contents of an input-port given to ws-send!. Streamed message fragments will be no larger than (rfc6455-stream-buffer-size) bytes each.


(hybi00-framing-mode)  (or/c 'new 'old)

(hybi00-framing-mode mode)  void?
  mode : (or/c 'new 'old)
Used with pre-hybi-00 peers. Selects either the "new" or "old" framing modes. You only have to worry about this if you’re trying to communicate with truly ancient, pre-hybi-00 peers, and then you no doubt have bigger problems.


(ws-idle-timeout)  number?

(ws-idle-timeout seconds)  void?
  seconds : number?
Idle timeout in seconds. If the interval between successive received frames (of any type) exceeds this number of seconds, the connection will be closed.

This parameter defaults to 300 seconds, i.e. five minutes.