N2O/WebSocket for Standard ML

Namdak Tonpa
4 min readDec 10, 2018

--

The N2O ECO Standard ML implementation.

This page contains the description of WebSocket and static HTTP server implementation and protocol stack for application development of top of it that conforms to N2O ECO specification.

As you may know there was no WebSocket implementation for Standard ML until now. Here is the first WebSocket server with switch to static HTML if needed for serving host pages of WebSocket client. The demo echo application is presented as N2O protocol for Standard ML languages at final.

$ wscat -c ws://127.0.0.1:8989/ws/
connected (press CTRL+C to quit)
> helo
< helo

We also updated N2O ECO site with additional o1 (Standard ML) implementation to existent o3 (Haskell) and o7 (Erlang) implementations.

N2O has two default transports: WebSocket and MQTT and Standard ML version provides WebSocket/HTTP server along with its own vision of typing N2O tract as it differs from Haskell version.

TCP Server

In Standard ML one book you will need for implementing the WebSocket server is Unix System Programming with Standard ML.

In Standard ML you have two major distribution that support standard concurrency library CML, Concurrent ML extension implemented as part of base library with its own scheduler implemented in Standard ML. This library is supported by SML/NJ and MLton compilers, so N2O for Standard ML supports both of them out of the box.

fun run (program_name, arglist) =
let val s = INetSock.TCP.socket()
in Socket.Ctl.setREUSEADDR (s, true); Socket.bind(s, INetSock.any 8989); Socket.listen(s, 5); acceptLoop s end

Acceptor loop uses CML spawn primitive for lightweight context creation for socket connection:

fun acceptLoop server_sock =
let val (s, _) = Socket.accept server_sock
in CML.spawn (fn () => connMain(s)); acceptLoop server_sock end

WebSocket RFC 6455

In pure and refined web stack WebSocket and static HTTP server could be unified up to switch function that performs HTTP 101 upgrade:

fun switch sock = case serve sock of (req, resp) => (sendResp sock resp; if (#status resp) <> 101 then ignore (Socket.close sock) else WebSocket.serve sock (fn msg => (M.hnd (req,msg))))

101 upgrade command logic could be obtained from RFC 6455.

fun upgrade sock req = (checkHandshake req; { body = Word8Vector.fromList nil, status = 101, headers = [(“Upgrade”, “websocket”),(“Connection”, “Upgrade”), (“Sec-WebSocket-Accept”, getKey req)] })

Also we have needUpgrade flag function that checks headers:

fun needUpgrade req = case header “Upgrade” req of SOME (_,v) => (lower v) = “websocket” | _ => false

The WebSocket structure of ML language contains the decode and encoder to/from raw packets Frame and datatype used in higher level code:

datatype Msg = Text of V.vector | Bin of V.vector | Close of Word32.word | Cont of V.vector | Ping | Pong

type Frame = { fin : bool, rsv1 : bool, rsv2 : bool, rsv3 : bool, typ : FrameType, payload : V.vector }

SHA-1 RFC 3174

The getKey function is used in SHA-1 protocol which is separate RFC 3174, and also need to be implemented. Fortunately one implementation by Sophia Donataccio was existed, so we could create a smaller one.

fun getKey req = case header “Sec-WebSocket-Key” req of NONE => raise BadRequest “No Sec-WebSocket-Key header” | SOME (_,key) => let val magic = “258EAFA5-E914–47DA-95CA-C5AB0DC85B11” in Base64.encode (SHA1.encode (Byte.stringToBytes (key^magic))) end

HTTP/1.1 RFC 2068

Check handshake has only three fail cases:

fun checkHandshake req = (if #cmd req <> “GET” then raise BadRequest “Method must be GET” else (); if #vers req <> “HTTP/1.1” then raise BadRequest “HTTP version must be 1.1” else (); case header “Sec-WebSocket-Version” req of SOME (_,”13") => () | _ => raise BadRequest “WebSocket version must be 13”)

Web Server

The internal types for HTTP and WebSocket server are specify Req and Resp types that are reused in HTTP and N2O layers for all implementations:

structure HTTP = struct
type
Headers = (string*string) list
type Req = { cmd : string, path : string, headers : Headers, vers : string }
type Resp = { status : int, headers : Headers, body : Word8Vector.vector } end

The handler signature hides the call chain from HTTP request and WebScoket frame to binary result for returning to socket. You should create your own application level implementation to provide this abstraction that will be called in context of TCP server:

signature HANDLER = sig
val hnd : HTTP.Req*WebSocket.Msg -> WebSocket.Res end

N2O Application Server

The N2O types specifies the I/O types along with Context and run function:

signature PROTO = sig
type Prot type Ev type Res type Req
val proto : Prot -> Ev end

The N2O functor wraps the Context and runner with protocol type which provides application level functionality.

functor MkN2O(M : PROTO) = struct
type Cx = {req: M.Req, module: M.Ev -> M.Res}
fun run (cx : Cx) (handlers : (Cx -> Cx) list) (msg : M.Prot) =
(#module cx) (M.proto msg)
end

Echo Application

According to provided specification we have only one chance to write echo server example application that will unveils the protocol implementation and context implementation:

structure EchoProto : PROTO = struct
type Prot = WS.Msg type Ev = Word8Vector.vector option
type Res = WS.Res type Req = HTTP.Req
fun proto (WS.TextMsg s) = SOME s | proto _ = NONE
end structure Echo = MkN2O(EchoProto)

The echo handler contains all server context packed in single structure. Here echo is a page subprotocol that is plugged as local handler for protocol runner. The router function provides module extraction that returns only echo subprotocol in context for each request. The run function is called with a given router for each message and each request:

structure EchoHandler : HANDLER = struct
fun echo NONE=WS.Ok|echo(SOME s) =WS.Reply(WS.Text s)
fun router (cx : Echo.Cx) = {req=(#req cx),module=echo}
fun hnd (req,msg) = Echo.run {req=req,module=echo} [router] msg
end structure Server = MkServer(EchoHandler)

The code is provided at gihub.com/o1/n2o.

--

--