pushlet 2.0.3 源碼分析(服務器端)

----服務器端
1 總體架構
Pushlet從功能上實現了服務器推技術,整個框架涉及了服務器端以及客戶端的部署。服務器端採用servlet技術,監聽客戶端請求。客戶端分爲兩大類,瀏覽器以及桌面應用程序。下圖描述了系統的整體框架:


圖1 pushlet總體架構圖
從圖中可以看出服務器端返回響應的出口只有一個,那就是clientAdapter,它只是一個接口,根據不同的客戶端類型來產生相應的adapter發送響應結果。
各個類的主要職責描述:
Pushlet:負責接收所有用戶請求,並將請求包裝爲event對象,在根據session、event、request、response對象構造一個command對象,最後將command對象交由controller處理。
Session:代表一次用戶會話,此類不同於httpsession,因爲此session的實現是使用類似url重寫方式,在服務器分配了sessionid之後的每個請求中都加入這個參數以標識會話。會話在其存活期內有效。
Controller:是所有命令的執行器,包括各種控制命令以及數據推送命令。不過對於數據推送的實際執行並不是在controller中實現,而是委託給subscriber只執行。
Subscriber:這是核心類之一。它維護了一個阻塞的事件隊列,根據客戶端使用的不同模式(框架定義的模式有:stream,pull/poll,stream使用了http長連接,pull/poll則是通過客戶端定時刷新實現服務端推送)來發送響應事件。
Dispatcher:事件分發器,也是核心類之一。事件來源可以是客戶(通過publish命令發佈事件),也可以是eventSource。實現了多播,廣播以及單播事件,具體採用哪種方式根據事件屬性決定。事件接收端即是subscriber的事件隊列。
clientAdapter:有3個具體實現,browserAdapter、XMLAdapter、serializedAdapter。分別用來發送javascript、xml、序列化數據。使用於不同的客戶端。具體使用哪種adapter需要根據用戶請求事件的format參數決定。
其他公共類:提供了日誌、可配置等功能。


圖2 核心類的對應關係
2 原理分析
Pushlet採用服務器端回調技術以及HTTP長連接實現了服務器推服務的兩種模式,即stream,pull/poll。其中對於瀏覽器客戶端還應用了DHTML技術,通過回調javascript函數在不刷新頁面的前提下實時更新,普通的桌面應用很容易便可以實現這種效果。爲了提高系統的可靠性以及健壯性,通信過程中開通了兩條通道,控制通道和數據通道。控制通道不會阻塞,能夠實時接收客戶命令,而數據通道工作在阻塞模式下,當傳輸模式爲stream時,數據通道連接不斷開,直至用戶發送斷開命令或客戶端退出或服務器異常,爲了防止阻塞時間過長導致客戶端無法得知服務器是否正常工作,系統設置了阻塞過期時間,並且在過期之後向客戶端發送心跳消息表明自身仍然存活。而在pull/poll模式下,阻塞直至有數據可以發送,然後斷開連接。瀏覽器客戶端需要不停的發送心跳請求,目的是爲了解決瀏覽器一直繁忙的狀態。以下是系統的協議服務(protocol services)

 

 

Service Description
join 啓動一個會話
leave 結束會話
subscribe 訂閱相關主題
unsubscribe 取消訂閱相關主題
listen 打開數據通道,以stream、pull或poll模式開始數據流傳輸。在pull/poll模式下,服務器提供所謂的刷新操作,實際上是客戶端來重新請求以獲取數據
join-listen 通過一個請求完成會話啓動,訂閱並監聽數據。執行完之後的狀態與執行完listen類似。
publish 發佈數據,然後服務器將其分發。客戶端可以使用它進行多播或單播數據。
heartbeat 表明會話存活


