转载

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

前言

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

HTTP请求

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

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

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

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

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

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

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

HTTP短连接和长连接

短链接执行流程

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

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

长连接执行流程

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

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

可能的使用场景

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

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

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

Netty基于HTTP包装介绍

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

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

以下我们就来应用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);
    }
}

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

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

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

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

浏览器不断发起请求效果

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

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

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

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

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

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

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

总结

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

原文  https://blog.51cto.com/14815984/2507167
正文到此结束
Loading...