Netty专题

1.Netty是什么

面试官:介绍一下自己对netty的认识吧!小伙砸~
我:好的!我就用简单的几点来概括下netty吧

  1. Netty 是一个基于NIO 的 client-server(客户端服务端框架),使用它可以快速简单第开发网络应用程序。
  2. 它极大地简化并优化了TCP和UDP套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好。
  3. 支持多种协议 如FTP,SMTP,HTTP以及各种二进制和基于文本的传统协议。

用官方的总结就是:Netty 成功地找到了一种在不妥协可维护和性能的情况下实现易于开发,性能,稳定性和灵活性的方法

除了上面介绍的之外,很多开源项目比如我们常用的 Dubbo、RocketMQ、Elasticsearch、gRPC 等等都用到了 Netty。

网络编程我愿意称 Netty 为王 。

2.为什么要用Netty

面试官:为什么要用Netty,说说来
我:因为Netty具有以下这些优点,并且相比较于直接使用JDK自带的NIO相关API来说更加易用。

  • 统一的API,支持多种传输类型,阻塞和非阻塞的。
  • 简单而强大的线程模型。
  • 自带编译器解决TCP粘包/拆包问题。
  • 自带各种协议栈。
  • 真正的无连接数据包套接字支持。
  • 比直接使用Java核心API有更高的吞吐量,更低的延迟,更低的资源消耗和更少的内存复制。
  • 安全性不错,有完整的SSL/TLS以及StartTLS支持。
  • 社区活跃。
  • 成熟稳定,经历了大型项目的使用和考验,并且很多的开源项目都使用到了Netty,比如我们经常接触的Dubbo,RocketMQ等等。

3.Netty 应用场景了解么?

面试官:能不能通俗地说一下使用 Netty 可以做什么事情?

我:凭借自己的了解,简单说一下吧!理论上来说,NIO 可以做的事情 ,使用 Netty 都可以做并且更好。Netty 主要用来做网络通信 :

  1. 作为 RPC 框架的网络通信工具 :我们在分布式系统中,不同服务节点之间经常需要相互调用,这个时候就需要 RPC
    框架了。不同服务节点之间的通信是如何做的呢?可以使用 Netty
    来做。比如我调用另外一个节点的方法的话,至少是要让对方知道我调用的是哪个类中的哪个方法以及相关参数吧!

  2. 实现一个自己的 HTTP 服务器 :通过 Netty 我们可以自己实现一个简单的 HTTP 服务器,这个大家应该不陌生。说到 HTTP 服务器的话,作为 Java 后端开发,我们一般使用 Tomcat 比较多。一个最基本的 HTTP 服务器可要以处理常见的 HTTP Method 的请求,比如 POST 请求、GET 请求等等。

  3. 实现一个即时通讯系统 :使用 Netty 我们可以实现一个可以聊天类似微信的
    即时通讯系统,这方面的开源项目还蛮多的,可以自行去 Github 找一找。

  4. 实现消息推送系统 :市面上有很多消息推送系统都是基于 Netty 来做的。

4.Netty 核心组件有哪些?分别有什么作用?

1.Channel

Channel 接口是 Netty 对网络操作抽象类,它除了包括基本的 I/O 操作,如 bind()、connect()、read()、write() 等。

比较常用的Channel接口实现类是NioServerSocketChannel(服务端)和NioSocketChannel(客户端),这两个 Channel 可以和 BIO 编程模型中的ServerSocket以及Socket两个概念对应上。Netty 的 Channel 接口所提供的 API,大大地降低了直接使用 Socket 类的复杂性。

2.Buffer:
与 Channel 进行交互,数据是从 Channel 读入缓冲区,从缓冲区写入 Channel 中的 flip 方法 : 反转此缓冲区,将 position 给 limit,然后将 position 置为 0,其实就是切换读写模式 clear 方法 :清除此缓冲区,将 position 置为 0,把 capacity 的值给 limit。 rewind 方法 : 重绕此缓冲区,将 position 置为0,DirectByteBuffer 可减少一次系统空间到用户空间的拷贝。但 Buffer 创建和销毁的成本更高,不可控,通常会用内存池来提高性能。直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。如果数据量比较小的中小应用情况下,可以考虑使用 heapBuffer,由 JVM 进行管理。

