1. 程式人生 > >JAVA寫HTTP代理伺服器(二)-netty實現

JAVA寫HTTP代理伺服器(二)-netty實現

上一篇,因為是用BIO的一個連線一個執行緒去處理,對於HTTP這種短連線協議來說CPU開銷是非常大的,就算加入了執行緒池也不能完美解決BIO的缺陷,所以可以用NIO進行伺服器的優化,NIO基於IO多路複用以實現單執行緒處理大量連線,但是編寫起來比較複雜,所以就選擇了netty,這裡就不在多敘netty是什麼了。

思路

netty有內建的http編解碼器,那就可以輕易做到不只是轉發原始資料,而是可以修改響應內容,當然僅限http代理,因為https代理的話私鑰都存在客戶端和目標伺服器上,代理伺服器只能捕獲到雙方的公鑰,無法解密成明文,除非代理伺服器製作證書,並實現SSL/TLS握手。

image.png

實現

EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup(2);
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option
(ChannelOption.SO_BACKLOG, 100) .option(ChannelOption.TCP_NODELAY, true) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<Channel>() { @Override protected void initChannel(Channel ch) throws Exception { ch.pipeline
().addLast("httpCodec",new HttpServerCodec()); ch.pipeline().addLast("httpObject",new HttpObjectAggregator(65536)); ch.pipeline().addLast("serverHandle",new HttpProxyServerHandle()); } }); ChannelFuture f = b .bind(port) .sync(); f.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); }
public class HttpProxyServerHandle extends ChannelInboundHandlerAdapter {

    private ChannelFuture cf;
    private String host;
    private int port;

    @Override
    public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
        if (msg instanceof FullHttpRequest) {
            FullHttpRequest request = (FullHttpRequest) msg;
            String host = request.headers().get("host");
            String[] temp = host.split(":");
            int port = 80;
            if (temp.length > 1) {
                port = Integer.parseInt(temp[1]);
            } else {
                if (request.uri().indexOf("https") == 0) {
                    port = 443;
                }
            }
            this.host = temp[0];
            this.port = port;
            if ("CONNECT".equalsIgnoreCase(request.method().name())) {//HTTPS建立代理握手
                HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, NettyHttpProxyServer.SUCCESS);
                ctx.writeAndFlush(response);
                ctx.pipeline().remove("httpCodec");
                ctx.pipeline().remove("httpObject");
                return;
            }
            //連線至目標伺服器
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(ctx.channel().eventLoop()) // 註冊執行緒池
                    .channel(ctx.channel().getClass()) // 使用NioSocketChannel來作為連線用的channel類
                    .handler(new HttpProxyInitializer(ctx.channel()));

            ChannelFuture cf = bootstrap.connect(temp[0], port);
            cf.addListener(new ChannelFutureListener() {
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (future.isSuccess()) {
                        future.channel().writeAndFlush(msg);
                    } else {
                        ctx.channel().close();
                    }
                }
            });
//            ChannelFuture cf = bootstrap.connect(temp[0], port).sync();
//            cf.channel().writeAndFlush(request);
        } else { // https 只轉發資料,不做處理
            if (cf == null) {
                //連線至目標伺服器
                Bootstrap bootstrap = new Bootstrap();
                bootstrap.group(ctx.channel().eventLoop()) // 複用客戶端連線執行緒池
                        .channel(ctx.channel().getClass()) // 使用NioSocketChannel來作為連線用的channel類
                        .handler(new ChannelInitializer() {

                            @Override
                            protected void initChannel(Channel ch) throws Exception {
                                ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                                    @Override
                                    public void channelRead(ChannelHandlerContext ctx0, Object msg) throws Exception {
                                        ctx.channel().writeAndFlush(msg);
                                    }
                                });
                            }
                        });
                cf = bootstrap.connect(host, port);
                cf.addListener(new ChannelFutureListener() {
                    public void operationComplete(ChannelFuture future) throws Exception {
                        if (future.isSuccess()) {
                            future.channel().writeAndFlush(msg);
                        } else {
                            ctx.channel().close();
                        }
                    }
                });
            } else {
                cf.channel().writeAndFlush(msg);
            }
        }
    }


}
public class HttpProxyInitializer extends ChannelInitializer{

    private Channel clientChannel;

    public HttpProxyInitializer(Channel clientChannel) {
        this.clientChannel = clientChannel;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ch.pipeline().addLast(new HttpClientCodec());
        ch.pipeline().addLast(new HttpObjectAggregator(6553600));
        ch.pipeline().addLast(new HttpProxyClientHandle(clientChannel));
    }
}
public class HttpProxyClientHandle extends ChannelInboundHandlerAdapter {

    private Channel clientChannel;

    public HttpProxyClientHandle(Channel clientChannel) {
        this.clientChannel = clientChannel;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        FullHttpResponse response = (FullHttpResponse) msg;
        //修改http響應體返回至客戶端
        response.headers().add("test","from proxy");
        clientChannel.writeAndFlush(msg);
    }
}

後記

netty框架下開發簡單的高效能http代理伺服器,並且用內建的http編解碼器實現了響應體的修改。後續會再深入HTTPS明文捕獲,由代理伺服器製作CA證書,並實現與目標伺服器SSL/TLS握手。
程式碼託管在github上,歡迎start