使用 MTOM 高效传输 SOAP 中的二进制内容

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...点击查看项目介绍 ;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;

截止目前, 星球 内专栏累计输出 63w+ 字,讲解图 2808+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2200+ 小伙伴加入学习 ,欢迎点击围观

基于 JSON 的 REST 服务很流行,但是在集成企业服务时,SOAP 仍然被广泛使用。在最近的一个项目中,我不得不编写一个基于 spring boot 的微服务,它是一种基于 Microsoft WCF 的 第三方基于 SOAP 的 Web 服务的网关。调用时,微服务从其他微服务收集一些数据,从存储系统加载 PDF 文档,并在一次 SOAP 调用中将数据和 PDF 传输到 SOAP 服务。当第一个实现完成并部署后,我检查了访问日志:哇,某些请求的请求大小非常大。好的,PDF 的大小从几 kB 到几 MB 不等,但请求似乎要大得多。这是因为 SOAP 使用 Base64 对二进制内容进行编码。而Base64将6个Bits编码成一个字符,即3个Bytes被编码成4个字符。这比原始数据多 33% 的内容。对于小内容,这不是问题,但如果您必须传输 4MB 而不是 3MB……那就是问题了。下面是一个包含二进制内容的简单 SOAP 请求示例。内容只有 30 个字节,编码为 40 个字符(为了便于阅读,XML 已经格式化):


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

MTOM

为了克服这个问题,W3C 发明并标准化了 MTOM (消息传输优化机制)。不是以 Base64 编码的字符形式提供二进制内容(作为元素文本包含在 SOAP 消息中),而是使用多部分 mime 格式来从 SOAP 消息中分离二进制内容。意思是:SOAP消息本身包含在一个部分中,二进制内容在一个单独的部分中。 SOAP 消息中的内容元素仅具有对二进制部分的引用。听起来怪怪的?看看它(同样,XML 的格式是为了便于阅读):


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

如您所见,包含二进制数据的部分仅包含 30 个字节,而不是更多。但是对于您必须支付的多部分元数据肯定会有一些开销。根据经验,MTOM 仅对 > 1 kB 的内容有意义。如果您查看内容元素,您会注意到 xop 元素:


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

虽然 MTOM 描述了 SOAP 中优化传输的抽象特征,但使用 MIME 多部分的具体实现保留在单独的规范中: XOP ,即 XML-binary Optimized Packaging。这样,它就可以独立于 SOAP 用于 XML 文档中的任何二进制内容。因此,您经常会发现 MTOM/XOP 的措辞。

示例应用程序

为了给你一个简单的例子,我 在 github 上准备了一个简单的 SOAP 服务器和客户端, 用 spring boot 实现。它们是使用 STS 构建的,但您可以仅使用纯 Java 构建和运行它们;只需检查自述文件。克隆存储库并切换到分支 base64 ,它提供了两个 spring 项目 mtom-server mtom-client 初始设置。只需按照自述文件中的描述启动服务器和客户端。客户要求您提供要上传的文档大小。如果您输入一个大小,客户端将生成一个该大小的文档(包含一些随机二进制数据),并将其上传到服务器。客户端和服务器也会跟踪请求和响应,因此您可以检查它们。

客户端控制台输出:


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

服务器控制台输出:


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

SOAP 服务的基础通常是定义类型和操作的 WSDL。在我们的例子中,我们只是在服务器项目中提供一个描述类型的模式 documents.xsd 并使用 JAXB 来创建 Java 类。 Spring 为我们创建了 WSDL 而不是动态的。只需启动服务器并浏览到 http://localhost:9090/ws/documents.wsdl 。 WSDL 也包含在 wsdl/documents.wsdl 下的资源中的客户端中。以下是 WSDL 的摘录:


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

所以我们只需要在 storeDocument 中有一个 document ,在 storeDocumentResponse storeDocumentRequest 一个布尔值。 document 本身包含一些元数据,如 name author ,最后是二进制 content

使用 MTOM

