待完善
Channel、EventLoop和ChannelFuture
Channel
、EventLoop
和ChannelFuture
這些類組合在一起,可以被認為是Netty網絡抽象的代表:
-
Channel
—Socket; -
EventLoop
—控制流、多線程處理、并發; -
ChannelFuture
—異步通知
Channel
暫略
EventLoop
EventLoop
定義了Netty的核心抽象,用于處理連接的生命周期中所發生的事件。
一個
EventLoopGroup
包含一個或者多個EventLoop
;一個
EventLoop
在它的生命周期內只和一個Thread
綁定;所有由
EventLoop
處理的I/O事件都將在它專有的Thread
上被處理;一個
Channel
在它的生命周期內只注冊于一個EventLoop
;一個
EventLoop
可能會被分配給一個或多個Channel
。
注意,在這種設計中,一定程度上消除了對于同步的需要。
ChannelFuture
暫略
ChannelHandler和ChannelPipeline
ChannelHandler
Handles an I/O event or intercepts an I/O operation, and forwards it to its next handler in its ChannelPipeline
.
-
ChannelHandler
public interface ChannelHandler { // ChannelHandler添加到ChannelPipeline中時被調用 void handlerAdded(ChannelHandlerContext ctx) throws Exception; // ChannelHandler從ChannelPipeline中移除時被調用 void handlerRemoved(ChannelHandlerContext ctx) throws Exception; // 處理過程中在ChannelPipeline中有錯誤產生時被調用 void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception; }
-
ChannelHandler
層次結構如下圖所示:圖片.pngChannelHandler
itself does not provide many methods, but you usually have to implement one of its subtypes:-
ChannelInboundHandler
to handle inbound I/O events, and -
ChannelOutboundHandler
to handle outbound I/O operations.
Alternatively, the following adapter classes are provided for your convenience:
-
ChannelInboundHandlerAdapter
to handle inbound I/O events, -
ChannelOutboundHandlerAdapter
to handle outbound I/O operations, and -
ChannelDuplexHandler
to handle both inbound and outbound events
-
A ChannelHandler often needs to store some stateful information?
-
ChannelInboundHandler
public interface ChannelInboundHandler extends ChannelHandler { void channelRegistered(ChannelHandlerContext ctx) throws Exception; void channelUnregistered(ChannelHandlerContext ctx) throws Exception; void channelActive(ChannelHandlerContext ctx) throws Exception; void channelInactive(ChannelHandlerContext ctx) throws Exception; void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception; void channelReadComplete(ChannelHandlerContext ctx) throws Exception; void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception; void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception; void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception; }
-
ChannelInboundHandlerAdapter
public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler { @Override public void channelRegistered(ChannelHandlerContext ctx) throws Exception { ctx.fireChannelRegistered(); } @Override public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { ctx.fireChannelUnregistered(); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.fireChannelActive(); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { ctx.fireChannelInactive(); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ctx.fireChannelRead(msg); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.fireChannelReadComplete(); } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { ctx.fireUserEventTriggered(evt); } @Override public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { ctx.fireChannelWritabilityChanged(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.fireExceptionCaught(cause); } }
ChannelInboundHandlerAdapter
的channelRead
方法處理完消息后不會自動釋放消息,若想自動釋放收到的消息,可以使用SimpleChannelInboundHandler
。Usually,
channelRead()
handler method is implemented like the following:@Override public void channelRead(ChannelHandlerContext ctx, Object msg) { try { // Do something with msg } finally { ReferenceCountUtil.release(msg); } }
-
ChannelOutboundHandler
public interface ChannelOutboundHandler extends ChannelHandler { void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception; void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) throws Exception; void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception; void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception; void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception; void read(ChannelHandlerContext ctx) throws Exception; void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception; void flush(ChannelHandlerContext ctx) throws Exception; }
ChannelOutboundHandlerAdapter
```java
public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelOutboundHandler {
@Override
public void bind(ChannelHandlerContext ctx, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
ctx.bind(localAddress, promise);
}
@Override
public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,
SocketAddress localAddress, ChannelPromise promise) throws Exception {
ctx.connect(remoteAddress, localAddress, promise);
}
@Override
public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise)
throws Exception {
ctx.disconnect(promise);
}
@Override
public void close(ChannelHandlerContext ctx, ChannelPromise promise)
throws Exception {
ctx.close(promise);
}
@Override
public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
ctx.deregister(promise);
}
@Override
public void read(ChannelHandlerContext ctx) throws Exception {
ctx.read();
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ctx.write(msg, promise);
}
@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
}
```
ChannelHandlerContext
使ChannelHandler
能夠與其ChannelPipeline
和其他處理程序進行交互。除其他事項外,處理程序可以通知ChannelPipeline
中的下一個ChannelHandler
,也可以動態修改它所屬的ChannelPipeline
。
-
Notify
You can notify the closest handler in the same ChannelPipeline by calling one of the various methods provided here.
-
Modifying a pipeline
You can get the
ChannelPipeline
your handler belongs to by callingpipeline()
. A non-trivial application could insert, remove, or replace handlers in the pipeline dynamically at runtime. -
Retrieving for later use
You can keep the ChannelHandlerContext for later use, such as triggering an event outside the handler methods, even from a different thread.
public class MyHandler extends ChannelDuplexHandler { private ChannelHandlerContext ctx; public void beforeAdd(ChannelHandlerContext ctx) { this.ctx = ctx; } public void login(String username, password) { ctx.write(new LoginMessage(username, password)); } ... }
-
Storing stateful information
attr(AttributeKey)
allow you to store and access stateful information that is related with a handler and its context. Please refer toChannelHandler
to learn various recommended ways to manage stateful information. -
A handler can have more than one context
Please note that a
ChannelHandler
instance can be added to more than oneChannelPipeline
. It means a singleChannelHandler
instance can have more than oneChannelHandlerContext
and therefore the single instance can be invoked with differentChannelHandlerContexts
if it is added to one or moreChannelPipelines
more than once.For example, the following handler will have as many independent AttributeKeys as how many times it is added to pipelines, regardless if it is added to the same pipeline multiple times or added to different pipelines multiple times:
public class FactorialHandler extends ChannelInboundHandlerAdapter { private final AttributeKey<Integer> counter = AttributeKey.valueOf("counter"); // This handler will receive a sequence of increasing integers starting // from 1. @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { Integer a = ctx.attr(counter).get(); if (a == null) { a = 1; } attr.set(a * (Integer) msg); } } // Different context objects are given to "f1", "f2", "f3", and "f4" even if // they refer to the same handler instance. Because the FactorialHandler // stores its state in a context object (using an AttributeKey), the factorial is // calculated correctly 4 times once the two pipelines (p1 and p2) are active. FactorialHandler fh = new FactorialHandler(); ChannelPipeline p1 = Channels.pipeline(); p1.addLast("f1", fh); p1.addLast("f2", fh); ChannelPipeline p2 = Channels.pipeline(); p2.addLast("f3", fh); p2.addLast("f4", fh);
ChannelPipeline
ChannelPipeline
提供了ChannelHandler
鏈的容器,并定義了用于在該鏈上傳播入站和出站事件流的API。當Channel
被創建時,它會被自動地分配到它專屬的ChannelPipeline
。
-
Creation of a pipeline
==Each channel has its own pipeline and it is created automatically== when a new channel is created.
-
How an event flows in a pipeline
The following diagram describes how I/O events are processed by
ChannelHandler
s in aChannelPipeline
typically. An I/O event is handled by either aChannelInboundHandler
or aChannelOutboundHandler
and be forwarded to its closest handler by calling the event propagation methods defined inChannelHandlerContext
, such asChannelHandlerContext.fireChannelRead(Object)
andChannelHandlerContext.write(Object)
.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) | +-------------------------------------------------------------------+
An inbound event is handled by the inbound handlers in the ==bottom-up direction== as shown on the left side of the diagram. An inbound handler usually handles the inbound data generated by the I/O thread on the bottom of the diagram. The inbound data is often read from a remote peer via the actual input operation such as
SocketChannel.read(ByteBuffer)
. If an inbound event goes beyond the top inbound handler, it is discarded silently, or logged if it needs your attention.An outbound event is handled by the outbound handler in the ==top-down direction== as shown on the right side of the diagram. An outbound handler usually generates or transforms the outbound traffic such as write requests. If an outbound event goes beyond the bottom outbound handler, it is handled by an I/O thread associated with the Channel. The I/O thread often performs the actual output operation such as
SocketChannel.write(ByteBuffer)
. -
Forwarding an event to the next handler
a handler has to invoke the event propagation methods in
ChannelHandlerContext
to forward an event to its next handler. Those methods include:-
Inbound event propagation methods:
ChannelHandlerContext.fireChannelRegistered()
ChannelHandlerContext.fireChannelActive()
ChannelHandlerContext.fireChannelRead(Object)
ChannelHandlerContext.fireChannelReadComplete()
ChannelHandlerContext.fireExceptionCaught(Throwable)
ChannelHandlerContext.fireUserEventTriggered(Object)
ChannelHandlerContext.fireChannelWritabilityChanged()
ChannelHandlerContext.fireChannelInactive()
ChannelHandlerContext.fireChannelUnregistered()
-
Outbound event propagation methods:
ChannelHandlerContext.bind(SocketAddress, ChannelPromise)
ChannelHandlerContext.connect(SocketAddress, SocketAddress, ChannelPromise)
ChannelHandlerContext.write(Object, ChannelPromise)
ChannelHandlerContext.flush()
ChannelHandlerContext.read()
ChannelHandlerContext.disconnect(ChannelPromise)
ChannelHandlerContext.close(ChannelPromise)
ChannelHandlerContext.deregister(ChannelPromise)
-
-
Building a pipeline
A user is supposed to have one or more
ChannelHandler
s in a pipeline to receive I/O events (e.g. read) and to request I/O operations (e.g. write and close). For example, a typical server will have the following handlers in each channel's pipeline, but your mileage may vary depending on the complexity and characteristics of the protocol and business logic:- Protocol Decoder - translates binary data (e.g.
ByteBuf
) into a Java object. - Protocol Encoder - translates a Java object into binary data.
- Business Logic Handler - performs the actual business logic (e.g. database access).
and it could be represented as shown in the following example:
static final EventExecutorGroup group = new DefaultEventExecutorGroup(16); ... ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast("decoder", new MyProtocolDecoder()); pipeline.addLast("encoder", new MyProtocolEncoder()); // Tell the pipeline to run MyBusinessLogicHandler's event handler methods // in a different thread than an I/O thread so that the I/O thread is not blocked by // a time-consuming task. // If your business logic is fully asynchronous or finished very quickly, you don't // need to specify a group. pipeline.addLast(group, "handler", new MyBusinessLogicHandler());
- Protocol Decoder - translates binary data (e.g.
-
Thread safety
A
ChannelHandler
can be added or removed at any time because aChannelPipeline
is thread safe. For example, you can insert an encryption handler when sensitive information is about to be exchanged, and remove it after the exchange.
ServerBootstrap和Bootstrap
在深入地學習了ChannelPipeline
、ChannelHandler
和EventLoop
之后,你接下來 的問題可能是:“如何將這些部分組織起來,成為一個可實際運行的應用程序呢?”
答案是?==“引導”(Bootstrapping)==。簡單來說,引導一個應用程序是指對它進行配置,并使它運行起來的過程—盡管該過程的具體細節可能并不如它的定義那樣簡單,尤其是對于一個網絡應用程序來說。
引導類層次結構
ServerBootstrap
和Bootstrap
分別作用于==服務器==和==客戶端==。ServerBootstrap
致力于使用一個==父Channel==來接受來自客戶端的連接,并創建==子Channel==以用于它們之間的通信;而客戶端只需要==一個單獨的、沒有父Channel的Channel==來用于所有的網絡交互(這也適用于無連接的傳輸協議,如UDP,因為它們并不是每個連接都需要一個單獨的Channel)。
public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>,
C extends Channel> implements Cloneable
子類型B
是其父類型的一個類型參數,因此可以返回到運行時實例的引用以 支持方法的鏈式調用(也就是所謂的流式語法)
為什么引導類是
Cloneable
?
你有時可能會需要創建多個具有類似配置或者完全相同配置的 Channel 。為了支持這種模式而又不 需 要 為 每 個 Channel 都 創 建 并 配 置 一 個 新 的 引 導 類 實 例 , AbstractBootstrap 被 標 記 為 了 Cloneable 。在一個已經配置完成的引導類實例上調用 clone() 方法將返回另一個可以立即使用的引 導類實例。
注意,這種方式只會創建引導類實例的 EventLoopGroup 的一個淺拷貝,所以,后者 將在所有克 隆的 Channel 實例之間共享。這是可以接受的,因為通常這些克隆的 Channel 的生命周期都很短暫,一 個典型的場景是——創建一個 Channel 以進行一次HTTP請求。
Bootstrap
Bootstrap
類被用于客戶端或者使用了無連接協議的應用程序中。Bootstrap
類的API如下:
-
Bootstrap group(EventLoopGroup)
設置用于處理Channel所有事件的EventLoopGroup
Bootstrap channel( Class<? extends C>)
-
Bootstrap channelFactory(ChannelFactory<? extends C>)
channel()
方法指定了Channel的實現類。如果該實現類沒提供默認的構造函數 , 可以通過調用channelFactory()
方法來指定一個工廠類,它將會被bind()
方法調用。 -
Bootstrap localAddress(SocketAddress)
指定Channel應該綁定到的本地地址。如果沒有指定,則將由操作系統創建一個隨機的地址。或者,也可以通過
bind()
或者connect()
方法指定localAddress。 -
<T> Bootstrap option(ChannelOption<T> option, T value)
設置
ChannelOption
, 其將被應用到每個新創建的Channel的ChannelConfig。 這些選項將會通過bind()
或者connect()
方法設置到Channel ,不管哪個先被調用。這個方法在Channel已經被創建后再調用將不會有任何的效果。支持的ChannelOption取決于使用的 Channel類型。 -
<T> Bootstrap attr( Attribute<T> key, T value)
指定新創建的Channel的屬性值。這些屬性值是通過
bind()
或者connect()
方法設置到Channel的,具體取決于誰最先被調用。這個方法在Channel被創建后將不會有任何的效果。 -
Bootstrap handler(ChannelHandler)
設置將被添加到
ChannelPipeline
以接收事件通知的ChannelHandler
。 -
Bootstrap clone()
創建一個當前Bootstrap的克隆,其具有和原始的Bootstrap相同的設置信息。
-
Bootstrap remoteAddress(SocketAddress)
設置遠程地址。或者,也可以通過
connect()
方法來指定它。 -
ChannelFuture connect()
連接到遠程節點并返回一個ChannelFuture,其將會在連接操作完成后接收到通知。
-
ChannelFuture bind()
綁定Channel并返回一個ChannelFuture,其將會在綁定操作完成后接收到通知,在那之后必須調用
Channel.connect()
方法來建立連接。
Bootstrap
類負責為客戶端和使用無連接協議的應用程序創建Channel,如圖所示:
在引導的過程中,在調用
bind()
或者connect()
方法之前,必須調用以下方法來設置所需的組件:
- group();
- channel()或者channelFactory();
- handler()
如果不這樣做,則將會導致IllegalStateException 。對handler()方法的調用尤其重要,因為它需要配置好ChannelPipeline 。
ServerBootstrap
ServerBootstrap
的API如下:
-
group
設置ServerBootstrap要用的EventLoopGroup。這個EventLoopGroup將用于ServerChannel和被接受的子Channel的I/O處理。
-
channel
設置將要被實例化的ServerChannel類。
-
channelFactory
如果不能通過默認的構造函數創建Channel,那么可以提供一個ChannelFactory。
-
localAddress
指定ServerChannel應該綁定到的本地地址。如果沒有指定,則將由操作系統使用一個隨機地址。或者,可以通過
bind()
方法來指定該localAddress。 -
option
指定要應用到新創建的ServerChannel的ChannelConfig的
ChannelOption
。這些選項將會通過bind()
方法設置到Channel。在bind()
方法被調用之后,設置或者改變ChannelOption
都不會有任何的效果。所支持的ChannelOption取決于所使用的Channel類型。 -
childOption
指定當
子Channel
被接受時,應用到==子Channel的ChannelConfig==的ChannelOption
。所支持的ChannelOption取決于所使用的Channel的類型。 -
attr
指定
ServerChannel
上的屬性,屬性將會通過bind()
方法設置給Channel。在調用bind()
方法之后改變它們將不會有任何的效果 -
childAttr
將屬性設置給已經被接受的
子Channel
。 -
handler
設置被添加到ServerChannel的ChannelPipeline中的ChannelHandler。
-
childHandler
設置將被添加到已被接受的子Channel的ChannelPipeline中的ChannelHandler。
handler()
方法和childHandler()
方法之間的區別是:前者所添加的ChannelHandler由接受子Channel的ServerChannel處理,而childHandler()方法所添加ChannelHandler將由已被接受的子Channel處理,其代表一個綁定到遠程節點的套接字。 -
clone
克隆一個設置和原始的ServerBootstrap相同的ServerBootstrap。
-
bind
綁定ServerChannel并且返回一個ChannelFuture成后收到通知(帶著成功或者失敗的結果)
ServerBootstrap在bind()
方法被調用時創建了一個ServerChannel
,并且該ServerChannel
管理了多個子Channel
。
從Channel引導客戶端?
ChannelInitializer
在引導的過程中調用了handler()
或者childHandler()
方法來添加單個的ChannelHandler
。對于簡單的應用程序來說可能已經足夠了,但是它不能滿足更加復雜的需求。
可以通過在ChannelPipeline
中將它們鏈接在一起來部署盡可能多的ChannelHandler
,Netty提供了一個特殊的ChannelInitializer
類。
A special
ChannelInboundHandler
which offers an easy way to initialize aChannel
once it was registered to itsEventLoop
. Implementations are most often used in the context ofBootstrap.handler(ChannelHandler)
,ServerBootstrap.handler(ChannelHandler)
andServerBootstrap.childHandler(ChannelHandler)
to setup theChannelPipeline
of aChannel
.
public abstract class ChannelInitializer<C extends Channel>
extends ChannelInboundHandlerAdapter
它定義了下面的方法:
protected abstract void initChannel(C ch) throws Exception;
這個方法提供了一種將多個ChannelHandler
添加到一個ChannelPipeline
中的簡便方法。只需要簡單地向Bootstrap
或ServerBootstrap
的實例提供你的ChannelInitializer
實現即可,并且一旦Channel
被注冊到了它的EventLoop
之后,就會調用你的initChannel
方法。在該方法返回之后,ChannelInitializer
的實例將會從ChannelPipeline
中移除它自己。
示例代碼如下:
public class MyChannelInitializer extends ChannelInitializer {
public void initChannel(Channel channel) {
channel.pipeline().addLast("myHandler", new MyHandler());
}
}
ServerBootstrap bootstrap = ...;
...
bootstrap.childHandler(new MyChannelInitializer());
...
ChannelOption和屬性
使用option()
方法可以將ChannelOption
應用到引導,你所提供的值將會被自動應用到引導所創建的所有Channel
(這樣就可以不用在每個Channel創建時都手動配置它。)。可用的ChannelOption
包括了底層連接的詳細信息,如keep-alive或者超時屬性以及緩存區設置。
Netty應用程序通常與組織的專有軟件集成在一起,而像Channel
這樣的組件可能甚至會在正常的Netty生命周期之外被使用。 在某些常用的屬性和數據不可用時, Netty提供了 AttributeMap
抽象(一個由Channel
和引導類提供的集合)以及AttributeKey<T>
(一個用于插入和獲取屬性值的泛型類)。使用這些工具,便可以安全地將任何類型的數據項與客戶端和服務器Channel
(包含ServerChannel的子Channel)相關聯了。
有點重要?
優雅關閉
優雅是指干凈地釋放資源。關閉Netty應用程序并沒有太多的魔法,最重要的是需要關閉EventLoopGroup
,它將處理任何掛起的事件和任務,并且隨后釋放所有活動的線程。
// 創建處理 I/O 的 EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
// 創建一個 Bootstrap 類的實例并配置它
bootstrap.group(group)
.channel(NioSocketChannel.class);
...
Future<?> future = group.shutdownGracefully();
// block until the group has shutdown
future.syncUninterruptibly();
資源管理
每當通過調用 ChannelInboundHandler.channelRead()或者 ChannelOutbound- Handler.write()方法來處理數據時,你都需要確保沒有任何的資源泄漏。
為了幫助你診斷潛在的(資源泄漏)問題,Netty提供了class ResourceLeakDetector1, 它將對你應用程序的緩沖區分配做大約 1%的采樣來檢測內存泄露。