摘要 基于C/S设计模式,构建了远程方法调用的程序设计框架,以使编程人员能够集中精力进行商业业务逻辑的设计。整个框架的构造围绕同一个例子展开,代码具有良好的可读性,框架设计过程中用到了诸如Socket、线程、串行化、数据流包装、编发、通讯协议、证书签名与认证等许多Java高级知识。 关键词 分布式程序设计,远程方法调用,C/S模式,编发
一、引论
程序开发中存在三种不同的应用方式:传统方法、C/S方法和基于构件(中间件)的方法。 传统方法中,表示逻辑、业务逻辑和数据库连接都混合到了单个程序代码中。这种基于整块应用(monolithic applications)的设计方法缺点很多:比如应用中出现很小的变动,整个应用都必须重新编译、再次集成,这将直接导致代码更新和应用再发布的代价攀升。 考虑到传统方法的缺点,后来引入了客户/服务器(C/S)构件,在这种架构中,数据从客户端分离出来,存储在服务器的集结处(数据库、文件、XML等)。由于这种架构并没有将业务逻辑和表示逻辑分离出来,因此又称两层构架。在这种架构中,数据库连接代码的程序在服务器端,鉴于业务逻辑和表示逻辑的存放位置的不同,又分为:胖客户端和瘦客户端。如果业务逻辑和表示逻辑被放置在客户端,则为胖客户端,如果在服务器端,则为瘦客户端。两层架构的缺点主要有以下两点:首先,业务逻辑的任何变动都可能影响表示逻辑或者数据库连接代码的变更;其次,使用两层架构的应用很难放大。由于客户连接直接面对数据库,而服务器端客户可用的数据库连接数目又受诸多因素(驱动程序、系统资源)限制,连接请求经常会因为超过这个限制而被拒绝。为了改变这个缺点,引入了三层或者N层架构设计。 三层架构中,表示逻辑驻留在客户端、数据库访问代码驻留在服务器端。业务逻辑单独分离出来作为中间件,这样,用户连接和数据库访问面对的就是业务逻辑,对用户而言,不需要直接操作数据库,对服务器而言,访问数据库的不是难以应付的多客户连接,而是可以使用缓冲池控制的有限的几个可用连接。这种架构模式使得程序设计者可以把精力集中转移到业务逻辑的实现上,业务策略的变动只需要更改表示逻辑就可以了。这种构件的集中特性使得开发、维护、部署都非常容易。 表示逻辑、业务逻辑和数据库驻留在多台机器上的应用称为分布式应用。开发分布式应用有很多种方法:一种是使用SUN公司提供的远程方法调用(RMI),使用这种方法程序员不需要知道Socket编程,不需要知道多线程管理,不需要知道网络传输可靠性控制和安全控制,只要集中考虑如何开发业务逻辑就可以了。另外一种方法就是自己编写Socket程序。使用套接字来处理跨边界的由一个宿主机到另外一个宿主机的数据传递。由于使用SUN公司的RMI需要在服务器端启动RMI远程注册表,给程序分发带来很多障碍,为了解除这种障碍,同时也为了引明RMI程序的模拟实现机制。使程序员在开发基于分布式应用的三层架构的程序中更得心用手,特为B/S模式设计和C/S模式设计引入了实现这种架构的具体程序框架与实例代码。该篇讲的是C/S模式下架构的实现,B/S模式下架构的实现请参看下一期的相应。
二、Socket与多线程
1.Socket Socket接口介于应用程序与硬件之间,并可以提供标准函数以符合不同的网络硬件规格。对Socket的理解可以简化为:它是封装了数据流(Stream)的从机器到机器的一条软接线,通过这条软接线,并借助于线两端的收发程序,网络上的机器间实现了信息的交流与互通。分离在软线两端的应用程序(服务器端程序和客户端程序)可以通过调用Socket接口来开发具有TCP/IP网络功能的程序应用。所以,Socket接口的介入使得开发分布式应用程序变得更为简单。 如果把机器比作房间,那么每个在房间里创建了Socket(套接字)的程序都是这个房间的一个后门。只要了解了该Socket接收数据的协议(后门的钥匙),就可以从房间外轻松地打开房间的门而进入该房间。因特网的极度膨胀和自发扩张使得程序的分发不再受时间和地域的限制。为了在网络上部署安全的信息服务,我们有必要在服务器端控制来自客户的对信息的访问行为。Socket正是迎合这种需求而被广泛应用的一种网络信息访问技术。 Java提供了包java.net支持Socket编程,在应用程序中引入该包。代码如下: import java.net.*; 服务器端要做的就是为提供服务的程序(后门)固定一个位置,便于进入房间的人能够找到,这个位置实际就是一个数字,Socket中叫端口。端口从1025开始(其他的已经被系统作为保留端口使用),代码如下: ServerSocket serverListener=new ServerSocket(1234); 需要注意的是该例程的任务只是用来负责接收监听,也就是记录访问该后门的所有客户,真正与客户交流的应该是另外一个例程: Socket server=serverListener.accept(); 作为客户端,即要知道服务程序所在房间的房间号(IP地址),又要知道后门的位置(端口)。这样才能正确地进入房间。正如你把机票订错了一样,飞机把你带到了一个陌生的国度,你不懂他们说的语言,他们也不懂你说的语言,正常的交流怎么能进行呢?客户端的代码如下: Socket client =new Socket(InetAddress.getByName(“25.100.0.1”),1234); 类InetAddress的静态方法getByName()取得特定的Java对象,该对象包含了服务程序所在机器的有效IP地址(或DNS域名),最关键的是该对象是创建客户端Socket所必须的。通过这样一个对接过程,Socket server和client就连通了。 2.数据编发 软线建立完成后,还必须有数据传输的规则,这就是服务端和客户端商量着来的事情了。数据使用什么封装,传送时遵循什么规则(收发协议)、传输是否采用压缩技术和加解密技术等。将数据封装起来,可以让程序的设计者像操作本地文件一样来操作流动在网络上的信息。不同的数据封装方式将决定客户端和服务器端数据的访问方式。这就是为什么我们使用RMI生成代码存根时必须指定版本号的原因了。 rmic –v1.1 AddTwoNumberServer 在1.1版本中, 使用DataInputStream和DataOutputStream封装数据,因此编发类型仅限于标量(基本型别)和字符串对象,而1.2中,使用ObjectInputStream和ObjectOutputStream封装数据,其编发类型扩展到所有实现了java.io.Serializable的对象。 鉴于考虑对低版本的支持,一并演示对象在网络上的串行化机制,这里列出了本例中使用的数据封装代码: DataOutputStream out=new DataOutputStream(server.getOutputStream()); DataInputStream in=new DataInputStream(server.getInputStream()); 我们可以使用同样的代码来封装客户端Socket数据流,这里就不列出了。 在网络上使用DataOutputStream和DataInputStream封装数据流并不是特别方便,需要我们考虑有关对象到字节的串行化以及字节流到对象的还原问题。以下是两种操作的代码及简单说明: (1)从对象到字节流: //首先将对象转为字节流 ByteArrayOutputStream buf=new ByteArrayOutputStream(); ObjectOutputStream oos=new ObjectOutputStream(buf); oos.writeObject(obj); byte[] bytes=buf.toByteArray(); int l=bytes.length; //写对象长度到DataOutputStream out.writeInt(l); //写对象字节流到DataOutputStream out.write(buf.toByteArray()); out.flush();
(2)从字节流到对象:
//从DataInputStream中读入对象的长度 int l=in.readInt(); //从DataInputStream中读入对象字节 byte[] bytes=new byte[l]; in.readFully(bytes); //返回对象本身 ByteArrayInputStream buf=new ByteArrayInputStream(bytes); ObjectInputStream ois=new ObjectInputStream(buf); return ois.readObject(); 3.多线程 作为一种例程服务,必须要考虑其同时服务于多用户的问题。这个问题的解决可以通过Java线程技术来实现,而且相当简单。基本思路是:在服务器内产生单一的监听Socket,并循环调用accept()等待新的软线连接。Accept()每返回一次,就会带回与特定客户有关的Socket,将该Socket的运作安排到一个独立的线程中,这样其服务就是针对特定客户的了。为了维持与客户的通信状态,在程序中引入了一个类专门存放客户session,并将所有用户的session放到了哈希表Hashtable中。具体代码及说明请参照TunnelServer中的startServer()方法。 考虑到多线程对共享资源访问时的同步问题如果处理不当就会造成线程的不安全,必须为临界区设置线程锁。由于多个用户会同时访问哈希表并更改其中内容,所以,使用关键字Synchronized来加锁访问共享信息的区段(即临界区)。主要代码如下: synchronized (ht){ count++; Object serverBusObject=getRealObject(); cs=new ClientSession(count,clientSocket,serverBusObject); ht.put(Integer.toString(count),cs); }
三、架构实现
这部分将为大家创建一个分布式的业务逻辑实现的例子:编写一个程序,用户可以在客户端通过网络直接浏览到服务器的磁盘文件结构。
1. 架构组成类的结构图 架构组成类的结构如图1所示:

