Http proxy server - Netty version

Posted by smonsivaes on Tue, 01 Mar 2022 16:34:52 +0100

Basic ideas

First, explain the basic ideas. The following figure shows the location of the proxy server:

From the position of the proxy server, its functions have three points

  • Receive data from the requester
  • Forward request to target server
  • Forward the data of the target server to the requester

Therefore, when we use Netty to build a proxy server, we need to meet the above three functions. Let's think about how to realize these three functions.

  1. How to receive data from the requester?
    To receive Http requests, it is natural to use Netty to build an Http server, Http codec, aggregator, etc.
  2. How do I forward requests to the target server?
    How can I initiate a request to the target server?
    Create a client that can also send Http requests.
    To whom? What was sent to the server?
    In 1), we have received the Request from the requester and send it to the target server using the client.
  3. How to forward the data returned by the target server to the requester?
    After the request is sent, the server must have returned data. How can the proxy server return these data to the requester?
    When the requester establishes a connection with the proxy server, a Channel will be generated, indicating the connection between the two. With this Channel, the data returned by the target server can be forwarded to the requester.

Following the above ideas, the following design drawings can be generated:

code implementation

Netty's startup service

Basically, there are two points:

  • Http server
  • After receiving the request, how to create a client and forward information [this part is the logic of HttpProxyHandler]
public class NettyBootstrapService {

    public void start() throws InterruptedException {
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();

        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(boss, worker)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 1024)
                .option(ChannelOption.TCP_NODELAY, false)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        //Http codec
                        pipeline.addLast(ChannelHandlerDefine.HTTP_CODEC,new HttpServerCodec());
                        pipeline.addLast(ChannelHandlerDefine.HTTP_AGGREGATOR,new HttpObjectAggregator(100*1024*1024));
                        //Http proxy service
                        pipeline.addLast(ChannelHandlerDefine.HTTP_PROXY,new HttpProxyHandler());
                    }
                });
        ChannelFuture bindFuture = serverBootstrap.bind(8080).sync();

        try {
            bindFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

Http proxy service processing

Agency service

  • Create Http request client
  • Forward the request to the target machine through the client [this part is the responsibility of DataTranHandler]

DataTransHandler is to complete step 3) and forward the data of the target server to the requester.
It should be noted that when the path connecting the requesting side < – > proxy server < – > target server is established, the Http codec will be cancelled to make the data completely transparent transmission. Of course, you can capture data and conduct various interception operations here.

public class HttpProxyHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        //Get the destination address from the request header
        //The request header is negotiated between the sender and the proxy service, or the common request header host is used
        String hostAddress = request.headers().get("agent");
        if (StringUtil.isNullOrEmpty(hostAddress)) {
            ctx.writeAndFlush(getResponse(HttpResponseStatus.BAD_REQUEST, "Destination address is empty")).addListener(ChannelFutureListener.CLOSE);
            return;
        }
        //Modify destination address
        request.headers().set(HttpHeaderNames.HOST, hostAddress);

        ReferenceCountUtil.retain(request);

        //Create client connection target machine
        connectToRemote(ctx, hostAddress, 80, 1000).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                if (channelFuture.isSuccess()) {
                    //The proxy server successfully connected to the target server
                    //Send message to target server
                    //Close long connection
                    request.headers().set(HttpHeaderNames.CONNECTION, "close");

                    //Forward request to target server
                    channelFuture.channel().writeAndFlush(request).addListener(new ChannelFutureListener() {
                        @Override
                        public void operationComplete(ChannelFuture channelFuture) throws Exception {
                            if (channelFuture.isSuccess()) {
                                //Remove the client's http codec
                                channelFuture.channel().pipeline().remove(ChannelHandlerDefine.HTTP_CLIENT_CODEC);
                                //Remove the http codec and aggregator between the proxy service and the requester channel
                                ctx.channel().pipeline().remove(ChannelHandlerDefine.HTTP_CODEC);
                                ctx.channel().pipeline().remove(ChannelHandlerDefine.HTTP_AGGREGATOR);
                                //After removal, let the channel directly become a simple ByteBuf transmission
                            }
                        }
                    });
                } else {
                    ReferenceCountUtil.retain(request);
                    ctx.writeAndFlush(getResponse(HttpResponseStatus.BAD_REQUEST, "The proxy service failed to connect to the remote service"))
                            .addListener(ChannelFutureListener.CLOSE);
                }
            }
        });
    }

    private DefaultFullHttpResponse getResponse(HttpResponseStatus statusCode, String message) {
        return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, statusCode, Unpooled.copiedBuffer(message, CharsetUtil.UTF_8));
    }

    private ChannelFuture connectToRemote(ChannelHandlerContext ctx, String targetHost, int targetPort, int timeout) {
        return new Bootstrap().group(ctx.channel().eventLoop())
                .channel(NioSocketChannel.class)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, timeout)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        //Add http encoder
                        pipeline.addLast(ChannelHandlerDefine.HTTP_CLIENT_CODEC, new HttpClientCodec());
                        //Add a data transmission channel
                        pipeline.addLast(new DataTransHandler(ctx.channel()));
                    }
                })
                .connect(targetHost, targetPort);
    }
}

Data transmission service

This part realizes forwarding the data of the target server to the requester

channel is the connection between the proxy service and the requester. Through it, the proxy server can transmit data to the requester.

public class DataTransHandler extends ChannelInboundHandlerAdapter {

    private Channel channel;

    public DataTransHandler(Channel channel) {
        this.channel = channel;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (!channel.isOpen()) {
            ReferenceCountUtil.release(msg);
            return;
        }
        channel.writeAndFlush(msg);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //The target server is disconnected from the proxy server
        //The proxy server is also disconnected from the original server
        if (channel != null) {
            //Send an empty buf and close the channel through listener monitoring to ensure that the data transmission in the channel is completed
            channel.writeAndFlush(PooledByteBufAllocator.DEFAULT.buffer()).addListener(ChannelFutureListener.CLOSE);
        }
        super.channelInactive(ctx);
    }
}

summary

Let's review the three basic functions of proxy server

  • Http request received
  • Be able to send Http requests to the target server
  • It can receive the data returned by the target server and transfer it back to the request

Around these three points, a simple Http proxy server is done

Topics: Database Netty Redis Cache