“穿越”防火墙的XML技术
(青苹果工作室编译 2001年09月17日 17:16)
程序员可能会经常碰到这样的事情:建立一个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> |
下图显示了参与框架结构的对象的XML类图表:
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' ?> |
下面的代码显示了HttpServiceClient类的执行,它将处理HTTP 请求的所有细节。你能看到,一旦你了解了那些请求的准确格式,这就是一个非常简单的过程:
public class HttpServiceClient { private static final String HTTP_VERSION = "1.0"; private String serviceUrl; public HttpServiceClient(String serviceUrl) public ServiceResponse executeRequest(ServiceRequest request) try //check for failures InputSource source = String theResponse = return serviceResponse; }
// No port? use default port 80 Socket soket = new Socket (url.getHost (), port); in = new BufferedReader (new InputStreamReader(soket.getInputStream ())); out.print (HTTP_POST_REQUEST + " " + url.getFile() + "HTTP/" + HTTP_VERSION + "\r\n"); try // I will ignore all the headers and keep reading //what remains of the input Stream is my payload } private void parseStatusLine(String statusLine) // this is the HTTP Version returnCode = Integer.parseInt (st.nextToken ()); StringBuffer retMessage = new StringBuffer (); while (st.hasMoreTokens ()) returnMessage = retMessage.toString (); } } |
Web服务器接受了HTTP请求后,它就创建一个HttpService servlet的新例示,接着调用doPost()方法,在HttpServletRequest和HttpServletResponse对象中传递。然后这个servlet 就恢复XML负载,并且创建ServiceRequest类的一个例示,最后将其转交给正确的处理器:
Document dom; |
下面的代码显示了DBServiceHandler类的执行情况,这个类创建数据库连接、执行查询并且生成DBServiceResponse对象。同样,要注意这个过程非常简单,因为许多复杂的问题都隐藏在ServiceResponse和ServiceRequest类及子类的后面了:
public class DBServiceHandler extends { String sql = request.getSqlStatement(); try } String theSql = sql.trim().toUpperCase(); try if(theSql.startsWith("SELECT")) Vector theResults = parseResultSet(resultSet); }catch(SQLException ex) response.setResponseCode(200); String res = response.serializeResponseToString("utf-8"); return response; |
在下面的代码中,你能看到ServiceRequest 和DBServiceRequest的执行。请注意随着DBService-Request还提供了一个额外的构造器,它将ServiceRequest作为一个自变量。这就允许我们把原始请求的XML 文档作为当前文档来使用,从而向现有的数据提供一个额外的应用程序特有的界面:
public class ServiceRequest { protected Document dom; public ServiceRequest(Document request) public ServiceRequest() //initializes an empty request Element eRequestType = root.appendChild(eRequestType); Element eRequest = public String getRequestType() } public void setRequestType(String requestType) } public Node getRequest() return request.getFirstChild().cloneNode(true); } public Element createElementNode(String elementName) public Text createTextNode(String value) public void setRequest(Node request) try if(oldRequest != null) requestElement.appendChild(request); public byte[] serializeRequestToByteArray(String encoding) public String serializeRequestToString(String encoding) private ByteArrayOutputStream serializeDOM(String encoding) return bytes; protected String getTextAttribute(String name) protected void setTextAttribute(String name, String 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() public DBServiceRequest(Document request) public DBServiceRequest(ServiceRequest request) public void setSqlStatement(String sql) public String getSqlStatement() try eDBRequest.appendChild(dom.createTextNode("")); try |
扩展框架结构
我们可以对这个框架进行扩展,从而处理任何类型的服务。要想创建一个新的服务,首先必须要定义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隧道技术,允许一个桌面应用程序访问防火墙后面的服务,对其进行扩展后还提供对其它类型服务的简易访问。