Web Service 技术内幕
--Web Service 的详细教程和协议分析
2010/2/25 蒋彪 于南京
1. Web Service 的介 绍
1.1 Web Service 到底是什 么 ?
研究一下当前的 应 用程序 开发 ,你会 发现 一个 绝对 的 倾 向:人 们开 始偏 爱 基于 浏览 器的瘦客 户应 用程序。 这 当然不是因 为 瘦客 户 能 够 提供更好的用 户 界面,而是因 为 它能 够 避免花在桌面 应 用程序 发 布上的高成本。 发 布桌面 应 用程序成本很高,一半是因 为应 用程序安装和配置的 问题 ,另一半是因 为 客 户 和服 务 器之 间 通信的 问题 。
传统 的 Windows 富客 户应 用程序使用 DCOM 来与服 务 器 进 行通信和 调 用 远 程 对 象。配置好 DCOM 使其在一个大型的网 络 中正常工作将是一个极富挑 战 性的工作,同 时 也是 许 多 IT 工程 师 的噩梦。事 实 上, 许 多 IT 工程 师 宁愿忍受 浏览 器所 带 来的功能限制,也不愿在局域网上去运行一个 DCOM 。在我看来, 结 果就是一个 发 布容易,但 开发难 度大而且用 户 界面极其受限的 应 用程序。极端的 说 ,就是你花了更多的 资 金和 时间 ,却 开发 出从用 户 看来功能更弱的 应 用程序。不信 你 问问 你的会 计师对 新的基于 浏览 器的会 计软 件有什 么 想法: 绝 大多数商用程序用 户 希望使用更加友好的 Windows 用 户 界面。
关 于客 户 端与服 务 器的通信 问题 ,一个完美的解决方法是使用 HTTP 协议 来通信。 这 是因 为 任何运行 Web 浏览 器 的机器都在使用 HTTP 协议 。同 时 ,当前 许 多防火 墙 也配置 为 只允 许 HTTP 连 接。
许 多商用程序 还 面 临 另一个 问题 ,那就是与其他程序的互操作性。如果所有的 应 用程序都是使用 COM 或 .NET 语 言写的,并且都运行在 Windows 平台上,那就天下太平了。然而,事 实 上大多数商 业 数据仍然在大型主机上以非 关 系文件 (VSAM) 的形式存放,并由 COBOL 语 言 编 写的大型机程序 访问 。而且,目前 还 有很多商用程序 继续 在使用 C++ 、 Java 、 Visual Basic 和其他各 种 各 样 的 语 言 编 写。 现 在,除了最 简单 的程序之外,所有的 应 用程序都需要与运行在其他异构平台上的 应 用程序集成并 进 行数据交 换 。 这样 的任 务 通常都是由特殊的方法,如文件 传输 和分析,消息 队 列, 还 有 仅 适用于某些情况的的 API ,如 IBM 的 " 高 级 程序到程序交流 (APPC)" 等来完成的。在以前,没有一个 应 用程序通信 标 准,是独立于平台、 组 建模型和 编 程 语 言的。只有通 过 Web Service ,客 户 端和服 务 器才能 够 自由的用 HTTP 进 行通信,不 论 两个程序的平台和 编 程 语 言是什 么 。
什 么 是 Web Service
对这 个 问题 ,我 们 至少有两 种 答案。从表面上看, Web Service 就是一个 应 用程序,它向外界暴露出一个能 够 通 过 Web 进 行 调 用的 API 。 这 就是 说 ,你能 够 用 编 程的方法通 过 Web 来 调 用 这 个 应 用程序。我 们 把 调 用 这 个 Web Service 的 应 用程序叫做客 户 。例如,你想 创 建一个 Web Service ,它的作用是返回当前的天气情况。那 么 你可已建立一个 ASP 页 面,它接受 邮 政 编码 作 为查询 字符串,然后返回一个由逗号隔 开 的字符串,包含了当前的气温和天气。要 调 用 这 个 ASP 页 面,客 户 端需要 发 送下面的 这 个 HTTP GET 请 求:
http://host.company.com/weather.asp?zipcode=20171
返回的数据就 应该 是 这样 :
这 个 ASP 页 面就 应该 可以算作是 Web Service 了。因 为 它基于 HTTP GET 请 求,暴露出了一个可以通 过 Web 调 用的 API 。当然, Web Service 还 有更多的 东 西。
下面是 对 Web Service 更精确的解 释 : Web Service s 是建立可互操作的分布式 应 用程序的新平台。作 为 一个 Windows 程序 员 ,你可能已 经 用 COM 或 DCOM 建立 过 基于 组 件的分布式 应 用程序。 COM 是一个非常好的 组 件技 术 ,但是我 们 也很容易 举 出 COM 并不能 满 足要求的情况。
Web Service 平台是一套 标 准,它定 义 了 应 用程序如何在 Web 上 实现 互操作性。你可以用任何你喜 欢 的 语 言,在任何你喜 欢 的平台上写 Web Service ,只要我 们 可以通 过 Web Service 标 准 对这 些服 务进 行 查询 和 访问 。
新平台
Web Service 平台需要一套 协议 来 实现 分布式 应 用程序的 创 建。任何平台都有它的数据表示方法和 类 型系 统 。要 实现 互操作性, Web Service 平台必 须 提供一套 标 准的 类 型系 统 ,用于沟通不同平台、 编 程 语 言和 组 件模型中的不同 类 型系 统 。在 传统 的分布式系 统 中,基于界面 (interface) 的平台提供了一些方法来描述界面、方法和参数 ( 译 注:如 COM 和 COBAR 中的 IDL 语 言 ) 。同 样 的, Web Service 平台也必 须 提供一 种标 准来描述 Web Service , 让 客 户 可以得到足 够 的信息来 调 用 这 个 Web Service 。最后,我 们还 必 须 有一 种 方法来 对这 个 Web Service 进 行 远 程 调 用。 这种 方法 实际 是一 种远 程 过 程 调 用 协议 (RPC) 。 为 了达到互操作性, 这种 RPC 协议还 必 须 与平台和 编 程 语 言无 关 。 下面几个小 节 就 简 要介 绍 了 组 成 Web Service 平台的 这 三个技 术 。
XML 和 XSD
可 扩 展的 标记语 言 (XML) 是 Web Service 平台中表示数据的基本格式。除了易于建立和易于分析外, XML 主要的 优 点在于它既是平台无 关 的,又是厂商无 关 的。无 关 性是比技 术优 越性更重要的: 软 件厂商是不会 选择 一个由 竞 争 对 手所 发 明的技 术 的。
XML 解决了数据表示的 问题 ,但它没有定 义 一套 标 准的数据 类 型,更没有 说 怎 么 去 扩 展 这 套数据 类 型。例如,整形数到底代表什 么 ?16 位, 32 位, 还 是 64 位 ? 这 些 细节对实现 互操作性都是很重要的。 W3C 制定的 XML Schema(XSD) 就是 专门 解决 这 个 问题 的一套 标 准。它定 义 了一套 标 准的数据 类 型,并 给 出了一 种语 言来 扩 展 这 套数据 类 型。 Web Service 平台就是用 XSD 来作 为 其数据 类 型系 统 的。当你用某 种语 言 ( 如 VB.NET 或 C#) 来构造一个 Web Service 时 , 为 了符合 Web Service 标 准,所有你使用的数据 类 型都必 须 被 转换为 XSD 类 型。 你用的工具可能已 经 自 动 帮你完成了 这 个 转换 ,但你很可能会根据你的需要修改一下 转换过 程。在第二章中,我 们 将深入 XSD ,学 习 怎 样转换 自定 义 的数据 类 型 ( 例如 类 ) 到 XSD 的 类 型。
SOAP
Web Service 建好以后,你或者其他人就会去 调 用它。 简单对 象 访问协议 (SOAP) 提供了 标 准的 RPC 方法来 调 用 Web Service 。 实际 上, SOAP 在 这 里有点用 词 不当:它意味着下面的 Web Service 是以 对 象的方式表示的,但事 实 并不一定如此:你完全可以把你的 Web Service 写成一系列的 C 函数,并仍然使用 SOAP 进 行 调 用。 SOAP 规 范定 义 了 SOAP 消息的格式,以及怎 样 通 过 HTTP 协议 来使用 SOAP 。 SOAP 也是基于 XML 和 XSD 的, XML 是 SOAP 的数据 编码 方式。第三章我 们 会 讨论 SOAP ,并 结识 SOAP 消息的各 种 元素。
WSDL
你会怎 样 向 别 人介 绍 你的 Web Service 有什 么 功能,以及 每 个函数 调 用 时 的参数呢 ? 你可能会自己写一套文档,你甚至可能会口 头 上告 诉 需要使用你的 Web Service 的人。 这 些非正式的方法至少都有一个 严 重的 问题 :当程序 员 坐到 电脑 前,想要使用你的 Web Service 的 时 候,他 们 的工具 ( 如 Visual Studio) 无法 给 他 们 提供任 何帮助,因 为这 些工具根本就不了解你的 Web
service 。解决方法是:用机器能 阅读 的方式提供一个正式的描述文档。 Web Service 描述 语 言 (WSDL) 就是 这样 一个基于 XML 的 语 言,用于描述 Web Service 及其函数、参数和返回 值 。因 为 是基于 XML 的,所以 WSDL 既是机器可 阅读 的,又是人可 阅读 的, 这 将是一个很大的好 处 。一些最新的 开发 工具既能根据你的 Web Service 生成 WSDL 文档,又能 导 入 WSDL 文档,生成 调 用相 应 Web Service 的代 码 。
一句话总结: Web Service 就是高级版的 DCOM 和 CORBA
1.2 Web Service 的体系 结 构 图
通过上图,我们能看出来, WebService 的运行机制有点类似于 CORBA 。
1. 通过 WSDL 描述 WebService
2. 通过 SOAP 在互联网环境内传递数据
3. 通过 JAXB 实现 Java 和 XML 之间的互相转换。
具体的 Web Service 的体系结构可以参照以下文件: ====================================================== 《 深入浅出 JAX-WS 2.0 》 http://blog.csdn.net/nanjingjiangbiao/archive/2010/02/11/5306034.aspx 《 SOA 技 术 研究之 图 解 JAX-WS 技 术 》 http://blog.csdn.net/nanjingjiangbiao/archive/2010/02/10/5305049.aspx 《 手把手教你在 Interstage 上部署 WebService 》 http://blog.csdn.net/nanjingjiangbiao/archive/2010/02/22/5317356.aspx ====================================================== . |
1.3 一个 简单 的 WebService 运行 实 例
WebService 代码:
package stock.server ; @ javax.jws. WebService public class StockQuoteProvider { public StockQuoteProvider () { } public float getLastTradePrice (String tickerSymbol) { return "abc" .equals(tickerSymbol)? 1234.0f : 0.0f; } } |
客户端代码:
import java.lang.annotation.Annotation ; import stock.server.*; import javax.xml.ws.Service ;
public class StockQuoteClient { public static void main(String[] args) throws Exception { StockQuoteProviderService service = new StockQuoteProviderService(); StockQuoteProvider port = service.getStockQuoteProviderPort(); System. out .println(port.getLastTradePrice(args[0])); } } |
2. Web Service 技 术 研究的 环 境准 备
2.1 安装运行 环 境
推荐安装 GlassFish 服务器,具体安装方法可以参见以下文章:
http://blog.csdn.net/nanjingjiangbiao/archive/2010/01/28/5264913.aspx |
2. 2 下载 WebService API(JAX-WS) 的源代码
具体的下载 URL 参见以下地址:
https://jax-ws-sources.dev.java.net/source/browse/jax-ws-sources/ |
2. 3 部署服务器端
1. 把 1.3 中的服务器端部署到 GlassFish 中
2. 用 wsimport 命令生成 stub 中间程序
3. 把 1.3 中的客户端代码和 stub 代码,以及 2.2 下载的 WebService 源代码部署到 Eclipse 工程中,就可以 DEBUG 了。
具体的可以参照以下文章:
《 SOA 技 术 研究之 图 解 JAX-WS 技 术 》 http://blog.csdn.net/nanjingjiangbiao/archive/2010/02/10/5305049.aspx |
3. Web Service 的技 术 内幕
3.1 客 户 端是如何 和 WSDL 建立 关 系的?
1. 首先,我们看到客户端程序中有以下这样一行
StockQuoteProviderService service = new StockQuoteProviderService(); |
2. 这行代码,会调用到
public StockQuoteProviderService() { super ( STOCKQUOTEPROVIDERSERVICE_WSDL_LOCATION , new QName( "http://server.stock/" , "StockQuoteProviderService" )); } |
3. 这行代码,会调用到 c om.sun.xml.ws.spi.ProviderImpl 的
@Override public ServiceDelegate createServiceDelegate( URL wsdlDocumentLocation, QName serviceName, Class serviceClass) { return new WSServiceDelegate(wsdlDocumentLocation, serviceName, serviceClass ); } |
4. 这行代码,会调用到 com.sun.xml.ws.clientWSServiceDelegate 的
/** * @param serviceClass * Either {@link Service} .class or other generated service - derived classes. */ public WSServiceDelegate( @Nullable Source wsdl, @NotNull QName serviceName, @NotNull final Class<? extends Service> serviceClass) { ~省略~ } |
5 . 而其中最关键的就是以下这段,解析阅读 WSDL 的代码,这段代码主要是用 XMLStream 来构建 WSDL 代码,不足为奇。
WSDLServiceImpl service= null ; if (wsdl != null ) { try { URL url = wsdl.getSystemId()== null ? null : new URL(wsdl.getSystemId()); WSDLModelImpl model = parseWSDL(url, wsdl); service = model.getService( this . serviceName ); if (service == null ) throw new WebServiceException( ClientMessages. INVALID_SERVICE_NAME ( this . serviceName , buildNameList(model.getServices().keySet()))); // fill in statically known ports for (WSDLPortImpl port : service.getPorts()) ports .put(port.getName(), new PortInfo( this , port)); } catch (MalformedURLException e) { throw new WebServiceException(ClientMessages. INVALID_WSDL_URL (wsdl.getSystemId()), e); } } this . wsdlService = service; |
3. 2 客 户 端是如何 和 SEI 建立 关 系的?
1. 首先,我们看到客户端程序中有以下这样一行
StockQuoteProvider port = service.getStockQuoteProviderPort(); |
2. 这行代码,会调用到 com.sun.xml.ws.clientWSServiceDelegate 的
private <T> T getPort (WSEndpointReference wsepr, QName portName, Class<T> portInterface, WebServiceFeature... features) { SEIPortInfo spi = addSEI(portName, portInterface, features); return createEndpointIFBaseProxy(wsepr,portName,portInterface,features, spi); } |
3. 这行代码,会调用到 com.sun.xml.ws.clientWSServiceDelegate 的
/** * Creates a new pipeline for the given port name. */ private Tube createPipeline (PortInfo portInfo, WSBinding binding) { //Check all required WSDL extensions are understood checkAllWSDLExtensionsUnderstood(portInfo,binding); SEIModel seiModel = null ; if (portInfo instanceof SEIPortInfo) { seiModel = ((SEIPortInfo)portInfo). model ; } BindingID bindingId = portInfo. bindingId ; TubelineAssembler assembler = TubelineAssemblerFactory.create( Thread. currentThread ().getContextClassLoader(), bindingId) ; if (assembler == null ) throw new WebServiceException( "Unable to process bindingID=" + bindingId); // TODO : i18n return assembler.createClient( new ClientTubeAssemblerContext( portInfo. targetEndpoint , portInfo. portModel , this , binding, container ,((BindingImpl)binding).createCodec(),seiModel)); } |
在这段代码中,使用了 TUBE 技术,把客户端和服务器之间建立了关系。
4. 这行代码,会调用到 com.sun.xml.ws.util.pipe .StandaloneTubeAssembler 的
@NotNull public Tube createClient(ClientTubeAssemblerContext context) { Tube head = context.createTransportTube(); head = context.createSecurityTube(head); if ( dump ) { // for debugging inject a dump pipe. this is left in the production code, // as it would be very handy for a trouble-shooting at the production site. head = context.createDumpTube( "client" , System. out , head); } head = context.createWsaTube(head); head = context.createClientMUTube(head); head = context.createValidationTube(head); return context. createHandlerTube (head); }
|
在以上代码中,开始和 SOAP 绑定,准备发送请求和调用函数等等。
3. 3 客 户 端是如何 发送请求的 ?
1. 首先,我们看到客户端程序中有以下这样一行
port.getLastTradePrice(args[0]) |
2. 这行代码,会调用到 com.sun.xml.ws.client.sei.SEIStub 的
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { MethodHandler handler = methodHandlers .get(method); if (handler != null ) { return handler.invoke(proxy, args); } else { // we handle the other method invocations by ourselves try { return method.invoke( this , args); } catch (IllegalAccessException e) { // impossible throw new AssertionError(e); } catch (IllegalArgumentException e) { throw new AssertionError(e); } catch (InvocationTargetException e) { throw e.getCause(); } } } |
3. 这行代码,会调用到 com.sun.xml.ws.client. s tub 的 ( 中间省去了 2 层不重要的代码 )
/** * Passes a message to a pipe for processing. * <p> * Unlike {@link Tube} instances, * this method is thread - safe and can be invoked from * multiple threads concurrently. * * @param packet The message to be sent to the server * @param requestContext The {@link RequestContext} when this invocation is originally scheduled. * This must be the same object as {@link #requestContext} for synchronous * invocations, but for asynchronous invocations, it needs to be a snapshot * captured at the point of invocation, to correctly satisfy the spec requirement. * @param receiver Receives the {@link ResponseContext} . Since the spec requires * that the asynchronous invocations must not update response context, * depending on the mode of invocation they have to go to different places. * So we take a setter that abstracts that away. */ protected final Packet process(Packet packet, RequestContext requestContext, ResponseContextReceiver receiver) { configureRequestPacket(packet, requestContext); Pool<Tube> pool = tubes ; if (pool == null ) throw new WebServiceException( "close method has already been invoked" ); // TODO : i18n
Fiber fiber = engine . createFiber (); // then send it away! Tube tube = pool.take();
try { return fiber.runSync(tube, packet); } finally { // this allows us to capture the packet even when the call failed with an exception. // when the call fails with an exception it's no longer a 'reply' but it may provide some information // about what went wrong.
// note that Packet can still be updated after // ResponseContext is created. Packet reply = (fiber.getPacket() == null ) ? packet : fiber.getPacket(); receiver.setResponseContext( new ResponseContext(reply));
pool.recycle(tube); } } |
4. 这行代码,会调用到 com.sun.xml.ws.api.pipe . __doRun 的 ( 中间省去了 3 层不重要的代码 )
这段代码开始发送打成数据包的 http 请求
/** * To be invoked from {@link #doRun(Tube)} . * * @see #doRun(Tube) */ private Tube __doRun(Tube next) { final Fiber old = CURRENT_FIBER .get(); CURRENT_FIBER .set( this );
// if true, lots of debug messages to show what's being executed final boolean traceEnabled = LOGGER .isLoggable(Level. FINER );
try { while (!isBlocking() && ! needsToReenter ) { try { NextAction na; Tube last; if ( throwable != null ) { if ( contsSize ==0) { // nothing else to execute. we are done. return null ; } last = popCont(); if (traceEnabled) LOGGER .finer(getName()+ ' ' +last+ ".processException(" + throwable + ')' ); na = last.processException( throwable ); } else { if (next!= null ) { if (traceEnabled) LOGGER .finer(getName()+ ' ' +next+ ".processRequest(" + packet + ')' ); na = next.processRequest( packet ); last = next; } else { if ( contsSize ==0) { // nothing else to execute. we are done. return null ; } last = popCont(); if (traceEnabled) LOGGER .finer(getName()+ ' ' +last+ ".processResponse(" + packet + ')' ); na = last.processResponse( packet ); } } ~省略~ |
5. 这行代码,会调用到 com.sun.xml.ws. encoding . StreamSOAPCodec 的 ( 中间省去了无数层不重要的代码 ) 的
public ContentType encode(Packet packet, OutputStream out) { if (packet.getMessage() != null ) { XMLStreamWriter writer = XMLStreamWriterFactory. create (out); try { packet.getMessage().writeTo(writer); writer.flush(); } catch (XMLStreamException e) { throw new WebServiceException(e); } XMLStreamWriterFactory. recycle (writer); } return getContentType(packet. soapAction ); } |
大家可以看到,他发出请求了。
3. 4 服务器端 是如何 接受请求的 ?
1. 首先,服务器端有一个名叫 JAXWSServlet 的 Servlet 常驻服务器,监听请求。所以,请求会首先被转发给 com.sun.enterprise.webservice.JAXWSServlet 的
protected void doPost (HttpServletRequest request, HttpServletResponse response) throws ServletException ,IOException{ /** * This requirement came from the jbi team. If the WebServiceEndpoint * is a jbi endpoint which is private throw an error whenever a get * or a post request is made */ Endpoint endpt = wsEngine_ .getEndpoint(request.getServletPath()); ~省略~ |
2. 最后,代码会调转到以下 com.sun.xml.ws.transport.http.HttpAdapter 的
final class HttpToolkit extends Adapter.Toolkit { public void handle (WSHTTPConnection con) throws IOException { boolean invoke = false ; try { Packet packet = new Packet(); try { packet = decodePacket(con, codec ); invoke = true ; } catch (ExceptionHasMessage e) { LOGGER .log(Level. SEVERE , "JAXWS2015: An ExceptionHasMessage occurred. " + e.getMessage(), e); packet.setMessage(e.getFaultMessage()); } catch (UnsupportedMediaException e) { LOGGER .log(Level. SEVERE , "JAXWS2016: An UnsupportedMediaException occurred. " + e.getMessage(), e); con.setStatus(WSHTTPConnection. UNSUPPORTED_MEDIA ); } catch (Exception e) { LOGGER .log(Level. SEVERE , "JAXWS2017: A ServerRtException occurred. " + e.getMessage(), e); con.setStatus(HttpURLConnection. HTTP_INTERNAL_ERROR ); } if (invoke) { try { packet = head .process(packet, con.getWebServiceContextDelegate(), packet. transportBackChannel ); } catch (Exception e) { LOGGER .log(Level. SEVERE , "JAXWS2018: An Exception occurred. " + e.getMessage(), e); if (!con.isClosed()) { writeInternalServerError(con); } return ; } } encodePacket(packet, con, codec ); } finally { if (!con.isClosed()) { con.close(); } } } } |
以上代码分为三块大的处理,分别是
packet = decodePacket(con, codec );
packet = head .process(packet, con.getWebServiceContextDelegate(),
packet. transportBackChannel );
encodePacket(packet, con, codec );
用来解析数据包,调用服务,发送回复数据包。这里就不详细介绍了。
4. 总结
希望能够通过这样的文章,更清晰的,直白的把WebService的详细技术内幕公布给大家,为开源软件运动作一份自己的贡献。
## 以上 ##