BoringSSL SSL_write 延迟两秒问题解析与解决方案
2024-12-13 07:07:47
BoringSSL 中 SSL_write 延迟两秒问题解析与解决
在网络应用开发中,SSL/TLS 加密通信是保障数据安全的重要手段。然而,使用 BoringSSL 进行 SSL_write 数据发送时,有时会遇到数据包延迟发送的问题,例如出现两秒延迟。本文将深入探讨这个问题的原因,并提供几种解决方案。
问题分析:SSL_write 延迟的可能原因
SSL_write 出现延迟,Wireshark 捕获到应用数据包有两秒延迟,可能由以下几个方面原因导致:
-
Nagle 算法的影响 :Nagle 算法旨在减少小数据包在网络上的传输,提高网络利用率。它会合并多个小数据包,延迟发送直到累积到足够的数据量或收到确认。虽然代码中已通过设置
TCP_NODELAY
禁用该算法,但仍需确认是否在其他地方被重新启用或未正确设置。 -
底层 TCP 套接字阻塞 :SSL_write 底层依赖于 TCP 套接字的发送操作。如果 TCP 套接字发送缓冲区已满,或者网络状况不佳导致数据包发送阻塞,SSL_write 就会等待。尽管设置为阻塞模式,也应排查网络连接或远程服务器是否存在问题。
-
SSL/TLS 握手或会话复用延迟 :在 SSL_write 之前,如果需要进行 SSL/TLS 握手或者会话复用,此过程可能产生延迟。日志信息显示了握手阶段的操作,需要检查握手过程是否存在异常,或者复用会话时是否出现问题。
-
BoringSSL 内部机制 :BoringSSL 自身的一些机制,例如记录层缓冲区管理、延迟刷新等,也可能导致写入延迟。 需要进一步分析 BoringSSL 的内部实现,排查是否存在类似机制导致问题。
-
系统资源限制 :系统资源不足,例如 CPU、内存或网络带宽瓶颈,也可能影响 SSL_write 的性能。
-
应用层代码逻辑 :虽然提供的代码片段仅展示了 SSL_write 的核心部分,但仍需检查应用层是否存在其他代码逻辑导致延时。例如,其他线程或任务的干扰、数据准备过程耗时等。
解决方案
针对上述可能的原因,可以尝试以下几种解决方案:
1. 彻底禁用 Nagle 算法并验证
确保 TCP_NODELAY
选项在 SSL socket 创建后立即设置,并且在整个连接过程中保持不变。可以使用以下代码示例进行设置:
int sock = socket(AF_INET, SOCK_STREAM, 0);
int flag = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(flag));
// ... 之后的操作,包括SSL context的创建以及将该 socket 应用于 SSL
操作步骤:
- 定位 socket 创建位置。
- 紧随其后添加以上代码片段,确保在 SSL 上下文关联 socket 之前设置
TCP_NODELAY
。 - 使用 Wireshark 或 tcpdump 等工具抓包,验证 TCP 数据包的发送时机,确认是否立即发送小数据包。
2. 调整 SSL 记录层缓冲区大小或刷新策略
BoringSSL 内部有记录层缓冲区,可以通过调整缓冲区大小或刷新策略来减少延迟。虽然BoringSSL不直接提供API调整缓冲区,但可以通过控制SSL_write的调用次数和每次写入的数据量,间接影响刷新行为。另一种方法是尝试在每次SSL_write后,手动触发底层socket的flush操作,这取决于所使用的网络库。 以下代码示例展示了每次写入少量数据,并进行检查性flush的策略(注意,底层socket的flush机制需自行实现)。
static int ssl_write() {
int send_len, len = total_len;
unsigned char *cur_p = send_data;
print_timestamp("Before SSL_write");
while (len > 0) {
send_len = (len > TCP_MAX_SSL_SEND_BUFFER_SIZE) ? TCP_MAX_SSL_SEND_BUFFER_SIZE : len;
// 限制每次写入大小,如 1KB
send_len = (send_len > 1024) ? 1024: send_len;
ret = SSL_write((SSL *)ssl_handle.handle, cur_p, send_len);
print_timestamp("After SSL_write");
if (ret != send_len) {
LOG("SslSend data error: %d, exp: %d\n", ret, send_len);
memset(send_data, 0, total_len);
if (dyn_allocate) {
uai_free(send_data);
}
return (g_err_code = UAI_ERROR);
}
len -= send_len;
cur_p += send_len;
// 尝试 flush socket (注意:实际系统中需要自行实现socket_flush)
socket_flush(g_ssl_sock_id); //这是一个示意性的函数调用
}
memset(send_data, 0, total_len);
if (dyn_allocate) {
uai_free(send_data);
}
return (g_err_code = UAI_OK);
}
// 简单的socket flush 仅用于演示, 需要根据实际情况实现
int socket_flush(int socket_fd){
#ifdef __linux__
int flags = fcntl(socket_fd, F_GETFL, 0);
if (flags >= 0) {
if(fcntl(socket_fd, F_SETFL, flags | O_NONBLOCK) == -1){
perror("fcntl set nonblocking failed");
}
char dummy_buffer; // 极小缓冲区,不实际发送数据
if(send(socket_fd, &dummy_buffer, 0 , MSG_DONTWAIT) == -1 && errno != EAGAIN && errno != EWOULDBLOCK ){
perror("send failed in socket_flush");
}
if(fcntl(socket_fd, F_SETFL, flags) == -1){ // 恢复阻塞模式
perror("fcntl restore flags failed");
}
} else {
perror("fcntl get flags failed");
return -1;
}
return 0;
#else
// 其他系统的socket flush 方案 比如 Windows 可以使用 send 配合 flags = MSG_DONTROUTE
return -1; // 或其他平台特定实现
#endif
}
操作步骤:
- 修改代码,限制每次 SSL_write 的数据量,例如限制为 1KB。
- 添加socket_flush 的实现代码(需要根据具体平台实现)。
- 编译代码并测试,观察是否解决了延迟问题。
3. 检查 SSL/TLS 握手和会话复用
仔细检查 SSL/TLS 握手过程的日志,确认是否存在异常或长时间延迟。如果使用了会话复用,需要确认复用过程是否正常,以及是否存在复用失败后重新握手的情况。可以尝试禁用会话复用,或者强制重新握手,以排除会话复用导致的问题。
代码示例 (禁用会话复用):
SSL_CTX_set_session_cache_mode(ssl_ctx, SSL_SESS_CACHE_OFF);
操作步骤:
- 在SSL上下文创建后,调用
SSL_CTX_set_session_cache_mode(ssl_ctx, SSL_SESS_CACHE_OFF)
禁用会话复用。 - 编译并测试,查看是否解决了延迟问题,并分析握手日志。
4. 排除网络和服务器问题
- 使用网络诊断工具(如 ping, traceroute)检查网络连通性和延迟。
- 确认远程服务器负载是否过高,或者存在其他性能瓶颈。
- 尝试使用其他网络环境或服务器进行测试,以排除特定网络或服务器的问题。
5. 监测系统资源使用情况
在运行应用时,使用系统工具(如 top, htop, perf)监测 CPU、内存、网络 I/O 等资源使用情况。如果发现资源瓶颈,需要优化代码或增加系统资源。
命令行示例(Linux):
top # 查看 CPU 和内存使用情况
htop # 更直观地查看资源使用情况
perf top # 查看 CPU 性能瓶颈
iostat -x 1 # 查看网络 I/O 统计信息
操作步骤:
- 在应用运行时,执行上述命令,观察资源使用情况。
- 分析数据,判断是否存在资源瓶颈。
- 如有必要,优化代码或增加系统资源。
6. 代码审查和性能分析
对应用层代码进行全面审查,特别是与 SSL_write 相关的部分。使用性能分析工具(如 gprof, valgrind