《Java架构师的第一性原理》26Java基础之Netty入门

1 Reactor模式演进

1)读请求内容——》解码——》计算处理——》编码回复——》回复

2)采用基于事件驱动的设计,当有事件触发时,才会调用处理器进行数据处理

3)改进:使用多线程处理业务逻辑

4)继续改进:对于多个CPU的机器,为充分利用系统资源,将Reactor拆分为两部分

 

 

 

 

2 JVM和内核是怎样进行IO交互的

学习方法

strace -ff -o out pid

man 2 select
man 2 poll
-- epoll IO的event
man epoll
man 2 epoll_create
man 2 epoll_ctl
man 2 epoll_wait 

2.2 select和poll

 

2.3 epoll

 

 

 

3 Netty

1)粘(nian)包和拆包

TCP 不管你发出的数据包的业务含义,它只考虑一次性发多少字节比较合适,这就导致了有可能两个不相关的数据包被合并发送,这就叫粘包。解决办法就是设置包的业务边界,直到边界出现再取出完整的。

换行符拆包

2)应用

dubbo、spark(netty混搭模式)

4 使用Netty通信遇到了编解码问题?试试MessagePack如何解决

使用Netty主要是为了进行网络通信,而网络通信就要涉及到传输数据,数据是不能直接传递的,需要进行一系列处理。java序列化就是其中一种处理方式,但是由于各种各样的缺点,一般不会用,在这里我们介绍一个比较优秀的编码解码技术MessagePack。

一、为什么不用java序列化

不用java进行序列化,大体上可以归结为以下几条。

1、无法跨语言

比如说java编码后的数据,C++不认识,也不能解码。

2、性能低

编码解码的速度太慢。

3、码流太大

编码之后增加了很多其他的数据,占据空间。

4、开发难度大

对开发人员不友好。

以上几条随便一条都是极大地缺点,因此我们在这里先介绍其中一种编码和解码的技术,叫MessagePack。为什么要使用这个呢?因为以上四条都能很好的解决,所以要用它。当然还有很多其他的优秀技术,比如Protobuf等,以后用到的时候再说,我目前的项目由于是C++和java通信,因此选择了这个框架。

既然这个MessagePack这么好,下面我们就直接来看如何在Netty中使用。

使用Netty通信遇到了编解码问题?试试MessagePack如何解决

5 Netty通信遇到了TCP拆包粘包问题?

Netty通信遇到了TCP拆包粘包问题?看这篇文章如何解决

  • 1、什么是TCP粘包拆包
  • 2、Netty中粘包问题的问题重现
  • 3、Netty中粘包问题的解决方案

一、什么是TCP拆包和粘包

我们使用TCP协议在传输数据的时候,如果数据块比较大,就会考虑将其切分。把一个大的数据包进行切割成一个个小的数据包发送。这时候就会遇到拆包和粘包的问题。

为什么会出现这样的问题呢?想要解释清楚,就必须要考虑到计算机网络的相关知识了,TCP在接受数据的时候,有一个滑动窗口来控制接受数据的大小,这个滑动窗口你就可以理解为一个缓冲区的大小。缓冲区满了就会把数据发送。数据包的大小是不固定的,有时候比缓冲区大,有时候小。这时候就会出现上面的现象。

这里输出的是服务端的信息,从上面的输出结果你就会发现,其实客户端的“我爱你”都被黏在了一块。本来100条但是现在却只有17条了,这就是发生了粘包现象。

如何来解决呢?下面我们看看。

三、粘包问题解决

解决的思路很简单,也就是每次发送一个数据包的时候,添加一个标识符,读的时候一直读到这个标识符才表示一个完整的数据包。在上面我们添加的是line.separator,也就是换行符“\n”。

LineBasedFrameDecoder的作用是在读取数据的时候,一直读到是否含有换行符“\n”或者是“\r\n”。如果读到了就表示该结束了。因此就拿到了这一行的数据包。

DelimiterBasedFrameDecoder前面这个可以自动完成以分隔符做结束标志的消息。