3.EventLoop

《Netty 实战》这本书是这样介绍它的:

“EventLoop 定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。”

说白了,EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。

那 Channel 和 EventLoop 直接有啥联系呢?

Channel 为 Netty 网络操作(读写等操作)抽象类,EventLoop 负责处理注册到其上的Channel 处理 I/O 操作,两者配合参与 I/O 操作。

4.ChannelFuture

Netty 是异步非阻塞的,所有的 I/O 操作都为异步的。

因此,我们不能立刻得到操作是否执行成功,但是,你可以通过 ChannelFuture 接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。

并且,你还可以通过ChannelFuture 的 channel() 方法获取关联的Channel
public interface ChannelFuture extends Future { Channel channel(); ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> var1); … ChannelFuture sync() throws InterruptedException;}

另外,我们还可以通过 ChannelFuture 接口的 sync()方法让异步的操作变成同步的。

5.ChannelHandler 和 ChannelPipeline

下面这段代码使用过 Netty 的小伙伴应该不会陌生,我们指定了序列化编解码器以及自定义的 ChannelHandler 处理消息。

b.group(eventLoopGroup) .handler(new ChannelInitializer() { @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new NettyKryoDecoder(kryoSerializer, RpcResponse.class)); ch.pipeline().addLast(new NettyKryoEncoder(kryoSerializer, RpcRequest.class)); ch.pipeline().addLast(new KryoClientHandler()); } });

ChannelHandler 是消息的具体处理器。他负责处理读写操作、客户端连接等事情。

ChannelPipeline 为 ChannelHandler 的链,提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API 。当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline。

我们可以在 ChannelPipeline 上通过 addLast() 方法添加一个或者多个ChannelHandler ,因为一个数据或者事件可能会被多个 Handler 处理。当一个 ChannelHandler 处理完之后就将数据交给下一个 ChannelHandler 。

5.EventloopGroup 了解么?和 EventLoop 啥关系?

面试官:刚刚你也介绍了 EventLoop。那你再说说 EventloopGroup 吧!和 EventLoop 啥关系?
我:

EventLoopGroup 包含多个 EventLoop(每一个 EventLoop 通常内部包含一个线程),上面我们已经说了 EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。

并且 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理,即 Thread 和 EventLoop 属于 1 : 1 的关系,从而保证线程安全。

上图是一个服务端对 EventLoopGroup 使用的大致模块图,其中 Boss EventloopGroup 用于接收连接,Worker EventloopGroup 用于具体的处理(消息的读写以及其他逻辑处理)。

从上图可以看出:当客户端通过 connect 方法连接服务端时,bossGroup 处理客户端连接请求。当客户端处理完成后,会将这个连接提交给 workerGroup 来处理,然后 workerGroup 负责处理其 IO 相关操作。

6.Bootstrap 和 ServerBootstrap 了解么?

面试官:你再说说自己对 Bootstrap 和 ServerBootstrap 的了解吧!

我:

Bootstrap 是客户端的启动引导类/辅助类,具体使用方法如下:

