前言

在互联网开发领域,基于 JSON 的 RESTful API 早已成为绝对的主流。然而,只要你走进任何一家医院的 IT 信息中心,你就会发现,在这片极其封闭且高度强调稳定性的“围墙”内, WebService(基于 SOAP 协议) 依然是异构系统集成的绝对霸主。

无论是 HIS(医院信息系统)与 EMR(电子病历系统)的对接、LIS(实验室信息系统)的检验结果回传、PACS(医学影像系统)的报告调取,还是社保/医保接口、三方支付网关的联调,WebService 都扮演着核心的传输纽带。在医院建设集成平台(ESB)或实施互联互通测评时,WebService 更是被用于承载成百上千个核心业务接口。

这篇文章将带你深入探究:为什么医院依然大量使用 WebService?如何一行行读懂复杂的 WSDL 文档?SOAP 1.1 与 1.2 有何本质区别?不同开发语言如何实现高效对接,以及如何解决现场调试中那些令人抓狂的 SSL、乱码、CDATA 兼容性巨坑?


一、 为什么医院信息化依然钟爱 WebService?

在解答“是什么”之前,我们必须理解“为什么”。医院信息化有其极其独特的历史局限性和对稳定性的近乎偏执的要求:

1
2
3
4
5
6
7
8
9
10
11
12
                              +--------------------+
