我认为这个线程有些混乱,因为有一些事情需要澄清。让我们从断言::asio::ssl::context == SSL_CTX 开始。两者合而为一。
其次,当使用boost::asio::ssl 时,除非您在绕过内部init 对象的地方做一些疯狂的事情,否则您无需手动设置加密锁定回调。正如您在源代码here 中看到的那样,这是为您完成的。
实际上,这样做可能会导致问题,因为init 对象的析构函数在假设它们已在内部完成此工作的情况下运行。对最后一点持保留态度,因为我没有深入审查过这一点。
第三,我相信您将 SSL 流与 SSL 上下文混淆了。为简单起见,将 SSL 流视为套接字,将 SSL 上下文视为套接字可用于各种 SSL 功能的单独对象,例如与特定协商密钥的握手,或作为服务器,提供有关您的服务器证书的信息到已连接的客户端,以便您可以与客户端握手。
提到链归结为防止对一个特定流(套接字)的可能同时 IO,而不是上下文。显然,尝试在同一个套接字上同时从同一个缓冲区读取和写入将是一个问题。因此,当您向各种::asio::async_X 方法提供由链包裹的完成处理程序时,您将强制执行特定的排序以防止上述情况。您可以在this answer 中阅读更多内容,该内容由比我了解更多的人提供。
现在就上下文而言,David Schwartz 在 cmets 和他写的另一个我需要挖掘的答案中指出,上下文本身的全部目的是提供有助于跨多个 SSL 流实现 SSL 功能的信息。考虑到它们的预期目的,他似乎暗示它们本质上必须是线程安全的。我相信他可能是在::asio::ssl::context 的上下文中说话,只是因为::asio::ssl 正确使用线程安全回调的方式,或者他只是在多线程程序中正确使用openSSL 的上下文中说话。
无论如何,除了关于 SO 的此类 cmets 和答案以及我自己的实践经验之外,很难在文档中找到具体的证据,或者明确定义线程安全与非线程安全之间的界限。 boost::asio::ssl:context 是,正如大卫也指出的那样,只是 SSL_CTX 的一个非常薄的包装。我还要补充一点,它旨在为使用底层结构提供更“c++ ish”的感觉。它的设计可能也是为了将::asio::ssl 和底层实现库解耦,但它没有实现这一点,两者是紧密结合的。 David 再次正确地提到这个瘦包装器的文档记录很差,必须查看实现才能获得洞察力。
如果您开始深入研究实现,有一种相当简单的方法可以找出在上下文方面什么是线程安全的,什么不是线程安全的。您可以在ssl_lib.c 等来源中搜索CRYPTO_LOCK_SSL_CTX。
int SSL_CTX_set_generate_session_id(SSL_CTX *ctx, GEN_SESSION_CB cb)
{
CRYPTO_w_lock(CRYPTO_LOCK_SSL_CTX);
ctx->generate_session_id = cb;
CRYPTO_w_unlock(CRYPTO_LOCK_SSL_CTX);
return 1;
}
如您所见,使用了CRYPTO_w_lock,这使我们回到了有关openSSL和线程的官方页面here,其中指出:
OpenSSL 可以安全地用于提供的多线程应用程序中
至少设置了两个回调函数,locking_function 和
threadid_func。
现在我们回到这个答案第一段中链接的asio/ssl/detail/impl/openssl_init.ipp源代码,我们在哪里看到:
do_init()
{
::SSL_library_init();
::SSL_load_error_strings();
::OpenSSL_add_all_algorithms();
mutexes_.resize(::CRYPTO_num_locks());
for (size_t i = 0; i < mutexes_.size(); ++i)
mutexes_[i].reset(new boost::asio::detail::mutex);
::CRYPTO_set_locking_callback(&do_init::openssl_locking_func);
::CRYPTO_set_id_callback(&do_init::openssl_id_func);
#if !defined(SSL_OP_NO_COMPRESSION) \
&& (OPENSSL_VERSION_NUMBER >= 0x00908000L)
null_compression_methods_ = sk_SSL_COMP_new_null();
#endif // !defined(SSL_OP_NO_COMPRESSION)
// && (OPENSSL_VERSION_NUMBER >= 0x00908000L)
}
当然要注意:
CRYPTO_set_locking_callback
CRYPTO_set_id_callback
所以至少就::asio::ssl::context 而言,这里的线程安全与链无关,而与openSSL 工作有关的一切都与openSSL 的工作有关,因为openSSL 旨在在多线程程序中正确使用时工作。
回到最初的问题,现在已经解释了所有这些,大卫也在 cmets 中非常简单地给出了答案:
最常用的方法是每个 SSL 连接使用一个链。
以提供example.com 内容的HTTPS 服务器为例。服务器有一个单独的上下文,配置了诸如example.com 的证书之类的信息。客户端连接,此上下文用于所有连接的客户端以执行握手等。您将连接的客户端包装在一个新的 session 对象中,您可以在其中处理该客户端。在此会话中,您将拥有一个单链,无论是隐式还是显式,来保护 socket,而不是上下文。
虽然我无论如何都不是专家,并且我欢迎对此答案进行更正,但我已将我对这些主题的所有了解都应用到开源透明过滤 HTTPS 代理中。总行数超过 17K 的 cmets 与代码的比率略高于 50%,所以我所知道的一切都写在那里(无论是对还是错;))。如果您想查看这些东西的实际示例,您可以查看TlsCapableHttpBridge.hpp 源,它在每个主机、每个连接的基础上充当客户端和服务器。
服务器上下文和证书被欺骗/生成一次,并在平移多个线程的所有客户端之间共享。唯一完成的手动锁定是在上下文的存储和检索期间。网桥的每 side 有一根线,一根用于真正的下游客户端套接字,一根用于上游服务器连接,尽管它们在技术上甚至不是必需的,因为操作顺序无论如何都会创建一个隐式线.
请注意,该项目正在开发中,因为我正在重写很多东西(dep build 指令尚不存在),但就 MITM SSL 代码而言,一切都是功能性的,因此您正在查看一个功能齐全的类和相关的组件。