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.
- 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. - 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. - 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