深入理解Apache Mina (1)---- Mina的幾個類3 ]5 B# z5 @+ d' h# k8 m) I, e7 S! @4 b |
(1) IoService' F. D8 q+ N( j. ?/ P- K8 K5 l
(2) BaseIoService- X( m% R# A% J) H
(3) BaseIoAcceptor
(4) IoAcceptor
(5) IoConnector
這幾個類和接口是整個服務器或客戶端程序(IoConnector)的入口程序,其中就Mina的整體上來說,IoService是所有IO通信的入口程序,下面的幾個接口和類都是繼承或者實現了IoService接口。
下面先給出Mina(入口程序)的整體架構圖:
在這裏先提出幾個個問題:
(1)爲什麼有了一個IoService還要再有一個BaseIoService?
(2)BaseIoService和IoAcceptor(IoConnector)有什麼區別?
(3)BaseIoAcceptor(BaseIoConnector)爲什麼不去直接實現IoService,而是又添加了( W( c/ O5 Y/ G7 f
IoAcceptor(IoConnector)?
帶着這幾個問題我們來解讀一下Mina的源碼:
首先,解答第一個問題,爲什麼有了一個IoService還要再有一個BaseIoService?IoService和BaseIoService最明顯的區別就是IoService是一個接口,而BaseIoService是一個抽象類。BaseIoService實現了IoService中的部分方法。
這裏先把IoService接口中要實現的方法和BaseIoService中實現的方法列舉如下:- t: t) d# Q, I4 \4 {6 M$ j
通過對IoService和BaseIoService的比較可以發現,除了 getDefaultConfig()這個方法沒有在BaseIoService中實現之外,其他的方法都已經在BaseIoService實現了。這裏就有一個問題,爲什麼BaseIoService只是實現了IoService的部分方法,而沒有全部實現IoService的方法呢?通常都知道,接口中的方法是必須要由實現類來實現的,這點是毋庸置疑的。你可以寫一個空方法,裏面沒有任何的邏輯處理,但是你的實現類中卻不能沒有該方法。但是在Mina 中作爲實現類的BaseIoService卻沒有IoService指定的方法getDefaultConfig(),難道Mina真的有獨到之處?不是!仔細看看( n: D3 G4 U; |1 g2 n& q
BaseIoService你就會知道,BaseIoService是一個抽象類,抽象類就是用來被繼承的,它提供了一些其子類公用的一些方法,當抽象類實現一個接口時,抽象類可以有選擇性的實現其所有子類都需要的實現的一些方法,對於接口中指定法方法,抽象類可以選擇全部實現或者部分實現。在Mina中如果沒有BaseIoService這個抽象類,而是由 BaseIoAcceptor和BaseIoConnector直接去實現BaseIoService接口,那麼必然會導致這個兩個實現類中都要重寫相應的方法,這樣就脫離了面向對象設計的本質,沒有達到複用的目的。在BaseIoAcceptor/BaseIoConnector和 BaseIoService之間添加一個BaseIoService就是爲了達到代碼複用的目的。在這個問題上主要是要記住兩點:
1)抽象類在實現接口的時候可以部分或者全部實現接口中的方法。但是當抽象類只是實 3 ^' U i& I4 v% _
現了接口中的部分方法的時候,抽象類的子類必須要實現抽象類中未實現的接口的方法。在此處,IoService的getDefaultConfig()方法在BaseIoService(BaseIoAcceptor
$ I7 X& H: S# k. C$ g; O) r. l
是BaseIoService的子類,但它也是一個抽象類,所以它也沒有實現getDefaultConfig()),# O9 K: Z% U. \/ R( J+ G# B8 R5 n& @
getDefaultConfig() 是 由BaseIoAcceptor的子類們來實現的(如SocketAcceptor,這是一個
具體實現類)。所以接口的所有方法必須被具體的實現類實現和抽象類在實現接口的時候可以部分
或者全部實現接口中的方法是不矛盾的。) J! G+ M8 Q2 K M z" {
0 R2 o0 U7 y/ ]- s
2)注意代碼的重用。在面向對象的編程語言中都提供了抽象類和接口,抽象類和接口最大的區別
1 V8 k2 c# z. i% \
就是抽象類提供了方法的具體實現,供其子類來調用;而接口只是提供了對方法的聲明,其方
法的實現要由其具體實現類來做。在Java中一個子類只能有一個父類,但是卻能實現多個接口。7 U4 ~9 v9 h4 R; |( N/ m# W
個人認爲接口和抽象類各有特色,接口的使用比較靈活,不同的接口可以讓其子類扮演不同的角
色,側重於類的複用,在很大程度上解決了代碼複用的問題;抽象類更側重的是方法的複用,某
種意義上講,抽象類的使用對於程序來說使用起來更加輕鬆,但是是使用抽象類還是接口要根據0 i+ a! b+ B. O- `" U! F4 y& ~% a$ P
具體的情況而定。
對於接口和抽象類的具體的用法請參考閆宏的《Java與模式》中相關部分的講解。
之所以在這裏羅列這麼些問題,目的不僅僅是爲了講解Mina的原理,而是想從一個高的角度來看待的這個經典的開源項目,通過對Mina的學習和理解,能夠真正的懂得什麼是一個項目,什麼是面向對象編程,更本質的東西是怎麼靈活運用 Java來達到上面的兩個目的。這個纔是最重要的,哪怕是你在看完本文後對Mina的理解還是有點模糊,但是你至少要知道在編寫一個程序的時候怎樣從面向對象的角度上去思考一個問題,而不是在用着面向對象的語言寫着結構化的程序。這些東西都是自己做開發這麼長時間的一些心得,在這裏總結出來,目的主要是用於交流和學習,不是在賣弄,只是想讓更多的初學者少走一些彎路,懂得學習的方法。
還是回到對Mina的剛提出的那幾個問題上來,現在,第一個問題已經解決了,爲什麼有了一個IoService還要再有一個BaseIoService?答案就是爲了代碼的複用。
其次,下面開始討論第二個問題,BaseIoService和IoAcceptor(IoConnector)有什麼區別?' c- e2 H9 q1 ], T, ]
在討論這個問題之前,還是先給出這兩個類(接口)提供的方法,如下圖:
' d, S9 D2 I, M! P! T
, q& _1 L1 |, \* O5 L+ O8 M, @
在討論第一個問題的時候我們已經看過了BaseIoService的方法了,但是沒有對這些方法的功能做些梳理,現在就對這些方法做些簡單的介紹:9 c0 ~- a3 R/ q" f
getFilterChainBuilder()和setFilterChainBuilder():這兩個方法主要是對一個服務的IoFilter的操作,關於IoFilter的詳細介紹會在後面給出,現在你可以將其理解爲是一個處理業務邏輯的模塊,例如:黑名單的處理、數據的轉換、日誌信息的處理等等都可以在這個IoFilter中實現,它的工作原理和Servlet中的過濾器很相似。) {3 X5 N1 j# w* B3 \
addListener()和removeListener():這兩個方法通過名字看就可以理解了,就是給當前的服務添加和刪除一個監聽器,這個監聽器主要是用於對當前連接到服務的IoSession進行管理,這個也會在後面做詳細的講解。
getManagerServiceAddress()和getManagerSessions():這兩個方法的功能比較相似,一個是獲取當前服務所管理的遠程地址,一個是獲取當前服務所管理的會話IoSession,IoSession對SocketAddress做了一個完整的封裝,你也可以先將這兩個方法的功能理解爲是一回事,具體的區別會在後面給出。isManaged():檢測某個SocketAddress是否處於被管理的狀態。
getListeners():獲取當前服務的監聽器。" U, b' x3 c6 H) a
$ |8 W; }, v7 t, b
看了上面對BaseIoService功能的介紹,現在我們可以理解 BaseIoService提供的方法主要是用於對當前服務的管理。那麼要管理一個服務,前提條件是這個服務必須存在,存在的前提是什麼,就是要啓動一個服務,或者是連接到一個遠程主機上,這兩個任務分別是IoAcceptor和IoConnector來完成的,此處要注意的是這兩個對象都是接口,沒有具體的實現,具體的實現會由下面介紹的它們相關的子類(SocketAcceptor等)來實現。這樣IoAcceptor/IoConnector的功能我們就可以總結出來了,就是啓動和停止一個服務。
0 w7 V, _$ A3 u6 O" E+ W9 [
對於一個完整的服務來說,既要有啓動這樣的前提條件,還要有對服務的管理和對服務響應的邏輯處理,這兩個缺一不可,回到第二個問題,BaseIoService和IoAcceptor(IoConnector)有什麼區別?區別就在於它們實現的功能不一樣,但都是爲了一個完整的服務來打基礎的,兩者缺一都不能稱爲一個完整的服務。這三個都是IoService子類(子接口),oService只是提供了一些服務應該具有多基本的方法,BaseIoService提供了IoService部分方法的具體實現,而 IoAcceptor(IoConnector)是對特定服務要具備的操作的做了一些擴展,這樣一個服務完整的模型正在逐漸向我們清晰的展現出來。0 ~- e) l2 U6 A E2 M' w* g3 ~
再次,討論一下第三個問題。 BaseIoAcceptor(BaseIoConnector)爲什麼不去直接實現IoService,而是又添加了 IoAcceptor(IoConnector)?這個問題其實在上面已經有所涉及,爲了達到對象複用的目的,所以Mina的設計者給出了一個 BaseIoService,IoAcceptor(IoConnector)是實現一個特定服務必須要提供的一些方法。更具體一點,IoAcceptor(IoConnector)是爲了一個特定的服務(服務器/客戶端)而設計的,而IoService只是提供了一個服務應該具備的一些基本的方法。所以在Mina中給出了一個針對具體服務的一個接口IoAcceptor(IoConnector),這樣 BaseIoAcceptor(BaseIoConnector)就提供了一個服務所必備的一些條件。因爲它即實現了 IoAcceptor(IoConnector)接口又繼承了抽象類BaseIoService,這樣就實現了IoService中的所有方法,並且也添加了特定服務應該具有的方法(即IoAcceptor(IoConnector)中的方法)。以上就是第三個問題的答案。
Mina中提供的幾個特定的服務0 n9 ]5 `0 V7 Y1 e
從上面的討論中我們已經知道了Mina上層的類和接口的一些功能。即圖中所示的已經在上面解釋清楚了。* ~, e: ]* s8 B
! @: h* S7 ^3 a% r- p
, V- S+ N% a" P( I2 `& u- ]
7 R: x- o8 t* `( W' s6 L8 h# a% i
在此我們可以把Mina的上層結構簡單的定義爲Mina的“抽象層”,既然有了抽象層,肯定就會有其具體實現,抽象中最重要的兩個類是BaseIoAcceptor和BaseIoConnector,它們分別是用於服務器和客戶端的一個入口程序。
首先,說一下BaseIoAcceptor中的三個具體實現類:$ x- Y5 [: U6 P# S! T) B
DatagramAcceptorDelegate:數據報UDP通信的服務器入口程序。該類使用UDP協 議進行通信,UDP協議主要是用在視頻、遠程服務的監聽(如心跳程序)中等數據傳輸 要求不是很高的地方。3 |- U$ ?1 `, w \' v6 |
VmPipeAcceptor:虛擬通道(VM)通信的服務器入口程序。虛擬管道協議主要用於無線通信方面。 ) _ C. t8 a3 F4 D' a
SocketAcceptor:TCP/IP通信的服務器入口程序。這個是比較常用的協議,該協議主要 數據傳輸要求較高的地方,比如實時系統、遊戲服務器等。
BaseIoAcceptor及其子類" V- \% R' G7 h9 \3 _3 W1 `/ i( ?
與BaseIoAcceptor相對應的就是BaseIoConnector,該類主要用於客戶端程序。其具體的子類就不再贅述,這裏只給出BaseIoConnector及其子類的結構圖。0 B1 w5 _/ H$ I( E/ ]; I8 `
. Z/ [6 U, @* h# F6 L; B6 r
- }" G/ A) x% }, Q1 ?- k+ L
BaseIoConnector及其子類
關於SocketAcceptor、IoFilter、IoProcessor、IoHandler等會有專門的文章來討論。這裏就不在對這些組件類做詳細的說明了。
從名字上看知道IoFilter應該是一個過濾器,不錯,它確實是一個過濾器,它和Servlet中的過濾器類似,主要用於攔截和過濾I/O操作中的各種信息。在Mina的官方文檔中已經提到了IoFilter的作用:
(1)記錄事件的日誌(這個在本文中關於LoggingFilter的講述中會提到): p! v9 v7 J; x* F: C& i' N) E& `
(2)測量系統性能
(3)信息驗證
}* h& y7 ?2 M5 j" j: Q
(4)過載控制
! n' x. i# T6 a% O
(5)信息的轉換 (例如:編碼和解碼,這個會在關於ProtocolCodecFilter的講述中會提到)# D4 b9 p& }( _) d0 K1 ]* ?+ g
(6)和其他更多的信息+ C9 J! {3 A% r$ }" D
: ]( `4 X5 d/ O+ y9 }( ~
還是上一篇文檔一樣,先提出幾個問題,然後沿着這幾個問題的思路一個一個的對IoFilter進行講解。7 K& A: ~' Q( [; S& p
/ T. h _! j* b2 m% o
(1)什麼時候需要用到IoFilter,如果在自己的應用中不添加過濾器可以嗎?
& R2 \! x, e; j+ K3 s$ g$ [
(2)如果在IoService中添加多個過濾器可以嗎?若可以,如何進行添加,這多個過濾
) a8 }/ v3 _5 x+ W
器是如何工作的?1 d+ z8 D9 b$ d& x* ~! n6 L
1 N( y' J7 B2 D- r$ E0 P7 a9 a
(3)Mina中提供了協議編、解碼器,IoFilter也可以實現IO數據的編解碼功能,在實際
的使用中如何選擇?# n% R1 s7 L$ p+ H% c* T; Q
在開始對上面的問題進行討論前,爲了對IoFilter提供的方法有一個具體的瞭解,先對Mina自身提供的一個最簡單的過濾器進行一些講解----LoggingFilter(源碼在附件中,配有中文翻譯)。
& T% U( k8 v+ r5 N b2 \
首先還是看一下LoggingFilter中提供的幾個方法。列舉如下(方法中的參數就不再給出,完整方法的實現請參考附件中LoggingFilter的源碼):% x* }$ j5 W" D; u7 F# A3 N, F& Q
- O: j- a0 ^1 k. I7 j% c8 @6 ?
(1)sessionCreated()
) |3 P. r i/ f; A
(2)sessionOpened()
(3)sessionClosed()
B" y* u+ s/ }0 j
(4)sessionIdle()
(5)exceptionCaught()& q: V- B+ }' H5 k8 T
(6)messageReceived()9 O! I2 G. M" a6 t( S- g
& K$ K' X, u0 R" S. c: g
(7)messageSent()+ p0 v, K. H4 c& @* Y
% {& {- _8 t& q' g5 N
(8)filterWrite()# n* s+ n; Q' B. h& h: x8 S
(9)filterClose()
; \4 z5 x* D& k
這幾個方法都由相應會話(或者說是連接的狀態,讀、寫、空閒、連接的開閉等)的狀態的改變來觸發的。當一個會話開啓時,LoggingFilter捕獲到會話開啓的事件,會觸發sessionCreated()方法,記錄該會話開啓的日誌信息。同樣當一個會話發送數據時,Logging捕獲到會話發送消息的事件會記錄消息發送的日誌信息。這裏只是給出messageReceived()的完成方法的實現,其他方法的完整實現請參考附件中 LoggingFilter的源碼。
/**
* 記錄會話接收信息時的信息,然後將該信息傳遞到過濾器鏈中的下一個過濾器
* */
public void messageReceived(NextFilter nextFilter, IoSession session,9 Z6 C' Z) ?! F; _9 P. }" [
Object message) {
if (SessionLog.isInfoEnabled(session)) {8 ?& C$ ]1 a8 A2 Y+ W: f
SessionLog.info(session, "RECEIVED: " + message);
}
nextFilter.messageReceived(session, message);
} 7 `" q% v: C+ ?3 T5 N. y; a* s
LoggingFilter繼承與IoFilterAdpater,IoFilterAdpater是IoFilter的一個實現類,該類只是提供了IoFilter方法的簡單實現----將傳遞到各方法中的消息轉發到下一個過濾器中。你可以根據自己的需求繼承IoFilterAdpater,並重寫相關的方法。LoggingFilter就是重寫了上面提到的幾個方法,用於記錄當前的會話各種操作的日誌信息。通過上面的例子,我們可以大體的瞭解了IoFilter的基本功能:根據當前會話狀態,來捕獲和處理當前會話中所傳遞的消息。$ }0 h t! f i0 Z% ?- ?
IoFilter的UML圖如下:$ p( i2 ^: U: p2 |5 ^( ?
從上面的類圖我們可以清晰的看到IoFilter是一個接口,它有兩個具體的實現類:
IoFilterAdpater:該類提供了IoFilter所有方法的方法體,但是沒有任何邏輯處理,你可以根據你具體的需求繼承該類,並重寫相關的方法。IoFilterAdpater是在過濾器中使用的較多的一個類。
ReferenceCountingIoFilter:該類封裝IoFilter的實例,它使用監視使用該IoFilter的對象的數量,當沒有任何對象使用該IoFilter時,該類會銷燬該IoFilter。
IoFilterAdpater有三個子類,它們的作用分別如下:: ?! c9 {" J l9 N5 f5 N+ i* [
LoggingFilter:日誌工具,該類處理記錄IoFilter每個狀態觸發時的日誌信息外不對數據做任何處理。它實現了IoFilter接口的所有方法。你可以通過閱讀該類的源碼學習如何實現你自己的IoFilter。 {1 C6 O5 f/ p$ b8 D6 n4 F1 b
ExcuterFilter:這個Mina自身提供的一個線程池,在 Mina中你可以使用這個類配置你自己的線程池,由於創建和銷燬一個線程,需要耗費很多資源,特別是在高性能的程序中這點尤其重要,因此在你的程序中配置一個線程池是很重要的。它有助於你提高你的應用程序的性能。關於配置Mina的線程池在後續的文檔中會給出詳細的配置方法。: s( x* ^4 E0 ^8 o% |+ F
# h" j4 v- ?3 u" n- ?
ProtocolFilter:該類是Mina提供的一個協議編解碼器,在socket通信中最重要的就是協議的編碼和解碼工作,Mina提供了幾個默認的編解碼器的實現,在下面的例子中使用了 ObjectSerializationCodecFactory,這是Mina提供的一個Java對象的序列化和反序列化方法。使用這個編解碼器,你可以在你的Java客戶端和服務器之間傳遞任何類型的Java對象。但是對於不同的平臺之間的數據傳遞需要自己定義編解碼器,關於這點的介紹會在後續的文檔中給出。
; p$ k& v' o9 z# l$ e* @; O
爲了更加清楚的理解這個過濾器的作用我們先來看一個簡單的例子,這個例子的功能就是服務器在客戶端連接到服務器時創建一個會話,然後向客戶端發送一個字符串(完整的源碼在附件中,這裏只給出程序的簡要內容):
* @6 Z1 j& u, t; t# }$ |
Java代碼
ServerMain: - r2 m+ }8 n7 j+ v+ |% Y, e
public class ServerMain { + ^" P- }3 d9 t4 @4 v% Y
) a" n3 N$ I7 }) p: M
public static void main(String[] args) throws IOException {
SocketAddress address = new InetSocketAddress("localhost", 4321); 7 m2 P; e7 M( s) H1 d {. ~
IoAcceptor acceptor = new SocketAcceptor(); ( C( |! {, d( q8 M0 ~
IoServiceConfig config = acceptor.getDefaultConfig();
9 \& j2 `% P: y$ ~& ]
// 配置數據的編解碼器 + K+ |3 l; @5 T- K* W! v
config.getFilterChain().addLast("codec", , k Y( }6 m( D# ^' @& Q7 w
new ProtocolCodecFilter(new ObjectSerializationCodecFactory()));
" x+ l' w6 S; }& }2 w. ^
// 綁定服務器端口 5 y$ D! x) `+ ]- U" }, l
acceptor.bind(address, new ServerHandler());
System.out.println(" 服務器開始在 8000 端口監聽 .......");
}
}
o+ n5 c0 U8 z$ j1 D2 ]$ d' _* A2 Z
ServerHandler: ( T- _4 Q: N8 o
public class ServerHandler extends IoHandlerAdapter {
' D6 |2 o, E7 d0 L0 l
// 創建會話 & u6 T3 [$ T0 I) w# b( r
public void sessionOpened(IoSession session) throws Exception { 2 Y1 R8 |% n1 k: t+ w1 A# Q
System.out.println(" 服務器創建了會話 ");
session.write(" 服務器創建會話時發送的信息 。");
}
/ j% M6 Y- F* f6 ]! `
// 發送信息 $ r# ~. }# j' v" B
public void messageSent(IoSession session, Object message) throws Exception { 9 E( ^& s; j$ b) H
} 6 m: \$ F4 V; L+ ]- A
( p0 t5 W2 O0 u3 E4 g& |) m
// 接收信息
public void messageReceived(IoSession session, Object message)
throws Exception { + Z* x. r3 y* Q0 ^" m
}
}
ClientMain: ( K' ~' \! v z
public class ClientMain {
public static void main(String[] args) { - D7 A# ^5 C7 E
9 X0 T' @7 t! N& p
SocketAddress address = new InetSocketAddress("localhost", 4321);
IoConnector connector = new SocketConnector();
IoServiceConfig config = connector.getDefaultConfig(); / P+ [0 I: j3 q
// 配置數據的編解碼器 & B, M7 c9 J0 r+ c {
config.getFilterChain().addLast("codec", / W, ?8 N% T' l2 D
new ProtocolCodecFilter(new ObjectSerializationCodecFactory()));
& }- `' }) H' N5 [" [$ G6 k; p
config.getFilterChain().addLast("logger", new LoggingFilter());
, ^" F4 c) K* Z5 y
// 連接到服務器
connector.connect(address, new ClientHandler()); 6 r; i7 r' u: ?1 ~
System.out.println(" 已經連接到了服務器 " + address); 7 K7 ?3 Y8 E' s) Z9 d* W" o% s/ L6 C0 I
} , j( Z2 d, \2 u' }9 C
} ; R# n; V, n9 Y: W& p) _
2 [4 ~) Z$ q2 h
ClientHandler:
public class ClientHandler extends IoHandlerAdapter { % u. X- O; t) u. _8 r3 h
// 發送信息 & [! w, n' z1 R& v1 B
public void messageSent(IoSession session, Object message) throws Exception {
}
8 v# ~! `+ g/ u1 r# K
// 接收信息
public void messageReceived(IoSession session, Object message) ~4 X0 g$ U4 ~
throws Exception {
System.out.println(" 客戶端接收到的服務器的信息是 " + message); & Z! Q) i+ G/ T6 Q
}
}
其中ServerMain和ClientMain分別是服務器和客戶端的主程序,ServerHandler和ClientHandler是服務器和客戶端的數據處理句柄,關於IoHandler會在後面的文檔中做詳細的講解,這裏只是簡單說明一下,IoHandler主要是對數據進行邏輯操作,也可以理解爲程序的業務邏輯層。其中:
Java代碼 5 C, E! d1 m( X% u# L/ m- M( A
// 配置數據的編解碼器
config.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new ObjectSerializationCodecFactory()));
這行代碼的功能是將網絡傳輸中的數據在發送時編碼成二進制數據,解碼時將二進制數據還原成一個對象或者是基本類型的數據。: w" b4 d7 C W, O) g8 d: N; M* D2 ^
Java代碼7 a5 t6 C# q4 B) y
9 ~! K; c6 Q8 P
運行這個程序會得到如下結果:
已經連接到了服務器 localhost/127.0.0.1:4321
2009-7-9 23:36:46 org.apache.mina.util.SessionLog info
信息: [localhost/127.0.0.1:4321] CREATED
2009-7-9 23:36:46 org.apache.mina.util.SessionLog info
信息: [localhost/127.0.0.1:4321] OPENED
2009-7-9 23:36:46 org.apache.mina.util.SessionLog info
信息: [localhost/127.0.0.1:4321] RECEIVED: 服務器創建會話時發送的信息 。
客戶端接收到的服務器的信息是 服務器創建會話時發送的信息 。
' i, p; _4 N# k3 }
其中的紅字部分是LoggingFilter打印出的事件信息。黑體部分是程序中System.out的輸出。在ClientMain中的這兩行代碼是向過濾器鏈中添加IoFilter:4 O2 a& I* n5 Y: D+ v. x) s
Java代碼
. X* W' o ?: G- y6 _+ H/ G( J+ c4 V8 u
// 配置數據的編解碼器
config.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new ObjectSerializationCodecFactory())); 6 b) F8 i) t! B3 t, Y
config.getFilterChain().addLast("logger", new LoggingFilter());//添加日誌工具
上圖表示了數據在本程序中通過過濾器鏈的過程,日誌過濾器是根據會話 (IoSession)的狀態(創建、開啓、發送、接收、異常等等)來記錄會話的事件信息的,編解碼器是根據會話的接收和發送數據來觸發事件的,從這裏我們也可以瞭解通過過濾器我們可以專門針對會話的某個或某幾個狀態來專門處理相關的事件,如異常事件,我們可以專門定義一個Exception的 IoFilter來處理Mina在通信中所發生的異常信息。! z4 ]& t& [$ O
還有一個比較有意思的問題是,假如我們將上面過濾器的順序該成下面的樣子:
$ A1 }1 H' f2 m5 r* F7 n& D
Java代碼
8 R9 Y4 T% J* Z0 x7 P# D
config.getFilterChain().addLast("logger", new LoggingFilter());//添加日誌工具
// 配置數據的編解碼器 2 g) z" j- v5 n, S2 O! c$ g& g
config.getFilterChain().addLast("codec", ' S Q" m. }( e/ {
new ProtocolCodecFilter(new ObjectSerializationCodecFactory())); : D* S& _* B0 D: G! v" j
6 _# B- e% n2 W7 E$ c
程序的執行結果如下: ' P/ G- o }1 a1 T4 S5 K
已經連接到了服務器 localhost/127.0.0.1:4321
2009-7-10 0:30:12 org.apache.mina.util.SessionLog info
信息: [localhost/127.0.0.1:4321] CREATED , w5 g$ j% P q* Q* G1 }# F
2009-7-10 0:30:12 org.apache.mina.util.SessionLog info : t9 c% N: c8 b7 [% e
信息: [localhost/127.0.0.1:4321] OPENED
2009-7-10 0:30:12 org.apache.mina.util.SessionLog info
信息: [localhost/127.0.0.1:4321] RECEIVED: DirectBuffer[pos=0 lim=56 cap=1024: 00 00 00 34 AC ED 00 05 74 00 2D 20 20 E6 9C 8D E5 8A A1 E5 99 A8 E5 88 9B E5 BB BA E4 BC 9A E8 AF 9D
E6 97 B6 E5 8F 91 E9 80 81 E7 9A 84 E4 BF A1 E6 81 AF 20 E3 80 82] 3 L+ M9 i9 g! j% ?
客戶端接收到的服務器的信息是 服務器創建會話時發送的信息 。
很明顯的是在順序變化了之後,日誌中多了接收到的二進制數據,這是因爲在上面數據已經有解碼器將數據還原成了Java對象,所以我們就看不到二進制數據了,而在順序變換後,由於先執行的是打印信息,此時的數據還沒有還原成java對象,所以接收到的數據是二進制的。* Y, Q- \* i# A# `
在上面的例子中我們清楚了整個IoFilter或者是IoFilter的工作流程,那麼IoFilter在Mina中的作用如何?所有的數據在發送到Mina程序中時,數據都是先通過IoFilter,經過處理後再轉發到業務層。這裏IoFilter就起到了一個承上啓下的作用。! m3 S& z) Q0 n/ q5 l) T' g
. L6 k9 E0 O. I' H0 [% m
到這裏我們就可以回答本文開始提到的問題了:4 O( B3 x* J3 J9 I
(1)什麼時候需要用到IoFilter,如果在自己的應用中不添加過濾器可以嗎?
在你自己的程序中可以添加過濾器,也可以不添加,但是在數據發送之前,所發送的數據必須轉換成二進制數據,這個可以有IoFilter完成,也可以由ProtocolCodecFilter完成(關於這個問題會在後面的文章中詳細講述),否則Mina會拋出Write requests mustbe transformed to class org.apache.mina.common.ByteBuffer:異常。這是因爲網絡中傳輸的數據只能是二進制數據。因此無論添加不添加過濾器,都必須將要發送的數據轉換成二進制數據。* w9 i+ V$ j, U X
(2)如果在IoService中添加多個過濾器可以嗎?若可以,如何進行添加,這多個過濾器是如何工作的?
在IoService中可以添加多個過濾器,這個在上面的程序中已經給處理,添加的方式也很簡單,通過程序一目瞭然。' @; z. j! M4 ]7 }8 I; x8 t, f9 S4 a+ c
(3)Mina中提供了協議編、解碼器,IoFilter也可以實現IO數據的編解碼功能,在實際的使用中如何選擇?
Mina的編解碼器在Mina的使用中是最關鍵的一個問題,特別是在不同語言之間進行通信的時候,比如Java和C/C++等,由於Mina自身沒有提供這些編解碼器,所以需要自己來實現。Mina提供了一個Decoder/Encoder,你可以實現兩個類來完成不同平之間的通信。關於這個問題會在後面的文檔給出具體的實習方法。
至此,關於IoFilter的作用就講述完了,希望對你能有所幫助。
在上一篇文檔中我們已經瞭解了IoFilter的用法和其在Mina中的作用,作爲Mina數據傳輸過程中比較重要的組件,IoFilter起到了承上啓下的作用----接收數據,編/解碼,將數據傳遞到邏輯層,當數據傳遞地到邏輯層時,IoFilter的使命就完成了,那麼邏輯層的數據由誰來處理呢?如何處理的?這就是本文要講述的內容----IoHandler。
/ A6 N( `' K& n3 q* G2 z: i
在介紹IoFilter的時候,文中首先是從IoFilter 的結構和其在Mina中的作用談起的,最後添加了一個使用IoFilter的例子,之前我將其傳給幾個同學看時,感覺這種方式比較晦澀,應該將例子提到前面,由於時間的關係我不能在對IoFilter的介紹做過多的修改,所以在本篇文檔中我就先以一個例子開頭,介紹IoHandler,希望這種講述方式能對你理解Mina有更多的幫助。
好了,言歸正傳,我們的例子還是以上篇文檔中的IoFilter的例子爲基礎,在此基礎上着重突出IoHandler的作用。/ U2 T8 u. @ \$ @+ X9 T6 Y
% X0 h9 k [0 A6 x, o2 e4 n) D
Java代碼
% f" y2 U4 U1 J* ~2 `) t3 M* c, L
ServerMain:
public class ServerMain { * ]6 C, Y- G9 y$ t* E
public static void main(String[] args) throws IOException { / I8 q' M& p" l7 N; _: D V
SocketAddress address = new InetSocketAddress
("localhost", 4321);
IoAcceptor acceptor = new SocketAcceptor();
IoServiceConfig config = acceptor.getDefaultConfig 8 B8 F8 T9 I) Z5 g0 p8 Z
9 @# X. e( R; ~* }) d9 Y
();
7 F! x& h9 g7 J5 h
// 配置數據的編解碼器
config.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new
ObjectSerializationCodecFactory()));
config.getFilterChain().addLast("logger", new
LoggingFilter());
// 綁定服務器端口
acceptor.bind(address, new ServerHandler());
System.out.println(" 服務器開始在 8000 端口監聽
......."); 9 D2 y' a4 r5 C" A1 D# q
}
}
ServerHandler:
public class ServerHandler extends IoHandlerAdapter { 3 _4 q% V3 G: J1 A0 X" Y8 p
// 創建會話
public void sessionOpened(IoSession session) throws 2 n9 o" q: s+ p, L% y
Exception {
System.out.println(" 服務器創建了會話 "); " f+ H! t! t6 v7 O3 y
session.write(" 服務器創建會話時發送的信息 。"); ; ^$ s2 L7 d' N- D/ {
}
// 發送信息
public void messageSent(IoSession session, Object message) 5 U) v: d6 S ^4 s" K2 w
throws Exception { 0 Q* e3 d! o+ d% C, S0 @
}
// 接收信息 0 Y6 c3 L4 a9 g2 r
public void messageReceived(IoSession session, Object / `4 E$ L( F1 v. i- F( f
6 Q P* R' X4 Z! \
message) ! u& r4 t' A4 \; ?
throws Exception { / }+ V* l! d% |9 ]1 ? E
}
} 2 o# _& c+ }! q @. z+ C+ @7 R
ClientMain: 5 _# a9 A: y t" J9 e9 k
public class ClientMain {
public static void main(String[] args) { % o, \) h0 [+ w! c1 J" I. N
SocketAddress address = new InetSocketAddress
+ }8 E7 c+ B; H5 y" O
("localhost", 4321); , y# s+ Y' ^- t( ? J6 @
IoConnector connector = new SocketConnector();
IoServiceConfig config =
connector.getDefaultConfig(); 7 ?9 O. ]8 b4 v; ]# H
// 配置數據的編解碼器 5 o- A5 m$ X5 s% W4 B2 D/ W9 I/ t
config.getFilterChain().addLast("codec", ' g, t; X6 Y* l' j# j/ c3 I( V
new ProtocolCodecFilter(new
ObjectSerializationCodecFactory())); ' @5 E. ~6 C% h1 y
config.getFilterChain().addLast("logger", new 4 q0 Y# `2 p: ~. e! P
LoggingFilter()); * `% Q* y' e9 F+ f
// 連接到服務器
connector.connect(address, new ClientHandler());
System.out.println(" 已經連接到了服務器 " + address);
}
} ^' M H) _# o- b. \$ Y) e
ClientHandler: 6 \! V* z/ t7 ~4 Z2 S: V
public class ClientHandler extends IoHandlerAdapter {
// 發送信息 , {) w' o4 @# e/ q; C' V3 T
public void messageSent(IoSession session, Object message)
throws Exception { 1 c" u4 L7 P+ `7 ?7 L
}
// 接收信息 $ u1 w5 g2 V% w+ D
public void messageReceived(IoSession session, Object
$ H0 k4 o2 r" h6 g9 ]1 v
message)
throws Exception {
System.out.println(" 客戶端接收到的服務器的信息是 "
+ message); ( \5 D3 U! X; w4 G2 N$ j
} 6 v+ N I; C7 {2 `1 Y# }
}
上面給出裏這個例子中的主要的代碼,當先後啓動服務器和客戶端後,服務器會在客戶端連接到服務器後發送一個字符串的消息。這個消息的發送就是在 IoHandler中發送的。IoHandler在Mina中屬於業務層,這裏的 IoHandler更相是J2EE中的Servlet的作用,在IoHandler中你可以不用考慮底層數據的封裝和轉換,前提是你已經在 IoFilter中已經完成了數據的轉換。這裏需要提到的一個是,所謂數據的轉換,是指將二進制數據轉換成Java中的可用對象或者是基本類型的數據。由於網絡傳輸中傳輸的都是二進制數據,這就需要有一個專門的數據轉換層,就Mina中的編解碼器來實現這個功能。如果使用RMI,對於這個問題應該不陌生,二進制和對象之間的轉化過程其實就是對象的序列化和反序列化的過程。關於Mina中實現對象的序列化和反序列化會在後續的文檔中詳細介紹,在此不在贅述。
0 F4 B; F+ s5 T* Z) a
既然IoHandler是邏輯層,我們就用IoHandler實現一個簡單的邏輯實現。先聽一個小故事:一個淘氣的小孩要去KFC買漢堡,由於KFC生意比較好,人比較多,服務員忙不過來,於是KFC專門設立了一個自動售漢堡的機器,這個機器只是一個簡單的數據收發裝置,(由於漢堡的價格時常變化,所以價格會實時更新,因此該機器需要和KFC的漢堡的價格服務器相連)小朋友買漢堡時只要向機器中投入硬幣,機器就會查詢服務器,看價格是否符合,若符合,則送給小朋友一個漢堡
,若不符合則提示小朋友錢不夠,買不到這個漢堡。% P2 I+ B# J! H8 _* _. V
8 X' r% f. _2 } @
上面是我自己虛構的一個小故事,我們先不管現實中的KFC是如何運作的,我們就當故事是真的了,那麼現在這個小的項目分配給了我們,我們需要抽象出它的需求:5 t( G0 c+ P+ Y$ B& X; m
客戶端要向服務器發送數據,查詢價格,根據價格是否合理給出相應的顯示服務器接收客戶度的價格查詢請求,根據服務器中存儲的價格信息,返回響應的結果。
根據上面的需求,我們使用Mina來實現上面的服務器和客戶端,程序的代碼如下(完整代碼在附件中):
' j/ A8 n5 l" g# U- Q
Java代碼
KFCFoodPriceHandler(服務器句柄):
public class KFCFoodPriceHandler extends IoHandlerAdapter { ) {0 T9 j9 w5 c5 @
// 創建會話 $ m+ L+ I+ ]6 a" ?5 [
public void sessionOpened(IoSession session) throws 7 ^# h3 a# @' Q" G
Exception { 8 b6 l$ A) f6 k7 P7 x7 @2 J) t6 P
// System.out.println(" 服務器創建了會話 "); 2 e' }1 M3 O- j& t
} 0 U" l0 k/ R9 h! _
2 g# S- z* k8 k( h& w& }6 a
// 接收信息 H" S# L5 y9 K/ Z& A: f
public void messageReceived(IoSession session, Object
/ b6 q' T2 r, t* ]7 b: ~1 r- Q
message) 0 a3 ]' K' s, @
throws Exception {
HashMap<String, Object> map = (HashMap<String, : E, c: ~4 j+ U8 b# R- x3 ]
6 h; E) A& i7 z- I, L, Z
Object>) message; . H8 C$ |9 _% p$ B# Q
String buythings = (String) map.get("購買"); 2 L( r3 N" F- V
// System.out.println(" 服務器接收到的信息 " + ( z- F& ?9 G) \% i0 i
) o! ^1 N6 j# J* Z1 x; B! n0 @5 O
buythings);
if (buythings.equals("漢堡")) {
HashMap<String, Object> map2 = new # x! ], t4 b- e+ n
. W- V& K$ D" d/ b% r: v" }
HashMap<String, Object>();
map2.put("食品", "漢堡"); - T* |! C! R5 l4 ^6 K
map2.put("價格", 4); 3 f* X& g& |1 h
session.write(map2);
} else if (buythings.equals("雞翅")) {
HashMap<String, Object> map2 = new ( B# F s0 \5 Z3 r
' F- |2 m# @- I# n" A# s, }0 f
HashMap<String, Object>(); " }( ~: ]; Y3 u+ ^
map2.put("食品", "雞翅"); % v! s# {7 K$ C
map2.put("價格", 5); - A1 |1 J% y$ g; e$ l9 y) a
session.write(map2); 5 f; K7 \5 D3 Q5 b+ H/ M- i
} else {
session.write(" 該種物品已經出售完畢,謝謝 $ B5 D9 a( R* v1 l2 x- K
) w3 D% r2 w! `. \, K
惠顧!");
} & \8 c# ?7 z' ~8 z. U
} 2 L, v, I4 ~1 F; |$ |4 ~1 }! o% Q
} ; r( Q7 f3 U9 A
6 u' Z5 B2 O/ r, I6 O5 X
KFCSellerHandler(客戶端句柄): - ~5 q5 X7 V+ M7 [
public class KFCSellerHandler extends IoHandlerAdapter { ' F: a$ D3 ~+ B" |; e$ F( U
; L; G+ ~% o) E) o! I
private Integer childInputMoney_Ham = 4; $ | Q$ D& \$ [8 X9 i% T
private Integer childInputMoney_Chick = 5;
// 創建會話 " E7 e& [4 o+ g j. m: {
public void sessionOpened(IoSession session) throws . R& ?0 P: L% }2 i' u( _) y
/ m$ A8 B# p$ j# W% d3 x
Exception { 0 N# V0 e. R3 y3 X$ R; g3 ]; k* U
* g* u. v' Q Y9 i( ^: U
HashMap<String, Object> map = new
HashMap<String, Object>();
map.put("購買", "漢堡");
session.write(map);
}
. {1 k/ H5 @' i2 ~8 b
// 接收信息
public void messageReceived(IoSession session, Object
. t# ^# p* @, _' R; ]. C$ R- t8 j
message) ( F. E! E6 X5 j" m9 C& G
throws Exception { L% G( a9 H% }, t
// System.out.println(" 客戶端接收到的服務器的信息是 " 6 ~2 o& o$ q7 f
// + (HashMap<String, Object>) * K- q; R) p8 i0 V
* V% P. J; u! u1 [! g4 d) T z; h
message);
HashMap<String, Object> priceInfor =
(HashMap<String, Object>) message; 5 z* K! L2 \' x' l
// System.out.println("============" +
priceInfor.get("食品"));
String foodName = (String) priceInfor.get("食品"); 7 Z9 v6 C( `; y: h3 A) w
if (foodName.equals("漢堡")) { 8 y: ~% \% T- O3 N1 Y
Integer foodPrice = (Integer) priceInfor.get & I) ~) n4 V/ t. S* s
("價格"); + u0 t& C! A' z+ S
if (foodPrice.equals ! X" e" N: y4 n# g3 l9 N
N" y3 d- ^& h
(childInputMoney_Ham)) {
System.out.println(" 您好,請收好
你的漢堡,歡迎下次光臨!");
} else {
System.out.println(" 對不起,你投 + n9 K& }: j/ c- ]" r2 }, I/ F- I; O
如的錢幣數量不夠,錢已經如數歸還,請收好!"); + ?: s- [5 p) p s
} 3 p5 j# ^7 R% S+ \. s5 t. p
} else if (foodName.equals("雞翅")) {
Integer foodPrice = (Integer) priceInfor.get
/ M. k, |7 ]$ R. _6 W: F2 Q9 h
("價格"); 3 _$ [9 E' x; w0 `/ [
if (foodPrice.equals
/ w* S/ T% \3 {3 |' K0 \7 `
(childInputMoney_Chick)) {
System.out.println(" 您好,請收好 2 ~- Z0 p& e# b" p" c0 ]
你的漢堡,歡迎下次光臨!");
} else { / Z4 u0 o: |) L- H- p
System.out.println(" 對不起,你投
! P# z1 t% t, G: P# k1 D" b$ x
如的錢幣數量不夠,錢已經如數歸還,請收好!"); . P; g5 Q' t, q: E# M* u; m( P
} , R( F7 \, R( m# q$ p) t! z
}
} $ {9 t6 P5 w4 }
}
通過上面的程序我們可以看出Mina的中業務邏輯處理都可以在IoHandler中,而不需要考慮對象的序列化和反序列化問題。關於IoHandler的簡單用法就說這麼多。下面再看看與IoHandler相關的幾個中要的類。' r1 k/ [$ ?, N$ w
按照慣例,還是先給出IoHandler及其相關類的類圖:- |/ U" ~9 [" u d0 c* i7 Y
從上面的類圖我們可以清晰的看到IoHandler是一個接口,它有兩個子類:
IoHandlerAdpater:它只是提供了IoHandler中定義的方法體,沒有任何的邏輯處理,你可以根據你自己的需求重寫該類中的相關方法。這個類在實際的開發中使用的是較多的。我們上面寫的例子都是繼承於這個類來實現的。! C4 C( \1 P, P, A; u% g! L
SingleSessionIoHandlerDelegate:這是一個服務器和客戶端只有一個會話時使用的類,在該類的方法中沒有提供session的參數,該類在實際的開發中使用的較少,如果需要對該類進行更深入的瞭解,請參考Mina 1.1.7的API文檔。
在Mina提供的IoHandler的具體實現中,大部分的實現類都是繼承與IoHandlerApater,IoHandlerAdpater在Mina 1.1.7中的子類有三個:$ ?/ t q a# F# [8 z2 l5 n1 @( y
ChainedIoHandler:這個類主要是用於處理IoHandler的messageReceived事件,它和IoHandlerChain配合使用。當在業務邏輯中有多個IoHandler需要處理時,你可以將你的每個IoHandler添加到IoHandlerChain中,這個和過濾器
鏈比較相似,關於IoFilter和IoHandlerChain的具體用法和區別會在後續的文檔中給出。1 J9 y8 E4 z1 @# s6 a5 s$ V, `
StreamIoHandler:該類也是用於處理IoHandler的messageReceived事件,它主要用於文件傳輸的系統中,比如FTP服務器中,如果需要對該類進行更深入的瞭解,請參考Mina 1.1.7的API文檔。4 Z5 \' L9 \# Z- Y/ r0 c
DemuxingIoHandler:該類主要是用於處理多個IoHandler的messageReceived,由於在TCP/IP協議的數據傳輸中會出現數據的截斷現象(由於socket傳輸的數據包的長度是固定的,當數據包大於該長度,數據包就會被截斷),所以提供這個類主要是保證IoHandler所處理的數據包的完整性,這個和編解碼器中的CumulativeProtocolDecoder類似,關於這兩個類的具體介紹會在後續的文檔中給出。
至此,關於IoHandler的作用就講述完了,希望對你能有所幫助。
在《與IoFilter相關的幾個類》和《與IoHandler相關的幾個類》兩篇文檔中我們瞭解了IoFilter和IoHandler的基本用法,以及其相關類的作用和用途。在本文中主要探討IoFilter和IoHandler的主要區別和聯繫。& e3 ^, M) D* q2 d) ?% \
在上面的兩篇文檔中都提到了IoFilter和IoHandler都是對服務器或客戶端(IoAcceptor/IoConnector)接收到的數據進行處理。在Mina的官方文檔《The high-performance protocol constructiontoolkit》給出了IoFilter和IoHandler在Mina數據傳輸中的執行順序,如下圖:
上圖顯示了IoService進行數據讀寫時,各主要組件的執行順序:
(1)IoService讀取數據時個組件的執行順序是:IoProcessor-->IoFilter-->IoHandler。4 b0 g: q0 o1 a
(2)IoService發送數據時的執行數順序:IoHandler-->IoFilter-->IoProcessor。" }% r: V, y3 Y. c6 S& c
IoProcessor是一個處理線程,它的主要作用是根據當前連接的狀態的變化(創建會話、開啓會話、接收數據、發送數據、發生異常等等),來將數據或事件通知到IoFilter,當IoFilter的相應的方法接收到該狀態的變化信息是會對接收到的數據進行處理,處理完畢後會將該事件轉發到IoHandler中,有IoHandler完成最終的處理。在這裏IoProcessor的主要功能是創建資源(創建/分配線程給IoFilter)和數據轉發(轉發到IoFilter),IoFilter對數據進行基本的分類(如編解碼),IoHandler則負責具體的邏輯實現。也就是說IoFilter對接收到的數據包的具體內容不做處理,而是有IoHandler來對所接收到的數據包進行處理,根據數據包的內容向客戶端返回響應的信息。
我們以《與IoHandler相關的幾個類》中KFC售貨機的例子來做一個具體的解釋,在該例子中,客戶端需要想服務器發送查詢價格的請求,服務器根據接收到的請求查詢物品的價格,然後將該物品的價格返回到客戶端。客戶端在向服務器發送數據前會有IoFilterr將發送的信息序列化爲二進制數據,然後有IoProcess發送出去,簡化如下:
IoHandler發送客戶端數據-->IoFilter進行序列化-->IoProcessor
上面是數據的發送過程,當服務器接收到客戶端的的請求數據後,先有IoProcessor將該數據轉發到IoFilter,IoFilter將對象進行反序列化,反序列化的結果完成後將數據轉發到IoHandler中,過程簡化如下:& H; y* p& P( i. k1 n7 F: \
IoProcessor接收客戶度端的數據-->IoFilter進行反序列化-->IoHandler根據請求查詢價格這樣一個完整的數據請求的過程就完成了。# q& v, |- i- N+ L+ B8 ?
上面簡單介紹了IoFilter和IoHandler在Mina中的作用,前者是數據的轉換層,後者是業務層。但是兩者在很多地方都有相似之處,爲了將兩者的區別做更詳細的討論,先給出兩者的結構圖:4 }/ T" J1 [% `7 h" w! c+ R
. Y, E" X/ _) T# j
5 [# a0 I" o6 p3 h
圖中的IoFilter比IoHandler中多出的一個最重要的方法就是filterWriter(),該方法會在程序調用session.write()的時候觸發,該方法的重要之處就在於它表明了IoFilter和IoHandler的重要區別,即進行IoFilter是數據的收發層,也可以說是一個數據的收發器,而IoHandler則是邏輯層,並不負責數據的收發,如果把IoProcessor說成是底層的數據收發層,則IoFilter則是一個上層的數據收發層。關於IoFilter中on*()的方法的使用和作用請參考幫助文檔,這裏不再給出具體的解釋。到此我們就可以明白了IoFilter是一個數據收發和轉化的裝置,而 IoHandler則是一個單一的業務處理裝置,你的所有業務邏輯都應該寫在這個類中。如果沒有在IoService中配置IoFilter,那麼在 IoHandler中接收到的數據是一個ByteBuffer,你需要在你的IoHandler(業務層)中完成數據的轉化,但是這樣就破壞了Mina中各個組件層的關係,這樣你的程序結構就不在清晰,因此建議在使用Mina時將數據的轉化(即二進制與對象之間的轉換放在IoFilter層來處理)。在 Mina中必須要配置IoHandler,因爲Mina中提供的IoService中的bind方法必須要有一個IoHandler,因此 IoHandler不能省略。- {0 F" N1 a2 }" x, Y' O
到這裏對於IoFilter和IoHandler的內容已經講述完畢,下面的內容是對我在開發中遇到的一些問題的一些總結,順便也給自己以前的問題寫出答案:" Q6 G; e, y9 B, N. Q7 J
(1)IoHandler和IoHandlerCommand的區別和聯繫。0 b0 Z& G- H# J7 ]
3 o5 e# J" p8 Z( z9 J
IoHandler和IoHandlerCommand是兩個接口,在開發中經常遇到的他們兩個現類分別是IoHandlerAdpater和 IoHandlerChain,IoHandlerAdpater的子類ChainedIoHandler和IoHandlerChain結合使用可以實現多個邏輯功能,IoHandlerChain代表IoHandlerCommand)是業務邏輯的處理單元,而ChainedIoHandler(代表 Iohandler)則是處理這些邏輯單元的組件。因此它們的區別是:IoHandler是刀俎,而IoHandlerCommand則是魚肉。他們的一般用法如下: 5 e+ y. m$ d& F, a- M( M' a
1 {6 x7 p/ R5 G
Java代碼 # a/ k( ~) V7 b' m
# s0 t8 A, Z7 Q+ L5 B; i
IoHandlerChain chain = new IoHandlerChain();// 創建邏輯處理組件 # J* L4 ~* Y/ b
chain.addLast("first", new FistCommand);// 添加邏輯組件單元一
chain.addLast("second", new SecondCommand);// 邏輯組件單元二 q9 ]+ \/ N& e7 S; B4 m' f) w! G
ChainedIoHandler chained = new ChainedIoHandler(chain);// 創建邏輯組件執行模塊 # n9 K+ }8 o' n* f2 f2 O% Y
chained.messageReceived(session, message);// 當messageReceived觸發該事件
8 z2 g9 @' u2 A. X: J3 \
(2)IoFilter和IoHandler可以同時使用嗎?# \8 o' g4 v( H3 v$ ^0 w% @' T
/ J2 z6 M v3 t" H1 u
IoFilter和IoHandler由於分工不同,因此他們需要同時使用,但是這不是絕對的,在Mina4 T9 [; w0 ]7 @" x
% `/ o8 Y4 L: ?# q6 A/ b- Y
的IoService中可以不配置IoFilter,但是必須配置IoHandler。但是,這不是提倡的方式,
6 Q9 x4 L% [- n! R- R
因爲這破壞的mina的分層結構,因此建議在使用Mina的時候同時使用IoFilter和* |% p" ~ A! ]
& R& R- W# Y. [' S. S
IoHandler。
# Z% e+ [; x! N ]7 m5 B |
(3)IoFilter和IoHandlerCommand/IoHandler的區別和聯繫。% h: o0 N6 M# V' w3 f0 V. H/ v# ~
# n! C ` a( w. h, u
這個問題的答案請參考問題(1)和(2)給出的解釋。
(4)IoHandlerAdpater和IoFilterAdpater的區別和聯繫。( a. U! G: Y5 u E4 [( V* a T
IoHandlerAdpater和IoFilterAdpater一個是業務邏輯層的監聽器,一個數據傳輸層的監
聽器,他們的區別就是IoHandler和IoFilter的區別,這個在上面已經討論清楚了,不在
. X0 s" M* \8 D$ |+ x) {& W
詳細說明。+ {; `: j. S% X- I
5 U' s- m3 d- ~4 G
(5)IoFilterChainBuilder和ChainedIoHandler的區別和聯繫。5 D# n! `7 c7 Z) c E
# M6 P4 ]1 q/ u
關於這個問題的討論會在後續的文檔中給出。
在 Mina的使用中,線程池的配置一個比較關鍵的環節,同時它也是Mina性能提高的一個有效的方法,在Mina的2.0以上版本中已經不再需要對Mina 線程池的配置了,本系列文章都是基於當前的穩定版本Mina 1.1.7版來進行講述的,Mina的2.0以上版本現在還都是M(millestone,即里程碑)版的,在1.5版本上2.0M版爲穩定版本,但是在 1.5+以上則爲非穩定版本,所以,爲了更好的進行討論和學習,還是基於Mina 1.1.7版本進行討論,如果使用Mina 2.0進行開發要注意JDK的版本問題,當然如果有能力的話也可以自行修改和編譯Mina的2.0版本,這裏對此就不再多說,使用2.0版本的同學可以不用理會本文的內容。2 E9 Y/ o) }# p" m& D3 ?" \! }4 x
上面的內容都是基於Apache Mina提供的文檔講述,如有需要,請自行查找相關資料,在此不再贅述。
下面開始對Mina的線程模型的配置、使用、及ExcutorFilter的基本原理進行簡單的講解。
4 E, T, c; `) }$ `4 V$ l7 h
配置Mina的三種工作線程
% f! V8 P1 E9 c
在Mina的NIO模式中有三種I/O工作線程(這三種線程模型只在NIO Socket中有效,在NIO數據包和虛擬管道中沒有,也不需要配置):( k: `& g1 U* @1 M; S
; X6 m9 U y: P& }
Acceptor thread9 ]# c0 ~$ c6 Q3 _ U- [: {# l, q
該線程的作用是接收客戶端的連接,並將客戶端的連接導入到I/O processor線程模型中。所謂的I/O processor線程模型就是Mina的I/O processor thread。Acceptor thread在調用了Acceptor.bind()方法後啓動。每個Acceptor只能創建一個Acceptor thread,該線程模型不能配置,它由Mina自身提供。+ Q1 M% I" N9 ]. q& _# ~2 B8 ?
Connector thread% Z' h3 C; b* L5 P( e+ Z
該線程模型是客戶端的連接線程模型,它的作用和Acceptor thread類似,它將客戶端與服務器的連接導入到I/O processor線程模型中。同樣地,該線程模型也是由Mina的客戶端自動創建,該線程模型也不能進行配置。
I/O processor thread# i/ i( b5 E0 w
4 h! R" I; k, N* W( C& |
該線程模型的主要作用就行接收和發送數據,所有的IO操作在服務器與客戶端的連接建立後,所有的數據的接收和發送都是有該線程模型來負責的,知道客戶端與服務器的連接關閉,該線程模型才停止工作。該線程模型可以由程序員根據需要進行配置。該線程模型默認的線程的數量爲cpu的核數+1。若你的cpu爲雙核的,則你的I/O processor 線程的最大數量爲3,同理若你的若你的cpu爲四核的,那麼你的I/O processor 線程的最大數量爲5。 q; d H; }% H6 ~% l. C
由上面的內容我們可以知道在Mina中可以配置的線程數量只有I/O processor,對於每個IoService再創建其實例的時候可以配置該IoService的I/O processor的線程數量。在SokcetConnector和SocketAccpetor中I/O Processor的數量是由CPU的核數+1來決定的。
他們的配置方式如下:
Java代碼 深入理解Apache Mina(5)---- 配置Mina的 線程模型
7 H# K. {) A& d- ]) P# ^
<span><span style="font-size: small;"> /*** ! T% A* K9 R8 f2 \+ R
* 配置SocketAcceptor監聽器的I/O Processor的線程的數量,
* 此處的I/O Processor的線程數量由CPU的核數決定,但Acceptor
* 的線程數量只有一個,也就是接收客戶端連接的線程數只有一個,
* Acceptor的線程數量不能配置。 $ N: H: N' P) g
* */ ; ~: N8 M* ?% J. U# X
SocketAcceptor acceptor = new SocketAcceptor(Runtime.getRuntime() , Y t7 @5 T; }3 p
.availableProcessors() + 1, Executors.newCachedThreadPool()); 0 e$ \" ^; R, Z6 M8 T7 j& }7 A+ e
/*** 6 u E4 d* \5 I1 l+ j" U
* 配置SocketConnector監聽器的I/O Processor的線程的數量, 6 l6 T8 d/ X# K# ]6 t1 _5 ^- _
* 此處的I/O Processor的線程數量由CPU的核數決定,但SocketConnector 6 Z, {% W: O4 [) B1 h
* 的線程數量只有一個,也就是接收客戶端連接的線程數只有一個, ; K- n9 B5 ?' A+ W0 m
* SocketConnector的線程數量不能配置。
* */ % i" t+ Q V- T
SocketConnector connector = new SocketConnector(Runtime.getRuntime() % f, h" {( x* y- Q$ P- m: M# e
.availableProcessors() + 1, Executors.newCachedThreadPool()); + h, s- X; l9 z' n ~; x X( o
</span></span>
在上面的配置比較難以理解的地方就是Runtime.getRuntime().availableProcessors() + 1,它的意思就是由JVM根據系統的情況(即CPU的核數)來決定IO Processor的線程的數量。雖然這個線程的數量是在SocketAcceptor /SocketConnector 的構造器中進行的,但是對於SocketAcceptor /SocketConnector自身的線程沒有影響,SocketAcceptor /SocketConnector的線程數量仍然爲1。爲SocketAcceptor /SocketConnector本身就封裝了IO Processor,SocketAcceptor /SocketConnector只是由一個單獨的線程來負責接收外部連接/向外部請求建立連接,當連接建立後,SocketAcceptor /SocketConnector會把數據收發的任務轉交I/O Processor的線程。這個在本系列文章的《IoFilter和IoHandler的區別和聯繫》中的圖示中可以看。
$ v M& C% u% q* L
圖中清晰的顯示了IO Processor就是位於IoService和IoFilter之間,IoService負責和外部建立連接,而IoFilter則負責處理接收到的數據,IoProcessor則負責數據的收發工作。- {* o9 m ~8 S \0 f; s
關於配置IO Processor的線程數量還有一種比較“笨”的辦法,那就一個一個試,你可以根據你的PC的硬件情況從1開始,每次加1,然後得出IO Processor的最佳的線程的數量。但是這種方式個人建議最好不要用了,上面的方法足矣。配置方法如下:
Java代碼 ) S6 w, ^& A/ i9 H, p8 a( p
<span><span style="font-size: small;">//從1--N開始嘗試,N的最大數量爲CPU核數+1
SocketAcceptor acceptor = new SocketAcceptor(N, Executors.newCachedThreadPool());
</span></span>
爲Mina的IoFilterChain添加線程池* U k3 P( R" I
在Mina的 API中提供了一個ExecutorFilter,該線程池實現了IoFilter接口,它可以作爲一個IoFilter添加到 IoFilterChain中,它的作用就是將I/O Processor中的事件通過其自身封裝的一個線程池來轉發到下一個過濾器中。在沒有添加該線程模型時,I/O Processor的事件是通過方法來觸發的,然後轉發給IoHandler。在沒有添加該線程池的時候,所有的事件都是在單線程模式下運行的,也就是說有的事件和處理(IO Processor,IoHandler,IoFilter)都是運行在同一個線程上,這個線程就是IO Processor的線程,但是這個線程的數量受到CPU核數的影響,因此係統的性能也直接受CPU核數的影響。! I. U9 ?: j3 K C4 a
) R) Y* a* E6 P6 b3 w
比較複雜的應用一般都會用到該線程池,你可以根據你的需求在IoFilterchain中你可以添加任意數量的線程池,這些線程池可以組合成一個事件驅動(SEDA)的處理模型。對於一般的應用來說不是線程的數量越多越好,線程的數量越多可能會加劇CPU切換線程所耗費的時間,反而會影響系統的性能,因此,線程的數量需要根據實際的需要由小到大,逐步添加,知道找到適合你係統的最佳線程的數量。ExcutorFilter的配置過程如下:# D7 I* B5 K1 g( p, k( r: u
, W# C9 v) {' e& `5 ~
Java代碼
<span><span style="font-size: small;">SocketAcceptor acceptor = ...;
DefaultIoFilterChainBuilder filterChainBuilder = acceptor.getDefaultConfig().getFilterChain(); . ]0 Z- c, Q& v3 u( O
filterChainBuilder.addLast("threadPool", new ExecutorFilter(Executors.newCachedThreadPool()); % u/ O+ |& g- c- w% w! @
</span></span> * r% x" A7 Z( X
在配置該線程池的時候需要注意的一個問題是,當你使用自定的ProtocolCodecFactory時候一定要將線程池配置在該過濾器之後,如下所示:
Java代碼
<span><span style="font-size: small;">DefaultIoFilterChainBuilder filterChainBuilder = acceptor.getDefaultConfig().getFilterChain(); 9 `! f$ o4 `+ B
// 和CPU綁定的操作配置在過濾器的前面 # U/ O6 u1 z8 z+ N# B- M- J, o1 @* L
filterChainBuilder.addLast("codec", new ProtocolCodecFactory(...));
// 添加線程池 ( T( {9 \5 {! ^, h$ ]
filterChainBuilder.addLast("threadPool", new ExecutorFilter(Executors.newCachedThreadPool()); . U2 b0 Y+ z0 ~0 p
</span></span>5 j- g. }% h2 i, { O* J
因爲你自己實現的ProtocolCodecFactory直接讀取和轉換的是二進制數據,這些數據都是由和CPU綁定的I/O Processor來讀取和發送的,因此爲了不影響系統的性能,也應該將數據的編解碼操作綁定到I/O Processor線程中,因爲在Java中創建和線程切換都是比較耗資源的,因此建議將ProtocolCodecFactory配置在 ExecutorFilter的前面。關於ProtocolCodecFactory詳細講述會在後續的文檔中給出,此處就不多說了。# U9 p6 U8 z0 m9 m2 M
- Z/ i! t5 g% R7 r
最後給出一個服務器線程模型完整配置的例子,該例子和KFCClient一起配置使用,詳細代碼在附件中,此處只給出代碼的主要部分: , M, x- y. w4 n8 e% z1 ^1 X
c# ~: l2 Z# m8 w
Java代碼 深入理解Apache Mina(5)---- 配置Mina的 線程模型! r B" m& ?: w4 i' k; L% R: O
$ F3 _/ F6 p; f
<span><span style="font-size: small;">SocketAddress address = new InetSocketAddress("localhost", 4321); . ~. L# ]% X& P
/***
* 配置SocketAcceptor監聽器的I/O Processor的線程的數量, 此處的I/O 2 c1 Q3 C) x. m+ S
* Processor的線程數量由CPU的核數決定,但Acceptor 的線程數量只有一個,也就是接收客戶端連接的線程數只有一個,
* Acceptor的線程數量不能配置。
* */ ' w; t! D9 d- C; N ?- ]/ N
IoAcceptor acceptor = new SocketAcceptor(Runtime.getRuntime() 8 o( v4 _, E$ G8 W
.availableProcessors() + 1, Executors.newCachedThreadPool()); 7 S6 w7 m( c9 q( I2 ?
# g( Y' q. r3 i: _$ }2 e
acceptor.getDefaultConfig().setThreadModel(ThreadModel.MANUAL); 2 g& S& a% r- g& R' w& w
// 配置數據的編解碼器 ; ?4 f5 s8 y; H- z/ l$ W
acceptor.getDefaultConfig().getFilterChain().addLast("codec", * \. d; A4 H3 t: u
new ProtocolCodecFilter(new ObjectSerializationCodecFactory()));
# N+ ? b" K3 V$ V
// 此處爲你自己實現的編解碼器
// config.getFilterChain().addLast("codec", new
// ProtocolCodecFactory(...));
// 爲IoFilterChain添加線程池
acceptor.getDefaultConfig().getFilterChain().addLast("threadPool", 6 G& t) r8 J% L& E2 X. G7 n7 s
new ExecutorFilter(Executors.newCachedThreadPool()));
acceptor.getDefaultConfig().getFilterChain().addLast("logger", 8 \6 @! j( G6 L# N: ~! X) V
new LoggingFilter());
// 綁定服務器端口 ! Z# R( s" J( @# n6 C# ?: n3 U( V+ m
acceptor.bind(address, new KFCFoodPriceHandler()); $ w0 |% e. @0 h& y0 V: c2 F, t7 S, }
System.out.println(" 服務器開始在 8000 端口監聽 .......");
// ==========================================//
// 此處爲客戶端的I/O Processor線程數的配置,你可以模仿 //
// IoAcceptor配置來實現 // 0 ?4 E# d+ e+ ]5 J; M
// ==========================================//
/***
* 配置SocketConnector監聽器的I/O Processor的線程的數量, 此處的I/O
* Processor的線程數量由CPU的核數決定,但SocketConnector 9 A; X6 A$ x) P/ y( d+ v7 @1 U1 t
* 的線程數量只有一個,也就是接收客戶端連接的線程數只有一個, SocketConnector的線程數量不能配置。 # K8 D: }# i: h. T& B3 Q
* */
// SocketConnector connector = new SocketConnector(Runtime.getRuntime() 1 [& i/ N% O# n; T5 U
// .availableProcessors() + 1, Executors.newCachedThreadPool());
} </span></span><span><span style="font-size: small;"> </span></span>
爲了對後續關於Mina 的 ProtocolFilter( 編解碼器 ) 的編寫有一個更好的理解,本文講述一下關於 Mina ByteBuffer 和 Java Nio ByteBuffer 的區別。關於 Java Nio ByteBuffer 和 Mina ByteBuffer 及其子類的類圖在附件中都已經給出了。因爲 Mina 的 ByteBuffer 在 Mina 2.0 以上的版本中都改稱 IoBuffer 。爲了使後文關於 ByteBuffer 的名字不致混淆, Mina ByteBuffer 都統稱 IoBuffer , Java Nio ByteBuffer 統稱 ByteBuffer 。關於 IoBuffer 中的對 ByteBuffer 擴展及一些重要的方法都在 IoBuffer 的類圖中用紅色方框標出。詳細的信息請參考附件中。" e6 {2 @8 D3 A' K8 E
在開始對 IoBuffer 的討論前,先簡單的講述一下 ByteBuffer 的用法。 IoBuffer 是對 ByteBuffer 的一個封裝。 IoBuffer 中的很多方法都是對 ByteBuffer 的直接繼承。只是對 ByteBuffer 添加了一些擴展了更加實用的方法。% p$ I, e) Z5 M( o0 O) V. X
(1) ByteBuffer簡介9 K( N: i/ S3 l, m* w( j
ByteBuffer繼承於 Buffer 類, ByteBuffer 中存放的是字節,如果要將它們轉換成字符串則需要使用 Charset , Charset 是字符編碼。它提供了把字節流轉換成字符串 ( 解碼 ) 和將字符串轉換成字節流 ( 編碼 ) 的方法。這個和後面講述的 Mina 的編解碼的工作原理類似。對 ByteBuffer 的訪問可以使用 read() , write() 等方法。
# A! J# C+ R1 x
ByteBuffer有一下三個重要的屬性:
* H1 G& y" @# D/ A7 h8 n
1) 容量(capacity) :表示該緩存區可以存放多少數據。7 S, D$ H" Y. _1 u' Z6 E
2) 極限(limit) :表示讀寫緩存的位置,不能對超過位置進行數據的讀或寫操作。1 m1 u( J6 C; g# _% _6 f B P, m B
位置(position) :表示下一個緩存區的讀寫單元。每讀寫一次緩存區,位置都會變化。位置是一個非負整數。* J% ^% ]: T+ l: n7 M: G
# G$ m3 P* }1 |$ S: M
ByteBuffer的這三個屬性相當於三個標記位,來表示程序可以讀寫的區域:
|
9 ^ v6 v8 }4 h! I. B
(2) IoBuffer簡介
IoBuffer是對 ByteBuffer 的擴展,並不是和 ByteBuffer 毫無關係的。對 Mina 或者 Socket 應用來說, ByteBuffer 提供的方法存在一下不足:
1 N+ g5 J, H3 W6 I- J! z1 \
1) 它沒有提供足夠可用的put 和 set 方法,例如: fill 、 get/putString 、 get/putAsciiInt() 等。
2)很難將可變長度的數據放入 ByteBuffer 。
基於以上的缺點,Mina 提供了 IoBuffer 來補充了 ByteBuffer 的不足之處。
Let's drink code,來看看 Mina 的 IoBuffer 是如何讀寫字符串的。5 O9 J- R3 X; Y' v" c4 a
/ J/ Q) z$ w6 I9 t4 \
Java代碼
// 獲取一個容量爲1024字節的ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 設置系統字符集爲utf-8
Charset ch =Charset.forName("utf-8");
// 獲取utf-8的編碼器 ' t! a; v: b+ A5 B
CharsetEncoder encoder = ch.newEncoder(); 5 G9 W9 ^" v' s; J2 R) z
// 獲取utf-8的解碼器
CharsetDecoder decoder = ch.newDecoder();
7 c( {, w: G7 Y
System.out.println(buffer.remaining());
// 進行編碼的字符串 $ J& |$ ]7 _3 R; ~" R
String cs = "中國壹石頭";
// 將字符串編碼後放入緩存 Q Z$ x7 V2 y" N1 c/ t8 ?
buffer.putString(cs,encoder); ! i0 a) ~0 f0 N k. ^" s
System.out.println(buffer.remaining()); / \% ?$ m) P, T" |; A6 V
// 將緩存的位置設爲位置
buffer.limit(buffer.position()); * o8 o! ?7 j% E6 H' S+ M! S1 y
// 將緩存的位置設爲0
buffer.position(0); 8 J& ^, Z. B7 Q' i, r3 \
// 讀取緩存中的字符串 1 v; O) ~) C. F0 u5 P! F! J" }6 d
String str = buffer.getString(decoder); * v1 w8 J% G B3 E; q
// 打印輸出緩存中的信息 ' w, m8 r- v# I& b: V# O
System.out.println(str); 9 T* `8 m) ?+ V6 V
System.out.println(buffer.remaining()); 1 h! m& y, n/ l. c8 n1 o0 L% N2 h
注意此處用到了 Charset ,它的作用在上面已經說道,它主要用來進行編解碼的,因此對字符串進行編碼和解碼時注意要使用相同的編碼。
(3) IoBuffer的子類
爲了更好的使用IoBuffer 進行開發, IoBuffer 提供了兩個子類 BaseByteBuffer 和 ByteBufferProxy 。 BaseByteBuffer 實現了 IoBuffer 中定義的絕大多數方法。如果你在實際開發中要擴展適合於自己的方法時可以繼承該類,因爲它可以使你的實現更加簡單。 ByteBufferProxy 中封裝了一個 IoBuffer ,所有對 ByteBuffer 的操作都可以通過該類提供的方法來實現。 ) J+ U# m$ y/ q2 Y: c
本文只是簡單的介紹了IoBuffer 和 ByteBuffer 的基本知識,如果需要了解 IoBuffer 更多的信息請參考 Mina 的幫助文檔和 Mina 的源碼。
文章轉載自:http://bbs.cnw.com.cn/viewthread.php?tid=211392&extra=&page=1 |