WebSockets

WebSock'em Boppers!

Definining a WebSocket Path

Defining a websocket is very similar to any other kind of route. Use the ws method and give it a path and a block:

ws "/my_socket" do |ws, ctx|
  puts "new socket connection!"

  ws.on_message do |msg|
    puts "message received: #{msg}"
  end

  # send a message to the client
  ws.send("connected to websocket")
end

URL params work with websocket paths as well, so its easy to allow clients to establish connections to a room, like so:

ws "/room/:room_id" do |ws, ctx|
  room_id = ctx.params["room_id"]

  ws.on_message do |msg|
    puts "message received: #{msg}"
  end

  ws.on_close do
    puts "socket closed"
  end

  # send a message to the client
  ws.send("connected to room #{room_id}")
end

WebSocket blocks take two arguments: (1) the websocket, and (2) the context.

WebSocket Middlewares

There are two things that make WebSocket middlewares different from regular middlewares:

  1. They inherit from Raze::WebSocketHandler
  2. The handler's call method takes three arguments (ws, ctx, done) instead of two (ctx, done)

Here is an example of creating and implementing a custom WebSocket middleware:

class UserAuthenticator < Raze::WebSocketHandler
  def call(ws, ctx, done)
    user_authenticated = authenticate_user_here

    if (user_authenticated)
      ws.send("access granted")
      done.call
    else
      ctx.halt_plain "access denied", 403
    end
  end
end

ws "/my_socket" UserAuthenticator.new do |ws, ctx|
  ws.on_message do |msg|
    puts "message received: #{msg}"
  end

  # send a message to the client
  ws.send("websocket ready for action!")
end

WebSocket Channels

WebSockets channels are a way of broadcasting messages to all of the sockets in a group.

The normal workflow is as follows:

  1. Get/create a channel (Raze.ws_channel("channel_id"))
  2. Add WebSocket to channel (channel.add(websocket))
  3. Start broadcasting (channel.broadcast("my_message"))

The WebSocket will automatically be removed from the channel when it is closed.

# allow clients to join a room
ws "/room/:room_id" do |ws, ctx|
  room_id = ctx.params["room_id"]
  user_id = generate_user_id_here

  # get channel by some identifier (creates channel if not exists)
  channel = Raze.ws_channel(room_id)

  # Add socket connection to channel
  channel.add ws

  # broadcast to channel that this user joined
  channel.broadcast(
    {"user_id" => user_id, "msg" => "user joined"}.to_json
  )

  ws.on_message do |msg|
    # broadcast a json message to each websocket in the channel
    channel.broadcast(
      {"user_id" => user_id, "msg" => msg}.to_json
    )
  end

  ws.on_close do
    # broadcast to any remaining users that this user has left
    channel.broadcast(
      {"user_id" => user_id, "msg" => "user disconnected"}.to_json
    )
  end
end
Channels are made to make it easier to broadcast messages to clients connected to a single server. If you running an app across multiple servers, you will want to use something like [Redis' PubSub](https://redis.io/topics/pubsub).