如何使用Netty编写HTTP代理

6
我想使用Netty编写一个简单的程序,用于代理浏览器发送的HTTP请求。我认为它可以分为三个步骤:
1. 获取浏览器发送的请求。
2. 将请求发送到网站。
3. 接收网站数据并将其发送回浏览器。
问题:
1. 当我使用Bootstrap.connect(主机,端口)时,如何将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();
        }
    }
} 

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();
        }
    }
}

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);
    }
}

你能修好它吗? - acid_srvnn
@AcidBurn 很难修复它。你可以看下面的答案寻求帮助。 - Jet
1个回答

6
巧合的是,我也在开发一个Netty代理服务器以便学习。你可以在我的GitHub上找到我完整的可用代码,但我会在这里回答你的问题。Netty还有一个官方代理服务器示例这里,但与我的代码不同,他们没有单元测试。

(FYI, 我的代码是用Kotlin写的)。

核心思路

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

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

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

要获取主机和端口,请在客户端通道上调用Channel.remoteAddress()并将结果的SocketAddress转换为InetSocketAddress,从而可以获得主机和端口。如果存在类似的Host头,请不要忘记同样重置它。

如何获取响应数据

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


@Abhijit,你能看一下这个问题是否有什么输入吗?https://stackoverflow.com/questions/55483863/do-i-need-a-custom-encoder-implementation-if-i-have-to-process-httprequest-befor - brain storm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接