轉載:Java 編程思想(七) BIO/NIO/AIO的區別(Reactor和Proactor的區別)
轉載:Java 編程思想(八)BIO/NIO/AIO的具體實現
轉載:源碼之下無祕密 ── 做最好的 Netty 源碼分析教程
轉載:架構設計:系統間通信(6)——IO通信模型和Netty 上篇
轉載:架構設計:系統間通信(7)——IO通信模型和Netty 下篇
轉載:Netty實現原理淺析
一、Netty
Netty是由JBOSS提供的一個java開源框架。Netty提供異步的、事件驅動的網絡應用程序框架和工具,用以快速開發高性能、高可靠性的網絡服務器和客戶端程序。
二、Netty架構
Netty中包含的內容繁多,支持NIO和BIO模式,支持多種傳輸協議(HTTP/ProtoBuf/文本&二進制傳輸協議等),除此之外也支持安全特性,其特點如下:
1. 豐富的緩衝實現:
Netty使用自建的buffer API,而不是使用 NIO的 ByteBuffer來代表一個連續的字節序列。與 ByteBuffer相比這種方式擁有明顯的優勢。Netty使用新的 buffer 類型ChannelBuffer,ChannelBuffer被設計爲一個可從底層解決 ByteBuffer 問題,並可滿足日常網絡應用開發需要的緩衝類型。這些很酷的特性包括:1. 如果需要,允許使用自定義的緩衝類型。
2. 複合緩衝類型中內置的透明的零拷貝實現。
3. 開箱即用的動態緩衝類型,具有像 StringBuffer 一樣的動態緩衝能力。
4. 不再需要調用的 flip()方法。
5. 正常情況下具有比 ByteBuffer更快的響應速度。
2.統一的異步I/O API
Netty有一個叫做 Channel 的統一的異步 I/O編程接口,這個編程接口抽象了所有點對點的通信操作。也就是說,如果你的應用是基於 Netty的某一種傳輸實現,那麼同樣的,你的應用也可以運行在 Netty 的另一種傳輸實現上。Netty提供了幾種擁有相同編程接口的基本傳輸實現: NIO-based TCP/IP transport (See org.jboss.netty.channel.socket.nio),
OIO-based TCP/IP transport (See org.jboss.netty.channel.socket.oio),
OIO-based UDP/IP transport, and
Local transport (See org.jboss.netty.channel.local).
切換不同的傳輸實現通常只需對代碼進行幾行的修改調整,例如選擇一個不同的 ChannelFactory實現。
此外,你甚至可以利用新的傳輸實現沒有寫入的優勢,只需替換一些構造器的調用方法即可,例如串口通信。而且由於核心 API 具有高度的可擴展性,你還可以完成自己的傳輸實現。
3.基於攔截鏈模式的事件觸發模型
一個定義良好並具有擴展能力的事件模型是事件驅動開發的必要條件。 Netty具有定義良好的I/O事件模型。由於嚴格的層次結構區分了不同的事件類型,因此 Netty也允許你在不破壞現有代碼的情況下實現自己的事件類型。這是與其他框架相比另一個不同的地方。
很多 NIO 框架沒有或者僅有有限的事件模型概念;在你試圖添加一個新的事件類型的時候常常需要修改已有的代碼,或者根本就不允許你進行這種擴展。 在一個ChannelPipeline內部一個ChannelEvent被一組ChannelHandler處理。這個管道是攔截過濾器 模式的一種高級形式的實現,因此對於一個事件如何被處理以及管道內部處理器間的交互過程,你都將擁有絕對的控制力。
Netty使用主從Reactor模式,在實現上,Netty中的Boss類充當mainReactor,NioWorker類充當subReactor(默認
NioWorker的個數是Runtime.getRuntime().availableProcessors())。在處理新來的請求 時,NioWorker讀完已收到的數據到ChannelBuffer中,之後觸發ChannelPipeline中的ChannelHandler流。
4.適用快速開發的高級組件:
4.1.Codec框架:
從業務邏輯代碼中分離協議處理部分總是一個很不錯的想法。然而如果一切從零開始便會遭遇到實現上的複雜性。你不得不處理分段的消息。一些協議是多層的(例如構建在其他低層協議之上的協議)。一些協議過於複雜以致難以在一臺主機(single state machine)上實現。 因此,一個好的網絡應用框架應該提供一種可擴展,可重用,可單元測試並且是多層的 codec 框架,爲用戶提供易維護的 codec 代碼。
Netty提供了一組構建在其核心模塊之上的 codec 實現,這些簡單的或者高級的 codec 實現幫你解決了大部分在你進行協議處理開發過程會遇到的問題,無論這些協議是簡單的還是複雜的,二進制的或是簡單文本的。
4.2.SSL/TSL支持:
不同於傳統阻塞式的 I/O實現,在 NIO模式下支持 SSL 功能是一個艱難的工作。你不能只是簡單的包裝一下流數據並進行加密或解密工作,你不得不借助於javax.net.ssl.SSLEngine,SSLEngine是一個有狀態的實現,其複雜性不亞於 SSL 自身。你必須管理所有可能的狀態,例如密碼套件,密鑰協商(或重新協商),證書交換以及認證等。此外,與通常期望情況相反的是 SSLEngine 甚至不是一個絕對的線程安全實現。在 Netty內部,SslHandler 封裝了所有艱難的細節以及使用 SSLEngine 可能帶來的陷阱。你所做的僅是配置並將該 SslHandler 插入到你的ChannelPipeline中。同樣 Netty 也允許你實現像 StartTlS 那樣所擁有的高級特性,這很容易。
4.3.HTTP實現:
HTTP 無疑是互聯網上最受歡迎的協議,並且已經有了一些例如 Servlet 容器這樣的 HTTP 實現。因此,爲什麼 Netty還要在其核心模塊之上構建一套 HTTP 實現?與現有的 HTTP 實現相比 Netty 的 HTTP 實現是相當與衆不同的。在 HTTP 消息的低層交互過程中你將擁有絕對的控制力。這是因爲 Netty的 HTTP 實現只是一些HTTP codec 和 HTTP 消息類的簡單組合,這裏不存在任何限制——例如那種被迫選擇的線程模型。你可以隨心所欲的編寫那種可以完全按照你期望的工作方式工作的客戶端或服務器端代碼。這包括線程模型,連接生命期,快編碼,以及所有 HTTP 協議允許你做的,所有的一切,你都將擁有絕對的控制力。
由於這種高度可定製化的特性,你可以開發一個非常高效的 HTTP 服務器,例如:
要求持久化鏈接以及服務器端推送技術的聊天服務(e.g. Comet )
需要保持鏈接直至整個文件下載完成的媒體流服務(e.g. 2小時長的電影)
需要上傳大文件並且沒有內存壓力的文件服務(e.g. 上傳1GB 文件的請求)
支持大規模 mash-up 應用以及數以萬計連接的第三方 web services 異步處理平臺
4.4.Google Protocol Buffer整合:
Google Protocol Buffers 是快速實現一個高效的二進制協議的理想方案。通過使用 ProtobufEncoder 和ProtobufDecoder,你可以把 Google Protocol Buffers 編譯器 (protoc)生成的消息類放入到Netty 的 codec實現中。三、Netty的線程模型
1. 服務端線程模型
Netty的做法是服務端監聽線程和IO線程分離,即主從Reactor的模型:
Netty的主從Reactor線程模型創建過程分爲如下幾步:
1. 從用戶線程發起創建服務端操作
public class TestTCPNetty {
static {
BasicConfigurator.configure();
}
public static void main(String[] args) throws Exception {
ServerBootstrap serverBootstrap = new ServerBootstrap(); //這就是主要的服務啓動器
EventLoopGroup bossLoopGroup = new NioEventLoopGroup(1); //BOSS線程池
ThreadFactory threadFactory = new DefaultThreadFactory("work thread pool"); //WORK線程池
int processorsNumber = Runtime.getRuntime().availableProcessors(); //CPU個數
EventLoopGroup workLoogGroup = new NioEventLoopGroup(processorsNumber * 2, threadFactory, SelectorProvider.provider());
serverBootstrap.group(bossLoopGroup , workLoogGroup);
//如果是以下的申明方式,說明BOSS線程和WORK線程共享一個線程池
//serverBootstrap.group(workLoogGroup);
serverBootstrap.channel(NioServerSocketChannel.class);
//當然也可以這樣創建(那個SelectorProvider是不是感覺很熟悉?)
//serverBootstrap.channelFactory(new ChannelFactory<NioServerSocketChannel>() {
// @Override
// public NioServerSocketChannel newChannel() {
// return new NioServerSocketChannel(SelectorProvider.provider());
// }
//});
//爲了演示,這裏我們設置了一組簡單的ByteArrayDecoder和ByteArrayEncoder
//Netty的特色就在這一連串“通道水管”中的“處理器”
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ByteArrayEncoder());
ch.pipeline().addLast(new TCPServerHandler());
ch.pipeline().addLast(new ByteArrayDecoder());
}
});
//========================設置netty服務器綁定的ip和端口
serverBootstrap.option(ChannelOption.SO_BACKLOG, 128);
serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
serverBootstrap.bind(new InetSocketAddress("0.0.0.0", 83));
//還可以監控多個端口
//serverBootstrap.bind(new InetSocketAddress("0.0.0.0", 84));
}
}
通常情況下,服務端的創建是在用戶進程啓動的時候進行,因此一般由Main函數或者啓動類負責創建,服務端的創建由業務線程負責完成。在創建服務端的時候實例化了2個EventLoopGroup,1個EventLoopGroup實際就是一個EventLoop線程組,負責管理EventLoop的申請和釋放。
EventLoopGroup管理的線程數可以通過構造函數設置,如果沒有設置,默認取-Dio.netty.eventLoopThreads,如果該系統參數也沒有指定,則爲可用的CPU內核數 × 2。
bossGroup線程組實際就是Acceptor線程池,負責處理客戶端的TCP連接請求,如果系統只有一個服務端端口需要監聽,則建議bossGroup線程組線程數設置爲1。
workerGroup是真正負責I/O讀寫操作的線程組,通過ServerBootstrap的group方法進行設置,用於後續的Channel綁定。當WORK線程發現操作系統有一個它感興趣的IO事件時(例如SocketChannel的READ事件)則調用相應的ChannelHandler事件。當某個channel失效後(例如顯示調用ctx.close())這個channel將從綁定的EventLoop中被剔除。
EventLoopGroup 的初始化過程:
1. EventLoopGroup(其實是MultithreadEventExecutorGroup) 內部維護一個類型爲EventExecutorchildren 數組, 其大小是 nThreads, 這樣就構成了一個線程池。
2. 如果我們在實例化NioEventLoopGroup時, 如果指定線程池大小, 則 nThreads 就是指定的值, 反之是處理器核心數 * 2
3.MultithreadEventExecutorGroup中會調用 newChild 抽象方法來初始化 children 數組
4. 抽象方法 newChild 是在 NioEventLoopGroup 中實現的, 它返回一個NioEventLoop實例.
NioEventLoop 屬性:
SelectorProvider provider 屬性: NioEventLoopGroup 構造器中通過 SelectorProvider.provider() 獲取一個 SelectorProvider
Selector selector 屬性: NioEventLoop 構造器中通過調用通過 selector = provider.openSelector() 獲取一個selector對象.
2. Acceptor線程綁定監聽端口,啓動NIO服務端
Channel createChannel() {
EventLoop eventLoop = group().next();
return channelFactory().newChannel(eventLoop, childGroup);
}
其中group()返回的就是bossGroup,它的next方法用於從線程組中獲取可用線程:
@Override
public EventExecutor next() {
return children[Math.abs(childIndex.getAndIncrement() % children.length)];
}
服務端Channel創建完成之後,將其註冊到多路複用器Selector上,用於接收客戶端的TCP連接,核心代碼如下:
public NioServerSocketChannel(EventLoop eventLoop, EventLoopGroup childGroup) {
super(null, eventLoop, childGroup, newSocket(), SelectionKey.OP_ACCEPT);
config = new DefaultServerSocketChannelConfig(this, javaChannel.socket());
}
一個 NioSocketChannel 所需要做的工作:
-
調用 NioSocketChannel.newSocket(DEFAULT_SELECTOR_PROVIDER) 打開一個新的 Java NIO SocketChannel
-
AbstractChannel(Channel parent) 中初始化AbstractChannel的屬性:
-
parent 屬性置爲 null
-
unsafe 通過newUnsafe() 實例化一個 unsafe 對象, 它的類型是 AbstractNioByteChannel.NioByteUnsafe內部類
-
pipeline 是 new DefaultChannelPipeline(this) 新創建的實例.
Each channel has its own pipeline and it is created automatically when a new channel is created.
-
-
AbstractNioChannel 中的屬性:
-
SelectableChannel ch 被設置爲 Java SocketChannel, 即 NioSocketChannel#newSocket 返回的 Java NIO SocketChannel.
-
readInterestOp 被設置爲 SelectionKey.OP_READ
-
SelectableChannel ch 被配置爲非阻塞的 ch.configureBlocking(false)
-
-
NioSocketChannel 中的屬性:
-
SocketChannelConfig config = new NioSocketChannelConfig(this, socket.socket())
-
首先看Acceptor如何處理客戶端的接入:
try{
int readyOps = k.readyOps();
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
if (!ch.isOpen()) {
return;
}
}
}
調用unsafe的read()方法,對於NioServerSocketChannel,它調用了NioMessageUnsafe的read()方法,代碼如下:
Throwable exception = null;
try{
for (; ; ) {
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
}
}
}
最終它會調用NioServerSocketChannel的doReadMessages方法,代碼如下:
protected int doReadMessages(List<Object> buf) throws Exception {
SocketChannel ch = javaChannel().accept();
try{
if (ch != null) {
buf.add(new NioSocketChannel(this, childEventLoopGroup().next(), ch));
return 1;
}
}
}
其中childEventLoopGroup就是之前的workerGroup, 從中選擇一個I/O線程負責網絡消息的讀寫。
4. 選擇IO線程之後,將SocketChannel註冊到多路複用器上,監聽READ操作。
protected AbstractNioByteChannel(Channel parent, EventLoop eventLoop, SelectableChannel ch) {
super(parent, eventLoop, ch, SelectionKey.OP_READ);
}
int readyOps = k.readyOps();
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
if (!ch.isOpen()) {
return;
}
}
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
ch.unsafe().forceFlush();
}
2. 客戶端線程模型
相比於服務端,客戶端的線程模型簡單一些,它的工作原理如下:
1. 由用戶線程發起客戶端連接
EventLoopGroup group = new NioEventLoopGroup();
try{
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChanneInitialize<SocketChannel>(){
@Override
public void initChannel(SockectChannel ch) throws Exception {
ch.pipeline().addLast(new EchoClientHandler(firstMessageSize));
}
});
ChannelFuture f = b.connect(host, port).sync();
}
相比於服務端,客戶端只需要創建一個EventLoopGroup,因爲它不需要獨立的線程去監聽客戶端連接,也沒必要通過一個單獨的客戶端線程去連接服務端。Netty是異步事件驅動的NIO框架,它的連接和所有IO操作都是異步的,因此不需要創建單獨的連接線程。相關代碼如下:@Override
Channel createChannel(){
EventLoop eventLoop = group().next();
return channelFactory().newChannel(eventLoop);
}
當前的group()就是之前傳入的EventLoopGroup,從中獲取可用的IO線程EventLoop,然後作爲參數設置到新創建的NioSocketChannel中。
2. 發起連接操作,判斷連接結果
@Override
protected boolean doConnect(SocketAddress remoteAddress, SockectAddress localAddress) {
if (localAddress != null) {
javaChannel().socket().bind(localAddress);
}
boolean success = false;
try{
boolean connected = javaChannel().connect(remoteAddress);
if (!connected) {
SelectionKey().interestOps(SelectionKey.OP_CONNECT);
}
success = true;
return connected;
} finally {
if (!success) {
doClose();
}
}
}
判斷連接結果,如果沒有連接成功,則監聽連接網絡操作位SelectionKey.OP_CONNECT。如果連接成功,則調用pipeline().fireChannelActive()將監聽位修改爲READ。3. 由NioEventLoop的多路複用器輪詢連接操作結果
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
判斷連接結果,如果或連接成功,重新設置監聽位爲READ:
@Override
protected void doBeginRead() throws Exception{
if (inputShutdown) {
return;
}
final SelectionKey selectionKey = this.selectionKey;
if (!selectionKey.isValid) {
return;
}
final int interestOps = selectionKey.interestOps();
if ((interestOps & readInterestOp) == 0) {
selectionKey.interestOps(interestOps | readInterestOp);
}
}
4. 由NioEventLoop線程負責I/O讀寫,同服務端。總結:客戶端創建,線程模型如下:
1. 由用戶線程負責初始化客戶端資源,發起連接操作;
2. 如果連接成功,將SocketChannel註冊到IO線程組的NioEventLoop線程中,監聽讀操作位;
3. 如果沒有立即連接成功,將SocketChannel註冊到IO線程組的NioEventLoop線程中,監聽連接操作位;
4. 連接成功之後,修改監聽位爲READ,但是不需要切換線程。
四、Netty的通信過程
NIO的通信步驟:
①創建ServerSocketChannel,爲其配置非阻塞模式。
②綁定監聽,配置TCP參數,錄入backlog大小等。
③創建一個獨立的IO線程,用於輪詢多路複用器Selector。
④創建Selector,將之前創建的ServerSocketChannel註冊到Selector上,並設置監聽標識位SelectionKey.OP_ACCEPT。
⑤啓動IO線程,在循環體中執行Selector.select()方法,輪詢就緒的通道。
⑥當輪詢到處於就緒狀態的通道時,需要進行操作位判斷,如果是ACCEPT狀態,說明是新的客戶端接入,則調用accept方法接收新的客戶端。
⑦設置新接入客戶端的一些參數,如非阻塞,並將其繼續註冊到Selector上,設置監聽標識位等。
⑧如果輪詢的通道標識位是READ,則進行讀取,構造Buffer對象等。
⑨更細節的問題還有數據沒發送完成繼續發送的問題......
Netty通信的步驟:
①創建兩個NIO線程組,一個專門用於網絡事件處理(接受客戶端的連接),另一個則進行網絡通信的讀寫。
②創建一個ServerBootstrap對象,配置Netty的一系列參數,例如接受傳出數據的緩存大小等。
③創建一個用於實際處理數據的類ChannelInitializer,進行初始化的準備工作,比如設置接受傳出數據的字符集、格式以及實際處理數據的接口。
④綁定端口,執行同步阻塞方法等待服務器端啓動即可。
五、Netty的責任鏈ChannelPipeline/ChannelHandler
Channel通道。
可以使用JAVA NIO中的Channel去初次理解,但實際上它的意義和JAVA NIO中的通道意義還不一樣。我們可以理解成:“更抽象、更豐富”。如下如所示:
Netty中的Channel專門代表網絡通信,這個和JAVA NIO框架中的Channel不一樣,後者中還有類似FileChannel本地文件IO通道。由於前者專門代表網絡通信,所以它是由客戶端地址 + 服務器地址 + 網絡操作狀態構成的,請參見io.netty.channel.Channel接口的定義。
每一個Netty中的Channel,比JAVA NIO中的Channel更抽象。這是爲什麼呢?在Netty中,不止封裝了JAVA NIO的IO模型,還封裝了JAVA BIO的阻塞同步IO通信模型。將他們在表現上都抽象成Channel了。這就是爲什麼Netty中有io.netty.channel.oio.AbstractOioChannel這樣的抽象類。
從上圖我們也可以看出,每一個Channel都與一個ChannelPipeline對應。 而且在ChannelPipeline 中又維護了一個由ChannelHandlerContext 組成的雙向鏈表。這個鏈表的頭是HeadContext, 鏈表的尾是
TailContext, 並且每個 ChannelHandlerContext又與一個
ChannelHandler一一對應。
ChannelPipeline和ChannelHandler
Netty中的每一個Channel,都有一個獨立的ChannelPipeline,中文稱爲“通道水管”。只不過這個水管是雙向的裏面流淌着數據,數據可以通過這個“水管”流入到服務器,也可以通過這個“水管”從服務器流出。
在ChannelPipeline中,有若干的過濾器。我們稱之爲“ChannelHandler”(處理器或者過濾器)。同“流入”和“流出”的概念向對應:用於處理/過濾 流入數據的ChannelHandler,稱之爲“ChannelInboundHandler”;用於處理/過濾
流出數據的ChannelHandler,稱之爲“ChannelOutboundHandler”。
inbound 事件和 outbound 事件的流向是不一樣的, inbound 事件的流行是從下至上, 而 outbound 剛好相反, 是從上到下. 並且 inbound 的傳遞方式是通過調用相應的ChannelHandlerContext.fireIN_EVT() 方法, 而 outbound 方法的的傳遞方式是通過調用ChannelHandlerContext.OUT_EVT() 方法. 例如 ChannelHandlerContext.fireChannelRegistered() 調用會發送一個 ChannelRegistered 的 inbound 給下一個ChannelHandlerContext, 而 ChannelHandlerContext.bind 調用會發送一個 bind 的 outbound 事件給 下一個 ChannelHandlerContext。
Inbound 事件傳播方法有:
ChannelHandlerContext.fireChannelRegistered()
ChannelHandlerContext.fireChannelActive()
ChannelHandlerContext.fireChannelRead(Object)
ChannelHandlerContext.fireChannelReadComplete()
ChannelHandlerContext.fireExceptionCaught(Throwable)
ChannelHandlerContext.fireUserEventTriggered(Object)
ChannelHandlerContext.fireChannelWritabilityChanged()
ChannelHandlerContext.fireChannelInactive()
ChannelHandlerContext.fireChannelUnregistered()
Oubound 事件傳輸方法有:ChannelHandlerContext.bind(SocketAddress, ChannelPromise)
ChannelHandlerContext.connect(SocketAddress, SocketAddress, ChannelPromise)
ChannelHandlerContext.write(Object, ChannelPromise)
ChannelHandlerContext.flush()
ChannelHandlerContext.read()
ChannelHandlerContext.disconnect(ChannelPromise)
ChannelHandlerContext.close(ChannelPromise)
對於 Outbound事件:
1. Outbound 事件是請求事件(由 Connect 發起一個請求, 並最終由 unsafe 處理這個請求)
2. Outbound 事件的發起者是 Channel
3. Outbound 事件的處理者是 unsafe
4. Outbound 事件在 Pipeline 中的傳輸方向是 tail -> head.
5. 在 ChannelHandler 中處理事件時, 如果這個 Handler 不是最後一個 Hander, 則需要調用 ctx.xxx(例如 ctx.connect) 將此事件繼續傳播下去. 如果不這樣做, 那麼此事件的傳播會提前終止.
6. Outbound 事件流: Context.OUT_EVT -> Connect.findContextOutbound -> nextContext.invokeOUT_EVT -> nextHandler.OUT_EVT -> nextContext.OUT_EVT
對於 Inbound 事件:
1. Inbound 事件是通知事件, 當某件事情已經就緒後, 通知上層.
2. Inbound 事件發起者是 unsafe
3. Inbound 事件的處理者是 Channel, 如果用戶沒有實現自定義的處理方法, 那麼Inbound 事件默認的處理者是 TailContext, 並且其處理方法是空實現.
4. Inbound 事件在 Pipeline 中傳輸方向是 head -> tail
5. 在 ChannelHandler 中處理事件時, 如果這個 Handler 不是最後一個 Hnalder, 則需要調用 ctx.fireIN_EVT (例如 ctx.fireChannelActive) 將此事件繼續傳播下去. 如果不這樣做, 那麼此事件的傳播會提前終止.
6. Outbound 事件流: Context.fireIN_EVT -> Connect.findContextInbound -> nextContext.invokeIN_EVT -> nextHandler.IN_EVT -> nextContext.fireIN_EVT
責任鏈和適配器的應用
數據在ChannelPipeline中有一個一個的Handler進行處理,並形成一個新的數據狀態。這是典型的“責任鏈”模式。需要注意,雖然數據管道中的Handler是按照順序執行的,但不代表某一個Handler會處理任何一種由“上一個handler”發送過來的數據。某些Handler會檢查傳來的數據是否符合要求,如果不符合自己的處理要求,則不進行處理。
I/O Request
via Channel or
ChannelHandlerContext
|
+---------------------------------------------------+---------------+
| ChannelPipeline | |
| \|/ |
| +---------------------+ +-----------+----------+ |
| | Inbound Handler N | | Outbound Handler 1 | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
| | \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler N-1 | | Outbound Handler 2 | |
| +----------+----------+ +-----------+----------+ |
| /|\ . |
| . . |
| ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
| [ method call] [method call] |
| . . |
| . \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler 2 | | Outbound Handler M-1 | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
| | \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler 1 | | Outbound Handler M | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
+---------------+-----------------------------------+---------------+
| \|/
+---------------+-----------------------------------+---------------+
| | | |
| [ Socket.read() ] [ Socket.write() ] |
| |
| Netty Internal I/O Threads (Transport Implementation) |
+-------------------------------------------------------------------+
我們可以實現ChannelInboundHandler接口或者ChannelOutboundHandler接口,來實現我們自己業務的“數據流入處理器”或者“數據流出”處理器。但是這兩個接口的事件方法是比較多的,例如ChannelInboundHandler接口一共有11個需要實現的接口方法(包括父級ChannelHandler的,我們在下一節講解Channel的生命週期時,回專門講到這些事件的執行順序和執行狀態),一般情況下我們不需要把這些方法全部實現。
所以Netty中增加了兩個適配器“ChannelInboundHandlerAdapter”和“ChannelOutboundHandlerAdapter”來幫助我們去實現我們只需要實現的事件方法。其他的事件方法我們就不需要關心了。
ChannelInboundHandler類舉例
HttpRequestDecoder:實現了Http協議的數據輸入格式的解析。這個類將數據編碼爲HttpMessage對象,並交由下一個ChannelHandler進行處理。
ByteArrayDecoder:最基礎的數據流輸入處理器,將所有的byte轉換爲ByteBuf對象(一般的實現類是:io.netty.buffer.UnpooledUnsafeDirectByteBuf)。我們進行一般的文本格式信息傳輸到服務器時,最好使用這個Handler將byte數組轉換爲ByteBuf對象。
DelimiterBasedFrameDecoder:這個數據流輸入處理器,會按照外部傳入的數據中給定的某個關鍵字符/關鍵字符串,重新將數據組裝爲新的段落併發送給下一個Handler處理器。後文中,我們將使用這個處理器進行TCP半包的問題。
ProtobufDecoder:支持Google Protocol Buffers 數據格式解析的處理器。
ChannelOutboundHandler類舉例
HttpResponseEncoder:這個類和HttpRequestDecoder相對應,是將服務器端HttpReponse對象的描述轉換成ByteBuf對象形式,並向外傳播。
ByteArrayEncoder:這個類和ByteArrayDecoder,是將服務器端的ByteBuf對象轉換成byte數組的形式,並向外傳播。一般也和ByteArrayDecoder對象成對使用。
還有支持標準的編碼成Google Protocol Buffers格式、JBoss Marshalling 格式、ZIP壓縮格式的ProtobufEncoder、ProtobufVarint32LengthFieldPrepender、MarshallingEncoder、JZlibEncoder等
Channel的生命週期
一個Channel的生命週期如下(這個生命週期的事件方法調用順序只是針對Netty封裝使用JAVA NIO框架時,並且在進行TCP/IP協議監聽時的事件方法調用順序。):
六、Netty的NioEventLoop
1. 責任鏈的串行設計
Netty採用了串行化設計理念,從消息的讀取、編碼以及後續Handler的執行,始終都由IO線程NioEventLoop負責,這就意外着整個流程不會進行線程上下文的切換,數據也不會面臨被併發修改的風險,對於用戶而言,甚至不需要了解Netty的線程細節,這確實是個非常好的設計理念。
NioEventLoop 繼承於 SingleThreadEventExecutor, 而 SingleThreadEventExecutor 中有一個 Queue<Runnable> taskQueue 字段, 用於存放添加的 Task。 在 Netty 中,每個 Task 都使用一個實現了 Runnable 接口的實例來表示。
一個NioEventLoop聚合了一個多路複用器Selector,因此可以處理成百上千的客戶端連接,Netty的處理策略是每當有一個新的客戶端接入,則從NioEventLoop線程組中順序獲取一個可用的NioEventLoop,當到達數組上限之後,重新返回到0,通過這種方式,可以基本保證各個NioEventLoop的負載均衡。一個客戶端連接只註冊到一個NioEventLoop上,這樣就避免了多個IO線程去併發操作它。
Netty通過串行化設計理念降低了用戶的開發難度,提升了處理性能。利用線程組實現了多個串行化線程水平並行執行,線程之間並沒有交集,這樣既可以充分利用多核提升並行處理能力,同時避免了線程上下文的切換和併發保護帶來的額外性能損耗。
2. NioEventLoop的執行過程
在 Java NIO 中所講述的 Selector 的使用流程:
-
通過 Selector.open() 打開一個 Selector.
-
將 Channel 註冊到 Selector 中, 並設置需要監聽的事件(interest set)
-
不斷重複:
-
調用 select() 方法
-
調用 selector.selectedKeys() 獲取 selected keys
-
迭代每個 selected key:
-
1) 從 selected key 中獲取 對應的 Channel 和附加信息(如果有的話)
-
2) 判斷是哪些 IO 事件已經就緒了, 然後處理它們. 如果是 OP_ACCEPT 事件, 則調用 "SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept()" 獲取 SocketChannel, 並將它設置爲 非阻塞的, 然後將這個 Channel 註冊到 Selector 中.
-
3) 根據需要更改 selected key 的監聽事件.
-
4) 將已經處理過的 key 從 selected keys 集合中刪除.
-
-
Netty對NIO的過程進行了封裝:
1. Netty 中是通過調用 SelectorProvider.openSocketChannel() 來打開一個新的 Java NIO SocketChannel:
private static SocketChannel newSocket(SelectorProvider provider) {
...
return provider.openSocketChannel();
}
2.在客戶端的 Channel 註冊過程中, 會有如下調用鏈:
Bootstrap.initAndRegister ->
AbstractBootstrap.initAndRegister ->
MultithreadEventLoopGroup.register ->
SingleThreadEventLoop.register ->
AbstractUnsafe.register ->
AbstractUnsafe.register0 ->
AbstractNioChannel.doRegister
在 AbstractUnsafe.register 方法中調用了 register0 方法:
@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
// 省略條件判斷和錯誤處理
AbstractChannel.this.eventLoop = eventLoop;
register0(promise);
}
register0 又調用了 AbstractNioChannel.doRegister:
@Override
protected void doRegister() throws Exception {
// 省略錯誤處理
selectionKey = javaChannel().register(eventLoop().selector, 0, this);
}
3. thread 的 run 循環當 EventLoop.execute 第一次被調用時, 就會觸發 startThread() 的調用, 進而導致了 EventLoop 所對應的 Java 線程的啓動,
下面是此線程的 run() 方法, 我已經把一些異常處理和收尾工作的代碼都去掉了. 這個 run 方法可以說是十分簡單, 主要就是調用了 SingleThreadEventExecutor.this.run() 方法. 而 SingleThreadEventExecutor.run() 是一個抽象方法, 它的實現在NioEventLoop
中,繼續跟蹤到 NioEventLoop.run() 方法,其源碼如下:
@Override
protected void run() {
for (;;) {
boolean oldWakenUp = wakenUp.getAndSet(false);
try {
if (hasTasks()) {
selectNow();
} else {
select(oldWakenUp);
if (wakenUp.get()) {
selector.wakeup();
}
}
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
processSelectedKeys();
runAllTasks();
} else {
final long ioStartTime = System.nanoTime();
processSelectedKeys();
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
break;
}
}
} catch (Throwable t) {
...
}
}
}
對該函數進行分析:
1. IO 事件的輪詢
首先, 在 run 方法中, 第一步是調用 hasTasks() 方法來判斷當前任務隊列中是否有任務,檢查了一下 taskQueue 是否爲空:
void selectNow() throws IOException {
try {
selector.selectNow();
} finally {
// restore wakup state if needed
if (wakenUp.get()) {
selector.wakeup();
}
}
}
當 taskQueue 中沒有任務時,那麼 Netty 可以阻塞地等待 IO 就緒事件; 而當 taskQueue 中有任務時, 我們自然地希望所提交的任務可以儘快地執行, 因此 Netty 會調用非阻塞的 selectNow() 方法, 以保證 taskQueue 中的任務儘快可以執行.
2. IO 事件的處理
IO事件的處理如下所示:
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
processSelectedKeys();
runAllTasks();
} else {
final long ioStartTime = System.nanoTime();
processSelectedKeys();
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
processSelectedKeys() 調用:查詢就緒的 IO 事件, 然後處理它;
runAllTasks():運行 taskQueue 中的任務。
ioRatio:表示此線程分配給 IO 操作所佔的時間比(即運行 processSelectedKeys 耗時在整個循環中所佔用的時間)。例如 ioRatio 默認是 50, 則表示 IO 操作和執行 task 的所佔用的線程執行時間比是 1 : 1。
private void processSelectedKeys() {
if (selectedKeys != null) {
processSelectedKeysOptimized(selectedKeys.flip());
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
}
這個方法中, 會根據 selectedKeys字段是否爲空, 而分別調用 processSelectedKeysOptimized或
processSelectedKeysPlain。private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
for (int i = 0;; i ++) {
final SelectionKey k = selectedKeys[i];
if (k == null) {
break;
}
selectedKeys[i] = null;
final Object a = k.attachment();
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
...
}
}
主要工作爲:迭代 selectedKeys 獲取就緒的 IO 事件, 然後爲每個事件都調用 processSelectedKey來處理它.private static void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final NioUnsafe unsafe = ch.unsafe();
...
try {
int readyOps = k.readyOps();
// 可讀事件
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
if (!ch.isOpen()) {
// Connection already closed - no need to handle write.
return;
}
}
// 可寫事件
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
}
// 連接建立事件
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
// See https://github.com/netty/netty/issues/924
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
processSelectedKey 中處理了三個事件, 分別是:1. OP_READ, 可讀事件, 即 Channel 中收到了新數據可供上層讀取.
2. OP_WRITE, 可寫事件, 即上層可以向 Channel 寫入數據.
3. OP_CONNECT, 連接建立事件, 即 TCP 連接已經建立, Channel 處於 active 狀態.
當就緒的 IO 事件是 OP_READ, 代碼會調用 unsafe.read() 方法,unsafe對象實質上是一個NioSocketChannelUnsafe實例, 負責的是 Channel 的底層 IO 操作,它的實現源碼如下:
@Override
public final void read() {
...
ByteBuf byteBuf = null;
int messages = 0;
boolean close = false;
try {
int totalReadAmount = 0;
boolean readPendingReset = false;
do {
byteBuf = allocHandle.allocate(allocator);
int writable = byteBuf.writableBytes();
int localReadAmount = doReadBytes(byteBuf);
// 檢查讀取結果.
...
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
...
totalReadAmount += localReadAmount;
// 檢查是否是配置了自動讀取, 如果不是, 則立即退出循環.
...
} while (++ messages < maxMessagesPerRead);
pipeline.fireChannelReadComplete();
allocHandle.record(totalReadAmount);
if (close) {
closeOnRead(pipeline);
close = false;
}
} catch (Throwable t) {
handleReadException(pipeline, byteBuf, t, close);
} finally {
}
}
read 方法其實歸納起來, 可以認爲做了如下工作:1. 分配 ByteBuf
2. 從 SocketChannel 中讀取數據
3. 調用 pipeline.fireChannelRead 發送一個 inbound 事件.
當調用了 pipeline.fireIN_EVT() 後, 那麼就產生了一個 inbound 事件, 此事件會以 head -> customContext -> tail 的方向依次流經 ChannelPipeline 中的各個 handler。
調用了 pipeline.fireChannelRead 後, 就是 ChannelPipeline 中所需要做的工作了,最終ChannelPipeLine調用Read相關的Handler進行數據處理完成流程。
3. 定時任務與時間輪算法
在Netty中,有很多功能依賴定時任務,比較典型的有兩種(這兩種也是Netty實現的多於NIO的特性功能):
1. 客戶端連接超時控制;
2. 鏈路空閒檢測。
一種比較常用的設計理念是在NioEventLoop中聚合JDK的定時任務線程池ScheduledExecutorService,通過它來執行定時任務。這樣做單純從性能角度看不是最優,原因有如下三點:
1. 在IO線程中聚合了一個獨立的定時任務線程池,這樣在處理過程中會存在線程上下文切換問題,這就打破了Netty的串行化設計理念;
2. 存在多線程併發操作問題,因爲定時任務Task和IO線程NioEventLoop可能同時訪問並修改同一份數據;
3. JDK的ScheduledExecutorService從性能角度看,存在性能優化空間。
最早面臨上述問題的是操作系統和協議棧,例如TCP協議棧,其可靠傳輸依賴超時重傳機制,因此每個通過TCP傳輸的 packet 都需要一個 timer來調度 timeout 事件。這類超時可能是海量的,如果爲每個超時都創建一個定時器,從性能和資源消耗角度看都是不合理的。
根據George Varghese和Tony Lauck 1996年的論文《Hashed and Hierarchical Timing Wheels: data structures to efficiently implement a timer facility》提出了一種定時輪的方式來管理和維護大量的timer調度。Netty的定時任務調度就是基於時間輪算法調度。定時輪是一種數據結構,其主體是一個循環列表,每個列表中包含一個稱之爲slot的結構,它的原理圖如下:
定時輪的工作原理可以類比於時鍾,如上圖箭頭(指針)按某一個方向按固定頻率輪動,每一次跳動稱爲一個tick。這樣可以看出定時輪由個3個重要的屬性參數:ticksPerWheel(一輪的tick數),tickDuration(一個tick的持續時間)以及timeUnit(時間單位),例如當ticksPerWheel=60,tickDuration=1,timeUnit=1秒,這就和時鐘的秒針走動完全類似了。
時間輪的執行由NioEventLoop來複雜檢測,首先看任務隊列中是否有超時的定時任務和普通任務,如果有則按照比例循環執行這些任務,代碼如下:
protected void run(){
for (; ;) {
oldWakenUp = wakenUp.getAndSet(false);
try{
if (hasTasks()) {
selectNow();
} else {
...
}
}
}
}
如果沒有需要理解執行的任務,則調用Selector的select方法進行等待,等待的時間爲定時任務隊列中第一個超時的定時任務時延:int selectCnt = 0;
long currentTimeNanos = System.nanoTime();
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for (; ; ) {
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L);
if (timeoutMillis <= 0) {
if (selectCnt == 0) {
selector.selectNow();
selectCnt = 1;
}
break;
}
int selectedKeys = selector.select(timeoutMillis);
}
從定時任務Task隊列中彈出delay最小的Task,計算超時時間protected long delayNanos(long currentTimeNanos){
ScheduledFutureTask<?> delayedTask = delayedTaskQueue.peek();
if (delayedTask == null) {
return SCHEDULE_PURGE_INTERVAL;
}
return delayedTask.delayNanos(currentTimeNanos);
}
定時任務的執行:經過週期tick之後,掃描定時任務列表,將超時的定時任務移除到普通任務隊列中,等待執行,相關代碼如下:
private void fetchFromDelayedQueue(){
long nanoTime = 0L;
for (; ; ) {
ScheduledFutureTask<?> delayedTask = delayedTaskQueue.peek();
if (delayedTask == null) {
break;
}
if (nanoTime == 0L) {
nanoTime = ScheduledFutureTask.nanoTime();
}
if (delayedTask.deadlineNanos() <= nanoTime) {
delayedTaskQueue.remove();
taskQueue.add(delayedTask);
} else {
break;
}
}
}
檢測和拷貝任務完成之後,就執行超時的定時任務runAllTasks(上一節中沒有分析的函數)
protected boolean runAllTasks(long timeoutNanos) {
fetchFromDelayedQueue();
Runnable task = pollTask();
if (task == null) {
return false;
}
final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
long runTasks;
long lastExecutionTime;
for (; ; ) {
try{
task.run();
} catch (Throwable t) {
logger.warn("A task raised an execption.", t);
}
runTasks++;
}
}
爲了保證定時任務的執行不會因爲過度擠佔IO事件的處理,Netty提供了IO執行比例供用戶設置,用戶可以設置分配給IO的執行比例,防止因爲海量定時任務的執行導致IO處理超時或者積壓。因爲獲取系統的納秒時間是件耗時的操作,所以Netty每執行64個定時任務檢測一次是否達到執行的上限時間,達到則退出。如果沒有執行完,放到下次Selector輪詢時再處理,給IO事件的處理提供機會,代碼如下:
if ((runTasks & 0x3F) == 0) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
if (lastExecutionTime >= deadline) {
break;
}
}
task = pollTask();
if (task == null) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
break;
}
六、Netty編解碼技術
通常我們也習慣將編碼(Encode)成爲序列化,它將數據序列化爲字節數組,用於網絡傳輸、數據持久化或者其他用途。反之,解碼(Decode)/反序列化(deserialization)把從網絡、磁盤等讀取的字節數組還原成原始對象(通常是原始對象的拷貝),以方便後續的業務邏輯操作。進行遠程跨進程服務調用時(例如RPC調用),需要使用特定的編解碼技術,對需要進行網絡傳輸的對象做編碼或者解碼,以便完成遠程調用。主流的編解碼框架包括:
①JBoss的Marshalling包
②google的Protobuf
③基於Protobuf的Kyro
④MessagePack框架
⑤HTTP Request / HTTP Response 協議。
以下HTTP協議的示例表明瞭解碼器的使用方法:
package testNetty;
import java.net.InetSocketAddress;
import java.nio.channels.spi.SelectorProvider;
import java.util.concurrent.ThreadFactory;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.DefaultThreadFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.BasicConfigurator;
public class TestHTTPNetty {
static {
BasicConfigurator.configure();
}
public static void main(String[] args) throws Exception {
ServerBootstrap serverBootstrap = new ServerBootstrap();
EventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
ThreadFactory threadFactory = new DefaultThreadFactory("work thread pool");
int processorsNumber = Runtime.getRuntime().availableProcessors();
EventLoopGroup workLoogGroup = new NioEventLoopGroup(processorsNumber * 2, threadFactory, SelectorProvider.provider());
serverBootstrap.group(bossLoopGroup , workLoogGroup);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//我們在socket channel pipeline中加入http的編碼和解碼器
ch.pipeline().addLast(new HttpResponseEncoder());
ch.pipeline().addLast(new HttpRequestDecoder());
ch.pipeline().addLast(new HTTPServerHandler());
}
});
serverBootstrap.option(ChannelOption.SO_BACKLOG, 128);
serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
serverBootstrap.bind(new InetSocketAddress("0.0.0.0", 83));
}
}
/**
* @author yinwenjie
*/
@Sharable
class HTTPServerHandler extends ChannelInboundHandlerAdapter {
private static Log LOGGER = LogFactory.getLog(HTTPServerHandler.class);
/**
* 由於一次httpcontent可能沒有傳輸完全部的請求信息。所以這裏要做一個連續的記錄
* 然後在channelReadComplete方法中(執行了這個方法說明這次所有的http內容都傳輸完了)進行處理
*/
private static AttributeKey<StringBuffer> CONNTENT = AttributeKey.valueOf("content");
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
/*
* 在測試中,我們首先取出客戶端傳來的參數、URL信息,並且返回給一個確認信息。
* 要使用HTTP服務,我們首先要了解Netty中http的格式,如下:
* ----------------------------------------------
* | http request | http content | http content |
* ----------------------------------------------
*
* 所以通過HttpRequestDecoder channel handler解碼後的msg可能是兩種類型:
* HttpRquest:裏面包含了請求head、請求的url等信息
* HttpContent:請求的主體內容
* */
if(msg instanceof HttpRequest) {
HttpRequest request = (HttpRequest)msg;
HttpMethod method = request.getMethod();
String methodName = method.name();
String url = request.getUri();
HTTPServerHandler.LOGGER.info("methodName = " + methodName + " && url = " + url);
}
//如果條件成立,則在這個代碼段實現http請求內容的累加
if(msg instanceof HttpContent) {
StringBuffer content = ctx.attr(HTTPServerHandler.CONNTENT).get();
if(content == null) {
content = new StringBuffer();
ctx.attr(HTTPServerHandler.CONNTENT).set(content);
}
HttpContent httpContent = (HttpContent)msg;
ByteBuf contentBuf = httpContent.content();
String preContent = contentBuf.toString(io.netty.util.CharsetUtil.UTF_8);
content.append(preContent);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
HTTPServerHandler.LOGGER.info("super.channelReadComplete(ChannelHandlerContext ctx)");
/*
* 一旦本次http請求傳輸完成,則可以進行業務處理了。
* 並且返回響應
* */
StringBuffer content = ctx.attr(HTTPServerHandler.CONNTENT).get();
HTTPServerHandler.LOGGER.info("http客戶端傳來的信息爲:" + content);
//開始返回信息了
String returnValue = "return response";
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
HttpHeaders httpHeaders = response.headers();
//這些就是http response 的head信息咯,參見http規範。另外您還可以設置自己的head屬性
httpHeaders.add("param", "value");
response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/plain");
//一定要設置長度,否則http客戶端會一直等待(因爲返回的信息長度客戶端不知道)
response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, returnValue.length());
ByteBuf responseContent = response.content();
responseContent.writeBytes(returnValue.getBytes("UTF-8"));
//開始返回
ctx.writeAndFlush(response);
}
}
七、Netty半包和粘包問題的解決
無論是服務器端還是客戶端,當我們讀取或者發送數據的時候,都需要考慮TCP底層的粘包/拆包機制。
TCP是一個“流”協議,所謂流就是沒有界限的遺傳數據。TCP底層並不瞭解上層業務數據的具體含義,它會根據TCP緩衝區的具體情況進行包的劃分,也就是說,在業務上一個完整的包可能會被TCP分成多個包進行發送,也可能把多個小包封裝成一個大的數據包發送出去,這就是所謂的半包/粘包問題。
半包/粘包是一個應用層問題。要解決半包/粘包問題,就是在應用程序層面建立協商一致的信息還原依據。常見的有兩種方式:
一是消息定長,即保證每一個完整的信息描述的長度都是一定的,這樣無論TCP/IP協議如何進行分片,數據接收方都可以按照固定長度進行消息的還原。
二是在完整的一塊數據結束後增加協商一致的分隔符(例如增加一個回車符)。
在JAVA NIO技術框架中,半包和粘包問題我們需要自己解決,如果使用Netty框架,它其中提供了多種解碼器的封裝幫助我們解決半包和粘包問題。甚至針對不同的數據格式,Netty都提供了半包和粘包問題的現成解決方式。
1、使用FixedLengthFrameDecoder解決問題
FixedLengthFrameDecoder解碼處理器將TCP/IP的數據按照指定的長度進行重新拆分,如果接收到的數據不滿足設置的固定長度,Netty將等待新的數據到達:
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ByteArrayEncoder());
ch.pipeline().addLast(new FixedLengthFrameDecoder(20));
ch.pipeline().addLast(new TCPServerHandler());
ch.pipeline().addLast(new ByteArrayDecoder());
}
});
Netty上層的channelRead事件方法將在Channel接收到20個字符的情況下被觸發;而如果剩餘的內容不到20個字符,channelRead方法將不會被觸發(但注意channelReadComplete方法會觸發的啦)。2、使用LineBasedFrameDecoder解決問題
LineBasedFrameDecoder解碼器,基於最簡單的“換行符”進行接收到的信息的再組織。windows和linux兩個操作系統中的“換行符”是不一樣的,LineBasedFrameDecoder解碼器都支持。當然這個解碼器沒有我們後面介紹的DelimiterBasedFrameDecoder解碼器靈活。
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ByteArrayEncoder());
ch.pipeline().addLast(new LineBasedFrameDecoder(100));
ch.pipeline().addLast(new TCPServerHandler());
ch.pipeline().addLast(new ByteArrayDecoder());
}
});
那麼如果客戶端發送的數據是:
this is 0 client \r\n request 1 \r\n”
那麼接收方重新通過“換行符”重新組織後,將分兩次接受到數據:
this is 0 client
request 1
3、使用DelimiterBasedFrameDecoder解決問題DelimiterBasedFrameDecoder是按照“自定義”分隔符(也可以是“回車符”或者“空字符”注意windows系統中和linux系統中“回車符”的表示是不一樣的)進行信息的重新拆分。
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ByteArrayEncoder());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1500, false, Delimiters.lineDelimiter()));
ch.pipeline().addLast(new TCPServerHandler());
ch.pipeline().addLast(new ByteArrayDecoder());
}
});
DelimiterBasedFrameDecoder有三個參數,這裏介紹一下:
DelimiterBasedFrameDecoder(int maxFrameLength, boolean stripDelimiter, ByteBuf... delimiters)
/**
* maxFrameLength:最大分割長度,如果接收方在一段長度 大於maxFrameLength的數據段中,沒有找到指定的分隔符,那麼這個處理器會拋出TooLongFrameException異常。
*
* stripDelimiter:這個是一個布爾型參數,指代是否保留指定的分隔符。
*
* delimiters:設置的分隔符。一般使用Delimiters.lineDelimiter()或者Delimiters.nulDelimiter()。當然您也可以自定義分隔符,定義成bytebuf的類型就行了。
*/
八、Netty的使用經驗
1. Netty長連接的處理方式
兩臺機器(甚至多臺)使用Netty的通信方式大體分爲三種:
①使用長連接通道不斷開的形式進行通信,也就是服務器和客戶端的通道一直處於開啓狀態,如果服務器性能足夠好,並且客戶端數量也比較上的情況下,推薦這種方式。
②一次性批量提交數據,採用短連接方式。也就是說先把數據保存到本地臨時緩存區或者臨時表,當達到界值時進行一次性批量提交,又或者根據定時任務輪詢提交,這種情況的弊端是做不到實時性傳輸,對實時性要求不高的應用程序中推薦使用。
③使用一種特殊的長連接,在某一指定時間段內,服務器與某臺客戶端沒有任何通信,則斷開連接。下次連接則是客戶端向服務器發送請求的時候,再次建立連接。
在這裏將介紹使用Netty實現第三種方式的連接,但是我們需要考慮兩個因素:
①如何在超時(即服務器和客戶端沒有任何通信)後關閉通道?關閉通道後又如何再次建立連接?
②客戶端宕機時,我們無需考慮,下次重啓客戶端之後就可以與服務器建立連接,但服務器宕機時,客戶端如何與服務器端通信?
Netty爲超時控制封裝了兩個類ReadTimeoutHandler和WriteTimeoutHandler,ReadTimeoutHandler用於控制讀取數據的時候的超時,如果在設置時間段內都沒有數據讀取了,那麼就引發超時,然後關閉當前的channel;WriteTimeoutHandler用於控制數據輸出的時候的超時,如果在設置時間段內都沒有數據寫了,那麼就超時。它們都是IdleStateHandler的子類。
Netty 的超時類型 IdleState 主要分爲:
1. ALL_IDLE : 一段時間內沒有數據接收或者發送
2. READER_IDLE : 一段時間內沒有數據接收
3. WRITER_IDLE : 一段時間內沒有數據發送
在 Netty 的 timeout 包下,主要類有:
1. IdleStateEvent : 超時的事件
2. IdleStateHandler : 超時狀態處理
3. ReadTimeoutHandler : 讀超時狀態處理
4. WriteTimeoutHandler : 寫超時狀態處理
其使用時只需要在initChannel時添加相應的狀態處理Handler即可:
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
/* (non-Javadoc)
* @see io.netty.channel.ChannelInitializer#initChannel(io.netty.channel.Channel)
*/
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//我們在socket channel pipeline中加入http的編碼和解碼器
ch.pipeline().addLast(new HttpResponseEncoder());
ch.pipeline().addLast(new ReadTimeoutHandler(5));
ch.pipeline().addLast(new HttpRequestDecoder());
ch.pipeline().addLast(new HTTPServerHandler());
}
});
在實現中ReadTimeoutHandler使用定時任務添加到任務循環中,在收發數據後重置計數,觸發超時時拋出異常,即完成了超時判斷。
2. Netty心跳包
Netty應用心跳和重連的整個過程:
1)客戶端連接服務端
2)在客戶端的的ChannelPipeline中加入一個比較特殊的IdleStateHandler,設置一下客戶端的寫空閒時間,例如4s
3)當客戶端的所有ChannelHandler中4s內沒有write事件,則會觸發userEventTriggered方法(上文介紹過)
4)我們在客戶端的userEventTriggered中對應的觸發事件下發送一個心跳包給服務端,檢測服務端是否還存活,防止服務端已經宕機,客戶端還不知道。
5)同樣,服務端要對心跳包做出響應,其實給客戶端最好的回覆就是“不回覆”,這樣可以服務端的壓力,假如有10w個空閒Idle的連接,那麼服務端光發送心跳回復,則也是費事的事情,那麼怎麼才能告訴客戶端它還活着呢,其實很簡單,因爲5s服務端都會收到來自客戶端的心跳信息,那麼如果10秒內收不到,服務端可以認爲客戶端掛了,可以close鏈路。
6)加入服務端因爲什麼因素導致宕機的話,就會關閉所有的鏈路鏈接,所以作爲客戶端要做的事情就是短線重連
要寫工業級的Netty心跳重連的代碼,需要解決一下幾個問題:
1)ChannelPipeline中的ChannelHandlers的維護,首次連接和重連都需要對ChannelHandlers進行管理
2)重連對象的管理,也就是bootstrap對象的管理
3)重連機制編寫
1. 首先先定義一個接口ChannelHandlerHolder,用來保管ChannelPipeline中的Handlers的
package com.lyncc.netty.idle;
import io.netty.channel.ChannelHandler;
/**
*
* 客戶端的ChannelHandler集合,由子類實現,這樣做的好處:
* 繼承這個接口的所有子類可以很方便地獲取ChannelPipeline中的Handlers
* 獲取到handlers之後方便ChannelPipeline中的handler的初始化和在重連的時候也能很方便
* 地獲取所有的handlers
*/
public interface ChannelHandlerHolder {
ChannelHandler[] handlers();
}
2.編寫我們熟悉的服務端的ServerBootstrap
package com.lyncc.netty.idle;
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;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
public class HeartBeatServer {
private final AcceptorIdleStateTrigger idleStateTrigger = new AcceptorIdleStateTrigger();
private int port;
public HeartBeatServer(int port) {
this.port = port;
}
public void start() {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap sbs = new ServerBootstrap().group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.INFO))
.localAddress(new InetSocketAddress(port)).childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
ch.pipeline().addLast(idleStateTrigger);
ch.pipeline().addLast("decoder", new StringDecoder());
ch.pipeline().addLast("encoder", new StringEncoder());
ch.pipeline().addLast(new HeartBeatServerHandler());
};
}).option(ChannelOption.SO_BACKLOG, 128).childOption(ChannelOption.SO_KEEPALIVE, true);
// 綁定端口,開始接收進來的連接
ChannelFuture future = sbs.bind(port).sync();
System.out.println("Server start listen at " + port);
future.channel().closeFuture().sync();
} catch (Exception e) {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
} else {
port = 8080;
}
new HeartBeatServer(port).start();
}
}
3. 單獨寫一個AcceptorIdleStateTrigger,其實也是繼承ChannelInboundHandlerAdapter,重寫userEventTriggered方法,因爲客戶端是write,那麼服務端自然是read,設置的狀態就是IdleState.READER_IDLE,源碼如下:
package com.lyncc.netty.idle;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
@ChannelHandler.Sharable
public class AcceptorIdleStateTrigger extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleState state = ((IdleStateEvent) evt).state();
if (state == IdleState.READER_IDLE) {
throw new Exception("idle exception");
}
} else {
super.userEventTriggered(ctx, evt);
}
}
}
4. HeartBeatServerHandler就是一個很簡單的自定義的Handler,不是重點:
package com.lyncc.netty.idle;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class HeartBeatServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("server channelRead..");
System.out.println(ctx.channel().remoteAddress() + "->Server :" + msg.toString());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
5. 接下來就是重點,我們需要寫一個類,這個類可以去觀察鏈路是否斷了,如果斷了,進行循環的斷線重連操作,ConnectionWatchdog,顧名思義,鏈路檢測狗,我們先看完整代碼:
package netty.test.chapter14;
/**
* Created by Administrator on 2016/9/22.
*/
/**
*
* 重連檢測狗,當發現當前的鏈路不穩定關閉之後,進行12次重連
*/
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.util.Timeout;
import io.netty.util.Timer;
import io.netty.util.TimerTask;
import java.util.concurrent.TimeUnit;
@Sharable
public abstract class ConnectionWatchdog extends ChannelInboundHandlerAdapter implements TimerTask ,ChannelHandlerHolder{
private final Bootstrap bootstrap;
private final Timer timer;
private final int port;
private final String host;
private volatile boolean reconnect = true;
private int attempts;
public ConnectionWatchdog(Bootstrap bootstrap, Timer timer, int port,String host, boolean reconnect) {
this.bootstrap = bootstrap;
this.timer = timer;
this.port = port;
this.host = host;
this.reconnect = reconnect;
}
/**
* channel鏈路每次active的時候,將其連接的次數重新☞ 0
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("當前鏈路已經激活了,重連嘗試次數重新置爲0");
attempts = 0;
ctx.fireChannelActive();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("鏈接關閉");
if(reconnect){
System.out.println("鏈接關閉,將進行重連");
if (attempts < 12) {
attempts++;
} //重連的間隔時間會越來越長
int timeout = 2 << attempts;
timer.newTimeout(this, timeout, TimeUnit.MILLISECONDS);
}
ctx.fireChannelInactive();
}
public void run(Timeout timeout) throws Exception {
ChannelFuture future;
//bootstrap已經初始化好了,只需要將handler填入就可以了
synchronized (bootstrap) {
bootstrap.handler(new ChannelInitializer<Channel>(){
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(handlers());
}
});
future = bootstrap.connect(host,port);
}
//future對象
future.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture f) throws Exception {
boolean succeed = f.isSuccess();
//如果重連失敗,則調用ChannelInactive方法,再次出發重連事件,一直嘗試12次,如果失敗則不再重連
if (!succeed) {
System.out.println("重連失敗");
f.channel().pipeline().fireChannelInactive();
}else{
System.out.println("重連成功");
}
}
});
}
}
稍微分析一下:
1)繼承了ChannelInboundHandlerAdapter,說明它也是Handler,也對,作爲一個檢測對象,肯定會放在鏈路中,否則怎麼檢測
2)實現了2個接口,TimeTask,ChannelHandlerHolder
①TimeTask,我們就要寫run方法,這應該是一個定時任務,這個定時任務做的事情應該是重連的工作
②ChannelHandlerHolder的接口,這個接口我們剛纔說過是維護的所有的Handlers,因爲在重連的時候需要獲取Handlers
3)bootstrap對象,重連的時候依舊需要這個對象
4)當鏈路斷開的時候會觸發channelInactive這個方法,也就說觸發重連的導火索是從這邊開始的
6. HeartBeatsClient
package com.lyncc.netty.idle;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.HashedWheelTimer;
import java.util.concurrent.TimeUnit;
public class HeartBeatsClient {
protected final HashedWheelTimer timer = new HashedWheelTimer();
private Bootstrap boot;
private final ConnectorIdleStateTrigger idleStateTrigger = new ConnectorIdleStateTrigger();
public void connect(int port, String host) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
boot = new Bootstrap();
boot.group(group).channel(NioSocketChannel.class).handler(new LoggingHandler(LogLevel.INFO));
final ConnectionWatchdog watchdog = new ConnectionWatchdog(boot, timer, port,host, true) {
public ChannelHandler[] handlers() {
return new ChannelHandler[] {
this,
new IdleStateHandler(0, 4, 0, TimeUnit.SECONDS),
idleStateTrigger,
new StringDecoder(),
new StringEncoder(),
new HeartBeatClientHandler()
};
}
};
ChannelFuture future;
//進行連接
try {
synchronized (boot) {
boot.handler(new ChannelInitializer<Channel>() {
//初始化channel
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(watchdog.handlers());
}
});
future = boot.connect(host,port);
}
// 以下代碼在synchronized同步塊外面是安全的
future.sync();
} catch (Throwable t) {
throw new Exception("connects to fails", t);
}
}
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 採用默認值
}
}
new HeartBeatsClient().connect(port, "127.0.0.1");
}
}
也稍微說明一下:1)創建了ConnectionWatchdog對象,自然要實現handlers方法
2)初始化好bootstrap對象
3)4秒內沒有寫操作,進行心跳觸發,也就是IdleStateHandler這個方法
7. ConnectorIdleStateTrigger
package com.lyncc.netty.idle;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.CharsetUtil;
@Sharable
public class ConnectorIdleStateTrigger extends ChannelInboundHandlerAdapter {
private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Heartbeat", CharsetUtil.UTF_8));
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleState state = ((IdleStateEvent) evt).state();
if (state == IdleState.WRITER_IDLE) {
// write heartbeat to server
ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate());
}
} else {
super.userEventTriggered(ctx, evt);
}
}
}
此處由於示例只需要使用一個帶信息的包在"WRITER_IDLE"狀態下發送心跳而已,對於內容沒有要求,因此使用了
private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Heartbeat", CharsetUtil.UTF_8));
作爲心跳包,在實際生產環境中可以使用類似如下的消息格式:
+--------+-----+---------------+
| Length |Type | Content |
| 17 | 1 |"HELLO, WORLD" |
+--------+-----+---------------+
在服務端解析時可以直接獲取字節位置判斷消息的格式,從而判斷是否爲心跳包,心跳包時可丟棄,可做如下操作:
if (byteBuf.getByte(4) == PING_MSG) {
// throwHeartbeatMsg(context);
}
package com.lyncc.netty.idle;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
import java.util.Date;
@Sharable
public class HeartBeatClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("激活時間是:"+new Date());
System.out.println("HeartBeatClientHandler channelActive");
ctx.fireChannelActive();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("停止時間是:"+new Date());
System.out.println("HeartBeatClientHandler channelInactive");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String message = (String) msg;
System.out.println(message);
if (message.equals("Heartbeat")) {
ctx.write("has read message from server");
ctx.flush();
}
ReferenceCountUtil.release(msg);
}
}
以上轉載:Netty 之 Netty生產級的心跳和重連機制