FixedLengthFrameDecoder后面这个可以自动完成对定长消息的解码。都可以解决粘包拆包问题。

6 高频面试题:什么是零拷贝?在哪些地方使用了?

这是一道高频的面试题,而且在很多技术中都使用到了,比如javaNIO、kafka、Netty、Linux等等。作为一个非常重要的知识点,而且又是高频面试题,有必要从零开始好好地认识一下。即使你是刚入门的同行,相信也能看的懂。

6.1 什么是零拷贝?

6.1.1 从一个案例说起

为了解释这个概念,我们先要从一个需求说起,说某天某领导给你下发了一个任务,完成一个从文件中读取数据,并传输到网络上的一个小程序。代码很简单:

首先我们在我们的操作系统中找到这个文件,然后把数据先读到缓冲区,最后把缓冲区的数据发送到网络上。

代码是很简单,现在我们考虑一下,这个数据从电脑到网络整个传输的过程:

 

 

现在我们可以看到1->2->3->4的整个过程一共经历了四次拷贝的方式,但是真正消耗资源和浪费时间的是第二次和第三次,因为这两次都需要经过我们的CPU拷贝,而且还需要内核态和用户态之间的来回切换。想想看,我们的CPU资源是多么宝贵,要处理大量的任务。还要去拷贝大量的数据。如果能把CPU的这两次拷贝给去除掉,岂不快哉!!!既能节省CPU资源,还可以避免内核态和用户态之间的切换。

这里还要先说一下用户态和内核态的区别:

处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理器是可被抢占的;处于内核态执行时,则能访问所有的内存空间和对象,且所占有的处理器是不允许被抢占的。

6.1.2 优化方案

要去除第二次和第三次之间的拷贝,Linux开发人员也早就注意到了这个问题,于是在linux 2.1内核中,添加了 “数据被copy到socket buffer”的动作,于是我们的javaNIO,可以直接调用transferTo()的方法,就可以实现这种现象。

 

 现在一看,感觉性能资源都得到了很大的提升,不过现在还不并不是完美的。因为这三次拷贝还用到了CPU的拷贝技术,就是第二次。不过不要担心。Linux开发人员比我们更加深谋远虑。

6.1.3 零拷贝优化方案

在Linux2.4 内核做了优化,取而代之的是只包含关于数据的位置和长度的信息的描述符被追加到了socket buffer 缓冲区中。DMA引擎直接把数据从内核缓冲区传输到协议引擎(protocol engine),从而消除了最后一次CPU copy。经过上述过程,数据只经过了2次copy就从磁盘传送出去了。这个才是真正的Zero-Copy。

注意:这里的零拷贝其实是根据内核状态划分的,在这里没有经过CPU的拷贝,数据在用户态的状态下,经历了零次拷贝,所以才叫做零拷贝,但不是说不拷贝。

如果之前看过我的Netty系列的前两篇文章,应该都知道里面为了解决拆包和粘包的问题,Netty会在每一个数据包里面加一些特殊描述符。这里同样也是。

OK。现在我们已经了解了什么是零拷贝技术,下面我们再说一下那些数据结构会用到零拷贝技术。

6.2 哪些地方会用到零拷贝技术

6.2.1 java的NIO

先说java,是因为要给下面的netty做铺垫,在 Java NIO 中的通道(Channel)就相当于操作系统的内核空间(kernel space)的缓冲区,而缓冲区(Buffer)对应的相当于操作系统的用户空间(user space)中的用户缓冲区(user buffer)。

堆外内存(DirectBuffer)在使用后需要应用程序手动回收,而堆内存(HeapBuffer)的数据在 GC 时可能会被自动回收。因此,在使用 HeapBuffer 读写数据时,为了避免缓冲区数据因为 GC 而丢失,NIO 会先把 HeapBuffer 内部的数据拷贝到一个临时的 DirectBuffer 中的本地内存(native memory),这个拷贝涉及到 sun.misc.Unsafe.copyMemory() 的调用,背后的实现原理与 memcpy() 类似。 最后,将临时生成的 DirectBuffer 内部的数据的内存地址传给 I/O 调用函数,这样就避免了再去访问 Java 对象处理 I/O 读写。

