在分布式服务框架中,一个最基础的问题就是远程服务是怎么通讯的,在Java底层领域中有很多可实现远程通讯的技术,例如:RMI、MINA、ESB、Burlap、SOAP、EJB和JMS 等,在j2ee中,对java底层远程通讯的技术进行了封装,形成了 Hessian 、 HttpInvoker 、 XFire 、 Axis 等多种形式的远程调用技术。但对高级程序员而言仍需要掌握Java底层领域中远程通讯的技术,尤其是rmi,xml-rpc,JMS。
1.远程服务基本原理
1)底层协议
要实现网络机器间的通讯,首先得来看看计算机系统网络通信的基本原理,在底层层面去看,网络通信需要做的就是将流从一台计算机传输到另外一台计算机,基于传输协议和网络IO来实现,其中传输协议比较出名的有http、tcp、udp等等,http、tcp、udp都是在基于Socket概念上为某类应用场景而扩展出的传输协议,网络IO,主要有bio、nio、aio三种方式,所有的分布式应用通讯都基于这个原理而实现,只是为了
应用的易用,各种语言通常都会提供一些更为贴近应用易用的应用层协议。
2)应用级协议
远程服务通讯,需要达到的目标是在一台计算机发起请求,另外一台机器在接收到请求后进行相应的处理并将结果返回给请求端,这其中又会有诸如one way request、同步请求、异步请求等等请求方式,按照网络通信原理,需要实现这个需要做的就是将请求转换成流,通过传输协议传输至远端,远端计算机在接收到请求的流后进行处理,处理完毕后将结果转化为流,并通过传输协议返回给调用端。
在java领域中知名的远程通信的应用级协议有:RMI、XML-RPC、Binary-RPC、SOAP、JMS
2.RMI
2.1RMI原理
RMI,即Java RMI(Java Remote Method Invocation),Java远程方法调用.是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。
RMI是个典型的为java定制的远程通信协议,RMI全部的宗旨就是尽可能简化远程接口对象的使用。
RMI的基础是接口,RMI构架基于一个重要的原理:定义接口和定义接口的具体实现是分开的。
来看下基于RMI的一次完整的远程通信过程的原理:
1)客户端发起请求,请求转交至RMI客户端的stub类;
2)stub类将请求的接口、方法、参数等信息进行序列化;
3)基于socket将序列化后的流传输至服务器端;
4)服务器端接收到流后转发至相应的skelton类;
5)skelton类将请求的信息反序列化后调用实际的处理类;
6)处理类处理完毕后将结果返回给skelton类;
7)Skelton类将结果序列化,通过socket将流传送给客户端的stub;
8)stub在接收到流后反序列化,将反序列化后的Java Object返回给调用者。
2.2JAVA对RMI的支持
java.rmi是JAVA提供 RMI 包。RMI是一种机制,能够让在某个 Java 虚拟机上的对象调用另一个 Java 虚拟机中的对象上的方法。可以用此方法调用的任何对象必须实现该远程接口。调用这样一个对象时,其参数为 "marshalled" 并将其从本地虚拟机发送到远程虚拟机(该远程虚拟机的参数为 "unmarshalled")上。该方法终止时,将编组来自远程机的结果并将结果发送到调用方的虚拟机。如果方法调用导致抛出异常
,则该异常将指示给调用方.
Remote 接口用于标识其方法可以从非本地虚拟机上调用的接口。
2.3rmi在java中的应用
要使用RMI,必须构建四个主要的类:远程对象的本地接口、远程对象实现、RMI客户机和RMI服务器。RMI服务器生成远程对象实现的一个实例,并用一个专有的URL注册。RMI客户机在远程RMI服务器上查找服务对象,并将它转换成本地接口类型,然后像对待一个本地对象一样使用
它。
1)远程对象的本地接口
package test.rmi;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface HelloRMI extends Remote {
public String helloWorld() throws RemoteException;
public String sayHelloToSomeBody(String someBodyName) throws RemoteException;
}
2)远程对象实现
package test.rmi;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class HelloRMIImpl extends UnicastRemoteObject implements HelloRMI {
private static final long serialVersionUID = -5464145481720553926L;
public HelloRMIImpl() throws RemoteException {
}
public String helloWorld() throws RemoteException {
return "Hello World!";
}
public String sayHelloToSomeBody(String someBodyName) throws RemoteException {
return "你好," + someBodyName + "!";
}
}
3)RMI服务器
package test.rmi;
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class HelloRMIServer {
public static void main(String args[]) {
try {
//创建一个远程对象
HelloRMI rhello = new HelloRMIImpl();
//本地主机上的远程对象注册表Registry的实例,并指定端口为8888,这一步必不可少(Java默认端口是1099),必不可缺的一
步,缺少注册表创建,则无法绑定对象到远程注册表上
LocateRegistry.createRegistry(8888);
//把远程对象注册到RMI注册服务器上,并命名为RHello
//绑定的URL标准格式为:rmi://host:port/name(其中协议名可以省略,下面两种写法都是正确的)
Naming.bind("rmi://localhost:8888/RHello", rhello);
// Naming.bind("//localhost:8888/RHello",rhello);
System.out.println(">>>>>INFO:远程IHello对象绑定成功!");
} catch (RemoteException e) {
System.out.println("创建远程对象发生异常!");
e.printStackTrace();
} catch (AlreadyBoundException e) {
System.out.println("发生重复绑定对象异常!");
e.printStackTrace();
} catch (MalformedURLException e) {
System.out.println("发生URL畸形异常!");
e.printStackTrace();
}
}
}
4)RMI客户机
package test.rmi;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class HelloRMIClient {
public static void main(String args[]){
try {
//在RMI服务注册表中查找名称为RHello的对象,并调用其上的方法
HelloRMI rhello =(HelloRMI) Naming.lookup("rmi://localhost:8888/RHello");
System.out.println(rhello.helloWorld());
System.out.println(rhello.sayHelloToSomeBody("staratsky"));
} catch (NotBoundException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
总结:从上面的过程来看,RMI对服务器的IP地址和端口依赖很紧密,但是在开发的时候不知道将来的服务器IP和端口如何,但是客户端程序
依赖这个IP和端口。
这也是RMI的局限性之一。这个问题有两种解决途径:一是通过DNS来解决,二是通过封装将IP暴露到程序代码之外。
RMI的局限性之二是RMI是Java语言的远程调用,两端的程序语言必须是Java实现,对于不同语言间的通讯可以考虑用Web Service来实现。
3.XML-RPC
3.1原理
XML-RPC 网站是这样描述的:
它是允许运行在不同操作系统、不同环境中的软件进行基于 Internet 过程调用的规范和一组实现。这种远程过程调用使用 HTTP 作为传输协议,XML 作为编码格式。XML-RPC 的定义尽可能简单,但能够传送、处理和返回复杂的数据结构。
XML-RPC消息都是HTTP-POST请求。请求的主要部分的XML。服务器端执行后的返回结果同样也是XML格式。函数调用的参数可以是scalars, numbers, strings, dates等等;也可以是混合型的记录和结构体。
所以,要完成XML-RPC,需要完成3部分工作:
1)接口实现
package test.xmlrpc;
public class HelloHandler implements ServicesHandler {
public String execute(String str){
return "Hello," + str + "!";
}
}
2)远程调用
package test.xmlrpc;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Vector;
import java.net.URL;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.XmlRpcClient;
import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
public class TestClient {
public static void main(String [] args) throws Exception {
try {
// config client
XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl();
config.setServerURL(new URL("")); // should be modified according
to your configuration of jsp container
// create a new XmlRpcClient object and bind above config object with it
XmlRpcClient client = new XmlRpcClient();
client.setConfig(config);
// create parameter list
Vector params = new Vector();
params.addElement("MY");
// execute XML-RPC call
String result = (String) client.execute("HelloHandler.execute", params);
System.out.println(result);
} catch (MalformedURLException e) {
System.out.println(e.toString());
} catch (XmlRpcException e) {
System.out.println(e.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
}
3)建立一个web服务器
package test.xmlrpc;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import org.apache.xmlrpc.*;
import org.apache.xmlrpc.server.*;
import org.apache.xmlrpc.webserver.*;
public class XmlRpcServicesServlet extends HttpServlet {
private XmlRpcServletServer server;
public void init(ServletConfig pConfig) throws ServletException {
super.init(pConfig);
try {
// create a new XmlRpcServletServer object
server = new XmlRpcServletServer();
// set up handler mapping of XmlRpcServletServer object
PropertyHandlerMapping phm = new PropertyHandlerMapping();
phm.addHandler("HelloHandler", HelloHandler.class);
server.setHandlerMapping(phm);
// more config of XmlRpcServletServer object
XmlRpcServerConfigImpl serverConfig = (XmlRpcServerConfigImpl)server.getConfig();
serverConfig.setEnabledForExtensions(true);
serverConfig.setContentLengthOptional(false);
} catch (XmlRpcException e) {
try {
log("Failed to create XmlRpcServer: " + e.getMessage(), e);
} catch (Throwable ignore) {
}
throw new ServletException(e);
}
}
public void doPost(HttpServletRequest Request, HttpServletResponse Response)
throws IOException, ServletException {
server.execute(Request, Response);
}
public void doGet(HttpServletRequest Request, HttpServletResponse Response)
throws IOException, ServletException {
server.execute(Request, Response);
}
}
在web.xml中的配置
XmlRpcServer
test.xmlrpc.XmlRpcServicesServlet
XmlRpcServer
/HelloHandler
3.2RMI和RPC的区别
XML-RPC也是一种和RMI类似的远程调用的协议,它和RMI的不同之处在于它以标准的xml格式来定义请求的信息(请求的对象、方法、参数等).所以, 在RMI和RPC之间最主要的区别在于方法是如何别调用的。
在RMI中,远程接口使每个远程方法都具有方法签名。如果一个方法在服务器上执行,但是没有相匹配的签名被添加到这个远程接口上,那么这个新方法就不能被RMI客户方所调用。在RPC中,当一个请求到达RPC服务器时,这个请求就包含了一个参数集和一个文本值,通常形成“classname.methodname”的形式。这就向RPC服务器表明,被请求的方法在为“classname”的类中,名叫“methodname”。然后RPC服务器就去搜索与之相匹配的类和方法,并把它作为那种方法参数类型的输入。这里的参数类型是与RPC请求中的类型是匹配的。一旦匹配成功,这个方法就被调用了,其结果被编码后返回客户方。
3.3xml-rpc的缺点
1)XML-RPC的消息系统过于简单,没有完整意义上的消息模型
2)XML-RPC调用服务的方式要求直接指定对象和方法,称不上完整的面向服务的体系
3)XML-RPC服务器端提供的服务实际上是特定对象的某个方法,限制了服务器端的开发
4.Binary-RPC
Binary-RPC看名字就知道和XML-RPC是差不多的了,不同之处仅在于传输的标准格式由XML转为了二进制的格式。
Hessian是由caucho提供的一个基于binary-RPC实现的远程通讯library。
1)写一个接口:
package test.hassian;
public interface SayHello {
public String sayHello(String name);
public Student getStudent();
}
2)编写一个实现:
package test.hassian;
import com.caucho.hessian.server.HessianServlet;
public class SayHelloImpl extends HessianServlet implements SayHello {
public String sayHello(String name) {
return "hello "+name;
}
public Student getStudent(){
Student s=new Student();
s.setName("staratsky");
s.setSchool("ustc");
return s;
}
}
3)bean类
package test.hassian;
import java.io.Serializable;
public class Student implements Serializable {
private static final long serialVersionUID = -9006571629757493042L;
private String name;
private String school;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSchool() {
return school;
}
public void setSchool(String school) {
this.school = school;
}
public String toString() {
return name+" "+school;
}
}
4)远程调用类
package test.hassian;
import java.net.MalformedURLException;
import com.caucho.hessian.client.HessianProxyFactory;
public class HessianClientTest {
public static void main(String[] args) {
String url="";
HessianProxyFactory factory=new HessianProxyFactory();
try {
SayHello say=(SayHello) factory.create(SayHello.class, url);
System.out.println(say.sayHello("abc"));
System.out.println(say.getStudent());
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
服务器端的配置
在web.xml中,加入下面两段代码: