尚硅谷Netty笔记
Netty 是什么
Netty是一个异步的、基于事件驱动的网络应用·框架,用于快速开发可维护、高性能的网络服务器和客户端
Netty为什么传输快
Netty的传输快其实也是依赖了NIO的一个特性——零拷贝。我们知道,Java的内存有堆内存、栈内存和字符串常量池等等,其中堆内存是占用内存空间最大的一块,也是Java对象存放的地方,一般我们的数据如果需要从IO读取到堆内存,中间需要经过Socket缓冲区,也就是说一个数据会被拷贝两次才能到达他的的终点,如果数据量大,就会造成不必要的资源浪费。
Netty针对这种情况,使用了NIO中的另一大特性——零拷贝,当他需要接收数据的时候,他会在堆内存之外开辟一块内存,数据就直接从IO读到了那块内存中去,在netty里面通过ByteBuf可以直接对这些数据进行直接操作,从而加快了传输速度。
Netty简单例子
服务端
import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; 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.handler.codec.string.StringDecoder; public class HelloServer { public static void main(String[] args) { //1、ServerBootstrap:启动器,复制组装 netty组件,启动服务器 new ServerBootstrap() //2、一个Netty服务端启动时,通常会有两个NioEventLoopGroup:一个是监听线程组,主要是监听客户端请求,另一个是工作线程组,主要是处理与客户端的数据通讯。(这里只创建了一个线程组) //这里不断的循环,检查是否有新的事件 .group(new NioEventLoopGroup()) //3、选择服务器的 ServerSocketChannel 实现 .channel(NioServerSocketChannel.class)//还有OIO(BIO) //4、编写工作线程的具体业务代码 .childHandler( //5、Channel:代表和客户端进行数据读写的通道 Initializer:初始化,负责添加别的 handler new ChannelInitializer<NioSocketChannel>() { @Override //6、添加具体的 handler protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception { nioSocketChannel.pipeline().addLast(new StringDecoder()); //将 ByteBuf 转换为字符串 (ByteBuf 与 ByteBuffer 差不多,ByteBuf 在 ByteBuffer 上做了增强) nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){ //自定义 handler @Override //读事件触发后执行,也就是将 ByteBuf 转换为字符串后触发 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // 打印上一步转换好的字符串 System.out.println(msg); } }); } //7、绑定监听端口 }).bind(8080); } }
客户端
import io.netty.bootstrap.Bootstrap; 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 java.net.InetSocketAddress; public class HelloClient { public static void main(String[] args) throws Exception{ //1、启动类 new Bootstrap() //2、添加 EventLoop .group(new NioEventLoopGroup()) //3、选择客户端 channel 实现 .channel(NioSocketChannel.class) //4、添加处理器 .handler(new ChannelInitializer<NioSocketChannel>() { @Override //载链接建立后被调用 protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception { //编码,将字符串转换为 ByteBuf nioSocketChannel.pipeline().addLast(new StringEncoder()); } }) //5、连接到服务器 .connect(new InetSocketAddress("localhost",8080)) .sync()//阻塞方法,直到建立连接 .channel()//代表连接对象 //6、向服务端发送数据 .writeAndFlush("hello,world"); } }
组件
1、 EventLoop
事件循环对象
EventLoop本质是一个单线程执行器(同时维护了一个Selector),里面有run方法处理 Channel 上源源不断的 io 事件。
它的继承关系比较复杂:
- 一条线是继承自 j.u.c.ScheduledExecutorService ()因此包含了线程池中所有的方法;
- 另一条线程是继承 netty 自己的 OrderedEventExecutor。
事件循环组
EventLoopGroup 是一组 EventLoop,Channel 一般会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop,后续这个 Channel 上的 io 事件都由此 EventLoop 来处理(保证了 io 事件处理时的线程安全)。
- 继承自 netty 自己的 EventExecutorGroup
- 实现了 iterable 接口提供遍历 EventLoop 的能力
- 另有 next 方法获取集合中下一个 EventLoop
EventLoop 普通-定时任务
import io.netty.channel.DefaultEventLoopGroup; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; @Slf4j public class TestEventLoop { public static void main(String[] args) { //1、创建事件循环组 EventLoopGroup group = new NioEventLoopGroup(2); //能处理 io事件、普通任务、定时任务,如果不传参数,默认有8个线程 //EventLoopGroup eventExecutors = new DefaultEventLoopGroup();//能处理 普通任务、定时任务,如果不传参数,默认有8个线程 //2、获取下一个事件循环对象 System.out.println(group.next());//第一个线程 System.out.println(group.next());//第二个线程 System.out.println(group.next());//第一个线程 //3、执行普通任务 /* group.next().submit(() -> { try { Thread.sleep(1000); log.info("普通任务"); }catch (Exception e){ e.printStackTrace(); } }); */ //4、执行定时任务 //参数一:执行的任务 //参数二:等待多久后后执行 //参数三:每隔多久执行一次 //参数四:时间单位 group.next().scheduleAtFixedRate(() -> { log.info("定时任务"); },3,5, TimeUnit.SECONDS); log.info("主线程"); } }
EventLoop分工细化
服务端
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.Charset; @Slf4j public class EventLoopServer { public static void main(String[] args) { EventLoopGroup group = new DefaultEventLoopGroup();//默认为8个线程 new ServerBootstrap() //线程组一(new NioEventLoopGroup()):只负责 ServerSocketChannel 上的 accept 事件。没必要设置有多少个线程,不设置默认8个。 //线程组二(new NioEventLoopGroup(2)):只负责 SocketChannel 上的读写,可以设置有多少个线程,不设置默认为8个。 .group(new NioEventLoopGroup(),new NioEventLoopGroup(2)) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception { //参数一:handler 的名称 //参数二:触发事件后执行的业务逻辑 nioSocketChannel.pipeline().addLast("handler1",new ChannelInboundHandlerAdapter(){ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf)msg; log.info(buf.toString(Charset.forName("UTF-8"))); //让消息传递给下一个 handler,如果不调用此方法,消息传到 handler1 就断了,不会将消息传到 handler2 ctx.fireChannelRead(msg); } //多创建一个 handler //参数一:如果设置了,那么就不使用上面的 NioEventLoopGroup 线程组来处理读写,而是使用所设置的线程组(这里设置的是使用 DefaultEventLoopGroup 线程组) }).addLast(group,"handler2",new ChannelInboundHandlerAdapter(){ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf)msg; log.info(buf.toString(Charset.forName("UTF-8"))); } }); } }) .bind(8080); } }
客户端
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 java.net.InetSocketAddress; public class EventLoopClient { public static void main(String[] args) throws Exception{ String abc="hello,world"; //1、启动类 Channel channel = (Channel) new Bootstrap() //2、添加 EventLoop .group(new NioEventLoopGroup()) //3、选择客户端 channel 实现 .channel(NioSocketChannel.class) //4、添加处理器 .handler(new ChannelInitializer<NioSocketChannel>() { @Override //载链接建立后被调用 protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception { //编码,将字符串转换为 ByteBuf nioSocketChannel.pipeline().addLast(new StringEncoder()); } }) //5、连接到服务器 .connect(new InetSocketAddress("localhost", 8080)) .sync()//阻塞方法,直到建立连接 .channel() .writeAndFlush(abc); } }
Channel
channel的主要作用
- close() 可以用来关闭 channel
- closeFuture() 用来处理 channel 的关闭
- sync 方法作用是同步等待 channel 关闭
- 而 addListener 方法是异步等待 channel 关闭
- pipeline() 方法添加处理器
- write() 方法将数据写入
- writeAndFlush() 方法将数据写入并刷出
细分客户端
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 java.net.InetSocketAddress; public class EventLoopClient { public static void main(String[] args) throws Exception{ //带有 Future、Promise 的类型都是和异步方法配套使用,用来处理结果 ChannelFuture channelFuture = new Bootstrap() .group(new NioEventLoopGroup()) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception { nioSocketChannel.pipeline().addLast(new StringEncoder()); } }) //1、连接到服务器 //这个是异步非阻塞的,它是由 NioEventLoopGroup 线程组去执行与服务端连接 .connect(new InetSocketAddress("localhost", 8080)); //2.1 使用 sycn 方法同步处理结果 //如果不调用阻塞式的 sync()方法,那么客户端还没有与服务端建立起连接就调用 channel 此时的 channel 为空 /*channelFuture.sync();//阻塞,知道建立起连接 Channel channel = channelFuture.channel(); channel.writeAndFlush("hello,world");*/ //2.2 使用 addListener(回调对象) 方法异步处理结果。(他把等待建立,建立成功,处理结果,全都交给另外一个线程干,addListener 作用是:将来连接成功后需要进行什么操作) channelFuture.addListener(new ChannelFutureListener() { @Override //在 nio 线程连接建立好之后,会调用 operationComplete(ChannelFuture channelFuture) //参数中的 channelFuture 对象就是上面调用 addListener 的 channelFuture 对象 public void operationComplete(ChannelFuture channelFuture) throws Exception { Channel channel = channelFuture.channel(); channel.writeAndFlush("hello world"); } }); } }
进一步优化客户端
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 java.net.InetSocketAddress; import java.util.Scanner; public class EventLoopClient { public static void main(String[] args) throws Exception{ NioEventLoopGroup group = new NioEventLoopGroup(); ChannelFuture channelFuture = new Bootstrap() .group(group)//把 new NioEventLoopGroup() 放到外面创建,以便后续关闭线程 .channel(NioSocketChannel.class) .handler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception { nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); nioSocketChannel.pipeline().addLast(new StringEncoder()); } }) .connect(new InetSocketAddress("localhost", 8080)); Channel channel = channelFuture.sync().channel(); new Thread(() -> { while(true) { Scanner sc = new Scanner(System.in); String str = sc.nextLine(); channel.writeAndFlush(str); if (str.equals("exit")) { channel.close(); //close 是异步操作,也就是说 close() 还没执行完就往下面运行了。可以使用 CloseFuture 对象 break; } } }).start(); //获取 CloseFuture 对象, 1) 同步处理关闭; 2) 异步处理关闭 ChannelFuture closeFuture = channel.closeFuture(); //1、同步 /*closeFuture.sync(); //channel 阻塞到关闭后 group.shutdownGracefully(); System.out.println("channel 已经关闭");*/ //2、异步 closeFuture.addListener((ChannelFutureListener) -> { //channel 关闭后执行 System.out.println("channel 已经关闭"); group.shutdownGracefully();//停止接受新的任务,把现有的任务运行完后关闭 }); } }
Future & Promise
在异步处理时,经常用到这两个接口
首先要说明 netty 中的 Future 与 jdk 中的 Future 同名,但是是两个接口,netty 的 Future 继承自 jdk 的 Future,而 Promise 又对 netty Future 进行了扩展。
- jdk Future 只能同步等待任务结束(或成功、或失败)才能得到结果;
- netty Future 可以同步等待任务结束得到结果,也可以异步方式得到结果,但都是要等任务结束;
- netty Promise 不仅有 netty Future 的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器
功能/名称 | JDK Future | netty Future | Promise |
cancel | 取消任务 | ||
isCanceled | 任务是否取消 | ||
isDone | 任务是否完成,不能区分成功失败 | ||
get | 获取任务结果,阻塞等待 | ||
getNow | 获取任务结果,非阻塞,还未产生结果时返回 null | ||
await | 等待任务结束,如果任务失败,不会抛异常,而是通过 isSuccess判断 | ||
sync | 等待任务结束,如果任务失败,抛出异常 | ||
isSuccess | 判断任务是否成功 | ||
cause | 获取失败信息,非阻塞,如果没有失败,返回null | ||
addLinstener | 添加回调,异步接受结果 | ||
setSuccess | 设置成功结果 | ||
setFailure | 设置失败结果 |
JDK Future 演示
import lombok.extern.slf4j.Slf4j; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @Slf4j public class TestJdkFuture { public static void main(String[] args) throws Exception{ //1、线程池 ExecutorService service = Executors.newFixedThreadPool(2); //2、提交任务 非阻塞 execute是使用Runnable五返回结果 submit是使用Callable有返回结果 Future<Integer> future = service.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { log.info("正在执行"); Thread.sleep(1000); return 50; } }); //3、主线程通过 future 来获取结果 获取任务结果,阻塞等待 log.info("执行完毕 {}",future.get()); } }
Netty Future
import io.netty.channel.EventLoop; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.Callable; @Slf4j public class TestJdkFuture { public static void main(String[] args) throws Exception{ //线程组,默认有8个线程 NioEventLoopGroup group = new NioEventLoopGroup(); //获取一个线程 EventLoop eventLoop = group.next(); Future<Integer> future = eventLoop.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { log.info("执行计算"); Thread.sleep(1000); return 50; } }); //同步,阻塞等待 /*log.info("执行完毕 {}",future.get()); group.shutdownGracefully();*/ //异步 future.addListener(new GenericFutureListener<Future<? super Integer>>() { @Override public void operationComplete(Future<? super Integer> future) throws Exception { log.info("接收结果:{}" , future.getNow()); //执行任务完毕后关闭线程,关闭线程也需要时间 group.shutdownGracefully(); } }); } }
Promise 演示
Promise是可写的 Future, Future自身并没有写操作相关的接口, Netty通过 Promise对 Future进行扩展,用于设置IO操作的结果。
import io.netty.channel.EventLoop; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.util.concurrent.DefaultPromise; import lombok.extern.slf4j.Slf4j; @Slf4j public class TestNettyPromise { public static void main(String[] args) { //1、准备 EventLoop 对象 EventLoop eventLoop = new NioEventLoopGroup().next(); //2、可以主动创建 promise 对象,他是一个结果容器 DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop); new Thread(() -> { //3、任意一个线程执行计算,计算完毕后向 promise 填充结果 System.out.println("开始计算"); try{ Thread.sleep(1000); int i = 10 / 0; //设置成功结果 promise.setSuccess(80); }catch (Exception e){ e.printStackTrace(); //设置失败结果 promise.setFailure(e); } }).start(); //4、接收结果的线程 log.info("等待结果...."); try { //promise.get() 阻塞式 log.info("结果是:{}", promise.get()); }catch (Exception e){ e.printStackTrace(); } } }
Handler & Pipeline
ChannelHandler 用来处理 Channel 上的各种事件,分别为入站、出战两种。所有 ChannelHandler 被连成一串,就是 Pipeline。
- 入站处理器通常是 ChannelInboundHandlerAdapter 的子类,主要用来读取客户端数据,写回结果
- 出战处理器通常是 ChannelOutboundHandlerAdapter 的子类,主要对写回结果进行加工
打个比喻,每个 Channel 是一个产品的加工车间,Pipeline 是车间中的流水线,ChannelHandler 就是流水线上的各道工序,而后面要讲的ByteBuf 是原材料,经过一道道入站工序,再经过一道道出站工序最终变成产品。
Handler演示
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 io.netty.util.concurrent.DefaultPromise; import lombok.Data; import lombok.extern.slf4j.Slf4j; import java.nio.charset.Charset; @Slf4j public class TestPipeline { public static void main(String[] args) { new ServerBootstrap() .group(new NioEventLoopGroup()) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception { //1、通过 channel 拿到 pipeline ChannelPipeline pipeline = nioSocketChannel.pipeline(); //2、添加处理器 //入站:h1、h2、h3 出战:h4、h5、h6 执行顺序:h1 -> h2 -> h3 -> h6 -> h5 -> h4 (入站按顺序,出站反序) pipeline.addLast("h1",new ChannelInboundHandlerAdapter(){ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { log.info("1"); ByteBuf buf = (ByteBuf)msg; String name = buf.toString(Charset.defaultCharset()); //将数据传到下一个 Read 的 Handler super.channelRead(ctx,name); } }); pipeline.addLast("h2",new ChannelInboundHandlerAdapter(){ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { log.info("2"); Student student = new Student(msg.toString()); super.channelRead(ctx,student); } }); //出站 pipeline.addLast("h7", new ChannelOutboundHandlerAdapter() { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { log.info("7"); super.write(ctx, msg, promise); } }); pipeline.addLast("h3",new ChannelInboundHandlerAdapter(){ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { log.info("3"); log.info("msg:{}",msg); //如果没有写出,只会触发读,不会触发写 // 也就是说只会触发入站,不会触发出战 //nioSocketChannel.writeAndFlush(ctx.alloc().buffer().writeBytes("server...".getBytes())); //使用 ctx 调用 writeAndFlush 会往此方法上面找出站方法,所以执行顺序是:h1 -> h2 -> h3 -> h7 ctx.writeAndFlush(ctx.alloc().buffer().writeBytes("server ...".getBytes())); } }); pipeline.addLast("h4", new ChannelOutboundHandlerAdapter() { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { log.info("4"); super.write(ctx, msg, promise); } }); pipeline.addLast("h5", new ChannelOutboundHandlerAdapter() { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { log.info("5"); super.write(ctx, msg, promise); } }); pipeline.addLast("h6", new ChannelOutboundHandlerAdapter() { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { log.info("6"); super.write(ctx, msg, promise); } }); } }).bind(8080); } @Data static class Student{ public String name; public Student() { } public Student(String name) { this.name = name; } } }
ByteBuf
1)直接内存 VS 堆内存
可以使用下面的代码来创建池化基于堆的 ByteBuf
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);
也可以使用下面的代码来创建池化基于直接内存的 ByteBuf
ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10);
- 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化更能一起用;
- 直接内存堆 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放。
2)池化 VS 非池化
池化的最大意义在于可以重用 ByteBuf,优点:
- 没有池化,则每次都得创建新的 ByteBuf 实例,这个操作堆直接内存代价昂贵,就算是堆内存,也会增加 GC 压力;
- 有了池化,则可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率;
- 高并发时,池化功能更节约内存,减少内存溢出的可能
池化功能是否开启,可以通过下面的系统环境变量来设置
-Dio.netty.allocator.type={unpooled | pooled} (关闭 | 开启)
- 4.1 以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现;
- 4.1 之前,池化功能还不成熟,默认是非池化实现。
3)创建 ByteBuf
池化:
ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(); //池化,直接内存 ByteBuf buf2 = ByteBufAllocator.DEFAULT.heapBuffer(); //池化,堆内存
非池化:如果操作后,再创建
ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(); //非池化,直接内存 ByteBuf buf2 = ByteBufAllocator.DEFAULT.heapBuffer(); //非池化,堆内存
4) 写入
还有两个:
方法签名 | 含义 | 备注 |
writeBytes(ByteBuffer src) | 写入 nio 的 ByteBuffer | |
int writeCharSequence(CharSequence sequence, Charset charset) | 写入字符串 |
注意:
- 这些方法未指明返回值的,其返回值都是 ByteBuf,意味着可以链式调用;
- 网络船输,默认习惯是 Big Endian。
5)扩容
扩容规则:
- 如果写入后数据大小未超过 512,则选择下一个 16 的整数倍,例如写入后大小为12,则扩容后 capacity 是16;
- 如果写入后数据大小超过 512,则选择下一个 2^n ,例如写入后大小为 513,则扩容后 capacity 是 2^n=1024;
- 扩容不能超过 max capacity 会报错。
6)读取
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(1024); //池化,直接内存 byte[] b= {1,2,3,4,'a'}; buf.writeBytes(b); for (int i=0;i<b.length;i++){ System.out.println(buf.readByte()); }
读过的内容就属于废弃部分,再度只能读那些尚未读取的部分,如果需要重复读取一个数据,可以再 read 前先做个标记 mark,这时要重复读取,重置到标记位置 reset
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(1024); //池化,直接内存 byte[] b= {1,2,3,4,'a'}; buf.writeBytes(b); buf.markReaderIndex(); System.out.println(buf.readByte()); // 1 buf.resetReaderIndex(); System.out.println(buf.readByte()); // 1 buf.resetReaderIndex(); System.out.println(buf.readByte()); // 1
7)retain & release
由于 Netty 中有堆外内存的 ByteBuf 实现,堆外内存最好是手动来释放,而不是等 GC 垃圾回收。
- UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存即可;
- UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来回收内存;
- PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存。
Netty 这里采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口。
- 每个 ByteBuf 对象的初始计数为 1;
- 调用 release 方法技术减 1,如果计数为 0,ByteBuf 内存被回收;
- 调用 retain 方法技术加 1,表示调用者没用完之前,其他 handler 即使调用了 release 也不会造成回收;
- 当技术为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用。
解决黏包
@Slf4j public class TestLengthFieldDecoder { public static void main(String[] args) { //用于测试很方便 EmbeddedChannel channel = new EmbeddedChannel( //参数一:最大字节 //参数二:偏移量 //参数三:实际字节 //参数五:调整1个字节 //参数六:解析的结果后剥离4个字节(也就是说,再ByteBuf中没有被使用4个的字节被释放出来) new LengthFieldBasedFrameDecoder(1024,0,4,1,4), new LoggingHandler(LogLevel.DEBUG) ); // 4个字节的内容长度 ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(); send(buf, "hello,world"); send(buf, "hi!"); //将消息写入channel channel.writeInbound(buf); } public static void send(ByteBuf buf, String content) { byte[] bytes = content.getBytes(); //实际内容 int length = bytes.length; //设置写入12个字节,再写入内容 buf.writeInt(length); //再写入一个字节,此时如果不调整1个字节,会报错 buf.writeByte(1); buf.writeBytes(bytes); } }
协议设计与解析
1) http
import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.*; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import lombok.extern.slf4j.Slf4j; import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; @Slf4j public class TestHttp { public static void main(String[] args) { NioEventLoopGroup group = new NioEventLoopGroup(); NioEventLoopGroup group1 = new NioEventLoopGroup(); try{ ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.channel(NioServerSocketChannel.class); serverBootstrap.group(group ,group1); serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); socketChannel.pipeline().addLast(new HttpServerCodec());//请求解码器,会把请求解析成两部分:HttpRequest(请求行,请求头) HttpContent(请求体) //方式二:使用 new SimpleChannelInboundHandler<HttpRequest>() 来接收指定的请求 socketChannel.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() { @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpRequest httpRequest) throws Exception { //获取请求 log.info("{}",httpRequest.uri()); //返回响应 DefaultFullHttpResponse response = new DefaultFullHttpResponse(httpRequest.protocolVersion(), HttpResponseStatus.OK); // 参数一:协议的版本 参数二:响应状态码(HttpResponseStatus.OK 为200) byte[] bytes = "<h1>Hello,World</h1>".getBytes(); //告诉响应头,传输的字节大小 response.headers().setInt(CONTENT_LENGTH,bytes.length); //响应体 response.content().writeBytes(bytes); //写回响应 channelHandlerContext.writeAndFlush(response); } }); /* 方式一 socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { log.info("{}",msg.getClass()); if (msg instanceof HttpRequest){//请求行,请求头 }else if(msg instanceof HttpContent){//请求体 } } });*/ } }); ChannelFuture channelFuture = serverBootstrap.bind(8888).sync(); channelFuture.channel().closeFuture().sync(); }catch (Exception e){ e.printStackTrace(); }finally{ group.shutdownGracefully(); group1.shutdownGracefully(); } } }
2)自定义协议
要素(都是输入 ByteBuf 中):
- 魔数,用来在第一时间判定是否是无效数据包
- 版本号,可以支持协议的升级
- 序列化算法,只要是指消息正文,消息正文到底采用哪种序列化反序列化方法,可以由此扩展,例如:json、protobuf、hessian、jdk
- 指令类型,是登陆、注册、单聊、群聊...跟业务相关
- 请求序号,为了双工通信,提供异步能力
- 正文长度
- 消息正文
案例:
制作序列化与反序列化类
/* * 必须和 LengthFieldBasedFrameDecoder(处理黏包、半包) 一起使用,确保接到的 ByteBuf 消息是完整的 * * */ @ChannelHandler.Sharable @Slf4j public class MessageCodec extends MessageToMessageCodec<ByteBuf, Message> { @Override protected void encode(ChannelHandlerContext channelHandlerContext, Message message, List<Object> list) throws Exception { ByteBuf byteBuf = channelHandlerContext.alloc().buffer(); // 1、 4 字节的魔数 byteBuf.writeBytes(new byte[]{1,2,3,4}); // 2、 1 字节的版本 byteBuf.writeByte(1); // 3、 1字节的序列化方式 jdk 0 ,json 1 byteBuf.writeByte(0); // 4、 1字节的指令类型 byteBuf.writeByte(message.getMessageType()); // 5、 设置请求序号 4个字节 byteBuf.writeInt(4); // 6、 为了补齐为16个字节,填充1个字节的数据 byteBuf.writeByte(0xff); // 序列化msg ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(message); byte[] bytes = bos.toByteArray(); // 获得并设置正文长度 长度用4个字节标识 byteBuf.writeInt(bytes.length); // 设置消息正文 byteBuf.writeBytes(bytes); } @Override protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception { // 获取魔数 int magic = byteBuf.readInt(); // 获取版本号 byte version = byteBuf.readByte(); // 获得序列化方式 byte seqType = byteBuf.readByte(); // 获得指令类型 byte messageType = byteBuf.readByte(); // 获得请求序号 int sequenceId = byteBuf.readInt(); // 移除补齐字节 byteBuf.readByte(); // 获得正文长度 int length = byteBuf.readInt(); // 获得正文 byte[] bytes = new byte[length]; byteBuf.readBytes(bytes, 0, length); //反序列化 ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); Message message = (Message) ois.readObject(); // 将信息放入List中,传递给下一个handler list.add(message); // 打印获得的信息正文 System.out.println("===========魔数==========="); System.out.println(magic); System.out.println("===========版本号==========="); System.out.println(version); System.out.println("===========序列化方法==========="); System.out.println(seqType); System.out.println("===========指令类型==========="); System.out.println(messageType); System.out.println("===========请求序号==========="); System.out.println(sequenceId); System.out.println("===========正文长度==========="); System.out.println(length); System.out.println("===========正文==========="); System.out.println(message); } }
Message
public abstract class Message implements Serializable { public abstract int getMessageType(); public static final int LoginRequestMessage = 0; }
Student
public class Student extends Message{ public Integer id; public String name; public String clazz; @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", clazz='" + clazz + '\'' + '}'; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getClazz() { return clazz; } public void setClazz(String clazz) { this.clazz = clazz; } public Student() { } public Student(Integer id, String name, String clazz) { this.id = id; this.name = name; this.clazz = clazz; } @Override public int getMessageType() { return 1; } }
测试
@Slf4j public class TestCodec { public static void main(String[] args) throws Exception { EmbeddedChannel channel = new EmbeddedChannel(); // 添加解码器,避免粘包半包问题 //channel.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 12, 4, 0, 0)); channel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); channel.pipeline().addLast(new MessageCodec()); Student student = new Student(1,"zhangsan","class3"); // 测试编码与解码 ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(); new MessageCodec().encode(null, student, byteBuf); channel.writeInbound(byteBuf); } }