Netty入门以及粘包拆包

本文介绍了Netty的基础知识,包括NIO如何解决线程资源受限、线程切换效率低下和以字节为单位的读写问题。接着详细探讨了Netty在粘包拆包问题上的处理,提供了具体的编码器和解码器实现,以避免粘包和拆包带来的困扰。通过使用Netty,开发者可以更高效地处理网络通信并专注于业务逻辑。
摘要由CSDN通过智能技术生成

NIO编程

关于NIO相关的文章网上也有很多,这里不打算详细深入分析,下面简单描述一下NIO是如何解决BIO的线程资源受限,线程切换效率低下,以字节为单位三个问题的。

1. 解决线程资源受限

NIO编程模型中,新来一个连接不再创建一个新的线程,而是可以把这条连接直接绑定到某个固定的线程,然后这条连接所有的读写都由这个线程来负责,那么他是怎么做到的?我们用一幅图来对比一下IO与NIO

在这里插入图片描述

​ 如上图所示,IO模型中,一个连接来了,会创建一个线程,对应一个while死循环,死循环的目的就是不断监测这条连接上是否有数据可以读,大多数情况下,1w个连接里面同一时刻只有少量的连接有数据可读,因此,很多个while死循环都白白浪费掉了,因为读不出啥数据。

​ 而在NIO模型中,他把这么多while死循环变成一个死循环,这个死循环由一个线程控制,那么他又是如何做到一个线程,一个while死循环就能监测1w个连接是否有数据可读的呢? 这就是NIO模型中selector的作用,一条连接来了之后,现在不创建一个while死循环去监听是否有数据可读了,而是直接把这条连接注册到selector上,然后,通过检查这个selector,就可以批量监测出有数据可读的连接,进而读取数据,下面我再举个非常简单的生活中的例子说明IO与NIO的区别。

在一家幼儿园里,小朋友有上厕所的需求,小朋友都太小以至于你要问他要不要上厕所,他才会告诉你。幼儿园一共有100个小朋友,有两种方案可以解决小朋友上厕所的问题:

  1. 每个小朋友配一个老师。每个老师隔段时间询问小朋友是否要上厕所,如果要上,就领他去厕所,100个小朋友就需要100个老师来询问,并且每个小朋友上厕所的时候都需要一个老师领着他去上,这就是IO模型,一个连接对应一个线程。
  2. 所有的小朋友都配同一个老师。这个老师隔段时间询问所有的小朋友是否有人要上厕所,然后每一时刻把所有要上厕所的小朋友批量领到厕所,这就是NIO模型,所有小朋友都注册到同一个老师,对应的就是所有的连接都注册到一个线程,然后批量轮询。

2. 解决线程切换效率低下

由于NIO模型中线程数量大大降低,线程切换效率因此也大幅度提高

3. 解决IO读写以字节为单位

NIO解决这个问题的方式是数据读写不再以字节为单位,而是以字节块为单位。IO模型中,每次都是从操作系统底层一个字节一个字节地读取数据,而NIO维护一个缓冲区,每次可以从这个缓冲区里面读取一块的数据, 这就好比一盘美味的豆子放在你面前,你用筷子一个个夹(每次一个),肯定不如要勺子挖着吃(每次一批)效率来得高。

简单讲完了JDK NIO的解决方案之后,我们接下来使用NIO的方案替换掉IO的方案,我们先来看看,如果用JDK原生的NIO来实现服务端,该怎么做。

Netty编程

1.netty简介

在这里插入图片描述

用一句简单的话来说就是:Netty封装了JDK的NIO,让你用得更爽,你不用再写一大堆复杂的代码了。 用官方正式的话来说就是:Netty是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。

下面是我总结的使用Netty不使用JDK原生NIO的原因

  • 使用JDK自带的NIO需要了解太多的概念,编程复杂,一不小心bug横飞
  • Netty底层IO模型随意切换,而这一切只需要做微小的改动,改改参数,Netty可以直接从NIO模型变身为IO模型
  • Netty自带的拆包解包,异常检测等机制让你从NIO的繁重细节中脱离出来,让你只需要关心业务逻辑
  • Netty解决了JDK的很多包括空轮询在内的bug
  • Netty底层对线程,selector做了很多细小的优化,精心设计的reactor线程模型做到非常高效的并发处理
  • 自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手
  • Netty社区活跃,遇到问题随时邮件列表或者issue
  • Netty已经历各大rpc框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大

