
李林锋
《Netty权威指南》是异步非阻塞通信领域的经典之作,基于最新版本Netty 5.0编写,是国内首本深入介绍Netty原理和架构的技术书籍,也是作者多年实战经验的总结和浓缩。内容包含基础功能、高级应用、系统架构、源码分析和行业应用,深入阐述了Java I/O的Netty NIO开发、Netty编解码开发、Netty多协议开发等各方面的技术要点,包含了对源码的深刻解读,并且对Netty的应用现状和未来趋势进行分析,旨在帮助从业人员提升自我,更快更明确地发展职业道路。 本书适合架构师、设计师、软件开发工程师、测试人员和其他对Java NIO框架、Java通信感兴趣的相关人士阅读,相信通过学习本书,能够熟悉和掌握Netty这一优秀的异步通信框架,实现高可用分布式系统的构建。
零、Netty的组件分成了下面几种 (1)EventLoop:这里就说线程模型,bossGroup和workerGroup。这里的调优关心线程数量。 (2)Channel:对NioSocketChannel和NioServerSocketChannel的抽象,象征着IO事件。那两个子类一个是连接读写事件、一个是发起连接事件的抽象。 (3)ChannelHandler: (4)ChannelPipeline:过滤器链,初始化的时候就在这里放上一堆过滤器,通过EventLoop监视底层IO事件的时候,每个事件触发都要经过上层这一堆过滤器的处理之后才能完成。 (5)ByteBuf:这里可以说直接内存构成的内存池,每次申请内存都从这个内存池里面走。 这里是内存池的调优,关心每一个内存页的大小,16k还是32k,再关注整个内存池的大小。 一、 从BIO到NIO2.0 核心:每一次进化都是减少阻塞的 (1)BIO存在线程阻塞,每个连接符都对应一个线程,这里就存在大量线程资源浪费; (2)NIO1.0 是 Java4 的时候增加进来的,这之前Java只能用BIO,epoll那些系统调用调不到,而那时候高性能网络服务器都是用C++去写的,C++能直接调系统调用。 NIO1.0就把select、epoll这些加进来了,减少线程阻塞。 (3)select和epoll之间对比: select只能判断当前集合里有准备好的channel,但需要遍历一遍去寻找。这个遍历一遍的过程在大量连接的情况下极损性能。 理论上一种情况就是每个连接都有数据,非常活跃,这种情况select比epoll性能好,因为epoll会有多余处理。 (4)NIO和AIO epoll通知的时候数据只是在内核态准备好了,需要我们自己拷贝到用户态;但是IO_uring通知的时候,数据已经在用户态里面了,程序直接能顺着运行,这就省下来一个内核态和用户态的切换操作,也节约了用户态的阻塞。 (5)NIO2.0 NIO1.0存在读写时候的用户态阻塞,NIO2.0使用了底层的异步IO系统调用,如果IO完成了,操作系统会给JVM一个特殊的信号或者事件,JVM监听到这个信号或者事件之后,自己执行代码里的回调函数。 就为了节约用户态的IO阻塞时间。 二、EventLoop和EventLoopGroup (1)EventLoopGroop是一个线程池,是ScheduledThreadPoolExecutor定时线程池的子类。 (2)EventLoop是一个处理器,每个channel往EventLoopGroup上注册的时候,EventLoopGroop都会按照轮询策略分配一个EventLoop处理器出来,其实就是一个处理线程。 由于一个Channel只注册在一个处理线程上,所以多个EventLoop线程之间也没啥并发关联,各跑各的。 (3)然后这个线程在死循环执行run方法。 run方法里面两个核心流程,【一个是读取epoll的系统调用】,有没有待处理任务。如果有待处理任务,把这些任务放到自己的待处理任务队列里。 【第二个流程是全量跑自己的待处理任务队列】,执行所有注册的epoll读写任务以及定时任务。 (4)3和4讲的是客户端的EventLoop,【服务器端的EventLoop】的职责是接受连接,并且把SocketChannel分配给childEventLoopGroop,实现如下: 起一个ServerEventLoopGroop监控通过bind监听某个端口的accept操作,并且初始化的时候,给这个Channel上挂一个ChannelHandler,这个ChannelHandler的核心操作是改写channelRead方法,如果接收到连接,把这个连接封装一下,并且跟用户注册的channelHandler绑定起来,扔到childEventLoop里面去,等着childEventLoop的循环去监测上面的读写事件,并且触发用户自定义的channelHandler。 三、Channel Channel是一个数据通道,核心就是read和write两个方法,channel实现的作用是对IO资源的封装抽象,有FileChannel、SocketChannel、ServerSocketChannel各种IO资源。 1、JDK里的SocketChannel和ServerSocketChannel两种类 (1)系统调用都在ServerSocketChannel上,bind、listen、accept,从这里产出SocketChannel。 (2)SocketChannel(连接)上就只有有数据、没数据两种情况,不存在epoll系统调用,只有读写操作。 (3)这就是Netty为什么要封装一个Channel接口的原因,JDK里面是两个接口,不通用,把这两个合并抽出一个公共接口Channel来。 2、Channel和读写缓冲区 (1)Linux底层上每个socket_fd是附带了两个读写缓冲区的,这是TCP的发送和接受缓冲区,内核态的 (2)用户态给每个SocketChannel都做了两个一模一样的出来,用户态的读写缓冲区解决的问题: 写缓冲区要解决拆包的问题,如果内核态写不进去了,就先存在用户的写缓冲区里面,通过byteBuffer的写缓冲指针标识写到哪了 读缓冲区也解决拆包问题,还解决业务线程处理不过来的问题,暂时充当缓存。 (3)epoll可以注册的监控任务有 读、写、连接、断连 这些任务。写任务是因为TCP磁盘缓冲区不一定有位置,可能写不下去,所以先写到写操作对应的用户态环形缓冲区里面,等TCP磁盘缓冲区有位置了之后再执行写操作。 (4)所以当真执行写操作的时候,先写到用户态的读写缓冲里面,注册一个Linux内核的写事件的监控扔给EventLoop,等TCP在内核上的缓冲区可写之后,EventLoop执行这个写操作。 (5)每一个SocketChannel生成的时候,理论上都是通过ServerSocketChannel的accept方法生成的,生成之后把socketChannel跟channelHandler绑定在一起,扔给workerGroup去处理,channel和处理器的绑定是初始化生成出来的时候就绑定上的。 四、ChannelHandler 1、过滤器模式的优点: 过滤器提供了一种面向对象的模块化机制,用来将公共任务封装到可插入的组件中。这些组件通过Web部署配置文件(web.xml)进行声明,可以方便地添加和删除过滤器,无须改动任何应用程序代码或JSP页面,由Servlet进行动态调用。通过在请求/响应链中使用过滤器,可以对应用程序(而不是以任何方式替代)的Servlet或JSP页面提供的核心处理进行补充,而不破坏Servlet或JSP页面的功能。 2、 channelHandler里存在的各种回调API: read、write、readComplete、writeComplete、connect、disconnect、close; 各种回调,都跟Channel里面的API一一对应,永远都是Channel里先触发某个流程,然后去ChannelHandler里面去找对应的钩子函数。 3、channelHandler的应用 (1)链路的有效性校验,心跳机制 心跳就是一个定时任务,提交给EventLoop线程池去执行; 定时任务提交的时机,是channelHandler里的connect调用返回,服务端握手发一个消息回来,客户端就开始运行定时任务了 (2)断路重连机制 断路重连是服务端主动断开连接,客户端从disconnect里面读取到数据,或者从socket连接里面读出eof之类的报错,触发重连 (3)a. 协议解析:在实现特定网络协议时,可以通过Pipeline中的Handler来进行协议的解码和编码。 b. 日志和监控:可以在Pipeline中加入日志Handler来记录数据流经过的各个阶段,以便于调试和监控。 c. 安全性:通过加入如SSL处理的Handler,可以为数据传输提供加密和安全保障。 3、channelHandler的头尾处理器上面带真正的读写函数 4、ChannelPipeline核心就是维护这个过滤器链,跟Map、List的维护没什么区别,源码写的半斤八两。 五、ByteBuffer 这个底层就是一个数组,是对于InputStream的升级,升级点有两个:【非阻塞】和【直接内存】。 1、 传统的read、write函数,都是阻塞的,比如read(char[])就是,读取某个文件,除非报错、文件结尾EOF或者传入的数组读满了,不然会一直阻塞。 ByteBuffer结合【Channel可以实现非阻塞】(主要是channel实现的),如果当前channel里暂时没有内容了直接退出。 2、 ByteBuffer可以使用堆外的直接内存,较少内存复制。 3、ByteBuf源码讲解 (1)可以从field去理解核心逻辑,核心字段: byte[]数组、readIndex、writeIndex、最大容量 (2)field上相比于ByteBuffer的升级就是把读指针和写指针拆开了,ByteBuffer只有一个指针位,这一个指针位把数组分成三段,每次都要自己指定读取左边那段还是写右边那段 (3)完全的升级就是,【自动扩容】和【读写全双工】 ByteBuf如果容量不够了可以自己扩容,因为记录了写指针可以自己计算可用容量。 扩容的时候先尝试内存整理,把已经读过的那部分丢掉,重新整理一次byte[],如果够了就不用申请新的容量了;如果这还不够,那就走数组扩容流程,设置一个阈值,先倍增后步进,这个策略就是为了在浪费内存和更少扩容次数之间取平衡。 (4)其他一些API就是基于读写指针来操作的,比如复制就有duplicate和copy两种,共享一个byte[]搞两套读写指针,以及byte[]数组和读写指针都复制两份,两种策略 再就是多个ByteBuf进行聚合,组成一个更大的ByteBuf。 (5)粘包拆包的时候,从一个大的byteBuffer数组里面,通过slice能精确拆分出一部分出来,这个也是零拷贝的一种实现。 六、TCP和UDP 1、TCP的粘包拆包问题略 2、TCP以流的方式进行数据传输,上层的应用协议为了对消息进行区分,往往采用如下4种方式。 (1)消息长度固定,累计读取到长度总和为定长LEN的报文后,就认为读取到了一个完整的消息;将计数器置位,重新开始读取下一个数据报; (2)将回车换行符作为消息结束符,例如FTP协议,这种方式在文本协议中应用比较广泛; (3)将特殊的分隔符作为消息的结束标志,回车换行符就是一种特殊的结束分隔符; (4)通过在消息头中定义长度字段来标识消息的总长度。 Netty对上面四种应用做了统一的抽象,提供了4种解码器来解决对应的问题,使用起来非常方便。有了这些解码器,用户不需要自己对读取的报文进行人工解码,也不需要考虑TCP的粘包和拆包。 3、Netty里处理粘包拆包的处理器是半包处理器,半包处理器是第一个处理器,后面是编解码Encode/Decode处理器; 粘包拆包和编解码是两个不同的处理器 4、UDP连接的底层调用: (1)bind、sendto、recvfrom,只有三个系统调用,比TCP的减少了accept建立连接的过程,直接就从绑定到端口的那个套接字里面接受数据了。 (2)Netty的兼容处理,一个是建立连接的channel那里需要改,不需要建立连接; 另一个是handler函数要改,因为UDP数据报文不太一样,类型不一样。 七、 RPC的组成部分以及Netty组成之间的映射 1、 序列化考察标准 序列化的时间性能(快不快)、空间性能(序列化之后多大); 支持几种语言、扩展性、需要开发者开发的多不多 这几种性能卡一下xml和Java原生序列化,都是极差,不能采用。 2、 Thrift主要由5部分组成。 (1)语言系统以及IDL编译器:负责由用户给定的IDL文件生成相应语言的接口代码; (2)TProtocol:RPC的协议层,可以选择多种不同的对象序列化方式,如JSON和Binary; 这个是【报文内容的协议编码层】。 (3)TTransport:RPC的传输层,同样可以选择不同的传输层实现,如socket、NIO、MemoryBuffer等; 这个是【传输层的编码】,可以认为是Socket的报文编码,它的协议头尾。 (4)TProcessor:作为协议层和用户提供的服务实现之间的纽带,负责调用服务实现的接口; 这就是你提供的handler (5)TServer:聚合TProtocol、TTransport和TProcessor等对象。 这个可以理解成那个代理类,客户端和服务端的代理类,这个代理类需要实现功能聚合。 3、 序列化知识补充 (1)Unicode是一种字符集合,utf-8、utf-16、utf-32都是对这种字符集的一种实现;所以unicode8/16/32的字符范围都是一样的,就是效率不一样 Unicode-8是用1-4个字节来表示某一个Unicode字符,互联网最常用Unicode-8编码; Unicode-16包含了大部分东亚语言,编码变成了2-4字节; Unicode-32用4字节定长表示。 (2)base64解决的是二进制编码的打印问题 一串0101不一定能映射成String,所以6位一截断,重新编码之后找到能打印的字符。 (3)urlEncode的原理和原因 url里需要对各种特殊字符进行特殊识别,比如/?=#之类的,这些字符如果出现在url里面会有歧义,需要把这些特殊字符转化成%25这种百分号编码避免歧义。 (4)JSON序列化 A. Jackson和Gson对比: 性能速度:Jackson通常比Gson更快,因为它直接解析JSON流,而不是先将其转换为中间表示形式。 内存使用:Jackson的内存占用通常较低,因为它可以逐行处理JSON数据,避免将整个JSON加载到内存中。 解释Jackson对字符串的解析跟对象的生成是浑然一体的,Gson会先进行Json解析,变成JsonObject、JsonElement这种中间变量,汇聚成一棵Json树的形式,再把这棵Json树转变成对象; 所以Gson的内存占用高,且效率低。 B. FastJson FastJson会通过ASM的方式,根据已有的类定义,为这个类量身定做一个工具类进行序列化和反序列化,避免了反射操作。 FastJson的安全性问题,是类型直接写到了Json字符串里面去,数据类型是从Json字符串读出来的,通过@Type,而不是用户自己输入的,如果人为构造这个类型变成跟RMI相关的类就能操纵机器去连接远程服务器。 Gson是自己输入的类型,避免了安全问题。 (5)RPC二进制序列化方式 a. Hession的格式,类定义、字段名、数据类型、变长长度、value b. Thrift的格式,id、数据类型、变长长度、value c. gRpc的格式,id、变长长度、value
就简单介绍了netty的使用,没有深入原理,需要配套其他的书籍文档来深入netty。也讲了一些Java的知识,作为入门来说还算合格。这个推荐就是给的入门推荐
很多书没有对比就没有伤害,可以看到这本书作者花了很多心思,看完对 Netty 理解的很通透,感谢作者
由浅入深,有层次递进感,读了几本,感觉这本还算不错,也有深度,章节安排也合理;对于学习 netty 算是一本不错的教材 源码章节后续有空再看看。 不过netty 的内容确实很多,先整上几个案例,吃透几个,再来阅读,就心里有底多了。
全都一笔带过,像一个API解说文档
一个优秀的框架是怎么样的,基于本书来说,首先要立足于各种IO模型的优缺点,之后从传输层看TCP.UDP协议,以后选择序列化协议,最好能够跨语言,跨平台,极致压榨性能,方便开发者使用,再之后就是围绕框架的各种插件可插拔,腾讯的trpc和字节跳动的kratos都已经开源了,感兴趣的读者可以去看看
一本讲解netty使用的实战书,值得推荐!
书其实还行
整体说的挺清楚了 就是源码少了点
不明白其他人推荐的理由,我认为只适合入门新手看。