专栏首页JMCuiNetty 系列七(那些开箱即用的 ChannelHandler).

Netty 系列七(那些开箱即用的 ChannelHandler).

一、前言

    Netty 为许多通用协议提供了编解码器和处理器,几乎可以开箱即用, 这减少了你在那些相当繁琐的事务上本来会花费的时间与精力。另外,这篇文章中,就不涉及 Netty 对 WebSocket协议 的支持了,因为涉及的篇幅有点大,会在下一篇文章做一个具体的介绍。

二、SSL 协议

    SSL 协议是安全协议,层叠在其他协议之上。为了支持 SSL/TLS, Java 提供了 javax.net.ssl 包,它的 SSLContext 和 SSLEngine 类使得实现解密和加密相当简单直接。 Netty 通过一个名为 SslHandler 的 ChannelHandler 实现利用了这个 API, 其中 SslHandler 在内部使用 SSLEngine 来完成实际的工作。下图描述的是 SslHandler 的数据流。

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ByteBufAllocator byteBufAllocator = ch.alloc();
        //对于每个 SslHandler 实例,都使用 Channel 的 ByteBufAllocator 从 SslContext 获取一个新的 SSLEngine
        SSLEngine sslEngine = context.newEngine(byteBufAllocator);
        //服务器端模式,客户端模式设置为true
        sslEngine.setUseClientMode(false);
        //不需要验证客户端,客户端不设置该项
        sslEngine.setNeedClientAuth(false);
        //要将 SslHandler 设置为第一个 ChannelHandler。这确保了只有在所有其他的 ChannelHandler 将他们的逻辑应用到数据之后,才会进行加密。
        //startTls 如果为true,第一个写入的消息将不会被加密(客户端应该设置为true)
        ch.pipeline().addFirst("ssl",new SslHandler(sslEngine, startTls));
    }

tips:对于 ChannelPipeline 链中 ChannelHandler 执行的顺序 —— 入站事件顺序执行、出站事件逆序执行。

三、HTTP 协议

    HTTP 是基于请求/响应模式的:客户端向服务器发送一个 HTTP 请求,然后服务器将会返回一个 HTTP 响应。 下图展示了 Netty 中 HTTP请求和响应的组成部分:

    Netty 对 HTTP 协议的支持主要提供了以下 ChannelHandler:

HttpResponseDecoder:解码器,用于客户端,解码来自服务端的响应。 HttpRequestEncoder:编码器,用户客户端,编码向服务端发送的请求。 HttpRequestDecoder:解码器,用于服务端,解码来自客户端的请求。 HttpResponseEncoder:编码器,用于服务端,编码向客户端的响应。 HttpClientCodec:编解码器,用户客户端,效果等于 HttpResponseDecoder + HttpRequestEncoder。 HttpServerCodec:编解码器,用户服务端,效果等于 HttpRequestDecoder + HttpResponseEncoder。 HttpObjectAggregator:聚合器,由于 HTTP 的请求和响应可能由许多部分组成,需要聚合它们以形成完整的消息,HttpObjectAggregator 可以将多个消息部分合并为 FullHttpRequest 或者 FullHttpResponse 消息。 HttpContentCompressor:压缩,用户服务端,压缩要传输的数据,支持 gzip 和 deflate 压缩格式。 HttpContentDecompressor:解压缩,用于客户端,解压缩服务端传输的数据。

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        SSLEngine sslEngine = sslContext.newEngine(ch.alloc());
        if (isClient) {
            //使用 HTTPS,添加 SSL 认证
            pipeline.addFirst("ssl", new SslHandler(sslEngine, true));
            pipeline.addLast("codec", new HttpClientCodec());
            //1、建议开启压缩功能以尽可能多地减少传输数据的大小
            //2、客户端处理来自服务器的压缩内容
            pipeline.addLast("decompressor", new HttpContentDecompressor());
        }else {
            pipeline.addFirst("ssl", new SslHandler(sslEngine));
            //HttpServerCodec:将HTTP客户端请求转成HttpRequest对象,将HttpResponse对象编码成HTTP响应发送给客户端。
            pipeline.addLast("codec", new HttpServerCodec());
            //服务端,压缩数据
            pipeline.addLast("compressor", new HttpContentCompressor());
        }
        //目的多个消息转换为一个单一的FullHttpRequest或是FullHttpResponse
        //将最大的消息为 512KB 的HttpObjectAggregator 添加到 ChannelPipeline
        //在消息大于这个之后会抛出一个 TooLongFrameException 异常。
        pipeline.addLast("aggregator", new HttpObjectAggregator(512 * 1024));
    }

 tips:当使用 HTTP 时,建议开启压缩功能以尽可能多地减小传输数据的大小。虽然压缩会带来一些 CPU 时钟周期上的开销。

