I’m trying to understand how the websockets works. I’m building a basic chat with the least amount of setup code.
ws_handler = HTTP::WebSocketHandler.new do |ws, ctx|
ws.send({text: "Connecting"}.to_json)
ws.on_message do |message|
ws.send(message)
end
end
server = HTTP::Server.new [ws_handler]
When I open up 2 browser tabs, I send a message and I see they both hit the server, the server sends back to the browser, but only the tab that sent the message gets the message back. Is there something else I have to do to tell all connected clients to get the message?
Ok, one more just out of curiosity (and I know it’s a loaded question), but in that Kemal chat example, it’s shoving each websocket instance in to an Array. How would you handle that at scale?
Like 1 million connections. Just have an array (or maybe hash for some organization) with 1million instances in it? I guess you’d just throw more RAM at the machine at that point and try to cache and manage which connections are still alive and such at that level, right?
Each server in the cluster would have to maintain the list of clients in a chat, as in the example
and then you probably want to load balance between servers.
So you would use redis or a service mesh to distribute messages between servers so they can forward them to users connected to those servers.
Once you move to HA or scalable multi-server infrastructure the complexity of the application has to rise
Yeah, it makes sense that you’d eventually move to Redis or some other messaging system. The glue part that I’m missing is when you have a websocket connect, and you need to store something so you can reference that websocket later, what is that “something” if it’s not the websocket instance object? Since you can only shove Strings in redis, what are you shoving in to redis so when you pull it back out you can say “hey, this is WebSocket 456”? Or is that answer a bit too complicated?
I’m more just wondering if with Crystal, is there some identifier you can store to reference an open connection later, or does it become that you drop websockets, and start using a whole different system at that level?
In Kemal, you can define the URL that the clients use to create a websocket connection.
What I’ve done is asked the client to pass in a unique username to identify each connection:
ws "/actions/:username" do |socket, context|
username = context.ws_route_lookup.params["username"]
@socket_hash["#{username}"] = socket
socket.on_message do |message|
...
end
end
I agree with this approach, but you’ve gotta be careful with it. If a user has multiple tabs open and each have a WebSocket connection, you want to make sure one doesn’t overwrite the other. I recognize that you may have accounted for this in your app but simply cut it here for brevity, but others who aren’t as familiar with WebSockets may be surprised at the bugs this can cause.
I’m currently working on a Pusher-like app and the WebSocket service is easily the most complicated part when it comes to mapping channel subscriptions to individual connections and, especially, cleaning up when channels and connections are closed.
Agreed! This solution becomes more complex as you consider more use cases. Right, how would the app handle multiple browser connects from the same username? Good point!
I don’t find it really complicated (at least for my game + chat), if a user can be only connected once. Define a on_close which will remove the connection or set to nil.
A Set can be used for each user if multiple connections are allowed.
What I have is a Hash of Users, which can hold one nillable connection (or a set if you want multiples).
I have been trying to run it on Google Cloud Run, and I get a JS error: “‘WebSocket’: An insecure WebSocket connection may not be initiated from a page loaded over HTTPS.” Obviously, this is a bad interaction between the Google container which has a HTTPS address by default (can’t override) and the JS websocket library. As this seems to be a reasonable issue for those trying to run websockets in a production environment with increased security, I was curious if anyone had a solution around this.
From what I can tell, Kemal does not support SSL. Not sure if it would help anyway, since I have no access to the Google certificate.
Try setting this line to use wss://. wss is to ws as https is to http.
Your page is loaded over SSL, and your browser is complaining that the websocket isn’t because if the page is served from an encrypted connection, every asset served by the page should be, as well. Since the page and the websocket are served by the same process, you can simply tell it to use SSL for the websocket.
The reason this works is because Cloud Run has a load balancer in front of it that is handling SSL for you and sending plaintext requests to your application.