(1)MappedByteBuffer

MappedByteBuffer 是 NIO 基于内存映射(mmap)这种零拷贝方式的提供的一种实现,意思是把一个文件从 position 位置开始的 size 大小的区域映射为内存映像文件。这样添加地址映射,而不进行拷贝。

(2)DirectByteBuffer

DirectByteBuffer 的对象引用位于 Java 内存模型的堆里面,JVM 可以对 DirectByteBuffer 的对象进行内存分配和回收管理,是 MappedByteBuffer 的具体实现类。因此同样具有零拷贝技术。

(3)FileChannel

FileChannel 定义了 transferFrom() 和 transferTo() 两个抽象方法,它通过在通道和通道之间建立连接实现数据传输的。

我们直接看Linux2.4的版本,socket缓冲区做了调整,DMA带收集功能。

(1)DMA从拷贝至内核缓冲区

(2)将数据的位置和长度的信息的描述符增加至内核空间(socket缓冲区)

(3)DMA将数据从内核拷贝至协议引擎

这个复制过程是零拷贝过程。

6.2.2 Netty

Netty 中的零拷贝和上面提到的操作系统层面上的零拷贝不太一样, 我们所说的 Netty 零拷贝完全是基于(Java 层面)用户态的。

(1)Netty 通过 DefaultFileRegion 类对FileChannel 的 tranferTo() 方法进行包装,相当于是间接的通过java进行零拷贝。

(2)我们的数据传输一般都是通过TCP/IP协议实现的,在实际应用中,很有可能一条完整的消息被分割为多个数据包进行网络传输,而单个的数据包对你而言是没有意义的,只有当这些数据包组成

一条完整的消息时你才能做出正确的处理,而Netty可以通过零拷贝的方式将这些数据包组合成一条完整的消息供你来使用。

此时零拷贝的作用范围仅在用户空间中。

那Netty是如何实现的呢?为此我们就要找到Netty进行数据传输的接口,这个接口一定包含了可以实现零拷贝的功能,这个接口就是ChannelBuffer。

既然有接口肯定就有实现类,一个最主要的实现类是CompositeChannelBuffer,这个类的主要作用是将多个ChannelBuffer组成一个虚拟的ChannelBuffer来进行操作

为什么说是虚拟的呢,因为CompositeChannelBuffer并没有将多个ChannelBuffer真正的组合起来,而只是保存了他们的引用,这样就避免了数据的拷贝,实现了Zero Copy。

(3)ByteBuf 可以通过 wrap 操作把字节数组、ByteBuf、ByteBuffer 包装成一个 ByteBuf 对象, 进而避免了拷贝操作

(4)ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf,避免了内存的拷贝

6.2.3 kafka

Kafka 的索引文件使用的是 mmap + write 方式,数据文件使用的是 sendfile 方式。适用于系统日志消息这种高吞吐量的大块文件的数据持久化和传输。

如果有10个消费者,传统方式下,数据复制次数为4*10=40次,而使用“零拷贝技术”只需要1+10=11次,一次为从磁盘复制到页面缓存,10次表示10个消费者各自读取一次页面缓存

6.3 10道Java高级必备的Netty面试题

6.3.1 BIO、NIO 和 AIO 的区别?

  • BIO:一个连接一个线程,客户端有连接请求时服务器端就需要启动一个线程进行处理。线程开销大。
  • 伪异步 IO: 将请求连接放入线程池,一对多,但线程还是很宝贵的资源。
  • NIO:一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上,多路复用器 轮询到连接有 I/O 请求时才启动一个线程进行处理。
  • AIO:一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启 动线程进行处理,

BIO 是面向流的,NIO 是面向缓冲区的; BIO 的各种流是阻塞的。而 NIO 是非阻塞的;BIO 的 Stream 是单向的,而 NIO 的 channel 是双向的。

