netty初使用——实现http及https代理服务器

        承接前两篇用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的东西出来

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Netty是一款基于NIO的网络编程框架,提供了高效、稳定、灵活的网络编程能力。使用Netty实现代理服务器可以简化开发过程,提高性能和可维护性。 以下是使用Netty实现代理服务器的示例代码: ``` import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.channel.*; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.http.*; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.stream.ChunkedWriteHandler; public class ProxyServer { public static void main(String[] args) throws Exception { EventLoopGroup workerGroup = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(workerGroup) .channel(NioSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO)) .option(ChannelOption.AUTO_READ, false) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new HttpClientCodec()); ch.pipeline().addLast(new HttpObjectAggregator(65536)); ch.pipeline().addLast(new ChunkedWriteHandler()); ch.pipeline().addLast(new ProxyServerHandler()); } }); ChannelFuture future = bootstrap.connect("www.example.com", 80).sync(); future.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); } } private static class ProxyServerHandler extends ChannelInboundHandlerAdapter { private Channel remoteChannel; @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { remoteChannel = ctx.channel(); ctx.read(); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof HttpRequest) { HttpRequest request = (HttpRequest) msg; String host = request.headers().get("Host"); ChannelFuture future = new Bootstrap() .group(ctx.channel().eventLoop()) .channel(ctx.channel().getClass()) .handler(new LoggingHandler(LogLevel.INFO)) .option(ChannelOption.AUTO_READ, false) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new HttpResponseDecoder()); ch.pipeline().addLast(new HttpObjectAggregator(65536)); ch.pipeline().addLast(new ChunkedWriteHandler()); ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush(request); ctx.read(); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof HttpResponse) { HttpResponse response = (HttpResponse) msg; response.headers().remove("Transfer-Encoding"); response.headers().remove("Content-Length"); remoteChannel.writeAndFlush(response); remoteChannel.writeAndFlush(new ChunkedNioStream((ByteBuf) msg)); } else if (msg instanceof HttpContent) { remoteChannel.writeAndFlush(new ChunkedNioStream((ByteBuf) msg)); if (msg instanceof LastHttpContent) { remoteChannel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) .addListener(ChannelFutureListener.CLOSE); } } } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { remoteChannel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) .addListener(ChannelFutureListener.CLOSE); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); remoteChannel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) .addListener(ChannelFutureListener.CLOSE); } }); } }) .connect(host, 80); remoteChannel.config().setAutoRead(false); future.addListener((ChannelFutureListener) future1 -> { if (future1.isSuccess()) { remoteChannel.config().setAutoRead(true); ctx.channel().config().setAutoRead(true); } else { remoteChannel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) .addListener(ChannelFutureListener.CLOSE); ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) .addListener(ChannelFutureListener.CLOSE); } }); } else if (msg instanceof HttpContent) { remoteChannel.writeAndFlush(new ChunkedNioStream((ByteBuf) msg)); if (msg instanceof LastHttpContent) { remoteChannel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); } } } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { if (remoteChannel != null) { remoteChannel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) .addListener(ChannelFutureListener.CLOSE); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); if (remoteChannel != null) { remoteChannel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) .addListener(ChannelFutureListener.CLOSE); } ctx.close(); } } } ``` 以上代码中,代理服务器连接到目标服务器的IP地址和端口号是硬编码的,你需要根据实际情况进行修改。在启动代理服务器之后,当客户端发送HTTP请求时,会在一个新的线程中处理请求,解析请求并连接到目标服务器,将请求转发给目标服务器。接收到目标服务器的响应后,将响应转发给客户端。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值