图1 架构类示意图
2.基于TCP的收发协议 使用Socket编程,必须明确客户端与服务器端的收发协议。以下是针对该实例定义的收发协议: (1) 初始化(9999) 客户端:发送报头9999,发送用户名(String),发送密码(String) 服务端:接收报头9999,接收用户名和密码,验证登录权限 是合法用户:发送报头9999,发送欢迎信息(String) 是非法用户:发送报头-1,发送警告信息(UTF) 客户端:接收报头9999,获取欢迎信息(String)
(2) 获取文件名和目录名(1) 客户端:发送报头1,发送路径(String) 服务端:接收报头1,执行获取操作 发送报头1,发送结果(FileAndDirectory) 客户端:接收报头1,接收结果(FileAndDirectory),处理结果。
(3) 其他业务逻辑方法(n) 客户端:发送报头n,发送参数 服务端:接收报头n,执行相应操作 发送报头n,发送结果 客户端:接收报头n,接收结果,处理结果。
备注:如果客户端返回报头-1,则表示出错,紧跟着的就是一个UTF携带相应的出错信息。
3.架构的代码实现
第一步:编写业务逻辑的接口BusInterface.java。 该接口必须同时部署到客户端和服务器端。在服务器端,类BusObject实现该接口,并且该类是业务逻辑真正意义上的实现。接口中定义的所有业务操作都在该类被一一实现。 在客户端,类BusObjectProxy实现该接口,以便在客户端模拟服务器端的BusObject类,不过所有的业务逻辑方法只是实现了一个网络连接,并没有从真正意义上实现BusObject,所以称BusObjectProxy是服务器端BusObject类的一个代理。 业务逻辑在服务器端被具体实现、在客户端只做代理的这种特性称胖服务器或瘦客户端程序设计模式。定义接口不仅可以明确约束所有实现该接口的类的协议,而且由于其本身并不实现具体细节,因此程序设计时便于阅读和参考。实现的核心代码如下:
import java.util.Vector;
public interface BusInterface{ /** *获取指定路径下的所有文件,传入参数举例:path="c:\aa" *返回一个FileAndDirectory对象,包含两个Vector,一个是所有的文件名集合,一个是所有的目录名集合 */ public FileAndDirectory getAllFileName(String path);
/** *仿照上面的定义,你可以实现程序中所有其他可用的业务逻辑 * */ }
第二步:编写可以被网络串行化的助益类Helper Class(服务器)。 业务逻辑方法的入口参数、出口参数很可能因为比较复杂而被单独封装为一个类,像上面定义接口中的方法getAllFileName,其出口参数就是一个助益类FileAndDirectory。因为该参数会被编发并从服务器端传送到客户端,因此,该类必须可以被串行化Serializable。实现的核心代码如下: import java.io.*; import java.util.*;
public class FileAndDirectory implements Serializable{ Vector fileArray; //文件名集合 Vector directoryArray; //子文件夹集合
public FileAndDirectory(Vector files,Vector dirs){ fileArray=files; directoryArray=dirs; } public Vector getFileArray(){ return fileArray; } public void setFileArray(Vector files){ fileArray=files; } public Vector getDirectoryArray(){ return directoryArray; } public void setDirectoryArray(Vector dirs){ directoryArray=dirs; } }
第三步:编写业务逻辑实现类BusObject.java(服务器)。 该类实现了BusInterface接口,之所以设计使服务器端类BusObject和客户端代理BusObjectProxy类都实现同一个接口,主要目的就是使客户端程序调用服务器业务逻辑方法时能够透明化。对设计客户端的程序员来讲,它操作的就是服务器类本身,而屏蔽了代理与主类间网络层复杂的连接代码。简化客户端程序设计,实现的核心代码如下: import java.io.*; import java.util.Vector; import java.lang.*;
/** *商业对象(在服务器端) *实现接口BusInterface中定义的所有业务逻辑方法 */ public class BusObject implements BusInterface{
/** *获取指定PATH的所有文件名称和文件夹名称 */ public FileAndDirectory getAllFileName(String path){ Vector files=new Vector(); Vector dirs=new Vector(); FileAndDirectory fd;
//获取path中所有的文件名和目录名 File pathFile=new File(path); String[] list; list=pathFile.list();
for(int i=0;i<list.length;i++){ String strTmp=list[i]; strTmp=path+"\\"+strTmp; File fileTmp=new File(strTmp); if(fileTmp.isFile()){ files.addElement(list[i]); } else{ dirs.addElement(list[i]); } } fd=new FileAndDirectory(files,dirs); return fd; } //将单元测试程序放到该单元下是一个好习惯!! public static void main(String[] args){ BusObject bo=new BusObject();
//单元测试:获取指定目录下的所有文件名和文件夹名 String path="d:\\s"; FileAndDirectory fd=bo.getAllFileName(path); Vector v=new Vector(); v=fd.getFileArray(); System.out.println("输出所有的文件名:"); for(int i=0;i<v.size();i++){ System.out.println(v.elementAt(i)); }
v=fd.getDirectoryArray(); System.out.println("输出所有的目录名:"); for(int i=0;i<v.size();i++){ System.out.println(v.elementAt(i)); } } }
第四步:编写商业对象代理的网络层TunnelClient.java(客户端)
由于商业对象代理一边要负责假装实现商业对象的业务逻辑(所谓假装就是并不真正实现,只是做了以下两个操作:接收入口参数,将入口参数串行化成字节流传送出去;把网络上接收回的信息解释为从业务逻辑方法中运行返回的结果,对客户端程序员而言,他并看不到内部的这个工作),另一边又要实现对数据的网络封装、传送和接收。为了实现程序网络层与业务逻辑层的分离,我们使用了继承机制。把封装了网络功能的代码单独作为一个类TunnelClient。,把公用的东西和非公用的东西分开处理是一个很好的习惯。 其实该代理的网络层代码对服务器只是简单地做了数据的封装和网络协议的简单制定,对客户端程序员则透明地提供了远程方法调用。由于最终所有的参数都要转换成字节流进行传输,因此需要涉及到对象流化的一些方法,比如如何将一个实现了串化的对象构造为字节流,又如何还原。这些技巧在代码中都有体现。实现的核心代码如下: import java.io.*; import java.net.*;
/** *抽象类,是客户端代理的网络层实现,主要包含初始化方法: *方法:initialize()用来进行初始化。如果登录成功,则返回服务器的欢迎信息。 * 客户端程序可以通过getWelcome方法获取它。 */
public abstract class TunnelClient{ Socket socket; String host="127.0.0.1"; int port=7484;
DataOutputStream out; DataInputStream in;
private String userName; //用户名 private String userPass; //密码 private String welcome; //如果登录成功,则返回服务器的欢迎信息
/** *写入报头,9999表示初始化。其后紧跟着用户名和密码 *数据报协议: *服务器接收到flag=9999,则验证用户名和密码,如果正确,返回报头9999表示连接成功 *其后跟随服务器的欢迎信息 *否则返回-1表示连接失败,紧跟着字符串说明失败的原因 */ public void initialize() throws TunnelException{ //首先与服务器建立连接 try{ connectServer(host,port); } catch(Exception ex){ ex.printStackTrace(); } try{ //写报头 writeHeader(9999); //传入入口参数 writeObject(userName); writeObject(userPass); //读报头 readHeader(); //读出口参数 welcome=(String)readObject(); } catch(Exception ex){ ex.printStackTrace(); } }
//向输出流写入报头 public void writeHeader(int flag) throws TunnelException{ try{ out.writeInt(flag); out.flush(); }catch(IOException ex){ ex.printStackTrace(); throw new TunnelException(ex.getMessage()); } }
//读取报头,如果报头为-1则报错 public int readHeader() throws TunnelException{ int f=-1; try{ //获取从服务器返回的信息 //先读报头信息 f=in.readInt(); //如果出现错误,则返回 if(f==-1){ String msg=in.readUTF(); throw new TunnelException(msg); } }catch(IOException ex){ f=-1; ex.printStackTrace(); throw new TunnelException(ex.getMessage()); } return f; } //连接到服务器,并获取输入流和输出流 public void connectServer(String host,int port) throws Exception{ try{ socket=new Socket(InetAddress.getByName(host),port); //与指定服务器建立TCP Socket out=new DataOutputStream(socket.getOutputStream()); in=new DataInputStream(socket.getInputStream()); }catch(IOException ex){} } /** * 将对象写入字节流,先写入一个整型数,表示对象的长度,然后再写对象本身 * 调用该函数前应该先创建DataOutputStream对象out */ public void writeObject(Object obj) throws Exception{ try{ //首先将对象转为字节流 ByteArrayOutputStream buf=new ByteArrayOutputStream(); ObjectOutputStream oos=new ObjectOutputStream(buf); oos.writeObject(obj); byte[] bytes=buf.toByteArray(); int l=bytes.length; //写入对象长度 out.writeInt(l); //写入对象字节流 out.write(buf.toByteArray()); out.flush(); } catch(Exception ex){ ex.printStackTrace(); } } /** * 从输入流中读取一个对象,先读出该对象占用字节数,然后读对象本身 * 调用该函数前应该存在DataInputStream对象in */ public Object readObject() throws Exception{ try{ //读入对象的长度 int l=in.readInt(); //构造对象字节流数组 byte[] bytes=new byte[l]; in.readFully(bytes); //返回对象本身 ByteArrayInputStream buf=new ByteArrayInputStream(bytes); ObjectInputStream ois=new ObjectInputStream(buf); return ois.readObject(); } catch(Exception ex){ ex.printStackTrace(); return null; } }
public String getWelcome(){ return welcome; } public String getUserName(){ return userName; } public void setUserName(String name){ userName=name; } public String getUserPass(){ return userPass; } public void setUserPass(String pass){ userPass=pass; } }
第五步:定义自己的网络异常类TunnelException.java(客户端和服务器端共用)。 该类并没有做过多处理,只是简单地调用了Exception父类的构造方法。代码如下: import java.io.*;
/** *自定义异常类 */ public class TunnelException extends Exception{ public TunnelException(String message){ super(message); } }
第六步:编写商业对象代理类BusObjectProxy.java,实际也是网络数据流的解调制类(客户端)。 该类的构造函数就是初始化:包括接收客户端的用户名、密码登录信息,然后与服务器建立连接,并向服务器发送9999报头和用户名、密码,请求服务器通过验证。其他的方法就是实现接口BusInterface中定义的所有业务逻辑。当然这个实现只是象征性地,实现的代码如下: import java.io.*; import java.util.Vector;
/** * *服务器主类的客户端代理 */ public class BusObjectProxy extends TunnelClient implements BusInterface{ /** *调用TunnelClient的初始化函数,建立连接,并向服务器发送9999报头 *提供客户端用户名和密码进行登录请求。 */ public BusObjectProxy(String name,String pass) throws TunnelException,IOException{ setUserName(name); setUserPass(pass); initialize(); } /** *实现接口中定义的方法 *代理负责将入口参数打包发送到服务器 *并返回从服务器获得的结果 *方法1-- 功能:获取指定目录的所有文件名和目录名 * 编号:1 * 入口参数:路径名,例如"d:\\s" * 返回值:对象FileAndDirectory */ public FileAndDirectory getAllFileName(String path){ FileAndDirectory fd=null; try{ writeHeader(1); //向字节流中写入入口参数 writeObject(new String(path)); //读报头 readHeader(); //从输入流中获取执行的结果 fd=(FileAndDirectory)readObject(); } catch(Exception ex){ ex.printStackTrace(); fd=null; } return fd; }
}
第七步:编写服务器端负责调制的类的网络层封装部分TunnelServer.java(服务器)。 负责服务器端口监听的创建,信息在网络上的分发、接收,服务端界面创建,客户连接SESSION管理等,这段代码比较复杂,使用的Java程序设计技巧也多。比如抽象类定义、内隐类定义等,实现的代码如下: import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.net.*; import java.io.*; import java.util.*;
/** *服务器端负责处理网络数据报的调制类 * *参考类: Talk ClientSession */
public abstract class TunnelServer extends JFrame implements Runnable{ private static ServerSocket serverSocket; //服务器端监听socket private static Socket clientSocket; //与客户连接的句柄 private static ClientSession cs; //客户的SESSION private static Hashtable ht=new Hashtable(); //存放所有客户连接(“连接顺号-客户SESSION”)
private DataInputStream in; private DataOutputStream out; private int port=7484;
int count=0; //连接总数 public static JTextArea ta=new JTextArea(12,34); JButton btn=new JButton("启动");
public void startServer(int port) throws Exception{ try{ //建立服务器监听Socket serverSocket=new ServerSocket(port); ta.append("Server Listener Created at port "+port+"......\n"); }catch(IOException ex){} //启动客户连接侦听器 Thread thread=new Thread(this); thread.start(); } public void run(){ try{ //每隔500毫秒检测一次 Thread.sleep(500); //实现客户端循环监听 while(true){ //获取到一个客户连接 clientSocket=serverSocket.accept(); //指定数据流封装方式 in=new DataInputStream(clientSocket.getInputStream()); out=new DataOutputStream(clientSocket.getOutputStream()); //将该客户连接保存到哈希表中 // 主要有:商业业务逻辑对象,与客户相关的Socket,编号 synchronized (ht){ count++; Object serverBusObject=getRealObject(); cs=new ClientSession(count,clientSocket,serverBusObject); ht.put(Integer.toString(count),cs); } ta.append("Connection from "+ clientSocket.getInetAddress().getHostAddress()+"\n"); String lineSep=System.getProperty("line.separator"); //取得回车换行符 InetAddress addr=serverSocket.getInetAddress().getLocalHost(); String outData="编号为"+count+ "的访问者:欢迎您登陆EAC服务器。 服务器地址:"+ addr+ " 当前服务的端口号为:"+serverSocket.getLocalPort();
//根据收发协议的规定,首先进行身份验证
//初始化的第一个报头一定是9999,所以没有保存该报头的信息。 int flag=readHeader(); ta.append("::> request header:"+flag+"\n"); //从客户端获取用户名和密码 String userName=(String)readObject(); String userPass=(String)readObject(); ta.append("::> received from client"+count+" name="+userName); ta.append(" and password="+userPass+"\n");
//系统初始化,接收到9999报头,模拟系统身份验证 if(correctLogin(userName,userPass)){ ta.append("::> access is legal,client "+count+ ",you are welcome!"+"\n"); writeHeader(9999); writeObject(outData); //管理每一个客户的完整交互过程, //注意:客户的每个完整交互过程是在一个新线程中完成的。 new Talk(count,ht,ta){ public void executeMethod(Object busObject, int flag,DataInputStream in,DataOutputStream out) throws Exception{ executeMethods(busObject,flag,in,out); } }.start(); } else{ ta.append("::> guest logging in declined\n"); writeHeader(-1); out.writeUTF("你无权登录网络,请联系系统管理员"); } } }catch(Exception ex){} } /** * 将对象写入字节流,先写入一个整型数,表示对象的长度,然后再写对象本身 * 调用该函数前应该先创建DataOutputStream对象out */ public void writeObject(Object obj) throws Exception{ try{ //首先将对象转为字节流 ByteArrayOutputStream buf=new ByteArrayOutputStream(); ObjectOutputStream oos=new ObjectOutputStream(buf); oos.writeObject(obj); byte[] bytes=buf.toByteArray(); int l=bytes.length; //写入对象长度 out.writeInt(l); //写入对象字节流 out.write(buf.toByteArray()); out.flush(); } catch(Exception ex){ ex.printStackTrace(); } } /** * 从输入流中读取一个对象,先读出该对象占用字节数,然后读对象本身 * 调用该函数前应该存在DataInputStream对象in */ public Object readObject() throws Exception{ try{ //读入对象的长度 int l=in.readInt(); //构造对象字节流数组 byte[] bytes=new byte[l]; in.readFully(bytes); //返回对象本身 ByteArrayInputStream buf=new ByteArrayInputStream(bytes); ObjectInputStream ois=new ObjectInputStream(buf); return ois.readObject(); } catch(Exception ex){ ex.printStackTrace(); return null; } } //向输出流写入报头 public void writeHeader(int flag) throws TunnelException{ try{ out.writeInt(flag); out.flush(); }catch(IOException ex){ ex.printStackTrace(); throw new TunnelException(ex.getMessage()); } }
//读取报头 public int readHeader() throws TunnelException{ int f=-1; try{ //获取从服务器返回的信息 //先读报头信息 f=in.readInt(); }catch(IOException ex){ f=-1; ex.printStackTrace(); throw new TunnelException(ex.getMessage()); } return f; } //模拟系统登录过程,在这里可以使用数据库 private boolean correctLogin(String name,String pass){ if(name.equals("qixiaorui") && pass.equals("20000203")){ return true; } else{ return false; } } public int getPort(){ return port; } public void setPort(int port){ this.port=port; } /** *获取商业对象实例 */ public abstract Object getRealObject() throws TunnelException; /** * 执行flag指定的远程方法,详细内容在收发协议里有规定 */ public abstract void executeMethods(Object busObject, int flag,DataInputStream in,DataOutputStream out) throws Exception; } /** *用户交互监听处理类 *负责处理用户的完整交互过程 */ abstract class Talk extends Thread implements Runnable{ private int count; private Hashtable ht; private JTextArea ta; public Talk(int count,Hashtable ht,JTextArea ta){ this.count=count; this.ht=ht; this.ta=ta; } public void run(){ try{ while(true){ sleep(1000); String response=""; Socket clientSock= ((ClientSession)ht.get(Integer.toString(count))).getClientSock(); Object serverBusObject= ((ClientSession)ht.get(Integer.toString(count))).getBusObject();
DataInputStream in= new DataInputStream(clientSock.getInputStream()); DataOutputStream out= new DataOutputStream(clientSock.getOutputStream()); //读取报头 int flag=in.readInt(); ta.append("client "+count+ ":> execute remote method header="+flag+"\n"); //执行报头协议里定义的本地方法 executeMethod(serverBusObject,flag,in,out); } }catch(Exception ex){} } public abstract void executeMethod(Object busObject,int flag, DataInputStream in,DataOutputStream out) throws Exception; } /** * 用户SESSION类,记录用户的交互信息 * 包含:服务器为其分配的编号、该客户与服务器连接的句柄 * 和为该客户连接的商业对象 */ class ClientSession implements Serializable{ int count; Socket clientSock; Object bo;
public ClientSession(int count,Socket clientSock,Object bo){ this.count=count; this.clientSock=clientSock; this.bo=bo; } public int getCount(){ return count; } public void setCount(int count){ this.count=count; } public Socket getClientSock(){ return clientSock; } public void setClientSock(Socket sock){ this.clientSock=sock; } public Object getBusObject(){ return bo; } public void setBusObject(Object bo){ this.bo=bo; }
}
第八步:编写服务器调制类BusServer.java(服务器)。 通过继承TunnelServer类,实现了调制的网络层抽象类定义的几个方法。这样做的主要目的是使程序结构简单明了,程序设计者可以集中力量进行业务逻辑的描述与实现,而无须关心数据在网络上被如何封装、如何分发。 该类主要包含构造函数(构造服务器端监听界面)、获取本地商业对象的实例以及根据不同报头执行不同的本地商业对象的相应方法。另外,它也是服务器端的主运行类,具有main方法,实现的代码如下: /** *远程方法调用的服务器端,运行后启动监听监听 *参考类:TunnelServer MyWindowListener */
public class BusServer extends TunnelServer{ /** *构造函数,构造服务器端监听界面 */ public BusServer(int port){ setPort(port);
setTitle("服务器窗口"); Container c=getContentPane(); c.setLayout(new FlowLayout()); addWindowListener(new MyWindowListener()); btn.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ try{ startServer(getPort()); btn.setEnabled(false); } catch(Exception ex){} } });
c.add(btn); c.add(new JScrollPane(ta)); addWindowListener(new MyWindowListener()); setSize(new Dimension(400,300)); show(); } /** *返回商业对象实例 */ public Object getRealObject() throws TunnelException{ return new BusObject(); } /** *根据报头的不同执行不同的本地方法调用 */ public void executeMethods(Object busObject, int flag, DataInputStream in,DataOutputStream out) throws Exception{ BusObject bus=(BusObject) busObject; switch(flag){ case 1: //从服务器获取指定路径的所有文件名和子文件夹名 String path=(String)readObject(); FileAndDirectory ds=bus.getAllFileName(path); out.writeInt(flag); writeObject(ds); break; //在此添加其他方法的处理 // case 9998: // 取入口参数,执行本地方法,返回结果 // break; } } public static void main(String[] args) throws Exception{ BusServer ss=new BusServer(7484); } }
第九步:构造窗口关闭监听器MyWindowListener.java(服务器端)
作为一个助益类,该监听器功能非常简单,可以保证窗口被正确关闭。 代码如下: import java.awt.*; import java.awt.event.*; import javax.swing.*;
/** *窗口关闭用的监听器 */
public class MyWindowListener extends WindowAdapter{ //窗口关闭方法 public void windowClosing(WindowEvent e){ System.exit(1); } } 4.该框架中存在的缺点:未考虑信息在传递过程中的加密与解密;为对传递信息进行压缩传输;串行化对象大小受整型变量大小限制;未设置对SOKET的监听,连接断开后系统存在内存漏洞。 四、应用测试
1.编写客户端主运行程序
到现在为止,程序设计者已经可以集中力量编写客户端主运行程序了,入口参数、出口参数的网络传输已经被完整而准确地封装到了相关的类中,实现的核心代码如下: /** *远程方法调用的客户端,首先要提供用户名和密码登录,默认为:qixiaorui ,20000203 *然后在路径文本框中输入要查看的服务器目录,如:c:\\windows,单击获取按钮 *参考类:BusObjectProxy MyWindowListener */
public class BusClient extends JFrame{ JTextArea ta=new JTextArea(10,33); JLabel lbl1=new JLabel("用户名:"), lbl2=new JLabel("密码:"), lbl3=new JLabel("指定路径:"); JTextField tf1=new JTextField(8); JPasswordField tf2=new JPasswordField(8); JTextField tf3=new JTextField(19); JButton btn1=new JButton("获取"); JButton btn=new JButton("连接"); BusObjectProxy bus; String welcome=""; public BusClient(){ super("客户端窗口"); Container c=getContentPane(); c.setLayout(new FlowLayout(FlowLayout.LEFT)); c.add(lbl1);c.add(tf1); c.add(lbl2);c.add(tf2); c.add(btn); c.add(lbl3);c.add(tf3);c.add(btn1); c.add(new JScrollPane(ta));
//获取指定目录的所有文件名 btn1.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ try{ String path=tf3.getText(); ta.append("单元测试:获取服务器指定目录"+path+ "下的所有文件名和文件夹名"+"\n"); FileAndDirectory fd=bus.getAllFileName(path); Vector v=new Vector(); v=fd.getFileArray(); ta.append("输出所有的文件名:\n"); for(int i=0;i<v.size();i++){ ta.append(v.elementAt(i)+"\n"); }
v=fd.getDirectoryArray(); ta.append("输出所有的目录名:\n"); for(int i=0;i<v.size();i++){ ta.append(v.elementAt(i)+"\n"); } }catch(Exception ex){} } }); //登录 btn.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ try{ String name=tf1.getText(); String pass=new String(tf2.getPassword()); bus=new BusObjectProxy(name,pass); welcome=bus.getWelcome(); ta.append(welcome+"\n"); }catch(Exception ex){} } }); addWindowListener(new MyWindowListener()); setSize(new Dimension(400,300)); show(); } public static void main(String[] args){ BusClient bc=new BusClient(); } }
2. 运行效果 客户端的运行效果如图2所示,服务器端的运行效果如图3所示。

图2客户端窗口

图3服务器端窗口 (1)运行服务器端程序。命令:java BusServer (2)启动服务端口 (3)运行客户端程序。命令:java BusClient (4)首先输入错误的密码,则服务器拒绝用户登录,然后输入了正确的密码,并获取到服务器C:\\WINDOWS目录中的所有内容显示出来。 对该例子进行简单扩展后我们可以完成一个远程资源管理器的程序
五、部署描述
1.服务器端: Helper类:FileAndDirectory.class,MyWindowListener.class 架构类: BusInterface.class,BusObject.class,TunnelServer.class, Talk.class,ClientSession.class,TunnelException.class 主运行类:BusServer.class
2.客户端: Helper类:FileAndDirectory.class,MyWindowListener.class 架构类: BusInterface.class,BusObjectProxy.class,TunnelClient.class 主运行类:BusClient.class
|