3 具體實現
Pushlet採用了大量的單例和工廠模式,另外還有適配器模式、命令模式。實現中遵循面向接口以及抽象類編程,這些使得系統易於理解,易於擴展。系統的大多數屬性都是在配置文件中指定,如果有通過系統擴展點編寫的擴展類要替代默認實現的話,只需要修改配置文件指向你自己的類文件即可,不需改動代碼。接下來就沿着請求—響應的主線來分析系統源碼。
請求入口pushlet
Init()方法:
30 String webInfPath = getServletContext().getRealPath("/") + "/WEB-INF";
31 Config.load(webInfPath);//載入配置文件,存放在該類的變量中
32
33 Log.init();//初始化日誌類
34
35 // Start
36 Log.info("init() Pushlet Webapp - version=" + Version.SOFTWARE_VERSION + " built=" + Version.BUILD_DATE);
37
38 // Start session manager,負責管理session生命週期,這是系統的擴展點,下文詳解.
39 SessionManager.getInstance().start();
40
41 // Start event Dispatcher,負責分發系統或客戶事件
42 Dispatcher.getInstance().start();
43
44
45 if (Config.getBoolProperty(Config.SOURCES_ACTIVATE)) {
46 EventSourceManager.start(webInfPath);//啓動事件源管理器
47 } else {
48 Log.info("Not starting local event sources");
49 }
初始化完畢之後便可以處理用戶請求了.它可以處理兩種請求,get和post,處理方式主要是提取請求參數,然後將其封裝成event事件對象,再進一步構造command對象,最終的處理有交給了controller。這部分的代碼很簡單,因爲主要的處理邏輯都委託給了controller。這段代碼有幾點是值得學習的。
1) 抽象。Event對象封裝了屬性—值對,內部通過hashmap實現,原理上來講,它可以封裝任何信息,爲了使這樣的一個抽象能夠適於作爲系統的通用數據抽象形式,還需要加入一個必備屬性,即event_type。請求以及數據均被定義爲事件,然後通過內部協議來區分它們。通過抽象之後,系統可以以一致的處理形式應對各種數據。後面將要分析的subscriber維護着一個事件隊列,使用該隊列完成所有的交互。這便是使用了這個抽象機制的好處。
2) 命令模式command。一個命令裏包含了請求事件、響應事件以及response,request,session對象。Controller便是這個命令的執行器,通過一個簡單的doCommand接口隱藏了內部複雜的處理邏輯,降低了模塊的耦合度。執行完命令之後,要輸出的結果就是響應事件responseEvent。Controller處理代碼如下
49 // Update lease time to live,更新session生存期,防止過期
50 session.kick();
51
52 // Set remote IP address of client,設置遠程客戶端地址
53 session.setAddress(aCommand.httpReq.getRemoteAddr());
54
55 debug("doCommand() event=" + aCommand.reqEvent);
56
57 // Get event type
58 String eventType = aCommand.reqEvent.getEventType();
59
60 // Determine action based on event type,根據事件類型採取
相應的操作,分別構造響應事件
61 if (eventType.equals(Protocol.E_REFRESH)) {
62 // Pull/poll mode clients that refresh
63 doRefresh(aCommand);
64 } else if (eventType.equals(Protocol.E_SUBSCRIBE)) {
65 // Subscribe
66 doSubscribe(aCommand);
67 } else if (eventType.equals(Protocol.E_UNSUBSCRIBE)) {
68 // Unsubscribe
69 doUnsubscribe(aCommand);
70 } else if (eventType.equals(Protocol.E_JOIN)) {
71 // Join
72 doJoin(aCommand);
73 } else if (eventType.equals(Protocol.E_JOIN_LISTEN)) {
74 // Join and listen (for simple and e.g. REST apps)
75 doJoinListen(aCommand);
76 } else if (eventType.equals(Protocol.E_LEAVE)) {
77 // Leave
78 doLeave(aCommand);
79 } else if (eventType.equals(Protocol.E_HEARTBEAT)) {
80 // Heartbeat mainly to do away with browser "busy" cursor
81 doHeartbeat(aCommand);
82 } else if (eventType.equals(Protocol.E_PUBLISH)) {
83 // Publish event
84 doPublish(aCommand);
85 } else if (eventType.equals(Protocol.E_LISTEN)) {
86 // Listen to pushed events
87 doListen(aCommand);
88 }
89
90 // Handle response back to client
91 if (eventType.endsWith(Protocol.E_LISTEN) ||
92 eventType.equals(Protocol.E_REFRESH)) {
//請求類型是listen或refresh,表明是獲取數據
93 // Data channel events
94 // Loops until refresh or connection closed
95 getSubscriber().fetchEvents(aCommand);
96
97 } else {
98 // Send response for control commands,控制命令,直接返回。
99 sendControlResponse(aCommand);
00 }

