Skip to content

Commit

Permalink
Merge 8bcb688 into 581cbb1
Browse files Browse the repository at this point in the history
  • Loading branch information
szmarczak committed Sep 28, 2019
2 parents 581cbb1 + 8bcb688 commit ce33681
Show file tree
Hide file tree
Showing 14 changed files with 1,230 additions and 341 deletions.
170 changes: 124 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@ I recommend adapting to the [`http2`](https://nodejs.org/api/http2.html) module
> `$ npm install http2-wrapper`<br>
> `$ yarn add http2-wrapper`
It's best to run `http2-wrapper` under **the latest** version of Node. It provides the best stability.
It's best to run `http2-wrapper` under [**the latest**](https://nodejs.org/en/download/current/) version of Node. It provides the best stability.

## Usage
```js
'use strict';
const http2 = require('http2-wrapper');

const options = {
Expand Down Expand Up @@ -52,32 +51,34 @@ request.write('123');
request.end('456');

// statusCode: 200
// headers: { ':status': 200,
// date: 'Sat, 11 Aug 2018 09:37:41 GMT',
// headers: [Object: null prototype] {
// ':status': 200,
// date: 'Fri, 27 Sep 2019 19:45:46 GMT',
// 'content-type': 'application/json',
// 'content-length': '264',
// 'access-control-allow-origin': '*',
// 'access-control-allow-credentials': 'true',
// 'x-backend-header-rtt': '0.002997',
// 'content-length': '239',
// 'x-backend-header-rtt': '0.002516',
// 'strict-transport-security': 'max-age=31536000',
// server: 'nghttpx',
// via: '1.1 nghttpx',
// 'alt-svc': 'h3-23=":4433"; ma=3600',
// 'x-frame-options': 'SAMEORIGIN',
// 'x-xss-protection': '1; mode=block',
// 'x-content-type-options': 'nosniff' }
// 'x-content-type-options': 'nosniff'
// }
// body: {
// "args": {},
// "data": "123456",
// "files": {},
// "form": {},
// "headers": {
// "Content-Length": "6",
// "Host": "nghttp2.org:443",
// "Via": "2 nghttpx"
// "Host": "nghttp2.org"
// },
// "json": 123456,
// "origin": "xxx.xxx.xxx.xxx",
// "url": "https://nghttp2.org:443/httpbin/post"
// "url": "https://nghttp2.org/httpbin/post"
// }
```

Expand All @@ -93,7 +94,6 @@ Returns a Promise giving proper `ClientRequest` instance (depending on the ALPN)
**Tip**: the `agent` option also accepts an object with `http`, `https` and `http2` properties.

```js
'use strict';
const http2 = require('http2-wrapper');

const options = {
Expand Down Expand Up @@ -164,18 +164,25 @@ Returns a Promise giving proper request function depending on the ALPN protocol.

### http2.auto.resolveALPN(options)

Resolves ALPN using HTTP options.
Returns a Promise giving the best ALPN protocol possible. It can be either `h2` or `http/1.1`.

### http2.auto.protocolCache

An instance of [`quick-lru`](https://github.com/sindresorhus/quick-lru) used for caching ALPN.
An instance of [`quick-lru`](https://github.com/sindresorhus/quick-lru) used for ALPN cache.

There is a maximum of 100 entries. You can modify the limit through `protocolCache.maxSize` - note that the change will be visible globally.

### http2.request(url, options, callback)

Same as [`https.request`](https://nodejs.org/api/https.html#https_https_request_options_callback).

##### options.preconnect

Type: `boolean`<br>
Default: `true`

If set to `true`, it will try to connect to the server before sending the request.

### http2.get(url, options, callback)

Same as [`https.get`](https://nodejs.org/api/https.html#https_https_get_options_callback).
Expand All @@ -195,7 +202,6 @@ Same as [`https.IncomingMessage`](https://nodejs.org/api/https.html#https_class_
Usage example:

```js
'use strict';
const http2 = require('http2-wrapper');

class MyAgent extends http2.Agent {
Expand All @@ -222,96 +228,168 @@ Each option is assigned to each `Agent` instance and can be changed later.
Type: `number`<br>
Default: `60000`

If there's no activity in given time (milliseconds), the session is closed.
If there's no activity after `timeout` milliseconds, the session will be closed.

##### maxSessions

Type: `number`<br>
Default: `Infinity`

Max sessions per origin.
The maximum amount of sessions per origin.

##### maxFreeSessions

Type: `number`<br>
Default: `1`

Max free sessions per origin.
The maximum amount of free sessions per origin.

##### maxCachedTlsSessions

Type: `number`<br>
Default: `100`

The maximum amount of cached TLS sessions.

#### agent.getName(authority, options)
#### Agent.normalizeAuthority([authority](#authority), servername)

Returns a `string` containing a proper name for sessions created with these options.
Normalizes the authority URL.

#### agent.getSession(authority, options, name)
```js
Agent.normalizeAuthority('https://example.com:443');
// => 'https://example.com'
```

Returns a Promise giving free `Http2Session` under the provided name. If no free sessions are found, a new one is created.
#### Agent.normalizeOptions([options](https://github.com/szmarczak/http2-wrapper/blob/master/source/agent.js))

If the `name` argument is `undefined`, it defaults to `agent.getName(authority, options)`.
Returns a string containing normalized options.

##### authority
```js
Agent.normalizeOptions({servername: 'example.com'});
// => ':example.com'
```

Type: `string` `URL` `Object`
#### agent.settings

Type: `object`<br>
Default: `{enablePush: false}`

[Settings](https://nodejs.org/api/http2.html#http2_settings_object) used by the current agent instance.

#### agent.getSession(authority, options)

##### [authority](https://nodejs.org/api/http2.html#http2_http2_connect_authority_options_listener)

Type: `string` `URL` `object`

Authority used to create a new session.

##### options
##### [options](https://nodejs.org/api/http2.html#http2_http2_connect_authority_options_listener)

Type: `Object`
Type: `object`

Options used to create a new session.

#### agent.request(authority, options, headers)
Returns a Promise giving free `Http2Session`. If no free sessions are found, a new one is created.

#### agent.getSession([authority](#authority), [options](options-1), listener)

##### listener

Type: `object`

```
{
reject: error => void,
resolve: session => void
}
```

If the `listener` argument is present, the Promise will resolve immediately. It will use the `resolve` function to pass the session.

#### agent.request([authority](#authority), [options](#options-1), [headers](https://nodejs.org/api/http2.html#http2_headers_object))

Returns a Promise giving `Http2Stream`.

#### agent.createConnection(authority, options)
#### agent.createConnection([authority](#authority), [options](#options-1))

Returns a new `TLSSocket`. It defaults to `Agent.connect(authority, options)`.

#### agent.closeFreeSessions()

Makes an attempt to close free sessions. Only sessions with no concurrent streams are closed.
Makes an attempt to close free sessions. Only sessions with 0 concurrent streams are closed.

#### agent.destroy(reason)

Destroys **all** sessions.

#### Event: 'session'

```js
agent.on('session', session => {
// A new session has been created.
});
```

#### Event: 'close'

```js
agent.on('close', session => {
// A session has been closed.
});
```

#### Event: 'free'

```js
agent.on('free', session => {
// The session became free.
});
```

#### Event: 'busy'

```js
agent.on('busy', session => {
// The session became busy.
});
```

## Notes

- [WebSockets over HTTP2 is not supported yet](https://github.com/nodejs/node/issues/15230), although there is [a proposal](https://tools.ietf.org/html/rfc8441) already.
- If you're interested in [WebSockets over HTTP2](https://tools.ietf.org/html/rfc8441), then [check out this discussion](https://github.com/websockets/ws/issues/1458).
- [HTTP2 sockets cannot be malformed](https://github.com/nodejs/node/blob/cc8250fab86486632fdeb63892be735d7628cd13/lib/internal/http2/core.js#L725), therefore modifying the socket will have no effect.
- HTTP2 is a binary protocol. Headers are sent without any validation.
- You can make [a custom Agent](examples/push-stream/index.js) to support push streams.

## Benchmarks

CPU: Intel i7-7700k<br>
Server: H2O 2.2.5 [`h2o.conf`](h2o.conf)<br>
Node: v12.6.0
Node: v12.10.0

```
http2-wrapper x 11,886 ops/sec ±1.90% (84 runs sampled)
http2-wrapper - preconfigured session x 14,815 ops/sec ±1.58% (87 runs sampled)
http2 x 18,272 ops/sec ±1.76% (80 runs sampled)
http2 - using PassThrough proxies x 15,215 ops/sec ±2.18% (85 runs sampled)
https x 1,613 ops/sec ±4.56% (75 runs sampled)
http x 6,676 ops/sec ±5.17% (78 runs sampled)
http2-wrapper x 9,954 ops/sec ±3.72% (81 runs sampled)
http2-wrapper - preconfigured session x 12,309 ops/sec ±1.48% (87 runs sampled)
http2 x 14,664 ops/sec ±1.63% (78 runs sampled)
http2 - using PassThrough proxies x 11,884 ops/sec ±2.43% (82 runs sampled)
https x 1,586 ops/sec ±4.05% (79 runs sampled)
http x 5,886 ops/sec ±2.73% (76 runs sampled)
Fastest is http2
```

`http2-wrapper`:

- It's `1.537x` slower than `http2`.
- It's `1.280x` slower than `http2` with `PassThrough`.
- It's `7.369x` faster than `https`.
- It's `1.780x` faster than `http`.
- It's `1.473x` slower than `http2`.
- It's `1.194x` slower than `http2` with `2xPassThrough`.
- It's `6.276x` faster than `https`.
- It's `1.691x` faster than `http`.

`http2-wrapper - preconfigured session`:

- It's `1.233x` slower than `http2`.
- It's `1.027x` slower than `http2` with `PassThrough`.
- It's `9.185x` faster than `https`.
- It's `2.219x` faster than `http`.
- It's `1.191x` slower than `http2`.
- It's `1.036x` faster than `http2` with `2xPassThrough`.
- It's `7.761x` faster than `https`.
- It's `2.091x` faster than `http`.

## Related

Expand Down
50 changes: 31 additions & 19 deletions examples/push-stream/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class PushAgent extends http2.Agent {
session.pushCache = pushCache;

session.on('stream', (stream, requestHeaders) => {
const parsedPushHeaders = PushAgent._parsePushHeaders(requestHeaders);
const parsedPushHeaders = PushAgent._parsePushHeaders(undefined, requestHeaders);

if (pushCache.has(parsedPushHeaders)) {
stream.close(http2.constants.NGHTTP2_REFUSED_STREAM);
Expand All @@ -27,43 +27,55 @@ class PushAgent extends http2.Agent {
});
}

async request(authority, options, headers) {
const session = await this.getSession(authority, options);
request(authority, options, headers) {
return new Promise((resolve, reject) => {
// The code after `await agent.getSession()` isn't executed immediately after calling `resolve()`,
// so we need to use semi-callback style to support the `maxFreeSessions` option mechanism.

const parsedPushHeaders = PushAgent._parsePushHeaders(headers);
const cache = session.pushCache.get(parsedPushHeaders);
if (cache) {
const {stream, pushHeaders} = cache;
delete session.pushCache.delete(parsedPushHeaders);
// For further information please see the source code of the `processListeners` function (`source/agent.js` file).

setImmediate(() => {
stream.emit('response', pushHeaders);
});
this.getSession(authority, options, [{
reject,
resolve: session => {
const normalizedAuthority = http2.Agent.normalizeAuthority(authority, options.servername);

return stream;
}
const parsedPushHeaders = PushAgent._parsePushHeaders(normalizedAuthority, headers);
const cache = session.pushCache.get(parsedPushHeaders);
if (cache) {
const {stream, pushHeaders} = cache;
delete session.pushCache.delete(parsedPushHeaders);

const stream = session.request(headers);
setImmediate(() => {
stream.emit('response', pushHeaders);
});

return stream;
}
resolve(stream);
return;
}

static _parsePushHeaders(headers) {
// TODO: headers[':authority'] needs to be verified properly.
resolve(session.request(headers));
}
}]);
});
}

static _parsePushHeaders(authority, headers) {
return [
headers[':authority'] || authority,
headers[':path'] || '/',
headers[':method'] || 'GET'
];
}
}

(async () => {
const agent = new PushAgent();

const got = gotExtend({
baseUrl: 'https://localhost:3000',
request: http2.request,
rejectUnauthorized: false,
agent: new PushAgent()
agent
});

const response = await got('');
Expand Down

0 comments on commit ce33681

Please sign in to comment.