netty Wiki · GitHubhttps://github.com/netty/netty/wiki
io.netty
netty-all
4.1.39.Final
服务端
new ServerBootstrap()
.group(new NioEventLoopGroup()) // 1
.channel(NioServerSocketChannel.class) // 2
.childHandler(new ChannelInitializer() { // 3
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new StringDecoder()); // 5
ch.pipeline().addLast(new SimpleChannelInboundHandler() { // 6
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println(msg);
}
});
}
})
.bind(8080); // 4
客户端
new Bootstrap()
.group(new NioEventLoopGroup()) // 1
.channel(NioSocketChannel.class) // 2
.handler(new ChannelInitializer() { // 3
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new StringEncoder()); // 8
}
})
.connect("127.0.0.1", 8080) // 4
.sync() // 5
.channel() // 6
.writeAndFlush(new Date() + ": hello world!"); // 7
提示
一开始需要树立正确的观念
把 channel 理解为数据的通道
把 msg 理解为流动的数据,最开始输入是 ByteBuf,但经过 pipeline 的加工,会变成其它类型对象,最后输出又变成 ByteBuf
把 handler 理解为数据的处理工序
工序有多道,合在一起就是 pipeline,pipeline 负责发布事件(读、读取完成...)传播给每个 handler, handler 对自己感兴趣的事件进行处理(重写了相应事件处理方法)
handler 分 Inbound 和 Outbound 两类
把 eventLoop 理解为处理数据的工人
工人可以管理多个 channel 的 io 操作,并且一旦工人负责了某个 channel,就要负责到底(绑定)
工人既可以执行 io 操作,也可以进行任务处理,每位工人有任务队列,队列里可以堆放多个 channel 的待处理任务,任务分为普通任务、定时任务
工人按照 pipeline 顺序,依次按照 handler 的规划(代码)处理数据,可以为每道工序指定不同的工人
通过Bootstrap(ServerBootstrap)初始化好连接之后 调用 channel()即返回ChannelFuture 对象,通过sync() 会阻塞等待直到当前线程获取到Channel,还可以使用回调的方式
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
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 io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import java.util.Scanner;
@Slf4j
public class CloseFutureClient {
private static NioEventLoopGroup group;
public static void main(String[] args) throws InterruptedException {
group = new NioEventLoopGroup();
ChannelFuture channelFuture = new Bootstrap()
.channel(NioSocketChannel.class)
.group(group)
.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new StringEncoder());
}
})
.connect("localhost", 8080);
Channel channel = channelFuture.sync().channel();
log.debug("{}", channel);
new Thread(() -> {
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.next();
if (line.equals("q")) {
channel.close();
channel.close();
// debug 与 close 不是同一线程执行放的无法保证顺序
// log.debug("close");
break;
}
channel.writeAndFlush(line);
}
}).start();
// * 推荐两种种方式关闭 channel
if (Math.random() > 0.5)
closeWithSync(channel);
else
closeWithListener(channel);
// channel 关闭但线程池没有结束?
// 获取 group 对象 调用 shutdownGracefully() [在 closeFuture 中添加方法调用]
}
// 通过向Channel中添加回调的方式执行操作
private static void closeWithListener(Channel channel) {
ChannelFuture closeFuture = channel.closeFuture();
closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
log.debug("关闭之后的操作");
group.shutdownGracefully();
}
});
}
// 同步阻塞方式获取Channel
private static void closeWithSync(Channel channel) throws InterruptedException {
channel.closeFuture().sync();
log.debug("close");
}
}
异步提升的是什么
单线程没法异步提高效率,必须配合多线程、多核 cpu 才能发挥异步的优势
异步并没有缩短响应时间,反而有所增加
合理进行任务拆分,也是利用异步的关键
简单使用
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.concurrent.DefaultPromise;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutionException;
@Slf4j
public class TestNettyPromise {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 此处仅作为线程池使用
NioEventLoopGroup executorGroup = new NioEventLoopGroup();
// 为 Promise 指定一个 线程
final DefaultPromise promise = new DefaultPromise(executorGroup.next());
//
Runnable task = () -> {
try {
log.debug("开始计算");
Thread.sleep(3000L);
if (Math.random() > 0.5)
throw new RuntimeException("Random number generator");
promise.trySuccess(80);
} catch (Exception e) {
e.printStackTrace();
promise.setFailure(e);
}
};
// 创建任务并提交给一个新的线程
new Thread(task, "MyThread").start();
log.debug("获取结果:{},", promise.get());
}
}
io.netty.channel.ChannelHandler
打个比喻,每个 Channel 是一个产品的加工车间,Pipeline 是车间中的流水线,ChannelHandler 就是流水线上的各道工序,而后面要讲的 ByteBuf 是原材料,经过很多工序的加工:先经过一道道入站工序,再经过一道道出站工序最终变成产品
ChannelHandler 使用了拦截过滤的设计模式核心 J2EE 模式 - 拦截过滤器 (oracle.com)https://www.oracle.com/java/technologies/intercepting-filter.html
ChannelInboundHandler 的参考示例 io.netty.channel.SimpleChannelInboundHandler 入站(读取消息的 Handler 注意 ByteBuf 的释放)
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
if (acceptInboundMessage(msg)) {
@SuppressWarnings("unchecked")
I imsg = (I) msg;
channelRead0(ctx, imsg);
} else {
release = false;
ctx.fireChannelRead(msg);
}
} finally {
if (autoRelease && release) {
ReferenceCountUtil.release(msg);
}
}
}
由于 Netty 中有堆外内存的 ByteBuf 实现,堆外内存最好是手动来释放,而不是等 GC 垃圾回收。
UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存即可
UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来回收内存
PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存
回收内存的源码实现,请关注下面方法的不同实现
protected abstract void deallocate();
Netty 这里采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口
每个 ByteBuf 对象的初始计数为 1
调用 release 方法计数减 1,如果计数为 0,ByteBuf 内存被回收
调用 retain 方法计数加 1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收
当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用谁来负责 release 呢?
不是我们想象的(一般情况下)
ByteBuf buf = ...
try {
...
} finally {
buf.release();
}
因为 pipeline 的存在,一般需要将 ByteBuf 传递给下一个 ChannelHandler,如果在 finally 中 release 了,就失去了传递性(当然,如果在这个 ChannelHandler 内这个 ByteBuf 已完成了它的使命,那么便无须再传递)
基本规则是,谁是最后使用者,谁负责 release,详细分析如下
起点,对于 NIO 实现来讲,在 io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#read 方法中首次创建 ByteBuf 放入 pipeline(line 163 pipeline.fireChannelRead(byteBuf))
入站 ByteBuf 处理原则
对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,这时无须 release
将原始 ByteBuf 转换为其它类型的 Java 对象,这时 ByteBuf 就没用了,必须 release
如果不调用 ctx.fireChannelRead(msg) 向后传递,那么也必须 release
注意各种异常,如果 ByteBuf 没有成功传递到下一个 ChannelHandler,必须 release
假设消息一直向后传,那么 TailContext 会负责释放未处理消息(原始的 ByteBuf)
出站 ByteBuf 处理原则
出站消息最终都会转为 ByteBuf 输出,一直向前传,由 HeadContext flush 后 release
异常处理原则
有时候不清楚 ByteBuf 被引用了多少次,但又必须彻底释放,可以循环调用 release 直到返回 true
如果使用了 ByteBuf 的池化(PooledByteBufAllocator)功能,需要在操作完成后将 ByteBuf 归还到池中。这可以通过调用 ReferenceCountUtil.release(msg) 方法来实现,该方法会自动处理归还操作。
ByteBuf的自动释放http://t.csdn.cn/YBHfj
io.netty.channel.SimpleChannelInboundHandler#channelRead 释放消息.
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
if (acceptInboundMessage(msg)) {
@SuppressWarnings("unchecked")
I imsg = (I) msg;
// 如果仅处理 String 类型的数据,我们仅需要继承 SimpleChannelInboundHandler
// 重写 channelRead0 即可
channelRead0(ctx, imsg);
} else {
release = false;
ctx.fireChannelRead(msg);
}
} finally {
if (autoRelease && release) {
ReferenceCountUtil.release(msg);
}
}
}
TailContext 释放未处理消息逻辑
// io.netty.channel.DefaultChannelPipeline#onUnhandledInboundMessage(java.lang.Object)
protected void onUnhandledInboundMessage(Object msg) {
try {
logger.debug(
"Discarded inbound message {} that reached at the tail of the pipeline. " +
"Please check your pipeline configuration.", msg);
} finally {
ReferenceCountUtil.release(msg);
}
}
具体代码
// io.netty.util.ReferenceCountUtil#release(java.lang.Object)
public static boolean release(Object msg) {
if (msg instanceof ReferenceCounted) {
return ((ReferenceCounted) msg).release();
}
return false;
}
一个EventLoop如果有group 一定是属于一个EventLoopGroup,EventLoop可能管理一个以上的Channel.
关闭 Netty 应用程序通常就像关闭通过 创建的所有 EventLoopGroup一样简单。它返回一个 future,当 EventLoopGroup 已完全终止并且属于该组的所有通道都已关闭时,它会通知您。shutdownGracefully()
netty/example/src/main/java/io/netty/example/echo at 4.1 · netty/netty · GitHubhttps://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/echo