Netty网络编程实战 - 手写同时兼容SSL、支持压缩和解压缩、报文格式自定义的Http监听器

前言

一个完整的Http请求包括客户端(常常为浏览器)请求和服务器响应两大部分,那么你清楚在这个过程中底层都做了哪些事情吗?又如HTTP请求的短连接和长连接底层的区别是什么?再如何基于Netty定制开发符合特定业务场景的HTTP监听器 ... 等等这些问题都是今天我们要解决的问题。


HTTP请求

一次完整的HTTP请求需要经历以下过程:

image.png

其中在HTTP1.1及以上版本,开启keep-alive, 步骤1和步骤7只做一次。

步骤2和步骤3中请求的报文结构如下:

image.png

步骤4~步骤6的响应报文结构如下:

image.png



HTTP短连接和长连接

短链接执行流程

image.png

HTTP 是无状态的,浏览器和服务器每进行一次 HTTP 操作,就建立一次连接, 但任务结束就中断连接。


长连接执行流程

image.png

注: 使用http1.0开启keep-alived或http1.1 时,虽保持了TCP的长连接(默认300s), http请求的信息和状态是不会保存的,客户端仍然需使用额外的手段缓存这些信息如:Session,Cookie等;未改变http请求单向和无状态的特性;

可能的使用场景

长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况,。每个 TCP 连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话 那么处理速度会降低很多,所以每个操作完后都不断开,处理时直接发送数据包 就 OK 了,不用建立 TCP 连接。

数据库的连接用长连接, 如果用短连接频繁的通信会造成 socket 错 误,而且频繁的 socket 创建也是对资源的浪费。

而像 WEB 网站的 http 服务一般都用短链接,因为长连接对于服务端来说会 耗费一定的资源,而像 WEB 网站这么频繁的成千上万甚至上亿客户端的连接用 短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个 用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁 操作情况下需用短连好。


Netty基于HTTP包装介绍

Netty在HTTP请求和包装上,典型的包括:

image.png

以下我们就来应用Netty为我们提供的开箱即用的功能完成我们的设想。


代码设计实现

/**
* @author andychen https://blog.51cto.com/14815984
* @description:HTTP监听器启动类
*/
public class HttpListener {
   //主线程组
   public static final EventLoopGroup mainGroup = new NioEventLoopGroup();
   //工作线程组
   public static final EventLoopGroup workGroup = new NioEventLoopGroup();
   //启动对象
   public static final ServerBootstrap bootStrap = new ServerBootstrap();
   /**
    * 监听器启动入口
    * @param args
    */
   public static void main(String[] args) {
        if(0 < args.length) {
            try {
                //监听器主机
                final String host = args[0];
                //监听端口
                final int port = Integer.parseInt(args[1]);
                //证书文件
                String certFileName = args[2].trim();
                //私钥文件
                String keyFileName = args[3].trim();
                final ChannelFuture future = bootStrap.group(mainGroup, workGroup)
                        .channel(NioServerSocketChannel.class)
                        .localAddress(new InetSocketAddress(host, port))
                        .childHandler(new ChannelInitializerExt(certFileName, keyFileName))
                        .bind().sync();
                System.out.println("监听端:"+port+"已启动...");
                future.channel().closeFuture().sync();//阻塞至通道关闭
            }catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                mainGroup.shutdownGracefully();
                workGroup.shutdownGracefully();
            }
        }
   }
}

/**
* @author andychen https://blog.51cto.com/14815984
* @description:ChannelInitializer通道初始化器扩展类
*/
public class ChannelInitializerExt extends ChannelInitializer<Channel> {
   /**
    * 证书全名称(包含路径)
    */
   private final String cerFileName;
   /**
    * 证书私钥(包括路径)
    */
   private final String keyFileName;
   public ChannelInitializerExt(String cerFileName, String keyFileName) {
       this.cerFileName = cerFileName;
       this.keyFileName = keyFileName;
   }
   /**
    * 通道初始化
    * 初始化各种ChannelHandler
    * @param channel
    * @throws Exception
    */
   protected void initChannel(Channel channel) throws Exception {
       ChannelPipeline pipeline = channel.pipeline();
       /**
        * 添加入站请求处理,同时兼容http和https请求
        */
       pipeline.addFirst(new ChannelInboundHandlerAdapter(){
           @Override
           public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
               ByteBuf buf = (ByteBuf)msg;
               //判断协议头:https数据流的第一位是十六进制“16”,转换成十进制是22
               if(Constant.FIRST_BYTE_VAL == buf.getByte(0)){
                   //SSL支持
                   SslContext context = buildSslContext(cerFileName, keyFileName);
                   SSLEngine engine = context.newEngine(UnpooledByteBufAllocator.DEFAULT);
                   pipeline.addBefore("encoder_decoder", "ssl", new SslHandler(engine));
               }
               ctx.pipeline().remove(this);
               super.channelRead(ctx, msg);
           }
       });
       //包括HttpRequestDecoder解码器和HttpResponseEncoder编码器
       pipeline.addLast("encoder_decoder", new HttpServerCodec());
       //handler聚合,此handler必须
       pipeline.addLast("aggregator", new HttpObjectAggregator(Constant.MAX_CONTENT_LEN));
       //支持压缩传输
       pipeline.addLast("compressor", new HttpContentCompressor());
       //业务handler
       pipeline.addLast(new HttpChannelHandler());
   }
   /**
    * 构建ssl上下文
    * @param certFileName 证书文件名
    * @param keyFileName 证书私钥
    * @return
    * @throws SSLException
    */
   private static SslContext buildSslContext(final String certFileName, final String keyFileName) throws SSLException {
       File crtFile = null;
       File keyFile = null;
       try {
           crtFile = new File(certFileName);
           keyFile = new File(keyFileName);
//            /**
//             * 方式一:采用内置自带证书(适合用于本地测试)
//             */
//            SelfSignedCertificate ssc = new SelfSignedCertificate();
//            return SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
           /**
            * 方式二:映射安全证书和KEY
            */
           return SslContextBuilder.forServer(crtFile, keyFile)
                   .clientAuth(ClientAuth.NONE)
                   .sslProvider(SslProvider.OPENSSL)
                   .build();
       }finally {
           crtFile = null;
           keyFile = null;
       }
   }
}

