Netty HTTP2 Frame Forwarding / Proxing - вопрос конфигурации конвейера - PullRequest
1 голос
/ 01 мая 2019

Я пытаюсь создать POC Netty (4.1), который может пересылать кадры h2c (HTTP2 без TLS) на сервер h2c, то есть, по сути, создавать прокси-сервис Netty h2c.Wireshark показывает, что Netty отправляет кадры, и h2c-сервер отвечает (например, с заголовком ответа и данными), хотя у меня возникают некоторые проблемы с получением / обработкой HTTP-фреймов ответа внутри самой Netty.

В качестве отправной точки я адаптировал пример multiplex.server (io.netty.example.http2.helloworld.multiplex.server) так, чтобы в HelloWorldHttp2Handler вместо ответа на фиктивные сообщения я подключался к удаленному узлу:

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    Channel remoteChannel = null;

    // create or retrieve the remote channel (one to one mapping) associated with this incoming (client) channel
    synchronized (lock) {
        if (!ctx.channel().hasAttr(remoteChannelKey)) {
            remoteChannel = this.connectToRemoteBlocking(ctx.channel());
            ctx.channel().attr(remoteChannelKey).set(remoteChannel);
        } else {
            remoteChannel = ctx.channel().attr(remoteChannelKey).get();
        }
    }

    if (msg instanceof Http2HeadersFrame) {
        onHeadersRead(remoteChannel, (Http2HeadersFrame) msg);
    } else if (msg instanceof Http2DataFrame) {
        final Http2DataFrame data = (Http2DataFrame) msg;
        onDataRead(remoteChannel, (Http2DataFrame) msg);
        send(ctx.channel(), new DefaultHttp2WindowUpdateFrame(data.initialFlowControlledBytes()).stream(data.stream()));
    } else {
        super.channelRead(ctx, msg);
    }
}

private void send(Channel remoteChannel, Http2Frame frame) {
    remoteChannel.writeAndFlush(frame).addListener(new GenericFutureListener() {
        @Override
        public void operationComplete(Future future) throws Exception {
            if (!future.isSuccess()) {
                future.cause().printStackTrace();
            }
        }
    });
}

/**
 * If receive a frame with end-of-stream set, send a pre-canned response.
 */
private void onDataRead(Channel remoteChannel, Http2DataFrame data) throws Exception {
    if (data.isEndStream()) {
        send(remoteChannel, data);
    } else {
        // We do not send back the response to the remote-peer, so we need to release it.
        data.release();
    }
}

/**
 * If receive a frame with end-of-stream set, send a pre-canned response.
 */
private void onHeadersRead(Channel remoteChannel, Http2HeadersFrame headers)
        throws Exception {
    if (headers.isEndStream()) {
        send(remoteChannel, headers);
    }
}

