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
Connectiondatatype and plainIOinstead 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 whereimport 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 ProcessThe 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 shellHere, 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 pendingOnce the connection is accepted, we fork threads to stream data:
- We send everything that appears on
stdoutto 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 stdinNow 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 exitCodeThe 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 cThe 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
where
close e = case fromException e of
Just WS.ConnectionClosed -> IO.hClose h
Nothing -> throw eWhat is left is a super-simple main function to serve our Snap application
over HTTP:
main :: IO ()
main = Snap.httpServe config app
where
config =
Snap.setErrorLog Snap.ConfigNoLog $
Snap.setAccessLog Snap.ConfigNoLog $
Snap.defaultConfigAppendix: 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.