在C#中从SOAP WSA响应的二进制附件提取文件的最优方法是什么?
问题原因
- 你当前的实现存在两个核心错误:
- 方法声明返回
HttpWebResponse但实际返回字符串,属于逻辑笔误 - 用
StreamReader读取包含二进制附件的响应流:StreamReader是文本处理工具,会强制按字符编码转换字节数据,导致二进制ZIP附件被转码损坏,完全无法用该方式处理带二进制内容的响应
- 方法声明返回
- 你拿到的是*SOAP MTOM(消息传输优化机制)*规范的
multipart/related类型响应,属于多段结构,分别包含SOAP XML段和二进制附件段,需要先解析多段结构再提取附件。
最优实现方案
推荐优先使用HttpClient + 内置MultipartContent解析的方案,无需手动处理边界逻辑,稳定性最高:
// 建议将HttpClient声明为静态单例,不要每次请求都新建 private static readonly HttpClient _httpClient = new HttpClient(); public async Task<byte[]> ExtractZipAttachmentFromSoapResponse(string xmlRequest, string soapAction) { // 构造请求内容 var content = new StringContent(xmlRequest, Encoding.UTF8, "text/xml"); content.Headers.Add("SOAPAction", soapAction); content.Headers.Add("Use", "literal"); content.Headers.Add("Parameters-style", "Wrapped"); // 发送请求 using var response = await _httpClient.PostAsync(_endPoint, content); response.EnsureSuccessStatusCode(); // 解析多段内容 var multipart = await response.Content.ReadAsMultipartAsync(); // 遍历所有段,找到ZIP附件段 foreach (var part in multipart.Contents) { if (part.Headers.ContentType?.MediaType == "application/zip") { // 直接读取附件二进制内容 return await part.ReadAsByteArrayAsync(); } } // 未找到附件时返回空或抛出异常 return Array.Empty<byte>(); }
拿到ZIP二进制数组后,直接写入文件即可:
var zipBytes = await ExtractZipAttachmentFromSoapResponse(yourXml, yourSoapAction); if (zipBytes.Length > 0) { File.WriteAllBytes(@"C:\保存路径\附件.zip", zipBytes); }
兼容旧HttpWebRequest的方案
如果必须保留现有HttpWebRequest的实现,可按以下步骤手动解析:
- 从响应头
Content-Type中提取边界字符串 - 完整读取响应流为字节数组,全程不做编码转换
- 按边界切割字节数组,找到
Content-Type: application/zip对应的段 - 跳过段头的空行,剩余内容就是ZIP的二进制数据
代码示例:
public byte[] ExtractZipWithHttpWebRequest(string xmlRequest, string soapAction) { HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(_endPoint); webRequest.ServicePoint.Expect100Continue = true; webRequest.Headers.Add("Use", "literal"); webRequest.Headers.Add("Parameters-style", "Wrapped"); webRequest.ContentType = "text/xml; charset=utf-8"; webRequest.Method = "POST"; webRequest.KeepAlive = true; webRequest.Accept = "*/*"; webRequest.Headers.Add("SOAPAction", soapAction); var content = Encoding.UTF8.GetBytes(xmlRequest); webRequest.ContentLength = content.Length; using (var stream = webRequest.GetRequestStream()) { stream.Write(content, 0, content.Length); } using (var response = (HttpWebResponse)webRequest.GetResponse()) using (var responseStream = response.GetResponseStream()) using (var ms = new MemoryStream()) { // 完整读取响应流为字节数组 responseStream.CopyTo(ms); var allBytes = ms.ToArray(); // 提取边界 var contentType = response.Headers["Content-Type"]; var boundary = contentType.Split(';') .First(s => s.Trim().StartsWith("boundary=")) .Split('=')[1] .Trim('"'); var boundaryBytes = Encoding.ASCII.GetBytes($"--{boundary}"); // 查找ZIP段位置 var zipHeaderBytes = Encoding.ASCII.GetBytes("Content-Type: application/zip"); int zipHeaderPos = IndexOf(allBytes, zipHeaderBytes); if (zipHeaderPos < 0) return Array.Empty<byte>(); int emptyLinePos = IndexOf(allBytes, new byte[] { 13, 10, 13, 10 }, zipHeaderPos); if (emptyLinePos < 0) return Array.Empty<byte>(); int zipStartPos = emptyLinePos + 4; // 找到下一个边界的位置作为ZIP结束位置 int nextBoundaryPos = IndexOf(allBytes, boundaryBytes, zipStartPos); int zipLength = nextBoundaryPos - zipStartPos; var zipBytes = new byte[zipLength]; Array.Copy(allBytes, zipStartPos, zipBytes, 0, zipLength); return zipBytes; } } // 辅助方法:在字节数组中查找指定序列的位置 private int IndexOf(byte[] source, byte[] pattern, int startIndex = 0) { for (int i = startIndex; i <= source.Length - pattern.Length; i++) { bool match = true; for (int j = 0; j < pattern.Length; j++) { if (source[i + j] != pattern[j]) { match = false; break; } } if (match) return i; } return -1; }
注意事项
- 所有包含二进制附件的响应处理,都必须在字节流/字节数组层面完成,绝对不要使用字符串类、文本读取类做转换
- 解析多段结构时注意区分普通边界前缀
--边界值和结束边界后缀--边界值-- - 如果SOAP响应的附件做了Base64编码,可直接从SOAP Body中提取Base64字符串再转字节数组即可,不需要解析多段结构
内容的提问来源于stack exchange,提问作者SLS
相关产品推荐
相关产品推荐

