Announcing engine-io and socket-io for Haskell

I’ve just released three new libraries to Hackage:

Engine.IO

Engine.IO is a new framework from Automattic, which provides an abstraction for real-time client/server communication over the web. You can establish communication channels with clients over XHR long-polling, which works even through proxies and aggressive traffic rewriting, and connections are upgraded to use HTML 5 web sockets if available to reduce latency. Engine.IO also allows the transmission of binary data without overhead, while also gracefully falling back to using base 64 encoding if the client doesn’t support raw binary packets.

This is all very desirable stuff, but you’re going to have a hard time convincing me that I should switch to Node.js! I’m happy to announce that we now have a Haskell implementation for Engine.IO servers, which can be successfully used with the Engine.IO JavaScript client. A simple application may look like the following:

{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} module Main where import Control.Monad (forever) (forever) import qualified Control.Concurrent.STM as STM import qualified Network.EngineIO as EIO import qualified Network.EngineIO.Snap as EIOSnap import qualified Snap.CORS as CORS import qualified Snap.Http.Server as Snap handler :: EIO.Socket -> IO () () = forever $ handler sforever $ EIO.receive s >>= EIO.send s STM.atomicallyEIO.receive sEIO.send s main :: IO () () = do main <- EIO.initialize eioEIO.initialize $ CORS.applyCORS CORS.defaultOptions $ Snap.quickHttpServeCORS.applyCORS CORS.defaultOptions pure handler) EIOSnap.snapAPI EIO.handler eio (handler) EIOSnap.snapAPI

This example uses engine-io-snap to run an Engine.IO application using Snap’s server, which allows me to concentrate on the important stuff. The body of the application is the handler , which is called every time a socket connects. In this case, we have a basic echo server, which constantly reads (blocking) from the client, and echos the message straight back.

As mentioned, you can also do binary transmission - the following handler transmits the lovable doge.png to clients:

= do handler s <- BS.readFile "doge.png" bytesBS.readFile $ STM.atomically EIO.BinaryPacket bytes) EIO.send socket (bytes)

On the client side, this can be displayed as an image by using data URIs, or manipulated using the HTML 5 File API.

Socket.IO

Socket.IO builds on top of Engine.IO to provide an abstraction to build applications in terms of events. In Socket.IO, clients connect to a server, and then receive and emit events, which can often provide a simpler architecture for web applications.

My Socket.IO implementation in Haskell also strives for simplicity, by taking advantage of the aeson library a lot of the encoding and decoding of packets is hidden, allowing you to focus on your application logic. I’ve implemented the example chat application, originally written in Node.js, using my Haskell server:

data AddUser = AddUser Text.Text instance Aeson.FromJSON AddUser where = Aeson.withText "AddUser" $ pure . AddUser parseJSONAeson.withText data NumConnected = NumConnected ! Int instance Aeson.ToJSON NumConnected where NumConnected n) = Aeson.object [ "numUsers" .= n] toJSON (n)Aeson.object [n] data NewMessage = NewMessage Text.Text instance Aeson.FromJSON NewMessage where = Aeson.withText "NewMessage" $ pure . NewMessage parseJSONAeson.withText data Said = Said Text.Text Text.Text instance Aeson.ToJSON Said where Said username message) = Aeson.object toJSON (username message)Aeson.object [ "username" .= username username , "message" .= message message ] data UserName = UserName Text.Text instance Aeson.ToJSON UserName where UserName un) = Aeson.object [ "username" .= un ] toJSON (un)Aeson.object [un ] data UserJoined = UserJoined Text.Text Int instance Aeson.ToJSON UserJoined where UserJoined un n) = Aeson.object toJSON (un n)Aeson.object [ "username" .= un un , "numUsers" .= n ] -------------------------------------------------------------------------------- data ServerState = ServerState { ssNConnected :: STM.TVar Int } server :: ServerState -> SocketIO.Router () () = do server state <- liftIO STM.newEmptyTMVarIO userNameMVarliftIO STM.newEmptyTMVarIO let forUserName m = liftIO (STM.atomically (STM.tryReadTMVar userNameMVar)) >>= mapM_ m forUserName mliftIO (STM.atomically (STM.tryReadTMVar userNameMVar)) "new message" $ \( NewMessage message) -> SocketIO.on\(message) $ \userName -> forUserName\userName "new message" ( Said userName message) SocketIO.broadcastuserName message) "add user" $ \( AddUser userName) -> do SocketIO.on\(userName) n <- liftIO $ STM.atomically $ do liftIOSTM.atomically n <- ( + 1 ) <$> STM.readTVar (ssNConnected state) STM.readTVar (ssNConnected state) STM.putTMVar userNameMVar userName STM.writeTVar (ssNConnected state) n return n "login" ( NumConnected n) SocketIO.emitn) "user joined" ( UserJoined userName n) SocketIO.broadcastuserName n) "typing" $ SocketIO.on_ $ \userName -> forUserName\userName "typing" ( UserName userName) SocketIO.broadcastuserName) "stop typing" $ SocketIO.on_ $ \userName -> forUserName\userName "stop typing" ( UserName userName) SocketIO.broadcastuserName)

We define a few data types and their JSON representations, and then define our server application below. Users of the library don’t have to worry about parsing and validating data for routing, as this is handled transparently by defining event handlers. In the above example, we listen for the add user event, and expect it to have a JSON payload that can be decoded to the AddUser data type. This follows the best-practice of pushing validation to the boundaries of your application, so you can spend more time working with stronger types.

By stronger types, I really do mean stronger types - at Fynder we’re using this very library with the singletons library in order to provide strongly typed publish/subscribe channels. If you’re interested in this, be sure to come along to the Haskell eXchange, where I’ll be talking about exactly that!

You can contact me via email at ollie@ocharles.org.uk or tweet to me @acid2. I share almost all of my work at GitHub. This post is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.