深度解析 Openssl ssl_error_syscall
错误:原因与解决方法
在开发或维护基于 OpenSSL 的网络应用程序时,SSL_ERROR_SYSCALL
是一个开发者和系统管理员可能遇到的、令人困惑且难以排查的错误。不同于那些直接指向 SSL/TLS 协议本身的错误(如证书问题、握手失败等),SSL_ERROR_SYSCALL
并非 OpenSSL 代码本身的失败,而是 OpenSSL 在执行底层系统调用(如读写网络套接字、连接、接受连接等)时,这些系统调用返回了错误。这使得故障排除变得复杂,因为问题根源可能在于网络、操作系统、资源限制或应用程序代码本身,而不仅仅是 OpenSSL 的配置或使用方式。
本文将深入探讨 SSL_ERROR_SYSCALL
错误的本质、常见的触发原因以及一套系统的排查和解决策略。
1. SSL_ERROR_SYSCALL
错误是什么?
首先,理解 OpenSSL 的内部工作原理对于理解 SSL_ERROR_SYSCALL
至关重要。OpenSSL 是一个实现了 SSL/TLS 协议的库,它负责加密、解密、证书校验、握手协商等协议层面的工作。然而,OpenSSL 本身并不知道如何直接发送或接收网络数据包。它依赖于底层操作系统提供的网络 I/O 功能,最常见的就是通过标准套接字(socket)接口进行通信。
当您在 OpenSSL 中调用 SSL_read()
或 SSL_write()
函数来读取或写入加密数据时,OpenSSL 会在内部执行一系列操作:
1. 处理(解密或加密)应用层数据。
2. 如果需要发送,它会调用底层的系统函数,如 Unix/Linux 上的 read()
、write()
,或 Windows 上的 recv()
、send()
,将数据发送到套接字。
3. 如果需要接收,它会调用底层的系统函数,如 read()
或 recv()
,从套接字读取原始数据。
4. 接收到的数据会被 OpenSSL 处理(解密)。
SSL_get_error()
函数用于获取 OpenSSL 函数调用的返回码所对应的具体错误类型。当 SSL_read()
、SSL_write()
、SSL_connect()
、SSL_accept()
等函数返回一个表示失败的值(通常是小于等于 0 的值,具体取决于函数)时,您需要立即调用 SSL_get_error(ssl, ret)
来确定错误的性质。
SSL_ERROR_SYSCALL
就是 SSL_get_error()
可能返回的一种错误类型。它的含义是:OpenSSL 在执行某个底层的、与 I/O 相关的系统调用(如 read()
, write()
, connect()
, accept()
等)时,该系统调用返回了一个错误。
关键点在于:
SSL_ERROR_SYSCALL
本身并没有提供关于底层系统错误的具体信息。- 在
SSL_get_error()
返回SSL_ERROR_SYSCALL
后,您需要立即检查系统全局的错误指示变量(在 Unix/Linux 上是errno
,在 Windows 上是WSAGetLastError()
)来获取底层系统调用的具体错误码。这个系统错误码才是揭示问题根源的关键。
所以,当看到 SSL_ERROR_SYSCALL
时,它就像一个信封,告诉你“里面有份重要的系统错误信息”,而信封里的具体内容(即 errno
或 WSAGetLastError()
)才是你需要打开和阅读的。
2. ssl_error_syscall
的常见原因
由于 ssl_error_syscall
本质上是底层系统调用的失败,其原因多种多样,涵盖了网络、操作系统、资源以及应用程序代码的各个层面。以下是一些最常见的触发原因:
2.1 网络问题
这是导致 SSL_ERROR_SYSCALL
最常见的原因之一,特别是与套接字读写相关的 SYSCALL
错误。
- 连接重置 (
ECONNRESET
): 这是最常见的errno
值之一。它表示通信的另一端(对等方)突然关闭了连接,通常是通过发送一个 TCP RST (Reset) 分组。导致ECONNRESET
的原因很多:- 对等方应用程序崩溃或异常退出: 导致其套接字资源被操作系统清理并发送 RST。
- 对等方在接收到数据时,其应用程序没有在监听或处理该连接: 例如,服务器在处理请求时发生内部错误并崩溃,或者客户端在发送数据后立即关闭。
- 防火墙或网络设备终止了空闲连接: 许多防火墙会为了节省资源而关闭长时间不活跃的连接。当一端尝试在已关闭的连接上发送数据时,防火墙或对等方会发送 RST。
- 接收方系统过载: 可能导致内核无法处理传入的数据,发送 RST。
- 路由或 NAT 问题: 数据包无法正确到达对等方或返回。
- 管道破裂 (
EPIPE
): 通常在使用SSL_write
时发生。这意味着您正尝试写入到一个套接字,而另一端已经关闭了连接的读取端。与ECONNRESET
类似,但也可能发生在单向关闭的情况下。在某些系统中,对已关闭读端的套接字写入会导致SIGPIPE
信号,如果未被捕获或忽略,会终止进程。如果忽略了SIGPIPE
,写入操作会返回EPIPE
错误。 - 连接超时 (
ETIMEDOUT
): 通常在尝试建立连接 (SSL_connect
) 或在现有连接上进行读写时发生。这表示在预定的时间内没有收到对等方的响应。原因可能包括:- 网络拥塞或高延迟。
- 对等方服务器无响应或已关闭。
- 防火墙丢弃了数据包。
- 路由问题。
- 网络不可达 (
ENETUNREACH
,EHOSTUNREACH
): 表示客户端尝试连接的目标网络或主机当前无法通过本地路由到达。这通常是客户端在尝试SSL_connect
时遇到。 - 连接被拒绝 (
ECONNREFUSED
): 通常在客户端尝试SSL_connect
时发生。这表示目标主机的端口上没有服务在监听。也可能是在服务刚刚重启或崩溃时短暂出现。 - 套接字缓冲区满 (
ENOBUFS
): 系统没有足够的缓冲区空间来发送数据。这通常发生在发送大量数据且接收方处理缓慢,或系统资源紧张时。
2.2 操作系统和资源限制问题
- 文件描述符耗尽 (
EMFILE
,ENFILE
): 每个套接字都会占用一个文件描述符。如果应用程序或系统打开了过多的文件或套接字,超出了进程或系统的限制,后续的socket()
、accept()
等系统调用可能会失败,导致SSL_ERROR_SYSCALL
。 - 内存不足 (
ENOMEM
): 虽然不常见,但在系统内存资源极度紧张的情况下,某些系统调用(如创建新的套接字)可能会失败。 - 进程限制 (
ulimit
): 在 Unix/Linux 系统中,用户的ulimit
设置(尤其是-n
参数控制的最大文件描述符数)可能会限制进程可以打开的套接字数量。 - TCP/IP 栈配置问题: 操作系统的 TCP/IP 参数配置不当,如连接队列溢出、端口范围限制等,也可能间接导致与连接建立相关的系统调用失败。
2.3 应用程序代码问题
即使底层网络和系统正常,应用程序代码中的错误也可能导致 ssl_error_syscall
。
- 在已关闭的套接字上进行操作 (
EBADF
): 如果应用程序意外地关闭了 OpenSSL 关联的底层套接字,然后又尝试在该套接字上执行SSL_read
或SSL_write
,底层的read
/write
调用将返回EBADF
(Bad File Descriptor),从而导致SSL_ERROR_SYSCALL
。这通常是由于资源管理不当引起的。 - 多线程并发问题: 如果多个线程在没有适当同步的情况下同时访问同一个
SSL
对象或底层套接字,可能导致套接字状态混乱,一个线程可能在另一个线程关闭套接字后尝试使用它,从而引发EBADF
或其他与套接字相关的错误。 - 不恰当的非阻塞 I/O 处理: 如果应用程序使用了非阻塞套接字,但在
SSL_read
/SSL_write
返回SSL_ERROR_WANT_READ
/SSL_ERROR_WANT_WRITE
后,没有正确地使用select
、poll
、epoll
或IOCP
等机制等待套接字变为可读写,而是在套接字未准备好时反复调用读写,虽然这更可能导致无限循环或高 CPU 使用,但在某些极端情况下,如果底层read
/write
调用以某种方式失败(而不是返回EAGAIN
/EWOULDBLOCK
),也可能引发SYSCALL
。 - 错误的套接字状态: 例如,在套接字未完全连接之前就尝试读写,或者在接受连接 (
accept
) 之前就尝试在其监听套接字上执行其他操作。
2.4 SSL/TLS 协议或握手问题(间接原因)
虽然 SSL_ERROR_SYSCALL
不是直接的协议错误,但某些协议层面的问题可能 间接 导致底层套接字错误。例如:
- 握手失败后没有正确关闭连接: 如果 SSL 握手失败(例如,证书验证失败),一方决定终止连接,但没有执行优雅的 SSL 关闭(
SSL_shutdown
),而是直接关闭了底层套接字。另一方稍后尝试在认为仍然开放的连接上进行读写,就会遇到ECONNRESET
或EPIPE
,表现为SSL_ERROR_SYSCALL
。 - 接收到畸形或恶意的 SSL 记录: 如果 OpenSSL 收到一个无法解析的 SSL 记录,它可能会决定关闭连接。如果在关闭套接字之前,应用程序尝试读写,也可能触发 SYSCALL。
3. 如何排查和解决 ssl_error_syscall
错误
由于 ssl_error_syscall
的多样性,有效的故障排除需要系统化的方法。以下是一套推荐的排查步骤:
步骤 1: 捕获并记录完整的错误信息
这是最关键的第一步。仅仅知道是 SSL_ERROR_SYSCALL
是不够的。您必须捕获并记录以下信息:
- 哪个 OpenSSL 函数返回了错误? (
SSL_read
,SSL_write
,SSL_connect
,SSL_accept
等) - 调用
SSL_get_error()
后,返回码是多少? 确认确实是SSL_ERROR_SYSCALL
。 - 紧接着,底层的系统错误码是多少?
- 在 Unix/Linux 上,获取
errno
的值。在 C/C++ 中,通常在调用失败的系统函数或返回SSL_ERROR_SYSCALL
的 OpenSSL 函数 之后立即 检查全局变量errno
。最好使用strerror(errno)
或perror()
来获取可读的错误消息。 - 在 Windows 上,获取
WSAGetLastError()
的值。
- 在 Unix/Linux 上,获取
- 错误发生时的上下文: 错误发生在连接建立阶段还是数据传输阶段?是服务器端还是客户端?涉及的具体 IP 地址和端口是什么?应用程序当时正在执行什么操作?
示例代码片段 (C/C++):
“`c++
// 假设 ret 是 SSL_read 或 SSL_write 的返回值
int ssl_err = SSL_get_error(ssl, ret);
if (ssl_err == SSL_ERROR_SYSCALL) {
// 立即获取系统错误码
int sys_errno = errno; // Unix/Linux
// 或 int sys_errno = WSAGetLastError(); // Windows
fprintf(stderr, "SSL_ERROR_SYSCALL occurred.\n");
fprintf(stderr, " OpenSSL function returned: %d\n", ret);
fprintf(stderr, " System Error Code (%s): %d\n",
#ifdef _WIN32
"WSAGetLastError"
#else
"errno"
#endif
, sys_errno);
// 获取可读的系统错误消息
#ifdef _WIN32
LPSTR messageBuffer = nullptr;
size_t size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, sys_errno, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, nullptr);
fprintf(stderr, " System Error Message: %s\n", messageBuffer ? messageBuffer : "Unknown error");
LocalFree(messageBuffer);
#else
fprintf(stderr, " System Error Message: %s\n", strerror(sys_errno));
#endif
// 记录其他上下文信息...
} else if (ssl_err == SSL_ERROR_SSL) {
// 处理 SSL 协议错误
fprintf(stderr, “SSL_ERROR_SSL occurred.\n”);
unsigned long err_code;
while ((err_code = ERR_get_error()) != 0) {
char err_buf[256];
ERR_error_string(err_code, err_buf);
fprintf(stderr, ” SSL error: %s\n”, err_buf);
}
}
// … 处理其他 OpenSSL 错误类型
“`
获取到具体的系统错误码后,查阅相关的文档(如 man errno
在 Unix/Linux,或 MSDN 文档在 Windows)来理解其含义。最常见的 errno
值及其含义已在原因部分列出。
步骤 2: 分析系统错误码
根据获取到的 errno
或 WSAGetLastError()
值,结合错误发生的上下文,初步判断问题类别。
ECONNRESET
,EPIPE
,ETIMEDOUT
: 高度怀疑是网络或对等方的问题。EMFILE
,ENFILE
,ENOMEM
: 怀疑是本地资源耗尽。EBADF
: 几乎肯定是对底层套接字的操作失误,是应用程序代码的错误。ECONNREFUSED
,ENETUNREACH
,EHOSTUNREACH
: 怀疑是连接建立阶段的网络或目标地址问题。
步骤 3: 检查网络连接和环境
如果系统错误码指向网络问题(如 ECONNRESET
, ETIMEDOUT
),则需要深入检查网络环境。
- 基本连通性测试: 使用
ping
和traceroute
(或tracert
在 Windows) 检查客户端到服务器的网络可达性和路径。 - 端口可达性测试: 使用
telnet
、netcat
(nc
) 或curl
(curl <https://your_server:your_port>
) 测试到目标地址和端口的连接。先尝试非 SSL 连接(如果可能),然后尝试 SSL 连接。这有助于区分是网络路由/防火墙问题还是 SSL 层面的问题。telnet your_server your_port
nc -zv your_server your_port
curl -v <https://your_server:your_port/>
- 防火墙检查: 确认本地和远程防火墙(包括操作系统自带防火墙、网络防火墙设备)没有阻挡相关的 IP 地址、端口或协议。检查是否有配置导致空闲连接被关闭。
- 网络抓包分析: 使用 Wireshark (图形界面) 或 tcpdump (命令行) 在客户端和/或服务器端捕获通信流量。这是诊断
ECONNRESET
和ETIMEDOUT
等网络错误的“黄金标准”方法。- 捕获过滤器:
host your_peer_ip and port your_peer_port
- 分析抓包: 查找 TCP RST (Reset) 分组。是谁发送的 RST?在发送 RST 之前发生了什么?查找 TCP FIN (Finish) 分组以确定连接是否被正常关闭。查找数据包丢失、重传、乱序等迹象。如果在服务器端捕获到客户端发送的数据,但服务器没有响应,可能指向服务器应用程序问题。如果在客户端看到发送数据后长时间没有响应,可能指向网络延迟或服务器无响应。
- 捕获过滤器:
- 检查对等方状态: 如果错误是
ECONNRESET
或EPIPE
,检查对等方的应用程序日志,看是否有崩溃、异常退出或主动关闭连接的记录。确认对等方的服务正在运行且工作正常。
步骤 4: 检查系统资源限制
如果系统错误码指向资源问题(如 EMFILE
, ENFILE
, ENOMEM
),则需要检查系统资源使用情况和限制。
- 检查文件描述符限制:
- 在 Unix/Linux 上,使用
ulimit -n
查看当前进程的文件描述符限制。使用cat /proc/sys/fs/file-max
查看系统范围的限制。使用lsof -p your_pid | wc -l
查看特定进程当前打开的文件描述符数量。如果接近或超过限制,就需要调整ulimit
或系统配置,并查找应用程序是否存在文件描述符泄漏(即创建了套接字或其他文件描述符但未关闭)。 - 在 Windows 上,文件句柄限制通常远高于 Unix-like 系统,但仍然存在。可以使用任务管理器或专门的工具检查进程的句柄数。
- 在 Unix/Linux 上,使用
- 检查内存使用: 使用
free
、top
(Unix/Linux) 或任务管理器 (Windows) 查看系统的内存使用情况。如果系统内存或交换空间耗尽,可能会导致各种系统调用失败。 - 检查其他系统限制: 查看是否有其他相关的系统限制(如进程数、线程数等)可能被达到。
步骤 5: 审查应用程序代码
如果系统错误码是 EBADF
或其他看似与底层资源管理直接相关的错误,或者排除了网络和系统资源问题,则需要仔细检查应用程序的代码。
- 套接字生命周期管理: 确认套接字在被 OpenSSL 使用期间没有被应用程序意外关闭。检查是否存在多处关闭套接字的代码,或者在多线程环境中没有正确同步对套接字的访问。
- SSL 对象与套接字的关联: 确认
SSL
对象与正确的套接字相关联 (SSL_set_fd
),并且在套接字关闭之前,SSL 对象已经被清理(SSL_free
)。虽然SSL_free
通常在内部关闭套接字(取决于SSL_MODE_AUTO_RETRY
和其他设置,但更安全的是先管理套接字关闭或明确设置套接字模式),但在 SYSCALL 发生时,检查套接字状态至关重要。 - 多线程同步: 如果在多线程环境中使用 OpenSSL,确保对共享的
SSL
对象或底层套接字有适当的锁定机制(mutexes),以防止竞态条件导致状态损坏或使用已释放/关闭的资源。OpenSSL 本身在某些版本和配置下需要应用程序提供线程锁回调。 - 错误处理逻辑: 确认应用程序正确地检查了 OpenSSL 函数的返回值,并且在返回错误时调用了
SSL_get_error()
,特别是在返回表示失败的值时。更重要的是,在SSL_get_error()
返回SSL_ERROR_SYSCALL
后,要立即检查并处理系统错误码 (errno
/WSAGetLastError()
)。 - 非阻塞 I/O 处理: 如果使用了非阻塞套接字,确保在
SSL_read
/SSL_write
返回SSL_ERROR_WANT_READ
或SSL_ERROR_WANT_WRITE
时,应用程序没有立即重试,而是通过select
,poll
,epoll
,IOCP
等机制等待套接字变为可读写后再进行操作。虽然这主要与SSL_ERROR_WANT_*
相关,但处理不当可能间接影响SYSCALL
的表现。 - 优雅关闭: 在连接不再需要时,尝试执行 SSL 层的优雅关闭 (
SSL_shutdown
),然后关闭底层套接字。直接强制关闭套接字可能导致对等方收到 RST,从而在对等方触发ECONNRESET
。
6. 简化和隔离问题
如果上述步骤未能确定问题,尝试简化复现环境或代码:
- 使用简单的测试工具: 使用 OpenSSL 自带的命令行工具
openssl s_client
或openssl s_server
来与您的应用程序进行通信。如果这些工具能够正常工作,或者它们也遇到类似的错误,可以帮助缩小问题范围。例如,如果s_client
连接到您的服务器时也出现 SYSCALL 错误,问题可能更接近服务器端、服务器的网络环境或服务器应用程序本身;如果s_client
工作正常,问题可能更偏向于您自己客户端应用程序的代码或环境。 - 编写最小可复现示例: 尝试编写一个只包含 OpenSSL 初始化、连接建立、一次简单的读写以及清理的最小程序。如果在最小程序中能够重现问题,将极大地简化调试。
- 禁用部分功能: 如果可能,临时禁用应用程序中的非核心功能,看是否与特定功能有关联。
7. 检查 OpenSSL 版本和依赖项
虽然不常见,但 OpenSSL 本身或其依赖库(如 C 标准库、网络库)中的 bug 可能导致底层的系统调用出现异常。
- 更新 OpenSSL: 确保您使用的 OpenSSL 版本不是已知存在严重 bug 的旧版本。考虑升级到较新的稳定版本。
- 检查依赖库: 确认系统上的 C 标准库、网络库等是正常且兼容的。
4. 避免 ssl_error_syscall
的最佳实践
预防总是优于治疗。遵循以下最佳实践可以减少遇到 ssl_error_syscall
的几率:
- 全面的错误处理: 始终检查 OpenSSL 函数的返回值,并在返回错误时调用
SSL_get_error()
。当SSL_get_error()
返回SSL_ERROR_SYSCALL
时,务必立即获取并记录系统错误码 (errno
/WSAGetLastError()
) 及其可读描述。 - 资源管理: 确保应用程序在不再需要套接字和
SSL
对象时,能够及时、正确地释放它们。避免文件描述符泄漏。 - 正确的非阻塞 I/O 使用: 如果使用非阻塞套接字,严格遵循事件驱动模型,使用
select
,poll
,epoll
,IOCP
等机制等待 I/O 就绪,而不是在未就绪时重试。 - 多线程安全: 在多线程环境中正确同步对共享 OpenSSL 资源和套接字的访问。如果 OpenSSL 版本需要,提供必要的线程锁回调。
- 优雅关闭连接: 在可能的情况下,执行 SSL 层的优雅关闭 (
SSL_shutdown
),而不是直接强制关闭底层套接字。虽然不能保证对等方也执行优雅关闭,但您的应用程序遵循此实践有助于在某些情况下避免EPIPE
或ECONNRESET
。 - 系统资源监控: 在生产环境中,监控关键的系统资源,如文件描述符使用量、内存、网络连接数等。设置预警机制,以便在达到危险阈值前采取措施。
- 日志记录: 实现详细的日志记录,包括连接建立、数据传输过程中的关键事件以及所有的错误信息(包括 OpenSSL 错误和系统错误码)。详细的日志是故障排除的基础。
- 环境稳定性: 确保应用程序运行的网络环境稳定,防火墙规则清晰且不会随意终止活动连接。确保对等方服务健康运行。
5. SSL_ERROR_SYSCALL
与其他 OpenSSL 错误的区别
为了更好地理解 SSL_ERROR_SYSCALL
,有必要将其与一些其他常见的 OpenSSL 错误类型进行对比:
SSL_ERROR_NONE
: 操作成功完成。SSL_ERROR_SSL
: 发生了 SSL/TLS 协议错误。这通常意味着在 SSL/TLS 协议层面出现了问题,例如握手失败(版本不匹配、密码套件不匹配、证书无效、证书验证失败等),或者接收到了格式不正确的 SSL 记录,或者违反了协议状态。这些错误的信息可以通过ERR_get_error()
和ERR_error_string()
函数族来获取更详细的 OpenSSL 错误栈信息。区别:SSL_ERROR_SSL
是协议层面的错误,而SSL_ERROR_SYSCALL
是底层系统 I/O 错误。SSL_ERROR_WANT_READ
/SSL_ERROR_WANT_WRITE
: 在使用非阻塞套接字时,表示SSL_read
或SSL_write
操作未能立即完成,因为底层套接字当前不可读或不可写。应用程序需要等待套接字变为可读写状态后再次调用相应的 OpenSSL 函数。区别:SSL_ERROR_WANT_*
表示操作阻塞,需要等待;SSL_ERROR_SYSCALL
表示底层系统调用失败,通常是连接断开、资源不足等终态错误(尽管EINTR
或EAGAIN
在特定情况下也可能导致 SYSCALL,但通常非阻塞 I/O 的常见阻塞会报告WANT_*
)。SSL_ERROR_ZERO_RETURN
: 在SSL_read
中表示对等方已经通过正常的 SSL 关闭过程(发送了close_notify
警告)关闭了连接。这是 SSL 层的优雅关闭。区别:SSL_ERROR_ZERO_RETURN
是正常的连接关闭信号;SSL_ERROR_SYSCALL
通常表示非正常的底层连接断开。
理解这些错误类型的区别,有助于在故障排除时快速定位问题的层次。SSL_ERROR_SYSCALL
明确地将问题指向了 OpenSSL 与操作系统 I/O 接口之间的交互失败。
结论
SSL_ERROR_SYSCALL
是一个指示底层系统调用失败的 OpenSSL 错误。它本身不提供具体的错误原因,需要结合操作系统层面的错误码(errno
或 WSAGetLastError()
)来诊断。常见的诱因包括网络连接问题(如连接重置、超时)、系统资源限制(如文件描述符耗尽)、以及应用程序代码中的套接字管理或多线程同步错误。
解决 SSL_ERROR_SYSCALL
错误的关键在于:
- 准确捕获: 在获取 OpenSSL 错误的同时,立即捕获并记录底层的系统错误码。
- 系统分析: 根据系统错误码及其上下文,系统地排查网络连通性、防火墙、系统资源、对等方状态和应用程序代码。
- 利用工具: 熟练使用
ping
,traceroute
,telnet
,netcat
,curl
等网络工具,以及 Wireshark/tcpdump 等网络抓包工具。在 Unix/Linux 上,了解errno
,strerror
,perror
,ulimit
,lsof
等命令。在 Windows 上,了解WSAGetLastError()
和网络监控工具。 - 代码审查: 特别关注套接字和 SSL 对象的生命周期管理、多线程安全以及非阻塞 I/O 的处理。
通过遵循本文提出的系统化排查步骤和最佳实践,开发者和系统管理员可以更有效地诊断、解决并预防 SSL_ERROR_SYSCALL
错误,确保基于 OpenSSL 的应用程序稳定可靠地运行。记住,当遇到 SSL_ERROR_SYSCALL
时,不要只盯着 OpenSSL 的错误码,深入探究背后的系统错误,才能找到真正的症结所在。