基于OpenSSL的HTTPS通讯C++实现

  HTTPS是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,所以加密的详细内容就须要SSL。Nebula是一个为开发者提供一个快速开发高并发网络服务程序或搭建高并发分布式服务集群的高性能事件驱动网络框架。Nebula做为通用网络框架提供HTTPS支持十分重要,Nebula既可用做https服务器,又可用做https客户端。本文将结合Nebula框架的https实现详细讲述基于openssl的SSL编程。若是以为本文对你有用,帮忙到Nebula的Github码云给个star,谢谢。Nebula不只是一个框架,还提供了一系列基于这个框架的应用,目标是打造一个高性能分布式服务集群解决方案。Nebula的主要应用领域:即时通信(成功应用于一款IM)、消息推送平台、数据实时分析计算(成功案例)等,Bwar还计划基于Nebula开发爬虫应用。html

1. SSL加密通讯

  HTTPS通讯是在TCP通讯层与HTTP应用层之间增长了SSL层,若是应用层不是HTTP协议也是可使用SSL加密通讯的,好比WebSocket协议WS的加上SSL层以后的WSS。Nebula框架能够经过更换Codec达到不修改代码变动通信协议目的,Nebula增长SSL支持后,全部Nebula支持的通信协议都有了SSL加密通信支持,基于Nebula的业务代码无须作任何修改。nginx

https_communication

  Socket链接创建后的SSL链接创建过程:git

ssl_communication

2. OpenSSL API

  OpenSSL的API不少,但并非都会被使用到,若是须要查看某个API的详细使用方法能够阅读API文档github

2.1 初始化OpenSSL

  OpenSSL在使用以前,必须进行相应的初始化工做。在创建SSL链接以前,要为Client和Server分别指定本次链接采用的协议及其版本,目前可以使用的协议版本包括SSLv二、SSLv三、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函数:shell

//客户端、服务端都须要调用 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()】对握手进行响应,从而完成握手过程。两函数原型以下:json

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相关代码(包括客户端和服务端的实现)都封装在SocketChannelSslImpl这个类中。Nebula的SSL通讯是基于异步非阻塞的socket通讯,而且不使用openssl的BIO(由于没有必要,代码还更复杂了)。

  SocketChannelSslImpl是SocketChannelImpl的派生类,在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通讯是如何实现的:

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_test

  查看资源使用状况,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码云给个star,谢谢。

<br/>

参考资料:

相关文章
相关标签/搜索