HTTPS 是以安全为目标的 HTTP 通道, 简单讲是 HTTP 的安全版. 即 HTTP 下加入 SSL 层, HTTPS 的安全基础是 SSL, 因此加密的详细内容就需要 SSL. https://github.com/Bwar/Nebula 是一个为开发者提供一个快速开发高并发网络服务程序或搭建高并发分布式服务集群的高性能事件驱动网络框架. Nebula 作为通用网络框架提供 HTTPS 支持十分重要, Nebula 既可用作 https 服务器, 又可用作 https 客户端. 本文将结合 Nebula 框架的 https 实现详细讲述基于 openssl 的 SSL 编程. 如果觉得本文对你有用, 帮忙到 Nebula 的 GitHub https://github.com/Bwar/Nebula 或码云 https://gitee.com/Bwar/Nebula 给个 star, 谢谢. Nebula 不仅是一个框架, 还提供了一系列基于这个框架的应用, 目标是打造一个高性能分布式服务集群解决方案. Nebula 的主要应用领域: 即时通讯 (成功应用于一款 IM), 消息推送平台, 数据实时分析计算(成功案例 https://github.com/Bwar/Nebio ) 等, Bwar 还计划基于 Nebula 开发爬虫应用.
1. SSL 加密通信
HTTPS 通信是在 TCP 通信层与 HTTP 应用层之间增加了 SSL 层, 如果应用层不是 HTTP 协议也是可以使用 SSL 加密通信的, 比如 webSocket 协议 WS 的加上 SSL 层之后的 WSS.Nebula 框架可以通过更换 Codec 达到不修改代码变更通讯协议目的, Nebula 增加 SSL 支持后, 所有 Nebula 支持的通讯协议都有了 SSL 加密通讯支持, 基于 Nebula 的业务代码无须做任何修改.
Socket 连接建立后的 SSL 连接建立过程:
2. OpenSSL API
OpenSSL 的 API 很多, 但并不是都会被使用到, 如果需要查看某个 API 的详细使用方法可以阅读 API 文档 https://www.openssl.org/docs/man1.1.0/ssl/ .
2.1 初始化 OpenSSL
OpenSSL 在使用之前, 必须进行相应的初始化工作. 在建立 SSL 连接之前, 要为 Client 和 Server 分别指定本次连接采用的协议及其版本, 目前能够使用的协议版本包括 SSLv2,SSLv3,SSLv2/v3 和 TLSv1.0.SSL 连接若要正常建立, 则要求 Client 和 Server 必须使用相互兼容的协议. 下面是 Nebula 框架 SocketChannelSslImpl::SslInit()函数初始化 OpenSSL 的代码, 根据 OpenSSL 的不同版本调用了不同的 API 进行初始化.
- #if OPENSSL_VERSION_NUMBER>= 0x10100003L
- if (OPENSSL_init_ssl(OPENSSL_INIT_LOAD_CONFIG, NULL) == 0)
- {
- pLogger->WriteLog(neb::Logger::ERROR, __FILE__, __LINE__, __FUNCTION__, "OPENSSL_init_ssl() failed!");
- return(ERR_SSL_INIT);
- }
- /*
- * OPENSSL_init_ssl() may leave errors in the error queue
- * while returning success
- */
- ERR_clear_error();
- #else
- OPENSSL_config(NULL);
- SSL_library_init(); // 初始化 SSL 算法库函数( 加载要用到的算法 ), 调用 SSL 函数之前必须调用此函数
- SSL_load_error_strings(); // 错误信息的初始化
- OpenSSL_add_all_algorithms();
- #endif
2.2 创建 CTX
CTX 是 SSL 会话环境, 建立连接时使用不同的协议, 其 CTX 也不一样. 创建 CTX 的相关 OpenSSL 函数:
- // 客户端, 服务端都需要调用
- SSL_CTX_new(); // 申请 SSL 会话环境
- // 若有验证对方证书的需求, 则需调用
- SSL_CTX_set_verify(); // 指定证书验证方式
- SSL_CTX_load_verify_location(); // 为 SSL 会话环境加载本应用所信任的 CA 证书列表
- // 若有加载证书的需求, 则需调用
- int SSL_CTX_use_certificate_file(); // 为 SSL 会话加载本应用的证书
- int SSL_CTX_use_certificate_chain_file();// 为 SSL 会话加载本应用的证书所属的证书链
- int SSL_CTX_use_PrivateKey_file(); // 为 SSL 会话加载本应用的私钥
- int SSL_CTX_check_private_key(); // 验证所加载的私钥和证书是否相匹配
2.3 创建 SSL 套接字
在创建 SSL 套接字之前要先创建 Socket 套接字, 建立 TCP 连接. 创建 SSL 套接字相关函数:
- SSL *SSl_new(SSL_CTX *ctx); // 创建一个 SSL 套接字
- int SSL_set_fd(SSL *ssl, int fd); // 以读写模式绑定流套接字
- int SSL_set_rfd(SSL *ssl, int fd); // 以只读模式绑定流套接字
- int SSL_set_wfd(SSL *ssl, int fd); // 以只写模式绑定流套接字
2.4 完成 SSL 握手
在这一步, 我们需要在普通 TCP 连接的基础上, 建立 SSL 连接. 与普通流套接字建立连接的过程类似: Client 使用函数 SSL_connect()[类似于流套接字中用的 connect()] 发起握手, 而 Server 使用函数 SSL_ accept()[类似于流套接字中用的 accept()] 对握手进行响应, 从而完成握手过程. 两函数原型如下:
- int SSL_connect(SSL *ssl);
- int SSL_accept(SSL *ssl);
握手过程完成之后, Client 通常会要求 Server 发送证书信息, 以便对 Server 进行鉴别. 其实现会用到以下两个函数:
- X509 *SSL_get_peer_certificate(SSL *ssl); // 从 SSL 套接字中获取对方的证书信息
- X509_NAME *X509_get_subject_name(X509 *a); // 得到证书所用者的名字
2.5 数据传输
经过前面的一系列过程后, 就可以进行安全的数据传输了. 在数据传输阶段, 需要使用 SSL_read( )和 SSL_write( )来代替普通流套接字所使用的 read( )和 write( )函数, 以此完成对 SSL 套接字的读写操作, 两个新函数的原型分别如下:
- int SSL_read(SSL *ssl,void *buf,int num); // 从 SSL 套接字读取数据
- int SSL_write(SSL *ssl,const void *buf,int num); // 向 SSL 套接字写入数据
2.6 会话结束
当 Client 和 Server 之间的通信过程完成后, 就使用以下函数来释放前面过程中申请的 SSL 资源:
- int SSL_shutdown(SSL *ssl); // 关闭 SSL 套接字
- void SSl_free(SSL *ssl); // 释放 SSL 套接字
- void SSL_CTX_free(SSL_CTX *ctx); // 释放 SSL 会话环境
3. SSL 和 TLS
HTTPS 使用 SSL(Secure Socket Layer) 和 TLS(Transport LayerSecurity)这两个协议. SSL 技术最初是由浏览器开发商网景通信公司率先倡导的, 开发过 SSL3.0 之前的版本. 目前主导权已转移到 IETF(Internet Engineering Task Force,Internet 工程任务组)的手中.
IETF 以 SSL3.0 为基准, 后又制定了 TLS1.0,TLS1.1 和 TLS1.2.TSL 是以 SSL 为原型开发的协议, 有时会统一称该协议为 SSL. 当前主流的版本是 SSL3.0 和 TLS1.0.
由于 SSL1.0 协议在设计之初被发现出了问题, 就没有实际投入使用. SSL2.0 也被发现存在问题, 所以很多浏览器直接废除了该协议版本.
4. Nebula 中的 SSL 通讯实现
Nebula 框架同时支持 SSL 服务端应用和 SSL 客户端应用, 对 openssl 的初始化只需要初始化一次即可 (SslInit() 只需调用一次).Nebula 框架的 SSL 相关代码 (包括客户端和服务端的实现) 都封装在这个类中. Nebula 的 SSL 通信是基于异步非阻塞的 socket 通信, 并且不使用 openssl 的 BIO(因为没有必要, 代码还更复杂了).
SocketChannelSslImpl 是的派生类, 在 SocketChannelImpl 常规 TCP 通信之上增加了 SSL 通信层, 两个类的调用几乎没有差异. SocketChannelSslImpl 类声明如下:
- class SocketChannelSslImpl : public SocketChannelImpl
- {
- public:
- SocketChannelSslImpl(SocketChannel* pSocketChannel, std::shared_ptr<NetLogger> pLogger, int iFd, uint32 ulSeq, ev_tstamp dKeepAlive = 0.0);
- virtual ~SocketChannelSslImpl();
- static int SslInit(std::shared_ptr<NetLogger> pLogger);
- static int SslServerCtxCreate(std::shared_ptr<NetLogger> pLogger);
- static int SslServerCertificate(std::shared_ptr<NetLogger> pLogger,
- const std::string& strCertFile, const std::string& strKeyFile);
- static void SslFree();
- int SslClientCtxCreate();
- int SslCreateConnection();
- int SslHandshake();
- int SslShutdown();
- virtual bool Init(E_CODEC_TYPE eCodecType, bool bIsClient = false) override;
- // 覆盖基类的 Send()方法, 实现非阻塞 socket 连接建立后继续建立 SSL 连接, 并收发数据
- virtual E_CODEC_STATUS Send() override;
- virtual E_CODEC_STATUS Send(int32 iCmd, uint32 uiSeq, const MsgBody& oMsgBody) override;
- virtual E_CODEC_STATUS Send(const HttpMsg& oHttpMsg, uint32 ulStepSeq) override;
- virtual E_CODEC_STATUS Recv(MsgHead& oMsgHead, MsgBody& oMsgBody) override;
- virtual E_CODEC_STATUS Recv(HttpMsg& oHttpMsg) override;
- virtual E_CODEC_STATUS Recv(MsgHead& oMsgHead, MsgBody& oMsgBody, HttpMsg& oHttpMsg) override;
- virtual bool Close() override;
- protected:
- virtual int Write(CBuffer* pBuff, int& iErrno) override;
- virtual int Read(CBuffer* pBuff, int& iErrno) override;
- private:
- E_SSL_CHANNEL_STATUS m_eSslChannelStatus; // 在基类 m_ucChannelStatus 通道状态基础上增加 SSL 通道状态
- bool m_bIsClientConnection;
- SSL* m_pSslConnection;
- static SSL_CTX* m_pServerSslCtx; // 当打开 ssl 选项编译, 启动 Nebula 服务则自动创建
- static SSL_CTX* m_pClientSslCtx; // 默认为空, 当打开 ssl 选项编译并且第一次发起了对其他 SSL 服务的连接时 (比如访问一个 https 地址) 创建
- };
SocketChannelSslImpl 类中带 override 关键字的方法都是覆盖基类 SocketChannelImpl 的同名方法, 也是实现 SSL 通信与非 SSL 通信调用透明的关键. 不带 override 关键字的方法都是 SSL 通信相关方法, 这些方法里有 openssl 的函数调用. 不带 override 的方法中有静态和非静态之分, 静态方法在进程中只会被调用一次, 与具体 Channel 对象无关. SocketChannel 外部不需要调用非静态的 ssl 相关方法.
因为是非阻塞的 socket,SSL_do_handshake()和 SSL_write(),SSL_read()返回值并不完全能判断是否出错, 还需要 SSL_get_error()获取错误码. SSL_ERROR_WANT_READ 和 SSL_ERROR_WANT_WRITE 都是正常的.
网上的大部分 openssl 例子程序是按顺序调用 openssl 函数简单实现同步 ssl 通信, 在非阻塞 IO 应用中, ssl 通信要复杂许多. SocketChannelSslImpl 实现的是非阻塞的 ssl 通信, 从该类的实现上看整个通信过程并非完全线性的. 下面的 SSL 通信图更清晰地说明了 Nebula 框架中 SSL 通信是如何实现的:
SocketChannelSslImpl 中的静态方法在进程生命期内只需调用一次, 也可以理解成 SSL_CTX_new(),SSL_CTX_free()等方法只需调用一次. 更进一步理解 SSL_CTX 结构体在进程内只需要创建一次 (在 Nebula 中分别为 Server 和 Client 各创建一个) 就可以为所有 SSL 连接所用; 当然, 为每个 SSL 连接创建独立的 SSL_CTX 也没问题(Nebula 0.4 中实测过为每个 Client 创建独立的 SSL_CTX), 但一般不这么做, 因为这样会消耗更多的内存资源, 并且效率也会更低.
建立 SSL 连接时, 客户端调用 SSL_connect(), 服务端调用 SSL_accept(), 许多 openssl 的 demo 都是这么用的. Nebula 中用的是 SSL_do_handshake(), 这个方法同时适用于客户端和服务端, 在兼具 client 和 server 功能的服务更适合用 SSL_do_handshake(). 注意调用 SSL_do_handshake()前, 如果是 client 端需要先调用 SSL_set_connect_state(), 如果是 server 端则需要先调用 SSL_set_accept_state(). 非阻塞 IO 中, SSL_do_handshake()可能需要调用多次才能完成握手, 具体调用时机需根据 SSL_get_error()获取错误码 SSL_ERROR_WANT_READ 和 SSL_ERROR_WANT_WRITE 判断需监听读事件还是写事件, 在对应事件触发时再次调用 SSL_do_handshake(). 详细实现请参考 SocketChannelSslImpl 的 Send 和 Recv 方法.
关闭 SSL 连接时先调用 SSL_shutdown()正常关闭 SSL 层连接 (非阻塞 IO 中 SSL_shutdown() 亦可能需要调用多次)再调用 SSL_free()释放 SSL 连接资源, 最后关闭 socket 连接. SSL_CTX 无须释放. 整个 SSL 通信顺利完成, Nebula 0.4 在开多个终端用 shell 脚本死循环调用 curl 简单压测中 SSL client 和 SSL server 功能一切正常:
- while :
- do
- curl -v -k -H "Content-Type:application/json" -X POST -d '{"hello":"nebula ssl test"}' https://192.168.157.168:16003/test_ssl
- done
测试方法如下图:
查看资源使用情况, SSL Server 端的内存使用一直在增长, 疑似有内存泄漏, 不过 pmap -d 查看某一项 anon 内存达到近 18MB 时不再增长, 说明可能不是内存泄漏, 只是部分内存被 openssl 当作 cache 使用了. 这个问题网上没找到解决办法. 从 struct ssl_ctx_st 结构体定义发现端倪, 再从 nginx 源码中发现了 SSL_CTX_remove_session(), 于是在 SSL_free()之前加上 SSL_CTX_remove_session().session 复用可以提高 SSL 通信效率, 不过 Nebula 暂时不需要.
这种测试方法把 NebulaInterface 作为 SSL 服务端, NebulaLogic 作为 SSL 客户端, 同时完成了 Nebula 框架 SSL 服务端和客户端功能测试, 简单的压力测试. Nebula 框架的 SSL 通信测试通过, 也可以投入生产应用, 在后续应用中肯定还会继续完善. openssl 真的难用, 难怪被吐槽那么多, 或许不久之后的 Nebula 版本将用其他 ssl 库替换掉 openssl.
5. 结束
加上 SSL 支持的 Nebula 框架测试通过, 虽然不算太复杂, 但过程还是蛮曲折, 耗时也挺长. 这里把 Nebula 使用 openssl 开发 SSL 通信分享出来, 希望对准备使用 openssl 的开发者有用. 如果觉得本文对你有用, 别忘了到 Nebula 的 GitHub https://github.com/Bwar/Nebula 或码云 https://gitee.com/Bwar/Nebula 给个 star, 谢谢.
<br/>
参考资料:
基于 OpenSSL 实现的安全连接
SSL API 文档 https://www.openssl.org/docs/man1.1.0/ssl/
Https 协议详解
HTTPS 是大势所趋? 看腾讯专家通过 Epoll+OpenSSL 在高并发压测机器人中支持 https
openssl 编程入门(含完整示例)
SSL 连接建立过程分析
SSL socket 通讯详解
HTTPS 从原理到应用(三):SSL/TLS 协议 https://www.jianshu.com/p/c93612b3abac
SSL/TLS 握手优化详解 http://blog.jobbole.com/94332/
非阻塞 / 异步(epoll) openssl
两个基于 openssl 的 https client 例子 https://my.oschina.net/vincentwy/blog/620282?p=1
OpenSSL 编程初探 1 --- 使用 OpenSSL API 建立 SSL 通信的一般流程简介
OpenSSL 编程初探 2 --- 关于证书文件的加载
来源: https://www.cnblogs.com/bwar/p/9879893.html