JAVA寫HTTP代理伺服器(二)-netty實現
阿新 • • 發佈:2019-02-04
接上一篇,因為是用BIO的一個連線一個執行緒去處理,對於HTTP這種短連線協議來說CPU開銷是非常大的,就算加入了執行緒池也不能完美解決BIO的缺陷,所以可以用NIO進行伺服器的優化,NIO基於IO多路複用以實現單執行緒處理大量連線,但是編寫起來比較複雜,所以就選擇了netty,這裡就不在多敘netty是什麼了。
思路
netty有內建的http編解碼器,那就可以輕易做到不只是轉發原始資料,而是可以修改響應內容,當然僅限http代理,因為https代理的話私鑰都存在客戶端和目標伺服器上,代理伺服器只能捕獲到雙方的公鑰,無法解密成明文,除非代理伺服器製作證書,並實現SSL/TLS握手。
實現
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