Golang实现SOAP客户端的经验总结

引言

SOAP 是一种“简单”的基于 XML 的协议,在 10 多年前流行过一阵。与现在行其道的基于 JSON 协议的 restFul API 相比,SOAP 无疑要笨重许多。

如果项目的开发语言是.net 或者 Java,那还好还,因为 IDE 有方便的内置功能来解析 SOAP 服务的 WSDL 文件,自动生成 Client 的相关代码。但如果是用 Golang,那就麻烦了。

下面说说 SOAP 的不同之处,或者说麻烦的地方。

SOAP 的不同之处

  • Request/Response 结构复杂,嵌套层级多且深

下面是一个 Request Body 的示例,这个 XML 是必须指定特定的 namespace 的。API 的验证密钥信息,也是要写在 Request Body 中。

<soapenv:Envelope xmlns:ns="http://www.id3global.com/ID3gWS/2013/04" xmlns:soap="soap" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
   <soapenv:Header xmlns:wsa="http://www.w3.org/2005/08/addressing">
      <wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
         <wsse:UsernameToken>
            <wsse:Username></wsse:Username>
            <wsse:Password></wsse:Password>
         </wsse:UsernameToken>
      </wsse:Security>
   </soapenv:Header>
   <soapenv:Body>
   </soapenv:Body>
</soapenv:Envelope>

通常soapenv:Body部分通常是会更加复杂的,如果没有工具支持,纯手工来写,累人不说,还容易出错。

  • Header 的设置
Content-Type: text/xml; charset="utf-8"
SOAPAction: http://www.id3global.com/ID3gWS/2013/04/IGlobalAuthenticate/AuthenticateSP

header 中的Content-Typeapplication/json, 可不要下意识的写成application/json哦。而SOAPAction这个 header 就是特别注意了,需要设置成要调用的 action,而且需要注意的是,不同的 action 的 baseUrl 可能是不一样的。相比而言,restFul 的 API 可没这么多坑。

  • multiple part 的 reponse

虽说大部分时候,SOAP 的返回数据是 XML 格式,但有时会是如下这种 multiple part 的格式,解析起来也是需要些功夫的。

--uuid:6d335978-5c93-40b7-b5c4-3acc067e3d8c+id=302
Content-ID: <http://tempuri.org/0>
Content-Transfer-Encoding: 8bit
Content-Type: application/xop+xml;charset=utf-8;type="text/xml"

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header><o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"></s:Header>
<s:Body></s:Body>
</s:Envelope>
--uuid:6d335978-5c93-40b7-b5c4-3acc067e3d8c+id=302--
  • 时间的反序列化与 Golang 的 time.Time 类型

Golang 的 time.Time 类型在 unmarshal 时是需要 time zone 的信息的,简单来说就是结尾至少要有个 Z,例:2021-09-23T14:55:43.835Z。如果没有 time zone 信息,如:2024-07-13T00:00:00,那么,或者你不用 time.Time 而用 string,再或者就是重写UnmarshalXML的方法了。

解决方案

写了这么多,你也会发现,如果没有现成的工具,自己要处理的细节不是一般的多。

本着技术领域没有新问题,或者说你永远不会是第一个遇到这个问题的人,我开始搜索之路,不得不说搜索还是要 google!

我通过 medium 上的一篇文章,找到心仪的工具, gowsdl,你可以用这个工具方便的生成模板代码:

gowsdl [options] myservice.wsdl

但是呢,我不建议你直接使用生成的代码,而只是使用它定义的结构,自己再重新封装一个 client,原因是:

  1. 通常一个 wsdl 里定义的 action 特别多,而你只需要用其中一两个 action
  2. 再则,生成的代码关于时间字段默认使用的还是time.Time,建议还是自己手动改成 gowsdl 中定义好的XSDDateTime。想想也是好笑,为啥不直接用 XSDDateTime 呢。

总体来说,生成的代码也是有很多优点的:

  1. 可以识别出 Enum,会生成特定的类型及值。
  2. 可以识别出每个 action 对应的SOAPAction的 http header

最后,再提一点,不知道是我集成的第三方服务的 SOAP 协议特别古老了,还是这个库有 bug,总之,是在处理 multiple part 的 response 时,有一些检查的逻辑没过,我是提了一个PR,虽说目前还没人处理,但确实有些 tricky。

在这种情况,就涉及到另一个问题,在用 go mod 管理第三方包的时候,如何 hotfix 在引用包中发现的问题?

其实,主要是可以分 3 步:

  1. 在 github 上,fork 一个自己的 repo
  2. 改动相关代码,然后 Push 一个新的 tag
  3. 但因为这个 Module 的引用名称还要保持原来的名字,如果用自己克隆的 repo 的,就会遇到下面这个错误
go: github.com/ksloveyuan/gowsdl@v0.5.1: parsing go.mod:
	module declares its path as: github.com/hooklift/gowsdl
	        but was required as: github.com/ksloveyuan/gowsdl

我没深入调查原因,但感觉是和 go.mod 有关。还是那句话,你不会是第一个遇到这个问题人,解决方法是使用 go.mod 中的 replace 功能,在 go.mod 文件的最后,加入下面这行就可以修复这个错误了

replace github.com/hooklift/gowsdl v0.5.0 => github.com/ksloveyuan/gowsdl v0.5.1