I’ve just released three new libraries to Hackage:
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)
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 s $ EIO.receive s >>= EIO.send s
STM.atomically
main :: IO ()
= do
main <- EIO.initialize
eio $ CORS.applyCORS CORS.defaultOptions $
Snap.quickHttpServe pure handler) EIOSnap.snapAPI EIO.handler eio (
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"
bytes $
STM.atomically EIO.BinaryPacket bytes) EIO.send socket (
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 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
parseJSON
data NumConnected = NumConnected !Int
instance Aeson.ToJSON NumConnected where
NumConnected n) = Aeson.object [ "numUsers" .= n]
toJSON (
data NewMessage = NewMessage Text.Text
instance Aeson.FromJSON NewMessage where
= Aeson.withText "NewMessage" $ pure . NewMessage
parseJSON
data Said = Said Text.Text Text.Text
instance Aeson.ToJSON Said where
Said username message) = Aeson.object
toJSON ("username" .= username
[ "message" .= message
,
]
data UserName = UserName Text.Text
instance Aeson.ToJSON UserName where
UserName un) = Aeson.object [ "username" .= un ]
toJSON (
data UserJoined = UserJoined Text.Text Int
instance Aeson.ToJSON UserJoined where
UserJoined un n) = Aeson.object
toJSON ("username" .= un
[ "numUsers" .= n
,
]
--------------------------------------------------------------------------------
data ServerState = ServerState { ssNConnected :: STM.TVar Int }
server :: ServerState -> SocketIO.Router ()
= do
server state <- liftIO STM.newEmptyTMVarIO
userNameMVar let forUserName m = liftIO (STM.atomically (STM.tryReadTMVar userNameMVar)) >>= mapM_ m
"new message" $ \(NewMessage message) ->
SocketIO.on $ \userName ->
forUserName "new message" (Said userName message)
SocketIO.broadcast
"add user" $ \(AddUser userName) -> do
SocketIO.on <- liftIO $ STM.atomically $ do
n <- (+ 1) <$> STM.readTVar (ssNConnected state)
n
STM.putTMVar userNameMVar userName
STM.writeTVar (ssNConnected state) nreturn n
"login" (NumConnected n)
SocketIO.emit "user joined" (UserJoined userName n)
SocketIO.broadcast
"typing" $
SocketIO.on_ $ \userName ->
forUserName "typing" (UserName userName)
SocketIO.broadcast
"stop typing" $
SocketIO.on_ $ \userName ->
forUserName "stop typing" (UserName userName) SocketIO.broadcast
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.