16. Netty发送消息有几种方式(2种):*
- 直接写入Channel中,消息从ChannelPipeline的尾部开始移动;
- 写入和ChannelHandler绑定的ChannelHandlerContext中,消息从ChannelPipeline中的向下一个 ChannelHandler中移动。
17. Netty编解码器:
- 编码俗称序列化,解码俗称反序列。
- 由于是双向通信,在服务端和客户端中均需要添加编解码器。
- Netty的编解码器中入站为解码,出站为编码:
- 解码器(Decoder):将消息从字节或其他序列形式转换成指定的消息对象。即负责"入站 InboundHandler"数据。
- 编码器(Encoder):将消息对象转换成字节或其他序列形式在网络上传输。即负责"出站 OutboundHandler" 数据。
- Netty提供的抽象基类:
- 解码器:ByteToMessageDecoder、MessageToMessageDecoder,重写decoder()
- 编码器:MessageToByteEncoder、MessageToMessageEncoder,重写encoder()
18. TCP 粘包/拆包的原因及解决方法?
概念:
TCP层不了解上层业务数据的具体含义,TCP会根据缓冲区的实际情况进行包的划分,所以数据层一个完整的包可能会被TCP拆成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是TCP拆包和粘包。
粘包/拆包的原因:
- 应用程序write写入的字节大小大于Socket接口发送到缓存区字节的大小。
- 进行MSS大小的TCP分段时,发生拆包:当TCP报文长度 - TCP头部长度 > MSS的时候将发生拆包 。
- 以太网帧的payload(净荷)大于MTU(1500字节)进行ip分片(不记)。
解决方法:
- 消息定长,不够空格补齐:FixedLengthFrameDecoder类。
- 包尾增加特殊字符分割:
- 行分隔符类:LineBasedFrameDecoder类。 Frame【福瑞母】
- 自定义分隔符类 :DelimiterBasedFrameDecoder类。
- 将消息分为消息头和消息体:LengthFieldBasedFrameDecoder + LengthFieldPrepender类。分为有头部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。
- 更复杂的应用层协议。
19. 默认情况Netty起多少线程?何时启动?
Netty默认是CPU处理器数的两倍(NioEventLoopGroup类,默认是0 -> 它继承MultithreadEventLoopGroup,这里会先获取"io.netty.eventLoopThreads",如果没有值,就是CPU两倍);
在bind(),完之后启动。
20. Netty和Tomcat的区别?
- 作用不同:Tomcat是Servlet容器,可以视为Web服务器,而Netty是异步事件驱动的网络应用程序框架和工具用于简化网络编程,例如TCP和UDP套接字服务器。
- 协议不同:Tomcat是基于Http协议的Web服务器,而Netty能通过编程自定义各种协议,因为Netty本身自己能编码/解码字节流。Netty可以实现:HTTP服务器、FTP服务器、UDP服务器、RPC服务器、WebSocket服务器、Redis的Proxy服务器、MySQL的Proxy服务器等。
21. Netty使用代码:
服务端:

客户端:

