程序員可能會經常碰到這樣的事情:建立一個servlet應用程序,它與公司的數據庫相連接,為客戶提供一種特定的服務,這個應用程序受到一個強大的驗證機制保護,全世界有成千上萬的客戶都在使用它。現在就出現了一個問題:當應用程序處在公司的防火墻之外時,你將如何從應用程序提供用戶對數據庫的訪問?你知道,網絡管理員是不會專門為你的應用程序與數據庫相連接而打開一個特殊端口的。
HTTP隧道技術和XML 如何越過防火墻與客戶/服務器應用程序相連接這個問題已經困擾程序員很久了。在多數情況下,一個公司的防火墻總是盡可能少地打開端口。一般情況下,你能夠使用的唯一端口就是80,這也就是Web所使用的端口。
解決這個問題的方法就是使用HTTP隧道技術(HTTP tunneling)。這個方法首先將請求包裝在一個HTTP POST請求中,然后這個請求再由一個在防火墻內的Web 服務器上的CGI 應用程序(例如一個servlet)來處理。
Servlet恢復原始的請求,執行它,然后將結果插入到HTTP響應流中。防火墻將這種相互作用解釋為對一個 Web頁面的常規請求,并允許對它繼續進行處理。這就象特洛伊木馬一樣:看起來是一個普通的請求,但其中隱藏著預料不到的負載。
下一個問題是如何對請求進行格式化?當然,使用XML是將這些負載送入一個HTTP請求中去的最佳選擇。這個觀念很有獨創性,HTTP之上的XML是一個熱門的新興領域,一些新的規則正在編寫中,將來可以成為分布式應用程序的標準通訊協議。其中,簡單對象訪問協議(SOAP)是最得到公認的。
遺憾的是,現在還沒有一個穩定執行的SOAP供我們使用,目前所能找到的最好的一個來自Apache集團,但是它只支持簡單的返回類型。因此,它對于我們的項目是沒有用的。但是這也不錯,這意味著我們可以提出自己的HTTP上的XML的協議,并且借此來學習其中包含的概念。 概念 現在我們來建立一個簡單的框架結構(framework),它將HTTP上的XML作為基本的通訊策略,從而讓我們能夠創建一套服務,而且使得這些服務從分布在Internet上四面八方的桌面應用程序都可以進行訪問。
首先,我們需要建立普通請求和響應的語法。請求看起來是這樣的:
<?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>
響應看起來是這樣的:
<?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>
為了理解這個框架結構背后的概念,我們要編寫一個應用服務例程:一個簡單的數據庫服務,它對任何SQL語句進行處理,并且將結果作為一個向量來返回。請求的細節,也就是請求元素所包含的XML標記非常簡單,如下:
<sql-statement> [The SQL statement to be executed] </ sql-statement > 響應結果是這樣的: <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,它響應一個POST請求,恢復XML負載,并使用它來創建一個ServiceRequest例示。然后,根據請求的類型,將請求交給HttpServiceHandler 抽象類的一個特定子類。Handler類執行請求,在ServiceResponse的一個例示中存儲結果,然后將這個結果發送回客戶端應用程序。
通過按照慣例為服務處理器類命名,我們可以利用Java的映象功能來創建處理器類的一個例示,這個處理器類僅僅是建立在ServiceRequest 對象的服務類型屬性的基礎上。這就取消了HttpService和所有服務處理器類之間的依賴性,從而意味著當我們增加一個新服務時,不再需要改變 HttpService類。
在我們這個例子中,服務的類型是DBService,因此我們將創建HttpServiceHandler的一個子類,叫做DBServiceHandler。在HttpService中,我們使用以下代碼:
String className = PACKAGE_NAME + "." + request.getRequestType() + "Handler"; HttpServiceHandler handler = Class.fromName(className).newInstance(); handler.handleRequest(request);
一個HttpServiceHandler子類需要執行一個方法processRequest(),它需要取一個ServiceRequest對象,然后返回一個ServiceResponse對象。這個方法是在handleRequest 方法的過程中由子類調用的:
Public void handleRequest(ServiceRequest request) { Serviceresponse response = processRequest(request); SendResponse(response); }
這是使用Template(模板)方法模式的一個典型例子,在這個過程中,抽象超類調用在一個子類中執行的方法。
ServiceRequest類將服務特定數據存儲為一個XML 文檔。這個類由訪問者來設置和獲取請求類型,它還有方法來處理請求的細節。getRequest()方法返回包含在請求標記中的XML節點,setRequest()方法則用一個新生成的請求來覆蓋原來的請求。這個類還使用兩個factory方法,創建新的元素和文本節點,允許開發人員生成新的請求。ServiceResponse類以一種非常簡單的方法來處理請求的細節。
雖然這兩個類允許我們對所有類型的請求和響應進行處理,但是開發人員也必須要了解每個請求的特殊語法。開發人員不能對請求的格式是否正確進行任何確認。
為了簡化這個過程,我們將創建ServiceRequest和 ServiceResponse的子類,它們分別叫做DBServiceRequest和DBServiceResponse,它們都有處理服務特定細節的方法。例如,DBServiceRequest有設置和獲取SQL 語句的方法,同時 DBServiceResponse有設置和獲取結果記數值和結果設置矢量的方法。
服務是用HttpServiceClient類來訪問的。在客戶端應用程序中,有以下的代碼:
HttpServiceClient client = new HttpServiceClient(serviceURL); DBServiceRequest request = new DBServiceRequest(); request.setSqlStatement(statement); DBServiceResponse response = new DBServiceResponse(client.executeRequest(request));
其中服務的URL是這樣的:
http://myHost/servlet/httpservice.HttpService.
細節 上面我們已經看到了框架結構中的所有元素,現在來看看那些有趣的細節。首先讓我們來注意一下協議層。我們應該如何創建包裝XML負載的HTTP POST 請求?我們應該如何處理HTTP響應?
HTTP 請求是標準化的、基于ASCII的、與一個Web 服務器的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服務器的響應如下所示:
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類的執行,它將處理HTTP 請求的所有細節。你能看到,一旦你了解了那些請求的準確格式,這就是一個非常簡單的過程:
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服務器接受了HTTP請求后,它就創建一個HttpService servlet的新例示,接著調用doPost()方法,在HttpServletRequest和HttpServletResponse對象中傳遞。然后這個servlet 就恢復XML負載,并且創建ServiceRequest類的一個例示,最后將其轉交給正確的處理器:
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類的執行情況,這個類創建數據庫連接、執行查詢并且生成DBServiceResponse對象。同樣,要注意這個過程非常簡單,因為許多復雜的問題都隱藏在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的執行。請注意隨著DBService-Request還提供了一個額外的構造器,它將ServiceRequest作為一個自變量。這就允許我們把原始請求的XML 文檔作為當前文檔來使用,從而向現有的數據提供一個額外的應用程序特有的界面:
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) { } }
擴展框架結構 我們可以對這個框架進行擴展,從而處理任何類型的服務。要想創建一個新的服務,首先必須要定義XML請求和響應的語法。然后,創建ServiceRequest和ServiceResponse的一個子類,它們將幫助處理服務特有的數據。最后,創建Http-ServiceHandler的一個新子類,它將處理請求并生成適當的響應。整個過程就是這樣。
雖然這個框架包含了一些功能,但是如果不增加一些更多功能的話,它在實際應用情況下還不是很實用。我有意識地省略了這些功能,以使框架結構保持簡單,并將注意力集中到了最重要的細節上。為了完整起見,現在我們來簡要分析這個框架結構的一些局限性以及應該如何去克服這些局限性。
首先,這個框架結構不能限制對服務的訪問。這就意味著知道如何訪問這個服務的每個人都能夠進行訪問。在你允許對服務的訪問之前,請求某種證明可以解決這個問題。你可以用這同一個框架來創建一個證明服務,這樣就能確認用戶并生成一個唯一的ID,當用戶訪問任何其它服務時,都會被要求這個ID。系統會將這個ID存儲在某些類型的訪問列表中,并且要求在一個有限時間內,每個請求都必須通過一個有效ID才能訪問服務。
另外,每次用戶訪問服務時,服務都要創建一個與數據庫的連接。將服務與一個連接pooling框架組合起來可以解決這個問題,這樣就將給請求分配一個現有連接,而不是每次都創建新連接。
最后一個局限是缺乏session的管理。由于我們是通過一個socket 直接訪問servlet,因此不能使用特別有用的HttpSession對象。由于在Web 服務器與瀏覽器相互作用的過程中,在客戶計算機上沒有生成session cookie ,因此不能管理自動session。為了克服這個局限,我們可以執行我們自己的session管理。一個session可以是一個與唯一ID相關聯的對象,它可以存儲其它對象。例如,你可以有一個上下文hash信號表格,在其中存儲session對象,使用唯一的ID 作為關鍵字。這個session對象還可以包含一個hash信號表格,它存儲了你想在session中堅持使用的每個對象。
這個框架利用HTTP隧道技術,允許一個桌面應用程序訪問防火墻后面的服務,對其進行擴展后還提供對其它類型服務的簡易訪問。
|