You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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()前执行该操作

问题

  1. 如何在发送请求前获知连接状态?
  2. 如何初始化连接状态?
  3. 使用阻塞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下,按以下步骤检测:

  1. 调用BIO_check_read(bio)判断是否有数据可读(包括TLS层的close notify)
  2. 若有数据,临时将BIO切换为非阻塞模式,调用一次BIO_read()(哪怕读取dummy数据),触发SSL层处理close notify
  3. 检查SSL_get_shutdown(ssl)是否包含SSL_RECEIVED_SHUTDOWN,若为真则说明连接已被服务器优雅关闭,需重连
  4. 发送请求时,若BIO_write()失败,通过SSL_get_error()判断是否为SSL_ERROR_ZERO_RETURN(连接已关闭)

2. 如何初始化连接状态?

每次重连后必须完成以下操作:

  1. 调用BIO_reset(tmpBio)重置连接
  2. 重新执行BIO_do_handshake(tmpBio)完成TLS握手
  3. 调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 02:47:36