- 原生 NIO 存在的問題:
- NIO 的類庫和 API 繁雜,使用麻煩:需要熟練掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
- 需要具備其他的額外技能:要熟悉 Java 多線程編程,因為 NIO 編程涉及到 Reactor 模式,你必須對多線程和網絡編程非常熟悉,才能編寫出高質量的 NIO 程序。
- 開發工作量和難度都非常大:例如客戶端面臨斷連重連、網絡閃斷、半包讀寫、失敗緩存、網絡擁塞和異常流的處理等等。
- JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它會導致 Selector 空輪詢,最終導致 CPU100%。直到 JDK1.7 版本該問題仍舊存在,沒有被根本解決。
- Netty的優點:Netty 對 JDK 自帶的 NIO 的 API 進行了封裝,解決了上述問題。
- 設計優雅:適用于各種傳輸類型的統一 API 阻塞和非阻塞 Socket;基于靈活且可擴展的事件模型,可以清晰地分離關注點;高度可定制的線程模型-單線程,一個或多個線程池。
- 使用方便:詳細記錄的 Javadoc,用戶指南和示例;沒有其他依賴項,JDK5(Netty3.x)或 6(Netty4.x)就足夠了。
- 高性能、吞吐量更高:延遲更低;減少資源消耗;最小化不必要的內存復制。
- 安全:完整的 SSL/TLS 和 StartTLS 支持。
- 社區活躍、不斷更新:社區活躍,版本迭代周期短,發現的 Bug 可以被及時修復,同時更多的新功能會被加入。
- 目前存在的線程模型有:
傳統阻塞I/O服務模型
和Reactor模式
。
傳統阻塞I/O服務模型
- 傳統阻塞I/O服務模型的特點:
- 采用阻塞 IO 的模式獲取輸入的數據;
- 每個連接都需要獨立的線程完成數據的輸入,業務處理,數據返回。
- 上圖反應的問題分析:
- 當并發數很大,就會創建大量的線程,占用很大的系統資源;
- 連接創建后,若當前線程暫時沒有數據可讀,則該線程會阻塞在 read 操作,造成線程資源浪費。
- 針對傳統阻塞 I/O 服務模型的缺點,給出以下解決方案:
-
基于 I/O 復用模型
:多個連接共用一個阻塞對象,應用程序只需要在一個阻塞對象等待,無需阻塞等待所有連接。當某個連接有新的數據可以處理時,操作系統通知應用程序,線程從阻塞狀態返回,開始進行業務處理。 -
基于線程池復用線程資源
:不必再為每個連接創建線程,將連接完成后的業務處理任務分配給線程進行處理,一個線程可以處理多個連接的業務。
-
- Reactor模式有3鐘叫法:①
反應器模式
;②分發者模式(Dispatcher)
;③通知者模式(notifier)
。
Reactor模式
- 對上圖的說明:
-
Reactor模式
:通過一個或多個輸入同時傳遞給服務處理器的模式(基于事件驅動)。 - 服務器端程序處理傳入的多個請求,并將它們同步分派到相應的處理線程,因此 Reactor 模式也叫
Dispatcher模式
。 - Reactor 模式使用 IO 復用監聽事件,收到事件后,分發給某個線程(進程),這點就是網絡服務器高并發的處理關鍵。
-
- 根據 Reactor 的數量和處理資源池線程的數量不同,有 3 種典型的實現:①
單Reactor單線程
;②單Reactor多線程
;③主從Reactor多線程
。Netty 主要基于主從Reactor多線程
模型做了一定的改進,其中主從 Reactor 多線程模型有多個 Reactor。
單Reactor單線程
- 對上圖的說明:
-
select
是前面 I/O 復用模型介紹的標準網絡編程 API,可以實現應用程序通過一個阻塞對象監聽多路連接請求; - Reactor 對象通過 Select 監控客戶端請求事件,收到事件后
Dispatch
分發給處理進程; - 若是
建立連接請求事件
,則由 Acceptor 通過 Accept 處理連接請求,然后創建一個 Handler 對象處理連接完成后的后續業務處理; - 若不是建立連接事件,則 Reactor 會分發調用連接對應的 Handler 來響應
Handler,會完成 Read → 業務處理 → Send 的完整業務流程。 - 優點:模型簡單,沒有多線程、進程通信、競爭的問題,全部都在一個線程中完成;
- 缺點:性能問題,只有一個線程,
無法完全發揮多核CPU的性能
。Handler在處理某個連接上的業務時,整個進程無法處理其他連接事件,很容易導致性能瓶頸。可靠性問題:線程意外終止,或者進入死循環,會導致整個系統通信模塊不可用,不能接收和處理外部消息,造成節點故障。 - 使用場景:客戶端的數量有限,業務處理非常快速,比如 Redis 在業務處理方面的時間復雜度為
的情況。
-
單Reactor多線程
- 對上圖的說明:
- Reactor 對象通過 Select 監控客戶端請求事件,收到事件后,通過 Dispatch 進行分發;
- 若建立連接請求,則由 Acceptor 通過 accept 處理連接請求,然后創建一個 Handler 對象處理完成連接后的各種事件;
- 若不是連接請求,則由 Reactor 分發調用連接對應的 handler 來處理;
- handler 只負責響應事件,不做具體的業務處理,通過 read 讀取數據后,會分發給后面的 worker 線程池的某個線程處理業務;
- worker 線程池會分配獨立的線程完成真正的業務,并將結果返回給 handler;
- handler 收到響應后,通過 send 將結果返回給 client。
- 優點:可以充分的利用多核 cpu 的處理能力;
- 缺點:多線程數據共享和訪問比較復雜,Reactor 處理所有的事件的監聽和響應,在單線程中運行,在高并發場景時運行容易出現性能瓶頸。
主從Reactor多線程
- 對上圖的說明:
- Reactor 主線程 MainReactor 對象通過 select 監聽連接事件,收到事件后,通過 Acceptor 處理連接事件;
- 當 Acceptor 處理連接事件后,MainReactor 將連接分配給 SubReactor;
- Subreactor 將連接加入到連接隊列進行監聽,并創建 handler 進行各種事件處理,當有新事件發生時,Subreactor 就會調用對應的 handler 處理;
- handler 通過 read 讀取數據,分發給后面的 worker 線程處理,worker 線程池分配獨立的 worker 線程進行業務處理,并返回結果;
- handler 收到響應的結果后,再通過 send 將結果返回給 client;
- Reactor 主線程可以對應多個 Reactor 子線程,即 MainRecator 可以關聯多個 SubReactor。
- 優點:父線程與子線程的數據交互簡單職責明確,父線程只需要接收新連接,子線程完成后續的業務處理。父線程與子線程的數據交互簡單,Reactor 主線程只需要把新連接傳給子線程,子線程無需返回數據。
- 缺點:編程復雜度較高。
- 應用場景:這種模型在許多項目中廣泛使用,包括 Nginx 主從 Reactor 多進程模型,Memcached 主從多線程,Netty 主從多線程模型的支持。
- Reactor模式的優點:①響應快,不必為單個同步時間所阻塞,雖然 Reactor 本身依然是同步的;②可以最大程度地避免復雜的多線程及同步問題、多線程/進程的切換帶來的開銷;③擴展性好,可以方便的通過增加 Reactor 實例個數來充分利用 CPU 資源;④復用性好,Reactor 模型本身與具體事件處理邏輯無關,具有很高的復用性。
- 對上圖的說明:
- BossGroup 線程維護 Selector,只關注 Accecpt;
- 當接收到 Accept 事件,獲取到對應的 SocketChannel,封裝成 NIOScoketChannel,并注冊到 Worker 線程(事件循環)進行維護;
- 當 Worker 線程監聽到 Selector 中注冊的通道發生自己感興趣的事件后,就進行處理(由 handler來完成)。注意: handler 已經加入到通道。
- 對上圖的說明:
- Netty 抽象出兩組線程池:BossGroup 專門負責接收客戶端的連接,WorkerGroup 專門負責網絡的讀寫;
- BossGroup 和 WorkerGroup 類型都是 NioEventLoopGroup;
- NioEventLoopGroup 相當于一個事件循環組,這個組中含有多個事件循環,每一個事件循環是 NioEventLoop;
- NioEventLoop 表示一個不斷循環的執行處理任務的線程,每個 NioEventLoop 都有一個 Selector,用于監聽綁定在其上的 socket 的網絡通訊;
- NioEventLoopGroup 可以有多個線程,即可以含有多個 NioEventLoop;
- 每個 BossNioEventLoop 循環執行的步驟有 3 步:
- 輪詢 accept 事件;
- 處理 accept 事件,與 client 建立連接,生成 NioScocketChannel,并將其注冊到某個 worker NIOEventLoop 上的 Selector;
- 處理任務隊列的任務,即 runAllTasks;
- 每個 Worker NIOEventLoop 循環執行的步驟:
- 輪詢 read,write 事件;
- 處理 I/O 事件,即 read,write 事件,在對應 NioScocketChannel 處理;
- 處理任務隊列的任務,即 runAllTasks;
- 每個 Worker NIOEventLoop 處理業務時,會使用 pipeline(管道),pipeline 中包含了 channel,即通過 pipeline 可以獲取到對應通道,管道中維護了很多的處理器。
- Netty入門案例:TCP通信。
- 導入maven依賴:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.54.Final</version>
</dependency>
- NettyServer.java
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;
public class NettyServer {
public static void main(String[] args) throws Exception {
//創建BossGroup 和 WorkerGroup
//說明
//1、創建兩個線程組 bossGroup 和 workerGroup
//2、bossGroup 只是處理連接請求,真正的客戶端業務處理是會交給 workerGroup 完成
//3、兩個都是無限循環
//4、bossGroup 和 workerGroup 含有的子線程(NioEventLoop)的個數:默認值為 cpu核數 * 2
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); //8個
try {
//創建服務器端的啟動對象,配置參數
ServerBootstrap bootstrap = new ServerBootstrap();
//使用鏈式編程來進行設置
bootstrap.group(bossGroup, workerGroup) //設置兩個線程組
.channel(NioServerSocketChannel.class) //使用 NioSocketChannel 作為服務器的通道實現
.option(ChannelOption.SO_BACKLOG, 128) //設置線程隊列等待連接的個數
.childOption(ChannelOption.SO_KEEPALIVE, true) //設置保持活動連接狀態
//.handler(null) // 該 handler對應 bossGroup , childHandler 對應 workerGroup
.childHandler(new ChannelInitializer<SocketChannel>() { //創建一個通道初始化對象(匿名對象)
//給 pipeline 設置處理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//可以使用一個集合管理 SocketChannel,在推送消息時,可以將業務加入到各個channel 對應的 NIOEventLoop 的 taskQueue 或者 scheduleTaskQueue 中
System.out.println("客戶socketChannel hashcode=" + ch.hashCode());
ch.pipeline().addLast(new NettyServerHandler()); //給 workerGroup 的 EventLoop 對應的管道設置處理器
}
});
System.out.println(".....服務器 is ready...");
//綁定一個端口并且同步處理,生成了一個 ChannelFuture 對象
//啟動服務器(并綁定端口)
ChannelFuture cf = bootstrap.bind(6668).sync();
//給 cf 注冊監聽器,監控我們關心的事件
//綁定端口是異步操作,當綁定操作處理完,將會調用相應的監聽器處理邏輯
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (cf.isSuccess()) {
System.out.println("監聽端口 6668 成功");
} else {
System.out.println("監聽端口 6668 失敗");
}
}
});
//對關閉通道進行監聽
cf.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
- NettyServerHandler.java
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/**
* 說明:自定義一個Handler 需要繼承 netty 規定好的某個HandlerAdapter(規范)
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//讀取數據實際(這里我們可以讀取客戶端發送的消息)
/**
* 1、ChannelHandlerContext ctx:上下文對象,含有管道 pipeline,通道channel,地址
* 2、Object msg:客戶端發送的數據,默認是 Object
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服務器讀取線程:" + Thread.currentThread().getName() + ",channel=" + ctx.channel());
System.out.println("server ctx:" + ctx);
System.out.println("看看channel 和 pipeline的關系");
Channel channel = ctx.channel();
//本質是一個雙向鏈接, 出站入站
ctx.pipeline();
//將 msg 轉成一個 ByteBuf
//ByteBuf 是 Netty 提供的,不是 NIO 的 ByteBuffer。
ByteBuf buf = (ByteBuf) msg;
System.out.println("客戶端發送消息是:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("客戶端地址:" + channel.remoteAddress());
}
//數據讀取完畢
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//writeAndFlush:write + flush
//將數據寫入到緩存,并刷新
//一般需要對發送的數據進行編碼
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客戶端~(>^ω^<)喵1", CharsetUtil.UTF_8));
}
//發生異常時,需要關閉通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
- NettyClient.java
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 Exception {
//客戶端需要一個事件循環組
EventLoopGroup group = new NioEventLoopGroup();
try {
//創建客戶端啟動對象
//注意客戶端使用的不是 ServerBootstrap 而是 Bootstrap
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("...客戶端 is ok...");
//啟動客戶端去連接服務器端
//關于ChannelFuture 涉及到netty的異步模型
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
//給關閉通道進行監聽
channelFuture.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
- NettyClientHandler.java
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;
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//當通道就緒就會觸發該方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client:" + 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();
}
}
- 任務隊列中的 Task 有 3 種典型使用場景:
- 用戶程序自定義的普通任務;
- 用戶自定義定時任務;
- 非當前 Reactor 線程調用 Channel 的各種方法:例如在推送系統的業務線程里面,根據用戶的標識,找到對應的 Channel 引用,然后調用 Write 類方法向該用戶推送消息,就會進入到這種場景,最終的 Write 會提交到任務隊列中后被異步消費。
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import java.util.concurrent.TimeUnit;
/**
* 說明:自定義一個Handler 需要繼承 netty 規定好的某個HandlerAdapter(規范)
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//讀取數據實際(這里我們可以讀取客戶端發送的消息)
/**
* 1、ChannelHandlerContext ctx:上下文對象,含有管道 pipeline,通道channel,地址
* 2、Object msg:客戶端發送的數據,默認是 Object
* 假設這里有一個非常耗時長的業務 -> 異步執行 -> 提交該channel 對應的 NIOEventLoop 的 taskQueue中
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服務器讀取線程:" + Thread.currentThread().getName() + ",channel=" + ctx.channel());
System.out.println("server ctx:" + ctx);
System.out.println("看看channel 和 pipeline的關系");
Channel channel = ctx.channel();
//本質是一個雙向鏈接, 出站入站
ctx.pipeline();
//將 msg 轉成一個 ByteBuf
//ByteBuf 是 Netty 提供的,不是 NIO 的 ByteBuffer。
ByteBuf buf = (ByteBuf) msg;
System.out.println("客戶端發送消息是:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("客戶端地址:" + channel.remoteAddress());
//解決方案1:用戶程序自定義的普通任務
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客戶端~(>^ω^<)喵2", CharsetUtil.UTF_8));
System.out.println("channel code:" + ctx.channel().hashCode());
} catch (Exception ex) {
System.out.println("發生異常" + ex.getMessage());
}
}
});
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客戶端~(>^ω^<)喵3", CharsetUtil.UTF_8));
System.out.println("channel code=" + ctx.channel().hashCode());
} catch (Exception ex) {
System.out.println("發生異常" + ex.getMessage());
}
}
});
//解決方案2 : 用戶自定義定時任務 -> 該任務是提交到 scheduleTaskQueue中
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客戶端~(>^ω^<)喵4", CharsetUtil.UTF_8));
System.out.println("channel code=" + ctx.channel().hashCode());
} catch (Exception ex) {
System.out.println("發生異常" + ex.getMessage());
}
}
}, 5, TimeUnit.SECONDS);
System.out.println("go on ...");
}
//數據讀取完畢
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//writeAndFlush:write + flush
//將數據寫入到緩存,并刷新
//一般需要對發送的數據進行編碼
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客戶端~(>^ω^<)喵1", CharsetUtil.UTF_8));
}
//發生異常時,需要關閉通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
- 每個 NioEventLoop 中包含有一個 Selector,一個 taskQueue;每個 NioEventLoop 的 Selector 上可以注冊監聽多個 NioChannel;每個 NioChannel 只會綁定在唯一的 NioEventLoop 上;每個 NioChannel 都綁定有一個自己的 ChannelPipeline。
- 異步模型:當一個異步過程調用發出后,調用者不能立刻得到結果。實際處理這個調用的組件在完成后,通過狀態、通知和回調來通知調用者。
- Netty 中的 I/O 操作是異步的,包括 Bind、Write、Connect 等操作會簡單的返回一個 ChannelFuture。調用者并不能立刻獲得結果,而是通過
Future-Listener
機制,用戶可以方便地主動獲取或者通過通知機制獲得 IO 操作結果。 - Netty 的異步模型是建立在 future 和 callback 的之上的。callback 就是回調。重點說 Future,它的核心思想是:假設一個方法 fun,計算過程可能非常耗時,等待 fun 返回顯然不合適。那么可以在調用 fun 的時候,立馬返回一個 Future,后續可以通過 Future 去監控方法 fun 的處理過程(即:Future-Listener 機制)。
- Future說明:表示異步的執行結果,可以通過它提供的方法來檢測執行是否完成,比如檢索計算等等。
ChannelFuture
是一個接口:public interface ChannelFuture extends Future<Void>
,可以添加自己的監聽器,當監聽的事件發生時,就會通知到監聽器。
工作原理示意圖
- Future-Listener機制:當 Future 對象剛剛創建時,處于非完成狀態,調用者可以通過返回的 ChannelFuture 來獲取操作執行的狀態,注冊監聽函數來執行完成后的操作。常見的操作如下:
- 通過
isDone
方法來判斷當前操作是否完成; - 通過
isSuccess
方法來判斷已完成的當前操作是否成功; - 通過
getCause
方法來獲取已完成的當前操作失敗的原因; - 通過
isCancelled
方法來判斷已完成的當前操作是否被取消; - 通過
addListener
方法來注冊監聽器,當操作已完成(isDone方法返回完成),將會通知指定的監聽器;若 Future 對象已完成,則通知指定的監聽器。
- 通過
- Netty入門案例:HTTP服務。
- TestServer.java
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 TestServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new TestServerInitializer());
ChannelFuture channelFuture = serverBootstrap.bind(23333).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
- TestServerInitializer.java
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;
public class TestServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//向管道加入處理器
//獲取管道
ChannelPipeline pipeline = ch.pipeline();
//加入一個netty 提供的httpServerCodec codec =>[coder - decoder]
//HttpServerCodec 說明
//1、HttpServerCodec 是netty提供的處理http的編-解碼器
pipeline.addLast("MyHttpServerCodec", new HttpServerCodec());
//2、增加一個自定義的handler
pipeline.addLast("MyTestHttpServerHandler", new TestHttpServerHandler());
System.out.println("ok~~~~");
}
}
- TestHttpServerHandler.java
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import java.net.URI;
/**
* 1、SimpleChannelInboundHandler 是 ChannelInboundHandlerAdapter 的抽象子類
* 2、HttpObject 客戶端和服務器端相互通訊的數據被封裝成 HttpObject
*/
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
//讀取客戶端數據
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
System.out.println("對應的channel=" + ctx.channel() + ",pipeline=" + ctx
.pipeline() + ",通過pipeline獲取channel" + ctx.pipeline().channel());
System.out.println("當前ctx的handler=" + ctx.handler());
//判斷 msg 是不是 http request請求
if (msg instanceof HttpRequest) {
System.out.println("ctx 類型=" + ctx.getClass());
System.out.println("pipeline hashcode" + ctx.pipeline().hashCode() + ",TestHttpServerHandler hash=" + this.hashCode());
System.out.println("msg 類型=" + msg.getClass());
System.out.println("客戶端地址" + ctx.channel().remoteAddress());
HttpRequest httpRequest = (HttpRequest) msg;
//獲取uri,過濾指定的資源
URI uri = new URI(httpRequest.uri());
if ("/favicon.ico".equals(uri.getPath())) {
System.out.println("請求了 favicon.ico,不做響應");
return;
}
//回復信息給瀏覽器 [http協議]
ByteBuf content = Unpooled.copiedBuffer("hello,我是服務器", CharsetUtil.UTF_8);
//構造一個http的響應,即 http response
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
//將構建好 response 并返回
ctx.writeAndFlush(response);
}
}
}
- Netty 中 Bootstrap 類是客戶端程序的啟動引導類,ServerBootstrap 是服務端啟動引導類。常見的方法有:
-
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup)
:該方法用于服務器端,用來設置兩個 EventLoop。 -
public B group(EventLoopGroup group)
:該方法用于客戶端,用來設置一個 EventLoop。 -
public B channel(Class<? extends C> channelClass)
:該方法用來設置一個服務器端的通道實現。 -
public <T> B option(ChannelOption<T> option, T value)
:用來給 ServerChannel 添加配置。 -
public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value)
:用來給接收到的通道添加配置。 -
public ServerBootstrap childHandler(ChannelHandler childHandler)
:該方法用來設置業務處理類(自定義的handler)。 -
public ChannelFuture bind(int inetPort)
:該方法用于服務器端,用來設置占用的端口號。 -
public ChannelFuture connect(String inetHost, int inetPort)
:該方法用于客戶端,用來連接服務器端。
-
- Netty 中所有的 IO 操作都是異步的,不能立刻得知消息是否被正確處理,但可以過一會等它執行完成或者直接注冊一個監聽(通過 Future 和 ChannelFutures來實現),當操作執行成功或失敗時會自動觸發注冊的監聽事件。常見的方法有:
-
Channel channel()
:返回當前正在進行 IO 操作的通道。 -
ChannelFuture sync()
:等待異步操作執行完畢。
-
-
Channel
是Netty 網絡通信的組件,能夠用于執行網絡 I/O 操作。 - 通過 Channel 可獲得當前網絡連接的通道的狀態。
- 通過 Channel 可獲得網絡連接的配置參數(例如接收緩沖區大小)。
- Channel 提供異步的網絡 I/O 操作(如建立連接,讀寫,綁定端口),異步調用意味著任何 I/O 調用都將立即返回,并且不保證在調用結束時所請求的 I/O 操作已完成。調用立即返回一個 ChannelFuture 實例,通過注冊監聽器到 ChannelFuture 上,可以 I/O 操作成功、失敗或取消時回調通知調用方。Channel 支持關聯 I/O 操作與對應的處理程序。不同協議、不同的阻塞類型的連接都有不同的 Channel 類型與之對應,常用的 Channel 類型:
-
NioSocketChannel
:異步的客戶端 TCP Socket 連接。 -
NioServerSocketChannel
:異步的服務器端 TCP Socket 連接。 -
NioDatagramChannel
:異步的 UDP 連接。 -
NioSctpChannel
:異步的客戶端 Sctp 連接。 -
NioSctpServerChannel
:異步的 Sctp 服務器端連接,這些通道涵蓋了 UDP 和 TCP 網絡 IO 以及文件 IO。
-
- Netty 基于 Selector 對象實現 I/O 多路復用,通過 Selector 一個線程可以監聽多個連接的 Channel 事件。
- 當向一個 Selector 中注冊 Channel 后,Selector 內部的機制就可以自動不斷地查詢(Select)這些注冊的 Channel 是否有已就緒的 I/O 事件(例如可讀,可寫,網絡連接完成等),這樣程序就可以很簡單地使用一個線程高效地管理多個 Channel。
- ChannelHandler 是一個接口,用于處理 I/O 事件或攔截 I/O 操作,并將其轉發到其 ChannelPipeline(業務處理鏈)中的下一個處理程序。
- ChannelHandler 本身并沒有提供很多方法,因為這個接口有許多的方法需要實現,方便使用期間,可以繼承它的子類。
ChannelHandler 及其實現類
- 我們經常需要自定義一個 Handler 類去繼承 ChannelInboundHandlerAdapter,然后通過重寫相應方法實現業務邏輯,接下來看看一般都需要重寫哪些方法:
- ChannelPipeline 是一個 Handler 的集合,它負責處理和攔截 inbound 或者 outbound 的事件和操作,相當于一個貫穿 Netty 的鏈。(也可以這樣理解:ChannelPipeline 是保存 ChannelHandler 的 List,用于處理或攔截 Channel 的入站事件和出站操作)
- ChannelPipeline 實現了一種高級形式的攔截過濾器模式,使用戶可以完全控制事件的處理方式,以及 Channel 中各個的 ChannelHandler 如何相互交互。
- 在 Netty 中每個 Channel 都有且僅有一個 ChannelPipeline 與之對應,它們的組成關系如下:
- ChannelPipeline 常用的相關方法:
ChannelPipeline addFirst(ChannelHandler... handlers)
:把一個業務處理類(handler)添加到鏈中的第一個位置;ChannelPipeline addLast(ChannelHandler... handlers)
:把一個業務處理類(handler)添加到鏈中的最后一個位置。 - ChannelHandlerContext 中包含一個具體的事件處理器 ChannelHandler,同時 ChannelHandlerContext 中也綁定了對應的 pipeline 和 Channel 的信息,方便對 ChannelHandler 進行調用。常用的方法如下:
-
ChannelFuture close()
:關閉通道。 -
ChannelOutboundInvoker flush()
:刷新。 -
ChannelFuture writeAndFlush(Object msg)
:將數據寫到ChannelPipeline 中當前 ChannelHandler 的下一個 ChannelHandler 開始處理(出站)。
-
- Netty 在創建 Channel 實例后,一般都需要設置
ChannelOption
參數。
ChannelOption 參數如下:-
ChannelOption.SO_BACKLOG
:對應 TCP/IP 協議 listen 函數中的 backlog 參數,用來初始化服務器可連接隊列大小。因為服務端處理客戶端連接請求是順序處理的,所以同一時間只能處理一個客戶端連接,多個客戶端來的時候,服務端將不能處理的客戶端連接請求放在隊列中等待處理,backlog 參數指定了隊列的大小。 -
ChannelOption.SO_KEEPALIVE
:一直保持連接活動狀態。
-
-
EventLoopGroup
是一組 EventLoop 的抽象,Netty 為了更好地利用多核 CPU 資源,一般會有多個 EventLoop 同時工作,每個 EventLoop 維護著一個 Selector 實例。 -
EventLoopGroup
提供 next 接口,可以從組里面按照一定規則獲取其中一個 EventLoop 來處理任務。在 Netty 服務器端編程中,一般都需要提供兩個 EventLoopGroup,例如:BossEventLoopGroup 和 WorkerEventLoopGroup。常用的方法如下:-
public NioEventLoopGroup()
:構造方法。 -
public Future<?> shutdownGracefully()
:斷開連接,關閉線程。
-
- 通常一個服務端口即一個 ServerSocketChannel 對應一個 Selector 和一個 EventLoop 線程。BossEventLoop 負責接收客戶端的連接并將 SocketChannel 交給 WorkerEventLoopGroup 來進行 IO 處理,如下圖所示:
- Netty 提供一個專門用來操作緩沖區(即 Netty 的數據容器)的工具類,常用的方法:
public static ByteBuf copiedBuffer(CharSequence string, Charset charset)
,類似于 NIO 中的 ByteBuffer 但有區別。
- NettyByteBuf01.java
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
public class NettyByteBuf01 {
public static void main(String[] args) {
//創建一個ByteBuf
//1、創建 ByteBuf 對象,該對象包含一個數組,是一個byte[10]
//2、在netty的buffer中,不需要使用flip進行反轉,其底層維護了 readerIndex 和 writerIndex
//3、通過 readerIndex、writerIndex 和 capacity,將buffer分成三個區域
//0 到 readerIndex:表示已經讀取的區域
//readerIndex 到 writerIndex:表示可讀的區域
//writerIndex 到 capacity:表示可寫的區域
ByteBuf buffer = Unpooled.buffer(10);
for (int i = 0; i < 10; i++) {
buffer.writeByte(i);
}
System.out.println("capacity=" + buffer.capacity());//10
/*for (int i = 0; i < buffer.capacity(); i++) {
System.out.println(buffer.getByte(i));
}*/
for (int i = 0; i < buffer.capacity(); i++) {
System.out.println(buffer.readByte());
}
System.out.println("執行完畢");
}
}
- NettyByteBuf02.java
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.nio.charset.StandardCharsets;
public class NettyByteBuf02 {
public static void main(String[] args) {
//創建ByteBuf
ByteBuf byteBuf = Unpooled.copiedBuffer("hello,world!", StandardCharsets.UTF_8);
if (byteBuf.hasArray()) { // true
byte[] content = byteBuf.array();
//將 content 轉成字符串
System.out.println(new String(content, StandardCharsets.UTF_8));
System.out.println("byteBuf=" + byteBuf);
System.out.println(byteBuf.arrayOffset()); // 0
System.out.println(byteBuf.readerIndex()); // 0
System.out.println(byteBuf.writerIndex()); // 12
System.out.println(byteBuf.capacity()); // 64
//System.out.println(byteBuf.readByte()); //注意:若讀取一個,則下面可讀取的字節數將減1
System.out.println(byteBuf.getByte(0)); // 104
int len = byteBuf.readableBytes(); //返回可讀的字節數 :12
System.out.println("len=" + len);
//使用for取出各個字節,注意:不會改變readerIndex的值
for (int i = 0; i < len; i++) {
System.out.print((char) byteBuf.getByte(i));
}
System.out.println();
//按照某個范圍讀取
System.out.println(byteBuf.getCharSequence(0, 4, StandardCharsets.UTF_8));
System.out.println(byteBuf.getCharSequence(4, 6, StandardCharsets.UTF_8));
}
}
}