/**
* @author andychen https://blog.51cto.com/14815984
* @description:HTTP监听器业务处理器
*/
public class HttpChannelHandler extends ChannelInboundHandlerAdapter {
   /**
    * 测试请求地址
    */
   private static final String REQ_URL = "/index";
   //请求名称
   private static final String REQ_PARA_NAME = "name";

   /**
    * 监听器接收网络数据
    * @param ctx 通道上下文
    * @param msg 消息
    * @throws Exception
    */
   @Override
   public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
       String data = null;
       //http请求
       FullHttpRequest request = (FullHttpRequest)msg;
       //(1)请求地址
       String uri = request.uri();
       /*
           验证请求是否为约定地址,这里可以做成各种请求映射表
           这里只说明思路
        */
       if(!uri.startsWith(REQ_URL)){
           data = Constant.HTML_TEMP.replace("{0}", "请求地址:["+uri+"]不存在(404)");
           this.response(ctx, data, HttpResponseStatus.NOT_FOUND);
           return;
       }
       //解析请求参数
       Map<String, String> params = parseRequestPara(uri);
       if(!params.containsKey(REQ_PARA_NAME)){
           data = Constant.HTML_TEMP.replace("{0}", "请求参数错误(401)");
           this.response(ctx, data, HttpResponseStatus.BAD_REQUEST);
           return;
       }
       //****其它验证逻辑*******
       //(2)请求头
       HttpHeaders headers = request.headers();
       System.out.println("请求头:"+headers);
       //(2)请求主体
       String body = request.content().toString(CharsetUtil.UTF_8);
       System.out.println("请求body:"+body);
       //(3)请求方法
       HttpMethod method = request.method();
       //(4)处理请求
       this.proce***equest(ctx, method);
   }

   /**
    * 异常捕获
    * @param ctx 处理器上下文
    * @param cause
    * @throws Exception
    */
   @Override
   public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
       cause.printStackTrace();
       ctx.close();
   }

   /**
    * 解析请求参数
    * @param uri 请求地址
    */
   private static Map<String,String> parseRequestPara(final String uri) {
       Map<String,String> map = new HashMap<>();
       QueryStringDecoder decoder = new QueryStringDecoder(uri);
       decoder.parameters().entrySet().forEach(entry -> {
           map.put(entry.getKey(), entry.getValue().get(0));
       });
       System.out.println("请求参数:"+decoder.parameters());

       return map;
   }

   /**
    * 处理HTTP请求
    * @param method 方法
    * @return
    */
   private void proce***equest(final ChannelHandlerContext ctx, final HttpMethod method){
       Random r = new Random();
       String content = Constant.ARTICLES[r.nextInt(Constant.ARTICLES.length)];

       //处理GET请求
       if(HttpMethod.GET.equals(method)){
           this.response(ctx, content, HttpResponseStatus.OK);
           return;
       }
       //处理POST请求
       if(HttpMethod.POST.equals(method)){
           //其它逻辑...
           return;
       }
       //PUT请求
       if(HttpMethod.PUT.equals(method)){
           //其它逻辑...
           return;
       }
       //DELETE请求
       if(HttpMethod.DELETE.equals(method)){
           //其它逻辑...
           return;
       }
   }

   /**
    * http响应
    * @param ctx
    * @param content
    * @param status
    */
   private void response(ChannelHandlerContext ctx, String content, HttpResponseStatus status){
       //写入数据到缓冲
       ByteBuf data = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8);
       //设置响应信息
       FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, data);
       response.headers().add(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=utf-8");
       //写入对端并监听通道关闭事件
       ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
   }
}

image.png

image.png

image.png

image.png


浏览器不断发起请求效果

image.png

image.png

image.png

image.png

image.png

image.png

image.png


总结

        以上代码实战中,接收请求的处理部分不是所有的请求方法类型都对应实现,但处理均有类似之处,参照实现即可。在工作中碰到需要定制开发轻量级HTTP监听实现我们的后端业务时,我们就可以考虑这种定制化的场景,比较灵活,可以在此基础上插拔更多需要的业务类插件。更多关于Netty的其它实战,请继续关注!






猜你喜欢

转载自blog.51cto.com/14815984/2507167