From 14fda86944ca5bce9d5f090e22dd1e3ab67d8b68 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Mon, 13 Apr 2020 12:30:46 +0200 Subject: [PATCH] Improvements to the Jetty server documentation. Written HTTP/2 low-level section. Signed-off-by: Simone Bordet --- .../client/http2/client-http2.adoc | 118 +------ .../main/asciidoc/embedded-guide/http2.adoc | 118 +++++++ .../server/http2/server-http2.adoc | 170 +++++++++- .../src/main/java/embedded/HTTP2Docs.java | 97 ++++++ .../client/http2/HTTP2ClientDocs.java | 60 +--- .../server/http2/HTTP2ServerDocs.java | 316 ++++++++++++++++++ .../org/eclipse/jetty/http2/api/Stream.java | 33 +- .../RawHTTP2ServerConnectionFactory.java | 5 + 8 files changed, 753 insertions(+), 164 deletions(-) create mode 100644 jetty-documentation/src/main/asciidoc/embedded-guide/http2.adoc create mode 100644 jetty-documentation/src/main/java/embedded/HTTP2Docs.java create mode 100644 jetty-documentation/src/main/java/embedded/server/http2/HTTP2ServerDocs.java diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/client/http2/client-http2.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http2/client-http2.adoc index e7c48438eaa..8cdc48187e6 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/client/http2/client-http2.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http2/client-http2.adoc @@ -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]. diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/http2.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/http2.adoc new file mode 100644 index 00000000000..b77a1768235 --- /dev/null +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/http2.adoc @@ -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[] diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/server/http2/server-http2.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/server/http2/server-http2.adoc index c6728c6ef0e..470ac968793 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/server/http2/server-http2.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/server/http2/server-http2.adoc @@ -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] +---- + + org.eclipse.jetty.http2 + http2-server + {version} + +---- + +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] +---- diff --git a/jetty-documentation/src/main/java/embedded/HTTP2Docs.java b/jetty-documentation/src/main/java/embedded/HTTP2Docs.java new file mode 100644 index 00000000000..cd32db6cd05 --- /dev/null +++ b/jetty-documentation/src/main/java/embedded/HTTP2Docs.java @@ -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 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 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[] + } +} diff --git a/jetty-documentation/src/main/java/embedded/client/http2/HTTP2ClientDocs.java b/jetty-documentation/src/main/java/embedded/client/http2/HTTP2ClientDocs.java index c5abbf7eacb..f15637ad8e5 100644 --- a/jetty-documentation/src/main/java/embedded/client/http2/HTTP2ClientDocs.java +++ b/jetty-documentation/src/main/java/embedded/client/http2/HTTP2ClientDocs.java @@ -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 dataCF1 = stream.data(new DataFrame(stream.getId(), buffer1, false)); + CompletableFuture 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 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 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(); diff --git a/jetty-documentation/src/main/java/embedded/server/http2/HTTP2ServerDocs.java b/jetty-documentation/src/main/java/embedded/server/http2/HTTP2ServerDocs.java new file mode 100644 index 00000000000..5b6658f76a3 --- /dev/null +++ b/jetty-documentation/src/main/java/embedded/server/http2/HTTP2ServerDocs.java @@ -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 onPreface(Session session) + { + // Customize the settings, for example: + Map 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 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[] + } +} diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java index ae48a46159a..7cd70196c89 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java @@ -52,6 +52,19 @@ public interface Stream */ public Session getSession(); + /** + *

Sends the given HEADERS {@code frame} representing an HTTP response.

+ * + * @param frame the HEADERS frame to send + * @return the CompletableFuture that gets notified when the frame has been sent + */ + public default CompletableFuture headers(HeadersFrame frame) + { + Promise.Completable result = new Promise.Completable<>(); + headers(frame, Callback.from(() -> result.succeeded(this), result::failed)); + return result; + } + /** *

Sends the given HEADERS {@code frame} representing an HTTP response.

* @@ -60,6 +73,20 @@ public interface Stream */ public void headers(HeadersFrame frame, Callback callback); + /** + *

Sends the given PUSH_PROMISE {@code frame}.

+ * + * @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 push(PushPromiseFrame frame, Listener listener) + { + Promise.Completable result = new Promise.Completable<>(); + push(frame, result, listener); + return result; + } + /** *

Sends the given PUSH_PROMISE {@code frame}.

* @@ -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 data(DataFrame frame) + public default CompletableFuture data(DataFrame frame) { - Callback.Completable result = new Callback.Completable(); - data(frame, result); + Promise.Completable result = new Promise.Completable<>(); + data(frame, Callback.from(() -> result.succeeded(this), result::failed)); return result; } diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/RawHTTP2ServerConnectionFactory.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/RawHTTP2ServerConnectionFactory.java index 6746c42516e..c4586d17ab9 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/RawHTTP2ServerConnectionFactory.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/RawHTTP2ServerConnectionFactory.java @@ -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);