Skip to content

Commit 4032346

Browse files
committed
Add experimental support for HTTP/2 built on top of curb/curl/nghttp2
1 parent f72ef6d commit 4032346

File tree

5 files changed

+117
-1
lines changed

5 files changed

+117
-1
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ gem 'oven', '0.1.0.rc1'
77
gem 'rubocop'
88
gem 'pry'
99
gem 'http_logger'
10+
gem 'curb'

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,31 @@ payload = {
6868
response = client.push(payload) # => sends a message to the topic
6969
```
7070

71+
## Using HTTP/2 (Exmerimental)
72+
73+
The current GitHub master branch ships with experimental support for HTTP/2. It takes advantage of the fantastic library, [libcurl](https://curl.haxx.se/libcurl/). In order to use it, replace `Android.new(...)` with `Android.http2(...)`:
74+
75+
```diff
76+
+# Do not forget to add the curb gem to your Gemfile
77+
+require 'curb'
78+
79+
-client = Andpush.new(server_key, pool_size: 25)
80+
+client = Andpush.http2(server_key) # no need to specify the `pool_size' as HTTP/2 maintains a single connection
81+
```
82+
83+
### Prerequisites
84+
85+
* [libcurl](https://curl.haxx.se/download.html) 7.43.0 or later
86+
* [nghttp2](https://nghttp2.org/blog/) 1.0 or later
87+
88+
**Make sure that your production environment has the compatible versions installed. If you are not sure what version of libcurl you are using, try running `curl --version` and make sure it has `HTTP2` listed in the Features:**
89+
90+
![Curl version](assets/curl-version.png "Curl version")
91+
92+
**If you wish to use the HTTP/2 client in heroku, make sure you are using [the `Heroku-18` stack](https://devcenter.heroku.com/articles/heroku-18-stack). Older stacks, such as `Heroku-16` and `Cedar-14` do not ship with a version of libcurl that has support for HTTP/2.**
93+
94+
If you are using an older version of libcurl that doesn't support HTTP/2, don't worry. It will just fall back to HTTP 1.1 (of course without header compression and multiplexing.)
95+
7196
## Performance
7297

7398
The andpush gem uses [HTTP persistent connections](https://en.wikipedia.org/wiki/HTTP_persistent_connection) to improve performance. This is done by [the net-http-persistent gem](https://github.com/drbrain/net-http-persistent). [A simple benchmark](https://gist.github.com/yuki24/e0db97e887b8b6eb1932c41b4cea4a99) shows that the andpush gem performs at least 3x faster than the fcm gem:

assets/curl-version.png

123 KB
Loading

lib/andpush.rb

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ def build(server_key, domain: nil, name: nil, proxy: nil, pool_size: Net::HTTP::
1313
.register_interceptor(Authenticator.new(server_key))
1414
end
1515
alias new build
16+
17+
def http2(server_key, domain: nil)
18+
begin
19+
require 'curb' if !defined?(Curl)
20+
rescue LoadError => error
21+
raise LoadError, "Could not load the curb gem. Make sure to install the gem by running:\n\n" \
22+
" $ gem i curb\n\n" \
23+
"Or the Gemfile has the following declaration:\n\n" \
24+
" gem 'curb'\n\n" \
25+
" (#{error.class}: #{error.message})"
26+
end
27+
28+
::Andpush::Client
29+
.new(domain || DOMAIN, request_handler: Http2RequestHandler.new)
30+
.register_interceptor(Authenticator.new(server_key))
31+
end
1632
end
1733

1834
class Authenticator
@@ -42,5 +58,47 @@ def call(request_class, uri, headers, body, *_)
4258
end
4359
end
4460

45-
private_constant :Authenticator, :ConnectionPool
61+
class Http2RequestHandler
62+
BY_HEADER_LINE = /[\r\n]+/.freeze
63+
HEADER_VALUE = /^(\S+): (.+)/.freeze
64+
65+
attr_reader :multi
66+
67+
def initialize(max_connects: 100)
68+
@multi = Curl::Multi.new
69+
70+
@multi.pipeline = Curl::CURLPIPE_MULTIPLEX
71+
@multi.max_connects = max_connects
72+
end
73+
74+
def call(request_class, uri, headers, body, *_)
75+
easy = Curl::Easy.new(uri.to_s)
76+
77+
# This ensures libcurl waits for the connection to reveal if it is
78+
# possible to pipeline/multiplex on before it continues.
79+
easy.setopt(Curl::CURLOPT_PIPEWAIT, 1)
80+
81+
easy.multi = @multi
82+
easy.version = Curl::HTTP_2_0
83+
easy.headers = headers || {}
84+
easy.post_body = body if request_class::REQUEST_HAS_BODY
85+
86+
easy.public_send(:"http_#{request_class::METHOD.downcase}")
87+
88+
Response.new(
89+
Hash[easy.header_str.split(BY_HEADER_LINE).flat_map {|s| s.scan(HEADER_VALUE) }],
90+
easy.body,
91+
easy.response_code.to_s, # to_s for comatibility with Net::HTTP
92+
easy,
93+
).freeze
94+
end
95+
96+
Response = Struct.new(:headers, :body, :code, :raw_response) do
97+
alias to_hash headers
98+
end
99+
100+
private_constant :BY_HEADER_LINE, :HEADER_VALUE, :Response
101+
end
102+
103+
private_constant :Authenticator, :ConnectionPool, :Http2RequestHandler
46104
end

test/andpush_test.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,36 @@ def test_it_makes_http_request_to_fcm
3030
assert_equal 0, json[:canonical_ids]
3131
assert_equal "fake_message_id", json[:results][0][:message_id]
3232
end
33+
34+
def test_http2_client_makes_http2_request_to_fcm
35+
server_key = ENV.fetch('FCM_TEST_SERVER_KEY')
36+
device_token = ENV.fetch('FCM_TEST_REGISTRATION_TOKEN')
37+
38+
client = Andpush.http2(server_key)
39+
json = {
40+
to: device_token,
41+
dry_run: true,
42+
notification: {
43+
title: "Update",
44+
body: "Your weekly summary is ready"
45+
},
46+
data: {
47+
extra: "data"
48+
}
49+
}
50+
51+
response = client.push(json)
52+
53+
assert_equal '200', response.code
54+
55+
json = response.json
56+
57+
assert_equal(-1, json[:multicast_id])
58+
assert_equal 1, json[:success]
59+
assert_equal 0, json[:failure]
60+
assert_equal 0, json[:canonical_ids]
61+
assert_equal "fake_message_id", json[:results][0][:message_id]
62+
63+
assert_match "HTTP/2", response.raw_response.header_str
64+
end
3365
end

0 commit comments

Comments
 (0)