如何利用BoringSSL的BIO API实现读写线程完全分离?
BoringSSL读写线程完全分离实现方案
1. 能否通过BIO API实现读写线程完全分离?
可以。BoringSSL的BIO系统原生支持读写路径的完全分离,通过为SSL对象配置独立的读BIO和写BIO,结合线程安全的BIO组件(如BIO_pair),可以实现读写线程无锁并行操作,无需在SSL_read/SSL_write上加全局锁。
2. 正确实现步骤
核心思路
使用BIO_pair作为SSL层与socket I/O线程之间的桥接:创建两组BIO对,一组负责SSL读与socket读线程的交互,另一组负责SSL写与socket写线程的交互,让SSL操作与底层socket I/O完全解耦。
代码实现示例
#include <openssl/ssl.h> #include <openssl/bio.h> #include <pthread.h> #include <unistd.h> SSL *ssl; int sockfd; BIO *socket_read_bio, *socket_write_bio; // 读线程:负责从socket读取数据,写入BIO供SSL读取 void* read_thread(void* arg) { char buf[4096]; ssize_t n; while (1) { n = recv(sockfd, buf, sizeof(buf), 0); if (n <= 0) { // 连接关闭或错误,通知SSL层 BIO_shutdown(socket_read_bio); break; } // 将socket数据写入BIO,供SSL_read读取 while (n > 0) { int written = BIO_write(socket_read_bio, buf, n); if (written <= 0) { if (!BIO_should_retry(socket_read_bio)) { // BIO不可用,退出 BIO_shutdown(socket_read_bio); goto exit; } continue; } n -= written; } } exit: return NULL; } // 写线程:负责从BIO读取加密后的数据,写入socket发送 void* write_thread(void* arg) { char buf[4096]; int n; while (1) { n = BIO_read(socket_write_bio, buf, sizeof(buf)); if (n <= 0) { if (BIO_should_retry(socket_write_bio)) { continue; } // 连接关闭或错误 break; } // 将加密数据发送到socket while (n > 0) { ssize_t sent = send(sockfd, buf, n, 0); if (sent <= 0) { // 发送错误,通知SSL层 BIO_shutdown(socket_write_bio); goto exit; } n -= sent; } } exit: return NULL; } // 初始化SSL与BIO void init_ssl_and_bio(SSL_CTX* ctx) { ssl = SSL_new(ctx); // 创建读BIO对:ssl_read_bio供SSL读取,socket_read_bio供读线程写入 BIO *ssl_read_bio; BIO_new_bio_pair(&ssl_read_bio, 8192, &socket_read_bio, 8192); // 创建写BIO对:ssl_write_bio供SSL写入,socket_write_bio供写线程读取 BIO *ssl_write_bio; BIO_new_bio_pair(&ssl_write_bio, 8192, &socket_write_bio, 8192); // 为SSL对象绑定独立的读/写BIO SSL_set_bio(ssl, ssl_read_bio, ssl_write_bio); // 建议先在当前线程完成SSL握手 int ret = SSL_connect(ssl); if (ret != 1) { // 处理握手错误,比如重试WANT_READ/WANT_WRITE while (ret != 1) { if (SSL_get_error(ssl, ret) == SSL_ERROR_WANT_READ) { // 等待读线程填充数据后重试 usleep(1000); } else if (SSL_get_error(ssl, ret) == SSL_ERROR_WANT_WRITE) { // 等待写线程发送数据后重试 usleep(1000); } else { // 致命错误 break; } ret = SSL_connect(ssl); } } // 启动读写线程 pthread_t rtid, wtid; pthread_create(&rtid, NULL, read_thread, NULL); pthread_create(&wtid, NULL, write_thread, NULL); }
关键操作说明
- 独立BIO配置:通过
BIO_new_bio_pair创建两组双向BIO,分别隔离读、写路径。SSL层只与ssl_read_bio/ssl_write_bio交互,socket I/O线程只操作socket_read_bio/socket_write_bio。 - 线程协调:无需显式锁,
BIO_pair原生支持多线程并发读写两端。当SSL_read返回SSL_ERROR_WANT_READ时,只需等待读线程向socket_read_bio写入数据后重试;SSL_write返回SSL_ERROR_WANT_WRITE时,等待写线程从socket_write_bio读取数据后重试。 - 握手处理:建议先在单线程完成SSL握手,避免握手阶段同时触发读写操作导致的线程协调复杂度。若必须在多线程下握手,需确保读写线程能及时响应BIO的I/O需求。
3. 推荐模式与最佳实践
推荐模式
- BIO_pair桥接模式:这是BoringSSL官方推荐的读写分离实现方式,完全解耦SSL逻辑与底层socket I/O,天然支持多线程并行。
- 预握手模式:在启动读写线程前完成SSL握手,减少线程协调成本。
最佳实践
- 设置合理的BIO缓冲区大小:创建
BIO_pair时指定足够的缓冲区(如8192字节),减少频繁重试,提升性能。 - 严格处理BIO重试标志:当
BIO_read/BIO_write返回-1时,用BIO_should_retry()判断是否需要重试,避免误判为连接错误。 - 资源安全清理:
SSL_free会自动释放关联的ssl_read_bio/ssl_write_bio,但socket_read_bio/socket_write_bio需要手动调用BIO_free释放,避免内存泄漏。 - 错误传播:当socket出现错误时,调用
BIO_shutdown()通知SSL层,确保SSL能正确处理连接关闭。
潜在陷阱与注意事项
- 忽略BIO重试机制:直接关闭连接会导致正常的重试场景(如缓冲区满/空)被误处理,引发不必要的连接中断。
- 缓冲区过小:过小的BIO缓冲区会导致
SSL_read/SSL_write频繁返回WANT状态,增加重试次数,降低吞吐量。 - 握手阶段线程冲突:若在读写线程启动后进行握手,需确保读写线程能及时处理BIO的I/O请求,否则会导致握手阻塞。
- BIO线程归属:每个BIO实例只能被一个线程操作,
socket_read_bio仅由读线程写入,socket_write_bio仅由写线程读取,避免多线程并发操作同一BIO引发的线程安全问题。
内容的提问来源于stack exchange,提问作者benrush
相关产品推荐
相关产品推荐

