程序員可能會經(jīng)常碰到這樣的事情:建立一個servlet應(yīng)用程序,它與公司的數(shù)據(jù)庫相連接,為客戶提供一種特定的服務(wù),這個應(yīng)用程序受到一個強大的驗證機制保護,全世界有成千上萬的客戶都在使用它。現(xiàn)在就出現(xiàn)了一個問題:當(dāng)應(yīng)用程序處在公司的防火墻之外時,你將如何從應(yīng)用程序提供用戶對數(shù)據(jù)庫的訪問?你知道,網(wǎng)絡(luò)管理員是不會專門為你的應(yīng)用程序與數(shù)據(jù)庫相連接而打開一個特殊端口的。
HTTP隧道技術(shù)和XML 如何越過防火墻與客戶/服務(wù)器應(yīng)用程序相連接這個問題已經(jīng)困擾程序員很久了。在多數(shù)情況下,一個公司的防火墻總是盡可能少地打開端口。一般情況下,你能夠使用的唯一端口就是80,這也就是Web所使用的端口。
解決這個問題的方法就是使用HTTP隧道技術(shù)(HTTP tunneling)。這個方法首先將請求包裝在一個HTTP POST請求中,然后這個請求再由一個在防火墻內(nèi)的Web 服務(wù)器上的CGI 應(yīng)用程序(例如一個servlet)來處理。
Servlet恢復(fù)原始的請求,執(zhí)行它,然后將結(jié)果插入到HTTP響應(yīng)流中。防火墻將這種相互作用解釋為對一個 Web頁面的常規(guī)請求,并允許對它繼續(xù)進行處理。這就象特洛伊木馬一樣:看起來是一個普通的請求,但其中隱藏著預(yù)料不到的負(fù)載。
下一個問題是如何對請求進行格式化?當(dāng)然,使用XML是將這些負(fù)載送入一個HTTP請求中去的最佳選擇。這個觀念很有獨創(chuàng)性,HTTP之上的XML是一個熱門的新興領(lǐng)域,一些新的規(guī)則正在編寫中,將來可以成為分布式應(yīng)用程序的標(biāo)準(zhǔn)通訊協(xié)議。其中,簡單對象訪問協(xié)議(SOAP)是最得到公認(rèn)的。
遺憾的是,現(xiàn)在還沒有一個穩(wěn)定執(zhí)行的SOAP供我們使用,目前所能找到的最好的一個來自Apache集團,但是它只支持簡單的返回類型。因此,它對于我們的項目是沒有用的。但是這也不錯,這意味著我們可以提出自己的HTTP上的XML的協(xié)議,并且借此來學(xué)習(xí)其中包含的概念。 概念 現(xiàn)在我們來建立一個簡單的框架結(jié)構(gòu)(framework),它將HTTP上的XML作為基本的通訊策略,從而讓我們能夠創(chuàng)建一套服務(wù),而且使得這些服務(wù)從分布在Internet上四面八方的桌面應(yīng)用程序都可以進行訪問。
首先,我們需要建立普通請求和響應(yīng)的語法。請求看起來是這樣的:
<?xml version='1.0' encoding='utf-8' ?> <http-request> <requestType> [type of request] </requestType> <request> [Application specific request.This will be an XML Elment ] </request> </http-request>
響應(yīng)看起來是這樣的:
<?xml version='1.0' encoding='utf-8' ?> <response> <responseMessage> [the response Message] </responseCode> <responseCode> [an application specific return code.] </responseCode> <response> [Application specific request.This will be an XML Element] </response> </http-response>
為了理解這個框架結(jié)構(gòu)背后的概念,我們要編寫一個應(yīng)用服務(wù)例程:一個簡單的數(shù)據(jù)庫服務(wù),它對任何SQL語句進行處理,并且將結(jié)果作為一個向量來返回。請求的細(xì)節(jié),也就是請求元素所包含的XML標(biāo)記非常簡單,如下:
<sql-statement> [The SQL statement to be executed] </ sql-statement > 響應(yīng)結(jié)果是這樣的: <result-set> </result-count> [the number of rows in the result set] </result-count> <row> <col name='name'> [the value] </col> ? </row> ? <result-set>
HTTPService是一個servlet,它響應(yīng)一個POST請求,恢復(fù)XML負(fù)載,并使用它來創(chuàng)建一個ServiceRequest例示。然后,根據(jù)請求的類型,將請求交給HttpServiceHandler 抽象類的一個特定子類。Handler類執(zhí)行請求,在ServiceResponse的一個例示中存儲結(jié)果,然后將這個結(jié)果發(fā)送回客戶端應(yīng)用程序。
通過按照慣例為服務(wù)處理器類命名,我們可以利用Java的映象功能來創(chuàng)建處理器類的一個例示,這個處理器類僅僅是建立在ServiceRequest 對象的服務(wù)類型屬性的基礎(chǔ)上。這就取消了HttpService和所有服務(wù)處理器類之間的依賴性,從而意味著當(dāng)我們增加一個新服務(wù)時,不再需要改變 HttpService類。
在我們這個例子中,服務(wù)的類型是DBService,因此我們將創(chuàng)建HttpServiceHandler的一個子類,叫做DBServiceHandler。在HttpService中,我們使用以下代碼:
String className = PACKAGE_NAME + "." + request.getRequestType() + "Handler"; HttpServiceHandler handler = Class.fromName(className).newInstance(); handler.handleRequest(request);
一個HttpServiceHandler子類需要執(zhí)行一個方法processRequest(),它需要取一個ServiceRequest對象,然后返回一個ServiceResponse對象。這個方法是在handleRequest 方法的過程中由子類調(diào)用的:
Public void handleRequest(ServiceRequest request) { Serviceresponse response = processRequest(request); SendResponse(response); }
這是使用Template(模板)方法模式的一個典型例子,在這個過程中,抽象超類調(diào)用在一個子類中執(zhí)行的方法。
ServiceRequest類將服務(wù)特定數(shù)據(jù)存儲為一個XML 文檔。這個類由訪問者來設(shè)置和獲取請求類型,它還有方法來處理請求的細(xì)節(jié)。getRequest()方法返回包含在請求標(biāo)記中的XML節(jié)點,setRequest()方法則用一個新生成的請求來覆蓋原來的請求。這個類還使用兩個factory方法,創(chuàng)建新的元素和文本節(jié)點,允許開發(fā)人員生成新的請求。ServiceResponse類以一種非常簡單的方法來處理請求的細(xì)節(jié)。
雖然這兩個類允許我們對所有類型的請求和響應(yīng)進行處理,但是開發(fā)人員也必須要了解每個請求的特殊語法。開發(fā)人員不能對請求的格式是否正確進行任何確認(rèn)。
為了簡化這個過程,我們將創(chuàng)建ServiceRequest和 ServiceResponse的子類,它們分別叫做DBServiceRequest和DBServiceResponse,它們都有處理服務(wù)特定細(xì)節(jié)的方法。例如,DBServiceRequest有設(shè)置和獲取SQL 語句的方法,同時 DBServiceResponse有設(shè)置和獲取結(jié)果記數(shù)值和結(jié)果設(shè)置矢量的方法。
服務(wù)是用HttpServiceClient類來訪問的。在客戶端應(yīng)用程序中,有以下的代碼:
HttpServiceClient client = new HttpServiceClient(serviceURL); DBServiceRequest request = new DBServiceRequest(); request.setSqlStatement(statement); DBServiceResponse response = new DBServiceResponse(client.executeRequest(request));
其中服務(wù)的URL是這樣的:
http://myHost/servlet/httpservice.HttpService.
細(xì)節(jié) 上面我們已經(jīng)看到了框架結(jié)構(gòu)中的所有元素,現(xiàn)在來看看那些有趣的細(xì)節(jié)。首先讓我們來注意一下協(xié)議層。我們應(yīng)該如何創(chuàng)建包裝XML負(fù)載的HTTP POST 請求?我們應(yīng)該如何處理HTTP響應(yīng)?
HTTP 請求是標(biāo)準(zhǔn)化的、基于ASCII的、與一個Web 服務(wù)器的socket通訊。這里有一個例子:
POST /servlet/ httpService.Httpservice HTTP/1.0 Host: localhostt:80 Content-Type: text/xml Content-Length: 248 <?xml version='1.0' encoding='utf-8' ?> <http-request> <requestType>DBService</requestType> <request> <sql-statement> SELECT * FROM MyTable </sql-statement > </request> </http-request>
來自Web服務(wù)器的響應(yīng)如下所示:
HTTP/1.0 200 OK Date: Fri, 24 Nov 2000 16:09:57 GMT Status: 200 Servlet-Engine: Tomcat Web Server/3.1 (JSP 1.1; Servlet 2.2; Java 1.3.0; Windows 2000 5.0 x86; java.vendor=Sun Microsystems Inc.) Content-Type: text/xml Content-Length: 726 Content-Language: en <?xml version='1.0' encoding='utf-8' ?> <http-response> <responseMessage>OK</responseCode> <responseCode>200</responseCode> <response> <result-set> </result-count>2 </result-count> <row> <col name='col1'>value11</col> <col name='col2'>value12</col> </row> <row> <col name='col1'>value21</col> <col name='col2'>value22/</col> </row> <result-set> </response> </http-response>
下面的代碼顯示了HttpServiceClient類的執(zhí)行,它將處理HTTP 請求的所有細(xì)節(jié)。你能看到,一旦你了解了那些請求的準(zhǔn)確格式,這就是一個非常簡單的過程:
public class HttpServiceClient { private static final String HTTP_VERSION = "1.0"; private static final String HTTP_POST_REQUEST ="POST"; private static final String HEADER_HOST = "Host"; private static final String HEADER_CONTENT_TYPE = "Content-Type"; private static final String XML_MIME_TYPE = "text/xml"; private static final String HEADER_CONTENT_LENGTH = "Content-Length"; private static final int DEFAULT_PORT = 80;
private String serviceUrl; private int returnCode; private String returnMessage; private Reader responsePayload;
public HttpServiceClient(String serviceUrl) { this.serviceUrl = serviceUrl; }
public ServiceResponse executeRequest(ServiceRequest request) throws HttpServiceException {
try { String data = request.serializeRequestToString("utf-8"); postRequest(data);
//check for failures if(returnCode != 200) throw new HttpServiceException(returnMessage);
InputSource source = new InputSource(responsePayload); DOMParser parser = new DOMParser(); parser.parse(source); ServiceResponse serviceResponse = new ServiceResponse(parser.getDocument());
String theResponse = serviceResponse.serializeResponseToString("utf-8"); System.err.println(theResponse);
return serviceResponse;
} catch(Exception ex) { ex.printStackTrace(System.err); throw new HttpServiceException(ex.getMessage()); } }
private void postRequest(String payload) throws HttpServiceException { PrintWriter out = null; BufferedReader in = null;
URL url = null; try { url = new URL(serviceUrl);
// No port? use default port 80 int port = url.getPort () < 0 ? DEFAULT_PORT : url.getPort();
Socket soket = new Socket (url.getHost (), port); out = new PrintWriter (soket.getOutputStream ());
in = new BufferedReader (new InputStreamReader(soket.getInputStream ())); } catch (Exception ex) { throw new HttpServiceException ("error opening socket: " + ex.getMessage ()); }
out.print (HTTP_POST_REQUEST + " " + url.getFile() + "HTTP/" + HTTP_VERSION + "\r\n"); out.print (HEADER_HOST + ": " + url.getHost () + ':' + url.getPort () + "\r\n"); out.print (HEADER_CONTENT_TYPE + ": " + XML_MIME_TYPE + "\r\n"); out.print (HEADER_CONTENT_LENGTH + ": " + payload.length () + "\r\n"); out.print ("\r\n"); out.print (payload); out.print ("\r\n\r\n"); out.flush ();
try { String statusLine = in.readLine(); System.err.println(statusLine); parseStatusLine(statusLine); } catch (Exception ex) { throw new HttpServiceException ("error parsing HTTP status line: " + ex.getMessage ()); }
// I will ignore all the headers and keep reading // until I get an empty line try { String headerLine = null; while ((headerLine = in.readLine ()) != null) { if (headerLine.length () == 0) break; } } catch (Exception ex) { throw new HttpServiceException ("error reading HTTP headers: " + ex.getMessage ()); }
//what remains of the input Stream is my payload responsePayload = in;
}
private void parseStatusLine(String statusLine) throws Exception { StringTokenizer st = new StringTokenizer (statusLine);
// this is the HTTP Version st.nextToken ();
returnCode = Integer.parseInt (st.nextToken ());
StringBuffer retMessage = new StringBuffer ();
while (st.hasMoreTokens ()) { retMessage.append (st.nextToken ()); if (st.hasMoreTokens ()) { retMessage.append (" "); } }
returnMessage = retMessage.toString ();
}
}
Web服務(wù)器接受了HTTP請求后,它就創(chuàng)建一個HttpService servlet的新例示,接著調(diào)用doPost()方法,在HttpServletRequest和HttpServletResponse對象中傳遞。然后這個servlet 就恢復(fù)XML負(fù)載,并且創(chuàng)建ServiceRequest類的一個例示,最后將其轉(zhuǎn)交給正確的處理器:
Document dom; DOMParser parser = new DOMParser(); InputSource input = new InputSource(request.getInputStream()); parser.parse(input); dom = parser.getDocument(); ServiceRequest serviceRequest = new ServiceRequest(dom); String className = PACKAGE_NAME + "." + request.getRequestType() + "Handler"; HttpServiceHandler handler = Class.fromName(className).newInstance(); handler.handleRequest(request);
下面的代碼顯示了DBServiceHandler類的執(zhí)行情況,這個類創(chuàng)建數(shù)據(jù)庫連接、執(zhí)行查詢并且生成DBServiceResponse對象。同樣,要注意這個過程非常簡單,因為許多復(fù)雜的問題都隱藏在ServiceResponse和ServiceRequest類及子類的后面了:
public class DBServiceHandler extends HttpServiceHandler
{ public ServiceResponse processRequest(ServiceRequest req) throws HttpServiceException { DBServiceRequest request = new DBServiceRequest(req);
String sql = request.getSqlStatement(); DBServiceResponse response = new DBServiceResponse(); Connection connection;
try { Class.forName("oracle.jdbc.driver.OracleDriver"); String connectionString = "jdbc:oracle:thin:@fender.openport.com:1521:iplp"; connection = DriverManager.getConnection(connectionString ,"op1","op1");
} catch(ClassNotFoundException ex) { ex.printStackTrace(System.err); response.setResponseCode(400); response.setResponseMessage("Oracle driver not found"); return response; } catch(SQLException ex2) { ex2.printStackTrace(System.err); response.setResponseCode(400); response.setResponseMessage("Could Not Connect To Database!"); return response; }
String theSql = sql.trim().toUpperCase(); ResultSet resultSet;
try { Statement statement = connection.createStatement();
if(theSql.startsWith("SELECT")) { resultSet = statement.executeQuery(theSql);
Vector theResults = parseResultSet(resultSet); response.setResultsCount(theResults.size() - 1); response.setResultSet(theResults); } else { statement.executeUpdate(theSql); response.setResultsCount(-1); response.setResultSet(new Vector()); }
}catch(SQLException ex) { response.setResponseCode(400); response.setResponseMessage(ex.getMessage()); return response; }
response.setResponseCode(200); response.setResponseMessage("OK");
String res = response.serializeResponseToString("utf-8"); System.out.println(res);
return response; } ? }
在下面的代碼中,你能看到ServiceRequest 和DBServiceRequest的執(zhí)行。請注意隨著DBService-Request還提供了一個額外的構(gòu)造器,它將ServiceRequest作為一個自變量。這就允許我們把原始請求的XML 文檔作為當(dāng)前文檔來使用,從而向現(xiàn)有的數(shù)據(jù)提供一個額外的應(yīng)用程序特有的界面:
public class ServiceRequest
{ public final static String REQUEST_TYPE_TAG_NAME = "requestType"; public final static String REQUEST_TAG_NAME = "request"; public final static String ROOT_TAG_NAME = "http-request";
protected Document dom;
public ServiceRequest(Document request) { dom = request; }
public ServiceRequest() { dom = new DocumentImpl(); initializeRequest(); }
//initializes an empty request private void initializeRequest() { Element root = dom.createElement(ROOT_TAG_NAME); dom.appendChild(root);
Element eRequestType = dom.createElement(REQUEST_TYPE_TAG_NAME); eRequestType.appendChild(dom.createTextNode(""));
root.appendChild(eRequestType);
Element eRequest = dom.createElement(REQUEST_TAG_NAME); root.appendChild(eRequest); }
public String getRequestType() throws HttpServiceException { try { return getTextAttribute(REQUEST_TYPE_TAG_NAME); } catch(Exception ex) { ex.printStackTrace(System.err); throw new HttpServiceException("Invalid Request Format."); }
}
public void setRequestType(String requestType) throws HttpServiceException { try { setTextAttribute(REQUEST_TYPE_TAG_NAME,requestType); } catch(Exception ex) { ex.printStackTrace(System.err); throw new HttpServiceException("Invalid Request Format."); }
}
public Node getRequest() throws HttpServiceException { try { Node request = ((NodeList)dom.getElementsByTagName(REQUEST_TAG_NAME)).item(0);
return request.getFirstChild().cloneNode(true);
} catch(Exception ex) { ex.printStackTrace(System.err); throw new HttpServiceException("Invalid Request Format."); } }
public Element createElementNode(String elementName) { return dom.createElement(elementName); }
public Text createTextNode(String value) { return dom.createTextNode(value); }
public void setRequest(Node request) throws HttpServiceException {
try { Node requestElement = ((NodeList)dom.getElementsByTagName(REQUEST_TAG_NAME)).item(0); Node oldRequest = requestElement.getFirstChild();
if(oldRequest != null) requestElement.removeChild(oldRequest);
requestElement.appendChild(request); } catch(Exception ex) { ex.printStackTrace(System.err); throw new HttpServiceException("Invalid Request Format."); } }
public byte[] serializeRequestToByteArray(String encoding) throws HttpServiceException { try { return serializeDOM(encoding).toByteArray(); } catch(Exception ex) { ex.printStackTrace(System.err); throw new HttpServiceException("Error during serialization"); } }
public String serializeRequestToString(String encoding) throws HttpServiceException { try { return serializeDOM(encoding).toString(); } catch(Exception ex) { ex.printStackTrace(System.err); throw new HttpServiceException( "Error during serialization"); } }
private ByteArrayOutputStream serializeDOM(String encoding) throws HttpServiceException { try { ByteArrayOutputStream bytes = new ByteArrayOutputStream (4096); PrintWriter out = new PrintWriter (new OutputStreamWriter (bytes,encoding),true) ; OutputFormat of = new OutputFormat(dom,encoding,true); XMLSerializer serializer = new XMLSerializer(out,of); serializer.serialize(dom); out.close();
return bytes; } catch(Exception ex) { ex.printStackTrace(System.err); throw new HttpServiceException("Error during serialization"); } }
protected String getTextAttribute(String name) { Node textAttributeNode = ((NodeList)dom.getElementsByTagName(name)).item(0); Node textAttribute = textAttributeNode.getFirstChild(); if(textAttribute.getNodeType() == Node.TEXT_NODE) return textAttribute.getNodeValue(); else return null; }
protected void setTextAttribute(String name, String value) { if (value == null) value =""; Node textAttributeNode =((NodeList)dom.getElementsByTagName(name)).item(0); Node textAttribute = textAttributeNode.getFirstChild(); textAttribute.setNodeValue(value);
} }
public class DBServiceRequest extends ServiceRequest {
public final static String SERVICE_NAME = "DBService";
public final static String SQL_STATEMENT_TAG_NAME = "sql-statement";
public DBServiceRequest() { super(); initializeParameters(); }
public DBServiceRequest(Document request) { super(request); }
public DBServiceRequest(ServiceRequest request) { dom = request.dom; }
public void setSqlStatement(String sql) { setTextAttribute(SQL_STATEMENT_TAG_NAME,sql); }
public String getSqlStatement() { return getTextAttribute(SQL_STATEMENT_TAG_NAME); } private void initializeParameters() { Element eDBRequest = null; //not very nice but this should never fail
try { setRequestType(SERVICE_NAME); eDBRequest = createElementNode(SQL_STATEMENT_TAG_NAME); } catch(Exception ex) {}
eDBRequest.appendChild(dom.createTextNode(""));
try { setRequest(eDBRequest); } catch(Exception ex) { } }
擴展框架結(jié)構(gòu) 我們可以對這個框架進行擴展,從而處理任何類型的服務(wù)。要想創(chuàng)建一個新的服務(wù),首先必須要定義XML請求和響應(yīng)的語法。然后,創(chuàng)建ServiceRequest和ServiceResponse的一個子類,它們將幫助處理服務(wù)特有的數(shù)據(jù)。最后,創(chuàng)建Http-ServiceHandler的一個新子類,它將處理請求并生成適當(dāng)?shù)捻憫?yīng)。整個過程就是這樣。
雖然這個框架包含了一些功能,但是如果不增加一些更多功能的話,它在實際應(yīng)用情況下還不是很實用。我有意識地省略了這些功能,以使框架結(jié)構(gòu)保持簡單,并將注意力集中到了最重要的細(xì)節(jié)上。為了完整起見,現(xiàn)在我們來簡要分析這個框架結(jié)構(gòu)的一些局限性以及應(yīng)該如何去克服這些局限性。
首先,這個框架結(jié)構(gòu)不能限制對服務(wù)的訪問。這就意味著知道如何訪問這個服務(wù)的每個人都能夠進行訪問。在你允許對服務(wù)的訪問之前,請求某種證明可以解決這個問題。你可以用這同一個框架來創(chuàng)建一個證明服務(wù),這樣就能確認(rèn)用戶并生成一個唯一的ID,當(dāng)用戶訪問任何其它服務(wù)時,都會被要求這個ID。系統(tǒng)會將這個ID存儲在某些類型的訪問列表中,并且要求在一個有限時間內(nèi),每個請求都必須通過一個有效ID才能訪問服務(wù)。
另外,每次用戶訪問服務(wù)時,服務(wù)都要創(chuàng)建一個與數(shù)據(jù)庫的連接。將服務(wù)與一個連接pooling框架組合起來可以解決這個問題,這樣就將給請求分配一個現(xiàn)有連接,而不是每次都創(chuàng)建新連接。
最后一個局限是缺乏session的管理。由于我們是通過一個socket 直接訪問servlet,因此不能使用特別有用的HttpSession對象。由于在Web 服務(wù)器與瀏覽器相互作用的過程中,在客戶計算機上沒有生成session cookie ,因此不能管理自動session。為了克服這個局限,我們可以執(zhí)行我們自己的session管理。一個session可以是一個與唯一ID相關(guān)聯(lián)的對象,它可以存儲其它對象。例如,你可以有一個上下文hash信號表格,在其中存儲session對象,使用唯一的ID 作為關(guān)鍵字。這個session對象還可以包含一個hash信號表格,它存儲了你想在session中堅持使用的每個對象。
這個框架利用HTTP隧道技術(shù),允許一個桌面應(yīng)用程序訪問防火墻后面的服務(wù),對其進行擴展后還提供對其它類型服務(wù)的簡易訪問。
|