Netty框架详解
一、Netty简介
Netty是一款基于Java NIO的网络编程、高性能、异步事件驱动的网络应用框架。它的设计目标是提供简单易用、高性能、可扩展的网络编程框架。它极大地简化了网络编程,提供了许多强大的功能。
二、Netty主要特点
- 高并发:Netty使用异步的、非阻塞的I/O模型,通过事件驱动的方式处理网络操作,能够高效地处理并发连接和大量的并发请求。
- 高性能:Netty采用了一系列优化策略,如零拷贝技术、内存池和可定制的线程模型等,以提供出色的性能和吞吐量。
- 多协议支持:Netty提供了丰富的协议支持,包括常用的网络协议(如HTTP、WebSocket、TCP和UDP)以及自定义协议。具备灵活的编解码器和处理器,简化了协议的实现和交互。
- 可扩展和灵活:Netty的架构和组件设计具有高度的可扩展性和灵活性。它提供了一组可重用的组件,可以根据应用需求进行定制和扩展。
- 安全性:Netty提供了一系列的安全性功能,包括SSL/TLS支持、加密和身份验证等。
- 简单易用:Netty提供了简洁的API和丰富的文档,使得开发人员可以快速上手,快速开发高质量的网络应用程序。
- 跨平台:Netty可以在多种操作系统平台上运行,包括Windows、Linux、MacOS等。
三、Netty使用场景
-
高性能网络服务器
Netty 非常适合构建高性能的网络服务器,如 Web 服务器、游戏服务器、即时通讯服务器等。
典型场景:- Web 服务器:Netty 可以作为 HTTP 服务器,处理大量的并发请求。
- 游戏服务器:Netty 支持 TCP/UDP 协议,适合构建实时性要求高的游戏服务器。
- 即时通讯服务器:Netty 的高并发和低延迟特性非常适合构建即时通讯(IM)系统。
优点:
- 高并发:基于 NIO 的非阻塞模型,支持大量并发连接。
- 低延迟:事件驱动模型减少了线程切换的开销。
- 可扩展:支持自定义协议和编解码器。 -
分布式系统通信
Netty 常用于分布式系统中的节点通信,如 RPC 框架、消息队列等。
典型场景:- RPC 框架:Netty 可以作为 RPC 框架的底层通信组件,如 Dubbo、gRPC。
- 消息队列:Netty 可以用于构建消息队列的客户端和服务器,如 Kafka、RocketMQ。
优点:
- 高性能:Netty 的零拷贝和内存池技术提高了通信效率。
- 灵活性:支持多种协议(如 HTTP、WebSocket、自定义协议)。 -
实时数据传输
Netty 适合构建实时数据传输系统,如实时日志收集、实时监控等。
典型场景:- 实时日志收集:Netty 可以用于构建高效的日志收集系统,将日志实时传输到中央服务器。
- 实时监控:Netty 可以用于构建监控系统,实时传输监控数据。
优点:
- 低延迟:适合实时性要求高的场景。
- 高吞吐量:支持批量数据传输,提高吞吐量。 -
物联网(IoT)
Netty 适合构建物联网设备与服务器之间的通信系统。
典型场景:- 设备接入:Netty 可以用于构建设备接入服务器,处理大量设备的并发连接。
- 协议适配:Netty 支持自定义协议,适合物联网中多样化的通信协议。
优点:
- 高并发:支持大量设备的并发连接。
- 灵活性:支持多种协议和编解码器。 -
文件传输
Netty 提供了零拷贝的文件传输功能,适合构建高效的文件传输系统。
典型场景:- 大文件传输:Netty 可以用于构建大文件传输系统,支持断点续传和分块传输。
- 分布式文件系统:Netty 可以用于构建分布式文件系统的通信组件。
优点:
- 高效传输:零拷贝技术减少了数据传输的开销。
- 可靠性:支持断点续传和分块传输。 -
HTTP/HTTPS 服务器和客户端
Netty 支持 HTTP/HTTPS 协议,适合构建高性能的 Web 服务器和客户端。
典型场景:- Web 服务器:Netty 可以用于构建高性能的 HTTP 服务器。
- API 网关:Netty 可以用于构建 API 网关,处理大量的 HTTP 请求。
优点:
- 高性能:基于 NIO 的非阻塞模型,支持高并发。
- 灵活性:支持 HTTP/2、WebSocket 等协议。 -
WebSocket 服务器
Netty 支持 WebSocket 协议,适合构建实时通信系统。
典型场景:- 实时聊天:Netty 可以用于构建实时聊天系统,支持双向通信。
- 实时数据推送:Netty 可以用于构建实时数据推送系统,如股票行情推送。
优点:
- 低延迟:适合实时性要求高的场景。
- 双向通信:支持客户端和服务器之间的双向通信。 -
SSL/TLS 加密通信
Netty 支持 SSL/TLS 加密通信,适合构建安全的网络应用。
典型场景:- 安全通信:Netty 可以用于构建安全的 HTTP/HTTPS 服务器和客户端。
- 加密数据传输:Netty 可以用于构建加密数据传输系统,保护数据的机密性。
优点:
- 安全性:支持 SSL/TLS 加密通信。
- 高性能:基于 NIO 的非阻塞模型,支持高并发。
四、Netty服务端工作架构流程
(1)服务端工作流程:
- 服务端初始化时会创建2个NioEventLoopGroup,BossGroup用于Accept连接事件处理并分发请求,WokerGroup用于处理读写事件和业务逻辑。
- 服务端启动时创建 ServerBootstrap 实例,并配置 EventLoopGroup、Channel 类型和处理器,通过调用 serverBootstrap.bind() 绑定服务器端口。
- BossGroup中的NioEventLoop不断轮询注册在其Selector上的ServerSocketChannel的 Accept 事件。
- 接收到Accept 事件后,再交由processSelectedKeys处理 accept 事件,与客户端建立连接,生成一个 NioSocketChannel,并将其注册到WokerGroup的某个 NioEventLoop 的 Selector 上。
- WokerGroup的 NioEventLoop 不断轮询注册在其Selector上的NioSocketChannel的 read/write 事件。
- 接收到read/write 事件后,再交由processSelectedKeys处理 read/write 事件,并调用ChannelPipeline中的相关的ChannelHandler 的channelRead()方法接收并处理客户端发送的数据。
- 处理完数据后,通过ChannelHandlerContext 的writeAndFlush()方法发送响应结果消息给客户端。
(2)客户端工作流程:
- 客户端初始化时会创建1个NioEventLoopGroup 用于处理客户端的I/O操作。
- 客户端启动时创建 Bootstrap 实例,并配置 EventLoopGroup、Channel 类型和处理器,通过调用 bootstrap.connect() 发起连接请求交给 NioEventLoopGroup 处理。
- NioEventLoopGroup 选择一个 NioEventLoop 来处理连接请求,并与服务端建立连接。
- 当客户端与服务端成功建立连接时,NioEventLoop 会创建一个 Channel 对象来表示连接,该对象维护了与连接相关的状态和属性。
- 再通过调用ChannelHandlerContext 的writeAndFlush()方法向服务端发送消息。
- 发送成功后,服务端接收到消息并返回处理结果时,ChannelHandler 的channelRead()方法能接收到服务端返回的响应结果消息。
五、Netty的核心组件
- Bootstrap:用于启动和配置网络应用程序配置类,ServerBootstrap用于服务端,Bootstrap用于客户端。
- NioEventLoopGroup:相当于一个事件循环组,这个组中含有多个事件循环,每个事件循环就是一个 NioEventLoop。在服务端会创建BoosEventLoopGroup 和WorkerEventLoopGroup两个NioEventLoopGroup实例。BossEventLoopGroup 只负责处理连接事件,WorkerEventLoopGroup则负责read/write 事件。客户端通常只有一个NioEventLoopGroup来发起连接处理I/O任务。
- NioEventLoop:表示一个不断循环的执行事件处理的线程,每个 NioEventLoop 都包含一个 Selector和一个TaskQueue,Selector用于监听注册在其上的 SocketChannel 上的I/O事件,如:read,write,accept,connect等;TaskQueue用于存放一些非I/O任务,如:register,bind等任务。
- Channel:是一种双向的连接通道,客户端与服务端之间的所有网络事件(数据传输、生命周期事件)都会通过 Channel 处理。不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应。常用的类型有:NioSocketChannel(客户端 TCP Socket 连接)、NioServerSocketChannel(服务器端 TCP Socket 连接)。
- ChannelHandler:是消息的具体处理器,负责处理输入输出数据的逻辑。可以接收入站事件(如数据接收)和出站事件(如数据发送),并执行相应的处理逻辑。
- ChannelHandlerContext:是 ChannelHandler 的上下文环境。包含与ChannelHandler相关联的各种信息,如Channel、EventLoop、ChannelPipeline等。ChannelHandlerContext还提供了丰富的方法,以便于ChannelHandler与其他组件进行交互。
- ChannelPipeline:是一个双向链表,拦截和处理事件的链式结构,主要负责管理ChannelHandler并协调它们的处理顺序。ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。
- ChannelFuture:主要用于接收异步I/O操作返回的执行结果。ChannelFuture提供了丰富的方法,用于检查操作的状态、添加监听器以便在操作完成时接收通知,并对操作的结果进行处理。
六、ChannelPipeline
ChannelPipeline 是一系列 ChannelHandler 的有序组合,每个 ChannelHandler 都可以处理特定类型的事件或数据。当一个事件(如连接、读取数据、写入数据等)发生时,ChannelPipeline 会按照添加的顺序依次调用相应的 ChannelHandler 来处理该事件。
-
ChannelHandler 类型:
- 入站处理器(Inbound Handler):处理入站 I/O 事件,如连接建立、数据读取等。典型的入站处理器包括 ChannelInboundHandlerAdapter、ChannelInboundByteHandler 等。
- 出站处理器(Outbound Handler):处理出站 I/O 操作,如发送数据、关闭连接等。典型的出站处理器包括 ChannelOutboundHandlerAdapter、ChannelOutboundByteHandler 等。
-
添加 ChannelHandler 到 ChannelPipeline:
ChannelPipeline pipeline = channel.pipeline(); pipeline.addLast(new MyInboundHandler1()); pipeline.addLast(new MyInboundHandler2()); pipeline.addLast(new MyOutboundHandler1()); pipeline.addLast(new MyOutboundHandler2());
-
ChannelHandler 的行为
每个 ChannelHandler 可以选择性地处理或修改事件,具体行为取决于其实现方式:- 处理事件:ChannelHandler 可以对事件进行处理,例如读取数据、修改数据、记录日志等。
- 传递事件:ChannelHandler 可以选择将事件传递给下一个 ChannelHandler,或者终止事件的传播。
- 修改事件:ChannelHandler 可以修改事件的内容或状态,例如修改数据、添加元数据等。
-
事件传播机制
- 入站事件传播
入站事件从 ChannelPipeline 的头部(Head)流向尾部(Tail),依次经过每个 InboundHandler。- 处理事件:InboundHandler 可以处理事件,例如读取数据、记录日志等。
- 传递事件:调用 ctx.fireChannelRead(msg) 将事件传递给下一个 InboundHandler。
- 终止传播:如果不调用 ctx.fireChannelRead(msg),事件传播将被终止。
- 出站事件传播
出站事件从 ChannelPipeline 的尾部(Tail)流向头部(Head),依次经过每个 OutboundHandler。- 处理事件:OutboundHandler 可以处理事件,例如修改数据、记录日志等。
- 传递事件:调用 ctx.write(msg) 将事件传递给下一个 OutboundHandler。
- 终止传播:如果不调用 ctx.write(msg),事件传播将被终止。
- 入站事件传播
-
ChannelHandler 的选择性处理:
在 ChannelPipeline 中,每个 ChannelHandler 都可以选择性地处理或修改事件,以及是否将事件传递给下一个 ChannelHandler。具体行为取决于业务需求:- 处理事件:如果 ChannelHandler 对事件感兴趣,可以处理事件并决定是否传递事件。
- 忽略事件:如果 ChannelHandler 对事件不感兴趣,可以直接传递事件。
public class MyInboundHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { if (msg instanceof String) { // 处理字符串数据 System.out.println("Received string: " + msg); } else { // 忽略其他类型的数据 ctx.fireChannelRead(msg); } } }
七、TCP 粘包与拆包
TCP 协议本身是一个面向流的协议,它并不保证消息边界,因此在网络传输过程中,多个小的数据包可能会被合并成一个大的数据包发送(粘包),或者一个大的数据包可能会被拆分成多个小的数据包发送(拆包)。TCP 协议本身是一个面向流的协议,它并不保证消息边界,因此在网络传输过程中,多个小的数据包可能会被合并成一个大的数据包发送(粘包),或者一个大的数据包可能会被拆分成多个小的数据包发送(拆包)。
-
产生粘包和拆包的原因:
- Nagle 算法:为了提高网络传输的效率,TCP 协议通常会使用 Nagle 算法将多个小的数据包合并成一个较大的数据块一起发送。
- TCP 缓冲区:发送端和接收端都有 TCP 缓冲区,当数据写入缓冲区时,如果缓冲区满了或者达到了某个阈值,数据就会被发送出去,这可能导致一个大的数据包被拆分。
- MTU 和 MSS:MTU(最大传输单元)和 MSS(最大报文段长度)限制了链路层和传输层一次可以发送的最大数据量,超过这个限制的数据包可能会被拆分。
-
粘包和拆包的解决方案
- 使用自定义协议:在消息头中定义消息的长度,接收端根据消息的长度来划分数据包的边界。这种方法需要自定义协议,并使用 Netty 提供的编码器和解码器来处理消息的拆解与重组。
- 固定长度消息:如果所有消息的长度都是固定的,那么接收端可以直接按照固定长度来读取消息。这种方法简单但不够灵活。
- 使用分隔符:在每条消息的末尾添加特定的分隔符来标识消息的边界。例如,可以使用换行符、特殊字符等作为分隔符。Netty 提供了 DelimiterBasedFrameDecoder 来支持这种方式。
- 基于长度的帧解码器:使用 Netty 提供的 LengthFieldBasedFrameDecoder 来根据消息头中的长度字段来解码消息。这种方法比较灵活,可以处理不同长度的消息。
以下是一个使用自定义协议和 LengthFieldBasedFrameDecoder 来解决粘包和拆包问题的示例代码:
// 自定义协议类
public class MessageProtocol {
private int length;
private byte[] content;
// 省略构造方法、getter 和 setter
}
// 编码器
public class MessageEncoder extends MessageToByteEncoder<MessageProtocol> {
@Override
protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
out.writeInt(msg.getLength()); // 写入消息长度
out.writeBytes(msg.getContent()); // 写入消息内容
}
}
// 解码器
public class MessageDecoder extends LengthFieldBasedFrameDecoder {
public MessageDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength) {
super(maxFrameLength, lengthFieldOffset, lengthFieldLength);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
int length = in.readInt(); // 读取消息长度
byte[] content = new byte[length];
in.readBytes(content); // 读取消息内容
return new MessageProtocol(length, content);
}
}
// 管道配置
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MessageDecoder(65535, 0, 4)); // 添加解码器
pipeline.addLast(new MessageEncoder()); // 添加编码器
pipeline.addLast(new MyServerHandler()); // 添加业务处理器
}
}
// 业务处理器
public class MyServerHandler extends SimpleChannelInboundHandler<MessageProtocol> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
// 处理消息
System.out.println("Received message: " + new String(msg.getContent()));
}
}
八、使用实例
- 添加 Netty 依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.97.Final</version>
</dependency>
- 编写Server服务端启动类
public class NettyServer {
/**
* 创建服务端实例并绑定端口
* @throws InterruptedException
*/
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 NettyServerHandler());
}
});
// 设置启动参数,初始化服务器连接队列大小。服务端处理客户端连接请求是顺序处理,一个时间内只能处理一个客户端请求
// 当有多个客户端同时来请求时,未处理的请求先放入队列中
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();
}
}
}
- 编写服务端处理器handler
public class NettyServerHandler 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("接收到客户端消息为: " + 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();
}
}
- 编写客户端启动类
public class NettyClient {
/**
* 创建客户端实例并向服务端发送连接请求
*/
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 NettyClientHandler());
}
});
// 向服务端发送连接请求
ChannelFuture channelFuture = bootstrap.connect("localhost", 8888).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 优雅地关闭线程
groupThread.shutdownGracefully();
}
}
}
- 编写客户端处理器handler
public class NettyClientHandler 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();
}
}
更多推荐



所有评论(0)