紧接C/S模式中的远程方法调用专题,接下来将设计一个基于B/S模式的远程方法调用框架。其实原理和前面讲到的一样,框架结构也基本一致。所不同的是在客户端,一个运行在独立进程中,借助于跟服务器建立的Socket连接进行通信;一个运行在浏览器中,被浏览器所解释。并借助于建立在HTTP协议本身通信通道基础上的HTTP隧道(TUNNELING)与服务器端类进行通信。
一、HTTP隧道、SESSION和本地安全访问
1.HTTP通道与HTTP隧道
HTTP(HyperText Transfer Protocol-超文本传输协议)是一种Internet客户机/服务器协议,它借助于客户端的浏览器通过HTTP收发协议与服务器端提供WWW服务的进程进行通信,其核心就是一种C/S的应用,只不过软线(通道)的建立过程不再需要额外考虑。在设计时需要重点考虑的是:如何利用浏览器和WWW服务例程已经建好的这个通道为远程方法调用服务。通俗地讲就是路已经修好了,现在的主要任务是商量在这路上跑什么车,如何跑?规则是什么,所以要求在这个通道的基础上再添加一种通信子协议(RMI协议),受这个子协议约束的客户/服务器程序将运行在通道的一个单独的空间,这个空间称为隧道(tunneling)。HTTP通道与HTTP隧道的关系如图1所示。
图1HTTP通道与隧道关系图
简单地讲,HTTP协议负责传送数据,我们的主要任务就是在这个协议上设定另外一个子协议,实现RMI。设计前必须要明确的一个概念就是在这个框架中,哪些类型的程序可以担当服务器端角色,哪些可以担当客户端角色。既然将RMI定位到了B/S模式中,要求客户端程序不能脱离开浏览器运行。而Java中的Applet正是符合这个要求的最好的客户端角色,其解释执行由JRE(Java Runtime Enviorment)来负责;担当服务器端角色的就是SERVLET了,它运行在服务器的包容器里,由包容器接管来自HTTP通道的信息解释后传送给SERVLET。其实,Java从一开始就是设计服务器端应用程序的最佳语言选择。
目前好多机器安装的防火墙都限制了远程程序对本地端口的访问,借助于HTTP协议,通过B/S模式的RMI框架,可以透过防火墙,这点对访问受限制的用户来讲是非常有用的。这也是为什么选择在HTTP协议上构建隧道的一个很重要的原因。
2.数据流编发
对子协议的定义体现在数据流的封装上。HTTP协议中要求数据以DataInputStream和DataOutputStream来进行编发,而RMI协议使用的封装数据的流为ObjectInputStream和ObjectOutputStream,由于最终的数据发送由通道来完成,所以数据编发时要求必须完成从ObjectStream到DataStream的转换,以下是转换用的主要代码和思路:
(1)客户端
客户程序与服务器端servlet建立连接的代码如下:
URL url=new
URL("http://127.0.0.1:8080/rmibs/BusServer;jsessionid="+jsessionid);
URLConnection con=url.openConnection();
其中,127.0.0.1是服务器IP地址,8080是包容器运行的端口,rmibs是包容器中webapp中的一个目录,BusServer是服务器的servlet服务程序,注意,它是一个经过了映射的虚拟名字,其全名应该是:com.ql.rmibs.BusServer,jsessionid是客户端通过收发协议获得的这次通信的SessionID。用以维持用户到服务器的这次对话。
从HTTP通道获取数据流,并用ObjectInputStream封装,进而形成自己的通信隧道。代码如下:
ObjectInputStream in=new ObjectInputStream(con.getInputStream());
如果要往服务器发送信息,采用如下的方法:
//首先,将要发送的信息按照ObjectOutputStream(ByteArrayOutputStream)的方式封装进行串化
ByteArrayOutputStream buffer=new ByteArrayOutputStream();
ObjectOutputStream out1=new ObjectOutputStream(buffer);
out1.writeObject(flag);
out1.flush();
//将串行化以后的数据转到字节流buf中
byte[] buf= buffer.toByteArray()
//设置连接属性
con.setUseCaches(false);
con.setDoOutput(true);
con.setDoInput(true);
con.setRequestProperty("Content-type",
"application/octet-stream");
//设置传送的数据长度
con.setRequestProperty("Content-length",""+buf.length);
//取得输出流,封装为DataOutputStream,这是HTTP协议规定的数据封装方式。
DataOutputStream out=new DataOutputStream(
con.getOutputStream());
out.write(buf);
//通过通道向服务器端发送已编发好的数据
out.flush();
out.close();
(2)服务器端
服务器端包容器提供的输入和输出流可以分别从以下对象中获得:request和response,以下代码是从输入中获取一个报头:
ObjectInputStream in=new ObjectInputStream(request.getInputStream());
int flag=in.readInt();
从服务器输出到客户端,需要做以下操作:
//设置输出流的内容类型
response.setContentType("application/octet-stream");
//将对象串化为字节流
ByteArrayOutputStream byteOut=new ByteArrayOutputStream();
ObjectOutputStream out=new ObjectOutputStream(byteOut);
out.writeInt(flag);
out.flush();
byte[] buf=byteOut.toByteArray();
//将串化的对象发送到客户端
response.setContentLength(buf.length);
ServletOutputStream servletOut=res.getOutputStream();
servletOut.write(buf);
servletOut.flush();
servletOut.close();
使用ObjectInputStream和ObjectOutputStream编发数据非常方便,可以直接将实现了java.io.Serializable接口的类传递到隧道中,其串行化操作也非常简单。
3.客户SESSION管理
在B/S模式中,维持一个客户对话非常重要。通常都使用HTTPServlet的SESSION对象。以下是在这个框架中维持客户对话的主要代码及思路:
首先,对客户会话状态的维护:客户登录时记录下客户的SESSIONID,并在客户的连接中显式地指定该SESSIONID。主要代码如下:
URL url=new URL("http://127.0.0.1:8080/"+
"rmibs/BusServer;jsessionid="+jsessionid);
其次,对与客户绑定的商业对象的维护:客户登录后将同时实例化一个商业对象,服务器端维护一个HASHTABLE,存放所有客户的商业对象,每个商业对象由该对象创建时的hashCode来索引。主要代码如下:
//以下是建立一个Hashtable:
HttpSession session=req.getSession(true);
Hashtable busobjectTable=(Hashtable)
session.getAttribute(HASHTABLENAME);
if (busobjectTable==null)
{
busobjectTable=new java.util.Hashtable();
session.setAttribute(HASHTABLENAME,busobjectTable);
}
以下是从Hashtable中获取商业对象实例:
objectCode=in.readInt(); //获取商业对象编号
serverbusObject=busobjectTable.get(new Integer(objectCode)); //从哈希表获取商业对象
4.本地安全访问
基于安全考虑,作为客户端小应用程序的Applet并不支持本地访问方法(如读写客户端的本地文件),为了能够使其具有本地访问的权利,必须对Applet进行签名认证。由客户端用户决定是否启用经过了签名的小应用程序。
(1)生成密钥库文件ql.store,命令如下(注意,斜体部分可以自己命名)
keytool –genkey –keystore ql.store –alila ql
所有的密钥在文件ql.store中,它的别名是ql,后面将用到这个别名,在进行该步操作时,需要提示用户输入开启密钥的密码,这里用20000203,注意记住这个密码,后面还要用到
(2)生成证书文件ql.cert,命令如下:
keytool –export –keystore ql.store –alias ql –file ql.cert
从密钥中导出证书文件,参数-keystore –alias分别指定上一步生成的密钥和别名,生成的证书文件在ql.cert中
(3)使用证书对要发布的jar文件签名,命令如下:
jarsigner –keystore ql.store RMIExample.jar ql
经过签名的jar文件就可以到网络上发布了。如果用户在使用时接受了签名,则该jar文件就自动被赋以本地访问权限。
二、架构实现
下面将编写一个程序,用户可以在客户端通过网络直接浏览到服务器的磁盘文件结构,并显示在Applet中。
1.基于HTTP的收发协议
使用HTTP隧道编程,也必须明确客户端applet与服务器端servlet的收发协议。以下是针对该实例定义的收发协议,与C/S版的略有不同,主要是因为C/S版和B/S版其管理SESSION的方式不同而造成的。
(1)登录验证(9999)
客户端:发送报头9999,发送用户名(String),发送密码(String)。
服务端:接收报头9999,接收用户名和密码,验证登录权限。
是合法用户:发送报头9999,发送用户SessionID(UTF)和欢迎信息(UTF)。
是非法用户:发送报头-1,发送登录错误信息(UTF)。
客户端:接收报头9999,获取欢迎信息(UTF)。
(2)初始化(0)
客户端:发送报头0。
服务端:接收报头0,实例化商业对象,将商业对象放到哈希表中(索引值为对象编号,即对象的hashCode)。
发送报头0,发送该索引值objectcode。
客户端:接收报头0,接收商业对象编号。
(3)获取文件名和目录名(1)
客户端:发送报头1,发送商业对象编号,发送路径(String)。
服务端:接收报头1,接收商业对象编号,执行获取操作。
发送报头1,发送结果(FileAndDirectory)。
客户端:接收报头1,接收结果(FileAndDirectory),处理结果。
(4)其他业务逻辑(n)
客户端:发送报头n,发送商业对象编号,发送参数。
服务端:接收报头n,接收商业对象编号,执行业务逻辑操作
发送报头n,发送结果。
客户端:接收报头n,接收结果,处理结果。
2.架构代码
所有B/S模式代码与C/S模式不同之处是:B/S中将所有的类都封装到了一个包里,即package com.ql.rmibs。
第一步:编写业务逻辑接口BusInterface.java。
(同C/S)
第二步:编写可以被网络串行化的助益类Helper Class(服务器)。
(同C/S)
第三步:编写业务逻辑实现类BusObject.java(服务器)。
(同C/S)
第四步:编写商业对象代理的网络层TunnelClient.java(客户端)。
由于商业对象代理一边要负责假装实现商业对象的业务逻辑(所谓假装就是并不真正实现,只是做了以下两个操作:接收入口参数,将入口参数串行化成字节流传送出去;把网络上接收回的信息解释为从业务逻辑方法中运行返回的结果,对客户端程序员而言,并看不到内部的这个工作),另一边又要实现对数据的网络封装、传送和接收。为了实现程序网络层与业务逻辑层的分离,这里使用了继承机制。把封装了网络功能的代码单独作为一个类TunnelClient。把公用的东西和非公用的东西分开处理是一个很好的习惯。
其实该代理的网络层代码对服务器只是简单地做了数据的封装和网络协议的简单制定,对客户端程序员则透明地提供了远程方法调用。通过使用流嵌套方式完成对发送数据的串行化工作。
初始化方法initialize()主要包括两个操作:与服务器端进行9999协议握手,服务器验证用户身份,并返回该次服务的SESSIONID;与服务器端进行0协议握手,服务器为客户产生商业对象实例,并返回可以索引该实例的对象编号。为客户绑定好商业对象后,客户就可以进行商业流程操作了。报头处理方法inputHeader()主要完成了对数据的简单封装(在流中添加报头信息,如果是非初始化握手协议,则同时添加对象编号),数据封装结构如图2所示:
图2 数据封装图
数据发送方法executeMethod()也完成了两项工作:第一,将构造好的数据流通过HTTP连接发送到服务器端;第二,从返回的结果流中将报头过滤掉(拆分数据包),实现的核心代码如下:
package com.ql.rmibs;
import java.io.*;
import java.net.*;
/**
* 抽象类:封装了三个主要方法:
* 方法1:initialize()用来进行初始化,
* 主要是让服务器端初始化要调用的对象,并返回对象的唯一标识
* 方法2:InputHeader()用来将报头信息写入要输出的流中
* 方法3:executeMethod()把指定的字节数组发送给服务器端的接受servlet
* 主要任务:
* 远程初始化服务器对象并取得服务器上创建的商业对象编号
* 写入本地要执行的远程服务器上的session编号、任务编号和入口参数
*/
public abstract class TunnelClient{
int objectcode; //对象编号
String jsessionid; //sessionID
private String userName; //用户名
private String userPass; //密码
private String welcome; //如果登录成功,则返回服务器的欢迎信息
public void initialize() throws TunnelException{
try{
ByteArrayOutputStream baos=new ByteArrayOutputStream();
//写入报头9999,传递用户名和密码
//获取该会话的sessionid,以备初始化时使用
ObjectOutputStream out=inputHeader(baos,9999);
out.writeObject(userName);
out.writeObject(userPass);
out.flush();
//先执行输出,然后获得输入in
ObjectInputStream in=executeMethod(baos.toByteArray());
//从输入流中取得执行的结果
jsessionid=in.readUTF();
welcome=in.readUTF();
in.close();
ByteArrayOutputStream buffer1=new ByteArrayOutputStream();
inputHeader(buffer1,0);
ObjectInputStream in1=executeMethod(buffer1.toByteArray());
objectcode=in1.readInt();
in1.close();
}
catch(Exception ex){
ex.printStackTrace();
}
}
/**
* 在流中加入报头信息
* 如果是初始化,则直接发送flag=0
* 否则: 发送flag=方法编号
* 发送SessionID=服务器的session编号
*/
public ObjectOutputStream inputHeader(ByteArrayOutputStream buffer,
int flag){
ObjectOutputStream out;
try{
out=new ObjectOutputStream(buffer);
out.writeInt(flag);
//如果不是初始化,紧接着写入session编号
if((flag!=0) && (flag!=9999)){
out.writeInt(objectcode);
}
out.flush();
}
catch(IOException ex){
ex.printStackTrace();
|