WebSockets 0.8



Published on October 22, 2013 under the tag New major release of the Haskell WebSockets libraryPublished 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 plain IO instead of a custom Monad ;

datatype and plain instead of a custom ; 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.

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) (forkIO) import Control.Exception (fromException, handle, throw) (fromException, handle, throw) import Control.Monad (forever, unless) (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 appSnap.route "" , Snap.ifTop $ Snap.serveFile "console.html" ) [ (, Snap.ifTopSnap.serveFile "console.js" , Snap.serveFile "console.js" ) , (, Snap.serveFile "style.css" , Snap.serveFile "style.css" ) , (, Snap.serveFile "console/:shell" , console) , (, 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" shellSnap.getParam $ consoleApp $ BC.unpack shell WS.runWebSocketsSnapconsoleAppBC.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 = do consoleApp shell pending <- Process.runInteractiveCommand shell (stdin, stdout, stderr, phandle)Process.runInteractiveCommand shell <- WS.acceptRequest pending connWS.acceptRequest pending

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

We send everything that appears on stdout to the browser;

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 forkIOcopyHandleToConn stdout conn _ <- forkIO $ copyHandleToConn stderr conn forkIOcopyHandleToConn stderr conn _ <- forkIO $ copyConnToHandle conn stdin forkIOcopyConnToHandle 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 exitCodeProcess.waitForProcess phandle putStrLn $ "consoleApp ended: " ++ show exitCode 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 bsB.hGetSome h $ do unless (B.null bs) putStrLn $ "> " ++ show bs 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 hhandle closeforever <- WS.receiveData c bsWS.receiveData c putStrLn $ "< " ++ show bs bs B.hPutStr h bs IO . hFlush h hFlush h where = case fromException e of close efromException e Just WS.ConnectionClosed -> IO . hClose h hClose h Nothing -> throw e throw e

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

main :: IO () () = Snap.httpServe config app mainSnap.httpServe config app 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.

, , : 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.