sendControlResponse()代碼:

31 aCommand.sendResponseHeaders();//設置響應頭,主要是客戶端//緩存無效
32
33 // Let clientAdapter determine how to send event
//通過clientAdapter發送響應事件
34 aCommand.getClientAdapter().start();
35
36 // Push to client through client adapter
37 aCommand.getClientAdapter().push(aCommand.getResponseEvent());
38
39 // One shot response
40 aCommand.getClientAdapter().stop();

Subscriber:
fetchEvents()部分代碼:
。。。。。。。。。。。。。。。。。
。。。。。。。。。。。。。。。。
15 Event[] events = null;
16
17 // Main loop: as long as connected, get events and push to client
18 long eventSeqNr = 1;
19 while (isActive()) {//這個循環保證了連接不被關閉,即可以以流的//方式發送響應到客戶端,真正意義上的服務器推送數據
20 // Indicate we are still alive
21 lastAlive = Sys.now();
22
23 // Update session time to live
24 session.kick();
25
26 // Get next events; blocks until timeout or entire contents
27 // of event queue is returned. Note that "poll" mode
28 // will return immediately when queue is empty.
29 try {
30 // Put heartbeat in queue when starting to listen in stream mode
31 // This speeds up the return of *_LISTEN_ACK
32 if (mode.equals(MODE_STREAM) && eventSeqNr == 1) {
33 eventQueue.enQueue(new Event(E_HEARTBEAT));
34 }
35 //此方法獲取事件隊列裏的事件,有超時設置,爲阻塞操作
36 events = eventQueue.deQueueAll(queueReadTimeoutMillis);
37 } catch (InterruptedException ie) {
38 warn("interrupted");
39 bailout();
40 }
41
42 // Send heartbeat when no events received,超時後,發送心跳信息
43 if (events == null) {
44 events = new Event[1];
45 events[0] = new Event(E_HEARTBEAT);
46 }
47
48 // ASSERT: one or more events available
49
50 // Send events to client using adapter
51 // debug("received event count=" + events.length);
52 for (int i = 0; i < events.length; i++) {
53 // Check for abort event
54 if (events[i].getEventType().equals(E_ABORT)) {
55 warn("Aborting Subscriber");
56 bailout();
57 }
58
59 // Push next Event to client
60 try {
61 // Set sequence number
62 events[i].setField(P_SEQ, eventSeqNr++);
63
64 // Push to client through client adapter
65 clientAdapter.push(events[i]);
66 } catch (Throwable t) {
67 bailout();
68 return;
69 }
70 }
71
72 // Force client refresh request in pull or poll modes
73 if (mode.equals(MODE_PULL) || mode.equals(MODE_POLL)) { //如果不是stream模式,就在向客戶端發送刷新命令,以獲取新 //的數據 ,並退出循環,服務器自動關閉連接。因爲這是http連接,響應方法//只要返回連接就會被關閉。 
74 sendRefresh(clientAdapter, refreshURL);
75
76 // Always leave loop in pull/poll mode
77 break;
78 }
79 }
最後,響應事件通過clientAdapter真正發送到客戶端。
BrowserAdapter部分代碼:
13 protected String event2JavaScript(Event event) throws IOException {
14//將事件對象轉化爲javascript腳本,實際上是回調腳本的代碼
15 // Convert the event to a comma-separated string.
16 String jsArgs = "";
17 for (Iterator iter = event.getFieldNames(); iter.hasNext();) {
18 String name = (String) iter.next();
19 String value = event.getField(name);
20 String nextArgument = (jsArgs.equals("") ? "" : ",") + "'" + name + "'" + ", /"" + value + "/"";
21 jsArgs += nextArgument;
22 }
23
24 // Construct and return the function call */
25 return "<script language=/"JavaScript/">parent.push(" + jsArgs + ");</script>";
26 }
Command部分代碼:使用適配器模式,可以將不同客戶端處理方式的不同點隱藏,客戶代碼使用同一接口調用,這樣可以很方便的添加其他客戶端類型的適配器。不過我個人覺得這三種適配器已經可以適應所有客戶端類型了,而且框架的作者也沒做擴展的打算。因爲在這裏是直接硬編碼生成適配器對象的,而沒有用到反射機制動態生成配置文件所定義的類型。
protected ClientAdapter createClientAdapter() throws PushletException {
96
97 // Assumed to be set by parent.獲取響應格式,系統定義了4種格式,
// js、xml、 ser(序列化對象)、xml-strict
98 String outputFormat = session.getFormat();
99
00 // Determine client adapter to create.根據不同的格式返回相應的//Adapter 
01 if (outputFormat.equals(FORMAT_JAVASCRIPT)) {
02 // Client expects to receive Events as JavaScript dispatch calls..
03 return new BrowserAdapter(httpRsp);
04 } else if (outputFormat.equals(FORMAT_SERIALIZED_JAVA_OBJECT)) {
05 // Client expects to receive Events as Serialized Java Objects.
06 return new SerializedAdapter(httpRsp);
07 } else if (outputFormat.equals(FORMAT_XML)) {
08 // Client expects to receive Events as stream of XML docs.
09 return new XMLAdapter(httpRsp);
10 } else if (outputFormat.equals(FORMAT_XML_STRICT)) {
11 // Client expects to receive Events embedded in single XML doc.
12 return new XMLAdapter(httpRsp, true);
13 } else {
14 throw new PushletException("Null or invalid output format: " + outputFormat);
15 }
16 }

