Netty是什么?
1)本质:基于NIO的通信框架做的一个 Jar 包。
2)目的:快速开发高性能、高可靠性的网络服务器和客户端程序。
3)优点:提供异步的、事件驱动的网络应用程序框架和工具。
Netty为什么并发高?
Netty 是一款基于 NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高
Netty为什么传输快?
Netty 的传输快其实也是依赖了 NIO 的一个特性——零拷贝。我们知道,Java的内存有堆内存、栈内存和字符串常量池等等,其中堆内存是占用内存空间最大的一块,也是Java对象存放的地方,一般我们的数据如果需要从IO读取到堆内存,中间需要经过Socket缓冲区,也就是说一个数据会被拷贝两次才能到达他的的终点,如果数据量大,就会造成不必要的资源浪费。
Netty针对这种情况,使用了NIO中的另一大特性——零拷贝,当他需要接收数据的时候,他会在堆内存之外开辟一块内存,数据就直接从IO读到了那块内存中去,在netty里面通过ByteBuf可以直接对这些数据进行直接操作,从而加快了传输速度。
先看一个 Demo
服务端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public class NettyServer { // 引导类最小化的参数配置就是下面四个:配置线程组、IO模型、处理逻辑、绑定端口。 public static void main(String[] args) { //████████ 第一步 ████████ //████注意这里创建了两个 NioEventLoopGroup 对象,其中 bossGroup 是监听客户端请求,另一个 workerGroup 是处理客户端请求,这种设计在中间件技术中很常用,比如 zookeeper 也是一个节点接受请求并转发,其他节点处理请求████ // bossGroup的作用是监听客户端请求 NioEventLoopGroup bossGroup = new NioEventLoopGroup(); // workerGroup的作用是处理每条连接的数据读写 NioEventLoopGroup workerGroup = new NioEventLoopGroup(); //████████ 第二步 ████████ //████创建一个 ServerBootstrap 引导类的对象████ ServerBootstrap serverBootstrap = new ServerBootstrap(); //████████ 第三步 ████████ serverBootstrap .group(bossGroup, workerGroup) //█3.1█配置第一步创建的 bossGroup 和 workerGroup 对象 .channel(NioServerSocketChannel.class) //█3.2█ .channel是配置服务端的IO模型,上面代码配置的是NIO模型。// 也可以配置为BIO,如OioServerSocketChannel.class。 .childHandler(new ChannelInitializer<NioSocketChannel>() { //█3.3█ .childHandler用于配置每条连接的数据读写和业务逻辑等。一般会再写一个单独的类也就是初始化器。==》一般会在这里自定义处理读写的逻辑!!! @Override protected void initChannel(NioSocketChannel ch) { System.out.println("============initChannel============"); } }) .handler(new ChannelInitializer<NioServerSocketChannel>() { //█3.4█ 用于指定服务端启动中的逻辑 @Override protected void initChannel(NioServerSocketChannel ch) { System.out.println("============服务端启动中============"); } }); //████████ 第四步:绑定端口号 ████████ serverBootstrap.bind(8000); } } |
客服端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
public class NettyClient { public static void main(String[] args) { //████████ 第一步 ████████ //██配置客户端的线程模型██ NioEventLoopGroup workerGroup = new NioEventLoopGroup(); //████████ 第二步 ████████ //██创建引导类对象(注意:服务端是 ServerBootstrap,而客户端是 Bootstrap)██ Bootstrap bootstrap = new Bootstrap(); //████████ 第三步 ████████ bootstrap // 1.指定线程模型 .group(workerGroup) // 2.指定 IO 类型为 NIO .channel(NioSocketChannel.class) // 3.IO 处理逻辑 .handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) { System.out.println("============ NettyClient: NettyClient:initChannel ============"); } }); // 4.建立连接 bootstrap.connect("127.0.0.1", 8000).addListener(future -> { if (future.isSuccess()) { System.out.println("NettyClient:连接成功!"); } else { System.err.println("NettyClient:连接失败!"); //重新连接 } }); } } |
服务端日志:
1 2 |
============服务端启动中============ ============initChannel============ |
客户端日志:
1 2 |
============ NettyClient: NettyClient:initChannel ============ NettyClient:连接成功! |
数据传输的载体:ByteBuf

- ByteBuf 是一个字节容器,容器里面的的数据分为三个部分,第一个部分是已经丢弃的字节,这部分数据是无效的;第二部分是可读字节,这部分数据是 ByteBuf 的主体数据, 从 ByteBuf 里面读取的数据都来自这一部分; 第三部分的数据是可写字节,所有写到 ByteBuf 的数据都会写到这一段。最后一部分虚线表示的是该 ByteBuf 最多还能扩容多少容量。
- 以上三段内容是被两个指针给划分出来的,从左到右,依次是读指针(readerIndex)、写指针(writerIndex),然后还有一个变量 capacity,表示 ByteBuf 底层内存的总容量。
- 从 ByteBuf 中每读取一个字节,readerIndex 自增1,ByteBuf 里面总共有 writerIndex-readerIndex 个字节可读, 由此可以推论出当 readerIndex 与 writerIndex 相等的时候,ByteBuf 不可读。
- 写数据是从 writerIndex 指向的部分开始写,每写一个字节,writerIndex 自增1,直到增到 capacity,这个时候,表示 ByteBuf 已经不可写了。
- ByteBuf 里面其实还有一个参数 maxCapacity,当向 ByteBuf 写数据的时候,如果容量不足,那么这个时候可以进行扩容,直到 capacity 扩容到 maxCapacity,超过 maxCapacity 就会报错。
Demo 自定义处理逻辑
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
public class NettyServer { public static void main(String[] args) { // 两个NioEventLoopGroup对象,可以看作两个线程组。bossGroup的作用是监听客户端请求。workerGroup的作用是处理每条连接的数据读写。 NioEventLoopGroup bossGroup = new NioEventLoopGroup(); NioEventLoopGroup workerGroup = new NioEventLoopGroup(); // ServerBootstrap是一个引导类,其对象的作用是引导服务器的启动工作 ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap // .group是配置上面两个线程组的角色,也就是谁去监听、谁去处理读写。上面只是创建了两个线程组,并没有实际使用 .group(bossGroup, workerGroup) // .channel是配置服务端的IO模型,上面代码配置的是NIO模型。也可以配置为BIO,如OioServerSocketChannel.class .channel(NioServerSocketChannel.class) // .childHandler用于配置每条连接的数据读写和业务逻辑等。实际使用中为了规范起见, // 会单独写一个类也就是初始化器,在里面写上需要的操作。就如Netty实战那篇中的代码一样。 .childHandler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) { ch.pipeline().addLast(new FirstServerHandler()); System.out.println("============initChannel============"); } }) // handler()方法:上面的childHandler是处理连接的读写逻辑,这个是用于指定服务端启动中的逻辑. .handler(new ChannelInitializer<NioServerSocketChannel>() { @Override protected void initChannel(NioServerSocketChannel ch) { System.out.println("============服务端启动中============"); } }); serverBootstrap.bind(8000); } } |
自定义处理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/** Desc: 服务器会响应传入进来的消息,所以它需要实现 ChannelInboundHandler 接口,用来定义响应"入站"事件的方法。 可以使用 ChannelInboundHandlerAdapter 即可,它已实现了ChannelInboundHandler接口接口并提供了 ChannelInboundHandler 的默认实现。 <p> * * @author zhouxingbin * @date 2020/12/24 */ public class FirstServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf byteBuf = (ByteBuf) msg; System.out.println(new Date() + ": 服务端读到数据 -> " + byteBuf.toString(Charset.forName("utf-8"))); //接收到客户端的消息后我们再回复客户端 ByteBuf out = getByteBuf(ctx); ctx.channel().writeAndFlush(out); } private ByteBuf getByteBuf(ChannelHandlerContext ctx) { byte[] bytes = "【服务器】:我是服务器,我收到你的消息了!".getBytes(Charset.forName("utf-8")); ByteBuf buffer = ctx.alloc().buffer(); buffer.writeBytes(bytes); return buffer; } } |
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class NettyClient { public static void main(String[] args) { NioEventLoopGroup workerGroup = new NioEventLoopGroup(); Bootstrap bootstrap = new Bootstrap(); bootstrap // 1.指定线程模型 .group(workerGroup) // 2.指定 IO 类型为 NIO .channel(NioSocketChannel.class) // 3.IO 处理逻辑 .handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) { System.out.println("============ NettyClient:initChannel ============"); ch.pipeline().addLast(new FirstClientHandler()); } }); // 4.建立连接 bootstrap.connect("127.0.0.1", 8000).addListener(future -> { if (future.isSuccess()) { System.out.println("NettyClient:连接成功!"); } else { System.err.println("NettyClient:连接失败!"); //重新连接 } }); } } |
自定义处理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public class FirstClientHandler extends ChannelInboundHandlerAdapter { // channelActive()方法会在客户端与服务器建立连接后调用。所以我们可以在这里面编写逻辑代码 @Override public void channelActive(ChannelHandlerContext ctx) { System.out.println("客户端发送消息..."); // 1. 获取数据 ByteBuf buffer = getByteBuf(ctx); // 2. 写数据(往服务端发送数据) ctx.channel().writeAndFlush(buffer); } private ByteBuf getByteBuf(ChannelHandlerContext ctx) { // 1. 获取二进制抽象 ByteBuf 【.alloc().buffer()的作用是把字符串的二进制数据填充到ByteBuf】 ByteBuf buffer = ctx.alloc().buffer(); // 2. 准备数据,指定字符串的字符集为 utf-8 byte[] bytes = ("【客户端】:这是客户端发送的消息:" + new Date()).getBytes(Charset.forName("utf-8")); // 3. 填充数据到 ByteBuf 【.writeBytes()的作用是把数据写到服务器】 buffer.writeBytes(bytes); return buffer; } // channelRead()在接受到服务端的消息后调用(从服务端接收数据) @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf byteBuf = (ByteBuf) msg; //接收服务端的消息并打印 System.out.println(byteBuf.toString(Charset.forName("utf-8"))); } } |
pipeline与channelHandler
通过上面的一些实战,可以发现我们所有的逻辑代码都写在了一个Handler类里面,幸好现在需要处理的业务不是很多。如果以后功能拓展,这个类会变得非常臃肿。Netty中的pipeline和channelHandler就是解决这个问题的,它们通过责任链设计模式来组织代码逻辑,并且能够支持逻辑的添加和删除,能够支持各类协议拓展,如HTTP、Websocket等。可以看看Netty实战博客中的初始化器类,里面就是通过pipeline添加了各类协议和一些逻辑代码。
pipeline与channelHandler的构成

我们知道一个连接对应一个channel,这个channel的所有处理逻辑在一个ChannelPipeline对象里,就是上图中的pipeline,这是它的对象名。然后这个对象里面是一个双向链表结构,每个节点是一个ChannelHandlerContext对象。这个对象能拿到与channel相关的所有上下文信息,这个对象还包含一个重要的对象:ChannelHandler,它的分类如下。

简单地说,它包含两个接口和这两个接口的实现类,图中左边的实现类是不是很熟悉,就是我们自己写的逻辑处理器里的继承的类。从名字就可以看出,它们的作用分别是读数据和写数据,或理解为入站和出战。最重要的两个方法分别为channelRead():消息入站。和write():消息出战。
参印象笔记:██【好啊】Netty入门与实战教程
Netty 在 xxl-job 中的使用
xxl-job 中的执行器会创建一个 netty server 来接收调度中心的 http “调度请求”:

