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]
……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值