單例模式以及工廠模式:
Dispatcher,SessionManager兩者都使用了單例模式,在全局維持一個實例,充當了全局對象的作用,因爲保存在這些對象裏的數據或方法可以很方便的被進程內的其他對象訪問,如session集合、dispatcher的各種分發事件的方法。這種方案在進程內可以很好的工作,但是如果想將應用擴展成爲分佈式應用,那就必須修改這些實現。
爲什麼要考慮分佈式的可能呢?因爲stream模式是通過HTTP長連接實現的。保持這個連接意味着每有一個訂閱請求,就會持續佔用那個連接,直到產生取消訂閱的請求或者服務器異常。因爲連接一致被佔用,相應的servlet線程也被佔用了,這樣系統的總吞吐量就取決於線程池的大小乃至操作系統的連接限制。這樣的限制直接導致了這個框架無法滿足中型以上的系統需求。其中一種解決方案就是使框架支持分佈式,通過多臺服務器並行處理請求,在分佈式系統中,相應的分佈式sessionManger,Dispatcher是必須的,但是實現這兩個類的難度顯然是很高的,不知在以後的版本中是否會有這種實現。
目前,我覺得pull/poll模式更爲實用,因爲這種模式不會持續保持連接,使線程池可以發揮作用,但是,它是靠客戶端定時刷新的,這樣會給服務器帶來較大的壓力,如果刷新很頻繁的話,實際的吞吐量也不高。(本人並沒有實際測試過,但是從理論分析應該是這樣的)
Controller、Session、Subscriber、Subscription、EventSourceManager這些類使用了工廠模式並結合java反射機制動態生成實例對象,這些都是框架預留的擴展點,開發人員可以通過擴展點實現符合自己需求的類,並通過配置文件將其整合到框架中來。一段典型的代碼如下:
摘自Controller.java
33 public static Controller create(Session aSession) throws PushletException {
34 Controller controller;
35 try {
//讀取配置文件,並生成實例對象
36 controller = (Controller) Config.getClass(CONTROLLER_CLASS, "nl.justobjects.pushlet.core.Controller").newInstance();
37 } catch (Throwable t) {
38 throw new PushletException("Cannot instantiate Controller from config", t);
39 }
40 controller.session = aSession;
41 return controller;
42 }

總結:通過閱讀pushlet的源碼,讓我學到了很多實戰編程經驗,希望本文可以給java愛好者一點點幫助。
注:本文並沒有分析所有代碼細節,而且只針對服務器端代碼,如果有興趣的話可以自己到網上下載pushlet的源碼,去體驗高人的風範!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章