Improvements to the Jetty server documentation.

Written HTTP/2 low-level section.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2020-04-13 12:30:46 +02:00
parent 34a2d6e673
commit 14fda86944
8 changed files with 753 additions and 164 deletions

View File

@ -33,6 +33,8 @@ The HTTP/2 client library has been designed for those applications that need
low-level access to HTTP/2 features such as _sessions_, _streams_ and
_frames_, and this is quite a rare use case.
See also the correspondent xref:eg-server-http2[HTTP/2 server library].
[[eg-client-http2-intro]]
==== Introducing HTTP2Client
@ -63,79 +65,24 @@ it should stop the `HTTP2Client` instance (or instances) that were started:
include::../../{doc_code}/embedded/client/http2/HTTP2ClientDocs.java[tags=stop]
----
`HTTP2Client` allows client applications to connect to a HTTP/2 server.
A _session_ represents a single TCP connection to a HTTP/2 server and is defined
by class `org.eclipse.jetty.http2.api.Session`.
`HTTP2Client` allows client applications to connect to an HTTP/2 server.
A _session_ represents a single TCP connection to an HTTP/2 server and is
defined by class `org.eclipse.jetty.http2.api.Session`.
A _session_ typically has a long life - once the TCP connection is established,
it remains open until it is not used anymore (and therefore it is closed by
the idle timeout mechanism), until a fatal error occurs (for example, a network
failure), or if one of the peers decides unilaterally to close the TCP
connection.
HTTP/2 is a multiplexed protocol: it allows multiple HTTP/2 requests to be sent
on the same TCP connection.
Each request/response cycle is represented by a _stream_.
Therefore, a single _session_ manages multiple concurrent _streams_.
A _stream_ has typically a very short life compared to the _session_: a
_stream_ only exists for the duration of the request/response cycle and then
disappears.
include::../../http2.adoc[tag=multiplex]
[[eg-client-http2-flow-control]]
===== HTTP/2 Flow Control
The HTTP/2 protocol is _flow controlled_ (see
link:https://tools.ietf.org/html/rfc7540#section-5.2[the specification]).
This means that a sender and a receiver maintain a _flow control window_ that
tracks the number of data bytes sent and received, respectively.
When a sender sends data bytes, it reduces its flow control window. When a
receiver receives data bytes, it also reduces its flow control window, and
then passes the received data bytes to the application.
The application consumes the data bytes and tells back the receiver that it
has consumed the data bytes.
The receiver then enlarges the flow control window, and arranges to send a
message to the sender with the number of bytes consumed, so that the sender
can enlarge its flow control window.
A sender can send data bytes up to its whole flow control window, then it must
stop sending until it receives a message from the receiver that the data bytes
have been consumed, which enlarges the flow control window, which allows the
sender to send more data bytes.
HTTP/2 defines _two_ flow control windows: one for each _session_, and one
for each _stream_. Let's see with an example how they interact, assuming that
in this example the session flow control window is 120 bytes and the stream
flow control window is 100 bytes.
The sender opens a session, and then opens `stream_1` on that session, and
sends `80` data bytes.
At this point the session flow control window is `40` bytes (`120 - 80`), and
``stream_1``'s flow control window is `20` bytes (`100 - 80`).
The sender now opens `stream_2` on the same session and sends `40` data bytes.
At this point, the session flow control window is `0` bytes (`40 - 40`),
while ``stream_2``'s flow control window is `60` (`100 - 40`).
Since now the session flow control window is `0`, the sender cannot send more
data bytes, neither on `stream_1` nor on `stream_2` despite both have their
stream flow control windows greater than `0`.
The receiver consumes ``stream_2``'s `40` data bytes and sends a message to
the sender with this information.
At this point, the session flow control window is `40` (`0 + 40`),
``stream_1``'s flow control window is still `20` and ``stream_2``'s flow
control window is `100` (`60 + 40`).
If the sender opens `stream_3` and would like to send 50 data bytes, it would
only be able to send `40` because that is the maximum allowed by the session
flow control window at this point.
It is therefore very important that applications notify the fact that they
have consumed data bytes as soon as possible, so that the implementation
(the receiver) can send a message to the sender (in the form of a
`WINDOW_UPDATE` frame) with the information to enlarge the flow control
window, therefore reducing the possibility that sender stalls due to the flow
control windows being reduced to `0`.
This is discussed in details in xref:eg-client-http2-response[this section].
include::../../http2.adoc[tag=flowControl]
How a client application should handle HTTP/2 flow control is discussed in
details in xref:eg-client-http2-response[this section].
[[eg-client-http2-connect]]
==== Connecting to the Server
@ -192,7 +139,7 @@ Sending an HTTP request to the server, and receiving a response, creates a
_stream_ that encapsulates the exchange of HTTP/2 frames that compose the
request and the response.
In order to send a HTTP request to the server, the client must send a
In order to send an HTTP request to the server, the client must send a
`HEADERS` frame.
`HEADERS` frames carry the request method, the request URI and the request
headers.
@ -230,9 +177,9 @@ Use the `Callback` APIs or `CompletableFuture` APIs to ensure that the second
Response events are delivered to the `Stream.Listener` passed to
`Session.newStream(...)`.
A HTTP response is typically composed of a `HEADERS` frame containing the HTTP
status code and the response headers, and optionally one or more `DATA` frames
containing the response content bytes.
An HTTP response is typically composed of a `HEADERS` frame containing the
HTTP status code and the response headers, and optionally one or more `DATA`
frames containing the response content bytes.
The HTTP/2 protocol also supports response trailers (that is, headers that are
sent after the response content) that also are sent using a `HEADERS` frame.
@ -245,38 +192,7 @@ by implementing the relevant methods in `Stream.Listener`:
include::../../{doc_code}/embedded/client/http2/HTTP2ClientDocs.java[tags=responseListener]
----
NOTE: Returning from the `onData(...)` method implicitly demands for
more `DATA` frames (unless the one just delivered was the last).
Additional `DATA` frames may be delivered immediately if they are available
or later, asynchronously, when they arrive.
Client applications that consume the content buffer within `onData(...)`
(for example, writing it to a file, or copying the bytes to another storage)
should succeed the callback as soon as they have consumed the content buffer.
This allows the implementation to reuse the buffer, reducing the memory
requirements needed to handle the response content.
Alternatively, a client application may store away _both_ the buffer and the
callback to consume the buffer bytes later.
IMPORTANT: Completing the `Callback` is very important not only to allow the
implementation to reuse the buffer, but also tells the implementation to
enlarge the stream and session flow control windows so that the server will
be able to send more `DATA` frames without stalling.
Client applications can also precisely control _when_ to demand more `DATA`
frames, by implementing the `onDataDemanded(...)` method instead of
`onData(...)`:
[source,java,indent=0]
----
include::../../{doc_code}/embedded/client/http2/HTTP2ClientDocs.java[tags=responseDataDemanded]
----
IMPORTANT: Applications that implement `onDataDemanded(...)` must remember
to call `Stream.demand(...)`. If they don't, the implementation will not
deliver `DATA` frames and the application will stall threadlessly until an
idle timeout fires to close the stream or the session.
include::../../http2.adoc[tag=apiFlowControl]
[[eg-client-http2-reset]]
==== Resetting a Request or Response
@ -298,9 +214,9 @@ include::../../{doc_code}/embedded/client/http2/HTTP2ClientDocs.java[tags=reset]
HTTP/2 servers have the ability to push resources related to a primary
resource.
When a HTTP/2 server pushes a resource, it send to the client a `PUSH_PROMISE`
frame that contains the request URI and headers that a client would use to
request explicitly that resource.
When an HTTP/2 server pushes a resource, it sends to the client a
`PUSH_PROMISE` frame that contains the request URI and headers that a client
would use to request explicitly that resource.
Client applications can be configured to tell the server to never push
resources, see xref:eg-client-http2-configure[this section].

View File

@ -0,0 +1,118 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
// Snippets of HTTP/2 documentation that are common between client and server.
tag::multiplex[]
HTTP/2 is a multiplexed protocol: it allows multiple HTTP/2 requests to be sent
on the same TCP connection.
Each request/response cycle is represented by a _stream_.
Therefore, a single _session_ manages multiple concurrent _streams_.
A _stream_ has typically a very short life compared to the _session_: a
_stream_ only exists for the duration of the request/response cycle and then
disappears.
end::multiplex[]
tag::flowControl[]
The HTTP/2 protocol is _flow controlled_ (see
link:https://tools.ietf.org/html/rfc7540#section-5.2[the specification]).
This means that a sender and a receiver maintain a _flow control window_ that
tracks the number of data bytes sent and received, respectively.
When a sender sends data bytes, it reduces its flow control window. When a
receiver receives data bytes, it also reduces its flow control window, and
then passes the received data bytes to the application.
The application consumes the data bytes and tells back the receiver that it
has consumed the data bytes.
The receiver then enlarges the flow control window, and arranges to send a
message to the sender with the number of bytes consumed, so that the sender
can enlarge its flow control window.
A sender can send data bytes up to its whole flow control window, then it must
stop sending until it receives a message from the receiver that the data bytes
have been consumed, which enlarges the flow control window, which allows the
sender to send more data bytes.
HTTP/2 defines _two_ flow control windows: one for each _session_, and one
for each _stream_. Let's see with an example how they interact, assuming that
in this example the session flow control window is 120 bytes and the stream
flow control window is 100 bytes.
The sender opens a session, and then opens `stream_1` on that session, and
sends `80` data bytes.
At this point the session flow control window is `40` bytes (`120 - 80`), and
``stream_1``'s flow control window is `20` bytes (`100 - 80`).
The sender now opens `stream_2` on the same session and sends `40` data bytes.
At this point, the session flow control window is `0` bytes (`40 - 40`),
while ``stream_2``'s flow control window is `60` (`100 - 40`).
Since now the session flow control window is `0`, the sender cannot send more
data bytes, neither on `stream_1` nor on `stream_2` despite both have their
stream flow control windows greater than `0`.
The receiver consumes ``stream_2``'s `40` data bytes and sends a message to
the sender with this information.
At this point, the session flow control window is `40` (`0 + 40`),
``stream_1``'s flow control window is still `20` and ``stream_2``'s flow
control window is `100` (`60 + 40`).
If the sender opens `stream_3` and would like to send 50 data bytes, it would
only be able to send `40` because that is the maximum allowed by the session
flow control window at this point.
It is therefore very important that applications notify the fact that they
have consumed data bytes as soon as possible, so that the implementation
(the receiver) can send a message to the sender (in the form of a
`WINDOW_UPDATE` frame) with the information to enlarge the flow control
window, therefore reducing the possibility that sender stalls due to the flow
control windows being reduced to `0`.
end::flowControl[]
tag::apiFlowControl[]
NOTE: Returning from the `onData(...)` method implicitly demands for
more `DATA` frames (unless the one just delivered was the last).
Additional `DATA` frames may be delivered immediately if they are available
or later, asynchronously, when they arrive.
Applications that consume the content buffer within `onData(...)`
(for example, writing it to a file, or copying the bytes to another storage)
should succeed the callback as soon as they have consumed the content buffer.
This allows the implementation to reuse the buffer, reducing the memory
requirements needed to handle the content buffers.
Alternatively, a client application may store away _both_ the buffer and the
callback to consume the buffer bytes later, or pass _both_ the buffer and
the callback to another asynchronous API (this is typical in proxy
applications).
IMPORTANT: Completing the `Callback` is very important not only to allow the
implementation to reuse the buffer, but also tells the implementation to
enlarge the stream and session flow control windows so that the sender will
be able to send more `DATA` frames without stalling.
Applications can also precisely control _when_ to demand more `DATA`
frames, by implementing the `onDataDemanded(...)` method instead of
`onData(...)`:
[source,java,indent=0]
----
include::{doc_code}/embedded/HTTP2Docs.java[tags=dataDemanded]
----
IMPORTANT: Applications that implement `onDataDemanded(...)` must remember
to call `Stream.demand(...)`. If they don't, the implementation will not
deliver `DATA` frames and the application will stall threadlessly until an
idle timeout fires to close the stream or the session.
end::apiFlowControl[]

View File

@ -17,6 +17,172 @@
//
[[eg-server-http2]]
=== HTTP/2 Server Libraries
=== HTTP/2 Server Library
TODO
In the vast majority of cases, server applications should use the generic,
high-level, xref:eg-server-http[HTTP server library] that also provides
HTTP/2 support via the HTTP/2 ``ConnectionFactory``s as described in details
xref:eg-server-http-connector-protocol-http2[here].
The low-level HTTP/2 server library has been designed for those applications
that need low-level access to HTTP/2 features such as _sessions_, _streams_
and _frames_, and this is quite a rare use case.
See also the correspondent xref:eg-client-http2[HTTP/2 client library].
[[eg-server-http2-intro]]
==== Introduction
The Maven artifact coordinates for the HTTP/2 client library are the following:
[source,xml,subs=normal]
----
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-server</artifactId>
<version>{version}</version>
</dependency>
----
include::../../http2.adoc[tag=multiplex]
[[eg-server-http2-flow-control]]
===== HTTP/2 Flow Control
include::../../http2.adoc[tag=flowControl]
How a server application should handle HTTP/2 flow control is discussed in
details in xref:eg-server-http2-request[this section].
[[eg-server-http2-setup]]
==== Server Setup
The low-level HTTP/2 support is provided by
`org.eclipse.jetty.http2.server.RawHTTP2ServerConnectionFactory` and
`org.eclipse.jetty.http2.api.server.ServerSessionListener`:
[source,java,indent=0]
----
include::../../{doc_code}/embedded/server/http2/HTTP2ServerDocs.java[tags=setup]
----
Where server applications using the
xref:eg-server-http[high-level server library] deal with HTTP
requests and responses in ``Handler``s, server applications using the
low-level HTTP/2 server library deal directly with HTTP/2 __session__s,
__stream__s and __frame__s in a `ServerSessionListener` implementation.
The `ServerSessionListener` interface defines a number of methods that are
invoked by the implementation upon the occurrence of HTTP/2 events, and that
server applications can override to react to those events.
Please refer to the `ServerSessionListener`
link:{JDURL}/org/eclipse/jetty/http2/api/server/ServerSessionListener.html[javadocs]
for the complete list of events.
The first event is the _accept_ event and happens when a client opens a new
TCP connection to the server and the server accepts the connection.
This is the first occasion where server applications have access to the HTTP/2
`Session` object:
[source,java,indent=0]
----
include::../../{doc_code}/embedded/server/http2/HTTP2ServerDocs.java[tags=accept]
----
After connecting to the server, a compliant HTTP/2 client must send the
link:https://tools.ietf.org/html/rfc7540#section-3.5[HTTP/2 client preface],
and when the server receives it, it generates the _preface_ event on the server.
This is where server applications can customize the connection settings by
returning a map of settings that the implementation will send to the client:
[source,java,indent=0]
----
include::../../{doc_code}/embedded/server/http2/HTTP2ServerDocs.java[tags=preface]
----
[[eg-server-http2-request]]
==== Receiving a Request
Receiving an HTTP request from the client, and sending a response, creates
a _stream_ that encapsulates the exchange of HTTP/2 frames that compose the
request and the response.
An HTTP request is made of a `HEADERS` frame, that carries the request method,
the request URI and the request headers, and optional `DATA` frames that carry
the request content.
Receiving the `HEADERS` frame opens the `Stream`:
[source,java,indent=0]
----
include::../../{doc_code}/embedded/server/http2/HTTP2ServerDocs.java[tags=request]
----
Server applications should return a `Stream.Listener` implementation from
`onNewStream(...)` to be notified of events generated by the client, such as
`DATA` frames carrying request content, or a `RST_STREAM` frame indicating
that the client wants to _reset_ the request, or an idle timeout event
indicating that the client was supposed to send more frames but it did not.
The example below shows how to receive request content:
[source,java,indent=0]
----
include::../../{doc_code}/embedded/server/http2/HTTP2ServerDocs.java[tags=requestContent]
----
include::../../http2.adoc[tag=apiFlowControl]
[[eg-server-http2-response]]
==== Sending a Response
After receiving an HTTP request, a server application must send an HTTP
response.
An HTTP response is typically composed of a `HEADERS` frame containing the
HTTP status code and the response headers, and optionally one or more `DATA`
frames containing the response content bytes.
The HTTP/2 protocol also supports response trailers (that is, headers that
are sent after the response content) that also are sent using a `HEADERS`
frame.
A server application can send a response in this way:
[source,java,indent=0]
----
include::../../{doc_code}/embedded/server/http2/HTTP2ServerDocs.java[tags=response;!exclude]
----
[[eg-server-http2-reset]]
==== Resetting a Request
A server application may decide that it does not want to accept the request.
For example, it may throttle the client because it sent too many requests in
a time window, or the request is invalid (and does not deserve a proper HTTP
response), etc.
A request can be reset in this way:
[source,java,indent=0]
----
include::../../{doc_code}/embedded/server/http2/HTTP2ServerDocs.java[tags=reset;!exclude]
----
[[eg-server-http2-push]]
==== HTTP/2 Push of Resources
A server application may _push_ secondary resources related to a primary
resource.
A client may inform the server that it does not accept pushed resources
(see link:https://tools.ietf.org/html/rfc7540#section-8.2[this section]
of the specification) via a `SETTINGS` frame.
Server applications must track `SETTINGS` frames and verify whether the
client supports HTTP/2 push, and only push if the client supports it:
[source,java,indent=0]
----
include::../../{doc_code}/embedded/server/http2/HTTP2ServerDocs.java[tags=push]
----

View File

@ -0,0 +1,97 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package embedded;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.client.HTTP2Client;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.util.Callback;
@SuppressWarnings("unused")
public class HTTP2Docs
{
public void dataDemanded() throws Exception
{
HTTP2Client http2Client = new HTTP2Client();
http2Client.start();
SocketAddress serverAddress = new InetSocketAddress("localhost", 8080);
CompletableFuture<Session> sessionCF = http2Client.connect(serverAddress, new Session.Listener.Adapter());
Session session = sessionCF.get();
HttpFields requestHeaders = new HttpFields();
requestHeaders.put(HttpHeader.USER_AGENT, "Jetty HTTP2Client {version}");
MetaData.Request request = new MetaData.Request("GET", new HttpURI("http://localhost:8080/path"), HttpVersion.HTTP_2, requestHeaders);
HeadersFrame headersFrame = new HeadersFrame(request, null, true);
// tag::dataDemanded[]
class Chunk
{
private final ByteBuffer buffer;
private final Callback callback;
Chunk(ByteBuffer buffer, Callback callback)
{
this.buffer = buffer;
this.callback = callback;
}
}
// A queue that consumers poll to consume content asynchronously.
Queue<Chunk> dataQueue = new ConcurrentLinkedQueue<>();
// Implementation of Stream.Listener.onDataDemanded(...)
// in case of asynchronous content consumption and demand.
Stream.Listener listener = new Stream.Listener.Adapter()
{
@Override
public void onDataDemanded(Stream stream, DataFrame frame, Callback callback)
{
// Get the content buffer.
ByteBuffer buffer = frame.getData();
// Store buffer to consume it asynchronously, and wrap the callback.
dataQueue.offer(new Chunk(buffer, Callback.from(() ->
{
// When the buffer has been consumed, then:
// A) succeed the nested callback.
callback.succeeded();
// B) demand more DATA frames.
stream.demand(1);
}, callback::failed)));
// Do not demand more content here, to avoid to overflow the queue.
}
};
// end::dataDemanded[]
}
}

View File

@ -24,9 +24,7 @@ import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
@ -199,11 +197,11 @@ public class HTTP2ClientDocs
// Send the first DATA frame on the stream, with endStream=false
// to signal that there are more frames in this stream.
CompletableFuture<Void> dataCF1 = stream.data(new DataFrame(stream.getId(), buffer1, false));
CompletableFuture<Stream> dataCF1 = stream.data(new DataFrame(stream.getId(), buffer1, false));
// Only when the first chunk has been sent we can send the second,
// with endStream=true to signal that there are no more frames.
dataCF1.thenCompose(ignored -> stream.data(new DataFrame(stream.getId(), buffer2, true)));
dataCF1.thenCompose(s -> s.data(new DataFrame(s.getId(), buffer2, true)));
// end::newStreamWithData[]
}
@ -260,60 +258,6 @@ public class HTTP2ClientDocs
// end::responseListener[]
}
public void responseDataDemanded() throws Exception
{
HTTP2Client http2Client = new HTTP2Client();
http2Client.start();
SocketAddress serverAddress = new InetSocketAddress("localhost", 8080);
CompletableFuture<Session> sessionCF = http2Client.connect(serverAddress, new Session.Listener.Adapter());
Session session = sessionCF.get();
HttpFields requestHeaders = new HttpFields();
requestHeaders.put(HttpHeader.USER_AGENT, "Jetty HTTP2Client {version}");
MetaData.Request request = new MetaData.Request("GET", new HttpURI("http://localhost:8080/path"), HttpVersion.HTTP_2, requestHeaders);
HeadersFrame headersFrame = new HeadersFrame(request, null, true);
// tag::responseDataDemanded[]
class Chunk
{
private final ByteBuffer buffer;
private final Callback callback;
Chunk(ByteBuffer buffer, Callback callback)
{
this.buffer = buffer;
this.callback = callback;
}
}
// A queue that consumers poll to consume content asynchronously.
Queue<Chunk> dataQueue = new ConcurrentLinkedQueue<>();
// Open a Stream by sending the HEADERS frame.
session.newStream(headersFrame, new Stream.Listener.Adapter()
{
@Override
public void onDataDemanded(Stream stream, DataFrame frame, Callback callback)
{
// Get the content buffer.
ByteBuffer buffer = frame.getData();
// Store buffer to consume it asynchronously, and wrap the callback.
dataQueue.offer(new Chunk(buffer, Callback.from(() ->
{
// When the buffer has been consumed, then:
// A) succeed the nested callback.
callback.succeeded();
// B) demand more DATA frames.
stream.demand(1);
}, callback::failed)));
// Do not demand more content here, to avoid to overflow the queue.
}
});
// end::responseDataDemanded[]
}
public void reset() throws Exception
{
HTTP2Client http2Client = new HTTP2Client();

View File

@ -0,0 +1,316 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package embedded.server.http2;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.server.RawHTTP2ServerConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.resource.Resource;
import static java.lang.System.Logger.Level.INFO;
@SuppressWarnings("unused")
public class HTTP2ServerDocs
{
public void setup() throws Exception
{
// tag::setup[]
// Create a Server instance.
Server server = new Server();
ServerSessionListener sessionListener = new ServerSessionListener.Adapter();
// Create a ServerConnector with RawHTTP2ServerConnectionFactory.
RawHTTP2ServerConnectionFactory http2 = new RawHTTP2ServerConnectionFactory(sessionListener);
// Configure RawHTTP2ServerConnectionFactory, for example:
// Configure the max number of concurrent requests.
http2.setMaxConcurrentStreams(128);
// Enable support for CONNECT.
http2.setConnectProtocolEnabled(true);
// Create the ServerConnector.
ServerConnector connector = new ServerConnector(server, http2);
// Add the Connector to the Server
server.addConnector(connector);
// Start the Server so it starts accepting connections from clients.
server.start();
// end::setup[]
}
public void accept()
{
// tag::accept[]
ServerSessionListener sessionListener = new ServerSessionListener.Adapter()
{
@Override
public void onAccept(Session session)
{
InetSocketAddress remoteAddress = session.getRemoteAddress();
System.getLogger("http2").log(INFO, "Connection from {0}", remoteAddress);
}
};
// end::accept[]
}
public void preface()
{
// tag::preface[]
ServerSessionListener sessionListener = new ServerSessionListener.Adapter()
{
@Override
public Map<Integer, Integer> onPreface(Session session)
{
// Customize the settings, for example:
Map<Integer, Integer> settings = new HashMap<>();
// Tell the client that HTTP/2 push is disabled.
settings.put(SettingsFrame.ENABLE_PUSH, 0);
return settings;
}
};
// end::preface[]
}
public void request()
{
// tag::request[]
ServerSessionListener sessionListener = new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
// This is the "new stream" event, so it's guaranteed to be a request.
MetaData.Request request = (MetaData.Request)frame.getMetaData();
// Return a Stream.Listener to handle the request events,
// for example request content events or a request reset.
return new Stream.Listener.Adapter();
}
};
// end::request[]
}
public void requestContent()
{
// tag::requestContent[]
ServerSessionListener sessionListener = new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
MetaData.Request request = (MetaData.Request)frame.getMetaData();
// Return a Stream.Listener to handle the request events.
return new Stream.Listener.Adapter()
{
@Override
public void onData(Stream stream, DataFrame frame, Callback callback)
{
// Get the content buffer.
ByteBuffer buffer = frame.getData();
// Consume the buffer, here - as an example - just log it.
System.getLogger("http2").log(INFO, "Consuming buffer {0}", buffer);
// Tell the implementation that the buffer has been consumed.
callback.succeeded();
// By returning from the method, implicitly tell the implementation
// to deliver to this method more DATA frames when they are available.
}
};
}
};
// end::requestContent[]
}
public void response()
{
// tag::response[]
ServerSessionListener sessionListener = new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
// Send a response after reading the request.
MetaData.Request request = (MetaData.Request)frame.getMetaData();
if (frame.isEndStream())
{
respond(stream, request);
return null;
}
else
{
return new Stream.Listener.Adapter()
{
@Override
public void onData(Stream stream, DataFrame frame, Callback callback)
{
// Consume the request content.
callback.succeeded();
if (frame.isEndStream())
respond(stream, request);
}
};
}
}
private void respond(Stream stream, MetaData.Request request)
{
// Prepare the response HEADERS frame.
// The response HTTP status and HTTP headers.
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields());
if (HttpMethod.GET.is(request.getMethod()))
{
// The response content.
ByteBuffer resourceBytes = getResourceBytes(request);
// Send the HEADERS frame with the response status and headers,
// and a DATA frame with the response content bytes.
stream.headers(new HeadersFrame(stream.getId(), response, null, false))
.thenCompose(s -> s.data(new DataFrame(s.getId(), resourceBytes, true)));
}
else
{
// Send just the HEADERS frame with the response status and headers.
stream.headers(new HeadersFrame(stream.getId(), response, null, true));
}
}
// tag::exclude[]
private ByteBuffer getResourceBytes(MetaData.Request request)
{
return ByteBuffer.allocate(1024);
}
// end::exclude[]
};
// end::response[]
}
public void reset()
{
float maxRequestRate = 0F;
// tag::reset[]
ServerSessionListener sessionListener = new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
float requestRate = calculateRequestRate();
if (requestRate > maxRequestRate)
{
stream.reset(new ResetFrame(stream.getId(), ErrorCode.REFUSED_STREAM_ERROR.code), Callback.NOOP);
return null;
}
else
{
// The request is accepted.
MetaData.Request request = (MetaData.Request)frame.getMetaData();
// Return a Stream.Listener to handle the request events.
return new Stream.Listener.Adapter();
}
}
// tag::exclude[]
private float calculateRequestRate()
{
return 0F;
}
// end::exclude[]
};
// end::reset[]
}
public void push() throws Exception
{
// tag::push[]
// The favicon bytes.
ByteBuffer faviconBuffer = BufferUtil.toBuffer(Resource.newResource("/path/to/favicon.ico"), true);
ServerSessionListener sessionListener = new ServerSessionListener.Adapter()
{
// By default, push is enabled.
private boolean pushEnabled = true;
@Override
public void onSettings(Session session, SettingsFrame frame)
{
// Check whether the client sent an ENABLE_PUSH setting.
Map<Integer, Integer> settings = frame.getSettings();
Integer enablePush = settings.get(SettingsFrame.ENABLE_PUSH);
if (enablePush != null)
pushEnabled = enablePush == 1;
}
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
MetaData.Request request = (MetaData.Request)frame.getMetaData();
if (pushEnabled && request.getURIString().endsWith("/index.html"))
{
// Push the favicon.
HttpURI pushedURI = new HttpURI(request.getURI());
pushedURI.setPath("/favicon.ico");
MetaData.Request pushedRequest = new MetaData.Request("GET", pushedURI, HttpVersion.HTTP_2, new HttpFields());
PushPromiseFrame promiseFrame = new PushPromiseFrame(stream.getId(), 0, pushedRequest);
stream.push(promiseFrame, new Stream.Listener.Adapter())
.thenCompose(pushedStream ->
{
// Send the favicon "response".
MetaData.Response pushedResponse = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields());
return pushedStream.headers(new HeadersFrame(pushedStream.getId(), pushedResponse, null, false))
.thenCompose(pushed -> pushed.data(new DataFrame(pushed.getId(), faviconBuffer, true)));
});
}
// Return a Stream.Listener to handle the request events.
return new Stream.Listener.Adapter();
}
};
// end::push[]
}
}

