Netty 简单的入门使用案例
1. 组件
1.1 EventLoop
EventLoop 事件循环对象
EventLoop本质上是一个单线程执行器(同时维护了一个Selector),通过run方法来处理 Channel 上源源不断的 IO 事件。
它的继承关系是比较复杂的:
- 继承了 j.u.c.ScheduledExecutorService 因此包含了线程池中所有的方法
- 继承 Netty 自己的 io.netty.util.concurrent.OrderedEventExecutor
- 提供了 boolean inEventLoop(Thread thread) 来判断一个线程是否属于当前 EventLoop
- 提供了 EventExecutorGroup parent() 来获取自己归属的 EventLoopGroup
EventLoopGroup
EventLoopGroup (事件循环组) 绑定了一组的 EventLoop,Channel 通过调用 EventLoopGroup 的 register() 方法来绑定一个 EventLoop 来处理事件,
绑定后的所有事件都将由这同一个 EventLoop 来处理。保证了 IO 事件处理时的线程安全问题。
- 继承 io.netty.util.concurrent.EventExecutorGroup
- 实现了 Iterable 可以使用 for 循环遍历 EventLoop
- 可以使用 next() 方法获取集合中的下一个 EventLoop
1.1.1 简单地创建案例
// 内部创建了两个 EventLoop, 每个 EventLoop 维护一个线程
DefaultEventLoopGroup group = new DefaultEventLoopGroup(2);
System.out.println(group.next());
System.out.println(group.next());
System.out.println(group.next());
输出结果
io.netty.channel.nio.NioEventLoop@7085bdee
io.netty.channel.nio.NioEventLoop@1ce92674
io.netty.channel.nio.NioEventLoop@7085bdee
优雅的关闭EventLoopGroup
使用shutdownGracefully()
可以实现优雅关闭, 当命令执行后 EventLoopGroup 会切换到关闭状态停止新任务的进入,
当现有任务都执行完毕后关闭,停止线程运行。从而保证应用整体是在正常有序的情况下退出的。
1.2 NioEventLoop 处理 IO 事件
Netty 创建服务端和客户端的基本步骤都是一样的,多写几遍熟练之后就不会觉得麻烦了
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
/**
* @ClassName: EventLoopServer
* TODO
* @date: 8/28/2021 2:07 PM
* @version: v1.0
*/
@Slf4j
public class EventLoopServer {
public static void main(String[] args) {
// 细分2: 非NIO Worker。如果某个 handler 执行时间比较长,可以抽离出 nio Worker,来防止nio性能受到损耗
EventLoopGroup nonNioGroup = new DefaultEventLoopGroup(2);
new ServerBootstrap()
/* group 可以使用两个来更细致的区分职能
* 细分1 : group(EventLoopGroup boss, EventLoopGroup worker)
* boss 只负责 ServerSocketChannel 上的 accept 事件, worker 只负责 SocketChannel 的读写事件
* * 一个EventLoop可以管理多个 channel,但是一个 channel 只能对应一个 EventLoop
*/
.group(new NioEventLoopGroup(), new NioEventLoopGroup(2))
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast("handler1", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
log.debug(buf.toString(StandardCharsets.UTF_8));
// 配置多个处理器需要手动触发下一个处理器
ctx.fireChannelRead(msg);
}
});
ch.pipeline().addLast(nonNioGroup, "handler2", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
log.debug(buf.toString(StandardCharsets.UTF_8));
}
});
}
})
.bind(8080);
}
}
package org.example.netty.demo.eventloop;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
/**
* @ClassName: EventLoopClient
* TODO
* @date: 8/28/2021 2:24 PM
* @version: v1.0
*/
@Slf4j
public class EventLoopClient {
public static void main(String[] args) throws InterruptedException {
Channel channel = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder(StandardCharsets.UTF_8));
}
})
.connect("localhost", 8080)
.sync()
.channel();
System.out.println(channel);
System.out.println();
}
}
1.2.1 Handler 是如何做到切换下一个执行的?
触发下一个Handler需要通过 ChannelHandlerContext 的 fireChannelRead(final Object msg)
方法触发
@Override
public ChannelHandlerContext fireChannelRead(final Object msg) {
invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg);
return this;
}
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
// 获取下一个执行的 EventExecutor
EventExecutor executor = next.executor();
// 通过 inEventLoop() 方法判断当前线程和下一个执行线程是否是同一个
if (executor.inEventLoop()) {
// 如果是同一个,直接调用执行下一个
next.invokeChannelRead(m);
} else {
// 不是,将执行函数交给下一个执行线程去处理。(换人执行)
// EventLoop 继承了JUC线程池的方法,所以使用开一个新任务的方式执行
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRead(m);
}
});
}
}
1.3 Channel
channel 的主要作用
- close() 可以用来关闭 channel
- closeFuture() 用来处理 channel 的关闭
- sync 方法作用是同步等待 channel 关闭
- 而 addListener 方法是异步等待 channel 关闭
- pipeline() 方法添加处理器
- write() 方法将数据写入
- writeAndFlush() 方法将数据写入并刷出
Channel 连接和写出
public static void main(String[] args) throws InterruptedException {
// 带有 future 或 promise 的类都是配合异步调用使用的
NioEventLoopGroup singleGroup = new NioEventLoopGroup();
ChannelFuture channelFuture = new Bootstrap()
.group(singleGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new StringEncoder());
}
})
// connect() 是一个异步方法, 通过 main 线程去调用另一个线程来执行,所以不能马上得到正确结果
.connect("localhost", 8080);
/* 方案一:通过 sync() 方法让 main 线程阻塞,等待 connect 方法的线程执行完毕后再唤醒 */
Channel channel = channelFuture.sync().channel();
// write() 将数据写入缓冲区中, 所以目标在这步还收不到数据
channel.write("msg");
// flush() 将缓冲区中的数据刷出
channel.flush();
// channel.writeAndFlush() 写出并刷新
/* 方案二: 通过 addListener() 方法,来异步处理结果 */
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Channel c = future.channel();
c.writeAndFlush("hello");
}
});
}
Channel 关闭
public static void main(String[] args) throws InterruptedException {
// 带有 future 或 promise 的类都是配合异步调用使用的
NioEventLoopGroup singleGroup = new NioEventLoopGroup();
ChannelFuture channelFuture = new Bootstrap()
.group(singleGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new StringEncoder());
}
})
// connect() 是一个异步方法, 通过 main 线程去调用另一个线程来执行,所以不能马上得到正确结果
.connect("localhost", 8080);
Channel channel = channelFuture.sync().channel();
log.debug("{}", channel);
new Thread(() -> {
Scanner sc = new Scanner(System.in);
while (true) {
String str = sc.nextLine();
if ("q".equals(str)) {
// close() 也是一个异步操作
channel.close();
break;
}
channel.writeAndFlush(str);
}
}).start();
// 两种解决方案: 1). sync() 同步等待结果; 2). closeFuture() 获取future对象,addListener()
ChannelFuture closeFuture = channel.closeFuture();
closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
log.debug("关闭Channel");
/** channel 关闭了java线程还没有结束,是因为 NioEventLoopGroup 的线程还没有关闭 */
// 优雅关闭 NioEventLoopGroup
singleGroup.shutdownGracefully();
}
});
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现