一、Netty是什么?
Netty是一个基于Java NIO的网络编程框架,提供了一套高效的、事件驱动的异步网络通信机制。简化了网络应用程序的开发过程,提供了可靠的、高性能的网络传输。
Netty的特点是什么?
- 异步和事件驱动:Netty使用异步的、非阻塞的I/O模型,通过事件驱动的方式处理网络操作。Netty能够高效地处理并发连接和大量的并发请求。
- 高性能:Netty采用了一系列优化策略,如零拷贝技术、内存池和可定制的线程模型等,以提供出色的性能和吞吐量。能处理高负载和大规模并发
- 多协议支持:Netty提供了丰富的协议支持,包括常用的网络协议(如HTTP、WebSocket、TCP和UDP)以及自定义协议。具备灵活的编解码器和处理器,简化了协议的实现和交互。
- 可扩展和灵活:Netty的架构和组件设计具有高度的可扩展性和灵活性。它提供了一组可重用的组件,可以根据应用需求进行定制和扩展。
- 安全性:Netty提供了强大的安全性支持,包括SSL/TLS的集成、加密和认证等机制,可以保护网络通信的安全性。
- 易于使用和学习:提供了清晰简洁的API和文档,易于使用和学习。它还具有丰富的示例和教程,帮助开发人员快速上手和构建可靠的网络应用。
既然Netty底层是基于NIO异步网络通信,我们来看下NIO核心的三大组件Selector、Channel、Buffer,如下是流程图。由于篇幅问题,NIO不过多的介绍。想深入了解的小伙伴可以去研究研究!
二、Netty核心组件
- 1、Bootstrap(启动类)
用于启动和配置网络应用程序配置类。通过Bootstrap可以绑定启动IP、端口,配置EventLoopGroup、选择传输协议(Channel)、配置ChannelHandler、参数调优设置等。Bootstrap通常与ServerBootstrap配合使用。ServerBootstrap用于服务端,Bootstrap用于客户端。
- 2、EventLoop(事件循环)
Netty为了避免线程与线程之间产生并发冲突,采用的策略。负责处理所有的I/O事件,如接收连接、读取数据、写入数据等。EventLoop在一个线程中循环执行,通过事件驱动的方式处理事件。一个EventLoop可以关联一个或多个Channel,但一个Channel只能关联一个EventLoop。EventLoop通常在服务端需要实例化2个对象,一个用于接受处理客户端的连接请求,另一个用户处理I/O读写事件。
- 3、Channel(通道)
一个与实际数据传输相关的连接。可以理解为数据在客户端和服务器之间的通道。Channel提供了异步的I/O操作,如读取、写入和关闭等。
- 4、ChannelHandler(通道处理器)
最核心的组件之一,负责处理输入输出数据的逻辑。可以接收入站事件(如数据接收)和出站事件(如数据发送),并执行相应的处理逻辑。ChannelHandler被添加到ChannelPipeline中,形成处理链。
- 5、ChannelHandlerContext(通道处理器上下文)
ChannelHandler的上下文环境。包含与ChannelHandler相关联的各种信息,如Channel、EventLoop、ChannelPipeline等。ChannelHandlerContext提供了丰富的方法,以便于ChannelHandler与其他组件进行交互。例如:区分谁是入站、出站就需要ChannelHandlerContext来维护管理
- 6、ChannelPipeline(通道管道)
ChannelPipeline是一个双向链表,拦截和处理事件的链式结构。ChannelPipeline中的ChannelHandler是一个传播链,ChannelPipeline管理ChannelHandler并协调它们的处理顺序。当数据通过Channel时,它会依次经过每个ChannelHandler进行处理。
ChannelPipeline还提供了方便的操作方法,如添加、移除和替换ChannelHandler。
Channel与ChannelPipeline组成关系:一个Channel 包含了一个ChannelPipeline,而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。
- 7、ByteBuf(字节缓冲区)
数据传输的基本单元,用于处理二进制数据的数据结构。ByteBuf提供了灵活的API,可以高效地读取、写入和操作数据。
- 8、Codec(编解码器)
用于处理数据的编码和解码的组件。Netty提供了一系列的编解码器,可以帮助开发者简化数据的编解码过程。常见的编解码器有字符串编解码器、对象序列化编解码器等。
- 9、ChannelFuture
异步操作的结果,例如连接的建立和关闭等。ChannelFuture提供了丰富的方法,用于检查操作的状态、添加监听器以便在操作完成时接收通知,并对操作的结果进行处理。
- 10、ChannelOption(通道选项)
调优参数,可对Netty高并发的情况下进行调优。
三、编写Demo
1、编写Server服务端启动类
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class NettyServer {
public static void main(String[] args) {
try {
bind();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void bind() throws InterruptedException {
// 创建boss线程组,用于接收连接
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 创建worker线程组,用于处理连接上的I/O操作,含有子线程NioEventGroup个数为CPU核数大小的2倍
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 创建ServerBootstrap实例,服务器启动对象
ServerBootstrap bootstrap = new ServerBootstrap();
// 使用链式编程配置参数
// 将boss线程组和worker线程组暂存到ServerBootstrap
bootstrap.group(bossGroup, workerGroup);
// 设置服务端Channel类型为NioServerSocketChannel作为通道实现
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// 添加ServerHandler到ChannelPipeline,对workerGroup的SocketChannel(客户端)设置处理器
pipeline.addLast(new ServerHandler());
}
});
// 设置启动参数,初始化服务器连接队列大小。服务端处理客户端连接请求是顺序处理,一个时间内只能处理一个客户端请求
// 当有多个客户端同时来请求时,未处理的请求先放入队列中
bootstrap.option(ChannelOption.SO_BACKLOG, 1024);
// 绑定端口并启动服务器,bind方法是异步的,sync方法是等待异步操作执行完成,返回ChannelFuture异步对象
ChannelFuture channelFuture = bootstrap.bind(8888).sync();
// 等待服务器关闭
channelFuture.channel().closeFuture().sync();
} finally {
// 优雅地关闭boss线程组
bossGroup.shutdownGracefully();
// 优雅地关闭worker线程组
workerGroup.shutdownGracefully();
}
}
}
2、编写服务端处理器handler
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
public class ServerHandler extends ChannelInboundHandlerAdapter {
/**
* 当 Channel 已经注册到它的 EventLoop 并且能够处理 I/O 时被调用
*
* @param ctx
* @throws Exception
*/
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
System.out.println("执行 channelRegistered");
}
/**
* 当 Channel 从它的 EventLoop 注销并且无法处理任何 I/O 时被调
* 用
*
* @param ctx
* @throws Exception
*/
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
System.out.println("执行 channelUnregistered");
}
/**
* 当 Channel 处于活动状态时被调用;Channel 已经连接/绑定并且已经就绪
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("执行 channelActive");
}
/**
* 当 Channel 离开活动状态并且不再连接它的远程节点时被调用
*
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("执行 channelInactive");
}
/**
* 当从 Channel 读取数据时被调用
*
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("执行 channelRead");
// 处理接收到的数据
ByteBuf byteBuf = (ByteBuf) msg;
try {
// 将接收到的字节数据转换为字符串
String message = byteBuf.toString(CharsetUtil.UTF_8);
// 打印接收到的消息
System.out.println("Server端收到客户消息: " + message);
// 发送响应消息给客户端
ctx.writeAndFlush(Unpooled.copiedBuffer("我是服务端,我收到你的消息啦~", CharsetUtil.UTF_8));
} finally {
// 释放ByteBuf资源
ReferenceCountUtil.release(byteBuf);
}
}
/**
* 当 Channel 上的一个读操作完成时被调用,对通道的读取完成的事件或通知。当读取完成可通知发送方或其他的相关方,告诉他们接受方读取完成
*
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("执行 channelReadComplete");
}
/**
* 当 ChannelnboundHandler.fireUserEventTriggered()方法被调用时被
* 调用
*
* @param ctx
* @param evt
* @throws Exception
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
System.out.println("执行 userEventTriggered");
}
/**
* 当 Channel 的可写状态发生改变时被调用。可以通过调用 Channel 的 isWritable()方法
* * 来检测 Channel 的可写性。与可写性相关的阈值可以通过
* * Channel.config().setWriteHighWaterMark()和 Channel.config().setWriteLowWaterMark()方法来
* * 设置
*
* @param ctx
* @throws Exception
*/
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
System.out.println("执行 channelWritabilityChanged");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("执行 exceptionCaught");
// 异常处理
cause.printStackTrace();
ctx.close();
}
}
3、编写客户端启动类
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class NettyClient {
public static void main(String[] args) {
start();
}
public static void start() {
// 创建EventLoopGroup,用于处理客户端的I/O操作
EventLoopGroup groupThread = new NioEventLoopGroup();
try {
// 创建Bootstrap实例,客户端启动对象
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(groupThread);
// 设置服务端Channel类型为NioSocketChannel作为通道实现
bootstrap.channel(NioSocketChannel.class);
// 设置客户端处理
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new ClientHandler());
}
});
// 绑定端口
ChannelFuture channelFuture = bootstrap.connect("localhost", 8888).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 优雅地关闭线程
groupThread.shutdownGracefully();
}
}
}
4、编写客户端处理器handler
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
public class ClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) {
// 连接建立时的处理,发送请求消息给服务器
ctx.writeAndFlush(Unpooled.copiedBuffer("你好,服务端!我是客户端,测试通道连接", CharsetUtil.UTF_8));
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 处理接收到的数据
ByteBuf byteBuf = (ByteBuf) msg;
try {
// 将接收到的字节数据转换为字符串
String message = byteBuf.toString(CharsetUtil.UTF_8);
// 打印接收到的消息
System.out.println("受到服务端响应的消息: " + message);
// TODO: 对数据进行业务处理
} finally {
// 释放ByteBuf资源
ReferenceCountUtil.release(byteBuf);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 异常处理
cause.printStackTrace();
ctx.close();
}
}
5、启动客户端、服务端,看控制台打印结果。可以看到,客户端启动时给服务端发送一条消息,服务端收到消息后给客户端回应消息。
在Netty 开发中我们主要编写handler处理器,ChannelHandler是处理入站和出站数据的应用程序逻辑的容器。ChannelHandler顶级接口中派生了两个重要的子接口ChannelInboundHandler和ChannelOutboundHandler以及适配器抽象类ChannelHandlerAdapter,使用规则如下:
- ChannelInboundHandler:用于处理入站事件,也称为输入处理器。可以处理从远程对等方(如客户端)到本地Netty服务器的事件,包括连接建立、数据接收、异常发生等。
- ChannelOutboundHandler:用于处理出站事件,也称为输出处理器。可以处理从本地Netty服务器到远程对等方(如服务器端)的事件,包括数据发送、连接建立、连接关闭等。
- ChannelInboundHandlerAdapter是一个抽象类,实现了
ChannelInboundHandler
接口,可以作为用户自定义的ChannelInboundHandler的基类。当使用时,需要覆盖其中的方法来实现自定义的处理逻辑,包括channelRead()
、channelReadComplete()
、exceptionCaught()
等方法。适用于需要实现自定义的、细粒度的处理逻辑的场景。它提供了更大的灵活性,可以对接收到的事件进行完全自定义的处理。 - SimpleChannelInboundHandler是
ChannelInboundHandlerAdapter
的子类,提供了更高层次的抽象和简化。里面封装了消息的类型匹配和释放资源的逻辑,用户只需要关注具体的消息类型和业务逻辑的处理。当使用时,需要指定具体的泛型类型,以指定处理的消息类型。
注意:
七、Netty的高并发高性能架构设计精髓
- 如果继承ChannelInboundHandlerAdapter适配处理器,在channelRead()方法中处理完业务逻辑后,如果不往下一个处理器传播,则需要自己释放资源ReferenceCountUtil.release(msg),如果需要往下一个处理器传播,则调用ctx.fireChannelRead(msg)方法往后传播;
- 在出站使用write()写方法写数据时Channel、ChannelPipeline、ChannelHandlerContext都支持写,唯一不同的是Channel、ChannelPipeline写是在整个传播链,而ChannelHandlerContext只在当前的传播链以及下一个处理事件的传播链。
-
四、Netty核心组件运转流程
1、客户端启动:
-
- 创建
Bootstrap
实例,并配置 EventLoopGroup、Channel 类型和处理器。 EventLoopGroup
是一个线程池,由多个EventLoop
组成,用于处理网络事件。EventLoop
是 Netty 的核心组件之一,它负责处理所有的 I/O 事件,包括接收连接、读取数据、写入数据等。Bootstrap
负责引导客户端启动,它会将连接请求交给EventLoopGroup
处理。- 调用
bootstrap.connect()
发起连接请求。 EventLoopGroup
选择一个EventLoop
来处理连接请求,并与服务端建立连接。- 当连接成功建立时,
ChannelFuture
的回调方法被触发。
- 创建
-
2、服务端启动:
-
- 创建
ServerBootstrap
实例,并配置 EventLoopGroup、Channel 类型和处理器。 EventLoopGroup
是一个线程池,由多个EventLoop
组成,用于处理网络事件。EventLoop
是 Netty 的核心组件之一,它负责处理所有的 I/O 事件,包括接收连接、读取数据、写入数据等。ServerBootstrap
负责引导服务端启动,它会将连接请求交给EventLoopGroup
处理。- 调用
serverBootstrap.bind()
绑定服务器端口。 EventLoopGroup
选择一个EventLoop
来处理绑定操作,并开始监听绑定的端口。- 当绑定成功时,
ChannelFuture
的回调方法被触发。
- 创建
-
3、连接建立:
-
- 当客户端与服务端成功建立连接时,由
EventLoop
处理连接事件。 EventLoop
会创建一个Channel
对象来表示连接,该对象维护了与连接相关的状态和属性。- 在客户端和服务端的
ChannelHandler
中的channelActive()
方法会被调用。 - 在客户端,该方法可以用于发送首次请求或认证信息。
- 在服务端,该方法可以用于记录连接日志、管理连接数等操作。
- 当客户端与服务端成功建立连接时,由
-
4、数据传输:
-
- 客户端通过
channel.writeAndFlush()
发送数据到服务端。 EventLoop
监听到写事件,并将数据发送给对应的Channel
。ChannelPipeline
是 Netty 的另一个核心组件,它由一系列的ChannelHandler
组成,用于处理入站和出站数据。- 在服务端的
ChannelHandler
中的channelRead0()
方法接收并处理客户端发送的数据。 - 服务端可以对接收到的数据进行处理,并根据业务逻辑选择将响应数据发送给特定客户端或广播给所有客户端。
- 在客户端的
ChannelHandler
中的channelRead()
方法接收到服务端发送的数据。 - 客户端可以对接收到的数据进行处理,例如打印到控制台或进行其他操作。
- 客户端通过
-
5、数据处理:
-
- 接收到的数据在
ChannelHandler
中被处理。 - 在服务端和客户端的
ChannelHandler
中,可以进行数据解码、编码、业务逻辑处理等操作。 - 可以使用内置的解码器和编码器,如
StringDecoder
和StringEncoder
来处理字符串数据的解码和编码。 - 自定义的
ChannelHandler
可以根据具体的业务需求进行数据处理。
- 接收到的数据在
-
6、数据发送:
-
- 服务端通过
ChannelGroup
将响应数据发送给特定客户端或广播给所有客户端。 ChannelGroup
是一个用于管理多个Channel
的容器,可以对其中的Channel
进行批量操作。- 客户端通过
channel.writeAndFlush()
发送数据到服务端。
- 服务端通过
-
7、连接关闭:
-
- 当连接关闭时,由
EventLoop
处理连接关闭事件。 - 在客户端和服务端的
ChannelHandler
中的channelInactive()
方法会被调用。 - 在客户端,该方法可以用于重新连接服务器或执行一些清理操作。
- 在服务端,该方法可以用于记录连接关闭日志、管理连接数等操作。
- 当连接关闭时,由
-
8、优雅关闭:
-
- 在客户端和服务端关闭时,需要执行优雅关闭的操作,释放资源。
- 调用
group.shutdownGracefully()
关闭EventLoopGroup
。 - 这将释放所有的线程和资源,并确保所有的连接都已关闭。
-
六、Netty应用场景
Netty应用于构建高性能、可扩展的网络应用程序。例如分布式开源框架doubbo、zookeeper、rocketmq底层rpc通讯使用都是Netty。
- 服务器和客户端之间的实时通信,如聊天服务器、游戏服务器等。
- 高性能的服务器,如Web服务器、代理服务器等。
- 分布式系统之间的通信,如分布式缓存、消息队列等。
- 主从Reactor线程模型:采用主从Reactor线程模型,其中包括一个主线程组负责接收客户端连接,多个从线程组负责处理客户端请求。这种设计利用多线程的优势实现了并发处理,提高了系统的吞吐量和响应性能。
- NIO多路复用非阻塞:Netty使用Java的NIO(Non-blocking I/O)机制,基于Selector实现了多路复用的非阻塞I/O操作。这种方式使得单个线程可以同时处理多个连接的I/O事件,避免了阻塞等待,提高了系统的并发能力和性能。
- 无锁串行化设计思想:Netty的设计中尽可能避免了锁的使用,采用了无锁的数据结构和并发设计思想,减少了锁竞争和线程阻塞,提高了并发性能和可伸缩性。当我们需要多个线程并发地访问共享数据时,传统的做法是使用锁来确保每个线程的访问是安全的。但是,锁的使用会引入竞争和阻塞,导致性能下降。而无锁串行化的思想就是为了解决这个问题。在Netty中,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。
- 支持高性能序列化协议:Netty提供了对多种高性能序列化协议的支持,如Google Protocol Buffers、MessagePack等。这些协议具有高效的编解码性能和数据压缩能力,提升了网络传输的效率和速度。
- 零拷贝:Netty通过使用直接内存(Direct Memory)实现了零拷贝的特性。它通过将数据直接从操作系统的内存缓冲区传输到网络中,避免了不必要的数据复制,提高了数据传输的效率。
- ByteBuf内存池设计:Netty使用了可配置的ByteBuf内存池,避免了频繁的内存分配和释放操作,减少了内存管理的开销,提高了内存的使用效率和性能。
- 灵活的TCP参数配置能力:Netty提供了灵活的TCP参数配置能力,开发人员可以根据实际需求调整TCP的各种参数,如TCP缓冲区大小、TCP_NODELAY选项等,以优化网络连接的性能和可靠性。
- 并发优化:Netty在设计上考虑了并发操作的优化。它采用了基于事件驱动的异步编程模型,通过回调和Future等机制实现非阻塞的并发处理。同时,它还提供了多种并发安全的组件和工具类,简化了并发编程的复杂性。
原文链接:https://zhuanlan.zhihu.com/p/630890093