NIO 的特点:事件驱动模型、单线程处理多任务、非阻塞 I/O,I/O 读写不再阻塞,而是返回 0、基于 block 的传输比基于流的传输更高效、更高级的 IO 函数 zero-copy、IO 多路复用大大提高了 Java 网络应用的可伸缩性和实用性。基于 Reactor 线程模型。

在 Reactor 模式中,事件分发器等待某个事件或者可应用或个操作的状态发生,事件分发器 就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。如 在 Reactor 中实现读:注册读就绪事件和相应的事件处理器、事件分发器等待事件、事件到 来,激活分发器,分发器调用事件对应的处理器、事件处理器完成实际的读操作,处理读到 的数据,注册新的事件,然后返还控制权。

6.3.2 NIO 的组成?

Buffer:与 Channel 进行交互,数据是从 Channel 读入缓冲区,从缓冲区写入 Channel 中的

  • flip 方法 : 反转此缓冲区,将 position 给 limit,然后将 position 置为 0,其实就是切换读写 模式
  • clear 方法 :清除此缓冲区,将 position 置为 0,把 capacity 的值给 limit。
  • rewind 方法 : 重绕此缓冲区,将 position 置为 0

DirectByteBuffer 可减少一次系统空间到用户空间的拷贝。但 Buffer 创建和销毁的成本更高, 不可控,通常会用内存池来提高性能。直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。如果数据量比较小的中小应用情况下,可以考虑使用 heapBuffer,由 JVM 进行管理。

Channel:表示 IO 源与目标打开的连接,是双向的,但不能直接访问数据,只能与 Buffer 进 行交互。通过源码可知,FileChannel 的 read 方法和 write 方法都导致数据复制了两次!

Selector 可使一个单独的线程管理多个 Channel,open 方法可创建 Selector,register 方法向多路复用器器注册通道,可以监听的事件类型:读、写、连接、accept。注册事件后会产生 一个 SelectionKey:它表示 SelectableChannel 和 Selector 之间的注册关系,wakeup 方法: 使尚未返回的第一个选择操作立即返回,唤醒的原因是:注册了新的 channel 或者事件; channel 关闭,取消注册;优先级更高的事件触发(如定时器事件),希望及时处理。

Selector 在 Linux 的实现类是 EPollSelectorImpl,委托给 EPollArrayWrapper 实现,其中三个 native 方法是对 epoll 的封装,而 EPollSelectorImpl. implRegister 方法,通过调用 epoll_ctl 向 epoll 实例中注册事件,还将注册的文件描述符(fd)与 SelectionKey 的对应关系添加到 fdToKey 中,这个 map 维护了文件描述符与 SelectionKey 的映射。

fdToKey 有时会变得非常大,因为注册到 Selector 上的 Channel 非常多(百万连接);过期或 失效的 Channel 没有及时关闭。fdToKey 总是串行读取的,而读取是在 select 方法中进行的, 该方法是非线程安全的。

Pipe:两个线程之间的单向数据连接,数据会被写到 sink 通道,从 source 通道读取

NIO 的服务端建立过程:Selector.open():打开一个 Selector;ServerSocketChannel.open():创 建服务端的 Channel;bind():绑定到某个端口上。并配置非阻塞模式;register():注册 Channel 和关注的事件到 Selector 上;select()轮询拿到已经就绪的事件。

6.3.3 Netty 的特点?

一个高性能、异步事件驱动的 NIO 框架,它提供了对 TCP、UDP 和文件传输的支持 使用更高效的 socket 底层,对 epoll 空轮询引起的 cpu 占用飙升在内部进行了处理,避免了 直接使用

NIO 的陷阱,简化了 NIO 的处理方式。

采用多种 decoder/encoder 支持,对 TCP 粘包/分包进行自动化处理 可使用接受/处理线程池,提高连接效率,对重连、心跳检测的简单支持

可配置 IO 线程数、TCP 参数, TCP 接收和发送缓冲区使用直接内存代替堆内存,通过内存 池的方式循环利用 ByteBuf

通过引用计数器及时申请释放不再引用的对象,降低了 GC 频率 使用单线程串行化的方式,高效的 Reactor 线程模型

