如何使用netty编写http代理

Jet*_*Jet 6 java proxy netty

我想使用netty编写一个简单的程序来代理浏览器发送的http请求。我觉得可以分为3个步骤

  1. 获取浏览器发送的请求
  2. 发送到网站
  3. 从网站接收数据并将其发送回浏览器。

题:

  1. 使用 Bootstrap.connect(host, port) 时如何将 url 转换为主机和端口;
  2. 当我使用 HttpServerResponseHandler.connect 和 ChannelHandlerContext.writeAndFlush(httpMessage); 要将数据发送到网站,如何从网站获取响应数据并将其发送回浏览器?

这是我学习netty的第一天,所以请尽量回答简单。非常感谢。

public class Server {
    public static void main(String[] args) throws InterruptedException {
        final int port = 8888;

        // copy from https://github.com/netty/netty/wiki/User-guide-for-4.x
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new HttpRequestDecoder(), new HttpServerRequestHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            // Bind and start to accept incoming connections.
            ChannelFuture f = b.bind(port).sync();

            // Wait until the server socket is closed.
            // In this example, this does not happen, but you can do that to gracefully
            // shut down your server.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
} 
Run Code Online (Sandbox Code Playgroud)
public class HttpServerRequestHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // step 1 get data from browser
        if (msg instanceof LastHttpContent) {
            ctx.close();
            return;
        }
        DefaultHttpRequest httpMessage = (DefaultHttpRequest) msg;
        System.out.println("?????====================");
        System.out.println(msg);
        System.out.println();
        doWork(ctx, httpMessage);
    }

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

    private void doWork(ChannelHandlerContext ctx, final DefaultHttpRequest msg) {
        // step 2 send data to website
        // translate url into host and port
        String host = msg.uri();
        int port = 80;
        if (host.startsWith("https://")) {
            host = host.replaceFirst("https://", "");
            port = 443;
        } else if (host.startsWith("http://")) {
            host = host.replaceFirst("http://", "");
            port = 80;
        }
        if (host.contains(":443")) {
            host = host.replace(":443", "");
            port = 443;
        }

        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(workerGroup);
            b.channel(NioSocketChannel.class);
            //b.option(ChannelOption.AUTO_READ, true);
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new HttpServerResponseHandler(msg), new HttpRequestEncoder());
                }
            });

            // question 1
            ChannelFuture f = b.connect(host, port).sync();
            //ChannelFuture f = b.connect("www.baidu.com", 443).sync();
            f.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)
public class HttpServerResponseHandler extends ChannelOutboundHandlerAdapter {

    private Object httpMessage;

    public HttpServerResponseHandler(Object o) {
        this.httpMessage = o;
    }


    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        System.out.println("??????=========================");
        System.out.println(httpMessage);
        System.out.println();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
        cause.printStackTrace();
        ctx.close();
    }

    @Override
    public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,
                        SocketAddress localAddress, ChannelPromise promise) throws Exception {
        System.out.println("connect !!!!!!!!!!!");
        // question 2
        ctx.writeAndFlush(httpMessage);
    }
}
Run Code Online (Sandbox Code Playgroud)

Abh*_*kar 6

很巧的是,出于学习目的,我也一直在做Netty代理服务器。我有一个完整的工作代码,您可以在我的GitHub上找到,但我将在这里回答您的问题。Netty 也有一个官方代理服务器示例但与我的代码不同,它们没有单元测试。

(仅供参考,我的代码是用 Kotlin 编写的)。

核心理念

创建代理服务器时,您需要一个服务器来接受客户端请求,以及一个用于远程代理的客户端。您创建了服务器,但没有创建客户端。最好重用EventLoop服务器创建的内容,而不是为客户端创建新的内容。每个事件循环都在专用线程上运行,因此创建更多事件循环会产生额外的线程,在接受者Channel和客户端之间交换数据时需要上下文切换Channel

如何将url转换为主机和端口

为了简单起见,我使用了HttpObjectAggregator将 anHttpMessage及其后续内容聚合HttpContents为单个FullHttpRequestor FullHttpResponse(取决于它是否用于处理请求或响应)。设置 URL 很简单:只需调用FullHttpRequest.setUri即可。

要获取主机和端口,请在客户端通道上调用Channel.remoteAddress()并将结果转换SocketAddressInetSocketAddress,从中您可以获取主机和端口。不要忘记类似地重置Host标题(如果存在)。

我怎样才能得到响应数据

建立客户端通道(您缺少该通道)后,您需要在该通道上发出请求。客户端通道有一个处理程序,其中包含对原始服务器通道的引用。一旦处理程序收到响应,它将其写入服务器通道。