2.netty的使用

  1. 首先引入maven依赖
    在这里插入图片描述

也可以 fail ->project Structure -> Modules ->dependencies 右侧的+号

Library ->new Library -> from maven 输入 io.netty:netty-all

  1. 服务端的实现:

    NettyServer.java
    
   package java_netty.simple;
   import io.netty.bootstrap.ServerBootstrap;
   import io.netty.channel.ChannelFuture;
   import io.netty.channel.ChannelInitializer;
   import io.netty.channel.ChannelOption;
   import io.netty.channel.EventLoopGroup;
   import io.netty.channel.nio.NioEventLoopGroup;
   import io.netty.channel.socket.SocketChannel;
   import io.netty.channel.socket.nio.NioServerSocketChannel;
   public class NettyServer {
       public static void main(String[] args) throws InterruptedException {
           //创建BossGroup 和WokerGroup
           //说明:
           //1.创建两个线程组 bossgroup和workergroup
           //2.BossGroup 只是处理连接请求
           //3.wokergroup 真正和客户端业务处理
           //4.两个都是无限循环
           //5. bossGroup, 和 workerGroup 含有的子线程(NioEventLoop)的个数
           // 默认实际( cpu核数 * 2)
          EventLoopGroup bossGroup=new NioEventLoopGroup(1);
          EventLoopGroup workerGroup=new NioEventLoopGroup();
          try {
          //创建服务器端的启动对象,配置参数
           ServerBootstrap bootstrap = new ServerBootstrap();
           //使用链式编程进行设置
           bootstrap.group(bossGroup,workerGroup)//设置两个线程组
                    .channel(NioServerSocketChannel.class)//使用NioSocketChannel 作为服务器的通道实现
                    .option(ChannelOption.SO_BACKLOG,128)//设置线程队列得到连接个数
                    .childOption(ChannelOption.SO_KEEPALIVE,true)//设置保持活动连接状态
                    .childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道测试对象(匿名类)
                        //给pipeline 设置处理器
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new NettyServerHandler());//给管道的最后添加一个处理器(即写的NettyServerHandler)
                        }
                    });// 给我们的workergroup 的EventLoopGroup 对应的管道设置处理器
           System.out.println("服务器 is ready");
           //绑定一个端口,并且同步处理,生成了一个ChannelFuture对象。
           //相当于启动服务器并把端口端口
           ChannelFuture channelFuture = bootstrap.bind(6668).sync();
           //对关闭通道进行监听(当有关闭操作的时候会进行监听
           channelFuture.channel().closeFuture().sync();
       }finally {
              bossGroup.shutdownGracefully();//优雅的关闭
              workerGroup.shutdownGracefully();//优雅的关闭
          }
          }
   }
  • boos对应了 IOServer.java中的接收新连接线程,主要负责创建新连接

  • worker对应 IOClient.java中的负责读取数据的线程,主要用于读取数据以及业务逻辑处理

        <!--说明:-->
        <!--1.创建两个线程组 bossgroup和workergroup-->
        <!--2.`BossGroup` 只是处理连接请求-->
        <!--3.`wokergroup` 真正和客户端业务处理-->
        <!--4.两个都是无限循环-->
    
     <!--5. bossGroup`, 和 workerGroup 含有的子线程(NioEventLoop)的个数-->
        <!--( cpu核数 * 2)-->
    
  1. 服务端的handler处理器

    NettyServerHandler.java

   package java_netty.simple;
   
   import io.netty.buffer.ByteBuf;
   import io.netty.buffer.Unpooled;
   import io.netty.channel.ChannelHandlerContext;
   import io.netty.channel.ChannelInboundHandlerAdapter;
   import io.netty.util.CharsetUtil;
   
   /*
   说明:
   1.自定义一个Handler 需要继续netty, 规定好某个HandlerAdapter
   2.这时的Handler 才能算一个Hander
    */
   public class NettyServerHandler extends ChannelInboundHandlerAdapter {
   
       //读取数据实际,(这里我们可以读取客户端发送的信息)
       /*
       1.ChannelHandlerContext  ctx: 上下文对象,含有管道 pipeline ,通道channel 地址
       2.Object msg 客户端发送的数据,以对象的形式传递,默认是Obj类
        */
       @Override
       public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
           System.out.println("服务器读取线程 : "+Thread.currentThread().getName());
           System.out.println("Server ctx=="+ctx+"  mgs=="+msg);
           //将mgs 转换成一个ByteBuf
           //ByteBuf  是Netty 提供的 ,不是Nio的ByteBuffer
           ByteBuf buf =(ByteBuf)msg;
           System.out.println("客户端发送的信息是:"+buf.toString(CharsetUtil.UTF_8));
           System.out.println("客户端地址:"+ctx.channel().remoteAddress());
       }
   
       //数据读取完毕:
       @Override
       public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
           // writeAndFlush 是write 和  flush 的合并,写到缓冲区再刷新
           //一般来讲,对这个发送的数据进行编码
           ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端",CharsetUtil.UTF_8));
       }
   
       //处理异常 ,出现异常关闭通道
       @Override
       public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
           ctx.close();
       }
   }
  1. 客户端

NettyClient.java

package java_netty.simple;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        //客户端需要一个事件循环组
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            //创建客户端启动对象
            //注意客户端使用的是Bootstrp,而服务端是ServerBootstrp
            Bootstrap bootstrap = new Bootstrap();

            //链式编程 设置相关参数
            bootstrap.group(group)//设置线程组
                    .channel(NioSocketChannel.class)// 设置客户端通道的实现类(反射)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new NettyClientHandler());//加入自己的处理器
                        }
                    });
            System.out.println("客户端ok..");
            //启动客户端去连接服务器,
            // 关于ChannelFuture 要分析 涉及到netty的异步模型。
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
            //给关闭通道 进行一个监听
            channelFuture.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }
}

客户端程序中, group对应了我们 IOClient.java中main函数起的线程。

  1. 客户端的handler处理器

    NettyClientHandler.java

 package java_netty.simple;
   
   import io.netty.buffer.ByteBuf;
   import io.netty.buffer.Unpooled;
   import io.netty.channel.ChannelHandlerContext;
   import io.netty.channel.ChannelInboundHandlerAdapter;
   import io.netty.util.CharsetUtil;
   
   // Inbound 是入栈的操作
   public class NettyClientHandler extends ChannelInboundHandlerAdapter {
       //当通道就绪时 就会触发该方法
       @Override
       public void channelActive(ChannelHandlerContext ctx) throws Exception {
           System.out.println("Clinet "+ctx);
           ctx.writeAndFlush(Unpooled.copiedBuffer("hello,server", CharsetUtil.UTF_8));
       }
       //当通道有读取事件时会触发
       @Override
       public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
           ByteBuf buf = (ByteBuf) msg;
           System.out.println("服务器回复的信息:"+buf.toString(CharsetUtil.UTF_8));
           System.out.println("服务器的地址 :"+ctx.channel().remoteAddress());
       }
       @Override
       public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
           cause.printStackTrace();
           ctx.close();
       }
   }

Netty对NIO封装得如此完美,写出来的代码非常优雅,另外一方面,使用Netty之后,网络通信这块的性能问题几乎不用操心。

Netty粘包拆包问题:

1. 粘包问题:

TCP是一个 流 的协议,所谓流就是没有界限的遗传数据,大家可以想象一下,如果河里的水相当于数据,他们是连成一片的,没有分界线,TCP底层并不了解上层的业务数据具体含义,他会根据TCP缓冲区的实际情况进行包的划分,也就是说在业务上,我们一个完整的包可能会被TCP分成多个包进行发送,也可能把多个小包封装成一个大的数据包发送出去,这就是所谓的粘包问题。
在这里插入图片描述

例如在tcp包里 运行客户端向服务端发送10条信息"Hello ,Server"。

10条信息 被分成了6个包发送:

在这里插入图片描述

这就是粘包问题

2.拆包

这里使用自定义协议包+编码器+解压器解决:

具体代码实现拆包:

核心代码: 协议包,编解码器

目录结构

在这里插入图片描述

  1. 服务端 (MyServer.java)
package java_netty.protocoltcp;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class MyServer {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup bossGroup=new NioEventLoopGroup(1 );
        EventLoopGroup workerGroup=new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup,workerGroup)//设置两个线程组
                    .channel(NioServerSocketChannel.class)//使用NioSocketChannel 作为服务器的通道实现
                    .childHandler(new MyServerInitalizer());//自定义初始化类

            ChannelFuture channelFuture = bootstrap.bind(7000).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();//优雅的关闭
            workerGroup.shutdownGracefully();//优雅的关闭
        }
    }
}
  1. 服务端的handler
   package java_netty.protocoltcp;
   
   import io.netty.buffer.ByteBuf;
   import io.netty.buffer.Unpooled;
   import io.netty.channel.ChannelHandlerContext;
   import io.netty.channel.SimpleChannelInboundHandler;
   
   import java.nio.charset.Charset;
   import java.util.UUID;
   
   // 处理业务的 handler
   public class MyServerHandler extends SimpleChannelInboundHandler<MessageProtocotcp> {
       private int count;
       @Override
       public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
           ctx.close();
       }
   
       @Override
       protected void channelRead0(ChannelHandlerContext channelHandlerContext, MessageProtocotcp msg) throws Exception {
         //接收到数据,并处理
           int len = msg.getLen();
           byte[] content = msg.getContent();
   
           System.out.println("服务器接收到的信息如下:");
           System.out.println("长度:"+len);
           System.out.println("内容:"+new String(content,Charset.forName("utf-8")));
           System.out.println("服务器接收到消息包(协议包)数量"+(++this.count));
           
           //回复消息
           String responseContent = "你好客户端,你发送的信息已经收到";
           int responselen =responseContent.getBytes("utf-8").length;
           //         int responselen =responseContent.length;
           byte[] responseContentBytes = responseContent.getBytes("utf-8");
   
           //构建一个协议包
           MessageProtocotcp messageProtocotcp =new MessageProtocotcp();
           messageProtocotcp.setLen(responselen);
           messageProtocotcp.setContent(responseContentBytes);
   
           //构建完协议包 即可发送
           //但需要在ServerHandler中加一个编码器  ClientHandler中加入解码器
           channelHandlerContext.writeAndFlush(messageProtocotcp);
       }
   }
   
  1. 自定义的初始化类(MyServerInitalizer.java):给服务器的启动对象配置handler,编码器,解码器
package java_netty.protocoltcp;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;

public class MyServerInitalizer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();

        pipeline.addLast(new MyMessageDecoder());// 解码器
        pipeline.addLast(new MyMessageEncoder());// 编码器
        pipeline.addLast(new MyServerHandler());
    }
}
  1. 客户端:
   package java_netty.protocoltcp;
   import io.netty.bootstrap.Bootstrap;
   import io.netty.channel.ChannelFuture;
   import io.netty.channel.EventLoopGroup;
   import io.netty.channel.nio.NioEventLoopGroup;
   import io.netty.channel.socket.nio.NioSocketChannel;
   public class MyClient {
       public static void main(String[] args) throws InterruptedException {
           EventLoopGroup groups = new NioEventLoopGroup();
           try {
               Bootstrap bootstrap = new Bootstrap();
               bootstrap.group(groups).channel(NioSocketChannel.class)// 设置客户端通道的实现类(反射)
                        .handler(new MyClientInitializer());//自定义一个初始化类
               ChannelFuture channelFuture = bootstrap.connect("localhost", 7000).sync();
               //给关闭通道 进行一个监听
               channelFuture.channel().closeFuture().sync();
           }finally {
               groups.shutdownGracefully();
           }
       }
   }
  
  1. 客户端的handler
   package java_netty.protocoltcp;
   
   import io.netty.buffer.ByteBuf;
   import io.netty.buffer.Unpooled;
   import io.netty.channel.ChannelHandlerContext;
   import io.netty.channel.SimpleChannelInboundHandler;
   
   import java.nio.charset.Charset;
   
   public class MyClientHandler extends SimpleChannelInboundHandler<MessageProtocotcp> {
       private int count;
       @Override
       public void channelActive(ChannelHandlerContext ctx) throws Exception {
           //使用客户端发送5条数据 呵呵 给服务端,
           for(int i=0;i<5;i++){
               String mes=" 呵呵服务端,你好";
               byte[] content = mes.getBytes(Charset.forName("utf-8"));
               int length =mes.getBytes(Charset.forName("utf-8")).length;
               //创建协议包对象;
               MessageProtocotcp messageProtocotcp=new MessageProtocotcp();
               messageProtocotcp.setLen(length);
               messageProtocotcp.setContent(content);
               ctx.writeAndFlush(messageProtocotcp);
           }
       }
       @Override
       protected void channelRead0(ChannelHandlerContext channelHandlerContext, MessageProtocotcp msg) throws Exception {
           int len = msg.getLen();
           byte[] content = msg.getContent();
           System.out.println("客户端接收到的信息如下:");
           System.out.println("长度:"+len);
           System.out.println("内容:"+new String(content,Charset.forName("utf-8")));
           System.out.println("服务器接收到消息包(协议包)数量"+(++this.count));
       }
   
       @Override
       public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
          System.out.println("异常信息: "+cause.getMessage());
           ctx.close();
       }
   }
  1. 客户端的初始化类
   package java_netty.protocoltcp;
   import io.netty.channel.ChannelInitializer;
   import io.netty.channel.ChannelPipeline;
   import io.netty.channel.socket.SocketChannel;
   public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
       @Override
       protected void initChannel(SocketChannel socketChannel) throws Exception {
           ChannelPipeline pipeline = socketChannel.pipeline();
           pipeline.addLast(new MyMessageEncoder()); // 加入编码器
           pipeline.addLast(new MyMessageDecoder()); // 加入解码器
           pipeline.addLast(new MyClientHandler());
       }
   }
编码器(Decoder):
  1. 编码器
package java_netty.protocoltcp;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

//编码器
public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocotcp> {
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, MessageProtocotcp messageProtocotcp, ByteBuf byteBuf) throws Exception {
        System.out.println("MyMessageEncoder  encode 方法被调用");
        byteBuf.writeInt(messageProtocotcp.getLen());
        byteBuf.writeBytes(messageProtocotcp.getContent());

    }
}
解码器(Encoder)
  1. 解码器(Encoder) 这里用readInt()方法可以自动获取length长度
package java_netty.protocoltcp;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;

import java.util.List;
// 解码器
public class MyMessageDecoder  extends ReplayingDecoder {
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        System.out.println(" MyMessageDecoder  decoder  被调用");
        //需要将得到的二进制字节码 -> MessageProtocol 数据包(对象)
        int length = byteBuf.readInt(); //自动获取length长度
        byte [] content=new byte[length];
        byteBuf.readBytes(content);
        //封装成 MessageProtocol 对象, 放入 list 传递下一个handler 业务处理
        MessageProtocotcp messageProtocotcp = new MessageProtocotcp();
        messageProtocotcp.setLen(length);
        messageProtocotcp.setContent(content);
        list.add(messageProtocotcp);
    }
}
协议包
  1. 协议包(MessageProtocotcp)

(定义长度和发送的数据(一般用字节数组))

package java_netty.protocoltcp;
// 协议包
public class MessageProtocotcp {
    //定义长度 ,关键
    private int len;
    //发送的数据 一般用 字节数组
    private byte[] content;
    public int getLen() {
        return len;
    }
    public void setLen(int len) {
        this.len = len;
    }
    public byte[] getContent() {
        return content;
    }
    public void setContent(byte[] content) {
        this.content = content;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值