大量使用了 volitale、使用了 CAS 和原子类、线程安全类的使用、读写锁的使用

6.3.4 Netty 的线程模型?

Netty 通过 Reactor 模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss 线程池和 work 线程池,其中 boss 线程池的线程负责处理请求的 accept 事件,当接收到 accept 事

件的请求时,把对应的 socket 封装到一个 NioSocketChannel 中,并交给 work 线程池,其 中 work 线程池负责请求的 read 和 write 事件,由对应的 Handler 处理。 

单线程模型:所有 I/O 操作都由一个线程完成,即多路复用、事件分发和处理都是在一个Reactor 线程上完成的。既要接收客户端的连接请求,向服务端发起连接,又要发送/读取请求 或应答/响应消息。一个 NIO 线程同时处理成百上千的链路,性能上无法支撑,速度慢,若 线程进入死循环,整个程序不可用,对于高负载、大并发的应用场景不合适。 

多线程模型:有一个 NIO 线程(Acceptor) 只负责监听服务端,接收客户端的 TCP 连接请 求;NIO 线程池负责网络 IO 的操作,即消息的读取、解码、编码和发送;1 个 NIO 线程可 以同时处理 N 条链路,但是 1 个链路只对应 1 个 NIO 线程,这是为了防止发生并发操作 问题。但在并发百万客户端连接或需要安全认证时,一个 Acceptor 线程可能会存在性能不 足问题。

主从多线程模型:Acceptor 线程用于绑定监听端口,接收客户端连接,将 SocketChannel 从 主线程池的 Reactor 线程的多路复用器上移除,重新注册到 Sub 线程池的线程上,用于处 理 I/O 的读写等操作,从而保证 mainReactor 只负责接入认证、握手等操作; 

6.3.5 TCP 粘包/拆包的原因及解决方法?

TCP 是以流的方式来处理数据,一个完整的包可能会被 TCP 拆分成多个包进行发送,也可能 把小的封装成一个大的数据包发送。

TCP 粘包/分包的原因:

应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象,而应用程序写入 数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包现 象;
进行 MSS 大小的 TCP 分段,当 TCP 报文长度-TCP 头部长度>MSS 的时候将发生拆包 以太网帧的 payload(净荷)大于 MTU(1500 字节)进行 ip 分片。

解决方法

消息定长:FixedLengthFrameDecoder 类 包尾增加特殊字符分割:行分隔符类:LineBasedFrameDecoder 或自定义分隔符类 : DelimiterBasedFrameDecoder

将消息分为消息头和消息体:LengthFieldBasedFrameDecoder 类。分为有头部的拆包与粘包、 长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。 

6.3.6 了解哪几种序列化协议? 

序列化(编码)是将对象序列化为二进制形式(字节数组),主要用于网络传输、数据持久 化等;而反序列化(解码)则是将从网络、磁盘等读取的字节数组还原成原始对象,主要用 于网络传输对象的解码,以便完成远程调用。 

影响序列化性能的关键因素:序列化后的码流大小(网络带宽的占用)、序列化的性能(CPU资源占用);是否支持跨语言(异构系统的对接和开发语言切换)。 

Java 默认提供的序列化:无法跨语言、序列化后的码流太大、序列化的性能差

XML,优点:人机可读性好,可指定元素或特性的名称。缺点:序列化数据只包含数据本身 以及类的结构,不包括类型标识和程序集信息;只能序列化公共属性和字段;不能序列化方 法;文件庞大,文件格式复杂,传输占带宽。适用场景:当做配置文件存储数据,实时数据 转换。

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 调用、具有良好的跨防火墙的访问属性、适 合应用层对象的持久化其它。

protostuff 基于 protobuf 协议,但不需要配置 proto 文件,直接导包即可 Jboss marshaling 可以直接序列化 java 类, 无须实 java.io.Serializable 接口 Message pack 一个高效的二进制序列化格式
Hessian 采用二进制协议的轻量级 remoting onhttp 工具

