如何使用netty编写http代理

时间:2017-09-11 13:33:09

标签: java proxy netty

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

  1. 通过浏览器发送请求
  2. 将其发送到网站
  3. 从网站接收数据并将其发送回浏览器。
  4. 问题:

    1. 当我使用Bootstrap.connect(host,port)时,如何将url转换为主机和端口;
    2. 当我使用HttpServerResponseHandler.connect和ChannelHandlerContext.writeAndFlush(httpMessage)时;要将数据发送到网站,我如何从网站获取响应数据并将其发送回浏览器?
    3. 这是我学习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);
          }
      }
      

1 个答案:

答案 0 :(得分:2)

巧合的是,我也一直在研究Netty代理服务器以便学习。我已经在GitHub找到了一个完整的代码,但我会在这里回答您的问题。 Netty还有一个官方代理服务器示例here,但与我的代码不同,他们没有单元测试。

(仅供参考,我的代码在Kotlin中)。

核心理念

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

  

如何将网址翻译成主机和端口

为了简单起见,我使用HttpObjectAggregatorHttpContents及其后续FullHttpRequest汇总到一个FullHttpResponseSocketAddress (取决于它是否用于处理请求或响应)。设置URL很简单:只需拨打FullHttpRequest.setUri即可。

要获取主机和端口,请在客户端通道上调用Channel.remoteAddress()并将生成的InetSocketAddress强制转换为Host,您可以从中获取主机和端口。如果存在,请不要忘记同样重置{{1}}标题。

  

如何获取响应数据

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