標籤:des class code java ext width
本文將介紹如何使用 Apache MINA2(以下簡稱 MINA2)解決複雜 Web 系統內各子系統之間同步訊息中介軟體的問題。MINA2 為開發高效能和高可用性的網路應用程式提供了非常便利的架構。從本文中可以瞭解 MINA2 的基本原理和主要功能,此外在本文中您還可以看到 MINA2 實現訊息中介軟體的服務端和用戶端程式的詳細內容。
4 評論:
蘇 夢, 軟體工程師, IBM
尹 文清, Java 開發工程師, 百度線上
2011 年 8 月 25 日
項目背景介紹系統發展遇到的瓶頸問題
目前主流網站都是由開源軟體構建的。使用 Nginx 做為 Web 服務器,Tomcat/Resin 做 App 容器,Memcached 做通用 Cache,MySQL 做資料庫,使用 Linux 作業系統。網站系統剛上線初期,使用者數並不多,所有的模組都整合一個系統中,所有業務由一個應用提供,此時採取將全部的邏輯都放在一個應用的方式利於系統的維護和管理。但是,隨著網站使用者的不斷增加,系統的訪問壓力越來越大,為了滿足越來越多使用者的需求,原有的系統需要增加新的功能進來,隨著系統功能模組的增多,系統就會變得越來越難以維護和擴充,同時系統伸縮性和可用性也會受到影響。例如一個網站初期只有使用者服務功能,隨著網站發展,可能會需要使用者資訊中心、儲值支付中心、商戶服務中心等越來越多的子系統,如果把這些子系統都整合在原有的系統中,整個網站將會變得非常複雜,並且難以維護。另外,由於所有子系統都整合在一起,只要有一個模組出問題,那麼所有的功能都會受影響,造成非常嚴重的後果。所以系統發展遇到的瓶頸就是隨著系統的發展,如果所有模組都整合在一起,系統的延展性和擴充性將受到影響。
如何解決系統發展遇到的瓶頸問題
遇到以上瓶頸該如何解決呢?明智的辦法就是系統拆分,將系統根據一定的標準,比如業務相關性等拆分為不同的子系統, 不同的子系統負責不同的業務功。拆分完成後,每個子系統單獨進行擴充和維護,不會影響其他子系統,從而大大提高整個網站系統的擴充性和可維護性,同時系統的水平伸縮性也大大提升了。對於壓力比較大的子系統可以再進行擴充而不影響其他子系統,如果某個子系統出現問題也不會影響其他服務。從而增強了整個網站系統的健壯性,更有利於保障核心業務。因此一個大型的互連網應用,肯定是要經過系統拆分的,因為只有進行拆分,系統的擴充性、維護性、伸縮性、可用性才會變得更好。但是拆分也會給系統帶來問題,就是子系統之間如何通訊。本文介紹 MINA2 就是用來充當訊息中介軟體解決各子系統之間的通訊問題
回頁首
MINA2 的原理及主要功能MINA2 簡介
MINA2 是一個網路通訊應用程式框架,它主要用於基於 TCP/IP、UDP/IP 協議棧的通訊架構,也可以提供 Java 對象的序列化服務、虛擬機器管道通訊服務等。MINA2 可以協助我們快速開發高效能、高擴充性的網路通訊應用。MINA2 提供了事件驅動、非同步(MINA2 的非同步 IO 預設使用的是 Java NIO 作為底層支援)操作的編程模型。
MINA2 同時提供了網路通訊的 Server 端、Client 端的封裝,無論是哪端,MINA2 在整個網路通訊結構中都處於如下的位置:
圖 1.MINA2 在網路通訊中的作用圖
可見 MINA2 的 API 將真正的網路通訊與我們的應用程式隔離開來,你只需要關心你要發送、接收的資料以及你的商務邏輯即可。同樣的,無論是哪端,MINA2 的執行流程如下所示:
圖 2.MINA2 執行流程圖
回頁首
MINA2 通訊原理非同步 IO 模型
非同步 I/O 模型大體上可以分為兩種,反應式 (Reactive) 模型和前攝式 (Proactive) 模型:
1. 傳統的 select / epoll / kqueue 模型,以及 Java NIO 模型,都是典型的反應式模型,即應用代碼對 I/O 描述符進行註冊,然後等待 I/O 事件。當某個或某些 I/O 描述符所對應的 I/O 裝置上產生 I/O 事件(可讀、可寫、異常等)時,系統將發出通知,於是應用便有機會進行 I/O 操作並避免阻塞。由於在反應式模型中應用代碼需要根據相應的事件類型採取不同的動作,最常見的結構便是嵌套的 if {...} else {...} 或 switch ,並常常需要結合狀態機器來完成複雜的邏輯。
2. 前攝式模型則不同。在前攝式模型中,應用代碼主動地投遞非同步作業而不管 I/O 裝置當前是否可讀或可寫。投遞的非同步 I/O 操作被系統接管,應用代碼也並不阻塞在該操作上,而是指定一個回呼函數並繼續自己的應用邏輯。當該非同步作業完成時,系統將發起通知並調用應用代碼指定的回呼函數。在前攝式模型中,程式邏輯由各個回呼函數串聯起來:非同步作業 A 的回調發起非同步作業 B ,B 的回調再發起非同步作業 C ,以此往複。 MINA2 便是一個前攝式的非同步 I/O 架構。
MINA2 基本概念
I/O服務:I/O 服務用來執行實際的 I/O 操作。MINA2 已經提供了一系列支援不同協議的 I/O 服務,如 TCP/IP、UDP/IP、串口和虛擬機器內部的管道等。開發人員也可以實現自己的 I/O 服務。由於 I/O 服務執行的是輸入和輸出兩種操作,實際上有兩種具體的子類型。一種稱為“I/O 接受器(I/O acceptor)”,用來接受串連,一般用在伺服器的實現中;另外一種稱為“I/O 連接器(I/O connector)”,用來發起串連,一般用在用戶端的實現中。對應在 MINA2 中的實現,org.apache.mina.core.service.IoService 是 I/O 服務的介面,而繼承自它的介面 org.apache.mina.core.service.IoAcceptor 和 org.apache.mina.core.service.IoConnector 則分別表示 I/O 接受器和 I/O 連接器。
I/O 接受器:I/O 接受器用來接受串連,與對等體(用戶端)進行通訊,並發出相應的 I/O 事件交給 I/O 處理器來處理。使用 I/O 接受器的時候,只需要調用 bind 方法並指定要監聽的通訊端地址。當不再接受串連的時候,調用 unbind 停止監聽即可。
I/O 連接器:I/O 連接器用來發起串連,與對等體(伺服器)進行通訊,並發出相應的 I/O 事件交給 I/O 處理器來處理。使用 I/O 連接器的時候,只需要調用 connect 方法串連指定的通訊端地址。另外可以通過 setConnectTimeoutMillis 設定連線逾時時間(毫秒數)。
I/O 會話:I/O 會話表示一個活動的網路連接,與所使用的傳輸方式無關。I/O 會話可以用來儲存使用者自訂的與應用相關的屬性。這些屬性通常用來儲存應用的狀態資訊,還可以用來在 I/O 過濾器和 I/O 處理器之間交換資料。I/O 會話在作用上類似於 Servlet 規範中的 HTTP 會話。
I/O過濾器:I/O 服務能夠傳輸的是位元組流,而上層應用需要的是特定的對象與資料結構。I/O 過濾器用來完成這兩者之間的轉換。I/O 過濾器的另外一個重要作用是對輸入輸出的資料進行處理,滿足橫切的需求。多個 I/O 過濾器串聯起來,形成 I/O 過濾器鏈。每個過濾器都可對通過的資料進行任意的操作,包括增加、刪除、更新、類型轉換等。先裝上的過濾器更靠近遠程端點 ( 用戶端),後裝上的更靠近本地端點 ( 伺服器)。
I/O處理器:I/O 事件通過過濾器鏈之後會到達 I/O 處理器。I/O 處理器中與 I/O 事件對應的方法會被調用。MINA2 中 org.apache.mina.core.service.IoHandler 是 I/O 處理器要實現的介面,一般情況下,只需要繼承自 org.apache.mina.core.service.IoHandlerAdapter 並覆寫所需方法即可。
MINA2 就是用來充當訊息中介軟體解決各子系統之間通訊的問題。在每個子系統增加 MINA2 的用戶端和服務端,負責接收和發送 Mina 訊息,調用其他子系統的業務功能和資料。
回頁首
MINA2 實現訊息中介軟體MINA2 在系統功能拆分中的作用
基於 MINA2 訊息中介軟體的系統架構如下所示
圖 3. 系統架構
以某業務運營平台拆分後的系統架構為例,整個平台包含三個子系統:業務運營子系統,負責提供使用者服務;使用者社區子系統是類似於 SNS 使用者互動平台;使用者子系統是使用者賬戶、個人資訊的子系統,是整個平台的公用基礎系統。整個平台的最頂層是 網頁伺服器層,包含數台 Nginx 伺服器(根據業務流量確定)。網頁伺服器層負責把不同的請求分發到對應的子系統,平台服務相關請求分發到業務運營子系統,使用者社區動態資訊相關請求分發到使用者社區子系統,使用者個人賬戶相關相關請求分發到使用者子系統。三個子系統的 APP 伺服器都是由一定數量的 Tomcat 伺服器組成,一般情況下,運行 Spring+Struts2+Hibernate 程式的 tomcat 伺服器能夠支援 130-150 個並發請求。APP 伺服器把大量資料緩衝在 Memcache 伺服器。另外通過 DB-Proxy 實現主從資料庫分離和叢集。每個子系統都有一個訊息中介軟體層,即 MINA 伺服器,通過 DB-Proxy 與本子系統的資料庫進行互動,訊息中介軟體層都包括 MINA 用戶端和 MINA 服務端用於接收和發送 MINA 訊息。
圖 4. 某業務運營平台拆分後的系統架構圖MINA2 實現伺服器端程式開發
- 建立 IOListener 類繼承 IOHandlerAdapter。IoHandlerAdapter 類實現了 IoHandler 介面要求的方法,但是都沒有任何商務邏輯處理。如果要編寫 Handler 時,可以擴充 IoHandlerAdapter,重寫需要的事件方法即可。一般情況,我們比較關心接收到請求訊息這個事件,那麼我們就可以覆蓋 messageReceived 方法,不用管其他方法。 程式清單如下: 清單 1. 建立 IOListener 類
import java.lang.reflect.Method; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.mina.core.service.IoHandlerAdapter; import org.apache.mina.core.session.IdleStatus; import org.apache.mina.core.session.IoSession; import org.springframework.beans.factory.annotation.Autowired; public class IOListener extends IoHandlerAdapter { private final static Log logger = LogFactory.getLog(MainProtocolHandler.class); @Autowired private AService aService; @Autowired private BService bService; …… }
- 重寫 messageReceived 方法。messageReceived 由 IoHandler 介面聲明。IoHandler 封裝了來自用戶端不同事件的處理,如果對某個事件感興趣,可以實現相應的方法,當該事件發生時,IoHandler 中的方法就會被觸發執行。
當接收到新的訊息的時候,該方法就會被調用。此處的邏輯是如果傳入 invoke_class 是 aService、,則通過反射機制調用 aService 的 invoke_method 方法,並把結果通過 session.write 發送回去。如果訊息參數中包含"return_method"值 , 則直接把 service 返回結果回寫給 session,由 session 通知用戶端調用"return_method"對應的方法。程式清單如下:
清單 2 訊息處理方法 public void messageReceived(IoSession session, Object message) { try { /** * invote_class 決定調用執行個體對象, * invoke_method 是要調用執行個體對象的方法 * return_method 是執行完成後的回調方法 */ Map<String, String> map = (Map<String, String>) message; if (!map.isEmpty() && map.containsKey("invoke_class") && map.containsKey("invoke_method")) { // 根據 message 中的 invoke_class 值調用對應 service if (map.get("invoke_class").equals("aService")) { if (!map.containsKey("return_method")) { // 如果 message 中包含 return_method 索引值,則調用 aService 中 return_method 鍵對應的方法 Method method = aService.getClass().getMethod( map.get("invoke_method"), new Class[] { Map.class }); method.invoke(aService, new Object[] { map }); session.write("done"); } else { // 通過 Java 的反射機制調用阿 Service 的方法,並把結果回寫給 session Method method = aService.getClass().getMethod( map.get("invoke_method"), new Class[] { Map.class }); session.write(method.invoke(aService, new Object[] { map })); } } …… } else { session.write("parameter error"); } } catch (Exception ex) { logger.error(ex.getMessage()); } return; }
- MINA2 與 Spring 整合的設定檔,其中 mainHandler 是處理器。
聲明 IO 過濾器包括:
- executorFilter:MINA 可以通過 ExecutorFilter 將 IO 線程與業務處理線程分開。
- textCodecFilter:ProtocolCodecFilter 用來在位元組流和訊息對象之間互相轉換。
- loggingFilter:記錄所有 MINA 的協議事件。由於該過濾器只是實現了 MINA 事件的簡單記 錄,實際作用不大,可配合 log4j 等日誌架構一起使用。
- filterChainBuilder:用來構建過濾器鏈。
清單 3 Spring 配置 MINA2 <bean id="mainHandler" class="com.xxx.ProcessHandler"></bean> <!-- the IoFilters --> <! — - 配置 executorFilter --> <bean id="executorFilter" class="org.apache.mina.filter.executor.ExecutorFilter"> <constructor-arg index="0"> <value>1000</value> </constructor-arg> <constructor-arg index="1"> <value>1800</value> </constructor-arg> </bean> <! — - 配置 textCodecFilter --> <bean id="textCodecFilter" class="org.apache.mina.filter.codec.ProtocolCodecFilter"> <constructor-arg> <bean class="org.apache.mina.filter.codec.textline.TextLineCodecFactory" /> </constructor-arg> </bean> <! — - 配置 codecFilter --> <bean id="codecFilter" class="org.apache.mina.filter.codec.ProtocolCodecFilter"> <constructor-arg> <bean class=" org.apache.mina.filter.codec.serialization.ObjectSerializationCodecFactory"/> </constructor-arg> </bean> <bean id="loggingFilter" class="org.apache.mina.filter.logging.LoggingFilter" /> <! —聲明過濾器鏈 --> <bean id="filterChainBuilder" class="org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder"> <property name="filters"> <map> <entry key="codecFilter" value-ref="codecFilter" /> <entry key="executor" value-ref="executorFilter" /> <entry key="loggingFilter" value-ref="loggingFilter" /> </map> </property> </bean> <!-- 設定 I/O 接受器,並指定接收到請求後交給 mainHandler 進行處理 --> <bean class="org.springframework.beans.factory.config.CustomEditorConfigurer" > <property name="customEditors" > <map> <entry key="Java.net.SocketAddress" > <bean class="org.apache.mina.integration.beans.InetSocketAddressEditor" /> </entry> </map> </property> </bean> <bean id="ioAcceptor" class="org.apache.mina.transport.socket.nio.NioSocketAcceptor" init-method="bind" destroy-method="unbind" > <property name="defaultLocalAddress" value=":1234" /> <property name="handler" ref="mainHandler" /> <property name="reuseAddress" value="true" /> <property name="filterChainBuilder" ref="filterChainBuilder" /> </bean>
MINA2 實現用戶端程式
建立 ProcessHandler 類繼承 IOHandlerAdapter。無論用戶端還是服務端,都需要建立繼承自 IOHandlerAdapter 的 hanlder 類。清單如下:
清單 4 建立 ProcessHandler 類
import java.lang.reflect.Method; import java.net.InetSocketAddress; import java.util.Map; import java.util.Random; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.mina.core.future.ConnectFuture; import org.apache.mina.core.service.IoHandlerAdapter; import org.apache.mina.core.session.IdleStatus; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolCodecFilter; import org.apache.mina.filter.codec.serialization.ObjectSerializationCodecFactory; import org.apache.mina.filter.logging.LoggingFilter; import org.apache.mina.transport.socket.nio.NioSocketConnector; public class ProcessHandler extends IoHandlerAdapter { private String hostName ; //MINA2 伺服器 IP 數組 private static final String[] HOSTS = {"xx.xx.xx.xx","xx.xx.xx.xx"}; private static final int CONNECT_TIMEOUT = 1000; private NioSocketConnector connector; private static final int PORT = 1234; private IoSession session; // 構造方法 public ProcessHandler (Map<String, String> map) { this.map = map; this.hostName = this.selectServer(); } // 隨機播放 MINA2 伺服器 IP,以實現 MINA2 叢集 private String selectServer() { try { int cc = HOSTS.length; if (cc <= 0) return null; Random rd = new Random(); int idx = (Math.abs(rd.nextInt()) % cc); return HOSTS[idx]; } catch (Exception e) { e.printStackTrace(); return null; } } …… }
由於訊息中介軟體需要處理較高並發的請求,所以一般使用 MINA2 伺服器叢集。在程式清 1 中,定義了一個 String 數組 HOSTS 儲存所有叢集的 MINA2 伺服器 IP,在初始化 ProcessHandler 對象執行個體的時候通過 selectServer()方法為本次串連選擇一個 MINA2 伺服器 IP。
與 Server 端 Handler 類不同的是,需要開發人員重寫相應的串連方法,已建立與 Server 端的串連。為滿足需要實現了三種串連方式:
- Client 不等待返回結果的串連方式,適合傳遞資料進行更新插入的請求,並返回串連建立後建立的會話,方便在同一個 Action 方法裡重用。代碼中的 IoSession connect() 就是這種串連方式。 清單 5.connect 方法
public IoSession connect() { if (session != null && session.isConnected()) { throw new IllegalStateException( "Already connected. Disconnect first."); } try { connector = new NioSocketConnector(); connector.setConnectTimeoutMillis(CONNECT_TIMEOUT ); connector.getFilterChain().addLast( "codec", new ProtocolCodecFilter( new ObjectSerializationCodecFactory())); connector.getFilterChain().addLast("logger", new LoggingFilter()); connector.setHandler(this); future = connector.connect(new InetSocketAddress(hostName, PORT )); future.awaitUninterruptibly(); if (!future.isConnected()) { return null; } session = future.getSession(); session.write(map); } catch (Exception ex) { throw new IllegalStateException("session is already closed"); } return session; }
- 建立串連並等待返回結果後關閉串連和會話。適合從 Server 端請求對象的操作。connectWithClose(boolean waitingFlag) 方法串連根據 waitinFlag 是否為真判斷是否等待返回結果,並關閉串連。 清單 6 connectWithClosure 方法
public boolean connectWithClosure(IoSession session, Map<String, String> map, boolean waitingFlag) { if (session != null && session.isConnected()) { try { session.write(map); if (waitingFlag) { session.getCloseFuture().awaitUninterruptibly(); } if (connector != null) { connector.dispose(); } return true; } catch (Exception ex) { throw new IllegalStateException("session is already closed"); } } return false; }
- 續用已建立的會話,並等待返回結果後關閉串連和會話。適合從 Server 端請求對象的操作。connectWithClose(IoSession session,Map<String, String> map, boolean waitingFlag) 方法複用 IoSession,根據 waitingFlag 是否為真判斷是否等待返回結果,並關閉串連。 清單 7 connectWithClosure 重寫方法
public boolean connectWithClosure(IoSession session, Map<String, String> map, boolean waitingFlag) { if (session != null && session.isConnected()) { try { session.write(map); if (waitingFlag) { session.getCloseFuture().awaitUninterruptibly(); } if (connector != null) { connector.dispose(); } return true; } catch (Exception ex) { throw new IllegalStateException("session is already closed"); } } return false; }
重寫請求結果回調方法,在請求返回結果後,通過反射調用對應方法。
清單 8.messageReceived 方法
@Override public void messageReceived(IoSession session, Object message) { try { if (!map.isEmpty() if (aService!=null){ Method method = aService.getClass().getMethod( map.get("return_method"), new Class[] { Object.class }); method.invoke(aService, new Object[] { message }); } session.close(true); } } catch (Exception ex) { logger.error("exception: " + ex.getMessage()); return; } }
MINA2 調用方法如下:
清單 9 MINA2 調用方法
Map<String,String> map = new HashMap<String,String>(); map.put("invoke_class", "userFeedService"); map.put("invoke_class","findUserFeed"); map.put("invoke_class","setFeedList"); map.put("userName", userName); ProcessHandler handler = new ProcessHandler (map,this); handler.connectWithClose(true);
以上代碼的含義是:建立 ProcessHandler 執行個體,調 MINA2 伺服器的 userFeedService 的 findUserFeed 方法, 並在接收到處理結果後直接調用本對象的 setFeedList 把結果賦給 feedList。
回頁首
總結與展望
本文首先提出了大型 Web 系統在發展過程中遇到的瓶頸—系統擴充性和維護性越來越困難,只有系統拆分才能突破這個瓶頸,而 MINA2 正是作為訊息中介軟體解決系統拆分後的同步通訊問題。本文接著介紹了 MINA2 的通訊原理和核心組件。本文提供了基於 MINA2 實現同步通訊的用戶端、服務端程式,方便讀者掌握 MINA2 開發訊息中介軟體。