2019.8.25笔记——netty入门

Netty

整体架构如下
在这里插入图片描述

ByteBuf

Netty没有使用NIO的buffer而是用它们自己的缓冲类,作用上来看两者几乎是一致的,但是使用起来ByteBuf会比buffer简单许多,两者的结构也很相似。

源码中的注释,画出了ByteBuf的结构,包括三个指针,被三个指针分成了三个区域
在这里插入图片描述
具体结构如下
在这里插入图片描述
使用时的注意要点

  1. 开始时的readerIndex和writerIndex都指向0

  2. ByteBuf中的数据开始时初始化为0

  3. 执行clear()方法时会将readerIndex和writerIndex都指向0,数据不会清空,当写时数据会覆盖

  4. 每当执行discardReadBytes()方法时都会直接丢弃readerIndex前的数据而且readerIndex变为0,writerIndex变为原来的数值减去readerIndex原来的数值,capacity保持不变,需要注意的是原来的writerIndex和现在的writerIndex之间的数据也不会清空

  5. setZero()会将ByteBuf清零(每一个字节设为0),但是指针的位置不会变

  6. ByteBuf在writerIndex指针将要超过capacity时会自动扩容

使用例程

创建一个ByteBuf

public static void main(String[] args) {
    //创建一个非池化的ByteBuf,大小为10个字节
    ByteBuf buffer = Unpooled.buffer(10);
    System.out.println("原始ByteBuf为===================>"+buffer.toString());
    System.out.println("ByteBuf中的内容为===================>"+ Arrays.toString(buffer.array()));
}

结果如下

原始ByteBuf为===================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 10)
ByteBuf中的内容为===================>[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

向ByteBuf中写入数据

public static void main(String[] args) {
    //创建一个非池化的ByteBuf,大小为10个字节
    ByteBuf buffer = Unpooled.buffer(10);
    byte[] bytes = {1,2,3,4,5};
    buffer.writeBytes(bytes);
    System.out.println("原始ByteBuf为===================>"+buffer.toString());
    System.out.println("ByteBuf中的内容为===================>"+ Arrays.toString(buffer.array()));
}

结果如下

原始ByteBuf为===================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 5, cap: 10)
ByteBuf中的内容为===================>[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]

从ByteBuf中读数据

public static void main(String[] args) {
    //创建一个非池化的ByteBuf,大小为10个字节
    ByteBuf buffer = Unpooled.buffer(10);
    byte[] bytes = {1,2,3,4,5};
    buffer.writeBytes(bytes);
    byte byte1 = buffer.readByte();
    byte byte2 = buffer.readByte();
    System.out.println("读到的数据是=====================>"+ Arrays.toString(new byte[]{byte1,byte2}));
    System.out.println("原始ByteBuf为===================>"+buffer.toString());
    System.out.println("ByteBuf中的内容为===================>"+ Arrays.toString(buffer.array()));
}

结果如下

读到的数据是=====================>[1, 2]
原始ByteBuf为===================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 2, widx: 5, cap: 10)
ByteBuf中的内容为===================>[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]

将读到的数据丢弃

public static void main(String[] args) {
    //创建一个非池化的ByteBuf,大小为10个字节
    ByteBuf buffer = Unpooled.buffer(10);
    byte[] bytes = {1,2,3,4,5};
    buffer.writeBytes(bytes);
    byte byte1 = buffer.readByte();
    byte byte2 = buffer.readByte();
    buffer.discardReadBytes();
    System.out.println("原始ByteBuf为===================>"+buffer.toString());
    System.out.println("ByteBuf中的内容为===================>"+ Arrays.toString(buffer.array()));
}

结果如下

原始ByteBuf为===================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 3, cap: 10)
ByteBuf中的内容为===================>[3, 4, 5, 4, 5, 0, 0, 0, 0, 0]

使用clear清空指针

public static void main(String[] args) {
    //创建一个非池化的ByteBuf,大小为10个字节
    ByteBuf buffer = Unpooled.buffer(10);
    byte[] bytes = {1,2,3,4,5};
    buffer.writeBytes(bytes);
    byte byte1 = buffer.readByte();
    byte byte2 = buffer.readByte();
    buffer.discardReadBytes();
    buffer.clear();
    System.out.println("原始ByteBuf为===================>"+buffer.toString());
    System.out.println("ByteBuf中的内容为===================>"+ Arrays.toString(buffer.array()));
}

结果如下,注意数据并没有清空

原始ByteBuf为===================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 10)
ByteBuf中的内容为===================>[3, 4, 5, 4, 5, 0, 0, 0, 0, 0]

将ByteBuf清零

public static void main(String[] args) {
    //创建一个非池化的ByteBuf,大小为10个字节
    ByteBuf buffer = Unpooled.buffer(10);
    byte[] bytes = {1,2,3,4,5};
    buffer.writeBytes(bytes);
    byte byte1 = buffer.readByte();
    byte byte2 = buffer.readByte();
    buffer.discardReadBytes();
    buffer.setZero(0,buffer.capacity());
    System.out.println("原始ByteBuf为===================>"+buffer.toString());
    System.out.println("ByteBuf中的内容为===================>"+ Arrays.toString(buffer.array()));
}

