文章目录
一 基本概念
1、什么是 Netty?
2、Netty 的优势?
3、Netty 有什么特点?
4、Netty 有哪些应用场景?
5、Netty 的高性能体现在?
6、相比原生 NIO 的优势?
7、Netty 和 Tomcat 的区别?
8、BIO. NIO. AIO 分别是什么?
9、Select、Poll、Epoll 的区别?
10、什么是 Reactor 模型?
二 架构组件
1、Netty 有哪些核心组件?
2、什么是 EventLoop 和 EventLoopGroup?
3、说说 Netty 的线程模型?
4、Netty 服务端的启动过程?
三 具体实现
1、Netty 的无锁化体现在哪里?
2、如何解决 JDK epoll 空轮询问题?
3、什么是拆包和粘包?
4、拆包粘包的解决方案?
5、Netty 如何解决拆包粘包?
6、Netty 零拷贝体现在哪里?
7、TCP 的长连接和短连接?
8、Netty 长连接、心跳机制了解么?
9、说说 Netty 的对象池技术?
10 有哪些序列化协议?
一 基本概念
1、什么是 Netty?
- Netty 是由 JBOSS 提供的一个 Java 开源框架。
- Netty 提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 IO 程序,是目前最流行的 NIO 框架,
- Netty 在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,知名的 Elasticsearch 、Dubbo 框架内部都采用了 Netty。
2、Netty 的优势?
- 使用简单:封装了 Java 原生 NIO 类库繁琐的 API,使用起来更加高效;
- 功能强大:预置多种编码能力,支持多种主流协议。同时通过 ChannelHandler 可以进行灵活的拓展,支持很强的定制能力;
- 高性能:与其它业界主流 NIO 框架相比,Netty 综合更优。主要体现在吞吐量更高、延迟更低、减少资源消耗以及最小化不必要的内存复制;
- 社区活跃与稳定:版本更新周期短,BUG 修复速度快,让开发者可以专注业务本身。
3、Netty 有什么特点?
- 高并发:Netty 是一款基于 NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架。
- 传输快:Netty 使用零拷贝特性,尽量减少不必要的内存拷贝,实现更快的传输效率。
- 封装好:Netty 封装了 NIO 操作的很多细节,提供易于使用的 API。
4、Netty 有哪些应用场景?
理论上来说,NIO 可以做的事情,Netty 都可以做并且更好。Netty 主要用来做网络通信:
- RPC 框架的网络通信工具;
- 实现一个 HTTP 服务器;
- 实现一个即时通讯系统;
- 实现消息推送系统。
5、Netty 的高性能体现在?
- IO 线程模型:同步非阻塞;
- 零拷贝:尽量做到不必要的内存拷贝:
- 内存池设计:使用直接内存,并且可重复利用;
- 串行化处理读写:避免使用锁带来的额外开销;
- 高性能序列化协议:支持 protobuf 等高性能序列化协议。
6、相比原生 NIO 的优势?
1)易用性:Netty 在 NIO 基础上封装了更加人性化的 API,大大降低开发人员的学习成本,同时还提供了很多开箱即用的工具。
2)稳定性:Netty 修复了 Java NIO 较多已知问题,如 select 空转导致 CPU 100%,TCP 断线重连,Keep-alive 检测等问题。
3)高性能:对象池复用(通过对象复用避免频繁创建和销毁带来的开销)和零拷贝技术。
7、Netty 和 Tomcat 的区别?
Netty 和 Tomcat 最大的区别在于对通信协议的支持:
- Tomcat 是基于 Http 协议的,本质是一个基于 http 协议的 web 容器,而 Netty 不仅支持 HTTP,还能通过编程自定义各种协议,通过 codec 自定义编码/解码字节流,完成数据传输。
- Tomcat 需要遵循 Servlet 规范(HTTP 协议的请求-响应模型),而 Netty 不需要受到 Servlet 规范约束,可以发挥 NIO 最大特性。
8、BIO. NIO. AIO 分别是什么?
- BIO(同步阻塞 IO)
- 服务器实现模式为一个连接一个线程,
- 即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
- BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK 1.4 以前的唯一选择,但程序直观简单易理解。
- NIO(同步非阻塞 IO)
- 服务器实现模式为一个请求一个线程,
- 即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 IO 请求时才启动一个线程进行处理。
- NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,
- 比如聊天服务器,并发局限于应用中,编程比较复杂,JDK 1.4 开始支持。
- AIO(异步非阻塞 IO)
- 服务器实现模式为一个有效请求一个线程,
- 客户端的 IO 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理。
- AIO 方式使用于连接数目多且连接比较长(重操作)的架构,
- 比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK 1.7 开始支持。
9、Select、Poll、Epoll 的区别?
ref: https://www.cnblogs.com/aspirant/p/9166944.html
select、poll和epoll都是IO多路复用的技术,可以让一个进程同时监视多个文件描述符的状态,从而提高IO效率。它们的区别主要有以下几点¹²³:
- - 底层实现:select的底层实现为数组,poll的底层实现为链表,而epoll的底层实现为红黑树。
- - 最大连接数:select的最大连接数为1024或2048,而poll和epoll是无上限的。
- - IO效率:select和poll都需要轮询所有的文件描述符,复杂度为O(n),而epoll只需要处理就绪的文件描述符,复杂度为O(1)。另外,select和poll每次调用都需要拷贝用户空间和内核空间的数据结构,而epoll只需要拷贝一次。
- - 模式:select和poll只支持水平触发(LT),即当文件描述符就绪时会不断通知进程,直到处理完毕;而epoll支持水平触发和边缘触发(ET),即当文件描述符就绪时只通知进程一次,如果没有处理完毕,下次不会再通知。
--IO多路复用
IO多路复用指的是使用一个线程或进程来同时监测多个文件描述符(通常是 socket)是否可以执行 IO 操作的能力。¹²³
IO多路复用的目的是为了提高 IO 效率,避免浪费 CPU 资源和线程/进程资源。¹²³
IO多路复用的原理是通过调用一些系统函数,如 select,poll,epoll 等,来注册需要监听的文件描述符和事件,然后阻塞等待某些事件的发生或超时。当有事件发生时,系统函数会返回,并告知哪些文件描述符可以进行 IO 操作。¹²³
不同的系统函数有不同的特点和优缺点,
- 比如 select 有最大文件描述符数量的限制,
- poll 没有这个限制,但是都需要遍历整个文件描述符集合,
- 而 epoll 可以利用红黑树和就绪链表来提高性能和扩展性。
--关于 Epoll
Epoll 是 event poll 的缩写,是 Linux 内核的一种可扩展的 I/O 事件通知机制。¹²³
Epoll 可以高效地监控多个文件描述符(fd)上的事件,比如可读、可写、错误等。¹²³
Epoll 提供了两种触发模式:水平触发(LT)和边沿触发(ET)。¹²³
Epoll 通过使用红黑树搜索被监视的文件描述符,以及使用就绪链表存储已经就绪的事件,来提高性能和扩展性。¹²³
Epoll 的相关函数有 epoll_create,epoll_ctl,epoll_wait 等。¹²³
--关于 水平触发和边缘触发
水平触发和边缘触发是两种不同的 Epoll 触发模式,它们的区别如下:¹²³
- **水平触发(LT)**:只要文件描述符关联的读或写缓冲区有数据,就会一直触发可读或可写信号,直到缓冲区为空或满为止。¹²³
- **边缘触发(ET)**:只有当文件描述符关联的读或写缓冲区由空变为非空或由满变为不满时,才会触发可读或可写信号,只通知一次。¹²³
两种触发模式的优缺点如下:¹²³
- **水平触发**的优点是编程简单,不会遗漏事件;缺点是可能会重复通知,浪费资源。¹²³
- **边缘触发**的优点是效率高,减少了系统调用;缺点是编程复杂,需要一次性处理完所有数据,否则可能会丢失事件。¹²³
--哪种触发模式更适合高并发场景?
10、什么是 Reactor 模型?
Reactor 模型是一种基于事件驱动的网络编程模式,它使用一个或多个输入源向一个服务处理器发送并发的服务请求,服务处理器负责监听和分发事件,将接收到的事件同步地分配给相应的请求处理器¹⁵。
Reactor 模型的核心是利用 IO 多路复用技术来实现高效的事件驱动
-- 拓展:Reactor 模型中的 事件驱动 指的是什么
- Reactor 模型中的 事件驱动 指的是一种编程模式,它使得应用程序不需要主动调用某个API来处理事件,而是通过注册回调函数来响应事件的发生¹³⁴。
- Reactor 模型是基于 IO多路复用构建起来的,IO多路复用本身就是借助于事件驱动模型来实现高效的网络通信¹。Reactor 模型可以提高应用程序的性能和可扩展性⁵。
-- 拓展:事件驱动模式和回调函数的区别
事件驱动模式和回调函数的区别是,
事件驱动模式是一种程序执行模式,
它使得程序的执行是由外部事件触发而不是顺序执行的方式。
回调函数是一种实现事件驱动模式的方法,它是一种特殊的函数,它作为参数传递给另一个函数,并在被调用函数执行完毕后被调用¹³。
-- 拓展:事件驱动模式和回调函数的联系
事件驱动模式和回调函数的联系是,
事件驱动模式需要有一个轮询中心来检测事件的发生,并通知感兴趣的对象。
回调函数就是这些对象提供的接口,用于处理事件的结果。事件驱动模式通过回调函数来实现代码的分离和复用。
二 架构组件
1、Netty 有哪些核心组件?
- Channel
基础的 IO 操作,如绑定、连接、读写等都依赖于底层网络传输所提供的原语,在 Java 的网络编程中,基础核心类是 Socket,而 Netty 的 Channel 提供了一组 API,极大地简化了直接与 Socket 进行操作的复杂性,并且 Channel 是很多类的父类,如 EmbeddedChannel、LocalServerChannel、NioDatagramChannel、NioSctpChannel、NioSocketChannel 等。
- EventLoop
EventLoop 定义了处理在连接过程中发生的事件的核心抽象。
说白了,EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 IO 操作的处理。
那 Channel 和 EventLoop 直接有啥联系呢?
Channel 为 Netty 网络操作(读写等操作)抽象类,EventLoop 负责处理注册到其上的 Channel 处理 IO 操作,两者配合参与 IO 操作。
- ChannelFuture
由于 Netty 是异步非阻塞的,所有的 IO 操作也都为异步的,我们不能立刻得到操作是否执行成功,因此 Netty 提供 ChannelFuture 接口,使用其 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。
并且,我们还可以通过 ChannelFuture 的 channel() 方法获取关联的Channel,甚至使用 sync() 方法让异步的操作变成同步的。
- ChannelHandler 和 ChannelPipeline
从应用开发者看来,ChannelHandler 是最重要的组件,其中存放用来处理进站和出站数据的用户逻辑。ChannelHandler 的方法被网络事件触发,可以用于几乎任何类型的操作,如将数据从一种格式转换为另一种格式或处理抛出的异常。如其子接口ChannelInboundHandler,接受进站的事件和数据以便被用户定义的逻辑处理,或者当响应所连接的客户端时刷新ChannelInboundHandler的数据。
ChannelPipeline为ChannelHandler 链提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API。当创建 Channel 时,会自动创建一个附属的 ChannelPipeline。
- Bootstrap 和 ServerBootstrap
Netty 的引导类应用程序网络层配置提供容器,其涉及将进程绑定到给定端口或连接一个进程到在指定主机上指定端口上运行的另一进程。引导类分为客户端引导 Bootstrap 和服务端引导 ServerBootstrap。
--拓展
你好,这是 Bing。根据网上的资料,Netty 有以下几个核心组件¹³⁴⁵:
- - **Channel**:
- Channel 是 Netty 中的一个基本抽象,它表示一个连接或者一个数据传输的载体,可以是 TCP 连接,UDP 连接,文件或者内存块等。
- - **EventLoop**:
- EventLoop 是 Netty 中的一个事件循环器,它负责处理 Channel 上的所有事件,包括连接、读写、异常等。
- 一个 EventLoop 可以绑定多个 Channel,但一个 Channel 只能绑定一个 EventLoop。
- - **ChannelFuture**:
- ChannelFuture 是 Netty 中的一个异步操作结果的占位符,
- 它可以用来监听 Channel 上的操作是否完成,以及获取操作的结果或者异常。
- - **ChannelHandler**:
- ChannelHandler 是 Netty 中最核心的组件,它充当了所有处理入站和出站数据的应用程序逻辑的容器。
- Netty 提供了多种 ChannelHandler 的实现,例如编解码器、压缩器、加密器等。
- - **ChannelPipeline**:
- ChannelPipeline 是 Netty 中的一个责任链模式的实现,它管理了一组 ChannelHandler,并按照顺序对 Channel 上的事件进行分发和处理。
- 每个 Channel 都有一个与之关联的 ChannelPipeline。
2、什么是 EventLoop 和 EventLoopGroup?
EventLoopGroup 包含多个 EventLoop(每一个 EventLoop 通常内部包含一个线程),上面我们已经说了 EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。
并且 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理,即 Thread 和 EventLoop 属于 1 : 1 的关系,从而保证线程安全。
上图是一个服务端对 EventLoopGroup 使用的大致模块图,其中 Boss EventloopGroup 用于接收连接,Worker EventloopGroup 用于具体的处理(消息的读写以及其他逻辑处理)。
从上图可以看出:当客户端通过 connect 方法连接服务端时,bossGroup 处理客户端连接请求。当客户端处理完成后,会将这个连接提交给 workerGroup 来处理,然后 workerGroup 负责处理其 IO 相关操作。
3、说说 Netty 的线程模型?
Netty 通过 Reactor 模型基于多路复用器接收并处理用户请求,
内部实现了两个线程池,boss 线程池和 work 线程池,
- 其中 boss 线程池的线程负责处理请求的 accept 事件,当接收到 accept 事件的请求时,把对应的 socket 封装到一个 NioSocketChannel 中并交给 work 线程池,
- 其中 work 线程池负责请求的 read 和 write 事件,由对应的 Handler 处理。
--拓展:根据网上的资料,Netty 的线程模型是基于 Reactor 模式的,它可以支持三种不同的线程模型¹²³⁴:
- - **单 Reactor 单线程**:所有的 I/O 操作都在同一个 NIO 线程上面完成,这种模型适合小容量的应用场景,但是在高负载、大并发的场景下性能不足,而且存在可靠性问题。
- - **单 Reactor 多线程**:有一个 NIO 线程负责接收新的连接,然后将新建立的连接分配给一组 NIO 线程来处理读写操作。这种模型可以提高并发度和吞吐量,但是存在线程切换和同步的开销。
- - **主从 Reactor 多线程**:有一个独立的 NIO 线程池负责接收客户端的连接,然后将连接注册到后端的 NIO 线程池中,由后端的 NIO 线程负责后续的 I/O 操作。这种模型可以进一步提升性能,特别是在接收连接时需要进行安全认证等耗时操作的场景。
Netty 的线程模型并不是一成不变的,它实际取决于用户的启动参数配置。
- Netty 提供了两种 EventLoopGroup 的实现:
- NioEventLoopGroup 和 EpollEventLoopGroup ,分别对应 NIO 和 Epoll 的模式。
- 用户可以通过设置不同的 EventLoopGroup 和线程数来选择合适的线程模型。
--拓展:根据网上的资料,NioEventLoopGroup 和 EpollEventLoopGroup 的区别是¹²³⁴:
- - NioEventLoopGroup 是基于 Java NIO 技术的,它使用 Selector 来监听和处理 Channel 上的事件,可以在多个线程之间共享 Selector ,以实现多路复用。
- - EpollEventLoopGroup 是基于 Linux 的 epoll 技术的,它使用 JNI 的方式调用本地方法来监听和处理 Channel 上的事件,可以使用边缘触发模式,以减少不必要的事件通知。
- - EpollEventLoopGroup 相比 NioEventLoopGroup 有更高的性能、更低的内存占用、更少的 GC 和更多的高级特性,例如 TCP_CORK, SO_REUSEADDR 等。但是它只能在 Linux 平台上使用。
- - Netty 可以根据用户的配置和操作系统的类型来选择合适的 EventLoopGroup 实现,用户只需将相应的类替换即可切换不同的模式。
--拓展: 用户如何选择合适的 EventLoopGroup 的方法是:
- - 根据操作系统的类型和支持的技术来选择。如果是 Linux 平台,并且支持 epoll 技术,那么可以使用 EpollEventLoopGroup 来获得更高的性能和更多的特性。如果是其他平台,或者不支持 epoll 技术,那么可以使用 NioEventLoopGroup 来兼容 NIO 技术。
- - 根据应用场景的需求和负载情况来选择。如果是小容量或者低并发的场景,可以使用单线程的 EventLoopGroup 来简化编程和减少资源占用。如果是高并发或者高吞吐量的场景,可以使用多线程的 EventLoopGroup 来提高并行度和效率。如果是需要进行安全认证或者其他耗时操作的场景,可以使用主从多线程的 EventLoopGroup 来分离连接和 I/O 处理。
- - 根据 Netty 的启动参数和配置来选择。Netty 提供了不同的 EventLoopGroup 的实现类,例如 NioEventLoopGroup, EpollEventLoopGroup, OioEventLoopGroup, LocalEventLoopGroup 等。用户可以通过设置不同的 EventLoopGroup 和线程数来创建不同的线程模型,例如单 Reactor 单线程,单 Reactor 多线程,主从 Reactor 多线程等。
单线程模型
所有 IO 操作都由一个线程完成,即多路复用、事件分发和处理都是在一个 Reactor 线程上完成的。既要接收客户端的连接请求,向服务端发起连接,又要发送、读取请求或应答、响应消息。一个 NIO 线程同时处理成百上千的链路,性能上无法支撑,速度慢,若线程进入死循环,整个程序不可用,对于高负载、高并发的应用场景不合适。
//1.eventGroup既用于处理客户端连接,又负责具体的处理。 EventLoopGroup eventGroup = new NioEventLoopGroup(1); //2.创建服务端启动引导/辅助类: ServerBootstrap ServerBootstrap b = new ServerBootstrap(); boobtstrap.group(eventGroup, eventGroup) //......
多线程模型
有一个 NIO 线程(Acceptor) 只负责监听服务端,接收客户端的 TCP 连接请求;NIO 线程池负责网络 IO 的操作,即消息的读取、解码、编码和发送;1 个 NIO 线程可以同时处理 N 条链路,但是 1 个链路只对应 1 个 NIO 线程,这是为了防止发生并发操作问题。但在并发百万客户端连接或需要安全认证时,一个 Acceptor 线程可能会存在性能不足问题。
对于代码:
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理 EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //2.创建服务端启动引导/辅助类: ServerBootstrap ServerBootstrap b = new ServerBootstrap(); //3.给引导类配置两大线程组,确定了线程模型 b.group(bossGroup, workerGroup) //...... }
主从多线程模型
Acceptor 线程用于绑定监听端口,接收客户端连接,将 SocketChannel 从主线程池的 Reactor 线程的多路复用器上移除,重新注册到 Sub 线程池的线程上,用于处理 IO 的读写等操作,从而保证主 Reactor 只负责接入认证、握手等操作。如果多线程模型无法满足你的需求的时候,可以考虑使用主从多线程模型 。
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理 EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //2.创建服务端启动引导/辅助类: ServerBootstrap ServerBootstrap b = new ServerBootstrap(); //3.给引导类配置两大线程组,确定了线程模型 b.group(bossGroup, workerGroup) //...... }
4、Netty 服务端的启动过程?
先来看一段代码实现:
- 1. 创建 bossGroup,workerGroup 其中 bossGroup用于接收连接,workerGroup 用于具体的处理
- 2. 创建服务端启动引导/辅助类:ServerBootstrap
- 3. 给引导类配置两大线程组,确定了线程模型
- 4.指定 IO 模型
- 5.可以自定义客户端消息的业务处理逻辑
- 6.绑定端口,调用 sync 方法阻塞知道绑定完成
- 7.阻塞等待直到服务器Channel关闭(closeFuture()方法获取Channel 的CloseFuture对象,然后调用sync()方法)
- 8.优雅关闭相关线程组资源
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.创建服务端启动引导/辅助类:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.给引导类配置两大线程组,确定了线程模型
b.group(bossGroup, workerGroup) // (非必备)打印日志
.handler(new LoggingHandler(LogLevel.INFO)) // 4.指定 IO 模型
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
//5.可以自定义客户端消息的业务处理逻辑
p.addLast(new HelloServerHandler());
}
});
// 6.绑定端口,调用 sync 方法阻塞知道绑定完成
ChannelFuture f = b.bind(port).sync();
// 7.阻塞等待直到服务器Channel关闭(closeFuture()方法获取Channel 的CloseFuture对象,然后调用sync()方法)
f.channel().closeFuture().sync();
} finally {
//8.优雅关闭相关线程组资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
三 具体实现
1、Netty 的无锁化体现在哪里?
- Netty 采用了串行无锁化设计,在 IO 线程内部进行串行操作,避免多线程竞争导致的性能下降。
- 表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。
- 但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,
- 这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。
Netty 的 NioEventLoop 读取到消息后,直接调用 ChannelPipeline 的 fireChannelRead(Object msg),只要用户不主动切换线程,一直会由 NioEventLoop 调用到用户的 handler,期间不进行线程切换,这种串行化处理方式避免了多线程操作导致的锁竞争,从性能角度看是最优的。
2、如何解决 JDK epoll 空轮询问题?
这个 BUG 是指 Java 的 NIO 在 Linux 下进行 selector.select() 时,本来如果轮询的结果为空并且不调用 wakeup 方法的话,这个 selector.select() 应该是一直阻塞的,但是 Java 却会打破阻塞,继续执行,导致程序无限空转,造成 CPU 使用率 100%。(这个问题只存在 Linux 是因为 Linux 的 NIO 是基于 epoll 实现的,而 Java 实现的 epoll 存在 BUG,windows 下 NIO 基于 poll 就不存在此问题)
Netty 的解决方案:
为 Selector 的 select 操作设置超时时间,同时定义可以跳出阻塞的四种情况
- 有事件发生
- wakeup
- 超时
- 空轮询 BUG
而前两种返回值不为 0,可以跳出循环,超时有时间戳记录,所以每次空轮询,有专门的计数器进行 +1,如果空轮询的次数超过了 512 次(默认),就认为其触发了空轮询 BUG。
当触发 BUG 后,Netty 直接重建一个 Selector,将原来的 Channel 重新注册到新的 Selector 上,并将旧的 Selector 关掉。
3、什么是拆包和粘包?
TCP 是一个面向流的传输协议,所谓流,就是没有界限的一串数据。TCP 底层并不了解上层业务数据的具体含义,它会根据 TCP 缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的 TCP 粘包和拆包问题。
粘包和拆包是 TCP 网络编程中不可避免的,无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑 TCP 底层的粘包/拆包机制。
数据从发送方到接收方需要经过操作系统的缓冲区,而造成粘包和拆包的主要原因就在这个缓冲区上。粘包可以理解为缓冲区数据堆积,导致多个请求数据粘在一起,而拆包可以理解为发送的数据大于缓冲区,进行拆分处理。
详细来说,造成粘包和拆包的原因主要有以下三个:
1)应用程序 write 写入的字节大小大于套接口发送缓冲区大小;
2)进行 MSS 大小的 TCP 分段;
3)以太网帧的 payload 大于 MTU 进行 IP 分片。
TCP的拆包和粘包是指在基于TCP协议的通信中,发送方发送的数据包在接收方接收时可能被拆分或合并的现象¹²。
- 这是因为TCP是面向字节流的协议,没有消息边界的概念,
- 而且TCP会根据缓冲区和网络状况进行数据包的划分和优化¹²³⁴⁵。
常见的解决方案有以下几种¹²⁴:
- - 发送端将每个包都封装成固定的长度,如果不足可以用空格或其他字符进行填充;
- - 发送端在每个包的末尾使用特殊的分隔符,例如\\r\\n,接收端根据分隔符进行拆分;
- - 发送端将消息分为头部和消息体,头部中保存整个消息的长度,接收端根据长度进行拆分;
- - 发送端使用自定义的协议进行粘包和拆包的处理,例如Netty提供了一些解码器来解决这个问题。
TCP发送的数据包大小和TCP缓存区的关系是这样的¹²³:
- - TCP发送的数据包大小不是由应用程序决定的,而是由TCP根据对方给出的窗口值和当前网络拥塞的程度来动态调整的²。因此,TCP发送的数据包大小可能会变化,而不是固定不变的。
- - TCP缓存区是指发送方和接收方各自为每个TCP连接分配的一段内存空间,用来暂存发送或接收的数据¹²。TCP缓存区的大小可以通过SO_SNDBUF和SO_RCVBUF选项来设置,也可以通过/proc/sys/net/ipv4/tcp_rmem和/proc/sys/net/ipv4/tcp_wmem文件来设置¹³。
- - TCP缓存区的大小会影响TCP的性能,如果缓存区太小,会导致发送或接收数据频繁阻塞,降低吞吐量;如果缓存区太大,会导致内存浪费,增加重传延迟¹²。因此,需要根据实际情况合理地设置缓存区的大小。
--TCP发送的数据包大小 是由 TCP 缓存区大小决定的吗
不是的。
TCP发送的数据包大小是由TCP根据对方给出的窗口值和当前网络拥塞的程度来动态调整的,而不是由TCP缓存区大小决定的。
TCP缓存区大小只是影响发送或接收数据时的内存使用,而不是影响数据包的划分。
--TCP 数据包的划分 是根据什么呢。
根据我的搜索结果,TCP数据包的划分是根据以下几个因素的¹²³⁴⁵:
- - TCP的最大分段大小(MSS),它是TCP数据段能够携带的数据上限,通常由TCP连接双方在三次握手时协商确定,一般为1460字节²³⁴。
- - IP的最大传输单元(MTU),它是IP数据包能够传输的数据上限,通常由物理设备或网络路径决定,一般为1500字节²³⁵。
- - TCP的滑动窗口机制,它是TCP进行流量控制和拥塞控制的方法,通过动态调整发送方的窗口大小来控制发送速率和发送数量¹²⁴。
综合以上因素,TCP会根据MSS和MTU来划分数据段,并根据窗口大小来发送数据段。如果MSS小于MTU,那么TCP会尽量发送MSS大小的数据段;如果MSS大于MTU,那么TCP会将数据段分割成多个小于MTU的数据包,并交给IP层进行传输。
4、拆包粘包的解决方案?
由于底层的 TCP 无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议可以归纳出以下解决方案:
1)消息长度固定,累计读取到长度和为定长LEN的报文后,就认为读取到了一个完整的信息。
2)将特殊的分隔符作为消息的结束标志,如回车换行符。
3)通过在消息头中定义长度字段来标识消息的总长度。
拆包粘包是指在使用 TCP 协议进行数据传输时,由于 TCP 是面向字节流的,没有消息边界的概念,所以可能会出现一个数据包被拆分成多个包发送,或者多个数据包被合并成一个包发送的情况¹²。
有一些方法可以解决拆包粘包的问题,例如:
- - 发送端将每个包都封装成固定的长度,比如 100 字节大小。如果不足 100 字节可通过补 0 或空等进行填充到指定长度¹²³。
- - 发送端在每个包的末尾使用固定的分隔符,例如 \\r\\n。如果发生拆包需等待多个包发送过来之后再找到其中的 \\r\\n 进行合并;例如,FTP 协议¹²³。
- - 将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息¹²³。
- - 通过自定义协议进行粘包和拆包的处理¹²³。
5、Netty 如何解决拆包粘包?
相比粘包,拆包问题比较简单,用户可以自己定义自己的编码器进行处理,Netty 并没有提供相应的组件。对于粘包的问题,代码比较繁琐,Netty 提供了 4 种解码器来解决,分别如下:
- 固定长度的拆包器(FixedLengthFrameDecoder),每个应用层数据包的都拆分成都是固定长度的大小;
- 行拆包器(LineBasedFrameDecoder),每个应用层数据包都以换行符作为分隔符,进行分割拆分;
- 分隔符拆包器(DelimiterBasedFrameDecoder),每个应用层数据包,都通过自定义的分隔符,进行分割拆分;
- 基于数据包长度的拆包器(LengthFieldBasedFrameDecoder),将应用层数据包的长度,作为接收端应用层数据包的拆分依据。按照应用层数据包的大小,拆包。这个拆包器,有一个要求,就是应用层协议中包含数据包的长度。
6、Netty 零拷贝体现在哪里?
Zero-copy 就是在操作数据时, 不需要将数据 buffer从 一个内存区域拷贝到另一个内存区域。 少了一次内存的拷贝,CPU 效率就得到的提升。
- 接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝;
- 提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象,用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作;
- 文件传输采用了 transferTo 方法,它可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。
和操作系统上的零拷贝的区别?
- Netty 的 Zero-copy 完全是在用户态(Java 应用层)的, 更多的偏向于优化数据操作。
- 而在 OS 层面上的 Zero-copy 通常指避免在用户态(User-space)与内核态(Kernel-space)之间来回拷贝数据。
Netty 零拷贝和操作系统的零拷贝是有区别的¹⁴。
操作系统的零拷贝是指在网络上发送文件时,不需要将文件内容拷贝到用户空间而直接在内核空间中传输到网络的方式,避免了用户态和内核态之间的数据拷贝¹。
Netty 零拷贝是基于Java层面或者说用户空间的,它更多的是偏向于应用中的数据操作优化,而不是系统层面的操作优化¹
--拓展 Netty 零拷贝体现在以下几个方面²⁴⁶:
- - Netty的接收和发送 **ByteBuffer** 采用 **DIRECT BUFFERS**,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。
- - Netty提供了零拷贝的 **buffer**,在传输数据时,最终处理的数据会需要对单个传输的报文,进行组合和拆分,Nio原生的ByteBuffer无法做到,netty通过提供的 **Composite (组合)** 和 **Slice (拆分)** 两种buffer来实现零拷贝。
- - Netty利用 **FileRegion** 接口实现了文件传输的零拷贝,通过调用操作系统底层的 **sendfile** 方法,将文件直接从磁盘读取到目标 Channel 中,避免了用户空间和内核空间的数据拷贝。
7、TCP 的长连接和短连接?
我们知道 TCP 在进行读写之前,server 与 client 之间必须提前建立一个连接。建立连接的过程,需要我们常说的三次握手,释放/关闭连接的话需要四次挥手。这个过程是比较消耗网络资源并且有时间延迟的。
所谓,短连接说的就是 server 端 与 client 端建立连接之后,读写完成之后就关闭掉连接,如果下一次再要互相发送消息,就要重新连接。
- 短连接的有点很明显,就是管理和实现都比较简单,
- 缺点也很明显,每一次的读写都要建立连接必然会带来大量网络资源的消耗,并且连接的建立也需要耗费时间。
长连接说的就是 client 向 server 双方建立连接之后,即使 client 与 server 完成一次读写,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。
- 长连接的可以省去较多的 TCP 建立和关闭的操作,降低对网络资源的依赖,节约时间。
- 对于频繁请求资源的客户来说,非常适用长连接。
8、Netty 长连接、心跳机制了解么?
- 为什么要引入心跳机制
在 TCP 保持长连接的过程中,可能会出现断网等网络异常出现,异常发生的时候, client 与 server 之间如果没有交互的话,它们是无法发现对方已经掉线的。为了解决这个问题, 我们就需要引入心跳机制。
- 心跳机制原理
心跳机制的工作原理是: 在 client 与 server 之间在一定时间内没有数据交互(即处于 idle 状态)时, 客户端或服务器就会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互。所以, 当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性。
- Netty的长连接,心跳机制
TCP 实际上自带的就有长连接选项,本身是也有心跳包机制,也就是 TCP 的选项:SO_KEEPALIVE。但 TCP 协议层面的长连接灵活性不够,所以,一般情况下我们都是在应用层协议上实现自定义心跳机制的,也就是在 Netty 层面通过编码实现。通过 Netty 实现心跳机制的话,核心类是 IdleStateHandler 。
--拓展 :TCP 的长连接
TCP 的长连接是指在一个 TCP 连接上可以连续发送多个数据包,在 TCP 连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接¹²。TCP 的长连接的优点是可以减少连接建立和断开的开销,提高数据传输的效率,也可以方便实现数据的推送²³。
TCP 的长连接需要注意以下几个方面:
- - TCP 的长连接需要在应用层或者操作系统层实现保活机制,即定时发送心跳包或者探测包来检测连接的可用性²³ 。
- - TCP 的长连接需要考虑超时问题,即如果一段时间内没有收到对方的响应,就认为连接已经断开,并重新建立连接³ 。
- - TCP 的长连接需要考虑资源消耗问题,即如果有大量的空闲连接占用服务器的资源,就需要采取一些策略来关闭一些不活跃的连接,或者限制每个客户端的最大连接数²³。
-- 拓展:SO_KEEPALIVE
SO_KEEPALIVE 是一个套接字选项,用于启用或禁用 TCP 的保持连接功能¹²。保持连接功能是指在 TCP 连接上定期发送探测包,以检测对方主机是否崩溃或者连接是否正常²³⁴。
SO_KEEPALIVE 的设置方法和参数取决于操作系统的类型和版本¹²。一般来说,需要指定以下几个参数:
- on/off:表示是否开启 SO_KEEPALIVE 选项,一般为 0 或 1。
- keepalivetime:表示发送探测包的时间间隔,单位为毫秒。
- keepaliveinterval:表示两次重试探测包的时间间隔,单位为毫秒。
- keepalivecnt:表示重试探测包的次数,超过该次数后认为连接不可用。
SO_KEEPALIVE 的默认值和范围也取决于操作系统的类型和版本。一般来说,keepalivetime 的默认值为 7200 秒,keepaliveinterval 的默认值为 75 秒,keepalivecnt 的默认值为 9 或 10。
- SO_KEEPALIVE 的优点是可以利用操作系统提供的机制来实现保持连接和检测死链的功能,而不需要在应用层自己设计心跳包机制³⁴。
- 但是,SO_KEEPALIVE 的缺点是不能很好地与应用层交互,可能会产生大量无意义的带宽浪费,且不能灵活地调整探测包的格式和内容³⁴。
9、说说 Netty 的对象池技术?
- 对象池是什么
对象池其实就是缓存一些对象从而避免大量创建同一个类型的对象,类似线程池的概念。对象池缓存了一些已经创建好的对象,避免需要时才创建对象,同时限制了实例的个数。池化技术最终要的就是重复的使用池内已经创建的对象。从上面的内容就可以看出
- 对象池适用于以下几个场景:
- 创建对象的开销大;
- 会创建大量的实例;
- 限制一些资源的使用。
- Netty的对象池技术
Netty 自己实现了一套轻量级的对象池。
在 Netty 中,通常会有多个 IO 线程独立工作(基于 NioEventLoop 实现)。
每个 IO 线程轮询单独的 Selector 实例来检索 IO 事件,并在 IO 来临时开始处理。
最常见的 IO 操作就是读写,具体到 NIO 就是从内核缓冲区拷贝数据到用户缓冲区或者从用户缓冲区拷贝数据到内核缓冲区。这里会涉及到大量的创建和回收 Buffer, Netty 对 Buffer 进行了池化从而降低系统开销。
ref:https://www.jianshu.com/p/3bfe0de2b022
10 有哪些序列化协议?
- 序列化定义
序列化(编码)是将对象序列化为二进制形式(字节数组),
- 序列化的作用
主要用于网络传输、数据持久化等;
而反序列化(解码)则是将从网络、磁盘等读取的字节数组还原成原始对象,主要用于网络传输对象的解码,以便完成远程调用。
- 影响序列化性能的关键因素:
- 序列化后的码流大小(网络带宽的占用)、
- 序列化的性能(CPU资源占用);
- 是否支持跨语言(异构系统的对接和开发语言切换)。
- 目前几种主流协议
- Java 默认提供的序列化
- 无法跨语言、序列化后的码流太大、序列化的性能差。
- Java 默认提供的序列化
-
- XML
- 优点是人机可读性好,可指定元素或特性的名称。
- 缺点:序列化数据只包含数据本身以及类的结构,不包括类型标识和程序集信息;只能序列化公共属性和字段;不能序列化方法;文件庞大,文件格式复杂,传输占带宽。
- 适用场景:当做配置文件存储数据,实时数据转换。
- XML
-
- JSON
- 是一种轻量级的数据交换格式,
- 优点:兼容性高、数据格式比较简单,易于读写、序列化后数据较小,可扩展性好,兼容性好、与XML相比,其协议比较简单,解析速度比较快。
- 缺点:数据的描述性比XML差、不适合性能要求为ms级别的情况、额外空间开销比较大。
- 适用场景(可替代XML):跨防火墙访问、可调式性要求高、基于Web browser的Ajax请求、传输数据量相对小,实时性要求相对低(例如秒级别)的服务。
- JSON
-
- Fastjson
- 采用一种“假定有序快速匹配”的算法。
- 优点:接口简单易用、目前java语言中最快的json库。
- 缺点:过于注重快,而偏离了“标准”及功能性、代码质量不高,文档不全。
- 适用场景:协议交互、Web输出、Android客户端。
- Fastjson
-
- Thrift
- 不仅是序列化协议,还是一个RPC框架。
- 优点:序列化后的体积小, 速度快、支持多种语言和丰富的数据类型、对于数据字段的增删具有较强的兼容性、支持二进制压缩编码。
- 缺点:使用者较少、跨防火墙访问时,不安全、不具有可读性,调试代码时相对困难、不能与其他传输层协议共同使用(例如HTTP)、无法支持向持久层直接读写数据,即不适合做数据持久化序列化协议。
- 适用场景:分布式系统的RPC解决方案。
- Thrift
-
- Avro
- Hadoop的一个子项目,解决了JSON的冗长和没有IDL的问题。
- 优点:支持丰富的数据类型、简单的动态语言结合功能、具有自我描述属性、提高了数据解析速度、快速可压缩的二进制数据形式、可以实现远程过程调用RPC、支持跨编程语言实现。
- 缺点:对于习惯于静态类型语言的用户不直观。
- 适用场景:在Hadoop中做Hive、Pig和MapReduce的持久化数据格式。
- Avro
-
- Protobuf
- 将数据结构以.proto文件进行描述,通过代码生成工具可以生成对应数据结构的POJO对象和Protobuf相关的方法和属性。
- 优点:序列化后码流小,性能高、结构化数据存储格式(XML JSON等)、通过标识字段的顺序,可以实现协议的前向兼容、结构化的文档更容易管理和维护。
- 缺点:需要依赖于工具生成代码、支持的语言相对较少,官方只支持Java 、C++ 、python。
- 适用场景:对性能要求高的RPC调用、具有良好的跨防火墙的访问属性、适合应用层对象的持久化。
- Protobuf