最近遇到一个比较典型、但排查过程比较曲折的问题:
一台 Windows 服务器上的 C# 程序调用 HTTPS API 时,出现:
System.Net.WebException: 基础连接已经关闭: 发送时发生错误。
---> System.IO.IOException: 从传输流收到意外的 EOF 或 0 个字节。
最终通过逐层排查网络、Nginx、IIS、TLS 以及 Windows/.NET Framework 环境,最后调整 TLS 相关配置并重新验证,原来的 C# 程序已经可以正常请求 API,功能恢复正常。
这篇文章记录整个真实排查过程。
一、实际环境
为了避免暴露生产环境信息,下面对 IP 和域名进行脱敏。
实际架构可以简化成:
Windows
10.10.10.80
C# 程序
.NET Framework 2.0
│
│ HTTPS
▼
203.0.113.37
│
▼
Nginx
10.10.10.11
│
│ HTTP :13014
▼
Windows IIS
10.10.10.80:13014
也就是说:
C# 程序
↓ HTTPS
公网地址
↓
Nginx
↓ HTTP
IIS
↓
ASP.NET Core API
需要特别说明:
这个 C# 程序本身目标框架是 .NET Framework 2.0。
虽然这台 Windows 服务器上安装了 .NET Framework 4.8,但不能因此认为这个程序运行在 .NET Framework 4.8 上。
这一点在排查过程中非常重要。
二、最开始的异常
程序请求 API 时出现:
System.Net.WebException: 基础连接已经关闭: 发送时发生错误。
---> System.IO.IOException: 从传输流收到意外的 EOF 或 0 个字节。
堆栈最终定位到了:
System.Net.TlsStream.Write(...)
System.Net.ConnectStream.WriteHeaders(...)
System.Net.HttpWebRequest.GetRequestStream()
以及自己的代码:
HttpPostWithFormData(...)
也就是说,异常发生在:
request.GetRequestStream();
这里。
程序使用的是比较老的 HttpWebRequest,请求方式是自己拼接 multipart/form-data。
三、第一步:先排除 Nginx 自己的问题
首先直接在 Nginx 所在服务器上请求 HTTPS 地址:
curl -vk --connect-timeout 10 https://uat-api.example.com/
返回:
HTTP/2 200
说明:
Nginx
HTTPS
证书
TLS
至少从 Nginx 服务器自身访问来看是正常的。
所以暂时没有理由认为 HTTPS 服务本身已经挂掉。
四、第二步:测试 Nginx 到 IIS
然后直接从 Nginx 服务器请求 IIS:
curl -v --connect-timeout 10 http://10.10.10.80:13014/
返回:
HTTP/1.1 200
Server: Microsoft-IIS/10.0
说明:
Nginx → IIS
这一段也是正常的。
所以后端 IIS 并没有完全无法访问。
五、第三步:直接在发生问题的 Windows 上测试 HTTPS
接下来最关键。
因为真正报错的是 Windows 上的 C# 程序,所以直接在这台 Windows 上测试:
curl.exe -vk https://uat-api.example.com/
结果:
HTTP 200
这说明:
Windows
↓
公网 HTTPS
↓
Nginx
↓
IIS
整个链路实际上可以正常访问。
因此问题开始从:
“服务器/API 有问题”
逐渐缩小到:
“为什么 curl 可以,而这个 .NET Framework 2.0 程序不可以?”
六、第四步:分别测试 TLS 1.2 和 TLS 1.3
为了进一步排除 TLS 版本问题,分别进行测试。
TLS 1.2
curl.exe -vk --tlsv1.2 --tls-max 1.2 https://uat-api.example.com/
结果:
HTTP 200
正常。
TLS 1.3
curl.exe -vk --tlsv1.3 --tls-max 1.3 https://uat-api.example.com/
结果同样:
HTTP 200
正常。
因此这次没有继续修改 Nginx:
ssl_protocols TLSv1.2 TLSv1.3;
因为实际测试已经证明 TLS 1.2 和 TLS 1.3 都能够正常建立 HTTPS 请求。
七、第五步:测试 POST
因为实际程序使用的是 POST,所以继续测试 POST。
curl.exe -vk -X POST https://uat-api.example.com/ -d "test=123"
虽然返回:
HTTP 400
但这个结果反而是有意义的。
因为 / 并不是实际业务 POST API。
重要的是从 curl 输出可以看到:
> POST /
> Content-Length: 8
>
* upload completely sent off
也就是说:
POST 请求已经成功经过 TLS,并把请求数据发送到了服务器。
TLS 1.2:
curl.exe -vk --tlsv1.2 --tls-max 1.2 `
-X POST https://uat-api.example.com/ `
-d "test=123"
同样可以发送成功。
TLS 1.3:
curl.exe -vk --tlsv1.3 --tls-max 1.3 `
-X POST https://uat-api.example.com/ `
-d "test=123"
同样可以发送成功。
所以问题并不是:
HTTPS 完全不能 POST
也不是:
TLS 1.2 无法 POST
或者:
TLS 1.3 无法 POST
八、第六步:直接访问 IIS
再测试 Windows 到 IIS:
curl.exe -v http://10.10.10.80:13014/
返回:
HTTP/1.1 200
Server: Microsoft-IIS/10.0
因此 IIS 本身也是正常的。
到这里,基本可以确定:
Nginx 正常
Nginx → IIS 正常
Windows → IIS 正常
Windows → HTTPS 正常
TLS 1.2 正常
TLS 1.3 正常
HTTPS POST 正常
但:
.NET Framework 2.0 C# 程序
↓
HttpWebRequest
↓
GetRequestStream()
↓
仍然报错
问题范围已经明显缩小到了程序自身使用的 .NET Framework 网络/TLS 环境。
九、检查 Windows 上安装的 .NET Framework
检查 Windows:
Get-ItemProperty `
'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' |
Select-Object Release, Version
结果:
Release Version
------- -------
528449 4.8.04161
说明系统安装了:
.NET Framework 4.8
但是这里需要再次强调:
这不代表发生问题的程序运行在 .NET Framework 4.8。
实际程序目标框架是:
.NET Framework 2.0
十、检查 ServicePointManager
继续检查:
[Net.ServicePointManager]::SecurityProtocol
结果:
SystemDefault
说明当前环境没有显式指定某一个 TLS 版本。
十一、检查 .NET Framework TLS 相关注册表配置
继续检查:
Get-ItemProperty `
'HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319' `
-Name SchUseStrongCrypto `
-ErrorAction SilentlyContinue
Get-ItemProperty `
'HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319' `
-Name SystemDefaultTlsVersions `
-ErrorAction SilentlyContinue
以及 32 位:
Get-ItemProperty `
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319' `
-Name SchUseStrongCrypto `
-ErrorAction SilentlyContinue
Get-ItemProperty `
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319' `
-Name SystemDefaultTlsVersions `
-ErrorAction SilentlyContinue
当时没有返回对应配置。
十二、进行 TLS 环境配置调整
因为:
Windows 上的 curl 正常
TLS 1.2 正常
TLS 1.3 正常
HTTPS POST 正常
IIS 正常
Nginx 正常
只有旧的 .NET Framework 2.0 程序异常
因此这次没有修改 C# 程序代码,而是先从 Windows/.NET Framework TLS 环境配置入手。
64 位配置:
New-ItemProperty `
-Path 'HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319' `
-Name 'SchUseStrongCrypto' `
-PropertyType DWord `
-Value 1 `
-Force
New-ItemProperty `
-Path 'HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319' `
-Name 'SystemDefaultTlsVersions' `
-PropertyType DWord `
-Value 1 `
-Force
同时设置 32 位:
New-ItemProperty `
-Path 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319' `
-Name 'SchUseStrongCrypto' `
-PropertyType DWord `
-Value 1 `
-Force
New-ItemProperty `
-Path 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319' `
-Name 'SystemDefaultTlsVersions' `
-PropertyType DWord `
-Value 1 `
-Force
这里需要保持一个准确的表述:
这一步是针对 Windows/.NET Framework TLS 环境进行的修复尝试。
因为实际程序目标框架是 .NET Framework 2.0,所以不能简单写成:
“这是 .NET Framework 2.0 的官方 TLS 修复开关。”
本次排查中,我们实际做的是调整系统上的 TLS 兼容配置,然后继续验证原程序。
十三、检查 IIS App Pool
对应 IIS App Pool:
uat_client_usa_13010
尝试:
Restart-WebAppPool "uat_client_usa_13010"
结果却出现:
You have to start stopped object before restarting it.
这说明 App Pool 当时实际上已经是:
Stopped
并不是简单的“重启失败”。
因此改为:
Start-WebAppPool "uat_client_usa_13010"
然后:
Get-WebAppPoolState "uat_client_usa_13010"
确认状态。
如果 App Pool 启动失败,可以进一步查看:
Get-WinEvent -LogName System -MaxEvents 30 |
Where-Object {
$_.ProviderName -in @("WAS","W3SVC")
} |
Select-Object TimeCreated,ProviderName,Id,LevelDisplayName,Message |
Format-List
这一步主要用于确认 IIS/WAS 是否存在另外的启动问题。
十四、最终验证
完成 TLS 相关环境调整,并处理对应应用进程后,重新执行原来的 C# API 请求。
这一次:
请求成功。
原来的异常:
基础连接已经关闭: 发送时发生错误
不再出现。
程序可以正常调用 API,业务功能恢复正常。
这也是本次排查最重要的最终验证结果。
十五、这次排查最终确认了什么?
这次可以确认的事实是:
因此,最终可以把问题范围定位到:
Windows 上旧版 .NET Framework 2.0 程序与当前 TLS/网络环境之间的兼容性问题。
但这里还需要保留一个边界:
本次没有通过 Schannel 日志、网络抓包等手段进一步证明具体是哪一个 TLS 参数导致异常。
所以不应该把文章写成:
“最终确定就是
SchUseStrongCrypto导致的。”
更准确的说法是:
经过调整 TLS 相关环境配置并重新验证后,原来的 .NET Framework 2.0 C# 程序恢复正常。
这就是本次排查实际得到的结论。
十六、以后遇到类似问题,可以按照这个顺序排查
以后如果再遇到:
基础连接已经关闭
发送时发生错误
意外的 EOF 或 0 个字节
TlsStream
HttpWebRequest.GetRequestStream()
可以按照下面的顺序排查:
① 服务端 HTTPS 是否正常
↓
② Nginx → IIS 是否正常
↓
③ 故障 Windows → HTTPS 是否正常
↓
④ TLS 1.2 是否正常
↓
⑤ TLS 1.3 是否正常
↓
⑥ POST 是否能够真正发送出去
↓
⑦ Windows → IIS 是否正常
↓
⑧ 检查程序实际目标 .NET Framework 版本
↓
⑨ 检查 ServicePointManager.SecurityProtocol
↓
⑩ 检查 .NET Framework TLS 相关环境配置
↓
⑪ 检查 IIS App Pool
↓
⑫ 重新执行原始 C# 请求验证
↓
⑬ 如果仍失败,再检查 Schannel/WAS 日志
这样排查的好处是:
每一步都在缩小问题范围,而不是一开始就修改 Nginx、修改 TLS、修改代码。
十七、自查与修复 PowerShell
如果以后再次遇到类似问题,可以使用下面的 PowerShell 做基础检查和配置调整。
注意:这个脚本检查的是 Windows/.NET Framework TLS 环境配置,不代表它可以直接修复所有 .NET Framework 2.0 网络问题。执行后仍然必须使用原始 C# 程序进行最终验证。
param(
[string]$AppPoolName = ""
)
$ErrorActionPreference = "Stop"
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " .NET Framework TLS 自查与修复" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# 检查管理员权限
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
if (-not $principal.IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator
)) {
Write-Host "请使用管理员权限运行 PowerShell。" -ForegroundColor Red
exit 1
}
# 检查 .NET Framework 4.x
Write-Host "[1/4] 检查 .NET Framework..." -ForegroundColor Yellow
$dotnetPath = 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full'
$dotnet = Get-ItemProperty `
$dotnetPath `
-ErrorAction SilentlyContinue
if ($dotnet) {
Write-Host " Version : $($dotnet.Version)"
Write-Host " Release : $($dotnet.Release)"
}
else {
Write-Host " 未检测到 .NET Framework 4.x" `
-ForegroundColor Red
}
Write-Host ""
# 检查并修复 TLS 配置
Write-Host "[2/4] 检查 TLS 注册表配置..." -ForegroundColor Yellow
$registryPaths = @(
'HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319'
)
foreach ($path in $registryPaths) {
Write-Host ""
Write-Host " 路径:$path"
if (-not (Test-Path $path)) {
Write-Host " 注册表路径不存在,创建中..." `
-ForegroundColor Yellow
New-Item `
-Path $path `
-Force | Out-Null
}
$config = Get-ItemProperty `
$path `
-ErrorAction SilentlyContinue
$strongCrypto = $config.SchUseStrongCrypto
$systemDefaultTls = $config.SystemDefaultTlsVersions
Write-Host " SchUseStrongCrypto = $strongCrypto"
Write-Host " SystemDefaultTlsVersions = $systemDefaultTls"
if ($strongCrypto -ne 1) {
Write-Host " → 设置 SchUseStrongCrypto = 1" `
-ForegroundColor Yellow
New-ItemProperty `
-Path $path `
-Name 'SchUseStrongCrypto' `
-PropertyType DWord `
-Value 1 `
-Force | Out-Null
}
if ($systemDefaultTls -ne 1) {
Write-Host " → 设置 SystemDefaultTlsVersions = 1" `
-ForegroundColor Yellow
New-ItemProperty `
-Path $path `
-Name 'SystemDefaultTlsVersions' `
-PropertyType DWord `
-Value 1 `
-Force | Out-Null
}
}
Write-Host ""
# 验证
Write-Host "[3/4] 验证 TLS 配置..." -ForegroundColor Yellow
foreach ($path in $registryPaths) {
$config = Get-ItemProperty $path
Write-Host ""
Write-Host " $path"
Write-Host " SchUseStrongCrypto = $($config.SchUseStrongCrypto)"
Write-Host " SystemDefaultTlsVersions = $($config.SystemDefaultTlsVersions)"
}
Write-Host ""
# App Pool
Write-Host "[4/4] 检查应用进程..." -ForegroundColor Yellow
if ([string]::IsNullOrWhiteSpace($AppPoolName)) {
Write-Host " 未指定 App Pool,跳过 IIS 操作."
}
else {
Import-Module WebAdministration
$pool = Get-Item `
"IIS:\AppPools\$AppPoolName" `
-ErrorAction SilentlyContinue
if (-not $pool) {
Write-Host " 未找到 App Pool:$AppPoolName" `
-ForegroundColor Red
}
else {
$state = Get-WebAppPoolState `
$AppPoolName
Write-Host " 当前状态:$($state.Value)"
if ($state.Value -eq "Started") {
Write-Host " 正在重启 App Pool..." `
-ForegroundColor Yellow
Restart-WebAppPool $AppPoolName
}
elseif ($state.Value -eq "Stopped") {
Write-Host " App Pool 当前为 Stopped,正在启动..." `
-ForegroundColor Yellow
Start-WebAppPool $AppPoolName
}
Start-Sleep -Seconds 3
$newState = Get-WebAppPoolState `
$AppPoolName
Write-Host " 最终状态:$($newState.Value)" `
-ForegroundColor Green
}
}
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " 自查/修复操作完成" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "请重新执行原来的 C# API 请求进行验证。" `
-ForegroundColor Yellow
Write-Host ""
十八、最后总结
这次问题最开始看起来像是:
HTTPS 请求失败
但实际一步一步排查后发现:
Nginx 正常
IIS 正常
HTTPS 正常
TLS 1.2 正常
TLS 1.3 正常
POST 正常
curl 正常
真正异常的是:
.NET Framework 2.0
HttpWebRequest
GetRequestStream()
最终通过调整 Windows 上的 TLS 相关环境配置,并重新处理应用进程后:
原始 C# 程序
↓
HTTPS API
↓
请求成功
↓
业务恢复正常
这次排查给我的一个比较深的体会是:
看到“基础连接已经关闭”,不要第一时间就去改 Nginx TLS,也不要直接修改业务代码。
先把链路拆开:
客户端
↓
TLS
↓
Nginx
↓
IIS
↓
API
一层一层验证。
只要能够证明某一层是正常的,就把它从怀疑范围里排除。
最后再回到真正发生异常的客户端程序本身。
对于这种运行多年的老 .NET Framework 程序来说,操作系统、TLS、运行时环境与服务端配置之间的兼容性问题,往往比业务代码本身更值得优先排查。