Async::WebSocketGuidesRails Integration

Rails Integration

This guide explains how to use async-websocket with Rails.

Project Setup

Firstly, we will create a new project for the purpose of this guide:

$ rails new websockets
--- snip ---

Then, we need to add the Async::WebSocket gem:

$ bundle add async-websocket

Adding the WebSocket Controller

Firstly, generate the controller with a single method:

$ rails generate controller home index

Then edit your controller implementation:

require 'async/websocket/adapters/rails'

class HomeController < ApplicationController
	# WebSocket clients may not send CSRF tokens, so we need to disable this check.
	skip_before_action :verify_authenticity_token, only: [:index]
	
	def index
		self.response = Async::WebSocket::Adapters::Rails.open(request) do |connection|
			message = Protocol::WebSocket::TextMessage.generate({message: "Hello World"})
			connection.write(message)
		end
	end
end

Testing

You can quickly test that the above controller is working. First, start the Rails server:

$ rails s
=> Booting Puma
=> Rails 7.2.0.beta2 application starting in development 
=> Run `bin/rails server --help` for more startup options

Then you can connect to the server using a WebSocket client:

$ websocat ws://localhost:3000/home/index
{"message":"Hello World"}

Using Falcon

The default Rails server (Puma) is not suitable for handling a large number of connected WebSocket clients, as it has a limited number of threads (typically between 8 and 16). Each WebSocket connection will require a thread, so the server will quickly run out of threads and be unable to accept new connections. To solve this problem, we can use Falcon instead, which uses a fiber-per-request architecture and can handle a large number of connections.

We need to remove Puma and add Falcon::

$ bundle remove puma
$ bundle add falcon

Now when you start the server you should see something like this:

$ rails s
=> Booting Falcon v0.47.7
=> Rails 7.2.0.beta2 application starting in development http://localhost:3000
=> Run `bin/rails server --help` for more startup options

Using HTTP/2

Falcon supports HTTP/2, which can be used to improve the performance of WebSocket connections. HTTP/1.1 requires a separate TCP connection for each WebSocket connection, while HTTP/2 can handle multiple requessts and WebSocket connections over a single TCP connection. To use HTTP/2, you'd typically use https, which allows the client browser to use application layer protocol negotiation (ALPN) to negotiate the use of HTTP/2.

HTTP/2 WebSockets are a bit different from HTTP/1.1 WebSockets. In HTTP/1, the client sends a GET request with the upgrade: header. In HTTP/2, the client sends a CONNECT request with the :protocol pseud-header. The Rails routes must be adjusted to accept both methods:

Rails.application.routes.draw do
	# Previously it was this:
	# get "home/index"
	match "home/index", to: "home#index", via: [:get, :connect]
end

Once this is done, you need to bind falcon to an https endpoint:

$ falcon serve --bind "https://localhost:3000"

It's a bit more tricky to test this, but you can do so with the following Ruby code:

require 'async/http/endpoint'
require 'async/websocket/client'

endpoint = Async::HTTP::Endpoint.parse("https://localhost:3000/home/index")

Async::WebSocket::Client.connect(endpoint) do |connection|
	puts connection.framer.connection.class
	# Async::HTTP::Protocol::HTTP2::Client
	
	while message = connection.read
		puts message.inspect
	end
end