叶轻舟 北京林业大学 信息学院 在读本科生
需求概述
本文所讨论的是本周计算机网络课程实习任务中,一个关于透过TCP/UDP在客户端之间实现文字通信的小程序。我使用Java语言实现。具体要求如下:
1. 实现在控制台模式下,于单机上实现分别基于TCP/UDP协议的简单聊天工具;
2. 实现方式为一方发送、一方接收,交替进行;
针对需求的讨论
这是一道非常常见的传输层协议网络编程问题,相信很多同专业的同学在本科期间都曾经做过。看到这里,我开始思考如何构建整个软件。当然,使用纯面向过程的方法完全可以解决这个问题:
• 把连接建立的整个过程写在main方法里并且用两个变量封装Socket,用几个常量写死端口号和收发双方IP地址;或者再完善一点,写死端口号而要求用户自行输入对方IP;
• 然后,分别使用Socket的getOutputStream()和getInputStream()方法获取输入/输出流,利用Java的数据流机制,即DataInput/OutputStream方法分别获取输入/输出流并返回两个实力,并且透过这两个实力对数据的发送/接收进行操作
但,这样的程序虽然功能得以实现,但是实为线性结构,无从谈起代码复用性、可移植性等。据此,可以将此过程分解为以下几个部分,其问题可以概括为:
1. 主机连接建立过程:
• 用于收/发之TCP连接建立的过程,即Socket实例的获取过程;该过程涉及主机的角色(连接主动发起方/被动接收方)的问题:发起方需要提供接收方的IP地址和接收端口号,而被动接收方需要监听某个端口;
• 输入/输出流连接的建立过程,即使用Socket实例的getOutputStream()/getInputStream()方法获取输入/输出流连接的实例;并且在本地用其对数据输入/输出流,即DataInput/OutputStream实例进行初始化;
1. 数据交换过程,即使用刚刚建立的最外层的数据输入/输出流实例操作整个数据传输过程。此处涉及合适关闭Socket连接的问题:
• 程序启动时,监听Socket需要跟随初始化;
• 当用户决定要对其他主机主动发起连接时,关闭监听Socket,使用Socket([地址], [端口号])方法对发起连接使用的Socket进行初始化;
1. 会话结束过程:一方使用close()方法关闭Socket,妥善处理一方关闭连接后对方所产生的IOException问题。
面向对象大师Martin Fowler在《Analysis Pattern》一书中指出,模式分为:
1. 体系结构模式(Architectural Patterns):描述一个软件系统的基础组织结构,它提供了一组预定义的字系统,指定了各自的责任,并且包含了组成子系统之间相互关系的规则和指南。体系结构提供了软件系统结构的模板,它指出了系统级别上的应用程序的结构特性,对应用系统的子系统结构具有指导意义。它能帮助我们对系统进行合理的分解。相关的模式有:层(Layers)模式,管道过滤器 (Pipes and Filters)模式和黑板(Blackboard)模式等;
2. 设计模式(Design Patterns):供了一种细化软件系统中子系统或组件以及它们之间相互关系的策略。它描述了在某一特定场景下求解某一类普遍性问题的经常使用 的相关组件结构。设计模式提供了复杂功能组件分解的结构模式;
3. 惯用法(Idioms):是一种与特定编程语言相关的低层次模式,它描述了如何使用编程语言中提供的特性实现组建及其相关部分的具体内容。
在以上三种模式中,本文针对该程序的问题,着重讨论设计模式。基于经典的GOF设计模式,我根据整个系统的运行流程,提出了一个解决方案:
1. 在数据连接和的创建过程中,使用创建型模式(与对象创建有关)的工厂方法进行操作;
该模式具有两个特点:
• 将关于系统使用哪些具体的类信息封装起来;
• 隐藏了类实例是如何被创建和放在一起的,整个系统关于这些对象只能透过抽象类的接口访问(在本案中,我采用了接口的方式);
针对该问题仅需要产生一种类型的对象,而该种类型对象的构造方法又不确定的需求,我提出了一种既不同于所产生对象类型确定、无法实现多态的简单工厂模式,又相对于使用两级接口(其中一个为产品接口,定义了对返回值类型的抽象;另一级为工厂接口,定义了创建产品所需的工厂方法)的工厂方法来说较为简单的模式来进行操作,即取消产品接口,将返回值类型(Socket)写死。这样做的好处是节省了没有必要的多态开销,简化了整个连接创建模型的复杂度:
将本机作为TCP连接请求接收端/发送端所需获取的连接过程分别封装在包com.ani.tcpconnection中的两个类:
• ReceiveConnection:封装创建接受连接所用套接字所需要的port(端口)变量以及Socket establishConnection(int port)方法;
• SendConnection:封装创建发送连接所用套接字所需要的port(端口)、host(对方主机IP)以及SocketestablishConnection(int port, String host)方法;
在接下来的过程中,创建con.ani.datatransfer包,该包封装了产生最终数据流对象所使用的工厂接口DataStreamFactory及实现该接口的两个分别用于创建/接收连接的方法,以及封装最终返回值出/入数据流对象所使用的类DataTransferStream。由于该工厂所产生对象的类型仅为DataTransferStream一种,故不需要对所返回的类型进行抽象:
• DataStreamFactory接口:用于创建DataTransferStream类对象的工厂接口;
• DataTransferStream类:用于封装最终所产生的DataInputStream以及DataOutputStream两个类所产生的、用于主机之间数据交换的实例;
• ReceiveDataFactory:实现DataStreamFactory接口,用于创建在使用被动连接时所传输的数据流;
• SendDataFactory:实现DataStreamFactory接口,用于创建在主动连接对方主机时所传输的数据流;
1. 在结构型模式(处理类或对象的组合)上,使用适配器(Adapter)方案;由于该软件规模较小,不涉及模块问题,故在此不讨论结构模式。
2. 在行为型模式(对类或对象怎样交互及怎样分配的职责进行描述)上;包括:
• 中介者模式;
• 观察者模式;
• 策略模式;
• 模板方法;
其主要特点是“封装变化”,时将“行为”的对象与行为本身分开,达到降低耦合度的效果,使得“行为”对象可以被轻易维护,还可以透过类继承实现扩展,体现了面向对象设计的抽象特性。
行为型设计模式中的大多数模式涉及两种对象,两者间透过对象组合一起工作:
• 封装了可变化特征的接口对象;
• 使用这些接口对象的已有对象;
对于本程序,其行为包含:
• 会话的建立(一方主动发起连接并且停止监听,另一方收到后连接建立,停止监听的过程要使用工厂的实例实现);
• 发送信息(利用从工厂获取的DataOutputStream实例);
• 接收信息(利用从工厂获取的DataInputStream实例,当无信息读入时阻塞);
在TCP程序中,我尝试对行为模式进行封装,但由于抽象过程较为困难(例如,主动/被动两个连接创建过程中,所使用参数的数量不一至),其封装复杂度过高,故放弃对行为进行抽象和进一步封装。
然而,在UDP通信程序中,由于UDP具有不面向连接的特性,故没有建立连接这一过程,所以没有必要使用工厂模式。不过,我们可以使用行为型模式对其进行抽象与封装。在该程序中,其具有如下行为:
• 信息的生成:利用byte数组封装信息;
• 数据的封装(数据报的生成):DatagramPacket类,利用其构造方法进行封装;
• 数据报的发送:DaragramSocket的send方法;
• 数据报的接收和显示:DaragramPacket的实例接收DaragramSocket之receive方法传来的数据;
由此可知,系统的行为可以总体概括为发送/接收行为,这两种行为是互逆的。故,将数据交换行为封抽象为一个接口,然后分别实现其收/发命令。
小结
本次所实现的TCP端通信程序,规模较小。但很多有过实际开发经验的朋友应该了解,针对小模块结构的讨论是有实际意义的。尽管在开发实际的应用系统的时候我们会用到很多框架,比如我主要搞Java Web开发,平时会用到Struts + Spring做系统搭建,但对系统结构的设计同样是必不可少的。