java遠端通訊技術及簡單實現
本文轉載自:http://blog.sina.com.cn/s/blog_4ad7c2540100bwe1.html
在分散式服務框架中,一個最基礎的問題就是遠端服務是怎麼通訊的,在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)應用級協議
遠端服務通訊,需要達到的目標是在一臺計算機發起請求,另外一臺機器在接收到請求後進行相應的處理並將結果返回給請求端,這其中又會有諸如onewayrequest、同步請求、非同步請求等等請求方式,按照網路通訊原理,需要實現這個需要做的就是將請求轉換成流,通過傳輸協議傳輸至遠端,遠端計算機在接收到請求的流後進行處理,處理完畢後將結果轉化為流,並通過傳輸協議返回給呼叫端。
在java領域中知名的遠端通訊的應用級協議有:RMI、XML-RPC、Binary-RPC、SOAP、JMS
2.RMI
2.1RMI原理
RMI,即JavaRMI(Java Remote MethodInvocation),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{ publicString helloWorld() throwsRemoteException; publicString sayHelloToSomeBody(String someBodyName) throwsRemoteException;
}
2)遠端物件實現
package test.rmi;
importjava.rmi.RemoteException;
importjava.rmi.server.UnicastRemoteObject;
public class HelloRMIImpl extends UnicastRemoteObject implementsHelloRMI { privatestatic final long serialVersionUID =-5464145481720553926L; publicHelloRMIImpl() throws RemoteException{ } publicString helloWorld() throws RemoteException{ return
"Hello World!"; } publicString sayHelloToSomeBody(String someBodyName) throwsRemoteException { return
"你好," + someBodyName +"!"; }
}
3)RMI伺服器
package test.rmi;
importjava.net.MalformedURLException;
importjava.rmi.AlreadyBoundException;
import java.rmi.Naming;
importjava.rmi.RemoteException;
importjava.rmi.registry.LocateRegistry;
public class HelloRMIServer{ publicstatic void main(String args[]){ try
{ //建立一個遠端物件 HelloRMI
rhello = newHelloRMIImpl(); //本地主機上的遠端物件登錄檔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;
importjava.net.MalformedURLException;
import java.rmi.Naming;
importjava.rmi.NotBoundException;
importjava.rmi.RemoteException;
public class HelloRMIClient{ publicstatic void main(Stringargs[]){ 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實現,對於不同語言間的通訊可以考慮用WebService來實現。
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 {
publicString 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();
//
should be modified according
to your configuration of jsp container
//
create a new XmlRpcClient object and bind above config objectwith it
XmlRpcClient
client = new XmlRpcClient();
client.setConfig(config);
//
create parameter list
Vector<String>
params = newVector<String>();
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;
publicvoid 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 XmlRpcServletServerobject 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 voiddoPost(HttpServletRequest Request, HttpServletResponseResponse)
throws IOException, ServletException {
server.execute(Request,
Response);
}
public voiddoGet(HttpServletRequest Request, HttpServletResponse Response)
throwsIOException, ServletException {
server.execute(Request, Response);
}
}
在web.xml中的配置
<servlet>
<servlet-name>XmlRpcServer</servlet-name>
<servlet-class>test.xmlrpc.XmlRpcServicesServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>XmlRpcServer</servlet-name>
<url-pattern>/HelloHandler</url-pattern>
</servlet-mapping>
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{ publicString sayHello(Stringname); publicStudent getStudent();
}
2)編寫一個實現:
package test.hassian;
importcom.caucho.hessian.server.HessianServlet;
public class SayHelloImpl extends HessianServlet implementsSayHello { public String sayHello(String name){ return
"hello "+name; } publicStudent getStudent(){ Student
s=newStudent(); s.setName("staratsky"); s.setSchool("ustc"); returns; }
}
3)bean類
package test.hassian;
importjava.io.Serializable;
privatestatic final long serialVersionUID =-9006571629757493042L; privateString name; privateString school; publicString getName() { return
name; } public voidsetName(String name) { this.name
= name; } publicString getSchool() { return
school; } public voidsetSchool(String school){ this.school
= school; } publicString toString() { return
name+" "+school; }
}
4)遠端呼叫類
package test.hassian;
importjava.net.MalformedURLException;
importcom.caucho.hessian.client.HessianProxyFactory;
public class HessianClientTest{ publicstatic void main(String[] args){ HessianProxyFactory
factory=newHessianProxyFactory(); 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中,加入下面兩段程式碼:
<servlet> <servlet-name>hassiantest</servlet-name>