Skip to content

Commit

Permalink
Add a post about the new WebSockets
Browse files Browse the repository at this point in the history
  • Loading branch information
jaspervdj committed Oct 22, 2013
1 parent 54c245c commit 72fb81d
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 3 deletions.
7 changes: 4 additions & 3 deletions contact.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ on IRC -- I lurk as `jaspervdj` in the `#haskell` channel on [freenode].

For urgent matters, send a postcard to:

> Touwstraat 9
> 9160 Lokeren
> Belgium
> Jasper Van der Jeugt
> Zurlindenstrasse 50
> 8003 Zürich
> Switzerland
I like postcards.
Binary file added images/2013-10-22-websockets-console.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
192 changes: 192 additions & 0 deletions posts/2013-10-22-websockets-0.8.lhs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
---
title: WebSockets 0.8
description: New major release of the Haskell WebSockets library
tags: haskell
---

Introduction
============

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

[websockets]: http://jaspervdj.be/websockets

- 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`;
- Support for [deprecated] protocols has been removed, simplifying the API.

[enumerator]: http://hackage.haskell.org/package/enumerator
[io-streams]: http://hackage.haskell.org/package/io-streams
[deprecated]: http://hacks.mozilla.org/2010/12/websockets-disabled-in-firefox-4/

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](/images/2013-10-22-websockets-console.png)

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].

[this directory]: https://github.com/jaspervdj/websockets-snap/tree/master/example

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).

[snap backend]: http://hackage.haskell.org/package/websockets-snap
[wai]: http://hackage.haskell.org/package/wai
[contact]: /contact.html

> {-# 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:

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

> 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
> where
> 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
> where
> config =
> Snap.setErrorLog Snap.ConfigNoLog $
> Snap.setAccessLog Snap.ConfigNoLog $
> 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.

[war on IO libraries]: http://www.yesodweb.com/blog/2013/10/pipes-resource-problems

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.

0 comments on commit 72fb81d

Please sign in to comment.