前言
学习Netty之前建议先学习Java BIO、NIO等相关的网络知识,以便后续可以更深刻的理解Netty的底层原理。
一:Netty版本
Netty总共有三个大版本,分别为3.x版本、4.x版本、5.x版本。
3.x版本已经停止维护,但现在还有一些项目在使用。由于在线程模型(输入事件与输出事件使用了不同的线程池,导致频繁的线程上下文切换。在Netty4中通过一个channel绑定一个EventLoop解决,将输入、输出都绑定在同个线程中。)、内存管理(使用直接内存跟堆内存,默认不使用内存池。netty3会创建大量的对象,导致gc压力过高)、API设计等有缺陷,因此Netty4重新设计了,导致Netty4不兼容Netty3。
4.x版本现在还在维护,截止发文时间(2023-07-31)最新版本为Netty-4.1.97.Final。4.x版本解决了3.x版本中出现的一些问题,并且还在维护。因此我们一般选择这个版本
5.x版本由于一些缺陷,例如处理大量长连接时有可能出现内存泄漏、处理高并发HTTP请求时,HTTP解析性能下降、API的变更以及不兼容性等,已经被废弃并停止维护了,不推荐使用。
因此后文我们默认使用Netty代表Netty4。
二:Netty组件
1.Bootstrap(引导器)
分为ServerBootstrap跟Bootstrap。
ServerBootstrap用于配置和启动Netty服务器的辅助类。用于配置服务端参数、添加ChannelPipeline、绑定端口等。
Bootstrap用于配置和启动Netty客户端的辅助类。用于配置链接参数、添加ChannelPipeline、发起链接等。
2.Channel(通道)
主要负责数据的读写。网络传输载体,可以代表一条网络连接,例如Socket。服务端与客户端之间就是通过Channel进行数据传输。而在Netty中的Channel定义了统一的API,其下实现了多种传输类型,例如Nio、Epoll、KQueue等。我们可以根据自己的需求选择适合的传输类型。
3.EventLoop(事件循环)
负责处理所有的IO事件以及执行任务。每个EventLoop都管理一个或者多个Channel并负责他们的读写以及其他IO事件。
4.ChannelHandler(通道处理器)
用于数据的出站、入站以及事件处理。是我们自己创建Netty服务器中自定义程度最高的组件。主要的业务逻辑都是通过ChannelHandler去实现的。同时还包括了基础功能实现,例如数据编解码、限流、状态监控等。都可以通过ChannelHandler去实现。
5.ChannelPipeline(通道管道)
用于管理ChannelHandler以及定义数据的处理过程。它是一条处理器链,数据通过Pipeline顺序地经过各个ChannelHandler处理。
6.Codec(编解码器)
用于对源数据进行编解码。可通过自定义协议解决TCP粘包拆包问题。同时Netty为了简化编解码过程,还提供了一系列的编解码器,例如ByteToMessageDecoder、MessageToMessageCodec等。
7.ByteBuf
Netty提供的高性能的字节容器,用于读写数据。
三:Netty入门使用
1. maven依赖
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.97.Final</version>
</dependency>
2. 服务端
服务端启动类
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.DefaultEventExecutorGroup;
public class NettyServer {
public static void main(String[] args) {
//EventLoopGroup相当于线程池, 实际上也实现了线程池
//bossGroup是用来处理连接的
EventLoopGroup bossGroup = new NioEventLoopGroup();
//workerGroup是用来处理io的
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
//添加我们自定义的业务handler
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
// 第一个参数为EventExecutorGroup, 实际上是一个线程池,
// 作用是当执行handler时, 不使用workerGroup的io线程,
// 而是使用我们传进去的线程池去执行handler里面业务逻辑代码, 防止io线程堵塞造成并发量下降
ch.pipeline()
.addLast(new DefaultEventExecutorGroup(10), new FirstServerHandler());
}
});
//绑定本地的8002端口
serverBootstrap.bind(8002);
}
}
服务端自定义FirstServerHandler
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.nio.charset.Charset;
import java.util.Date;
public class FirstServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf byteBuf = (ByteBuf) msg;
String s = byteBuf.toString(Charset.forName("utf-8"));
System.out.println(new Date() + ": 服务端接收到客户端的数据 -> " + s);
//接收到客户端的消息后我们再回复客户端
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;
}
}
ChannelInboundHandlerAdapter为消息入站处理适配器,如果我们想处理对端传来的数据时,就可以继承这个类,并重写其中的channelRead(ChannelHandlerContext ctx, Object msg)方法,这样在客户端传送数据到服务端时,Netty会自动帮我们调用这个ChannelRead方法。我们自己的业务逻辑。
这里我们通过继承ChannelInboundHandlerAdapter来实现自定义的FirstServerHandler,用于接收客户端传送来的数据,将其打印到控制台后,我们通过ChannelHandlerContext获取到与当前服务端连接的客户端channel,并通过这个channel将我们服务端要发送的数据传给客户端。
3. 客户端
客户端启动类
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
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) throws InterruptedException {
EventLoopGroup workerGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap
// 1.指定线程模型
.group(workerGroup)
// 2.指定 IO 类型为 NIO
.channel(NioSocketChannel.class)
// 3.添加自定义handler,
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new FirstClientHandler());
}
});
// 4.建立连接
bootstrap.connect("127.0.0.1", 8002).addListener(future -> {
if (future.isSuccess()) {
System.out.println("连接成功!");
} else {
System.err.println("连接失败!");
}
});
}
}
客户端自定义FirstClientHandler
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.nio.charset.Charset;
import java.util.Date;
public class FirstClientHandler extends ChannelInboundHandlerAdapter {
@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
ByteBuf buffer = ctx.alloc().buffer();
// 2. 准备数据,指定字符串的字符集为 utf-8
byte[] bytes = ("我是客户端, 我传数据给你啦" + new Date()).getBytes(Charset.forName("utf-8"));
// 3. 填充数据到 ByteBuf
buffer.writeBytes(bytes);
return buffer;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf byteBuf = (ByteBuf) msg;
//接收服务端的消息并打印
System.out.println(byteBuf.toString(Charset.forName("utf-8")));
}
}
客户端的FirstClientHandler也是同样继承ChannelInboundHandlerAdapter类,只是实现的方法不同,我们通过实现void channelActive(ChannelHandlerContext ctx)方法, 这个方法在Channel通道被激活时被Netty调用,我们通过这个方法向服务端传送数据。
这样就成功构建了一个Netty服务端与客户端互相通信的样例了。后面都是围绕这个样例实现新的功能。