WebSockets 0.8
Published on October 22, 2013 under the tag haskell
Introduction
Today, I released version 0.8 of the websockets library. Important changes include:
- The underlying IO library has been changed from enumerator to io-streams;
- The API has been redesigned to work with a
Connection
datatype and plainIO
instead of a customMonad
; - Support for deprecated protocols has been removed, simplifying the API.
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.
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 ()
= Snap.route
app "", 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 ()
= do
console Just shell <- Snap.getParam "shell"
$ consoleApp $ BC.unpack shell WS.runWebSocketsSnap
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
= do
consoleApp shell pending <- Process.runInteractiveCommand shell
(stdin, stdout, stderr, phandle) <- WS.acceptRequest pending conn
Once the connection is accepted, we fork threads to stream data:
- We send everything that appears on
stdout
to the browser; - We do the same for
stderr
; - We send every message coming from the browser to
stdin
.
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.
<- Process.waitForProcess phandle
exitCode 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 ()
= do
copyHandleToConn h c <- B.hGetSome h 1024
bs $ do
unless (B.null bs) 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 ()
= handle close $ forever $ do
copyConnToHandle c h <- WS.receiveData c
bs putStrLn $ "< " ++ show bs
B.hPutStr h bsIO.hFlush h
where
= case fromException e of
close e 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 ()
= Snap.httpServe config app
main where
=
config Snap.ConfigNoLog $
Snap.setErrorLog Snap.ConfigNoLog $
Snap.setAccessLog Snap.defaultConfig
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:
pipes
,conduit
,enumerator
: provide high-level easy-to-use combinators for doing IO. This is a great way to rapidly write great applications.System.IO
,io-streams
: provide lower-level access to IO resources. I think these are great to write libraries, since libraries built upon these IO libraries can be easily integrated into any application.
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.