kryo 基于protobuf协议,只支持java语言,需要注册(Registration),然后序列化(Output), 反序列化(Input)

6.3.7 如何选择序列化协议?

具体场景

对于公司间的系统调用,如果性能要求在 100ms 以上的服务,基于 XML 的 SOAP 协议是一 个值得考虑的方案。
基于 Web browser 的 Ajax,以及 Mobile app 与服务端之间的通讯,JSON 协议是首选。对于 性能要求不太高,或者以动态类型语言为主,或者传输数据载荷很小的的运用场景,JSON 也是非常不错的选择。

对于调试环境比较恶劣的场景,采用 JSON 或 XML 能够极大的提高调试效率,降低系统开发 成本。
当对性能和简洁性有极高要求的场景,Protobuf,Thrift,Avro 之间具有一定的竞争关系。 对于 T 级别的数据的持久化应用场景,Protobuf 和 Avro 是首要选择。如果持久化后的数据 存储在 hadoop 子项目里,Avro 会是更好的选择。

对于持久层非 Hadoop 项目,以静态类型语言为主的应用场景,Protobuf 会更符合静态类型 语言工程师的开发习惯。由于 Avro 的设计理念偏向于动态类型语言,对于动态语言为主的 应用场景,Avro 是更好的选择。
如果需要提供一个完整的 RPC 解决方案,Thrift 是一个好的选择。 如果序列化之后需要支持不同的传输层协议,或者需要跨防火墙访问的高性能场景, Protobuf 可以优先考虑。

protobuf 的数据类型有多种:bool、double、float、int32、int64、string、bytes、enum、message。 protobuf的限定符:required: 必须赋值,不能为空、optional:字段可以赋值,也可以不赋值、 repeated: 该字段可以重复任意次数(包括 0 次)、枚举;只能用指定的常量集中的一个值作 为其值; 

protobuf 的基本规则:每个消息中必须至少留有一个 required 类型的字段、包含 0 个或多个 optional 类型的字段;repeated 表示的字段可以包含 0 个或多个数据;[1,15]之内的标识号在 编码的时候会占用一个字节(常用),[16,2047]之内的标识号则占用 2 个字节,标识号一定 不能重复、使用消息类型,也可以将消息嵌套任意多层,可用嵌套消息类型来代替组。

protobuf 的消息升级原则:不要更改任何已有的字段的数值标识;不能移除已经存在的required 字段,optional 和 repeated 类型的字段可以被移除,但要保留标号不能被重用。新 添加的字段必须是 optional 或 repeated。因为旧版本程序无法读取或写入新增的 required 限 定符的字段。 

编译器为每一个消息类型生成了一个.java 文件,以及一个特殊的 Builder 类(该类是用来创 建 消 息 类 接 口 的 ) 。 如 : UserProto.User.Builder builder = UserProto.User.newBuilder();builder.build();

Netty 中的使用:ProtobufVarint32FrameDecoder 是用于处理半包消息的解码类; ProtobufDecoder(UserProto.User.getDefaultInstance())这是创建的 UserProto.java 文件中的解码 类;ProtobufVarint32LengthFieldPrepender 对 protobuf 协议的消息头上加上一个长度为 32 的整形字段,用于标志这个消息的长度的类;ProtobufEncoder 是编码类

将 StringBuilder 转换为 ByteBuf 类型:copiedBuffer()方法 

6.3.8 Netty 的零拷贝实现?

Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写, 不需要进行字节缓冲区的二次拷贝。堆内存多了一次内存拷贝,JVM 会将堆内存 Buffer 拷贝 一份到直接内存中,然后才写入 Socket 中。ByteBuffer 由 ChannelConfig 分配,而 ChannelConfig 创建 ByteBufAllocator 默认使用 Direct Buffer

CompositeByteBuf 类可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了传统通过 内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer。addComponents 方法将 header 与 body 合并为一个逻辑上的 ByteBuf, 这两个 ByteBuf 在 CompositeByteBuf 内部都是单独存 在的, CompositeByteBuf 只是逻辑上是一个整体

