Pattern-Oriented Software Architecture v1巨詳細讀書筆記 8

上個筆記中,已經描述了Forwarder-Receiver模式的例子,及所需要解決的問題,所處的上下文環境,還有它的典型場景,那麼在實際設計過程中,是如何來實現Forwarder-Receiver模式,而應用這種實現方式後我們前面所舉的例子最終會變成什麼樣子?這些都能夠在這次的筆記中找到相關的解答。
本筆記是《Pattern-Oriented Software Architecture vol.1 A system of patterns》原書[page 313-321]的山寨翻譯:),包括了Forwarder-Receiver模式的後半部分,主要是[實現]小節,以及[例子解決方案]和[變體]。
-----------------------------------------------------

[page 313]
[實現]
通過迭代以下步驟可以實現Forwarder-Receiver設計模式:
    1 描述名字-地址映射。既然peer通過名字引用其他的peer,則需要引入適當的命名空間,命名空間定義在給定的上下文中名字必須遵循的規則和限制。例如:可以指定所有名字必須由15個字符組成,並且必須是由大寫字符打頭,像“PeerVideoServer”就是一個符合這種規則的合法名字;又例如:也許需要用UNIX格式的路徑名稱來表示結構化的名字,如,“/Server/VideoServer/AVIServer”。
    一個名字不一定只指向單個地址,也許指向的是一組地址。當peer給遠端發送一條帶着組名稱的消息時,消息將被髮送給組內的每個成員。甚至你也可以引入層次結構,這樣就能允許一組成爲另一組的成員。

    2 描述Peer和forwarder之間的消息協議,此協議定義了forwarder從它的peer接收到的信息數據結構細節。同樣也需要定義Receiver和peer之間的消息協議。
    我們的例子DwarfWare精簡了消息協議,它既沒有包括錯誤處理,也沒有包括如數據分包之類的通信細節。在調用forwarder時,Peer傳遞的是類Message的對象。在peer接收消息時,它的receiver也是返回了一個Message對象給它。在此例子中消息只包含了unicode字符串格式的sender和消息數據,沒有包含接收端的名字,因爲sender將名字作爲了一個額外的參數傳遞給forwarder,這就能允許將同一條消息發送給多個不同的接收端。
    class Message {
        public String sender;
        public String data;
        public Message(String thesender, String rawData) {
            sender = thesender;
            data = rawData;
        }
    }

[page 314]
    我們也需要forwarder和遠程peer的receiver之間的協議,從forwarder發送給遠程receiver的消息也包含了sender的名字。
    每條消息都是用一串byte傳輸的,其中前4個byte指定消息的總長度,後續字節包含了消息的sender和消息數據本身。
    通常也需要應付系統超時,如:爲了避免整個系統在receiver接收響應消息失敗時阻塞,peer爲forwarder和receiver指定超時時間;超時時間也可以由用戶在運行時指定;或者也可以在實現forwarder和receiver時就在內部指定超時時間。
    當然還需要考慮到當通信失敗時,forwarder和receiver該怎麼做。根據應用程序的需求和底層IPC機制的不同,他們可以多次發送或接收消息,也可以在第一次嘗試通信失敗時就立即報告異常。

    3 選擇一種通信機制。這主要是由你所使用的操作系統中可用的通信機制所決定的,在指定IPC設施時以下方面需要考慮到:
    - 如果效率比較重要,首選如TCP/IP[Ste90]這樣的底層機制,這樣的機制是非常高效的,並且採用這樣的機制構建的通信協議也將非常靈活。
    - 採用像TCP/IP這樣的底層機制,實現時要付出很大努力,且依賴於你所使用的平臺,限制了可移植性。如果你的系統必須在平臺間移植,最好是採用像socket這樣的IPC機制,socket在大多數平臺都可用並且對於大多數應用程序來說都足夠高效。
    在DwarfWare中我們決定採用socket作爲底層通信協議。

    
