Netty
Netty是一个异步的、甚于事件驱动的网络应用框架,用于快速开发可维护、高性能的网络服务器和客户端
1.使用
- 依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.39.Final</version>
</dependency>
- 服务端
public static void main(String[] args) {
// 服务启动器
new ServerBootstrap()
.group(new NioEventLoopGroup())
// TCP SOCKET通道为NioServerSocketChannel
// UDP DatagramChannel
.channel(NioServerSocketChannel.class)
// 当客户端注册读写事件时 初始化Handler
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg);
}
});
}
})
.bind(8080);
}
- 客户端
public static void main(String[] args) throws Exception {
// 启动类
new Bootstrap()
// 添加EventLoop
.group(new NioEventLoopGroup())
// 客户端channel实现
.channel(NioSocketChannel.class)
// 处理器
.handler(new ChannelInitializer<NioSocketChannel>() {
/**
* 连接后被调用
* @param ch
* @throws Exception
*/
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
}
})
.connect(new InetSocketAddress("localhost", 8080))
.sync()
.channel()
.writeAndFlush("Netty");
}
2.EventLoop
EventLoop本质是一个单线程执行器 (同时维护了一个Selector)。里面有run方法处理Channel上源源不断的io事件
它的继承关系比较复杂
- 一条线是继承自j.u.c.ScheduledExecutorService因此包含了线程池中所有的方法
- 另一条线是继承自netty自己的OrderedEventExecutor
-- 提供了boolean InEventloop(Thread thread)方法判断一个钱程是否属于此EventLoop
-- 提供了parent方法来看看自己属于哪个EventoopGroup
事件循环组
EventoopGroup是一组EventLoop, Channel 一般会调用EventLoopGroup的register方法来绑定其中一个EventLoop
后续这个Channel上的io事件都由此EventLoop来处理(保证了io事件处理时的线程安全)
- 继承自netty自己的EventExecutorGroup
-- 实现了Iterable接口提供遍历EventLoop的能力
-- 有next方法获取集合中下一个EventLoop
EventoopGroup里线程循环处理channel
2.1 基本使用
// 可以处理io 普通 定时任务
// 可以指定线程数 默认cpu * 2
EventLoopGroup event = new NioEventLoopGroup();
// 可以处理 普通 定时任务
EventLoopGroup eventLoopGroup1 = new DefaultEventLoop();
// 获取下一个事件
event.next();
// 执行普通任务
event.next().submit(() -> {
System.out.println("123");
});
// 执行定时任务 初始执行时间0s 间隔1s 单位秒
event.next().scheduleAtFixedRate(() -> {
System.out.println("321");
}, 0, 1, TimeUnit.SECONDS);
2.2 分工
new ServerBootstrap()
/**
* 可以划分为boss 和 worker
* boss负责ServerSocketChannel上的accept worker
* worker负责SocketChannel上的读写
*/
.group(new NioEventLoopGroup(), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg);
}
});
}
})
.bind(8080);
2.3 处理长handler
EventLoopGroup group = new DefaultEventLoopGroup();
new ServerBootstrap()
.group(new NioEventLoopGroup(), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
/**
* 处理handler时间比较长
* @param ch
* @throws Exception
*/
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(group, "name", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg);
// 传递给下一个handler
ctx.fireChannelRead(msg);
}
}).addLast(group, "name1", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg);
}
});
}
})
.bind(8080);
3.channel
channel的主要作用
- close 可以用来关闭channel
- closeFuture 用来处理channel的关闭
-- sync方法作用是同步等待channel关闭
-- 而addListener方法是异步等待channel关闭 - pipeline() 方法添加处理器
- write 方法将数据写入channel缓冲区,但不发送
- flush 立刻发送
- writeAndFlush() 方法将数据写入并刷出
3.1 channelFuture
- 异步问题
public static void main(String[] args) throws Exception {
ChannelFuture channelFuture = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
/**
* 连接后被调用
* @param ch
* @throws Exception
*/
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
}
})
// 异步非阻塞 main调用 nio线程连接
.connect(new InetSocketAddress("localhost", 8080));
// 1.阻塞一下 以获取连接到的channel
// channelFuture.sync();
// Channel channel = channelFuture.channel();
// channel.writeAndFlush("123");
// 2.异步处理结果
channelFuture.addListener(new ChannelFutureListener() {
/**
* 在nio线程里连接成功后调用
* @param future
* @throws Exception
*/
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Channel channel = future.channel();
channel.writeAndFlush("123");
}
});
}
- 异步问题
NioEventLoopGroup group = new NioEventLoopGroup();
new Thread( () -> {
Scanner scanner = new Scanner(System.in);
while(true) {
String line = scanner.nextLine();
if ("q".equals(line)) {
// 关闭也是异步的 在这里不能处理关闭操作
channel.close();
break;
}
channel.writeAndFlush(line);
}
}, "name").start();
/**
* 处理关闭操作
* 1.同步
* 2.异步
*/
ChannelFuture closeFuture = channel.closeFuture();
closeFuture.sync();
closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
group.shutdownGracefully();
}
});
4.Future Promise
在异步处理时,经常用到这两个接口
首先要说明netty中的Future与jdk中的Future同名,但是是两个接口,netty 的Future继承自jdk的Future
而Promise又对netty Future进行了扩展
- jdk Future只能同步等待任务结束(或成功、或失败)才能得到结果
- netty Future可以同步等待任务结束得到结果,也可以异步方式得到结果,但都是要等任务结束
- netty Promise不仅有netty Future的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器
4.1 Future
public static void main(String[] args) throws Exception {
NioEventLoopGroup group = new NioEventLoopGroup();
EventLoop eventLoop = group.next();
Future<Integer> future = eventLoop.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return null;
}
});
// 同步获取结果
future.get();
// 异步
future.addListener(new GenericFutureListener<Future<? super Integer>>() {
@Override
public void operationComplete(Future<? super Integer> future) throws Exception {
future.getNow();
}
});
}
4.2 Promise
public static void main(String[] args) throws Exception {
EventLoop loop = new NioEventLoopGroup().next();
DefaultPromise<Integer> promise = new DefaultPromise<Integer>(loop);
new Thread(() -> {
// 设置结果
promise.setSuccess(100);
}).start();
// 获取结果
promise.get();
}
5.Handler Pipeline
ChannelHandler用来处理Channel上的各种事件,分为入站、出站两种。
所有ChannelHandler被连成一串,就是Pipeline
- 入站处理器通常是ChannellnboundHandlerAdapter的子类,主要用来读取客户端数据,写回结果
- 出站处理器通常是ChannelOutboundHandlerAdapter的子类,主要对写回结果进行加工
打个比喻,每个Channel是一个产品的加工车间,Pipeline 是车间中的流水线
ChannelHandler 就是流水线上的各道工序,ByteBuf是原材料,经过很多工序的加工:先经过一道道入站工序,再经过一道道出站工序最终变成产品
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
/**
* 处理器顺序
* head -> h1 -> h2 -> tail
*/
pipeline.addLast("h1", new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
String name = buf.toString();
// 传递给下一个数据
super.channelRead(ctx, name);
}
});
pipeline.addLast("h2", new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println();
super.channelRead(ctx, msg);
// 向前去找出栈的处理器
ctx.writeAndFlush(msg);
// 向后去找出栈的处理器
ch.writeAndFlush(ctx.alloc().buffer().writeBytes("write".getBytes(StandardCharsets.UTF_8)));
}
});
// 只有写的时候才触发
pipeline.addLast("h3", new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
System.out.println();
super.write(ctx, msg, promise);
}
});
}
})
.bind(8080);
}
- 快速测试
public static void main(String[] args) {
ChannelInboundHandlerAdapter h1 = new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
super.channelRead(ctx, msg);
}
};
ChannelOutboundHandlerAdapter h2 = new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
super.write(ctx, msg, promise);
}
};
EmbeddedChannel channel = new EmbeddedChannel(h1, h2);
// 入栈
channel.writeInbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello world".getBytes(StandardCharsets.UTF_8)));
// 出栈
channel.writeOutbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello world".getBytes(StandardCharsets.UTF_8)));
}
6.ByteBuf
6.1 创建
public static void main(String[] args) {
// 创建 传参可以给初始大小
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
// 基于堆的
ByteBuf buf1 = ByteBufAllocator.DEFAULT.heapBuffer();
/**
* 基于直接内存
* 创建销毁代价高 读写性能高 配合池化
* GC压力小 要注意及时释放
*/
ByteBuf buf2 = ByteBufAllocator.DEFAULT.directBuffer();
}
6.2 池化
池化的最大意义在于可以重用ByteBuf,优点有
- 没有池化,则每次都得创建新的ByteBuf实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加GC压力
- 有了池化,则可以重用池中ByteBuf实例,并且采用了与jemalloc类似的内存分配算法提升分配效率
- 高并发时,池化功能更节约内存,减少内存溢出的可能
池化功能是否开启,可以通过下面的系统环境变量来设置
-Dio. netty. allocator . type= - 4.1以后,非Android平台默认启用池化实现,Android平台启用非池化实现
- 4.1之前,池化功能还不成熟,默认是非池化实现
- 判断是否池化
ByteBuf buf2 = ByteBufAllocator.DEFAULT.directBuffer();
// Pooled开头就是池化
System.out.println(buf1.getClass());
6.3 组成
- 容量
- 最大容量
- 读写指针
6.4 写入
6.5 扩容
扩容规则是
- 如何写入后数据大小未超过512,则选择下一个16的整数倍,例如写入后大小为12,则扩容后capacity是16
- 如果写入后数据大小超过512,则选择下一一个2^n, 例如写入后大小为513,则扩容后capacity是2^10=1024 (2^9=512 已经不够了)
- 扩容不能超过max capacity会报错
6.5 读取
// 一个字节一个字节读 读过的废弃
buf2.readByte();
// 标记重复读取
buf2.markReaderIndex();
buf2.readInt();
buf2.resetReaderIndex();
7.retain release
由于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
8.slice 零拷贝
零拷贝的体现之一,对原始ByteBuf进行切片成多个ByteBuf,切片后的ByteBuf并没有发生内存复制
还是使用原始ByteBuf的内存,切片后的ByteBuf维护独立的read, write 指针
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
buf.writeBytes(new byte[]{'1','2'});
// 获取前五个 且不能再修改 原来release 也不能获得切片
ByteBuf buf3 = buf.slice(0,5);
// 防止原来的释放内存
buf.retain();
9.duplicate
[零拷贝]的体现之一,就好比截取了原始 ByteBuf所有内容,并且没有max capacity的限制,也是与原始
ByteBuf使用同-块底层内存,只是读写指针是独立的
10.copy
会将底层内存数据进行深拷贝[因此无论读写,都与原始ByteBuf无关
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)