通过 FileRegion 包装的 FileChannel.tranferTo 方法 实现文件传输, 可以直接将文件缓冲区的 数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。

通过 wrap 方法, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer 等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作。

Selector BUG:若 Selector 的轮询结果为空,也没有 wakeup 或新消息处理,则发生空轮询, CPU 使用率 100%,

Netty 的解决办法:对 Selector 的 select 操作周期进行统计,每完成一次空的 select 操作进行 一次计数,若在某个周期内连续发生 N 次空轮询,则触发了 epoll 死循环 bug。重建 Selector, 判断是否是其他线程发起的重建请求,若不是则将原 SocketChannel 从旧的 Selector 上去除注册,重新注册到新的 Selector 上,并将原来的 Selector 关闭。 

6.3.9 Netty 的高性能表现在哪些方面?

心跳,对服务端:会定时清除闲置会话 inactive(netty5),对客户端:用来检测会话是否断开, 是否重来,检测网络延迟,其中 idleStateHandler 类 用来检测会话状态

串行无锁化设计,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就 避免了多线程竞争和同步锁。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。 但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种 局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。

可靠性,链路有效性检测:链路空闲检测机制,读/写空闲超时机制;内存保护机制:通过 内存池重用 ByteBuf;ByteBuf 的解码保护;优雅停机:不再接收新消息、退出前的预处理操 作、资源的释放操作。

Netty 安全性:支持的安全协议:SSL V2 和 V3,TLS,SSL 单向认证、双向认证和第三方 CA 认证。

高效并发编程的体现:volatile 的大量、正确使用;CAS 和原子类的广泛使用;线程安全容器 的使用;通过读写锁提升并发性能。IO 通信性能三原则:传输(AIO)、协议(Http)、线程 (主从多线程)

流量整型的作用(变压器):防止由于上下游网元性能不均衡导致下游网元被压垮,业务流 中断;防止由于通信模块接受消息过快,后端业务线程处理不及时导致撑死问题。

TCP 参数配置:SO_RCVBUF 和 SO_SNDBUF:通常建议值为 128K 或者 256K;SO_TCPNODELAY: NAGLE 算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止大量小封包的发送阻 塞网络,从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法; 

6.3.10 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 每次都会新建一个缓冲区对象。 

99 直接读这些牛人的原文

即时通讯网:

C10K问题及解决方案

 

周志垒老师授课:对标阿里P8,清晰明了,从nio,epoll一直学到netty

I/O 多路复用之select、poll、epoll实现原理及对比总结

 

高性能IO之Reactor模式

3万字加50张图,带你深度解析 Netty 架构与原理(上)

3万字加50张图,带你深度解析 Netty 架构与原理(下)

 

码海:45 张图深度解析 Netty 架构与原理

石杉的架构笔记:拜托!面试请不要再问我 Netty 底层架构原理!

bugstack虫洞栈 Netty4.x 专题案例

使用Netty如何做到单机秒级接收35万个对象

 

callingcim(cross IM) 适用于开发者的即时通讯系统 https://gitee.com/shach1990/cim

CIM是一套基于mina或netty框架下的推送系统 https://gitee.com/farsunset/cim

J-IM 是用JAVA语言,基于t-io开发的轻量、高性能、单机支持几十万至百万在线用户IM https://gitee.com/xchao/j-im

JAVA版基于netty的物联网高并发智能网关 https://gitee.com/willbeahero/IOTGate

iot_push基于netty+mqtt 3.1.1协议开发的物联网消息推送框架(此项目不维护了) 请参考新项目   reactor3实现的mqtt库 https://github.com/1ssqq1lxr/iot-harbor

TTIoT云端物联网组件;面向JAVA;以事件为驱动;为设备提供安全可靠的连接通信能力 https://gitee.com/cloudSj/TTIot

轻量级物联网MQTT服务器, 快速部署, 支持集群. https://gitee.com/recallcode/iot-mqtt-server

posted @ 2023-12-21 13:59  沙漏哟  阅读(10)  评论(0编辑  收藏  举报