[page 315]
    4 實現forwarder。在forwarder中封裝所有跨進程邊界的消息發送功能,封裝特定IPC機制的細節,通過公共接口對外提供功能。
    定義一個名字到物理地址的映射倉庫,forwarder在和遠程peer建立通信連接前訪問此倉庫獲得接收端的物理地址。此倉庫可以是預先確定的靜態表,也可以是運行時可以更改的動態表。動態表允許系統從表中動態地添加、移動或刪除peer項。確定每個forwarder是否需要擁有自己的私有映射倉庫,又或者所有的forwarder採用位於他們同一個進程中的公共映射倉庫。前一種情況允許你將同一個名字映射到不同的物理位置。例如:一個Peer能將名字‘Printer’關聯到多個不同peer的物理地址。你所使用的IPC機制決定了物理地址的結構,例如:如果用socket實現通信,則receiver的物理地址由Iternet地址和socket端口組成。可以使用hash表實現此倉庫。
    在我們的例子中,forwarder使用Registry作爲倉庫類來映射名稱-地址,此倉庫採用了標準java類庫的hash表來管理所有地址映射。遠程peer的物理地址是指目的機器名和socket端口號的組合,類Entry因而包含兩個數據成員:destinationID(目的機器名)和portNr(遠程peer的socket端口號)。倉庫類的實現中會將字符串映射到一個Entry類的實例:
    class Entry {
        private String destinationId; // target machine
        private int portNr; // socket port
        public EntryCString theDest, int theport) {
            destinationId = theDest;
            portNr = theport;
        {
        public String dest() {
            return destinationId;
        }
[page 316]
        public int port() {
            return portNr;
        }
    }

    class Registry (
        private Hashtable hTable = new Hashtable();
        public void put(String theKey, Entry theEntry) {
            hTable.put (theKey, theEntry) ;
        }
        public Entry get(String aKey) {
            return (Entry) hTable.get (theKey) ;
        }
    }
    下面引入Forwarder類,它的構造函數有個名爲theName的字符串參數,表示peer的邏輯名稱。當peer調用sendMsg時將發生以下事情:
    - sendMsg調用mashal將消息theMsg變成一串byte數據。
    - 調用deliver,此方法在本地倉庫中查找theDest的遠程peer的物理位置。
    爲了完成這些動作,全局類fr中的fr.reg存有一個映射倉庫實例;deliver將打開socket端口,連接到遠程peer,傳送消息,並關閉socket。
    class Forwarder {
        private Socket s;
        private Outputstream oStr;
        private String myName;
        public Forwarder(String theName) { myName = theName;}
        private byte [] marshal (Message theMsg) { / * . . . */ }
        private void deliver(String theDest, byte[] data) {
            try (Entry entry = fr.reg.get(theDest);
                s = new Socket(entry.dest() ,entry.port());
                oStr = s.getOutputStream() ;
                oStr.write (data) ;
                oStr.flush();
                oStr.close();
                s.close();
            }
            catch(I0Exception e) { /* . . . * / }
            }
        public void sendMsg(String theDest, Message theMsg) {
            deliver(theDest, marshal(theMsg)):
        }
    }

[page 317]
    將forwarder的職責(如:編碼,消息發送,映射倉庫)分離開是很有用的,所有功能都可分解到具體的IPC機制。可以採用Whole-part設計模式將forwarder的職責封裝在其分離的part組件中。
    
    5 實現receiver。將所有接收IPC消息的功能都封裝到receiver中,包含接收和解碼IPC消息的功能,(??Provide the receiver with a general interface that abstracts from details of a particular IPC mechanism.)給receiver提供從特定IPC機制細節中抽象出的通用接口。可以像第4步一樣,採用whole-part設計模式將這些receiver的職責封裝到分離的part組件中。
    設計receiver時特別需要考慮2個方面的問題。
    1 既然所有的peer都是以異步方式運行的,那麼就需要決定receiver是否應該阻塞,直到有消息到達:
    - 如果這樣,receiver會一直等待,直到有消息輸入時纔將控制權交還給peer,換句話說,peer在成功接收到消息之前不能繼續執行。當peer後續操作依賴輸入的消息才能完成的情況下,此情況是比較合適的。
    - 如果不這樣,就需實現非阻塞方式的receiver,允許peer指定超時時間(參見第2步)。如果在指定的時間範圍內沒有消息到達,receiver將返回一個異常給它的peer。
    如果底層IPC機制不支持非阻塞I/O,那需要在peer中使用單獨的線程來處理通信。
    2 另一個需要考慮的問題是,在receiver中使用多個通信通道。這種receiver能對多個通信通道進行多路分解(demultiplexing)——它會等待其中一個通道有數據到達,並在數據到達後將其返回給它的peer,如果同時有多個消息到達,則receiver可用一個內部消息隊列緩存這些消息。多路分解可能依賴於底層IPC機制,例如:UNIX系統中的select允許進程在一組文件或socket上等待事件輸入。如果IPC機制不支持多路分解,那需要你在receiver中用多線程來完成多路分解,其中每個線程負責一個通信通道。關於事件多路分解的細節可以參見Reactor模式[Sch94]。

[page 318]
    在我們的例子中提供了類Receiver。在peer實例一個receiver時,它會在其構造函數中傳入自己的名稱作爲參數,receiver用這個名字來確定接收消息的socket端口號。當peer要接收消息時,它會調用Receiver對象的receiveMsg()方法,receiveMsg隨後又會調用receive()方法,receive()做了2件事情:
    - 從全局的映射倉庫中獲取了socket端口後,它打開服務器的socket,並等待遠程peer連接。
    - 一旦連接建立起來,到達的消息和它的大小都從通信通道中讀取出來,receive()將讀取到的數據返回給receiveMsg。
    最後,receiveMsg()執行unmarshal將byte數據串轉換成Message對象並將此對象返回給peer。
    class Receiver {
        private Serversocket snrS;
        private Socket s;
        private Inputstream iStr;
        private String myName;
        public Receiver(String theName) { myName = theName;}
        private Message unmarshal (byte [] anArray) { /* . ., */ }
        private byte[] receive() {
            int val;
            byte buffer [] = null;
            try {
                Entry entry = fr.reg.get(myName);
                srvS = new ServerSocket(entry.port0, 1000);
                s = srvS.accept();iStr = s.getInputStream();
                val = iStr. read ( ) ; buffer = new byte [val] ;
                iStr.read(buffer1 ;
                iStr.close0; s.close(); srvS.close();
            }
            catch(I0Exception e) { /* . . . */ }
            return buffer;
        }
        public Message receiveMsg () {
            return unmarshal(receive()) ;
        }
    }
    
    6 實現peer。將peer分爲client和server兩個集合,兩者之間可以有交集。扮演client角色的peer會向遠程peer發送消息並等待響應,接收到響應後,它繼續執行自己的任務。扮演server角色的peer一直會等待有消息輸入,當消息到達時,它會執行此消息所請求的服務,並將響應發送給請求者。server有可能是別的server的client,甚至server和client會在運行時動態地變換他們之間的角色。
    兩個peer之間的通信不一定是雙向的,有時一個peer只是發送一條消息到另一peer中,並不需要響應(單向通信)。peer發送了消息後就繼續執行它的任務,消息的接收端通過他的receiver收到消息,但是不會發送響應給發送端。可以將單向通信應用於發送端和接收端的異步通信。
    這裏舉一個扮演server角色的peer的例子:
    class Server extends Thread {
        Receiver r;
        Forwarder f;
        public void run() {
            Message result = null ;
            r = new Receiver ( "Server") ;
            result = r.receiveMsg ( ) ;
            f = new Forwarder ( "Server") ;
            Message msg = new Message ("Server","I am alive" ) ;
            f.sendMsg(result.sender, msg);
        }
    }

    7 實現一個啓動配置。系統啓動時,forwarder和receiver必須用有效的名字-地址映射來初始化。單獨引入此步驟,是爲了創建映射倉庫並錄入所有的名字/地址關係。採用配置的方式可以從外部文件中將對應關係讀取進倉庫中,避免在改變映射時更改源代碼。
    如果系統允許不同的peer有不同的名字-地址映射,那需要讓啓動配置能根據此需求初始化不同的映射倉庫。
[page 320]
    如果配置能動態改變,則需要實現一些附加功能在運行時能修改映射倉庫。
    在DwarfWare例子中,實現了Configuration類,允許用戶在全局映射倉庫中註冊server和client:
    class Configuration {
        public Configuration ( ) {
            Entry entry = new Entry ("127.0.0.1" ,1111) ;
            fr.reg.put ("Client", entry);
            entry = new Entry("127.0.0.1",2222);
            fr.reg.put ("Server",entry );
        }
    }

[例子解決方案]
    在我們的網絡管理基礎架構中,公共協議決定了請求、消息和響應的格式。如果agent要從遠程agent獲取如當前資源內容這樣的信息,它會發送一條消息給接收端,接收端從它的receiver收到此消息後會把請求的信息數據打進響應包,並把響應包發回給消息的發送源。當agent傳送的是一條command消息,接收端收到消息後就會解析它,並執行適當的命令,然後會告訴發動端是否能成功執行此command。所有相關信息都會用圖形界面顯示在網絡管理控制檯上,爲了增加可用性,網絡中的每臺機器都能運行網絡管理控制檯。

[變體]
    沒有名字-地址映射的Forwarder-Receiver的映射。……

[page 321]
……

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