Netty入門以及粘包拆包

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;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章