You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

在旧版OpenSSL 1.1.1的阻塞连接中如何轮询用户数据

在旧版OpenSSL 1.1.1的阻塞连接中如何轮询用户数据

兄弟,我太懂你这种卡在旧版OpenSSL轮询的糟心感了——没SSL_poll就算了,还碰到FD一直被占着CPU空转的死循环,简直头大。我来给你捋捋亲测靠谱的解决思路,都是实际踩坑摸出来的:

核心问题根源

你碰到的死循环,本质是socket的FD里躺着SSL的控制数据(比如握手包、心跳帧、重协商报文),但这些不是用户数据,SSL_has_pending()会返回false,但poll又一直检测到FD可读。这时候必须让SSL把这些控制数据读进自己的内部缓冲区,把FD“清空”,poll才能正常等待下一次数据。

靠谱的解决方案:临时切换非阻塞模式处理SSL内部操作

因为你用的是阻塞socket,直接调用SSL_read会卡死,但我们可以临时把socket改成非阻塞,让SSL在不阻塞的情况下处理完所有待处理的控制数据,然后再切回阻塞模式。具体步骤如下:

  1. 先保存socket的原始阻塞状态
    每次处理poll事件前,先把socket当前的flags存下来,之后要切回去:

    int original_flags = fcntl(fd, F_GETFL, 0);
    
  2. 临时将socket设为非阻塞
    当poll检测到FD可读/可写时,先切换到非阻塞模式:

    fcntl(fd, F_SETFL, original_flags | O_NONBLOCK);
    
  3. 循环调用SSL_read处理内部数据
    用一个极小的缓冲区(比如1字节)调用SSL_read,即使没有用户数据,SSL也会把FD里的控制数据读进自己的缓冲区。循环直到SSL_read返回SSL_ERROR_WANT_READSSL_ERROR_WANT_WRITE

    char dummy_buf[1];
    int ssl_ret;
    do {
        ssl_ret = SSL_read(ssl, dummy_buf, sizeof(dummy_buf));
        if (ssl_ret > 0) {
            // 如果读到了用户数据,先存起来,之后再完整读取
            // 比如把dummy_buf的内容追加到你的用户数据缓冲区里
        } else {
            int ssl_err = SSL_get_error(ssl, ssl_ret);
            if (ssl_err == SSL_ERROR_WANT_READ || ssl_err == SSL_ERROR_WANT_WRITE) {
                // 如果是需要写,下次poll要监听POLLOUT事件
                if (ssl_err == SSL_ERROR_WANT_WRITE) {
                    pfd.events |= POLLOUT;
                } else {
                    pfd.events &= ~POLLOUT;
                }
                break;
            } else {
                // 处理真正的SSL错误,比如连接断开
                goto cleanup;
            }
        }
    } while (1);
    
  4. 切回socket的阻塞模式
    处理完SSL内部操作后,立刻把socket改回原来的阻塞状态:

    fcntl(fd, F_SETFL, original_flags);
    
  5. 读取用户数据
    这时候再检查SSL_has_pending(),如果返回true,就可以放心地用阻塞模式的SSL_read读取用户数据了:

    while (SSL_has_pending(ssl)) {
        char user_buf[1024];
        ssl_ret = SSL_read(ssl, user_buf, sizeof(user_buf));
        if (ssl_ret > 0) {
            // 处理你的用户数据,比如业务逻辑
        } else {
            // 这里理论上不会出错,除非有意外情况
            int ssl_err = SSL_get_error(ssl, ssl_ret);
            if (ssl_err != SSL_ERROR_WANT_READ && ssl_err != SSL_ERROR_WANT_WRITE) {
                goto cleanup;
            }
            break;
        }
    }
    

额外注意事项

  • 处理SSL的写需求:有时候SSL需要发送控制数据(比如握手应答),这时候会返回SSL_ERROR_WANT_WRITE,你需要把这个FD加入poll的POLLOUT监听事件,下次poll触发时,同样用非阻塞模式调用SSL_writeSSL_do_handshake(如果在握手阶段)完成操作。
  • 握手阶段的特殊处理:如果连接还没完成握手,要先循环调用SSL_do_handshake(非阻塞模式)直到SSL_is_init_finished(ssl)返回true,再处理用户数据,逻辑和上面的非阻塞处理一致。
  • 别用SSL_peekSSL_peek不会消耗FD里的数据,处理完FD还是会处于可读状态,poll还是会立刻触发,解决不了死循环问题。

总结一下,核心就是用临时非阻塞的方式,让SSL把FD里的控制数据“吃干抹净”,把FD还给poll正常监听,这样就不会出现CPU空转的死循环了。亲测在OpenSSL 1.1.1f上完全有效,你可以试试这个思路!

火山引擎 最新活动