非阻塞TLS场景下SSL_read返回WANT_READ/WANT_WRITE后能否调用SSL_write?
非阻塞TLS客户端:SSL_read等待时能否优先执行SSL_write?
先给你明确的结论:完全可以在SSL_read()返回SSL_ERROR_WANT_READ或SSL_ERROR_WANT_WRITE时调用SSL_write()——OpenSSL的非阻塞模式设计本身就支持同时处理读写操作,不需要严格等待读操作完成再执行写动作。
为什么示例会看起来要等读完成?
你提到的《Network Security with OpenSSL》里的示例只是最基础的演示场景,它简化了逻辑,只展示了"先读再写"的流程,但这不是强制规则。OpenSSL的SSL对象本身维护了独立的内部状态,只要你遵循非阻塞IO的事件驱动逻辑,读写操作完全可以交叉进行。
实现"有数据就读、随时可写"的正确逻辑
要满足你的需求,核心是结合IO多路复用工具(比如select()、poll()或者epoll)来监听套接字的读写事件,再根据事件和SSL的状态灵活调度操作:
初始化准备:
- 将套接字设置为非阻塞模式
- 完成TLS握手(注意握手阶段也可能返回WANT_READ/WANT_WRITE,这时同样可以穿插写操作,OpenSSL会自动先处理握手相关的IO,再处理你的写请求)
事件循环核心逻辑:
- 用多路复用工具同时监听套接字的可读事件(对应SSL需要读数据的场景,比如
SSL_read返回WANT_READ、握手需要读)和可写事件(对应SSL需要写数据的场景,比如待发送数据、SSL_write返回WANT_WRITE、握手需要写) - 维护一个"待发送数据队列",只要队列里有数据,就必须监听套接字的可写事件
- 当事件触发时:
- 可读事件:调用
SSL_read(),成功就读取并处理数据;如果返回WANT_READ/WANT_WRITE,更新后续要监听的事件(比如返回WANT_WRITE的话,下次循环要监听可写事件) - 可写事件:优先处理待发送数据,调用
SSL_write()发送;如果发送成功就从队列移除已发送部分;如果返回WANT_READ/WANT_WRITE,更新监听事件。哪怕是之前SSL_read返回WANT_WRITE触发的可写事件,你也可以先处理写操作,之后再回来重试读请求——SSL状态会帮你记住之前读请求的上下文。
- 可读事件:调用
- 用多路复用工具同时监听套接字的可读事件(对应SSL需要读数据的场景,比如
关键注意事项
- 不要丢弃读请求的上下文:如果
SSL_read()返回WANT_READ/WANT_WRITE,之后重试时必须使用和之前完全相同的参数(缓冲区、长度),这是手册明确要求的,但这不影响你中间调用SSL_write,只要之后记得回来重试这个读请求就行。 - 握手阶段自动处理:如果TLS握手还没完成,调用
SSL_write()时,OpenSSL会自动先完成握手所需的IO操作,再处理你的写请求,不需要额外干预。 - 禁止忙轮询:一定要用IO多路复用工具,不要直接循环调用
SSL_read/SSL_write,否则会浪费大量CPU资源。
伪代码示例
// 假设已完成套接字非阻塞设置、SSL初始化及TLS握手 fd_set read_fds, write_fds; char read_buf[1024]; // 模拟待发送数据 char* pending_write = "client status update: online"; size_t write_total_len = strlen(pending_write); size_t write_sent_len = 0; while (1) { FD_ZERO(&read_fds); FD_ZERO(&write_fds); FD_SET(sockfd, &read_fds); // 有未发送数据,监听可写事件 if (write_sent_len < write_total_len) { FD_SET(sockfd, &write_fds); } // 等待IO事件触发 select(sockfd + 1, &read_fds, &write_fds, NULL, NULL); // 处理可读事件 if (FD_ISSET(sockfd, &read_fds)) { int ret = SSL_read(ssl, read_buf, sizeof(read_buf)-1); if (ret > 0) { read_buf[ret] = '\0'; printf("Received server data: %s\n", read_buf); // 处理读取到的业务数据 } else { int err = SSL_get_error(ssl, ret); if (err == SSL_ERROR_WANT_READ) { // 继续监听可读事件即可 } else if (err == SSL_ERROR_WANT_WRITE) { // 后续需要监听可写事件,下次循环处理 FD_SET(sockfd, &write_fds); } else { // 处理连接关闭、证书错误等其他异常 break; } } } // 处理可写事件 if (FD_ISSET(sockfd, &write_fds)) { // 优先发送待处理数据 if (write_sent_len < write_total_len) { int ret = SSL_write(ssl, pending_write + write_sent_len, write_total_len - write_sent_len); if (ret > 0) { write_sent_len += ret; printf("Sent %d bytes to server\n", ret); // 发送完成后可以添加新的待发送数据到队列 } else { int err = SSL_get_error(ssl, ret); if (err == SSL_ERROR_WANT_WRITE) { // 继续监听可写事件 } else if (err == SSL_ERROR_WANT_READ) { // 后续需要监听可读事件 FD_SET(sockfd, &read_fds); } else { // 处理写失败异常 break; } } } else { // 没有待发送数据时,重试之前未完成的SSL_read请求 int ret = SSL_read(ssl, read_buf, sizeof(read_buf)-1); // 这里的处理逻辑和上面的可读事件分支一致 } } }
这个伪代码里,读写操作是并行调度的:只要有待发送数据就会触发写操作,同时不耽误读请求的处理——哪怕读请求处于等待状态,也可以先处理写,之后再回来重试读。
内容的提问来源于stack exchange,提问作者sklein
相关产品推荐
相关产品推荐

