WebSockets with Haskell Servant

This is a guide I wrote on the topic:
https://www.techmindful.blog/blog/websockets-in-servant

I may expand it into a series about eventually building a chat server as well.

Feedbacks are very welcome, especially if I made some hilarious mistakes.

5 Likes

Thanks for sharing! Always nice to see people make stuff with Haskell.

A couple of questions:

  • What role does servant play in this case? It seems to me like it could be replaced by running a server via the websockets library directly and avoid that big dependency, am I missing something?
  • How do you handle bidirectional communication with the socket? In tbe past I’ve used two threads per socket - one listens to the socket and dispatches, and one that send messages it receives from an stm queue to the socket. Are you using a similar pattern or something different?
1 Like

My pleasure! So I regard Servant simply as the context here. I didn’t intend it as how to make a websocket server with Servant. I intended it to be if we are already using Servant, then how to get websockets working. For example, the private chat service I was making had other REST API endpoints as well. The API type ended up looking like:

type API = "read-letter" :> Capture "letterId" :> Get '[ Servant.JSON ] LetterMeta
      :<|> "write-letter" :> ReqBody '[ Servant.JSON ] Letter :> Put '[ Servant.JSON ] String
       ...
      :<|> "chat" :> Capture "chatId" String :> WebSocket

So I took advantage of Servant’s type features, and managed to make websocket work as well.
Thanks to your question, I think this may be a point that I can clarify more :+1:

As for bidirectional communication, I only have the one handler thread for each connection with a client, and didn’t spawn threads myself for receiving and sending. This may be due to the nature of a chat server, where the server doesn’t need to spontaneously send data through ws. It only broadcasts messages that come from a client. The handler goes into an infinite loop when the connection is opened. It first calls receiveDataMessage, which blocks until a message arrives. Then it broadcasts the message to all the relevant clients, by sendTextData. Then it repeats.

The picture it draws is that when one handler receives a message, it sends data to other websockets, which are for other clients, not the client of this handler. It’s possible because I inserted each client’s ws connection to a global app state.

Your way of spawning two separate threads seems to be the commonly accepted one. I’m not sure if my approach has problems. It’s in fact best to figure out, before I continue to write anything about it. Was your use case also a chat server? If so, then I’m doing it differently. What do you think?

1 Like

I intended it to be if we are already using Servant

Ok I understand! Might be useful to mention that and maybe also mention that one can do this with just websockets.

Was your use case also a chat server? If so, then I’m doing it differently. What do you think?

Yes, it is. I think your solution makes sense - it means that the threads of other clients are sending messages to a clients sockets. In my case only the two client threads interact with their client’s socket and the inner communication between client threads is done via stm queues.

I’m wondering if the approach of other clients sending messages to sockets directly can present issues, for example race conditions like two writers writing to the same socket at the same time might outputs garbled text (that is the case with putStrLn for example), or the order of messages might be non-deterministic.

1 Like

That is a great concern I haven’t thought about! I’d like to test it first. I looked up briefly, maybe this function can be useful for spawning two threads that send to the same ws, at the same time? https://hackage.haskell.org/package/async-2.2.4/docs/Control-Concurrent-Async.html#v:concurrently

1 Like

Yes, I think that would work as a test. I would even try to write long strings with many writers at once using mapConcurrently or similar.