结果如下,注意指针位置没有改变

原始ByteBuf为===================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 3, cap: 10)
ByteBuf中的内容为===================>[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

实现自动扩容

public static void main(String[] args) {
    //创建一个非池化的ByteBuf,大小为10个字节
    ByteBuf buffer = Unpooled.buffer(10);
    byte[] bytes = {1,2,3,4,5};
    buffer.writeBytes(bytes);
    byte byte1 = buffer.readByte();
    byte byte2 = buffer.readByte();
    buffer.discardReadBytes();
    buffer.setZero(0,buffer.capacity());
    byte[] bytes1 = {1,2,3,4,5,6,7,8,9,10};
    buffer.writeBytes(bytes1);
    System.out.println("原始ByteBuf为===================>"+buffer.toString());
    System.out.println("ByteBuf中的内容为===================>"+ Arrays.toString(buffer.array()));
}

结果如下

原始ByteBuf为===================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 13, cap: 64)
ByteBuf中的内容为===================>[0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

pipeline

如果是用Netty实现NIO,不论是客户端还是服务端都有各自的pipeline,而且每个socketchannel都有各自的pipeline,而且各自相互独立,互不影响。

pipeline的大体结构如下,不过服务端的NioServersocketchannel所属的pipeline是不需要我们维护的,我们只需要实现客户端socket和服务端接收到客户端的socket的pipeline。
在这里插入图片描述
pipeline都有一个head和tail,这个是固定的,是初始化的时候就存在的,我们所要做的就是将我们要执行的业务代码写成Handler放在pipeline中去。
在这里插入图片描述
要加入的Handler要实现其对应的接口,这类接口一般为三类

  • Outbound,出栈
  • Inbound,入栈
  • Duplex,出栈和入栈

这样在有数据进来时就会执行继承Inbound类型的接口的Handler,需要输出数据时就会执行Outbound类型的接口的Handler。

执行的顺序和它们在pipeline中的顺序时一样的,但是它们是分别有序的,就是只有当有数据进来时才会开始执行Inbound类型的接口的Handler,同样的只有当要输出数据时才会执行Outbound类型的接口的Handler。

注意入栈处理器的顺序就是它们在pipeline中的顺序,出栈处理器的顺序与它们在pipeline中的顺序是相反的。

聊天室例程

客户端

public class TestClient {
    public static void main(String[] args) {
        EventLoopGroup bossGroup=new NioEventLoopGroup();
        try {
            Bootstrap bootstrap=new Bootstrap();
            bootstrap.group(bossGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new TestClientInitializer());
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1",8989).sync();
            channelFuture.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            bossGroup.shutdownGracefully();
        }
    }
}

注意这里分别有两个sync方法,第一个是方法是客户端连接服务端,这里加上sync方法使这个方法和当前线程同步(sync可加可不加)。

第二个方法是判断当前的通道是否关闭,这里加上的sync方法同样是使方法同步,这里必须加上这个方法,因为不加上主线程就直接执行完毕了,加上会阻塞住这个方法,知道客户端的通道关闭。
在这里插入图片描述
这里是向客户端的pipeline上放入第一个Handler
在这里插入图片描述
这个Handler属于初始化pipeline的,在执行完后就会被抛弃,主要作用就是初始化pipeline,将真正业务Handler加入pipeline

public class TestClientInitializer extends ChannelInitializer<SocketChannel>{

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        /**
         * 1) lengthFieldOffset  //长度字段的偏差
         * 2) lengthFieldLength  //长度字段占的字节数
         * 3) lengthAdjustment  //添加到长度字段的补偿值
         * 4) initialBytesToStrip  //从解码帧中第一次去除的字节数
         */
         //ChannelInboundHandlerAdapter
        pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4));
        //ChannelOutboundHandlerAdapter
        pipeline.addLast(new LengthFieldPrepender(4));//计算当前待发送消息的二进制字节长度,将该长度添加到ByteBuf的缓冲区头中
        //ChannelInboundHandlerAdapter
        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));  //将byte数据解码成String
        //ChannelOutboundHandlerAdapter
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));  //将字符串编码成byte数据
        //SimpleChannelInboundHandler
        pipeline.addLast(new TestClientHandler());
    }
}

客户端真正的业务Handler,属于接收数据类型的Handler

public class TestClientHandler extends SimpleChannelInboundHandler<String> {

    //读取客户端数据
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
        System.out.println(channelHandlerContext.channel().remoteAddress()+",client output"+s);
//        channelHandlerContext.writeAndFlush("form client"+ LocalDateTime.now());
    }


    //通道就绪
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i=0;i<10;i++){
            ctx.writeAndFlush("来自客户端的问候");
        }
    }

    //有异常发生
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.channel().close();
    }
}

服务端

