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個小朋友,有兩種方案可以解決小朋友上廁所的問題:
- 每個小朋友配一個老師。每個老師隔段時間詢問小朋友是否要上廁所,如果要上,就領他去廁所,100個小朋友就需要100個老師來詢問,並且每個小朋友上廁所的時候都需要一個老師領着他去上,這就是IO模型,一個連接對應一個線程。
- 所有的小朋友都配同一個老師。這個老師隔段時間詢問所有的小朋友是否有人要上廁所,然後每一時刻把所有要上廁所的小朋友批量領到廁所,這就是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的使用
- 首先引入maven依賴
也可以 fail ->project Structure -> Modules ->dependencies 右側的+號
Library ->new Library -> from maven 輸入 io.netty:netty-all
-
服務端的實現:
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)-->
-
服務端的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();
}
}
- 客戶端
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函數起的線程。
-
客戶端的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.拆包
這裏使用自定義協議包+編碼器+解壓器解決:
具體代碼實現拆包:
核心代碼: 協議包,編解碼器
目錄結構
- 服務端 (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();//優雅的關閉
}
}
}
- 服務端的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);
}
}
- 自定義的初始化類(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());
}
}
- 客戶端:
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();
}
}
}
- 客戶端的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();
}
}
- 客戶端的初始化類
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):
- 編碼器
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)
- 解碼器(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);
}
}
協議包
- 協議包(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;
}
}