简介
i/o 种类
Netty是什么?
Netty是一个基于Java NIO的网络编程框架,提供了一套高效的、事件驱动的异步网络通信机制。简化了网络应用程序的开发过程,提供了可靠的、高性能的网络传输。
Netty的特点是什么?
- 异步和事件驱动:Netty使用异步的、非阻塞的I/O模型,通过事件驱动的方式处理网络操作。Netty能够高效地处理并发连接和大量的并发请求。
- 高性能:Netty采用了一系列优化策略,如零拷贝技术、内存池和可定制的线程模型等,以提供出色的性能和吞吐量。能处理高负载和大规模并发
- 多协议支持:Netty提供了丰富的协议支持,包括常用的网络协议(如HTTP、WebSocket、TCP和UDP)以及自定义协议。具备灵活的编解码器和处理器,简化了协议的实现和交互。
- 可扩展和灵活:Netty的架构和组件设计具有高度的可扩展性和灵活性。它提供了一组可重用的组件,可以根据应用需求进行定制和扩展。
- 安全性:Netty提供了强大的安全性支持,包括SSL/TLS的集成、加密和认证等机制,可以保护网络通信的安全性。
架构和亮点
netty的主要作用就是提供一个简单的NIO框架可以和上层的各种协议相结合,最终实现高性能的服务器。下面是netty官网提供的架构图:
从上图可以看到netty的核心主要分成三部分,分别是可扩展的event model、统一的API、和强大的Byte Buffer。这三个特性是netty的制胜法宝。
下面会从这几个方面对netty的特性进行详细说明,务必让读者了解到netty的优秀之处。
丰富的Buffer数据机构
首先从最底层的Buffer数据结构开始,netty提供了一个io.netty.buffer的包,该包里面定义了各种类型的ByteBuf和其衍生的类型。
netty Buffer的基础是ByteBuf类,这是一个抽象类,其他的Buffer类基本上都是由该类衍生而得的,这个类也定义了netty整体Buffer的基调。
netty重写ByteBuf其目的是让最底层的ByteBuf比JDK自带的更快更适合扩展。具体而言,netty的ByteBuf要比JDK中的ByteBuffer要快,同时,扩展也更加容易,大家可以根据需要Buf进行自定义。另外netty有一些内置的复合缓冲区类型,所以可以实现透明的零拷贝。对于动态缓冲区类型来说,可以和StringBuffer一样按需扩展,非常好用。
统一的API
一般来说,在传统的JDK的IO API中,根据传输类型或者协议的不同,使用的API也是不同的。我们需要对不同的传输方式开发不同的应用程序,不能做到统一。这样的结果就是无法平滑的迁移,并且在程序扩展的时候需要进行额外的处理。
什么是传输方式呢?这里是指以什么样的方式来实现IO,比如传统的阻塞型IO,我们可以称之为OIO,java的new IO可以称之为NIO,异步IO可以称之为AIO等等。
并且JDK中的传统IO和NIO是割裂的,如果在最开始你使用的是传统IO,那么当你的客户数目增长到一定的程度准备切换到NIO的时候,就会发现切换起来异常复杂,因为他们是割裂的。
为了解决这个问题,netty提供了一个统一的类Channel来提供统一的API。
使用Channel就可以对NIO的TCP/IP,OIO的TCP/IP,OIO的UDP/IP和本地传输都能提供很好的支持。
传输方式的切换,只需要进行很小的成本替换。
当然,如果你对现有的实现都不满意的话,还可以对核心API进行自定义扩展。
事件驱动
netty是一个事件驱动的框架,事件驱动框架的基础就是事件模型,Netty专门为IO定义了一个非常有效的事件模型。可以在不破坏现有代码的情况下实现自己的事件类型。netty中自定义的事件类型通过严格的类型层次结构跟其他事件类型区分开来,所以可扩展性很强。
netty中的事件驱动是由ChannelEvent、ChannelHandler和ChannelPipeline共同作用的结果。其中ChannelEvent表示发生的事件,ChannelHandler定义了如何对事件进行处理,而ChannelPipeline类似一个拦截器,可以让用户自行对定义好的ChannelHandler进行控制,从而达到控制事件处理的结果。
其他优秀的特性
除了上面提到的三大核心特性之外,netty还有其他几个优点,方便程序员的开发工作。
比如对SSL / TLS的支持,对HTTP协议的实现,对WebSockets 实现和Google Protocol Buffers的实现等等,表明netty在各个方面多个场景都有很强的应用能力。
**总结: **netty是由三个核心组件构成:缓冲区、通道和事件模型,通过理解这三个核心组件是如何相互工作的,那么再去理解建立在netty之上的高级功能就不难了。
Netty Reactor线程模型
型各部分说明:
Netty 内部分为两个工作线程组: BossGroup 和 WorkerGroup。BossGroup 专门负责客户端连接的接收,WorkerGroup 专门负责网络的读写事件。
BossGroup 和 WorkerGroup 都是 NioEventLoopGroup 类型。一般情况下,BossGroup 的大小设置为1,WorkerGroup 的大小不设置,默认是 cpu数量 * 2。
BossGroup如果服务端只有一个监听端口,设置1即可。即使设置的boss线程大于1实际上也不会被使用到。如果同一个BossGroup 绑定多个端口,那么可以写对应的端口个数,但是一般不会这样做因为管理起来比较麻烦
为什么一个端口只能一个线程处理? **
因为操作系统底层的 selector的特性导致的,我们只能有一个线程持久selector,即便是我们在多个线程进行持有selector,如果想要获取通道上面的事件就需要调用Selector#select()方法。看一下他对应的介绍: 这个操作是阻塞的,同一时间只能有一个线程进行处理,所以天生不支持多线程**。
但是在 netty 中影响不大, 因为后续实际上的工作是交给了WorkerGroup 线程池处理的
NioEventLoopGroup 相当于一个事件循环线程组,一个组里含有多个事件循环线程,每一个事件循环线程的对象是 NioEventLoopGroup。
每一个事件循环线程都持有一个 selector,线程会监听发生在该 selector 上的网络通讯事件。
每个 Boss NioEventLoop 线程循环执行的步骤:
(1). 处理 accept 事件,与客户端建立连接,生成 NioSocketChannel;
(2). 将 NioSocketChannel 注册到 Woker NioEventLoop 的其中一个线程上的selector;(默认是轮询分配)
(3). 处理任务队列里的任务,runAllTasks。
每个 Worker NioEventLoop 线程循环执行的步骤:
(1). 轮询注册到自己 selector 上的所有 NioSocketChannel 的 read,write 事件;
(2). 处理 I/O事件,执行业务代码;
(3). runAllTasks 处理队列里的任务,耗时的任务可放进队列里,不影响 pipeline 的数据流动。
7. NioEventLoopGroup 处理 NioSocketChannel 上的网络事件时,会使用到 pipeline(管道),使用时可以往管道上添加自定义的 ChannelHandler。
Netty核心组件
Bootstrap(启动类)
用于启动和配置网络应用程序配置类。通过Bootstrap可以绑定启动IP、端口,配置EventLoopGroup、选择传输协议(Channel)、配置ChannelHandler、参数调优设置等。Bootstrap通常与ServerBootstrap配合使用。ServerBootstrap用于服务端,Bootstrap用于客户端。
EventLoop(事件循环)
Netty为了避免线程与线程之间产生并发冲突,采用的策略。负责处理所有的I/O事件,如接收连接、读取数据、写入数据等。EventLoop在一个线程中循环执行,通过事件驱动的方式处理事件。一个EventLoop可以关联一个或多个Channel,但一个Channel只能关联一个EventLoop。EventLoop通常在服务端需要实例化2个对象,一个用于接受处理客户端的连接请求,另一个用户处理I/O读写事件。
Channel(通道)
一个与实际数据传输相关的连接。可以理解为数据在客户端和服务器之间的通道。
底层网络传输 API 必须提供给应用 I/O操作的接口,如读,写,连接,绑定等等。对于我们来说,这是结构几乎总是会成为一个“socket”。 Netty 中的接口 Channel 定义了与 socket 丰富交互的操作集:bind, close, config, connect, isActive, isOpen, isWritable, read, write 等等。 Netty 提供大量的 Channel 实现来专门使用。这些包括 AbstractChannel,AbstractNioByteChannel,AbstractNioChannel,EmbeddedChannel, LocalServerChannel,NioSocketChannel 等等。
Netty系列教程(四)Netty组件之Channel通道 - 掘金
ChannelHandler(通道处理器)
最核心的组件之一
ChannelHandler 支持很多协议,并且提供用于数据处理的容器。我们已经知道 ChannelHandler 由特定事件触发。 ChannelHandler 可专用于几乎所有的动作,包括将一个对象转为字节(或相反),执行过程中抛出的异常处理。
常用的一个接口是 ChannelInboundHandler,这个类型接收到入站事件(包括接收到的数据)可以处理应用程序逻辑。当你需要提供响应时,你也可以从 ChannelInboundHandler 冲刷数据。一句话,业务逻辑经常存活于一个或者多个 ChannelInboundHandler。
ChannelHandlerContext(通道处理器上下文)
ChannelHandler的上下文环境。包含与ChannelHandler相关联的各种信息,如Channel、EventLoop、ChannelPipeline等。ChannelHandlerContext提供了丰富的方法,以便于ChannelHandler与其他组件进行交互。例如:区分谁是入站、出站就需要ChannelHandlerContext来维护管理
ChannelPipeline(通道管道)
ChannelPipeline 提供了一个容器给 ChannelHandler 链并提供了一个API 用于管理沿着链入站和出站事件的流动。每个 Channel 都有自己的ChannelPipeline,当 Channel 创建时自动创建的。
ChannelPipeline是一个双向链表,拦截和处理事件的链式结构。ChannelPipeline中的ChannelHandler是一个传播链,ChannelPipeline管理ChannelHandler并协调它们的处理顺序。当数据通过Channel时,它会依次经过每个ChannelHandler进行处理。
ChannelPipeline还提供了方便的操作方法,如添加、移除和替换ChannelHandler。
Channel与ChannelPipeline组成关系:一个Channel 包含了一个ChannelPipeline,而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。
**注意: ** 管道是按照添加时候的先后顺序执行的, 当前管道内的某个处理事件, 如果不往下推送事件,那么他下面的事件,是不会执行的,就比如channelInactive 事件
如果不执行super.channelInactive(ctx);那么就不在会继续往下传递了,其实本质是调用了 ctx.fireChannelInactive();
每个方法都会有一个这个 ctx.firexxx
开头的事件,就是用于往下一个管道传递。
ByteBuf(字节缓冲区)
数据传输的基本单元,用于处理二进制数据的数据结构。ByteBuf提供了灵活的API,可以高效地读取、写入和操作数据。
Netty 有自己的 ByteBuf ,做了如下增强:
- API 操作便捷性
- 动态扩容
- 多种 ByteBuf 实现
- 高效的零拷贝机制
注意:
- 如果手动创建的ByteBuf必须
byte.release();
释放内存否则就会泄漏 - 如果是在ChannelPipline 流水线中的ByteBuf入参和出参的时候会自动释放
- 在ByteBuf 一旦使用readBytes 读取,那么就会移动指针,下次读取就会从上次指针位置开始,这点需要注意。
Codec(编解码器)
用于处理数据的编码和解码的组件。Netty提供了一系列的编解码器,可以帮助开发者简化数据的编解码过程。常见的编解码器有字符串编解码器、对象序列化编解码器等。
ChannelFuture(结果处理)
异步操作的结果,例如连接的建立和关闭等。ChannelFuture提供了丰富的方法,用于检查操作的状态、添加监听器以便在操作完成时接收通知,并对操作的结果进行处理。
ChannelOption(通道选项)
调优参数,可对Netty高并发的情况下进行调优。
入门案例 DEMO
Maven
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.86.Final</version>
</dependency>
编写Server服务端启动类
package com.sjchen.netty;
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();
}
}
}
编写服务端处理器handler
package com.sjchen.netty;
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();
}
}
编写客户端启动类
package com.sjchen.netty;
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();
}
}
}
编写客户端处理器handler
package com.sjchen.netty;
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();
}
}
操作
启动客户端、服务端,看控制台打印结果。可以看到,客户端启动时给服务端发送一条消息,服务端收到消息后给客户端回应消息。
总结
在Netty 开发中我们主要编写handler处理器,ChannelHandler是处理入站和出站数据的应用程序逻辑的容器。ChannelHandler顶级接口中派生了两个重要的子接口ChannelInboundHandler和ChannelOutboundHandler以及适配器抽象类ChannelHandlerAdapter,使用规则如下:
- ChannelInboundHandler:用于处理入站事件,也称为输入处理器。可以处理从远程对等方(如客户端)到本地Netty服务器的事件,包括连接建立、数据接收、异常发生等。
- ChannelOutboundHandler:用于处理出站事件,也称为输出处理器。可以处理从本地Netty服务器到远程对等方(如服务器端)的事件,包括数据发送、连接建立、连接关闭等。
- ChannelInboundHandlerAdapter是一个抽象类,实现了ChannelInboundHandler接口,可以作为用户自定义的ChannelInboundHandler的基类。当使用时,需要覆盖其中的方法来实现自定义的处理逻辑,包括channelRead()、channelReadComplete()、exceptionCaught()等方法。适用于需要实现自定义的、细粒度的处理逻辑的场景。它提供了更大的灵活性,可以对接收到的事件进行完全自定义的处理。
- SimpleChannelInboundHandler是ChannelInboundHandlerAdapter的子类,提供了更高层次的抽象和简化。里面封装了消息的类型匹配和释放资源的逻辑,用户只需要关注具体的消息类型和业务逻辑的处理。当使用时,需要指定具体的泛型类型,以指定处理的消息类型。
注意:
- 如果继承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服务器、代理服务器等。
- 分布式系统之间的通信,如分布式缓存、消息队列等。
Netty的高并发高性能架构设计精髓
- 主从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等机制实现非阻塞的并发处理。同时,它还提供了多种并发安全的组件和工具类,简化了并发编程的复杂性。
Netty抓包工具推荐
Wireshark · Go Deep
Wireshark 专门抓 tcp, udp 等网络协议的包, 但是无法解析 http 协议的内容,如果需要抓 http 协议那么使用Charles 或Fiddler 是最好的选择
内存泄露检测
使用引用计数的缺点在于容易产生内存泄露,因为JVM不知道引用计数的存在。当一个对象不可达时,JVM可能会收回该对象,但这个对象的引用计数可能还不是0,这就导致该对象从池里分配的空间不能归还到池里,从而导致内存泄露。
Netty提供了一种内存泄露检测机制,可以通过配置参数不同选择不同的检测级别,参数设置为java -Dio.netty.leakDetection.level=advanced
- DISABLED :完全禁用内存泄露检测,不推荐
- SIMPLE :抽样1%的ByteBuf,提示是否有内存泄露
- ADVANCED :抽样1%的ByteBuf,提示哪里产生了内存泄露
- PARANOID :对每一个ByteBu进行检测,提示哪里产生了内存泄露
我在测试时,直接提示了ByteBuf内存泄露的位置,如下,找到自己程序代码,看哪里有新生成的ByteBuf对象没有释放,主动释放一下,调用对象的release()函数,或者用工具类帮助释放ReferenceCountUtil.release(msg)。