尚硅谷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);
    }
}

 

posted @ 2021-09-08 10:50  nicechen  阅读(368)  评论(0编辑  收藏  举报