public class TestServer {
    public static void main(String[] args) {
        EventLoopGroup bossGroup=new NioEventLoopGroup(1);
        EventLoopGroup workGroup=new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap=new ServerBootstrap();
            serverBootstrap.group(bossGroup,workGroup)
                            .channel(NioServerSocketChannel.class)
                            .childHandler(new TestServerInitializer());
            ChannelFuture channelFuture = serverBootstrap.bind(8989).sync();
            channelFuture.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
}

注意这里的两个NioEventLoopGroup,其实都是线程组,第一个是服务端负责接受客户端连接的线程组,不论有多少线程都只会有一个线程工作,所以为了不浪费性能一般个数设置为1。

第二个是服务端和客户端通信时的线程组,使用默认构造器就是线程数默认为cup核心数*2
在这里插入图片描述
这里第一个channel设置的是服务端接收客户端连接的pipeline初始化,这里固定使用NioServerSocketChannel,由netty自己来管理。

这里第二个childHandler是设置和客户端通信的socketchannel的pipeline
在这里插入图片描述
后面的sync方法和客户端同样的道理

同客户端一样的逻辑,初始化每一个和客户端连接的socketchannel的pipeline

public class TestServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        /**
         * 1) lengthFieldOffset  //长度字段的偏差
         * 2) lengthFieldLength  //长度字段占的字节数
         * 3) lengthAdjustment  //添加到长度字段的补偿值
         * 4) initialBytesToStrip  //从解码帧中第一次去除的字节数
         */

        //ChannelInboundHandlerAdapter
        pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4));
        //ChannelOutboundHandlerAdapter
        pipeline.addLast(new LengthFieldPrepender(4)); //计算当前待发送消息的二进制字节长度,将该长度添加到ByteBuf的缓冲区头中
        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8)); //ChannelInboundHandlerAdapter
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));//ChannelOutboundHandlerAdapter
        pipeline.addLast(new TestServerHandler());//SimpleChannelInboundHandler
    }
}

业务Handler类

public class TestServerHandler extends SimpleChannelInboundHandler<String>{
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
        System.out.println(channelHandlerContext.channel().remoteAddress()+","+s);
        channelHandlerContext.writeAndFlush("form server"+ UUID.randomUUID());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

粘包拆包问题

在客户端的pipeline初始化后客户端循环向服务端发送十句字符串,虽然发送时调用四个writeAndFlush()方法发送的,但是因为tcp协议的优化,在客户端接收时其实时十句字符串连在一起接受的。
在这里插入图片描述
这样显然不能达到我们的要求,为了将这种粘包情况拆包,我们可以使用netty为我们提供的两个Handler。

在发送数据时第二个Handler会将发送的数据的长度计算出来并且把计算出来的数值放在ByteBuf的缓冲区头,这里的参数时存放计算出来的数值的字节数。

在接收数据时第一个Handler会将接收的数据根据数据头的数值将指定的数值读出来,这样就不会出现多读或者漏读的问题,这里的参数如下

  1. maxFrameLength //字段长度的最大值
  2. lengthFieldOffset //长度字段的偏差
  3. lengthFieldLength //长度字段占的字节数
  4. lengthAdjustment //添加到长度字段的补偿值
  5. initialBytesToStrip //从解码帧中第一次去除的字节数

在这里插入图片描述
这样就可以解决粘包拆包问题

总的来说就是客户端做分包处理,加上总长度发到服务端,服务端就根据那个总长来做粘包处理,应该是这样,返回写数据的时候就反过来。

响应浏览器请求

public class TestServer {
    public static void main(String[] args) {
        EventLoopGroup bossGroup=new NioEventLoopGroup(1);  //接收客户端连接的线程组
        EventLoopGroup workGroup=new NioEventLoopGroup(); //真正处理读写事件的线程组   16
        try {
            ServerBootstrap serverBootstrap=new ServerBootstrap();
            serverBootstrap.group(bossGroup,workGroup)
                    .channel(NioServerSocketChannel.class)  //服务端用什么通道
                    .childHandler(new TestServerLnitializer()); //已经连接上来的客户端
            ChannelFuture channelFuture = serverBootstrap.bind(8989).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
}
/**
 * HttpRequestEncoder,将HttpRequest或HttpContent编码成ByteBuf
   HttpRequestDecoder,将ByteBuf解码成HttpRequest和HttpContent
   HttpResponseEncoder,将HttpResponse或HttpContent编码成ByteBuf
   HttpResponseDecoder,将ByteBuf解码成HttpResponse和HttpContent
 */
//ChannelInitializer   特殊的Handler
public class TestServerLnitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        pipeline.addLast("httpServerCodec",new HttpServerCodec());
        pipeline.addLast("testServerHandler",new TestServerHandler());
    }
}
public class TestServerHandler extends SimpleChannelInboundHandler<HttpObject> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject httpObject) throws Exception {
        if(httpObject instanceof  HttpRequest){
            HttpRequest httpRequest= (HttpRequest) httpObject;
            String uri = httpRequest.uri();
            System.out.println(uri);
            ByteBuf byteBuf = Unpooled.copiedBuffer("helloworld", CharsetUtil.UTF_8);
            FullHttpResponse fullHttpResponse=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK,byteBuf);
            fullHttpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain");
            fullHttpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH,byteBuf.readableBytes());
            channelHandlerContext.writeAndFlush(fullHttpResponse);
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值