Netty介绍
Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的socket服务开发。
官网地址:http://netty.io/
使用场景
Netty之所以能成为主流的NIO框架,是因为它有下面的优点:
- NIO的类库和API使用难度较高,Netty进行了封装,容易上手
- 高性能,功能强大,支持多种编解码功能,支持多种主流协议
- 成熟,稳定,已经在多个大型框架中使用(dubbo,RocketMQ,Hadoop,mycat,Spring5)
- …..
快速入门-Talk is cheap. Show me the code
添加依赖
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.21.Final</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.22</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.0.13</version>
</dependency>
</dependencies>
服务端代码
public class EchoService {
private final static Logger LOGGER = LoggerFactory.getLogger(EchoService.class);
public static final String SERVER_IP = "127.0.0.1";
public static final int SERVER_PORT = 54123;
public static void main(String[] args) {
ServerBootstrap b = new ServerBootstrap();
//创建reactor 线程组
EventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
EventLoopGroup workerLoopGroup = new NioEventLoopGroup();
try {
//1 设置reactor 线程组
b.group(bossLoopGroup, workerLoopGroup);
//2 设置nio类型的channel
b.channel(NioServerSocketChannel.class);
//3 设置监听端口
b.localAddress(SERVER_PORT);
//4 设置通道的参数
/**
*PooledByteBufAllocator 是 Netty 中默认的字节缓冲分配器实现。它使用池化的方式来管理字节缓冲,重用之前分配过的缓冲,从而减少内存分配和回收的开销。通过使用池化的方式,它能够在高并发情况下更高效地管理缓冲,适用于大量的、频繁的缓冲分配和回收操作。
*PooledByteBufAllocator 适合处理长期运行的网络应用程序,特别是在高并发的情况下,它可以显著提高性能和内存利用率。
*/
//b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
/**
* UnpooledByteBufAllocator 是 Netty 中另一种字节缓冲分配器实现。它不使用池化机制,每次请求都会创建一个新的字节缓冲对象,每个缓冲对象在使用完后都会被单独回收。因此,它的开销较大,不具备池化的优势。
* UnpooledByteBufAllocator 适合处理短期、简单的网络应用程序,特别是在并发较低的情况下,或者在需要临时创建大量缓冲时。
*/
b.option(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT);
b.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
b.childOption(ChannelOption.SO_KEEPALIVE, true);
//5 装配子通道流水线
b.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(EchoServiceHandler.INSTANCE);
}
});
// 6 开始绑定server
// 通过调用sync同步方法阻塞直到绑定成功
ChannelFuture channelFuture = b.bind();
channelFuture.addListener((future)->{
if(future.isSuccess())
{
LOGGER.info(" ========》反应器线程 回调 服务器启动成功,监听端口: " +
channelFuture.channel().localAddress());
}
});
// channelFuture.sync();
LOGGER.info(" 调用线程执行的,服务器启动成功,监听端口: " +
channelFuture.channel().localAddress());
// 7 等待通道关闭的异步任务结束
// 服务监听通道会一直等待通道关闭的异步任务结束
ChannelFuture closeFuture = channelFuture.channel().closeFuture();
closeFuture.sync();
} catch (Exception ex) {
ExceptionUtil.stacktraceToString(ex);
} finally {
// 8 优雅关闭EventLoopGroup,
// 释放掉所有资源包括创建的线程
workerLoopGroup.shutdownGracefully();
bossLoopGroup.shutdownGracefully();
}
}
}
@ChannelHandler.Sharable
public class EchoServiceHandler extends ChannelInboundHandlerAdapter {
private final static Logger LOGGER = LoggerFactory.getLogger(EchoServiceHandler.class);
public static final EchoServiceHandler INSTANCE = new EchoServiceHandler();
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
LOGGER.info("msg type: " + (in.hasArray()?"堆内存":"直接内存"));
//因为Netty4.1+默认是使用直接内存的buffer来存储Channel读到的数据, Java要进行处理这些数据, 先要拷贝到自己的堆中
//所以这里先建立一个对应长度的堆内数组
int len = in.readableBytes();
byte[] arr = new byte[len];
in.getBytes(0, arr);
LOGGER.info("server received: " + new String(arr, "UTF-8"));
//写回数据,异步任务
LOGGER.info("写回前,msg.refCnt:" + (in.refCnt()));//测试一下当前缓冲区的引用数量
ChannelFuture f = ctx.writeAndFlush(msg);
// ChannelFuture f = ctx.pipeline().writeAndFlush(msg);
// ChannelFuture f = ctx.channel().pipeline().writeAndFlush(msg);
f.addListener((ChannelFuture futureListener) -> {
LOGGER.info("写回后,msg.refCnt:" + in.refCnt());
});
//传递到下一个Handler - 当前案例只有一个用户自定义的Handler, 其实不写也无所谓.
//super.channelRead(ctx, msg);
}
}
上面的示例中,我们创建了一个
ServerBootstrap
对象,然后配置了bossGroup
和workerGroup
,设置了服务端的 Channel 类型为NioServerSocketChannel
,并添加了一个ChannelInitializer
来配置处理器链。
ServerBootstrap
的group()
方法用于设置两个线程组,一个负责接收客户端连接(bossGroup
),另一个负责处理客户端的读写请求(workerGroup
)。
ServerBootstrap
的channel()
方法用于指定服务端 Channel 的类型,NioServerSocketChannel.class
表示使用 NIO 的 ServerSocketChannel。
ServerBootstrap
的childHandler()
方法用于设置处理器链。在上面的示例中,我们添加了一个自定义的EchoServiceHandler
处理器。最后,我们通过调用
bind()
方法绑定端口并启动服务器。在ChannelFuture
中我们可以添加监听器来处理绑定结果和服务器关闭的操作。
客户端代码
public class EchoClient {
private final static Logger LOGGER = LoggerFactory.getLogger(EchoClient.class);
public static final String SERVER_IP = "127.0.0.1";
public static final int SERVER_PORT = 54123;
public static void main(String[] args) {
//Step1: 创建组装器 - 用于配置客户端的 - 事件轮询器 - 通道 - 处理器
Bootstrap b = new Bootstrap();
//Step2: 创建轮询器 - 封装了Selector, 用于选择数据传输事件
EventLoopGroup workerLoopGroup = new NioEventLoopGroup();
try {
b.group(workerLoopGroup);
//Step3.1:设置通道类型
b.channel(NioSocketChannel.class);
//Step3.2:设置监听端口
b.remoteAddress(SERVER_IP, SERVER_PORT);
//Step3.3:设置通道的参数
b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
//默认是30s, 如果在给定的时间不能成功建立连接或者被丢弃掉,将抛出ConnectTimeoutException
b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,10000);
b.option(ChannelOption.SO_KEEPALIVE, true);;
//Step4: 配置事件处理器
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(EchoClientHandler.INSTANCE);
}
});
//Step5: 循环链接服务端
ChannelFuture f = null;
boolean connected = false;
while (!connected) {
f = b.connect();
f.addListener((ChannelFuture futureListener) -> {
if (futureListener.isSuccess()) {
LOGGER.info("EchoClient客户端连接成功!");
} else {
LOGGER.info("EchoClient客户端连接失败!");
}
});
// sync作用: 因为上面的连接到服务器上以及监听都是异步操作, 执行后马上返回, 可能连接还未完全建立, 所以sync在此等待一下
// f.sync(); 发生错误会抛异常
f.awaitUninterruptibly();//发生错误不会抛异常
if (f.isCancelled()) {
LOGGER.info("用户取消连接:");
return;
// Connection attempt cancelled by user
} else if (f.isSuccess()) {
connected = true;
}
}
//StepX - 业务操作: 在连接完成之后, 获取到通道, 往通道里面写一些数据
//获取通道
Channel channel = f.channel();
Scanner scanner = new Scanner(System.in);
LOGGER.info("请输入发送内容:");
// 发送回调监听
GenericFutureListener sendCallBack = future -> {
if (future.isSuccess()) {
LOGGER.info("发送成功!");
} else {
LOGGER.info("发送失败!");
}
};
while (scanner.hasNext()) {
//获取输入的内容
String next = scanner.next();
byte[] bytes = (DateUtil.now() + " >>" + next).getBytes(StandardCharsets.UTF_8);
// 创建一个缓冲区, 用于存储待发送的信息
ByteBuf buffer = channel.alloc().buffer();
// 保存数据到直接内存的缓冲区
buffer.writeBytes(bytes);
// 通过通道将数据发送出去
ChannelFuture writeAndFlushFuture = channel.writeAndFlush(buffer);
writeAndFlushFuture.addListener(sendCallBack);
LOGGER.info("请输入发送内容:");
}
} catch (Exception ex) {
ExceptionUtil.stacktraceToString(ex);
} finally {
workerLoopGroup.shutdownGracefully();
}
}
}
@ChannelHandler.Sharable
public class EchoClientHandler extends ChannelInboundHandlerAdapter {
private final static Logger LOGGER = LoggerFactory.getLogger(EchoClientHandler.class);
public static final EchoClientHandler INSTANCE = new EchoClientHandler();
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
int len = in.readableBytes();
byte[] arr = new byte[len];
in.getBytes(0, arr);
LOGGER.info("client received: " + new String(arr, StandardCharsets.UTF_8));
//读取完成之后,要主动释放掉该buffer便于回收
in.release();
}
}
上面的示例中,我们创建了一个
Bootstrap
对象,然后配置了workerGroup
,设置了客户端的 Channel 类型为NioSocketChannel
,并添加了一个ChannelInitializer
来配置处理器链。
Bootstrap
的group()
方法用于设置一个线程组,用于处理客户端的读写请求(workerGroup
)。
Bootstrap
的channel()
方法用于指定客户端 Channel 的类型,NioSocketChannel.class
表示使用 NIO 的 SocketChannel。
Bootstrap
的handler()
方法用于设置处理器链。在上面的示例中,我们添加了一个自定义的 EchiClientHandler 处理器。最后,我们通过调用
connect()
方法连接到服务器,并通过ChannelFuture
来处理连接的结果。在这个示例中,我们简单地使用sync()
方法来等待连接完成。最后通过
connect()
方法来连接到服务器。在连接建立后,我们可以通过ChannelFuture
来添加监听器来处理连接的结果和客户端关闭的操作。总之,
Bootstrap
类提供了配置客户端的方法和启动客户端的方法,可以方便地创建和管理客户端的网络服务。
效果演示
服务端启动效果
客户端启动效果
发送消息效果