EventLoopGroup group = new NioEventLoopGroup(); try { //创建客户端启动引导/辅助类:Bootstrap Bootstrap b = new Bootstrap(); //指定线程模型 b.group(group). … // 尝试建立连接 ChannelFuture f = b.connect(host, port).sync(); f.channel().closeFuture().sync(); } finally { // 优雅关闭相关线程组资源 group.shutdownGracefully(); }

ServerBootstrap 客户端的启动引导类/辅助类,具体使用方法如下:

// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理 EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //2.创建服务端启动引导/辅助类:ServerBootstrap ServerBootstrap b = new ServerBootstrap(); //3.给引导类配置两大线程组,确定了线程模型 b.group(bossGroup, workerGroup). … // 6.绑定端口 ChannelFuture f = b.bind(port).sync(); // 等待连接关闭 f.channel().closeFuture().sync(); } finally { //7.优雅关闭相关线程组资源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }

从上面的示例中,我们可以看出:

Bootstrap 通常使用 connet() 方法连接到远程的主机和端口,作为一个 Netty TCP 协议通信中的客户端。另外,Bootstrap 也可以通过 bind() 方法绑定本地的一个端口,作为 UDP 协议通信中的一端。
ServerBootstrap通常使用 bind() 方法绑定本地的端口上,然后等待客户端的连接。
Bootstrap 只需要配置一个线程组— EventLoopGroup ,而 ServerBootstrap需要配置两个线程组— EventLoopGroup ,一个用于接收连接,一个用于具体的处理。

7.NioEventLoopGroup 默认的构造函数会起多少线程?

面试官:看过 Netty 的源码了么?NioEventLoopGroup 默认的构造函数会起多少线程呢?

我:嗯嗯!看过部分。

回顾我们在上面写的服务器端的代码:

// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup();

为了搞清楚NioEventLoopGroup 默认的构造函数 到底创建了多少个线程,我们来看一下它的源码。

/** * 无参构造函数。 * nThreads:0 / public NioEventLoopGroup() { //调用下一个构造方法 this(0); } /* * Executor:null / public NioEventLoopGroup(int nThreads) { //继续调用下一个构造方法 this(nThreads, (Executor) null); } //中间省略部分构造函数 /* * RejectedExecutionHandler():RejectedExecutionHandlers.reject() */ public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,final SelectStrategyFactory selectStrategyFactory) { //开始调用父类的构造函数 super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject()); }

一直向下走下去的话,你会发现在 MultithreadEventLoopGroup 类中有相关的指定线程数的代码,如下:

// 从1,系统属性,CPU核心数2 这三个值中取出一个最大的 //可以得出 DEFAULT_EVENT_LOOP_THREADS 的值为CPU核心数2 private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(“io.netty.eventLoopThreads”, NettyRuntime.availableProcessors() * 2)); // 被调用的父类构造函数,NioEventLoopGroup 默认的构造函数会起多少线程的秘密所在 // 当指定的线程数nThreads为0时,使用默认的线程数DEFAULT_EVENT_LOOP_THREADS protected MultithreadEventLoopGroup(int nThreads, ThreadFactory threadFactory, Object… args) { super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, threadFactory, args); }

综上,我们发现 NioEventLoopGroup 默认的构造函数实际会起的线程数为 CPU核心数*2。

另外,如果你继续深入下去看构造函数的话,你会发现每个NioEventLoopGroup对象内部都会分配一组NioEventLoop,其大小是 nThreads, 这样就构成了一个线程池, 一个NIOEventLoop 和一个线程相对应,这和我们上面说的 EventloopGroup 和 EventLoop关系这部分内容相对应。

8.Netty 线程模型了解么?

面试官:说一下 Netty 线程模型吧!

我:大部分网络框架都是基于 Reactor 模式设计开发的。

“Reactor 模式基于事件驱动,采用多路复用将事件分发给相应的 Handler 处理,非常适合处理海量 IO 的场景。

在 Netty 主要靠 NioEventLoopGroup 线程池来实现具体的线程模型的 。

我们实现服务端的时候,一般会初始化两个线程组:

bossGroup :接收连接。
workerGroup :负责具体的处理,交由对应的 Handler 处理。
下面我们来详细看一下 Netty 中的线程模型吧!

1.单线程模型:

一个线程需要执行处理所有的 accept、read、decode、process、encode、send 事件。对于高负载、高并发,并且对性能要求比较高的场景不适用。

对应到 Netty 代码是下面这样的

“使用 NioEventLoopGroup 类的无参构造函数设置线程数量的默认值就是 CPU 核心数 *2。

//1.eventGroup既用于处理客户端连接,又负责具体的处理。 EventLoopGroup eventGroup = new NioEventLoopGroup(1); //2.创建服务端启动引导/辅助类:ServerBootstrap ServerBootstrap b = new ServerBootstrap(); boobtstrap.group(eventGroup, eventGroup) //…

2.多线程模型

一个 Acceptor 线程只负责监听客户端的连接,一个 NIO 线程池负责具体处理:accept、read、decode、process、encode、send 事件。满足绝大部分应用场景,并发连接量不大的时候没啥问题,但是遇到并发连接大的时候就可能会出现问题,成为性能瓶颈。

对应到 Netty 代码是下面这样的:

// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup();try { //2.创建服务端启动引导/辅助类:ServerBootstrap ServerBootstrap b = new ServerBootstrap(); //3.给引导类配置两大线程组,确定了线程模型 b.group(bossGroup, workerGroup) //…

3.主从多线程模型

从一个 主线程 NIO 线程池中选择一个线程作为 Acceptor 线程,绑定监听端口,接收客户端连接的连接,其他线程负责后续的接入认证等工作。连接建立完成后,Sub NIO 线程池负责具体处理 I/O 读写。如果多线程模型无法满足你的需求的时候,可以考虑使用主从多线程模型 。

// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理EventLoopGroup bossGroup = new NioEventLoopGroup();EventLoopGroup workerGroup = new NioEventLoopGroup();try { //2.创建服务端启动引导/辅助类:ServerBootstrap ServerBootstrap b = new ServerBootstrap(); //3.给引导类配置两大线程组,确定了线程模型 b.group(bossGroup, workerGroup) //…

9.Netty 服务端和客户端的启动过程了解么?

服务端

// 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() { @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.首先你创建了两个 NioEventLoopGroup 对象实例:bossGroup 和 workerGroup。

bossGroup : 用于处理客户端的 TCP 连接请求。
workerGroup :负责每一条连接的具体读写数据的处理逻辑,真正负责 I/O 读写操作,交由对应的 Handler 处理。
举个例子:我们把公司的老板当做 bossGroup,员工当做 workerGroup,bossGroup 在外面接完活之后,扔给 workerGroup 去处理。一般情况下我们会指定 bossGroup 的 线程数为 1(并发连接量不大的时候) ,workGroup 的线程数量为 CPU 核心数 *2 。另外,根据源码来看,使用 NioEventLoopGroup 类的无参构造函数设置线程数量的默认值就是 CPU 核心数 *2 。

2.接下来 我们创建了一个服务端启动引导/辅助类:ServerBootstrap,这个类将引导我们进行服务端的启动工作。

3.通过 .group() 方法给引导类 ServerBootstrap 配置两大线程组,确定了线程模型。

通过下面的代码,我们实际配置的是多线程模型,这个在上面提到过。

EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup();

4.通过channel()方法给引导类 ServerBootstrap指定了 IO 模型为NIO

NioServerSocketChannel :指定服务端的 IO 模型为 NIO,与 BIO 编程模型中的ServerSocket对应
NioSocketChannel : 指定客户端的 IO 模型为 NIO, 与 BIO 编程模型中的Socket对应5.通过 .childHandler()给引导类创建一个ChannelInitializer ,然后制定了服务端消息的业务处理逻辑 HelloServerHandler 对象6.调用 ServerBootstrap 类的 bind()方法绑定端口
客户端

//1.创建一个 NioEventLoopGroup 对象实例 EventLoopGroup group = new NioEventLoopGroup(); try { //2.创建客户端启动引导/辅助类:Bootstrap Bootstrap b = new Bootstrap(); //3.指定线程组 b.group(group) //4.指定 IO 模型 .channel(NioSocketChannel.class) .handler(new ChannelInitializer() { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); // 5.这里可以自定义消息的业务处理逻辑 p.addLast(new HelloClientHandler(message)); } }); // 6.尝试建立连接 ChannelFuture f = b.connect(host, port).sync(); // 7.等待连接关闭(阻塞,直到Channel关闭) f.channel().closeFuture().sync(); } finally { group.shutdownGracefully(); }

继续分析一下客户端的创建流程:

1.创建一个 NioEventLoopGroup 对象实例

2.创建客户端启动的引导类是 Bootstrap

3.通过 .group() 方法给引导类 Bootstrap 配置一个线程组

4.通过channel()方法给引导类 Bootstrap指定了 IO 模型为NIO

5.通过 .childHandler()给引导类创建一个ChannelInitializer ,然后制定了客户端消息的业务处理逻辑 HelloClientHandler 对象

6.调用 Bootstrap 类的 connect()方法进行连接,这个方法需要指定两个参数:

inetHost : ip 地址
inetPort : 端口号
public ChannelFuture connect(String inetHost, int inetPort) { return this.connect(InetSocketAddress.createUnresolved(inetHost, inetPort)); } public ChannelFuture connect(SocketAddress remoteAddress) { ObjectUtil.checkNotNull(remoteAddress, “remoteAddress”); this.validate(); return this.doResolveAndConnect(remoteAddress, this.config.localAddress()); }

connect 方法返回的是一个 Future 类型的对象

public interface ChannelFuture extends Future { …}

也就是说这个方是异步的,我们通过 addListener 方法可以监听到连接是否成功,进而打印出连接信息。具体做法很简单,只需要对代码进行以下改动:

ChannelFuture f = b.connect(host, port).addListener(future -> { if (future.isSuccess()) { System.out.println(“连接成功!”); } else { System.err.println(“连接失败!”); }}).sync();

10.什么是 TCP 粘包/拆包?有什么解决办法呢?

TCP 是以流的方式来处理数据,一个完整的包可能会被 TCP 拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送。 TCP 粘包/分包的原因: 应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象,而应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包现象; 进行 MSS 大小的 TCP 分段,当 TCP 报文长度-TCP 头部长度>MSS 的时候将发生拆包以太网帧的 payload(净荷)大于 MTU( 1500 字节)进行 ip 分片。

解决办法:
1.使用 Netty 自带的解码器

LineBasedFrameDecoder : 发送端发送数据包的时候,每个数据包之间以换行符作为分隔,LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节,判断是否有换行符,然后进行相应的截取。
DelimiterBasedFrameDecoder : 可以自定义分隔符解码器,LineBasedFrameDecoder 实际上是一种特殊的 DelimiterBasedFrameDecoder 解码器。
FixedLengthFrameDecoder: 固定长度解码器,它能够按照指定的长度对消息进行相应的拆包。
LengthFieldBasedFrameDecoder:
2.自定义序列化编解码器

在 Java 中自带的有实现 Serializable 接口来实现序列化,但由于它性能、安全性等原因一般情况下是不会被使用到的。

通常情况下,我们使用 Protostuff、Hessian2、json 序列方式比较多,另外还有一些序列化性能非常好的序列化方式也是很好的选择:

专门针对 Java 语言的:Kryo,FST 等等
跨语言的:Protostuff(基于 protobuf 发展而来),ProtoBuf,Thrift,Avro,MsgPack 等等

11.Netty 长连接、心跳机制了解么?

面试官:TCP 长连接和短连接了解么?

我:我们知道 TCP 在进行读写之前,server 与 client 之间必须提前建立一个连接。建立连接的过程,需要我们常说的三次握手,释放/关闭连接的话需要四次挥手。这个过程是比较消耗网络资源并且有时间延迟的。

所谓,短连接说的就是 server 端 与 client 端建立连接之后,读写完成之后就关闭掉连接,如果下一次再要互相发送消息,就要重新连接。短连接的有点很明显,就是管理和实现都比较简单,缺点也很明显,每一次的读写都要建立连接必然会带来大量网络资源的消耗,并且连接的建立也需要耗费时间。

长连接说的就是 client 向 server 双方建立连接之后,即使 client 与 server 完成一次读写,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。长连接的可以省去较多的 TCP 建立和关闭的操作,降低对网络资源的依赖,节约时间。对于频繁请求资源的客户来说,非常适用长连接。

面试官:为什么需要心跳机制?Netty 中心跳机制了解么?

我:

在 TCP 保持长连接的过程中,可能会出现断网等网络异常出现,异常发生的时候, client 与 server 之间如果没有交互的话,它们是无法发现对方已经掉线的。为了解决这个问题, 我们就需要引入 心跳机制。

心跳机制的工作原理是: 在 client 与 server 之间在一定时间内没有数据交互时, 即处于 idle 状态时, 客户端或服务器就会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互。所以, 当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性.

TCP 实际上自带的就有长连接选项,本身是也有心跳包机制,也就是 TCP 的选项:SO_KEEPALIVE。但是,TCP 协议层面的长连接灵活性不够。所以,一般情况下我们都是在应用层协议上实现自定义心跳机制的,也就是在 Netty 层面通过编码实现。通过 Netty 实现心跳机制的话,核心类是 IdleStateHandler 。

Netty 的零拷贝了解么?

面试官:讲讲 Netty 的零拷贝?

我:

维基百科是这样介绍零拷贝的:

“零复制(英语:Zero-copy;也译零拷贝)技术是指计算机执行操作时,CPU 不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省 CPU 周期和内存带宽。

在 OS 层面上的 Zero-copy 通常指避免在 用户态(User-space) 与 内核态(Kernel-space) 之间来回拷贝数据。而在 Netty 层面 ,零拷贝主要体现在对于数据操作的优化。

Netty 中的零拷贝体现在以下几个方面:

使用 Netty 提供的 CompositeByteBuf 类, 可以将多个ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝。
ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝。
通过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题.

12.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 中实现读:注册读就绪事件和相应的事件处理器、事件分发器等待事件、事件到来,激活分发器,分发器调用事件对应的处理器、事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。

13.了解哪几种序列化协议?

序列化(编码)是将对象序列化为二进制形式(字节数组),主要用于网络传输、数据持久化等;而反序列化(解码)则是将从网络、磁盘等读取的字节数组还原成原始对象,主要用于网络传输对象的解码,以便完成远程调用。 影响序列化性能的关键因素:序列化后的码流大小(网络带宽的占用)、序列化的性能( CPU 资源占用);是否支持跨语言(异构系统的对接和开发语言切换)。 Java 默认提供的序列化:无法跨语言、序列化后的码流太大、序列化的性能差XML, 优点:人机可读性好,可指定元素或特性的名称。 缺点:序列化数据只包含数据本身以及类的结构,不包括类型标识和程序集信息;只能序列化公共属性和字段;不能序列化方法;文件庞大,文件格式复杂,传输占带宽。 适用场景:当做配置文件存储数据,实时数据转换。
JSON,是一种轻量级的数据交换格式 优点:兼容性高、数据格式比较简单,易于读写、序列化后数据较小,可扩展性好,兼容性好、与 XML 相比,其协议比较简单,解析速度比较快。 缺点:数据的描述性比 XML 差、不适合性能要求为 ms 级别的情况、额外空间开销比较大。 适用场景(可替代XML):跨防火墙访问、可调式性要求高、基于 Webbrowser 的 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)

14.如何选择序列化协议?

具体场景 对于公司间的系统调用,如果性能要求在 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.Builderbuilder=UserProto.User.newBuilder();builder.build();
Netty 中的使用: ProtobufVarint32FrameDecoder 是用于处理半包消息的解码类; ProtobufDecoder(UserProto.User.getDefaultInstance())这是创建的 UserProto.java 文件中的解码类; ProtobufVarint32LengthFieldPrepender 对 protobuf 协议的消息头上加上一个长度为32 的整形字段,用于标志这个消息的长度的类; ProtobufEncoder 是编码类将 StringBuilder 转换为 ByteBuf 类型: copiedBuffer()方法

15.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 算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止大量小封包的发送阻塞网络,从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法;

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值