为了使用 MTOM,我们必须对示例进行一些更改。 (您可以在示例中执行这些步骤,也可以直接在分支 mtom 中查看准备好的解决方案)。首先让我们在客户端启用 MTOM,这只是在 JAXB 编组器上启用它:


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

基本上,这与我们在服务器上也必须做的一样。但我们还必须告诉 spring 为端点使用该编组器:


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

我们还必须通过提供适当的多部分解析器来添加对多部分的支持:


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

就是这样,如果你运行它,你将在客户端控制台上输出以下内容:


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>


MTOM 和流媒体

如果查看从模型生成的 Java 类,您会看到二进制内容保存在字节数组中:


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

当你处理大的二进制数据时,这是一个问题,因为完整的数据必须保存在内存中。 OutOfMemoryException 正在等待您。这个问题的解决方案是流式传输:不是将数据保存在内存中,而是以流的形式提供数据。从流中读取数据。这是很自然的,因为大多数数据存储(例如文件系统、数据库等)都提供流接口。即使 MTOM 规范只字未提流式传输,XOP 规范也指出——即使它不是强制性的——大多数实现都提供了流式传输数据的可能性。让我们现在在我们的例子中这样做。同样,您可以按照这些步骤将您的 MTOM 应用程序转换为流,或检查项目的 master 分支。大师为您提供最终版本。首先,我们需要一种方法在我们的 Java 类中提供流接口。这样做的方法是稍微更改架构。只需将属性 xmime:expectedContentTypes="application/octet-stream" 添加到内容元素:


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

现在 JAXB 为字段内容生成一个 DataHandler 而不是 byte[]


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

DataHandler 基于流,因此您可以使用 Input- 和 OutputStreams 来传递数据。这就是我们所做的;我们没有先将内容读入字节数组,而是直接将输入流传递给客户端的 DataHandler:


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

在服务器端,我们还有一个 DataHandler,我们用它来直接从 InputStream 读取数据。就是这样?试一试……嗯,还是内存不足。客户端似乎仍然先将内容读入内存。这个问题的答案是 HTTP 协议,让我们回顾一下我们的 MTOM 请求:


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

HTTP 需要预先知道content-length,所以为了计算content-length,将content 全部读入内存。为了避免这种情况,我们必须使用 分块传输编码


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

现在我们得到了吗?让我们重试... 神圣的吉娃娃,现在服务器发出呻吟声 。但它现在应该马上工作?!?这是 SAAJ 实现中的错误,请参阅 SAAJ-31 。你可以在那里读到,我们必须设置一个开关来强制 SAAJ 使用 mimepull,所以我们这样做:


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

现在它起作用了……真的。尝试使用 1.000.000.000 字节(SOAP 跟踪在 master 中被禁用,所以不用担心)。这需要一些时间,因为我们的随机数据 InputStream 会生成每个字节:


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>


WCF 中的 MTOM 和流式处理

一开始我告诉过你我的项目,在这个项目中我必须与构建在 .NET 和 WCF 上的第三方服务器产品进行通信。该产品最初没有使用 MTOM,因此我也不得不对该软件进行更改。幸运的是,在 WCF 中,这只是您必须做的 一些配置 ……而且我可以访问该配置文件;-) 在 HTTP 绑定中,您必须将属性 messageEncoding 设置为 Mtom。要使用流式传输,只需将属性 transferMode 设置为 Streamed:


 POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header /> <SOAP-ENV:Body> <ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom"> <ns2:document> <ns2:name>30</ns2:name> <ns2:author>Herbert</ns2:author> <ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content> </ns2:document> </ns2:storeDocumentRequest> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

那很容易,是吗?是的,WCF 会为您处理所有这些漂亮的小细节。

结论

MTOM 允许您有效地传输大型二进制数据,甚至允许流式传输它以避免内存问题。是的,还有其他可用的流媒体机制;但是如果您希望您的 SOAP 服务与其他服务互操作,则 MTOM 标准是您的选择。

使困惑?你不会在这一集 Soap 之后!
Soap 的播音员,我喜欢看的 70 年代后期情景喜剧 :-)


相关文章