阻塞式OpenSSL 1.1.1版本下用户数据轮询及SSL控制数据非阻塞处理方案咨询
解决旧版OpenSSL阻塞套接字下的SSL控制数据处理问题
我之前也碰到过旧版OpenSSL这种头疼的问题——没有SSL_poll的情况下,阻塞套接字的SSL轮询很容易陷入FD一直就绪但无用户数据的死循环,CPU直接跑满。你的场景非常典型,核心矛盾就是:底层FD就绪是因为SSL层需要处理控制数据(握手、心跳这类),但直接调用阻塞的SSL_read/SSL_peek会卡住,不管的话又会导致poll反复触发,陷入循环。下面是针对阻塞套接字的可行解决方案,核心思路是临时切换套接字为非阻塞模式,处理SSL内部的控制数据,再恢复阻塞状态,同时正确应对SSL的各种状态。
关键思路
SSL协议的控制流程(握手、心跳、重协商等)往往需要双向读写,并非单纯的“可读即有用户数据”。当poll返回FD可读但SSL_has_pending()为0时,说明SSL层需要读取底层数据完成内部流程,此时必须触发SSL的读取操作,但不能阻塞——临时切换套接字为非阻塞是最稳妥的方式,既让SSL层处理完控制数据,又能清除FD的就绪状态,避免poll无限循环。
具体实现步骤
1. 封装非阻塞的SSL控制数据处理函数
先写一个辅助函数,临时将套接字设为非阻塞,触发SSL读取控制数据,处理完后恢复阻塞状态:
#include <fcntl.h> #include <openssl/ssl.h> // 处理SSL控制数据,不读取用户数据 int process_ssl_control_data(SSL *ssl, int fd) { int old_flags = fcntl(fd, F_GETFL); // 临时切换为非阻塞模式 fcntl(fd, F_SETFL, old_flags | O_NONBLOCK); // 调用SSL_read但不读取用户数据,仅触发SSL处理底层控制数据 char dummy_buf[1]; ssize_t ret = SSL_read(ssl, dummy_buf, sizeof(dummy_buf)); int ssl_err = SSL_get_error(ssl, ret); // 恢复阻塞模式 fcntl(fd, F_SETFL, old_flags); switch (ssl_err) { case SSL_ERROR_NONE: // 意外读到用户数据,后续正常处理即可 return 1; case SSL_ERROR_WANT_READ: // SSL还需要更多数据,FD就绪状态已处理,继续轮询 return 0; case SSL_ERROR_WANT_WRITE: // SSL需要发送数据,此时要监听FD的可写事件 return -2; case SSL_ERROR_ZERO_RETURN: // 连接已关闭 return -1; default: // 其他异常错误 return -1; } }
2. 调整轮询与处理主循环
修改你的主轮询逻辑,加入SSL控制数据的处理分支:
// 假设已初始化好pollfd和SSL对象 struct pollfd fds[1]; fds[0].fd = fd; fds[0].events = POLLIN; // 初始化阶段先检查是否在握手,是的话同时监听读写 if (SSL_in_init(ssl)) { fds[0].events = POLLIN | POLLOUT; } while (1) { int poll_ret = poll(fds, 1, -1); if (poll_ret == -1) { perror("poll failed"); break; } // 处理可读事件 if (fds[0].revents & POLLIN) { if (SSL_has_pending(ssl)) { // 有用户数据,直接用阻塞SSL_read读取 char user_buf[1024]; ssize_t read_len = SSL_read(ssl, user_buf, sizeof(user_buf)); if (read_len > 0) { process_user_data(user_buf, read_len); // 替换为你的业务逻辑 } else { handle_ssl_error(ssl, read_len); // 替换为你的错误处理 break; } } else { // 无用户数据,处理SSL控制数据 int ctrl_ret = process_ssl_control_data(ssl, fd); if (ctrl_ret == -2) { // SSL需要写数据,切换监听读写事件 fds[0].events = POLLIN | POLLOUT; } else if (ctrl_ret == -1) { handle_ssl_error(ssl, ctrl_ret); break; } // ctrl_ret为0或1时,FD就绪状态已处理,继续轮询 } } // 处理可写事件(握手、控制数据发送) if (fds[0].revents & POLLOUT) { int old_flags = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, old_flags | O_NONBLOCK); int handshake_ret = SSL_do_handshake(ssl); int ssl_err = SSL_get_error(ssl, handshake_ret); fcntl(fd, F_SETFL, old_flags); if (ssl_err == SSL_ERROR_NONE) { // 握手完成,恢复仅监听可读事件 fds[0].events = POLLIN; } else if (ssl_err == SSL_ERROR_WANT_WRITE) { // 仍需要写数据,保持监听读写 continue; } else if (ssl_err == SSL_ERROR_WANT_READ) { // 现在需要读数据,切换为监听可读 fds[0].events = POLLIN; } else { handle_ssl_error(ssl, handshake_ret); break; } } }
3. 额外注意事项
- 握手阶段强制监听读写:初始化SSL连接时,一定要先检查
SSL_in_init(ssl),如果为真,必须同时监听POLLIN和POLLOUT,因为握手过程需要双向通信。 - 心跳与重协商处理:如果你的会话启用了SSL心跳或可能触发重协商,同样需要处理
SSL_ERROR_WANT_READ/SSL_ERROR_WANT_WRITE的情况,逻辑和握手阶段一致。 - 错误处理要细致:
SSL_get_error返回的错误码一定要逐一处理,避免忽略错误导致的资源泄漏或异常崩溃。
为什么这个方案有效?
通过临时切换套接字为非阻塞,我们让SSL层能读取底层的控制数据而不阻塞程序,同时清除了FD的就绪状态,避免poll反复触发陷入循环。处理完SSL内部流程后恢复阻塞模式,保证后续用户数据的读取可以正常阻塞等待,兼顾了SSL控制流程的处理和阻塞套接字的原有逻辑。
内容的提问来源于stack exchange,提问作者Anonymous
相关产品推荐
相关产品推荐

