OpenSSL 3.0.9阻塞BIO下预检测服务器close notify的技术问题
OpenSSL 3.0.9 C++实现s_client功能的连接状态处理问题
背景
正在使用OpenSSL 3.0.9编写C++代码,模拟命令openssl s_client -connect <host>:<port> -tls1_2 -no_ticket -CAfile <file>的功能。需求是当服务器超时结束发送close notify通知时,程序停止响应。
当前代码存在的问题:
- 仅能通过
BIO_read()获取close notify消息,且该操作会初始化SSL连接状态后,才能用if (SSL_get_shutdown(ssl) == SSL_RECEIVED_SHUTDOWN)判断 - 使用阻塞BIO时,
BIO_read()会因服务器超时导致程序挂起,且无法在BIO_write()前执行该操作
问题
- 如何在发送请求前获知连接状态?
- 如何初始化连接状态?
- 使用阻塞BIO能否实现该需求?
现有代码
接收数据函数
string receive_some_data(BIO* bio) { SSL* ssl; BIO_get_ssl(bio, &ssl); char buffer[1024]; int len = BIO_read(bio, buffer, sizeof(buffer)); if (len < 0) { reportAndExit("Error in reading response"); } else if (len > 0) { return std::string(buffer, len); } else if (BIO_should_retry(bio)) { return receive_some_data(bio); } else if (len == 0 && (SSL_get_shutdown(ssl) == SSL_RECEIVED_SHUTDOWN) ) { reportAndExit("Get 'close notify' from server"); } else { reportAndExit("Empty response"); } }
主逻辑代码
... SSL_CTX* ctx; BIO* tmpBio = nullptr; SSL* tmpSsl = nullptr; auto method_ = TLS_client_method(); if (NULL == method_) reportAndExit("Error initializations of the client method"); tmpBio = BIO_new_ssl_connect(ctx_); if (NULL == tmpBio) reportAndExit("Error creating a new TLS connection"); BIO_get_ssl(tmpBio, &tmpSsl); if (tmpSsl == NULL) { reportAndExit("Get session error"); } SSL_set_mode(tmpSsl, SSL_MODE_AUTO_RETRY); setBioConnectParam(tmpBio, host, port); if (BIO_do_handshake(tmpBio) <= 0) { reportAndExit("Handshake error"); } while (true) { auto connectionStatus = SSL_get_shutdown(tmpSsl); string msg = "Reset connect, because "; if (connectionStatus == SSL_SENT_SHUTDOWN) { msg += "SSL_SENT_SHUTDOWN"; } if (connectionStatus == SSL_RECEIVED_SHUTDOWN) { msg += "SSL_RECEIVED_SHUTDOWN"; } if (connectionStatus > 0) { // reconnect after shutdown BIO_reset(tmpBio); BOOST_LOG_TRIVIAL(info) << msg; } std::string request = "GET " + pageName + " HTTP/1.1\n"; request += "Host: " + domen + "\n"; request += "Connection: keep-alive\n"; request += "\n"; int n = BIO_write(tmpBio, request.data(), request.size()); BIO_flush(tmpBio); std::string headers = receive_some_data(tmpBio); char* end_of_headers = strstr(&headers[0], "\n\n"); while (end_of_headers == nullptr) { headers += receive_some_data(tmpBio); end_of_headers = strstr(&headers[0], "\n\n"); } std::string body = std::string(end_of_headers + 2, &headers[headers.size()]); headers.resize(end_of_headers + 1 - &headers[0]); size_t content_length = 0; for (const std::string& line : splitHeaders(headers)) { if (const char* colon = strchr(line.c_str(), ':')) { auto header_name = std::string(&line[0], colon); if (header_name == "Content-Length") { content_length = std::stoul(colon + 1); } } } while (body.size() < content_length) { body += receive_some_data(bio_); } std::string result = headers + "\n" + body; std::this_thread::sleep_for(7s); }
解决方案
3. 使用阻塞BIO能否实现该需求?
可以实现,但需调整状态检测逻辑,避免直接调用阻塞BIO_read()导致程序挂起。
1. 如何在发送请求前获知连接状态?
不能直接依赖SSL_get_shutdown(),该状态仅在SSL层处理close notify后更新。阻塞BIO下,按以下步骤检测:
- 调用
BIO_check_read(bio)判断是否有数据可读(包括TLS层的close notify) - 若有数据,临时将BIO切换为非阻塞模式,调用一次
BIO_read()(哪怕读取dummy数据),触发SSL层处理close notify - 检查
SSL_get_shutdown(ssl)是否包含SSL_RECEIVED_SHUTDOWN,若为真则说明连接已被服务器优雅关闭,需重连 - 发送请求时,若
BIO_write()失败,通过SSL_get_error()判断是否为SSL_ERROR_ZERO_RETURN(连接已关闭)
2. 如何初始化连接状态?
每次重连后必须完成以下操作:
- 调用
BIO_reset(tmpBio)重置连接 - 重新执行
BIO_do_handshake(tmpBio)完成TLS握手 - 调用
SSL_set_shutdown(ssl, 0)清除之前的关闭状态,避免旧状态干扰新连接判断
代码修改建议
优化接收数据函数
string receive_some_data(BIO* bio) { SSL* ssl; BIO_get_ssl(bio, &ssl); char buffer[1024]; int len = BIO_read(bio, buffer, sizeof(buffer)); if (len < 0) { int ssl_err = SSL_get_error(ssl, len); if (ssl_err == SSL_ERROR_WANT_READ || ssl_err == SSL_ERROR_WANT_WRITE) { return receive_some_data(bio); } else { reportAndExit("Error in reading response: SSL error"); } } else if (len > 0) { return std::string(buffer, len); } else { if (SSL_get_shutdown(ssl) & SSL_RECEIVED_SHUTDOWN) { reportAndExit("Get 'close notify' from server"); } else { reportAndExit("Connection closed by server"); } } }
优化主循环的状态检测逻辑
while (true) { SSL* ssl; BIO_get_ssl(tmpBio, &ssl); // 发送前检测是否有TLS消息(如close notify) int check = BIO_check_read(tmpBio); if (check > 0) { // 临时切换为非阻塞模式 int old_mode = BIO_get_nbio(tmpBio, NULL); BIO_set_nbio(tmpBio, 1); char dummy[1]; int ret = BIO_read(tmpBio, dummy, 1); if (ret == 0 || (ret < 0 && SSL_get_error(ssl, ret) == SSL_ERROR_ZERO_RETURN)) { BOOST_LOG_TRIVIAL(info) << "Received close notify, reconnecting..."; BIO_reset(tmpBio); if (BIO_do_handshake(tmpBio) <= 0) { reportAndExit("Handshake error after reconnect"); } SSL_set_shutdown(ssl, 0); } // 恢复阻塞模式 BIO_set_nbio(tmpBio, old_mode); } // 检查SSL关闭状态 auto connectionStatus = SSL_get_shutdown(ssl); if (connectionStatus > 0) { string msg = "Reset connect, because "; if (connectionStatus & SSL_SENT_SHUTDOWN) { msg += "SSL_SENT_SHUTDOWN"; } if (connectionStatus & SSL_RECEIVED_SHUTDOWN) { msg += "SSL_RECEIVED_SHUTDOWN"; } BOOST_LOG_TRIVIAL(info) << msg; BIO_reset(tmpBio); if (BIO_do_handshake(tmpBio) <= 0) { reportAndExit("Handshake error after reconnect"); } SSL_set_shutdown(ssl, 0); } // 构造请求(注意用标准HTTP换行符\r\n) std::string request = "GET " + pageName + " HTTP/1.1\r\n"; request += "Host: " + domen + "\r\n"; request += "Connection: keep-alive\r\n"; request += "\r\n"; int n = BIO_write(tmpBio, request.data(), request.size()); if (n <= 0) { int ssl_err = SSL_get_error(ssl, n); if (ssl_err == SSL_ERROR_ZERO_RETURN) { reportAndExit("Connection closed before sending"); } else { reportAndExit("Error writing request"); } } BIO_flush(tmpBio); // 后续接收逻辑不变 std::string headers = receive_some_data(tmpBio); char* end_of_headers = strstr(&headers[0], "\r\n\r\n"); while (end_of_headers == nullptr) { headers += receive_some_data(tmpBio); end_of_headers = strstr(&headers[0], "\r\n\r\n"); } std::string body = std::string(end_of_headers + 4, &headers[headers.size()]); headers.resize(end_of_headers + 2 - &headers[0]); size_t content_length = 0; for (const std::string& line : splitHeaders(headers)) { if (const char* colon = strchr(line.c_str(), ':')) { auto header_name = std::string(&line[0], colon); if (header_name == "Content-Length") { content_length = std::stoul(colon + 1); } } } while (body.size() < content_length) { body += receive_some_data(tmpBio); } std::string result = headers + "\r\n" + body; std::this_thread::sleep_for(7s); }
内容的提问来源于stack exchange,提问作者Sergey Kiwi
相关产品推荐
相关产品推荐