其他:
0. NIOEventLoopGroup源码?
NioEventLoopGroup(其实是MultithreadEventExecutorGroup) 内部维护一个类型为 EventExecutor children [],默认大小是处理器核数 * 2,这样就构成了一个线程池,初始化EventExecutor时NioEventLoopGroup重载newChild方法,所以children元素的实际类型为NioEventLoop。
线程启动时调用SingleThreadEventExecutor的构造方法,执行NioEventLoop 类的run方法,首先会调用hasTasks()方法判断当前taskQueue是否有元素。如果taskQueue中有元素,执行 selectNow() 方法, 终执行 selector.selectNow(),该方法会立即返回。如果taskQueue没有元素,执行 select(oldWakenUp) 方法
select ( oldWakenUp) 方法解决了 Nio 中的 bug,selectCnt 用来记录 selector.select方法的执行次数和标识是否执行过selector.selectNow(),若触发了epoll的空轮询bug,则会反复执行selector.select(timeoutMillis),变量 selectCnt 会逐渐变大,当selectCnt 达到阈值(默认512),则执行 rebuildSelector方法,进行selector重建,解决cpu占用100%的bug。
rebuildSelector方法先通过openSelector方法创建一个新的selector。然后将 old selector的selectionKey执行cancel。 后将old selector的channel重新注册到新的selector中。rebuild后,需要重新执行方法selectNow,检查是否有已ready的selectionKey。
接下来调用processSelectedKeys 方法(处理I/O任务),当selectedKeys != null时,调用processSelectedKeysOptimized方法,迭代 selectedKeys 获取就绪的 IO 事件的selectkey存放在数组selectedKeys中, 然后为每个事件都调用 processSelectedKey 来处理它,processSelectedKey 中分别处理OP_READ; OP_WRITE;OP_CONNECT事件。
后调用runAllTasks方法(非IO任务),该方法首先会调用 fetchFromScheduledTaskQueue方法,把scheduledTaskQueue中已经超过延迟执行时间的任务移到taskQueue中等待被执行,然后依次从taskQueue中取任务执行,每执行64个任务,进行耗时检查,如果已执行时间超过预先设定的执行时间,则停止执行非IO任务,避免非IO任务太多,影响IO任务的执行。每个NioEventLoop对应一个线程和一个Selector,NioServerSocketChannel 会主动注册到某一个NioEventLoop的Selector上,NioEventLoop负责事件轮询。
Outbound 事件都是请求事件, 发起者是 Channel,处理者是 unsafe,通过 Outbound 事件进行通知,传播方向是 tail到head。Inbound 事件发起者是 unsafe,事件的处理者是 Channel, 是通知事件,传播方向是从头到尾。内存管理机制,首先会预申请一大块内存Arena,Arena由许多Chunk组成,而每个Chunk默认由2048个page组成。Chunk通过AVL树的形式组织Page,每个叶子节点表示一个Page,而中间节点表示内存区域,节点自己记录它在整个 Arena中的偏移地址。当区域被分配出去后,中间节点上的标记位会被标记,这样就表示这个中间节点以下的所有节点都已被分配了。大于8k的内存分配在 poolChunkList中,而PoolSubpage用于分配小于8k的内存,它会把一个page 分割成多段,进行内存分配。
ByteBuf的特点:支持自动扩容(4M),保证put方法不会抛出异常、通过内置的复合缓冲类型,实现零拷贝(zero-copy);不需要调用flip()来切换读/写模 式,读取和写入索引分开;方法链;引用计数基于AtomicIntegerFieldUpdater 用于内存回收;PooledByteBuf采用二叉树来实现一个内存池,集中管理内存的分配和释放,不用每次使用都新建一个缓冲区对象。UnpooledHeapByteBuf每次都会新建一个缓冲区对象。
1. 了解哪几种序列化协议?
概念:
- 序列化(编码)是将对象序列化为二进制形式(字节数组),主要用于网络传输、数据持久化等;
- 反序列化(解码)则是将从网络、磁盘等读取的字节数组还原成原始对象,主要用于网络传输对象的解码,以便完成远程调用。
影响性能因素:
- 序列化后的码流大小(网络带宽的占用)。
- 序列化的性能(CPU资源占用)。
- 是否支持跨语言(异构系统的对接和开发语言切换)。
(以下简单看第一点)
Java默认提供的序列化:
- 无法跨语言、序列化后的码流太大、序列化的性能差。
- 优点:人机可读性好,可指定元素或特性的名称。
- 缺点:序列化数据只包含数据本身以及类的结构,不包括类型标识和程序集信息;只能序列化公共属性和字段;不能序列化方法;文件庞大,文件格式复杂,传输占带宽。
- 适用场景:当做配置文件存储数据,实时数据转换。
JSON:
- 一种轻量级的数据交换格式。
- 优点:兼容性高、数据格式比较简单,易于读写、序列化后数据较小,可扩展性好,兼容性好、与XML相比,其协议比较简单,解析速度比较快。
- 缺点:数据的描述性比XML差、不适合性能要求为ms级别的情况、额外空间开销比较大。
- 适用场景(可替代XML):跨防火墙访问、可调式性要求高、基于Web browser的Ajax请求、传输数据量相对小,实时性要求相对低(例如秒级别)的服务。
Fastjson:
- 采用一种"假定有序快速匹配"的算法。
- 优点:接口简单易用、目前 java语言中 快的json库。
- 缺点:过于注重快,而偏离了"标准"及功能性、代码质量不高,文档不全。
- 适用场景:协议交互、Web输出、Android客户端
Thrift:
- 它不仅是序列化协议,还是一个RPC框架。
- 优点:序列化后的体积小, 速度快、支持多种语言和丰富的数据类型、对于数据字段的增删具有较强的兼容性、支持二进制压缩编码。
- 缺点:使用者较少、跨防火墙访问时,不安全、不具有可读性,调试代码时相对困难、不能与其他传输层协议共同使用(例如HTTP)、无法支持向持久层直接读写数据,即不适合做数据持久化序列化协议。
- 适用场景:分布式系统的RPC解决方案。
Avro:
- Hadoop的一个子项目,解决了JSON的冗长和没有IDL的问题。
- 优点:支持丰富的数据类型、简单的动态语言结合功能、具有自我描述属性、提高了数据解析速度、快速可压缩的二进制数据形式、可以实现远程过程调用RPC、支持跨编程语言实现。
- 缺点:对于习惯于静态类型语言的用户不直观。
- 适用场景:在Hadoop中做Hive、Pig和MapReduce的持久化数据格式。
Protobuf:
- 将数据结构以.proto文件进行描述,通过代码生成工具可以生成对应数据结构的POJO对象和Protobuf相关的方法和属性。
- 优点:序列化后码流小,性能高、结构化数据存储格式(XML JSON等)、通过标识字段的顺序,可以实现协议的前向兼容、结构化的文档更容易管理和维护
- 。缺点:需要依赖于工具生成代码、支持的语言相对较少,官方只支持Java 、C++ 、python。
- 适用场景:对性能要求高的RPC调用、具有良好的跨防火墙的访问属性、适合应用层对象的持久化
2. 如何选择序列化协议?
具体场景:
- 公司之间的系统调用,如果性能要求在100ms以上的服务,基于XML的SOAP协议是一个值得考虑的方案。
- 基于Web浏览器的Ajax,以及Mobile app与服务端之间的通讯,JSON协议是首选。
- 对于调试环境比较恶劣的场景,采用JSON或XML能够极大的提高调试效率,降低系统开发成本。
- 对于T级别的数据的持久化应用场景,Protobuf和Avro是首要选择。如果持久化后的数据存储在Hadoop子项目里,Avro会是更好的选择。
- 如果需要提供一个完整的RPC解决方案,Thrift是一个好的选择。
- 如果序列化之后需要支持不同的传输层协议,或者需要跨防火墙访问的高性能场景,Protobuf可以优先考虑。
Netty中的使用:
- ProtobufVarint32FrameDecoder是用于处理半包消息的解码类;
- ProtobufDecoder(UserProto.User.getDefaultInstance())这是创建的UserProto.java文件中的解码类;
- ProtobufVarint32LengthFieldPrepender对Protobuf协议的消息头上加上一个长度为32的整形字段,用于标志这个消息的长度的类;
- ProtobufEncoder是编码类将StringBuilder转换为ByteBuf类型:copiedBuffer()方