承接前两篇用java原生nio和bio写的https代理服务器,这篇是用netty实现https代理服务器。上篇用nio实现代理服务器时需要自己去控制消息发送和接收的次序,我们就用了一个selector,这个次序是所有注册在我们所有的io channel注册在这个统一的selector上就绪后的次序,那么你就必须自己严格控制客户端到代理服务器的channel和代理服务器到目标服务器的channel的channel读取信息以及写入信息的时机。为什么这么说,因为,在nio中需要你自己去控制读写buffer的起始读取位置及写入位置,这也就需要你去严格控制读取和写入流程及buffer位置的严格把控;而且在使用nio时会发现对channel的准备事件监听不好控制,为什么这么说,因为在很多的情况下,channel都是可写入的如果你一直将selector置为对channel的可写入感兴趣,那么计算机cpu就会占用很高,那么我们就需要自己去在合适的时候去启动selector对channel的可写入感兴趣操作,使之写入内容;当然,在上篇博文中我全程使用的是单线程,如果你要是使用多线程,初始化时生成多个selector,让客户端到代理服务器的channel和代理服务器到目标服务器的channel注册到两个不同的selector上时,那么你还需要去严格控制这两个channel的并发,以防止buffer数据的混乱。
这篇博文就来用netty实现下http(s)代理服务器,大家可以对比下两者使用(这里没有使用netty对http的封装的组件的,使用的还是比较底层的buffer,这里就方便理解netty和对比两者编程的优劣)。代码中我会给我认为比较重要的地方写上注释,然后,再挑一些出来讲讲,我认为你弄懂这些之后会少走很多弯路并且你能够去使用netty去实现http(s)代理服务器了。
public class NettyProxyHttpServer {
public static void main(String[] s) {
System.out.println("<<<<<<<<<<<<<<<<<");
NioEventLoopGroup bossGroup = new NioEventLoopGroup(8);
NioEventLoopGroup workGroup = new NioEventLoopGroup(8);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new NettyProxyServerHandler());
}
}).option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
try {
b.bind(11111).sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class NettyProxyServerHandler extends ChannelInboundHandlerAdapter{
private Map<Channel,Channel> channelMap = new HashMap<>();
private Map<Channel,ByteBuf> msgMap = new HashMap<Channel,ByteBuf>();//之所以保留这个map是担心,第一次建立连接时一次性无法获取客户端发来的全部信息
NioEventLoopGroup toServerGroup = new NioEventLoopGroup();
private Bootstrap bootstrap = new Bootstrap();
@SuppressWarnings("rawtypes")
public NettyProxyServerHandler() {
//原来在这里我是想用serverbootstrap 下的childGroup去注册我自己直接新建的Channel的,结果发现根本注册不了,需要新生成一个bootstrap
bootstrap.group(toServerGroup).channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast("toServer handler", new ToServerHandler(channelMap));
}
});
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
if(channelMap.containsKey(ctx.channel())) {
channelMap.remove(ctx.channel());
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Channel channel = ctx.channel();
if(channelMap.containsKey(channel)&&channelMap.get(channel) !=null) {
Channel toChannel = channelMap.get(channel);
toChannel.writeAndFlush(msg);
}else {
ByteBuf buffer = null;
if(msgMap.containsKey(channel)&&msgMap.get(channel)!=null) {
buffer = msgMap.get(channel);
}else {
buffer = ctx.alloc().buffer(1024*2);
}
buffer.writeBytes((ByteBuf) msg);
buffer.retain();
msgMap.put(channel, buffer);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
final Channel channel = ctx.channel();
if(!(channelMap.containsKey(channel)&&channelMap.get(channel)!=null)) {//如果是还没建立连接,需要对目标host和port进行解析
if(msgMap.get(channel)!=null) {
byte[] b =new byte[msgMap.get(channel).readableBytes()];
msgMap.get(channel).getBytes(0, b);
String header = new String(b);
String[] lineStrs = header.split("\\n");
String host="";
int port = 80;
int type=0; //默认是http方式
String hostTemp="";
for(int i=0 ; i<lineStrs.length ; i++) { //解析请求头
System.out.println(lineStrs[i]);
if(i==0) {
type = (lineStrs[i].split(" ")[0].equalsIgnoreCase("CONNECT") ? 1 : 0);
}else {
String[] hostLine = lineStrs[i].split(": ");
if(hostLine[0].equalsIgnoreCase("host")) {
hostTemp = hostLine[1];
}
}
}
if(hostTemp.split(":").length>1) {
host = hostTemp.split(":")[0];
port = Integer.valueOf(hostTemp.split(":")[1].split("\\r")[0]);
}else {
host = hostTemp.split(":")[0].split("\\r")[0];
}
final int requestType = type;
ChannelFuture future = bootstrap.connect(host, port).sync();
if(future.isSuccess()) { //建立到目标服务器的连接成功,把两者的连接映射放到map,方便后续使用
channelMap.put(channel, future.channel());
channelMap.put(future.channel(), channel);
if(requestType==1) { //https请求的话,直接返回给客户端下面这句话就行,客户端会在建立的通道上继续请求。
ByteBuf buffer = channel.alloc().buffer("HTTP/1.1 200 Connection Established\r\n\r\n".getBytes().length);
buffer.writeBytes("HTTP/1.1 200 Connection Established\r\n\r\n".getBytes());
channel.writeAndFlush(buffer);
msgMap.get(channel).release();
}else {
future.channel().writeAndFlush(msgMap.get(channel));
msgMap.get(channel).release();
}
System.out.println("=======连接建立成功");
}else {
System.out.println("=========connect failing");
}
}
}
}
}
class ToServerHandler extends ChannelInboundHandlerAdapter{
private Map<Channel,Channel> map = null;
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("代理到目标服务器的channel handler出错:");
cause.printStackTrace();
if(map.containsKey(ctx.channel())) {
map.remove(ctx.channel());
}
}
public ToServerHandler(Map<Channel, Channel> map) {
this.map = map;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Channel channel = ctx.channel();
Channel toChannel = map.get(channel);
toChannel.writeAndFlush(msg);
}
}
使用netty 开发的过程中,我印象比较深或说做的比较大的一个改动是下面这段代码,下面我们分析一下。
ByteBuf buffer = channel.alloc().buffer("HTTP/1.1 200 Connection Established\r\n\r\n".getBytes().length);
buffer.writeBytes("HTTP/1.1 200 Connection Established\r\n\r\n".getBytes());
channel.writeAndFlush(buffer);
msgMap.get(channel).release();
也许你不会遇到,因为假如你不会用我这种方式去暂时存放channel读出的信息,但是我觉得很幸运,因为能够有这样的错误来加深一下我的认知——在netty中要想往channel中写入信息,必须通过netty中channel的buffer (这里用的ByteBuf)往channel里写入,而不能直接用byte数组作为媒介,那样是写不进数据的,亲测过,这个坑也是耗费了很长时间的。这里也可以这么写,在实现ChannelInboundAdapter时往这个对象里传入将要写入的目的channel 这样就可以直接使用direct buffer了。我没有改过来的原因是我认为有可能会由于某种原因导致在第一次建立连接时有可能会一次读不完http头部信息,虽然很不可能,但是万一由于网络延迟或服务器硬件繁忙等原因导致一次没读完,那么就无法利用这种方式了。
当然,还有一点不是完美的地方是这里的channel的对应关系都存在于一个map中,其实这个map也可以去掉的,像上面介绍buffer的那样——在实现ChannelInboundAdapter时往这个对象里传入将要写入的目的channel 即可。
好了,这次netty实现代理服务器就算结束了,大家可以自己试试。有机会,会讲讲netty源码并且自己也试着用nio封装一个类似netty的东西出来