View File

@ -52,6 +52,19 @@ public interface Stream
*/
public Session getSession();
/**
* <p>Sends the given HEADERS {@code frame} representing an HTTP response.</p>
*
* @param frame the HEADERS frame to send
* @return the CompletableFuture that gets notified when the frame has been sent
*/
public default CompletableFuture<Stream> headers(HeadersFrame frame)
{
Promise.Completable<Stream> result = new Promise.Completable<>();
headers(frame, Callback.from(() -> result.succeeded(this), result::failed));
return result;
}
/**
* <p>Sends the given HEADERS {@code frame} representing an HTTP response.</p>
*
@ -60,6 +73,20 @@ public interface Stream
*/
public void headers(HeadersFrame frame, Callback callback);
/**
* <p>Sends the given PUSH_PROMISE {@code frame}.</p>
*
* @param frame the PUSH_PROMISE frame to send
* @param listener the listener that gets notified of stream events
* @return the CompletableFuture that gets notified of the pushed stream creation
*/
public default CompletableFuture<Stream> push(PushPromiseFrame frame, Listener listener)
{
Promise.Completable<Stream> result = new Promise.Completable<>();
push(frame, result, listener);
return result;
}
/**
* <p>Sends the given PUSH_PROMISE {@code frame}.</p>
*
@ -75,10 +102,10 @@ public interface Stream
* @param frame the DATA frame to send
* @return the CompletableFuture that gets notified when the frame has been sent
*/
public default CompletableFuture<Void> data(DataFrame frame)
public default CompletableFuture<Stream> data(DataFrame frame)
{
Callback.Completable result = new Callback.Completable();
data(frame, result);
Promise.Completable<Stream> result = new Promise.Completable<>();
data(frame, Callback.from(() -> result.succeeded(this), result::failed));
return result;
}

View File

@ -37,6 +37,11 @@ public class RawHTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnecti
{
private final ServerSessionListener listener;
public RawHTTP2ServerConnectionFactory(ServerSessionListener listener)
{
this(new HttpConfiguration(), listener);
}
public RawHTTP2ServerConnectionFactory(HttpConfiguration httpConfiguration, ServerSessionListener listener)
{
super(httpConfiguration);