四、拆包和粘包的解决方案

    TCP 传输过程中,客户端发送了两个数据包,而服务端却只收到一个数据包,客户端的两个数据包粘连在一起,称为粘包;

    TCP 传输过程中,客户端发送了两个数据包,服务端虽然收到了两个数据包,但是两个数据包都是不完整的,或多了数据,或少了数据,称为拆包;

    发生TCP粘包、拆包主要是由于下面一些原因:

1、应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。 2、应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。 3、进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包。 4、接收方法不及时读取套接字缓冲区数据,这将发生粘包。

    Netty 预定义了一些解码器用于解决粘包和拆包现象,其中大体分为两类:

基于分隔符的协议:在数据包之间使用定义的字符来标记消息或者消息段的开头或者结尾。这样,接收端通过这个字符就可以将不同的数据包拆分开。 基于长度的协议:发送端给每个数据包添加包头部,头部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包头部的长度字段,便知道每一个数据包的实际长度了。

    基于分隔符的协议

public class LineBasedHandlerInitializer extends ChannelInitializer<Channel> {
    
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ch.pipeline().addLast(
                // 将提取到的桢转发给下一个Channelhandler
                new LineBasedFrameDecoder(64 * 1024),
                // 添加 FrameHandler 以接收帧
                new FrameHandler()
        );
    }
    
    public static final class FrameHandler extends SimpleChannelInboundHandler<ByteBuf> {

        @Override
        protected void messageReceived(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
            //Do something with the data extracted from the frame
        }
    }
}

    基于长度的协议

    LengthFieldBasedFrameDecoder 是 Netty 基于长度协议解决拆包粘包问题的一个重要的类,主要结构就是 header+body 结构。我们只需要传入正确的参数就可以发送和接收正确的数据,那吗重点就在于这几个参数的意义。下面我们就具体了解一下这几个参数的意义。先来看一下LengthFieldBasedFrameDecoder主要的构造方法:

public LengthFieldBasedFrameDecoder(
            int maxFrameLength,
            int lengthFieldOffset, int lengthFieldLength,
            int lengthAdjustment, int initialBytesToStrip)

maxFrameLength:最大帧长度。也就是可以接收的数据的最大长度。如果超过,此次数据会被丢弃。 lengthFieldOffset:长度域偏移。就是说数据开始的几个字节可能不是表示数据长度,需要后移几个字节才是长度域。 lengthFieldLength:长度域字节数。用几个字节来表示数据长度。 lengthAdjustment:数据长度修正。因为长度域指定的长度可以使 header+body 的整个长度,也可以只是body的长度。如果表示header+body的整个长度,那么我们需要修正数据长度。 initialBytesToStrip:跳过的字节数。如果你需要接收 header+body 的所有数据,此值就是0,如果你只想接收body数据,那么需要跳过header所占用的字节数。

public class LengthBasedInitializer extends ChannelInitializer<Channel> {

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ch.pipeline().addLast(
                new LengthFieldBasedFrameDecoder(64 * 1024, 0, 8),
                new FrameHandler()
        );
    }

    public static final class FrameHandler extends SimpleChannelInboundHandler<ByteBuf> {

        @Override
        protected void messageReceived(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
            //处理桢的数据
        }
    }
}

 tips:UDP协议不会发生沾包或拆包现象, 因为UDP是基于报文发送的,在UDP首部采用了16bit来指示UDP数据报文的长度,因此在应用层能很好的将不同的数据报文区分开。

五、其他

    由于网络饱和的可能性,如何在异步框架中高效地写大块的数据是一个特殊的问题。Netty 通过一个 FileRegion 接口来实现,其在 Netty 的API 文档中的定义是:"通过支持零拷贝的文件传输的 Channel 来发送的文件区域"。但是该接口只适用于文件内容的直接传输,不包括应用程序对文件数据的任何处理。

View Code

    如果大块的数据要从文件系统复制到用户内存中时,可以安装一个 ChunkedWriteHandler,并用 ChunkedInput 实现写入文件数据。 它支持异步写大型数据流,而又不会导致大量的内存消耗。

public class ChunkedWriteHandlerInitializer extends ChannelInitializer<Channel> {
    private final File file;
    private final SslContext sslCtx;

    public ChunkedWriteHandlerInitializer(File file, SslContext sslCtx) {
        this.file = file;
        this.sslCtx = sslCtx;
    }
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ch.pipeline().addLast(
                new SslHandler(sslCtx.newEngine(ch.alloc())),
                // 添加 ChunkedWriteHandler 以处理作为 ChunkedInput 传入的数据
                new ChunkedWriteHandler(),
                new WriteStreamHandler()
        );
    }
    private final class WriteStreamHandler extends ChannelHandlerAdapter {
        //当连接建立时,channelActive() 方法将使用 ChunkedInput 写文件数据
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            super.channelActive(ctx);
            ctx.writeAndFlush(new ChunkedStream(new FileInputStream(file)));
        }
    }
}

    Netty提供的用于和JDK进行互操作的序列化类 :

    Netty提供的用于和 JBoss Marshalling 进行互操作的序列化类 :

