WebSockets 0.8

New major release of the Haskell WebSockets library
Posted in: haskell.


Today, I released version 0.8 of the websockets library. Important changes include:

Since this all implies means a huge simplification of the API, updating should be a pleasant experience – but please let me know if you run into trouble.

A fun example

Let us write a fun example using the new API. I implemented a super simple browser console, running a shell on the server and communicating with the browser using WebSockets.

A WebSockets-based browser console
A WebSockets-based browser console

Obviously this is quite insecure, so do not run this on your own server without adding proper authentication!

This blogpost is written in literate Haskell. However, it needs some HTML and JavaScript as well. In order to run this, run server.hs from this directory.

As always, we start with a bunch of imports. We are using the snap backend here. I still need to write support for wai (or Warp), hopefully I can do that soon (or contact me if you would like to hack on this).

Update: Ting-Yen Lai has been so kind to update the bindings for wai. You can now just use the wai-websockets package from Hackage!

{-# LANGUAGE OverloadedStrings #-}
module Main where
import Control.Concurrent (forkIO)
import Control.Exception (fromException, handle, throw)
import Control.Monad (forever, unless)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BC
import qualified Network.WebSockets as WS
import qualified Network.WebSockets.Snap as WS
import Snap.Core (Snap)
import qualified Snap.Core as Snap
import qualified Snap.Http.Server as Snap
import qualified Snap.Util.FileServe as Snap
import qualified System.IO as IO
import qualified System.Process as Process

The main Snap application only supports four routes. The index page, two additional resources and our console handler, which we will look at in detail next.

app :: Snap ()
app = Snap.route
[ ("", Snap.ifTop $ Snap.serveFile "console.html")
, ("console.js", Snap.serveFile "console.js")
, ("style.css", Snap.serveFile "style.css")
, ("console/:shell", console)

The browser will make a WebSockets connection to /console/:shell. The shell argument determines the command we will run. Obvious examples are /console/zsh, /console/bash, and /console/ghci.

By using WS.runWebSocketsSnap, the HTTP connection is passed from Snap to the WebSockets library, which runs the consoleApp.

console :: Snap ()
console = do
Just shell <- Snap.getParam "shell"
WS.runWebSocketsSnap $ consoleApp $ BC.unpack shell

Here, we have consoleApp, the actual WebSockets application. Note that WS.ServerApp is just a type synonym to WS.PendingConnection -> IO ().

We start out by running the shell command, obtaining handles to the stdin, stdout and stderr streams. We also accept the pending connection regardless of what’s in there: proper authentication would not be a bad idea.

consoleApp :: String -> WS.ServerApp
consoleApp shell pending = do
(stdin, stdout, stderr, phandle) <- Process.runInteractiveCommand shell
conn <- WS.acceptRequest pending

Once the connection is accepted, we fork threads to stream data:

The copyHandleToConn and copyConnToHandle functions are defined later in this file.

_ <- forkIO $ copyHandleToConn stdout conn
_ <- forkIO $ copyHandleToConn stderr conn
_ <- forkIO $ copyConnToHandle conn stdin

Now that our input/output is set up, we wait for the shell to finish. Once our WS.ServerApp completes, the WebSockets connection will be closed automatically.

exitCode <- Process.waitForProcess phandle
putStrLn $ "consoleApp ended: " ++ show exitCode

The first utility function is a loop reading from a plain IO.Handle, and using WS.sendTextData to send messages to the browser.

copyHandleToConn :: IO.Handle -> WS.Connection -> IO ()
copyHandleToConn h c = do
bs <- B.hGetSome h 1024
unless (B.null bs) $ do
putStrLn $ "> " ++ show bs
WS.sendTextData c bs
copyHandleToConn h c

The second utility function does the reverse. It uses WS.receiveData to wait for and receive messages from the browser. It writes these to the provided IO.Handle. We also watch for the WS.ConnectionClosed exception, so we can cleanly close the handle.

copyConnToHandle :: WS.Connection -> IO.Handle -> IO ()
copyConnToHandle c h = handle close $ forever $ do
bs <- WS.receiveData c
putStrLn $ "< " ++ show bs
B.hPutStr h bs
IO.hFlush h
close e = case fromException e of
Just WS.ConnectionClosed -> IO.hClose h
Nothing -> throw e

What is left is a super-simple main function to serve our Snap application over HTTP:

main :: IO ()
main = Snap.httpServe config app
config =
Snap.setErrorLog Snap.ConfigNoLog $
Snap.setAccessLog Snap.ConfigNoLog $

Appendix: IO libraries

Recently, the war on IO libraries started again. Since this blogpost is somewhat about a port between two IO libraries, some of you might think I have an enlightened opinion on this subject.

I do not. I can see how pipes and conduit make creating and composing IO streams easier, but I do not have a clue about which one is easier to use, or more formally correct.

The only reason I chose to use io-streams is out of practical considerations. I think the available IO libraries can be classified in two main groups:

I have had some trouble in the past integrating the enumerator-based WebSockets library with conduit-based Warp. I think it might also be tricky to use a pipes-based library in a conduit-based application, and vice versa.

However, by building libraries on top of io-streams (or System.IO, but I think io-streams is more convenient), I get libraries that are easy to integrate almost everywhere.