In this article you will find out how to easily create a distributed websocket chat application with Crystal and Onyx Framework.

Crystal is a rapidly growing compiled language with speed of C and Ruby-inspired syntax. It has already proven its superiority regarding to websockets performance in Serdar Doğruyol’s blog post – it is able to handle up to 60000 concurrent connections on a 2 GB DigitalOcean droplet:

It works great in a single process, but what if you wanted to scale your application? It is very simple to do so with Onyx::HTTP channels and Onyx::EDA events.

Note that Onyx::EDA relies on Redis Streams feature, that’s why it requires Redis version >=5. I use wscat to test the websockets in the terminal in this article.

The repository is available at github.com/vladfaust/onyx-40-loc-distributed-chat.

Here goes the complete code of the server:

require "onyx/http"
require "onyx/eda/redis" # This requires REDIS_URL environment variable

struct Message
  include Onyx::EDA::Event

  getter username, content

  def initialize(@username : String, @content : String)
  end
end

class Chat
  include Onyx::HTTP::Channel

  params do
    query do
      type username : String
    end
  end

  getter! sub : Onyx::EDA::Channel::Subscription(Message)

  def on_open
    @sub = Onyx::EDA.redis.subscribe(Message) do |message|
      socket.send("#{message.username}: #{message.content}")
    end
  end

  def on_message(message)
    Onyx::EDA.redis.emit(Message.new(params.query.username, message))
  end

  def on_close
    sub.unsubscribe
  end
end

Onyx::HTTP.ws "/", Chat
Onyx::HTTP.listen(port: ENV["PORT"].to_i) # You'll also need PORT variable to be set

First terminal:

> env PORT=5000 crystal src/onyx-chat.cr
 INFO [21:25:38.483] ⬛ Onyx::HTTP::Server is listening at http://127.0.0.1:5000

Second terminal (note another port):

> env PORT=5001 crystal src/onyx-chat.cr
 INFO [21:25:38.483] ⬛ Onyx::HTTP::Server is listening at http://127.0.0.1:5001

Third terminal:

> wscat --connect ws://localhost:5000?username=Alice
connected (press CTRL+C to quit)
> Hello! # Message sent from this terminal
< Alice: Hello!
< Bob: Hi!
>

Fourth terminal:

> wscat --connect ws://localhost:5001?username=Bob
connected (press CTRL+C to quit)
< Alice: Hello!
> Hi! # Message sent from this terminal
< Bob: Hi!
>

These are two separate websocket chat processes which use Redis as a back-end for synchronisation, in just in 40 lines of code!

Testing is good. And it is also simple with Onyx. This code would ensure everything is working as intended:

describe "server" do
  alice_socket = Onyx::HTTP::Spec.ws("/?username=Alice")
  bob_socket = Onyx::HTTP::Spec.ws("/?username=Bob")

  it do
    bob_socket.send("Hello")
    alice_socket.assert_response("Bob: Hello")
  end

  it do
    alice_socket.send("Hi")
    bob_socket.assert_response("Alice: Hi")
  end
end

Crystal dependencies (shards) you’d need:

dependencies:
  onyx:
    github: onyxframework/onyx
    version: ~> 0.4.0
  onyx-http:
    github: onyxframework/http
    version: ~> 0.8.0
  onyx-eda:
    github: onyxframework/eda
    version: ~> 0.3.0

The whole source code is available at github.com/vladfaust/onyx-40-loc-distributed-chat. Enjoy!

If you like this article but have no experience in Crystal yet, then follow out series of tutorials which would lead you through the basics.