This is an automated email from the ASF dual-hosted git repository.

zrlw pushed a commit to branch 3.3
in repository https://gitbox.apache.org/repos/asf/dubbo.git


The following commit(s) were added to refs/heads/3.3 by this push:
     new c31164ab32 [3.3] Add http2 client connection preface process (#15436)
c31164ab32 is described below

commit c31164ab323e7b22c3c8088e5ab596374fddbd1c
Author: zrlw <[email protected]>
AuthorDate: Mon Jun 9 17:05:40 2025 +0800

    [3.3] Add http2 client connection preface process (#15436)
    
    * Add http2 client connection preface process
    
    * Add comments to explain the reason of adding client connection preface 
processing
---
 .../transport/netty4/NettyConnectionClient.java    | 89 ++++++++++++++++++++++
 .../netty4/http2/Http2ClientSettingsHandler.java   | 54 +++++++++++++
 2 files changed, 143 insertions(+)

diff --git 
a/dubbo-remoting/dubbo-remoting-netty4/src/main/java/org/apache/dubbo/remoting/transport/netty4/NettyConnectionClient.java
 
b/dubbo-remoting/dubbo-remoting-netty4/src/main/java/org/apache/dubbo/remoting/transport/netty4/NettyConnectionClient.java
index 5838709943..528bf5b226 100644
--- 
a/dubbo-remoting/dubbo-remoting-netty4/src/main/java/org/apache/dubbo/remoting/transport/netty4/NettyConnectionClient.java
+++ 
b/dubbo-remoting/dubbo-remoting-netty4/src/main/java/org/apache/dubbo/remoting/transport/netty4/NettyConnectionClient.java
@@ -17,31 +17,45 @@
 package org.apache.dubbo.remoting.transport.netty4;
 
 import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.Version;
+import org.apache.dubbo.common.utils.NetUtils;
 import org.apache.dubbo.remoting.ChannelHandler;
 import org.apache.dubbo.remoting.Constants;
 import org.apache.dubbo.remoting.RemotingException;
 import org.apache.dubbo.remoting.api.WireProtocol;
+import 
org.apache.dubbo.remoting.transport.netty4.http2.Http2ClientSettingsHandler;
 import org.apache.dubbo.remoting.transport.netty4.ssl.SslClientTlsHandler;
 import org.apache.dubbo.remoting.transport.netty4.ssl.SslContexts;
 import org.apache.dubbo.remoting.utils.UrlUtils;
 
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
 import io.netty.bootstrap.Bootstrap;
 import io.netty.buffer.PooledByteBufAllocator;
 import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInitializer;
 import io.netty.channel.ChannelOption;
 import io.netty.channel.ChannelPipeline;
 import io.netty.channel.socket.SocketChannel;
+import io.netty.handler.codec.http2.Http2FrameCodec;
 import io.netty.handler.ssl.SslContext;
 import io.netty.handler.timeout.IdleStateHandler;
+import io.netty.util.concurrent.DefaultPromise;
+import io.netty.util.concurrent.GlobalEventExecutor;
+import io.netty.util.concurrent.Promise;
 
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static 
org.apache.dubbo.common.constants.LoggerCodeConstants.TRANSPORT_CLIENT_CONNECT_TIMEOUT;
 import static 
org.apache.dubbo.remoting.transport.netty4.NettyEventLoopFactory.socketChannelClass;
 
 public final class NettyConnectionClient extends AbstractNettyConnectionClient 
{
 
     private Bootstrap bootstrap;
 
+    private AtomicReference<Promise<Void>> connectionPrefaceReceivedPromiseRef;
+
     public NettyConnectionClient(URL url, ChannelHandler handler) throws 
RemotingException {
         super(url, handler);
     }
@@ -87,6 +101,16 @@ public final class NettyConnectionClient extends 
AbstractNettyConnectionClient {
 
                 NettyConfigOperator operator = new 
NettyConfigOperator(nettyChannel, getChannelHandler());
                 protocol.configClientPipeline(getUrl(), operator, 
nettySslContextOperator);
+
+                ChannelHandlerContext http2FrameCodecHandlerCtx = 
pipeline.context(Http2FrameCodec.class);
+                if (http2FrameCodecHandlerCtx != null) {
+                    connectionPrefaceReceivedPromiseRef = new 
AtomicReference<>();
+                    pipeline.addAfter(
+                            http2FrameCodecHandlerCtx.name(),
+                            "client-connection-preface-handler",
+                            new 
Http2ClientSettingsHandler(connectionPrefaceReceivedPromiseRef));
+                }
+
                 // set null but do not close this client, it will be 
reconnecting in the future
                 ch.closeFuture().addListener(channelFuture -> 
clearNettyChannel());
                 // TODO support Socks5
@@ -97,6 +121,71 @@ public final class NettyConnectionClient extends 
AbstractNettyConnectionClient {
 
     @Override
     protected ChannelFuture performConnect() {
+        if (connectionPrefaceReceivedPromiseRef != null) {
+            connectionPrefaceReceivedPromiseRef.compareAndSet(null, new 
DefaultPromise<>(GlobalEventExecutor.INSTANCE));
+        }
         return bootstrap.connect();
     }
+
+    @Override
+    protected void doConnect() throws RemotingException {
+        long start = System.currentTimeMillis();
+        super.doConnect();
+        waitConnectionPreface(start);
+    }
+
+    /**
+     * Wait connection preface
+     * <br>
+     * Http2 client should set max header list size of http2 encoder based on 
server connection preface before
+     * sending first data frame, otherwise the http2 server might send back 
GO_AWAY frame and disconnect the connection
+     * immediately if the size of client Headers frame is bigger than the 
MAX_HEADER_LIST_SIZE of server settings.<br>
+     * @see <a 
href="https://httpwg.org/specs/rfc7540.html#ConnectionHeader";>HTTP/2 Connection 
Preface</a><br>
+     * In HTTP/2, each endpoint is required to send a connection preface as a 
final confirmation of the protocol
+     * in use and to establish the initial settings for the HTTP/2 connection. 
The client and server each send a
+     * different connection preface. The client connection preface starts with 
a sequence of 24 octets,
+     * which in hex notation is:<br>
+     * 0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a<br>
+     * That is, the connection preface starts with the string PRI * 
HTTP/2.0\r\n\r\nSM\r\n\r\n<br>
+     * This sequence MUST be followed by a SETTINGS frame (Section 6.5), which 
MAY be empty.
+     * The server connection preface consists of a potentially empty SETTINGS 
frame (Section 6.5) that MUST be
+     * the first frame the server sends in the HTTP/2 connection.
+     *
+     * @param start start time of doConnect in milliseconds.
+     */
+    private void waitConnectionPreface(long start) throws RemotingException {
+        if (connectionPrefaceReceivedPromiseRef == null) {
+            return;
+        }
+        Promise<Void> connectionPrefaceReceivedPromise = 
connectionPrefaceReceivedPromiseRef.get();
+        if (connectionPrefaceReceivedPromise != null) {
+            long retainedTimeout = getConnectTimeout() - 
System.currentTimeMillis() + start;
+            boolean ret = 
connectionPrefaceReceivedPromise.awaitUninterruptibly(retainedTimeout, 
TimeUnit.MILLISECONDS);
+            // Only process once: destroy connectionPrefaceReceivedPromise 
after used
+            synchronized (this) {
+                connectionPrefaceReceivedPromiseRef.set(null);
+            }
+            if (!ret || !connectionPrefaceReceivedPromise.isSuccess()) {
+                // 6-2 Client-side connection preface timeout
+                RemotingException remotingException = new RemotingException(
+                        this,
+                        "client(url: " + getUrl() + ") failed to connect to 
server " + getConnectAddress()
+                                + " client-side connection preface timeout " + 
getConnectTimeout()
+                                + "ms (elapsed: "
+                                + (System.currentTimeMillis() - start) + "ms) 
from netty client "
+                                + NetUtils.getLocalHost()
+                                + " using dubbo version "
+                                + Version.getVersion());
+
+                logger.error(
+                        TRANSPORT_CLIENT_CONNECT_TIMEOUT,
+                        "provider crash",
+                        "",
+                        "Client-side connection preface timeout",
+                        remotingException);
+
+                throw remotingException;
+            }
+        }
+    }
 }
diff --git 
a/dubbo-remoting/dubbo-remoting-netty4/src/main/java/org/apache/dubbo/remoting/transport/netty4/http2/Http2ClientSettingsHandler.java
 
b/dubbo-remoting/dubbo-remoting-netty4/src/main/java/org/apache/dubbo/remoting/transport/netty4/http2/Http2ClientSettingsHandler.java
new file mode 100644
index 0000000000..35eb7bbb58
--- /dev/null
+++ 
b/dubbo-remoting/dubbo-remoting-netty4/src/main/java/org/apache/dubbo/remoting/transport/netty4/http2/Http2ClientSettingsHandler.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.remoting.transport.netty4.http2;
+
+import org.apache.dubbo.common.logger.Logger;
+import org.apache.dubbo.common.logger.LoggerFactory;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http2.Http2SettingsFrame;
+import io.netty.util.concurrent.Promise;
+
+public class Http2ClientSettingsHandler extends 
SimpleChannelInboundHandler<Http2SettingsFrame> {
+
+    private static final Logger logger = 
LoggerFactory.getLogger(Http2ClientSettingsHandler.class);
+
+    private final AtomicReference<Promise<Void>> 
connectionPrefaceReceivedPromiseRef;
+
+    public Http2ClientSettingsHandler(AtomicReference<Promise<Void>> 
connectionPrefaceReceivedPromiseRef) {
+        this.connectionPrefaceReceivedPromiseRef = 
connectionPrefaceReceivedPromiseRef;
+    }
+
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, Http2SettingsFrame 
msg) throws Exception {
+        if (logger.isDebugEnabled()) {
+            logger.debug("Receive Http2 Settings frame of " + 
ctx.channel().localAddress() + " -> "
+                    + ctx.channel().remoteAddress());
+        }
+        // connectionPrefaceReceivedPromise will be set null after first used.
+        Promise<Void> connectionPrefaceReceivedPromise = 
connectionPrefaceReceivedPromiseRef.get();
+        if (connectionPrefaceReceivedPromise == null) {
+            ctx.fireChannelRead(msg);
+        } else {
+            // Notify the connection preface is received when first inbound 
http2 settings frame is arrived.
+            connectionPrefaceReceivedPromise.trySuccess(null);
+        }
+    }
+}

Reply via email to