8000 Further improvements to documentation + rails integration guide. · socketry/async-websocket@fa30565 · GitHub
[go: up one dir, main page]

Skip to content

Commit fa30565

Browse files
committed
Further improvements to documentation + rails integration guide.
1 parent 81d09ba commit fa30565

File tree

11 files changed

+133
-120
lines changed

11 files changed

+133
-120
lines changed

examples/chat/client.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
require 'async'
88
require 'async/http/endpoint'
99
require_relative '../../lib/async/websocket/client'
10-
require 'protocol/websocket/json_message'
1110

1211
USER = ARGV.pop || "anonymous"
1312
URL = ARGV.pop || "https://localhost:8080"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2022, by Samuel Williams.
5+
6+
require 'sus/fixtures/async/http/server_context'
7+
require 'protocol/rack/adapter'
8+
9+
module Async
10+
module WebSocket
11+
module RackApplication
12+
include Sus::Fixtures::Async::HTTP::ServerContext
13+
14+
def builder
15+
Rack::Builder.parse_file(File.expand_path('rack_application/config.ru', __dir__))
16+
end
17+
18+
def app
19+
Protocol::Rack::Adapter.new(builder)
20+
end
21+
end
22+
end
23+
end

fixtures/rack_application/config.ru renamed to fixtures/async/websocket/rack_application/config.ru

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class ClosedLogger
1414
response = @app.call(env)
1515

1616
response[2] = Rack::BodyProxy.new(response[2]) do
17-
Console.logger.debug(self, "Connection closed!")
17+
Console.debug(self, "Connection closed!")
1818
end
1919

2020
return response
@@ -24,7 +24,7 @@ end
2424
# This wraps our response in a body proxy which ensures Falcon can handle the response not being an instance of `Protocol::HTTP::Body::Readable`.
2525
use ClosedLogger
2626

