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.
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.
Thanks for sharing! Always nice to see people make stuff with Haskell.
A couple of questions:
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
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?
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.
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
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.