| 医院信息集成平台 |
| (ESB / ESB 网关) |
+--------------------+
^ ^ ^
+-----------------------+ | +-----------------------+
| WebService (SOAP) | WebService (SOAP) | WebService (SOAP)
v v v
+-------------+ +-------------+ +-------------+
| HIS 系统 | | LIS 系统 | | PACS 系统 |
| (C#/.NET) | | (Delphi/C++)| | (C++/C#) |
+-------------+ +-------------+ +-------------+
  1. 异构系统的历史包袱重
    医院的系统并不是一次性建成的。HIS、LIS、PACS 往往由不同的软件厂商(如东华、东软、卫宁、万达、创业慧康等)在不同时代、使用不同的编程语言(Java, C#, Delphi, C++ 甚至早期 VB)开发。WebService 诞生于 20 世纪末,具有极强的跨平台、跨语言特性,天然契合这套错综复杂的“万国牌”架构。
  2. 强类型契约约束(安全性与严谨性)
    在医疗场景中,数据出错可能危及生命(例如患者姓名错误、药量单位写错)。RESTful API 常使用动态的 JSON,字段缺漏或类型变更很难在编译期发现。而 WebService 通过 WSDL (Web Services Description Language) 文件定义了一套强类型的接口契约。方法的入参、出参、字段类型都在 WSDL 中被严格规范(Schema),客户端必须完全匹配才能通过校验。
  3. 基于集成平台(ESB)的规范要求
    在国家“医疗健康信息互联互通标准化成熟度测评”的推动下,医院广泛建设 ESB 集成平台。互联互通规范定义了大量的服务标准(如统一患者注册、预约挂号服务),这些服务交互格式(如 HL7 V3 报文)全部基于 XML 和 SOAP 包装。WebService 作为 ESB 的标准接入协议,是各大厂商对接的标准方式。

二、 WSDL 文档结构详解:如何看懂“服务说明书”?

当外部厂商向你提供一个 WebService 时,通常只会给你一个以 ?wsdl 结尾的 URL(例如:http://192.168.1.100:8080/his/PatientService?wsdl)。在浏览器中打开这个 URL,你会看到一个庞大且看似杂乱无章的 XML 文档。

不要慌,一个标准的 WSDL 文档自底向上由以下 6 个核心元素组成:

1
2
3
4
5
6
7
8
+-----------------------------------------------------------+
| <wsdl:definitions> |
| ├─ <wsdl:types> --> 定义传输的数据结构 (XSD) |
| ├─ <wsdl:message> --> 定义请求/响应报文的具体参数 |
| ├─ <wsdl:portType> --> 定义接口的方法 (Operations) |
| ├─ <wsdl:binding> --> 定义传输协议与编码格式 (SOAP) |
| └─ <wsdl:service> --> 定义服务访问的 URL 网络端点 |
+-----------------------------------------------------------+

2.1 <wsdl:types>

定义了所有在消息中传输的数据结构。它通常包含一个 XML Schema (xs:schema),里面定义了每个参数的类型(如 string、int)和结构(如复杂对象 ComplexType)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<wsdl:types>
<s:schema elementFormDefault="qualified" targetNamespace="http://tempuri.org/">
<!-- 定义了 GetPatientInfo 请求的参数结构 -->
<s:element name="GetPatientInfo">
<s:complexType>
<s:sequence>
<s:element minOccurs="0" maxOccurs="1" name="patientId" type="s:string" />
</s:sequence>
</s:complexType>
</s:element>
<!-- 定义了 GetPatientInfo 响应的返回结构 -->
<s:element name="GetPatientInfoResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="0" maxOccurs="1" name="GetPatientInfoResult" type="s:string" />
</s:sequence>
</s:complexType>
</s:element>
</s:schema>
</wsdl:types>

2.2 <wsdl:message>

定义了具体方法输入(Input)和输出(Output)的数据参数。每个 message 包含一个或多个 <wsdl:part>,对应具体的方法参数。

1
2
3
4
5
6
7
8
<!-- 请求消息 -->
<wsdl:message name="GetPatientInfoSoapIn">
<wsdl:part name="parameters" element="tns:GetPatientInfo" />
</wsdl:message>
<!-- 响应消息 -->
<wsdl:message name="GetPatientInfoSoapOut">
<wsdl:part name="parameters" element="tns:GetPatientInfoResponse" />
</wsdl:message>

2.3 <wsdl:portType>

这是 WebService 的“接口类”(Interface)。它将 message 组合成具体的操作(Operations),即定义了该 Service 暴露了哪些方法。

1
2
3
4
5
6
<wsdl:portType name="PatientServiceSoap">
<wsdl:operation name="GetPatientInfo">
<wsdl:input message="tns:GetPatientInfoSoapIn" />
<wsdl:output message="tns:GetPatientInfoSoapOut" />
</wsdl:operation>
</wsdl:portType>

2.4 <wsdl:binding>

定义了具体的通信协议和数据格式。它指定了该操作是通过 HTTP 还是 JMS 传输,以及使用哪种版本的 SOAP(SOAP 1.1 还是 SOAP 1.2)。

1
2
3
4
5
6
7
8
9
10
11
12
13
<wsdl:binding name="PatientServiceSoap" type="tns:PatientServiceSoap">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http" />
<wsdl:operation name="GetPatientInfo">
<!-- SOAPAction 标头,SOAP 1.1 中极为重要 -->
<soap:operation soapAction="http://tempuri.org/GetPatientInfo" style="document" />
<wsdl:input>
<soap:body use="literal" />
</wsdl:input>
<wsdl:output>
<soap:body use="literal" />
</wsdl:output>
</wsdl:operation>
</wsdl:binding>

2.5 <wsdl:service>

定义了服务的具体物理访问地址(Endpoint)。客户端代理正是通过这个地址发送实际的 HTTP 请求的。

1
2
3
4
5
6
<wsdl:service name="PatientService">
<wsdl:port name="PatientServiceSoap" binding="tns:PatientServiceSoap">
<!-- 客户端发送请求的实际 URL -->
<soap:address location="http://192.168.1.100:8080/his/PatientService" />
</wsdl:port>
</wsdl:service>

三、 SOAP 1.1 与 SOAP 1.2 报文级对比

在对接医院系统时,经常会遇到 “SOAP版本不兼容” 的错误。我们通过对比两个版本请求的完整 HTTP 报文来观察差异:

3.1 SOAP 1.1 请求报文示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /his/PatientService HTTP/1.1
Host: 192.168.1.100:8080
Content-Type: text/xml; charset=utf-8
Content-Length: 382
SOAPAction: "http://tempuri.org/GetPatientInfo"

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetPatientInfo xmlns="http://tempuri.org/">
<patientId>PAT20260625001</patientId>
</GetPatientInfo>
</soap:Body>
</soap:Envelope>

3.2 SOAP 1.2 请求报文示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /his/PatientService HTTP/1.1
Host: 192.168.1.100:8080
Content-Type: application/soap+xml; charset=utf-8; action="http://tempuri.org/GetPatientInfo"
Content-Length: 368

<?xml version="1.0" encoding="utf-8"?>
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
<soap12:Body>
<GetPatientInfo xmlns="http://tempuri.org/">
<patientId>PAT20260625001</patientId>
</GetPatientInfo>
</soap12:Body>
</soap12:Envelope>

🔑 核心技术差异总结

  1. 命名空间不同:SOAP 1.1 的 Envelope 命名空间是 http://schemas.xmlsoap.org/soap/envelope/,而 SOAP 1.2 是 http://www.w3.org/2003/05/soap-envelope
  2. Content-Type 不同:1.1 版本是 text/xml,而 1.2 版本是 application/soap+xml
  3. SOAPAction 存放位置:1.1 需要在 HTTP Header 中独立设置 SOAPAction;而 1.2 则将 action 作为 Content-Type 标头的参数属性进行传递。
  4. 错误节点不同:当调用出错时,1.1 内部为 <faultcode><faultstring>,1.2 升级为更加规范的 <Code><Reason> 结构。

四、 跨语言集成实战代码

在实际开发中,我们通常有两类调用方式:原生 HTTP 直接构造报文发送,以及解析 WSDL 自动生成客户端代码

4.1 Python 实战:zeep 库与 requests 库双选方案

方案 A:使用 zeep 库自动化调用(首选)

zeep 是 Python 中最优秀的 WebService 客户端库,能够解析 WSDL 并自动生成对应的方法映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# pip install zeep requests
from zeep import Client
from zeep.transports import Transport
import requests

# 忽略 SSL 证书校验(很多医院的测试环境证书是自签名的)
session = requests.Session()
session.verify = False
transport = Transport(session=session)

wsdl_url = "http://192.168.1.100:8080/his/PatientService?wsdl"
client = Client(wsdl=wsdl_url, transport=transport)

# 直接调用对应的方法名
try:
result = client.service.GetPatientInfo(patientId="PAT20260625001")
print(f"患者姓名: {result}")
except Exception as e:
print(f"调用失败: {e}")

方案 B:使用 requests 纯手工发送 XML 报文(当 WSDL 不规范时)

有些医院的 WSDL 结构极其混乱,导致 zeep 报错。此时直接用 requests 库当成普通 POST 接口发报文,反而是最快、最稳妥的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests

url = "http://192.168.1.100:8080/his/PatientService"
# 针对 SOAP 1.1 手写 Header
headers = {
"Content-Type": "text/xml; charset=utf-8",
"SOAPAction": "http://tempuri.org/GetPatientInfo"
}

# 手写 XML 报文
payload = """<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetPatientInfo xmlns="http://tempuri.org/">
<patientId>PAT20260625001</patientId>
</GetPatientInfo>
</soap:Body>
</soap:Envelope>"""

response = requests.post(url, data=payload.encode('utf-8'), headers=headers, verify=False)
print("HTTP状态码:", response.status_code)
print("响应报文:", response.text)

4.2 Java 实战:使用 JDK 自带 wsimport

在 Java 环境下,我们可以利用 JDK 自带的 wsimport 工具(在 Java 8/9/11 及以下版本自带,高版本可选用 CXF 独立包)直接生成强类型代码:

1
2
# 命令行执行,生成代理类文件到指定包路径
wsimport -keep -p cn.iot2045.his.client http://192.168.1.100:8080/his/PatientService?wsdl

代码中直接使用生成的类进行业务开发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import cn.iot2045.his.client.PatientService;
import cn.iot2045.his.client.PatientServiceSoap;

public class HisClientTest {
public static void main(String[] args) {
// 实例化 Service 对象
PatientService service = new PatientService();
// 获取 Port 代理对象
PatientServiceSoap soap = service.getPatientServiceSoap();

// 像调用本地方法一样,由底层自动序列化 XML
String patientInfoXml = soap.getPatientInfo("PAT20260625001");
System.out.println("接口返回内容: " + patientInfoXml);
}
}

4.3 C# / .NET 实战:通过代码直接组装发送(非代理引用)

在 .NET 中除了使用“添加服务引用”,在需要高度自定义头部和报文的场景下,可以手写 HttpWebRequest 来发送 SOAP 报文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System;
using System.IO;
using System.Net;
using System.Text;

public class WebServiceCaller {
public static string CallWebService(string url, string action, string soapEnvelope) {
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Headers.Add("SOAPAction", action);
request.ContentType = "text/xml;charset=\"utf-8\"";
request.Accept = "text/xml";
request.Method = "POST";

byte[] bytes = Encoding.UTF8.GetBytes(soapEnvelope);
request.ContentLength = bytes.Length;

using (Stream requestStream = request.GetRequestStream()) {
requestStream.Write(bytes, 0, bytes.Length);
}

using (WebResponse response = request.GetResponse()) {
using (StreamReader rd = new StreamReader(response.GetResponseStream(), Encoding.UTF8)) {
return rd.ReadToEnd();
}
}
}
}

五、 现场对接与异常排查黄金指南

在医院现场进行集成联调时,由于网络隔离、服务器版本陈旧等原因,常会出现很多棘手的故障。以下是高频问题与解决对策:

5.1 TLS 版本不兼容导致连接握手失败

  • 现象:Java 客户端抛出 javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure,或者 Python 抛出 SSLError: [SSL: UNSUPPORTED_PROTOCOL]
  • 原因:医院很多 HIS 服务端部署在古老的 Windows Server 2008 上,IIS 只支持 TLS 1.0。而你使用的 Java 11/17 或现代 Linux/Python 环境默认禁用了 TLS 1.0 和 1.1,只允许 TLS 1.2 及以上。
  • 对策
    • Java:修改安装目录下的 conf/security/java.security 文件,找到 jdk.tls.disabledAlgorithms 项,将 TLSv1TLSv1.1 从禁用列表中删去;或者在启动参数中加入 -Djdk.tls.client.protocols=TLSv1
    • Python:在 requests 中强行指定 SSL 协议版本:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      import ssl
      from requests.adapters import HTTPAdapter
      from urllib3.poolmanager import PoolManager

      class TlsAdapter(HTTPAdapter):
      def init_poolmanager(self, *args, **kwargs):
      context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) # 强指定 TLS 1.0
      kwargs['ssl_context'] = context
      return super(TlsAdapter, self).init_poolmanager(*args, **kwargs)

5.2 让人抓狂的 “XML 套 XML” (CDATA 包装)

  • 现象:WSDL 极其简单,只有一个参数 xmlData (String),对方文档说“请传入 XML 字符串”。
  • 原因:开发厂商为了规避频繁改动 WSDL 的代价,将实际的业务结构放在一个普通的 String 类型中传输。
  • 对策:你必须进行“二次封装”。将你的实际业务 XML 字符串,放入 <![CDATA[ ... ]]> 中作为值传入。CDATA 能够保证内部的 <>& 等特殊符号不被外层的 SOAP XML 解析器当成标签进行解析。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- 外层是标准的 SOAP 报文 -->
    <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Body>
    <UploadReport>
    <!-- 内部的值是被包装过的真实业务 XML 字符串 -->
    <xmlData><![CDATA[<Report><Id>001</Id><Content>阴性</Content></Report>]]></xmlData>
    </UploadReport>
    </soap:Body>
    </soap:Envelope>

5.3 响应报文乱码或特殊字符截断

  • 现象:患者姓名中的生僻字乱码,或者出现 An invalid XML character... 错误,导致 XML 解析框架直接报错崩溃。
  • 原因:医院系统内部数据字符集混乱。很多老系统的数据库使用 GBK,而 WebService 服务端在输出时没有进行合法的字符过滤或编码转换。例如,XML 中包含不可见字符(如 0x00-0x08 等控制字符),这些字符在 XML 规范中是不合法的。
  • 对策
    • 强制将请求和响应编码设置为对方相同的编码格式(如 GBKGB2312)。
    • 如果使用的是原生 HTTP(requests 等),在解析前先对文本进行清洗,剔除不合法的 ASCII 控制字符:
      1
      2
      3
      import re
      # 过滤掉合法的 XML 字符集以外的非法控制字符
      clean_xml_text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f]', '', response.text)

结语

在现代软件开发中,WebService 确实显得有些累赘和笨重。但在医疗信息化、工业制造和金融异构集成领域,它以“强类型、高严谨、契约明晰”的特性稳坐泰山。作为项目开发人员或交付项目经理,理解 WebService 的底层机制并掌握核心排查方法,能让你在各类多厂商联调现场游刃有余。

下一篇,我们将详细讲解如何使用 Postman 来配置和调试复杂的 HTTP 接口以及 WebService/SOAP 报文,敬请期待!