一次“.NET Framework 2.0 程序基础连接已经关闭”的完整排查

一次“.NET Framework 2.0 程序基础连接已经关闭”的完整排查

_

最近遇到一个比较典型、但排查过程比较曲折的问题:

一台 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,业务功能恢复正常。

这也是本次排查最重要的最终验证结果。


十五、这次排查最终确认了什么?

这次可以确认的事实是:

检查项目

结果

Nginx HTTPS

正常

Nginx → IIS

正常

Windows → IIS

正常

Windows → HTTPS

正常

TLS 1.2

正常

TLS 1.3

正常

HTTPS POST

正常

IIS

正常

C# .NET Framework 2.0 程序

修复前异常

TLS 环境配置调整后

原程序请求成功

因此,最终可以把问题范围定位到:

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、运行时环境与服务端配置之间的兼容性问题,往往比业务代码本身更值得优先排查。

在 Windows 上让 Claude CLI 安心写代码:项目 Node 保持 14.17.1 2026-09-01

评论区