private Channel connectToRemoteBlocking(Channel clientChannel) {
    try {
        Bootstrap b = new Bootstrap();
        b.group(new NioEventLoopGroup());
        b.channel(NioSocketChannel.class);
        b.option(ChannelOption.SO_KEEPALIVE, true);
        b.remoteAddress("localhost", H2C_SERVER_PORT);
        b.handler(new Http2ClientInitializer());

        final Channel channel = b.connect().syncUninterruptibly().channel();

        channel.config().setAutoRead(true);
        channel.attr(clientChannelKey).set(clientChannel);

        return channel;
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

Когдаинициализируя конвейер канала (в Http2ClientInitializer), если я делаю что-то вроде:

@Override
public void initChannel(SocketChannel ch) throws Exception {
    ch.pipeline().addLast(Http2MultiplexCodecBuilder.forClient(new Http2OutboundClientHandler()).frameLogger(TESTLOGGER).build());
    ch.pipeline().addLast(new UserEventLogger());
}

Тогда я могу видеть, что кадры корректно пересылаются в Wireshark, и сервер h2c отвечает с данными заголовка и кадра, ноNetty отвечает GOAWAY [INTERNAL_ERROR] из-за:

14: 23: 09.324 [nioEventLoopGroup-3-1] WARN inchannel.DefaultChannelPipeline - было вызвано событие exceptionCaught (), и оно достигнуто прихвост трубопровода.Обычно это означает, что последний обработчик в конвейере не обработал исключение.java.lang.IllegalStateException: Потоковый объект требуется для идентификатора: 1 в io.netty.handler.codec.http2.Http2FrameCodec $ FrameListener.requireStream (Http2FrameCodec.java:587) в io.netty.handler.codec.http2.Lodeer $ra.onHeadersRead (Http2FrameCodec.java:550) по адресу io.netty.handler.codec.http2.Http2FrameCodec $ FrameListener.onHeadersRead (Http2FrameCodec.java:543) ...

Если вместо этого сделать попыткуиметь конвейерную конфигурацию из примера клиента http2, например:

@Override
public void initChannel(SocketChannel ch) throws Exception {
    final Http2Connection connection = new DefaultHttp2Connection(false);

    ch.pipeline().addLast(
        new Http2ConnectionHandlerBuilder()
            .connection(connection)
            .frameLogger(TESTLOGGER)
            .frameListener(new DelegatingDecompressorFrameListener(connection, new InboundHttp2ToHttpAdapterBuilder(connection)
                .maxContentLength(maxContentLength)
                .propagateSettings(true)
                .build() ))
            .build());
}

Тогда вместо этого я получаю:

java.lang.UnsupportedOperationException: неподдерживаемый тип сообщения: DefaultHttp2HeadersFrame (ожидается: ByteBuf, FileRegion) по адресу io.netty.channel.nio.AbstractNioByteChannel.filterOutboundMessage (AbstractNioByteChannel.java:283) по адресу io.netty.channel.AbstractChannel $ AbstractUnsafe.write (AbstractChannel.java:882nel).HeadContext.write (DefaultChannelPipeline.java:1365)

Если я добавлю HTTPКодек с 2 кадрами (Http2MultiplexCodec или Http2FrameCodec):

@Override
    public void initChannel(SocketChannel ch) throws Exception {
        final Http2Connection connection = new DefaultHttp2Connection(false);

        ch.pipeline().addLast(
            new Http2ConnectionHandlerBuilder()
                .connection(connection)
                .frameLogger(TESTLOGGER)
                .frameListener(new DelegatingDecompressorFrameListener(connection, new InboundHttp2ToHttpAdapterBuilder(connection)
                    .maxContentLength(maxContentLength)
                    .propagateSettings(true)
                    .build() ))
                .build());

        ch.pipeline().addLast(Http2MultiplexCodecBuilder.forClient(new Http2OutboundClientHandler()).frameLogger(TESTLOGGER).build());
    }

Затем Netty отправляет два предисловия на соединение, в результате чего сервер h2c отклоняется с GOAWAY [PROTOCOL_ERROR]:

Wireshark showing two 'Magic' (HTTP2 preface) frames being sent, leading to an invalid request


Так вот, где у меня возникают проблемы - то есть настройка конвейера удаленного канала таким образом, что он будет отправлять объекты Http2Frame без ошибок, но также затем получать / обрабатыватьих обратно в Netty при получении ответа.

У кого-нибудь есть какие-либо идеи / предложения, пожалуйста?

1 Ответ

0 голосов
/ 07 мая 2019

Я закончил тем, что получил эту работу;Следующие проблемы Github содержат полезный код / ​​информацию:

Мне нужно исследовать еще несколько предостережений, хотя суть подхода заключается в том, что вам нужно обернуть ваш канал в Http2StreamChannel, что означает, что мой метод connectToRemoteBlocking() заканчивается как:

private Http2StreamChannel connectToRemoteBlocking(Channel clientChannel) {
        try {
            Bootstrap b = new Bootstrap();
            b.group(new NioEventLoopGroup()); // TODO reuse existing event loop
            b.channel(NioSocketChannel.class);
            b.option(ChannelOption.SO_KEEPALIVE, true);
            b.remoteAddress("localhost", H2C_SERVER_PORT);
            b.handler(new Http2ClientInitializer());

            final Channel channel = b.connect().syncUninterruptibly().channel();

            channel.config().setAutoRead(true);
            channel.attr(clientChannelKey).set(clientChannel);

            // TODO make more robust, see example at https://github.com/netty/netty/issues/8692
            final Http2StreamChannelBootstrap bs = new Http2StreamChannelBootstrap(channel);
            final Http2StreamChannel http2Stream = bs.open().syncUninterruptibly().get();
            http2Stream.attr(clientChannelKey).set(clientChannel);
            http2Stream.pipeline().addLast(new Http2OutboundClientHandler()); // will read: DefaultHttp2HeadersFrame, DefaultHttp2DataFrame

            return http2Stream;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

Тогда для предотвращения "Потокового объекта, необходимого для идентификатора: 1" ошибка (что по сути означает: 'Этот (клиентский) запрос HTTP2 является новым, так почему же у нас этоконкретный поток? ' - поскольку мы неявно повторно использовали объект потока из первоначально полученного «серверного» запроса), нам необходимо изменить использование потока удаленного канала при пересылке наших данных по:

private void onHeadersRead(Http2StreamChannel remoteChannel, Http2HeadersFrame headers) throws Exception {
        if (headers.isEndStream()) {
            headers.stream(remoteChannel.stream());
            send(remoteChannel, headers);
        }
    }

Затем настроенный обработчик входящего канала (который я назвал Http2OutboundClientHandler из-за его использования) будет получать входящие кадры HTTP2 обычным способом:

@Sharable
public class Http2OutboundClientHandler extends SimpleChannelInboundHandler<Http2Frame> {

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
        cause.printStackTrace();
        ctx.close();
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, Http2Frame msg) throws Exception {
        System.out.println("Http2OutboundClientHandler Http2Frame Type: " + msg.getClass().toString());
    }

}
...