27-
run lambda {|env|
27+
run do |env|
2828
Async::WebSocket::Adapters::Rack.open(env, protocols: ['ws']) do |connection|
2929
$connections << connection
3030

@@ -36,9 +36,9 @@ run lambda {|env|
3636
end
3737
end
3838
rescue => error
39-
Console.logger.error(self, error)
39+
Console.error(self, error)
4040
ensure
4141
$connections.delete(connection)
4242
end
4343
end or [200, {}, ["Hello World"]]
44-
}
44+
end

fixtures/rack_application.rb

Lines changed: 0 additions & 19 deletions
This file was deleted.

fixtures/rack_application/client.rb

Lines changed: 0 additions & 37 deletions
This file was deleted.

fixtures/upgrade_application.rb

Lines changed: 0 additions & 28 deletions
This file was deleted.

guides/getting-started/readme.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ Async do |task|
3737
end
3838
end
3939

40-
connection.write({
40+
# Generate a text message by geneating a JSON payload from a hash:
41+
connection.write(Protocol::WebSocket::TextMessage.generate({
4142
user: USER,
4243
status: "connected",
43-
})
44+
}))
4445

4546
while message = connection.read
4647
puts message.inspect

guides/rails-integration/readme.md

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,10 @@ $ rails new websockets
1111
--- snip ---
1212
~~~
1313

14-
Then, we need to add the [Falcon](https://github.com/socketry/falcon) web server and the `Async::WebSocket` gem:
14+
Then, we need to add the `Async::WebSocket` gem:
1515

1616
~~~ bash
17-
$ bundle add falcon async-websocket
18-
$ bundle remove puma
19-
--- snip ---
20-
$ rails s
21-
=> Booting Falcon
22-
=> Rails 6.0.3.1 application starting in development http://localhost:3000
23-
=> Run `rails server --help` for more startup options
17+
$ bundle add async-websocket
2418
~~~
2519

2620
## Adding the WebSocket Controller
@@ -34,17 +28,94 @@ $ rails generate controller home index
3428
Then edit your controller implementation:
3529

3630
~~~ ruby
37-
require 'async/websocket/adapters/rack'
31+
require 'async/websocket/adapters/rails'
3832

3933
class HomeController < ApplicationController
34+
# WebSocket clients may not send CSRF tokens, so we need to disable this check.
35+
skip_before_action :verify_authenticity_token, only: [:index]
36+
4037
def index
41-
self.response = Async::WebSocket::Adapters::Rack.open(request.env) do |connection|
42-
connection.write({message: "Hello World"})
38+
self.response = Async::WebSocket::Adapters::Rails.open(request) do |connection|
39+
message = Protocol::WebSocket::TextMessage.generate({message: "Hello World"})
40+
connection.write(message)
4341
end
4442
end
4543
end
4644
~~~
4745

4846
### Testing
4947

50-
You can quickly test that the above controller is working using a websocket client:
48+
You can quickly test that the above controller is working. First, start the Rails server:
49+
50+
~~~ bash
51+
$ rails s
52+
=> Booting Puma
53+
=> Rails 7.2.0.beta2 application starting in development
54+
=> Run `bin/rails server --help` for more startup options
55+
~~~
56+
57+
Then you can connect to the server using a WebSocket client:
58+
59+
~~~ bash
60+
$ websocat ws://localhost:3000/home/index
61+
{"message":"Hello World"}
62+
~~~
63+
64+
### Using Falcon
65+
66+
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](https://github.com/socketry/falcon) instead, which uses a fiber-per-request architecture and can handle a large number of connections.
67+
68+
We need to remove Puma and add Falcon::
69+
70+
~~~ bash
71+
$ bundle remove puma
72+
$ bundle add falcon
73+
~~~
74+
75+
Now when you start the server you should see something like this:
76+
77+
~~~ bash
78+
$ rails s
79+
=> Booting Falcon v0.47.7
80+
=> Rails 7.2.0.beta2 application starting in development http://localhost:3000
81+
=> Run `bin/rails server --help` for more startup options
82+
~~~
83+
84+
85+
### Using HTTP/2
86+
87+
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.
88+
89+
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:
90+
91+
~~~ ruby
92+
Rails.application.routes.draw do
93+
# Previously it was this:
94+
# get "home/index"
95+
match "home/index", to: "home#index", via: [:get, :connect]
96+
end
97+
~~~
98+
99+
Once this is done, you need to bind falcon to an `https` endpoint:
100+
101+
~~~ bash
102+
$ falcon serve --bind "https://localhost:3000"
103+
~~~
104+
105+
It's a bit more tricky to test this, but you can do so with the following Ruby code:
106+
107+
~~~ ruby
108+
require 'async/http/endpoint'
109+
require 'async/websocket/client'
110+
111+
endpoint = Async::HTTP::Endpoint.parse("https://localhost:3000/home/index")
112+
113+
Async::WebSocket::Client.connect(endpoint) do |connection|
114+
puts connection.framer.connection.class
115+
# Async::HTTP::Protocol::HTTP2::Client
116+
117+
while message = connection.read
118+
puts message.inspect
119+
end
120+
end
121+
~~~

lib/async/websocket/client.rb

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,18 @@ def close(...)
5454

5555
# @return [Connection] an open websocket connection to the given endpoint.
5656
def self.connect(endpoint, *arguments, **options, &block)
57-
client = self.open(endpoint, *arguments)
58-
connection = client.connect(endpoint.authority, endpoint.path, **options)
59-
60-
return ClientCloseDecorator.new(client, connection) unless block_given?
61-
62-
begin
63-
yield connection
64-
ensure
65-
connection.close
66-
client.close
57+
Sync do
58+
client = self.open(endpoint, *arguments)
59+
connection = client.connect(endpoint.authority, endpoint.path, **options)
60+
61+
return ClientCloseDecorator.new(client, connection) unless block_given?
62+
63+
begin
64+
yield connection
65+
ensure
66+
connection.close
67+
client.close
68+
end
6769
end
6870
end
6971

@@ -80,6 +82,8 @@ def initialize(pool, connection, stream)
8082
@connection = connection
8183
end
8284

85+
attr :connection
86+
8387
def close
8488
super
8589

test/async/websocket/adapters/rack.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
require 'async/websocket'
88
require 'async/websocket/client'
99
require 'async/websocket/adapters/rack'
10-
require 'rack_application'
10+
require 'async/websocket/rack_application'
1111

1212
describe Async::WebSocket::Adapters::Rack do
1313
it "can determine whether a rack env is a websocket request" do
@@ -16,7 +16,7 @@
1616
end
1717

1818
with 'rack application' do
19-
include RackApplication
19+
include Async::WebSocket::RackApplication
2020

2121
it "can make non-websocket connection to server" do
2222
response = client.get("/")
@@ -28,7 +28,7 @@
2828
end
2929

3030
let(:message) do
31-
Protocol::WebSocket::JSONMessage.generate({text: "Hello World"})
31+
Protocol::WebSocket::TextMessage.generate({text: "Hello World"})
3232
end
3333

3434
it "can make websocket connection to server" do

test/async/websocket/server.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
# Released under the MIT License.
44
# Copyright, 2019-2023, by Samuel Williams.
55

6-
require 'protocol/websocket/json_message'
76
require 'protocol/http/middleware/builder'
87

98
require 'async/websocket/client'
@@ -47,7 +46,7 @@
4746
let(:app) do
4847
Protocol::HTTP::Middleware.for do |request|
4948
Async::WebSocket::Adapters::HTTP.open(request) do |connection|
50-
message = Protocol::WebSocket::JSONMessage.generate(request.headers.fields)
49+
message = Protocol::WebSocket::TextMessage.generate(request.headers.fields)
5150
message.send(connection)
5251

5352
connection.close
@@ -59,9 +58,9 @@
5958
connection = websocket_client.connect(endpoint.authority, "/headers", headers: headers)
6059

6160
begin
62-
json_message = Protocol::WebSocket::JSONMessage.wrap(connection.read)
61+
message = connection.read
6362

64-
expect(json_message.to_h).to have_keys(*headers.keys)
63+
expect(message.to_h).to have_keys(*headers.keys)
6564
expect(connection.read).to be_nil
6665
expect(connection).to be(:closed?)
6766
ensure

0 commit comments

Comments
 (0)
0