public class MarshallingInitializer extends ChannelInitializer<Channel> {
    private final MarshallerProvider marshallerProvider;
    private final UnmarshallerProvider unmarshallerProvider;

    public MarshallingInitializer(MarshallerProvider marshallerProvider, UnmarshallerProvider unmarshallerProvider) {
        this.marshallerProvider = marshallerProvider;
        this.unmarshallerProvider = unmarshallerProvider;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ch.pipeline().addLast(
                new MarshallingDecoder(unmarshallerProvider),
                new MarshallingEncoder(marshallerProvider),
                new ObjectHandler()
        );
    }

    public static final class ObjectHandler extends SimpleChannelInboundHandler<Serializable> {
        @Override
        protected void messageReceived(ChannelHandlerContext ctx, Serializable msg) throws Exception { }
    }
}

    Netty提供的用于和 Protocol Buffers 进行互操作的序列化类 :

参考资料:《Netty IN ACTION》

演示源代码:https://github.com/JMCuixy/NettyDemo/tree/master/src/main/java/org/netty/demo/protocol

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!
本文分享自作者个人站点/博客:http://www.cnblogs.com/jmcui/复制
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • Netty入门-概述

    Netty框架是什么?Netty是一款用于快速开发高性能的网络应用程序的Java框架。它封装了网络编程的复杂性,使网络编程和Web技术的最新进展能够被 比以往更...

    码农杰森
  • Netty 系列八(基于 WebSocket 的简单聊天室).

        之前写过一篇 Spring 集成 WebSocket 协议的文章 —— Spring消息之WebSocket ,所以对于 WebSocket 协议的介绍...

    JMCui
  • Netty 系列一(核心组件和实例).

        早期的 Java API 只支持由本地系统套接字库提供所谓的阻塞函数来支持网络编程。由于是阻塞 I/O ,要管理多个并发客户端,需要为每个新的客户端So...

    JMCui
  • 项目开发中如何选择编解码器?如何解决TCP粘包问题?(Netty二)

    ​在使用Netty进行通信开发,如何选择编码器?在TCP粘包/拆包的问题如何解决?服务端在启动 流程是什么样的?连接服务流程是什么?

    花花与Java
  • Netty 系列二(传输).

        上一篇文章我们提到 Netty 的核心组件是 Channel、回调、Future、ChannelHandler、EventLoop,这篇文章主要是对 C...

    JMCui
  • Netty 系列四(ChannelHandler 和 ChannelPipeline).

        先来整体的介绍一下这篇博文要介绍的几个概念(Channel、ChannelHandler、ChannelPipeline、ChannelHandlerC...

    JMCui
  • 源码分析 Netty:核心组件及启动过程分析

    Channel(通道)是 NIO 基本的结构。JDK的NIO包中,有Channel接口的介绍:

    程序员架构进阶
  • Java面试——Netty

    【1】阻塞 IO(Blocking I/O):同步阻塞I/O模式,当一条线程执行 read() 或者 write() 方法时,这条线程会一直阻塞直到读取一些数据...

    Java架构师必看
  • netty入门(一)

    老梁
  • netty系列之:Event、Handler和Pipeline

    上一节我们讲解了netty中的Channel,知道了channel是事件处理器和外部联通的桥梁。今天本文将会详细讲解netty的剩下几个非常总要的部分Event...

    程序那些事
  • 【死磕Netty】-----Netty的核心组件

    原文出处http://cmsblogs.com/ 『chenssy』 转载请注明原创出处,谢谢! 在第一篇博客中(【死磕Netty】-----NIO基础详解),...

    用户1655470
  • netty系列之:netty架构概述

    Netty为什么这么优秀,它在JDK本身的NIO基础上又做了什么改进呢?它的架构和工作流程如何呢?请走进今天的netty系列文章之:netty架构概述。

    程序那些事
  • Netty之旅二:口口相传的高性能Netty到底是什么?

    高清思维导图原件(xmind/pdf/jpg)可以关注公众号:一枝花算不算浪漫 回复netty01即可。

    一枝花算不算浪漫
  • Spring环境下使用Netty写Socket和Http详解

    Netty是目前最流行的由JBOSS提供的一个Java开源框架NIO框架,Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的...

    品茗IT
  • 13-Netty 核心模块组件

    Netty中所有的IO操作都是异步的, 不能立刻得知消息是否被正确处理, 但是可以过一会等它执行完成或者直接注册一个监听器, 具体的实现就是通过Future和C...

    彼岸舞
  • Netty in Action ——— Netty的组件和设计

    tomas家的小拨浪鼓
  • 详细图解Netty Reactor启动全流程 | 万字长文 | 多图预警

    大家先不要惊慌,问题不大,本文笔者的目的就是要让大家清晰的理解这幅流程图,从而深刻的理解Netty Reactor的启动全流程,包括其中涉及到的各种代码设计实现...

    bin的技术小屋

扫码关注腾讯云开发者

领取腾讯云代金券