Path: blob/master/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java
67707 views
/*1* Copyright (c) 2015, 2021, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package jdk.internal.net.http;2627import java.io.EOFException;28import java.io.IOException;29import java.io.UncheckedIOException;30import java.net.InetSocketAddress;31import java.net.URI;32import java.nio.ByteBuffer;33import java.nio.charset.StandardCharsets;34import java.util.Iterator;35import java.util.List;36import java.util.Locale;37import java.util.Map;38import java.util.Set;39import java.util.concurrent.CompletableFuture;40import java.util.ArrayList;41import java.util.Objects;42import java.util.concurrent.ConcurrentMap;43import java.util.concurrent.ConcurrentHashMap;44import java.util.concurrent.ConcurrentLinkedQueue;45import java.util.concurrent.Flow;46import java.util.function.Function;47import java.util.function.Supplier;48import javax.net.ssl.SSLEngine;49import javax.net.ssl.SSLException;50import java.net.http.HttpClient;51import java.net.http.HttpHeaders;52import jdk.internal.net.http.HttpConnection.HttpPublisher;53import jdk.internal.net.http.common.FlowTube;54import jdk.internal.net.http.common.FlowTube.TubeSubscriber;55import jdk.internal.net.http.common.HttpHeadersBuilder;56import jdk.internal.net.http.common.Log;57import jdk.internal.net.http.common.Logger;58import jdk.internal.net.http.common.MinimalFuture;59import jdk.internal.net.http.common.SequentialScheduler;60import jdk.internal.net.http.common.Utils;61import jdk.internal.net.http.frame.ContinuationFrame;62import jdk.internal.net.http.frame.DataFrame;63import jdk.internal.net.http.frame.ErrorFrame;64import jdk.internal.net.http.frame.FramesDecoder;65import jdk.internal.net.http.frame.FramesEncoder;66import jdk.internal.net.http.frame.GoAwayFrame;67import jdk.internal.net.http.frame.HeaderFrame;68import jdk.internal.net.http.frame.HeadersFrame;69import jdk.internal.net.http.frame.Http2Frame;70import jdk.internal.net.http.frame.MalformedFrame;71import jdk.internal.net.http.frame.OutgoingHeaders;72import jdk.internal.net.http.frame.PingFrame;73import jdk.internal.net.http.frame.PushPromiseFrame;74import jdk.internal.net.http.frame.ResetFrame;75import jdk.internal.net.http.frame.SettingsFrame;76import jdk.internal.net.http.frame.WindowUpdateFrame;77import jdk.internal.net.http.hpack.Encoder;78import jdk.internal.net.http.hpack.Decoder;79import jdk.internal.net.http.hpack.DecodingCallback;80import static java.nio.charset.StandardCharsets.UTF_8;81import static jdk.internal.net.http.frame.SettingsFrame.*;8283/**84* An Http2Connection. Encapsulates the socket(channel) and any SSLEngine used85* over it. Contains an HttpConnection which hides the SocketChannel SSL stuff.86*87* Http2Connections belong to a Http2ClientImpl, (one of) which belongs88* to a HttpClientImpl.89*90* Creation cases:91* 1) upgraded HTTP/1.1 plain tcp connection92* 2) prior knowledge directly created plain tcp connection93* 3) directly created HTTP/2 SSL connection which uses ALPN.94*95* Sending is done by writing directly to underlying HttpConnection object which96* is operating in async mode. No flow control applies on output at this level97* and all writes are just executed as puts to an output Q belonging to HttpConnection98* Flow control is implemented by HTTP/2 protocol itself.99*100* Hpack header compression101* and outgoing stream creation is also done here, because these operations102* must be synchronized at the socket level. Stream objects send frames simply103* by placing them on the connection's output Queue. sendFrame() is called104* from a higher level (Stream) thread.105*106* asyncReceive(ByteBuffer) is always called from the selector thread. It assembles107* incoming Http2Frames, and directs them to the appropriate Stream.incoming()108* or handles them directly itself. This thread performs hpack decompression109* and incoming stream creation (Server push). Incoming frames destined for a110* stream are provided by calling Stream.incoming().111*/112class Http2Connection {113114final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);115final static Logger DEBUG_LOGGER =116Utils.getDebugLogger("Http2Connection"::toString, Utils.DEBUG);117private final Logger debugHpack =118Utils.getHpackLogger(this::dbgString, Utils.DEBUG_HPACK);119static final ByteBuffer EMPTY_TRIGGER = ByteBuffer.allocate(0);120121static private final int MAX_CLIENT_STREAM_ID = Integer.MAX_VALUE; // 2147483647122static private final int MAX_SERVER_STREAM_ID = Integer.MAX_VALUE - 1; // 2147483646123124/**125* Flag set when no more streams to be opened on this connection.126* Two cases where it is used.127*128* 1. Two connections to the same server were opened concurrently, in which129* case one of them will be put in the cache, and the second will expire130* when all its opened streams (which usually should be a single client131* stream + possibly some additional push-promise server streams) complete.132* 2. A cached connection reaches its maximum number of streams (~ 2^31-1)133* either server / or client allocated, in which case it will be taken134* out of the cache - allowing a new connection to replace it. It will135* expire when all its still open streams (which could be many) eventually136* complete.137*/138private boolean finalStream;139140/*141* ByteBuffer pooling strategy for HTTP/2 protocol.142*143* In general there are 4 points where ByteBuffers are used:144* - incoming/outgoing frames from/to ByteBuffers plus incoming/outgoing145* encrypted data in case of SSL connection.146*147* 1. Outgoing frames encoded to ByteBuffers.148*149* Outgoing ByteBuffers are created with required size and frequently150* small (except DataFrames, etc). At this place no pools at all. All151* outgoing buffers should eventually be collected by GC.152*153* 2. Incoming ByteBuffers (decoded to frames).154*155* Here, total elimination of BB pool is not a good idea.156* We don't know how many bytes we will receive through network.157*158* A possible future improvement ( currently not implemented ):159* Allocate buffers of reasonable size. The following life of the BB:160* - If all frames decoded from the BB are other than DataFrame and161* HeaderFrame (and HeaderFrame subclasses) BB is returned to pool,162* - If a DataFrame is decoded from the BB. In that case DataFrame refers163* to sub-buffer obtained by slice(). Such a BB is never returned to the164* pool and will eventually be GC'ed.165* - If a HeadersFrame is decoded from the BB. Then header decoding is166* performed inside processFrame method and the buffer could be release167* back to pool.168*169* 3. SSL encrypted buffers ( received ).170*171* The current implementation recycles encrypted buffers read from the172* channel. The pool of buffers has a maximum size of 3, SocketTube.MAX_BUFFERS,173* direct buffers which are shared by all connections on a given client.174* The pool is used by all SSL connections - whether HTTP/1.1 or HTTP/2,175* but only for SSL encrypted buffers that circulate between the SocketTube176* Publisher and the SSLFlowDelegate Reader. Limiting the pool to this177* particular segment allows the use of direct buffers, thus avoiding any178* additional copy in the NIO socket channel implementation. See179* HttpClientImpl.SSLDirectBufferSupplier, SocketTube.SSLDirectBufferSource,180* and SSLTube.recycler.181*/182183184// A small class that allows to control frames with respect to the state of185// the connection preface. Any data received before the connection186// preface is sent will be buffered.187private final class FramesController {188volatile boolean prefaceSent;189volatile List<ByteBuffer> pending;190191boolean processReceivedData(FramesDecoder decoder, ByteBuffer buf)192throws IOException193{194// if preface is not sent, buffers data in the pending list195if (!prefaceSent) {196if (debug.on())197debug.log("Preface not sent: buffering %d", buf.remaining());198synchronized (this) {199if (!prefaceSent) {200if (pending == null) pending = new ArrayList<>();201pending.add(buf);202if (debug.on())203debug.log("there are now %d bytes buffered waiting for preface to be sent"204+ Utils.remaining(pending)205);206return false;207}208}209}210211// Preface is sent. Checks for pending data and flush it.212// We rely on this method being called from within the Http2TubeSubscriber213// scheduler, so we know that no other thread could execute this method214// concurrently while we're here.215// This ensures that later incoming buffers will not216// be processed before we have flushed the pending queue.217// No additional synchronization is therefore necessary here.218List<ByteBuffer> pending = this.pending;219this.pending = null;220if (pending != null) {221// flush pending data222if (debug.on()) debug.log(() -> "Processing buffered data: "223+ Utils.remaining(pending));224for (ByteBuffer b : pending) {225decoder.decode(b);226}227}228// push the received buffer to the frames decoder.229if (buf != EMPTY_TRIGGER) {230if (debug.on()) debug.log("Processing %d", buf.remaining());231decoder.decode(buf);232}233return true;234}235236// Mark that the connection preface is sent237void markPrefaceSent() {238assert !prefaceSent;239synchronized (this) {240prefaceSent = true;241}242}243}244245volatile boolean closed;246247//-------------------------------------248final HttpConnection connection;249private final Http2ClientImpl client2;250private final ConcurrentMap<Integer,Stream<?>> streams = new ConcurrentHashMap<>();251private int nextstreamid;252private int nextPushStream = 2;253// actual stream ids are not allocated until the Headers frame is ready254// to be sent. The following two fields are updated as soon as a stream255// is created and assigned to a connection. They are checked before256// assigning a stream to a connection.257private int lastReservedClientStreamid = 1;258private int lastReservedServerStreamid = 0;259private int numReservedClientStreams = 0; // count of current streams260private int numReservedServerStreams = 0; // count of current streams261private final Encoder hpackOut;262private final Decoder hpackIn;263final SettingsFrame clientSettings;264private volatile SettingsFrame serverSettings;265private final String key; // for HttpClientImpl.connections map266private final FramesDecoder framesDecoder;267private final FramesEncoder framesEncoder = new FramesEncoder();268269/**270* Send Window controller for both connection and stream windows.271* Each of this connection's Streams MUST use this controller.272*/273private final WindowController windowController = new WindowController();274private final FramesController framesController = new FramesController();275private final Http2TubeSubscriber subscriber;276final ConnectionWindowUpdateSender windowUpdater;277private volatile Throwable cause;278private volatile Supplier<ByteBuffer> initial;279280static final int DEFAULT_FRAME_SIZE = 16 * 1024;281282283// TODO: need list of control frames from other threads284// that need to be sent285286private Http2Connection(HttpConnection connection,287Http2ClientImpl client2,288int nextstreamid,289String key) {290this.connection = connection;291this.client2 = client2;292this.subscriber = new Http2TubeSubscriber(client2.client());293this.nextstreamid = nextstreamid;294this.key = key;295this.clientSettings = this.client2.getClientSettings();296this.framesDecoder = new FramesDecoder(this::processFrame,297clientSettings.getParameter(SettingsFrame.MAX_FRAME_SIZE));298// serverSettings will be updated by server299this.serverSettings = SettingsFrame.defaultRFCSettings();300this.hpackOut = new Encoder(serverSettings.getParameter(HEADER_TABLE_SIZE));301this.hpackIn = new Decoder(clientSettings.getParameter(HEADER_TABLE_SIZE));302if (debugHpack.on()) {303debugHpack.log("For the record:" + super.toString());304debugHpack.log("Decoder created: %s", hpackIn);305debugHpack.log("Encoder created: %s", hpackOut);306}307this.windowUpdater = new ConnectionWindowUpdateSender(this,308client2.getConnectionWindowSize(clientSettings));309}310311/**312* Case 1) Create from upgraded HTTP/1.1 connection.313* Is ready to use. Can't be SSL. exchange is the Exchange314* that initiated the connection, whose response will be delivered315* on a Stream.316*/317private Http2Connection(HttpConnection connection,318Http2ClientImpl client2,319Exchange<?> exchange,320Supplier<ByteBuffer> initial)321throws IOException, InterruptedException322{323this(connection,324client2,3253, // stream 1 is registered during the upgrade326keyFor(connection));327reserveStream(true);328Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());329330Stream<?> initialStream = createStream(exchange);331boolean opened = initialStream.registerStream(1, true);332if (debug.on() && !opened) {333debug.log("Initial stream was cancelled - but connection is maintained: " +334"reset frame will need to be sent later");335}336windowController.registerStream(1, getInitialSendWindowSize());337initialStream.requestSent();338// Upgrading:339// set callbacks before sending preface - makes sure anything that340// might be sent by the server will come our way.341this.initial = initial;342connectFlows(connection);343sendConnectionPreface();344if (!opened) {345debug.log("ensure reset frame is sent to cancel initial stream");346initialStream.sendCancelStreamFrame();347}348349}350351// Used when upgrading an HTTP/1.1 connection to HTTP/2 after receiving352// agreement from the server. Async style but completes immediately, because353// the connection is already connected.354static CompletableFuture<Http2Connection> createAsync(HttpConnection connection,355Http2ClientImpl client2,356Exchange<?> exchange,357Supplier<ByteBuffer> initial)358{359return MinimalFuture.supply(() -> new Http2Connection(connection, client2, exchange, initial));360}361362// Requires TLS handshake. So, is really async363static CompletableFuture<Http2Connection> createAsync(HttpRequestImpl request,364Http2ClientImpl h2client,365Exchange<?> exchange) {366assert request.secure();367AbstractAsyncSSLConnection connection = (AbstractAsyncSSLConnection)368HttpConnection.getConnection(request.getAddress(),369h2client.client(),370request,371HttpClient.Version.HTTP_2);372373// Expose the underlying connection to the exchange's aborter so it can374// be closed if a timeout occurs.375exchange.connectionAborter.connection(connection);376377return connection.connectAsync(exchange)378.thenCompose(unused -> connection.finishConnect())379.thenCompose(unused -> checkSSLConfig(connection))380.thenCompose(notused-> {381CompletableFuture<Http2Connection> cf = new MinimalFuture<>();382try {383Http2Connection hc = new Http2Connection(request, h2client, connection);384cf.complete(hc);385} catch (IOException e) {386cf.completeExceptionally(e);387}388return cf; } );389}390391/**392* Cases 2) 3)393*394* request is request to be sent.395*/396private Http2Connection(HttpRequestImpl request,397Http2ClientImpl h2client,398HttpConnection connection)399throws IOException400{401this(connection,402h2client,4031,404keyFor(request.uri(), request.proxy()));405406Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());407408// safe to resume async reading now.409connectFlows(connection);410sendConnectionPreface();411}412413private void connectFlows(HttpConnection connection) {414FlowTube tube = connection.getConnectionFlow();415// Connect the flow to our Http2TubeSubscriber:416tube.connectFlows(connection.publisher(), subscriber);417}418419final HttpClientImpl client() {420return client2.client();421}422423// call these before assigning a request/stream to a connection424// if false returned then a new Http2Connection is required425// if true, the stream may be assigned to this connection426// for server push, if false returned, then the stream should be cancelled427synchronized boolean reserveStream(boolean clientInitiated) throws IOException {428if (finalStream) {429return false;430}431if (clientInitiated && (lastReservedClientStreamid + 2) >= MAX_CLIENT_STREAM_ID) {432setFinalStream();433client2.deleteConnection(this);434return false;435} else if (!clientInitiated && (lastReservedServerStreamid + 2) >= MAX_SERVER_STREAM_ID) {436setFinalStream();437client2.deleteConnection(this);438return false;439}440if (clientInitiated)441lastReservedClientStreamid+=2;442else443lastReservedServerStreamid+=2;444445assert numReservedClientStreams >= 0;446assert numReservedServerStreams >= 0;447if (clientInitiated &&numReservedClientStreams >= maxConcurrentClientInitiatedStreams()) {448throw new IOException("too many concurrent streams");449} else if (clientInitiated) {450numReservedClientStreams++;451}452if (!clientInitiated && numReservedServerStreams >= maxConcurrentServerInitiatedStreams()) {453return false;454} else if (!clientInitiated) {455numReservedServerStreams++;456}457return true;458}459460/**461* Throws an IOException if h2 was not negotiated462*/463private static CompletableFuture<?> checkSSLConfig(AbstractAsyncSSLConnection aconn) {464assert aconn.isSecure();465466Function<String, CompletableFuture<Void>> checkAlpnCF = (alpn) -> {467CompletableFuture<Void> cf = new MinimalFuture<>();468SSLEngine engine = aconn.getEngine();469String engineAlpn = engine.getApplicationProtocol();470assert Objects.equals(alpn, engineAlpn)471: "alpn: %s, engine: %s".formatted(alpn, engineAlpn);472473DEBUG_LOGGER.log("checkSSLConfig: alpn: %s", alpn );474475if (alpn == null || !alpn.equals("h2")) {476String msg;477if (alpn == null) {478Log.logSSL("ALPN not supported");479msg = "ALPN not supported";480} else {481switch (alpn) {482case "":483Log.logSSL(msg = "No ALPN negotiated");484break;485case "http/1.1":486Log.logSSL( msg = "HTTP/1.1 ALPN returned");487break;488default:489Log.logSSL(msg = "Unexpected ALPN: " + alpn);490cf.completeExceptionally(new IOException(msg));491}492}493cf.completeExceptionally(new ALPNException(msg, aconn));494return cf;495}496cf.complete(null);497return cf;498};499500return aconn.getALPN()501.whenComplete((r,t) -> {502if (t != null && t instanceof SSLException) {503// something went wrong during the initial handshake504// close the connection505aconn.close();506}507})508.thenCompose(checkAlpnCF);509}510511synchronized boolean finalStream() {512return finalStream;513}514515/**516* Mark this connection so no more streams created on it and it will close when517* all are complete.518*/519synchronized void setFinalStream() {520finalStream = true;521}522523static String keyFor(HttpConnection connection) {524boolean isProxy = connection.isProxied(); // tunnel or plain clear connection through proxy525boolean isSecure = connection.isSecure();526InetSocketAddress addr = connection.address();527InetSocketAddress proxyAddr = connection.proxy();528assert isProxy == (proxyAddr != null);529530return keyString(isSecure, proxyAddr, addr.getHostString(), addr.getPort());531}532533static String keyFor(URI uri, InetSocketAddress proxy) {534boolean isSecure = uri.getScheme().equalsIgnoreCase("https");535536String host = uri.getHost();537int port = uri.getPort();538return keyString(isSecure, proxy, host, port);539}540541542// Compute the key for an HttpConnection in the Http2ClientImpl pool:543// The key string follows one of the three forms below:544// {C,S}:H:host:port545// C:P:proxy-host:proxy-port546// S:T:H:host:port;P:proxy-host:proxy-port547// C indicates clear text connection "http"548// S indicates secure "https"549// H indicates host (direct) connection550// P indicates proxy551// T indicates a tunnel connection through a proxy552//553// The first form indicates a direct connection to a server:554// - direct clear connection to an HTTP host:555// e.g.: "C:H:foo.com:80"556// - direct secure connection to an HTTPS host:557// e.g.: "S:H:foo.com:443"558// The second form indicates a clear connection to an HTTP/1.1 proxy:559// e.g.: "C:P:myproxy:8080"560// The third form indicates a secure tunnel connection to an HTTPS561// host through an HTTP/1.1 proxy:562// e.g: "S:T:H:foo.com:80;P:myproxy:8080"563static String keyString(boolean secure, InetSocketAddress proxy, String host, int port) {564if (secure && port == -1)565port = 443;566else if (!secure && port == -1)567port = 80;568var key = (secure ? "S:" : "C:");569if (proxy != null && !secure) {570// clear connection through proxy571key = key + "P:" + proxy.getHostString() + ":" + proxy.getPort();572} else if (proxy == null) {573// direct connection to host574key = key + "H:" + host + ":" + port;575} else {576// tunnel connection through proxy577key = key + "T:H:" + host + ":" + port + ";P:" + proxy.getHostString() + ":" + proxy.getPort();578}579return key;580}581582String key() {583return this.key;584}585586boolean offerConnection() {587return client2.offerConnection(this);588}589590private HttpPublisher publisher() {591return connection.publisher();592}593594private void decodeHeaders(HeaderFrame frame, DecodingCallback decoder)595throws IOException596{597if (debugHpack.on()) debugHpack.log("decodeHeaders(%s)", decoder);598599boolean endOfHeaders = frame.getFlag(HeaderFrame.END_HEADERS);600601List<ByteBuffer> buffers = frame.getHeaderBlock();602int len = buffers.size();603for (int i = 0; i < len; i++) {604ByteBuffer b = buffers.get(i);605hpackIn.decode(b, endOfHeaders && (i == len - 1), decoder);606}607}608609final int getInitialSendWindowSize() {610return serverSettings.getParameter(INITIAL_WINDOW_SIZE);611}612613final int maxConcurrentClientInitiatedStreams() {614return serverSettings.getParameter(MAX_CONCURRENT_STREAMS);615}616617final int maxConcurrentServerInitiatedStreams() {618return clientSettings.getParameter(MAX_CONCURRENT_STREAMS);619}620621void close() {622Log.logTrace("Closing HTTP/2 connection: to {0}", connection.address());623GoAwayFrame f = new GoAwayFrame(0,624ErrorFrame.NO_ERROR,625"Requested by user".getBytes(UTF_8));626// TODO: set last stream. For now zero ok.627sendFrame(f);628}629630long count;631final void asyncReceive(ByteBuffer buffer) {632// We don't need to read anything and633// we don't want to send anything back to the server634// until the connection preface has been sent.635// Therefore we're going to wait if needed before reading636// (and thus replying) to anything.637// Starting to reply to something (e.g send an ACK to a638// SettingsFrame sent by the server) before the connection639// preface is fully sent might result in the server640// sending a GOAWAY frame with 'invalid_preface'.641//642// Note: asyncReceive is only called from the Http2TubeSubscriber643// sequential scheduler.644try {645Supplier<ByteBuffer> bs = initial;646// ensure that we always handle the initial buffer first,647// if any.648if (bs != null) {649initial = null;650ByteBuffer b = bs.get();651if (b.hasRemaining()) {652long c = ++count;653if (debug.on())654debug.log(() -> "H2 Receiving Initial(" + c +"): " + b.remaining());655framesController.processReceivedData(framesDecoder, b);656}657}658ByteBuffer b = buffer;659// the Http2TubeSubscriber scheduler ensures that the order of incoming660// buffers is preserved.661if (b == EMPTY_TRIGGER) {662if (debug.on()) debug.log("H2 Received EMPTY_TRIGGER");663boolean prefaceSent = framesController.prefaceSent;664assert prefaceSent;665// call framesController.processReceivedData to potentially666// trigger the processing of all the data buffered there.667framesController.processReceivedData(framesDecoder, buffer);668if (debug.on()) debug.log("H2 processed buffered data");669} else {670long c = ++count;671if (debug.on())672debug.log("H2 Receiving(%d): %d", c, b.remaining());673framesController.processReceivedData(framesDecoder, buffer);674if (debug.on()) debug.log("H2 processed(%d)", c);675}676} catch (Throwable e) {677String msg = Utils.stackTrace(e);678Log.logTrace(msg);679shutdown(e);680}681}682683Throwable getRecordedCause() {684return cause;685}686687void shutdown(Throwable t) {688if (debug.on()) debug.log(() -> "Shutting down h2c (closed="+closed+"): " + t);689if (closed == true) return;690synchronized (this) {691if (closed == true) return;692closed = true;693}694if (Log.errors()) {695if (!(t instanceof EOFException) || isActive()) {696Log.logError(t);697} else if (t != null) {698Log.logError("Shutting down connection: {0}", t.getMessage());699}700}701Throwable initialCause = this.cause;702if (initialCause == null) this.cause = t;703client2.deleteConnection(this);704for (Stream<?> s : streams.values()) {705try {706s.connectionClosing(t);707} catch (Throwable e) {708Log.logError("Failed to close stream {0}: {1}", s.streamid, e);709}710}711connection.close();712}713714/**715* Streams initiated by a client MUST use odd-numbered stream716* identifiers; those initiated by the server MUST use even-numbered717* stream identifiers.718*/719private static final boolean isServerInitiatedStream(int streamid) {720return (streamid & 0x1) == 0;721}722723/**724* Handles stream 0 (common) frames that apply to whole connection and passes725* other stream specific frames to that Stream object.726*727* Invokes Stream.incoming() which is expected to process frame without728* blocking.729*/730void processFrame(Http2Frame frame) throws IOException {731Log.logFrames(frame, "IN");732int streamid = frame.streamid();733if (frame instanceof MalformedFrame) {734Log.logError(((MalformedFrame) frame).getMessage());735if (streamid == 0) {736framesDecoder.close("Malformed frame on stream 0");737protocolError(((MalformedFrame) frame).getErrorCode(),738((MalformedFrame) frame).getMessage());739} else {740if (debug.on())741debug.log(() -> "Reset stream: " + ((MalformedFrame) frame).getMessage());742resetStream(streamid, ((MalformedFrame) frame).getErrorCode());743}744return;745}746if (streamid == 0) {747handleConnectionFrame(frame);748} else {749if (frame instanceof SettingsFrame) {750// The stream identifier for a SETTINGS frame MUST be zero751framesDecoder.close(752"The stream identifier for a SETTINGS frame MUST be zero");753protocolError(GoAwayFrame.PROTOCOL_ERROR);754return;755}756757Stream<?> stream = getStream(streamid);758if (stream == null) {759// Should never receive a frame with unknown stream id760761if (frame instanceof HeaderFrame) {762// always decode the headers as they may affect763// connection-level HPACK decoding state764DecodingCallback decoder = new ValidatingHeadersConsumer();765try {766decodeHeaders((HeaderFrame) frame, decoder);767} catch (UncheckedIOException e) {768protocolError(ResetFrame.PROTOCOL_ERROR, e.getMessage());769return;770}771}772773if (!(frame instanceof ResetFrame)) {774if (frame instanceof DataFrame) {775dropDataFrame((DataFrame)frame);776}777if (isServerInitiatedStream(streamid)) {778if (streamid < nextPushStream) {779// trailing data on a cancelled push promise stream,780// reset will already have been sent, ignore781Log.logTrace("Ignoring cancelled push promise frame " + frame);782} else {783resetStream(streamid, ResetFrame.PROTOCOL_ERROR);784}785} else if (streamid >= nextstreamid) {786// otherwise the stream has already been reset/closed787resetStream(streamid, ResetFrame.PROTOCOL_ERROR);788}789}790return;791}792if (frame instanceof PushPromiseFrame) {793PushPromiseFrame pp = (PushPromiseFrame)frame;794try {795handlePushPromise(stream, pp);796} catch (UncheckedIOException e) {797protocolError(ResetFrame.PROTOCOL_ERROR, e.getMessage());798return;799}800} else if (frame instanceof HeaderFrame) {801// decode headers (or continuation)802try {803decodeHeaders((HeaderFrame) frame, stream.rspHeadersConsumer());804} catch (UncheckedIOException e) {805debug.log("Error decoding headers: " + e.getMessage(), e);806protocolError(ResetFrame.PROTOCOL_ERROR, e.getMessage());807return;808}809stream.incoming(frame);810} else {811stream.incoming(frame);812}813}814}815816final void dropDataFrame(DataFrame df) {817if (closed) return;818if (debug.on()) {819debug.log("Dropping data frame for stream %d (%d payload bytes)",820df.streamid(), df.payloadLength());821}822ensureWindowUpdated(df);823}824825final void ensureWindowUpdated(DataFrame df) {826try {827if (closed) return;828int length = df.payloadLength();829if (length > 0) {830windowUpdater.update(length);831}832} catch(Throwable t) {833Log.logError("Unexpected exception while updating window: {0}", (Object)t);834}835}836837private <T> void handlePushPromise(Stream<T> parent, PushPromiseFrame pp)838throws IOException839{840// always decode the headers as they may affect connection-level HPACK841// decoding state842HeaderDecoder decoder = new HeaderDecoder();843decodeHeaders(pp, decoder);844845HttpRequestImpl parentReq = parent.request;846int promisedStreamid = pp.getPromisedStream();847if (promisedStreamid != nextPushStream) {848resetStream(promisedStreamid, ResetFrame.PROTOCOL_ERROR);849return;850} else if (!reserveStream(false)) {851resetStream(promisedStreamid, ResetFrame.REFUSED_STREAM);852return;853} else {854nextPushStream += 2;855}856857HttpHeaders headers = decoder.headers();858HttpRequestImpl pushReq = HttpRequestImpl.createPushRequest(parentReq, headers);859Exchange<T> pushExch = new Exchange<>(pushReq, parent.exchange.multi);860Stream.PushedStream<T> pushStream = createPushStream(parent, pushExch);861pushExch.exchImpl = pushStream;862pushStream.registerStream(promisedStreamid, true);863parent.incoming_pushPromise(pushReq, pushStream);864}865866private void handleConnectionFrame(Http2Frame frame)867throws IOException868{869switch (frame.type()) {870case SettingsFrame.TYPE -> handleSettings((SettingsFrame) frame);871case PingFrame.TYPE -> handlePing((PingFrame) frame);872case GoAwayFrame.TYPE -> handleGoAway((GoAwayFrame) frame);873case WindowUpdateFrame.TYPE -> handleWindowUpdate((WindowUpdateFrame) frame);874875default -> protocolError(ErrorFrame.PROTOCOL_ERROR);876}877}878879void resetStream(int streamid, int code) {880try {881if (connection.channel().isOpen()) {882// no need to try & send a reset frame if the883// connection channel is already closed.884Log.logError(885"Resetting stream {0,number,integer} with error code {1,number,integer}",886streamid, code);887markStream(streamid, code);888ResetFrame frame = new ResetFrame(streamid, code);889sendFrame(frame);890} else if (debug.on()) {891debug.log("Channel already closed, no need to reset stream %d",892streamid);893}894} finally {895decrementStreamsCount(streamid);896closeStream(streamid);897}898}899900private void markStream(int streamid, int code) {901Stream<?> s = streams.get(streamid);902if (s != null) s.markStream(code);903}904905// reduce count of streams by 1 if stream still exists906synchronized void decrementStreamsCount(int streamid) {907Stream<?> s = streams.get(streamid);908if (s == null || !s.deRegister())909return;910if (streamid % 2 == 1) {911numReservedClientStreams--;912assert numReservedClientStreams >= 0 :913"negative client stream count for stream=" + streamid;914} else {915numReservedServerStreams--;916assert numReservedServerStreams >= 0 :917"negative server stream count for stream=" + streamid;918}919}920921void closeStream(int streamid) {922if (debug.on()) debug.log("Closed stream %d", streamid);923boolean isClient = (streamid % 2) == 1;924Stream<?> s = streams.remove(streamid);925if (s != null) {926// decrement the reference count on the HttpClientImpl927// to allow the SelectorManager thread to exit if no928// other operation is pending and the facade is no929// longer referenced.930client().streamUnreference();931}932// ## Remove s != null. It is a hack for delayed cancellation,reset933if (s != null && !(s instanceof Stream.PushedStream)) {934// Since PushStreams have no request body, then they have no935// corresponding entry in the window controller.936windowController.removeStream(streamid);937}938if (finalStream() && streams.isEmpty()) {939// should be only 1 stream, but there might be more if server push940close();941}942}943944/**945* Increments this connection's send Window by the amount in the given frame.946*/947private void handleWindowUpdate(WindowUpdateFrame f)948throws IOException949{950int amount = f.getUpdate();951if (amount <= 0) {952// ## temporarily disable to workaround a bug in Jetty where it953// ## sends Window updates with a 0 update value.954//protocolError(ErrorFrame.PROTOCOL_ERROR);955} else {956boolean success = windowController.increaseConnectionWindow(amount);957if (!success) {958protocolError(ErrorFrame.FLOW_CONTROL_ERROR); // overflow959}960}961}962963private void protocolError(int errorCode)964throws IOException965{966protocolError(errorCode, null);967}968969private void protocolError(int errorCode, String msg)970throws IOException971{972GoAwayFrame frame = new GoAwayFrame(0, errorCode);973sendFrame(frame);974shutdown(new IOException("protocol error" + (msg == null?"":(": " + msg))));975}976977private void handleSettings(SettingsFrame frame)978throws IOException979{980assert frame.streamid() == 0;981if (!frame.getFlag(SettingsFrame.ACK)) {982int newWindowSize = frame.getParameter(INITIAL_WINDOW_SIZE);983if (newWindowSize != -1) {984int oldWindowSize = serverSettings.getParameter(INITIAL_WINDOW_SIZE);985int diff = newWindowSize - oldWindowSize;986if (diff != 0) {987windowController.adjustActiveStreams(diff);988}989}990991serverSettings.update(frame);992sendFrame(new SettingsFrame(SettingsFrame.ACK));993}994}995996private void handlePing(PingFrame frame)997throws IOException998{999frame.setFlag(PingFrame.ACK);1000sendUnorderedFrame(frame);1001}10021003private void handleGoAway(GoAwayFrame frame)1004throws IOException1005{1006shutdown(new IOException(1007String.valueOf(connection.channel().getLocalAddress())1008+": GOAWAY received"));1009}10101011/**1012* Max frame size we are allowed to send1013*/1014public int getMaxSendFrameSize() {1015int param = serverSettings.getParameter(MAX_FRAME_SIZE);1016if (param == -1) {1017param = DEFAULT_FRAME_SIZE;1018}1019return param;1020}10211022/**1023* Max frame size we will receive1024*/1025public int getMaxReceiveFrameSize() {1026return clientSettings.getParameter(MAX_FRAME_SIZE);1027}10281029private static final String CLIENT_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";10301031private static final byte[] PREFACE_BYTES =1032CLIENT_PREFACE.getBytes(StandardCharsets.ISO_8859_1);10331034/**1035* Sends Connection preface and Settings frame with current preferred1036* values1037*/1038private void sendConnectionPreface() throws IOException {1039Log.logTrace("{0}: start sending connection preface to {1}",1040connection.channel().getLocalAddress(),1041connection.address());1042SettingsFrame sf = new SettingsFrame(clientSettings);1043ByteBuffer buf = framesEncoder.encodeConnectionPreface(PREFACE_BYTES, sf);1044Log.logFrames(sf, "OUT");1045// send preface bytes and SettingsFrame together1046HttpPublisher publisher = publisher();1047publisher.enqueueUnordered(List.of(buf));1048publisher.signalEnqueued();1049// mark preface sent.1050framesController.markPrefaceSent();1051Log.logTrace("PREFACE_BYTES sent");1052Log.logTrace("Settings Frame sent");10531054// send a Window update for the receive buffer we are using1055// minus the initial 64 K -1 specified in protocol:1056// RFC 7540, Section 6.9.2:1057// "[...] the connection flow-control window is set to the default1058// initial window size until a WINDOW_UPDATE frame is received."1059//1060// Note that the default initial window size, not to be confused1061// with the initial window size, is defined by RFC 7540 as1062// 64K -1.1063final int len = windowUpdater.initialWindowSize - DEFAULT_INITIAL_WINDOW_SIZE;1064if (len != 0) {1065if (Log.channel()) {1066Log.logChannel("Sending initial connection window update frame: {0} ({1} - {2})",1067len, windowUpdater.initialWindowSize, DEFAULT_INITIAL_WINDOW_SIZE);1068}1069windowUpdater.sendWindowUpdate(len);1070}1071// there will be an ACK to the windows update - which should1072// cause any pending data stored before the preface was sent to be1073// flushed (see PrefaceController).1074Log.logTrace("finished sending connection preface");1075if (debug.on())1076debug.log("Triggering processing of buffered data"1077+ " after sending connection preface");1078subscriber.onNext(List.of(EMPTY_TRIGGER));1079}10801081/**1082* Returns an existing Stream with given id, or null if doesn't exist1083*/1084@SuppressWarnings("unchecked")1085<T> Stream<T> getStream(int streamid) {1086return (Stream<T>)streams.get(streamid);1087}10881089/**1090* Creates Stream with given id.1091*/1092final <T> Stream<T> createStream(Exchange<T> exchange) {1093Stream<T> stream = new Stream<>(this, exchange, windowController);1094return stream;1095}10961097<T> Stream.PushedStream<T> createPushStream(Stream<T> parent, Exchange<T> pushEx) {1098PushGroup<T> pg = parent.exchange.getPushGroup();1099return new Stream.PushedStream<>(pg, this, pushEx);1100}11011102<T> void putStream(Stream<T> stream, int streamid) {1103// increment the reference count on the HttpClientImpl1104// to prevent the SelectorManager thread from exiting until1105// the stream is closed.1106client().streamReference();1107streams.put(streamid, stream);1108}11091110/**1111* Encode the headers into a List<ByteBuffer> and then create HEADERS1112* and CONTINUATION frames from the list and return the List<Http2Frame>.1113*/1114private List<HeaderFrame> encodeHeaders(OutgoingHeaders<Stream<?>> frame) {1115// max value of frame size is clamped by default frame size to avoid OOM1116int bufferSize = Math.min(Math.max(getMaxSendFrameSize(), 1024), DEFAULT_FRAME_SIZE);1117List<ByteBuffer> buffers = encodeHeadersImpl(1118bufferSize,1119frame.getAttachment().getRequestPseudoHeaders(),1120frame.getUserHeaders(),1121frame.getSystemHeaders());11221123List<HeaderFrame> frames = new ArrayList<>(buffers.size());1124Iterator<ByteBuffer> bufIterator = buffers.iterator();1125HeaderFrame oframe = new HeadersFrame(frame.streamid(), frame.getFlags(), bufIterator.next());1126frames.add(oframe);1127while(bufIterator.hasNext()) {1128oframe = new ContinuationFrame(frame.streamid(), bufIterator.next());1129frames.add(oframe);1130}1131oframe.setFlag(HeaderFrame.END_HEADERS);1132return frames;1133}11341135// Dedicated cache for headers encoding ByteBuffer.1136// There can be no concurrent access to this buffer as all access to this buffer1137// and its content happen within a single critical code block section protected1138// by the sendLock. / (see sendFrame())1139// private final ByteBufferPool headerEncodingPool = new ByteBufferPool();11401141private ByteBuffer getHeaderBuffer(int size) {1142ByteBuffer buf = ByteBuffer.allocate(size);1143buf.limit(size);1144return buf;1145}11461147/*1148* Encodes all the headers from the given HttpHeaders into the given List1149* of buffers.1150*1151* From https://tools.ietf.org/html/rfc7540#section-8.1.2 :1152*1153* ...Just as in HTTP/1.x, header field names are strings of ASCII1154* characters that are compared in a case-insensitive fashion. However,1155* header field names MUST be converted to lowercase prior to their1156* encoding in HTTP/2...1157*/1158private List<ByteBuffer> encodeHeadersImpl(int bufferSize, HttpHeaders... headers) {1159ByteBuffer buffer = getHeaderBuffer(bufferSize);1160List<ByteBuffer> buffers = new ArrayList<>();1161for(HttpHeaders header : headers) {1162for (Map.Entry<String, List<String>> e : header.map().entrySet()) {1163String lKey = e.getKey().toLowerCase(Locale.US);1164List<String> values = e.getValue();1165for (String value : values) {1166hpackOut.header(lKey, value);1167while (!hpackOut.encode(buffer)) {1168buffer.flip();1169buffers.add(buffer);1170buffer = getHeaderBuffer(bufferSize);1171}1172}1173}1174}1175buffer.flip();1176buffers.add(buffer);1177return buffers;1178}117911801181private List<ByteBuffer> encodeHeaders(OutgoingHeaders<Stream<?>> oh, Stream<?> stream) {1182oh.streamid(stream.streamid);1183if (Log.headers()) {1184StringBuilder sb = new StringBuilder("HEADERS FRAME (stream=");1185sb.append(stream.streamid).append(")\n");1186Log.dumpHeaders(sb, " ", oh.getAttachment().getRequestPseudoHeaders());1187Log.dumpHeaders(sb, " ", oh.getSystemHeaders());1188Log.dumpHeaders(sb, " ", oh.getUserHeaders());1189Log.logHeaders(sb.toString());1190}1191List<HeaderFrame> frames = encodeHeaders(oh);1192return encodeFrames(frames);1193}11941195private List<ByteBuffer> encodeFrames(List<HeaderFrame> frames) {1196if (Log.frames()) {1197frames.forEach(f -> Log.logFrames(f, "OUT"));1198}1199return framesEncoder.encodeFrames(frames);1200}12011202private Stream<?> registerNewStream(OutgoingHeaders<Stream<?>> oh) {1203Stream<?> stream = oh.getAttachment();1204assert stream.streamid == 0;1205int streamid = nextstreamid;1206if (stream.registerStream(streamid, false)) {1207// set outgoing window here. This allows thread sending1208// body to proceed.1209nextstreamid += 2;1210windowController.registerStream(streamid, getInitialSendWindowSize());1211return stream;1212} else {1213stream.cancelImpl(new IOException("Request cancelled"));1214if (finalStream() && streams.isEmpty()) {1215close();1216}1217return null;1218}1219}12201221private final Object sendlock = new Object();12221223void sendFrame(Http2Frame frame) {1224try {1225HttpPublisher publisher = publisher();1226synchronized (sendlock) {1227if (frame instanceof OutgoingHeaders) {1228@SuppressWarnings("unchecked")1229OutgoingHeaders<Stream<?>> oh = (OutgoingHeaders<Stream<?>>) frame;1230Stream<?> stream = registerNewStream(oh);1231// provide protection from inserting unordered frames between Headers and Continuation1232if (stream != null) {1233publisher.enqueue(encodeHeaders(oh, stream));1234}1235} else {1236publisher.enqueue(encodeFrame(frame));1237}1238}1239publisher.signalEnqueued();1240} catch (IOException e) {1241if (!closed) {1242Log.logError(e);1243shutdown(e);1244}1245}1246}12471248private List<ByteBuffer> encodeFrame(Http2Frame frame) {1249Log.logFrames(frame, "OUT");1250return framesEncoder.encodeFrame(frame);1251}12521253void sendDataFrame(DataFrame frame) {1254try {1255HttpPublisher publisher = publisher();1256publisher.enqueue(encodeFrame(frame));1257publisher.signalEnqueued();1258} catch (IOException e) {1259if (!closed) {1260Log.logError(e);1261shutdown(e);1262}1263}1264}12651266/*1267* Direct call of the method bypasses synchronization on "sendlock" and1268* allowed only of control frames: WindowUpdateFrame, PingFrame and etc.1269* prohibited for such frames as DataFrame, HeadersFrame, ContinuationFrame.1270*/1271void sendUnorderedFrame(Http2Frame frame) {1272try {1273HttpPublisher publisher = publisher();1274publisher.enqueueUnordered(encodeFrame(frame));1275publisher.signalEnqueued();1276} catch (IOException e) {1277if (!closed) {1278Log.logError(e);1279shutdown(e);1280}1281}1282}12831284/**1285* A simple tube subscriber for reading from the connection flow.1286*/1287final class Http2TubeSubscriber implements TubeSubscriber {1288private volatile Flow.Subscription subscription;1289private volatile boolean completed;1290private volatile boolean dropped;1291private volatile Throwable error;1292private final ConcurrentLinkedQueue<ByteBuffer> queue1293= new ConcurrentLinkedQueue<>();1294private final SequentialScheduler scheduler =1295SequentialScheduler.lockingScheduler(this::processQueue);1296private final HttpClientImpl client;12971298Http2TubeSubscriber(HttpClientImpl client) {1299this.client = Objects.requireNonNull(client);1300}13011302final void processQueue() {1303try {1304while (!queue.isEmpty() && !scheduler.isStopped()) {1305ByteBuffer buffer = queue.poll();1306if (debug.on())1307debug.log("sending %d to Http2Connection.asyncReceive",1308buffer.remaining());1309asyncReceive(buffer);1310}1311} catch (Throwable t) {1312Throwable x = error;1313if (x == null) error = t;1314} finally {1315Throwable x = error;1316if (x != null) {1317if (debug.on()) debug.log("Stopping scheduler", x);1318scheduler.stop();1319Http2Connection.this.shutdown(x);1320}1321}1322}13231324private final void runOrSchedule() {1325if (client.isSelectorThread()) {1326scheduler.runOrSchedule(client.theExecutor());1327} else scheduler.runOrSchedule();1328}13291330@Override1331public void onSubscribe(Flow.Subscription subscription) {1332// supports being called multiple time.1333// doesn't cancel the previous subscription, since that is1334// most probably the same as the new subscription.1335assert this.subscription == null || dropped == false;1336this.subscription = subscription;1337dropped = false;1338// TODO FIXME: request(1) should be done by the delegate.1339if (!completed) {1340if (debug.on())1341debug.log("onSubscribe: requesting Long.MAX_VALUE for reading");1342subscription.request(Long.MAX_VALUE);1343} else {1344if (debug.on()) debug.log("onSubscribe: already completed");1345}1346}13471348@Override1349public void onNext(List<ByteBuffer> item) {1350if (debug.on()) debug.log(() -> "onNext: got " + Utils.remaining(item)1351+ " bytes in " + item.size() + " buffers");1352queue.addAll(item);1353runOrSchedule();1354}13551356@Override1357public void onError(Throwable throwable) {1358if (debug.on()) debug.log(() -> "onError: " + throwable);1359error = throwable;1360completed = true;1361runOrSchedule();1362}13631364@Override1365public void onComplete() {1366String msg = isActive()1367? "EOF reached while reading"1368: "Idle connection closed by HTTP/2 peer";1369if (debug.on()) debug.log(msg);1370error = new EOFException(msg);1371completed = true;1372runOrSchedule();1373}13741375@Override1376public void dropSubscription() {1377if (debug.on()) debug.log("dropSubscription");1378// we could probably set subscription to null here...1379// then we might not need the 'dropped' boolean?1380dropped = true;1381}1382}13831384synchronized boolean isActive() {1385return numReservedClientStreams > 0 || numReservedServerStreams > 0;1386}13871388@Override1389public final String toString() {1390return dbgString();1391}13921393final String dbgString() {1394return "Http2Connection("1395+ connection.getConnectionFlow() + ")";1396}13971398static class HeaderDecoder extends ValidatingHeadersConsumer {13991400HttpHeadersBuilder headersBuilder;14011402HeaderDecoder() {1403this.headersBuilder = new HttpHeadersBuilder();1404}14051406@Override1407public void onDecoded(CharSequence name, CharSequence value) {1408String n = name.toString();1409String v = value.toString();1410super.onDecoded(n, v);1411headersBuilder.addHeader(n, v);1412}14131414HttpHeaders headers() {1415return headersBuilder.build();1416}1417}14181419/*1420* Checks RFC 7540 rules (relaxed) compliance regarding pseudo-headers.1421*/1422static class ValidatingHeadersConsumer implements DecodingCallback {14231424private static final Set<String> PSEUDO_HEADERS =1425Set.of(":authority", ":method", ":path", ":scheme", ":status");14261427/** Used to check that if there are pseudo-headers, they go first */1428private boolean pseudoHeadersEnded;14291430/**1431* Called when END_HEADERS was received. This consumer may be invoked1432* again after reset() is called, but for a whole new set of headers.1433*/1434void reset() {1435pseudoHeadersEnded = false;1436}14371438@Override1439public void onDecoded(CharSequence name, CharSequence value)1440throws UncheckedIOException1441{1442String n = name.toString();1443if (n.startsWith(":")) {1444if (pseudoHeadersEnded) {1445throw newException("Unexpected pseudo-header '%s'", n);1446} else if (!PSEUDO_HEADERS.contains(n)) {1447throw newException("Unknown pseudo-header '%s'", n);1448}1449} else {1450pseudoHeadersEnded = true;1451if (!Utils.isValidName(n)) {1452throw newException("Bad header name '%s'", n);1453}1454}1455String v = value.toString();1456if (!Utils.isValidValue(v)) {1457throw newException("Bad header value '%s'", v);1458}1459}14601461private UncheckedIOException newException(String message, String header)1462{1463return new UncheckedIOException(1464new IOException(String.format(message, header)));1465}1466}14671468static final class ConnectionWindowUpdateSender extends WindowUpdateSender {14691470final int initialWindowSize;1471public ConnectionWindowUpdateSender(Http2Connection connection,1472int initialWindowSize) {1473super(connection, initialWindowSize);1474this.initialWindowSize = initialWindowSize;1475}14761477@Override1478int getStreamId() {1479return 0;1480}1481}14821483/**1484* Thrown when https handshake negotiates http/1.1 alpn instead of h21485*/1486static final class ALPNException extends IOException {1487private static final long serialVersionUID = 0L;1488final transient AbstractAsyncSSLConnection connection;14891490ALPNException(String msg, AbstractAsyncSSLConnection connection) {1491super(msg);1492this.connection = connection;1493}14941495AbstractAsyncSSLConnection getConnection() {1496return connection;1497}1498}1499}150015011502