溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

HTTPS通信的C++實現(xiàn)

發(fā)布時間:2020-08-10 03:45:10 來源:ITPUB博客 閱讀:340 作者:Tybyq 欄目:建站服務(wù)器

?HTTPS是以安全為目標(biāo)的HTTP通道,簡單講是HTTP的安全版。即HTTP下加入SSL層,HTTPS的安全基礎(chǔ)是SSL,因此加密的詳細內(nèi)容就需要SSL。 Nebula 是一個為開發(fā)者提供一個快速開發(fā)高并發(fā)網(wǎng)絡(luò)服務(wù)程序或搭建高并發(fā)分布式服務(wù)集群的高性能事件驅(qū)動網(wǎng)絡(luò)框架。Nebula作為通用網(wǎng)絡(luò)框架提供HTTPS支持十分重要,Nebula既可用作https服務(wù)器,又可用作https客戶端。本文將結(jié)合Nebula框架的https實現(xiàn)詳細講述基于openssl的SSL編程。如果覺得本文對你有用,幫忙到Nebula的 Github 碼云 給個star,謝謝。Nebula不僅是一個框架,還提供了一系列基于這個框架的應(yīng)用,目標(biāo)是打造一個高性能分布式服務(wù)集群解決方案。Nebula的主要應(yīng)用領(lǐng)域:即時通訊(成功應(yīng)用于一款 IM )、消息推送平臺、數(shù)據(jù)實時分析計算( 成功案例 )等,Bwar還計劃基于Nebula開發(fā)爬蟲應(yīng)用。

1. SSL加密通信

??HTTPS通信是在TCP通信層與HTTP應(yīng)用層之間增加了SSL層,如果應(yīng)用層不是HTTP協(xié)議也是可以使用SSL加密通信的,比如WebSocket協(xié)議WS的加上SSL層之后的WSS。Nebula框架可以通過更換Codec達到不修改代碼變更通訊協(xié)議目的,Nebula增加SSL支持后,所有Nebula支持的通訊協(xié)議都有了SSL加密通訊支持,基于Nebula的業(yè)務(wù)代碼無須做任何修改。

HTTPS通信的C++實現(xiàn)

??Socket連接建立后的SSL連接建立過程:

HTTPS通信的C++實現(xiàn)

2. OpenSSL API

??OpenSSL的API很多,但并不是都會被使用到,如果需要查看某個API的詳細使用方法可以閱讀 API文檔 。

2.1 初始化OpenSSL

??OpenSSL在使用之前,必須進行相應(yīng)的初始化工作。在建立SSL連接之前,要為Client和Server分別指定本次連接采用的協(xié)議及其版本,目前能夠使用的協(xié)議版本包括SSLv2、SSLv3、SSLv2/v3和TLSv1.0。SSL連接若要正常建立,則要求Client和Server必須使用相互兼容的協(xié)議。 ??下面是Nebula框架SocketChannelSslImpl::SslInit()函數(shù)初始化OpenSSL的代碼,根據(jù)OpenSSL的不同版本調(diào)用了不同的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算法庫函數(shù)( 加載要用到的算法 ),調(diào)用SSL函數(shù)之前必須調(diào)用此函數(shù)
    SSL_load_error_strings();   // 錯誤信息的初始化
    OpenSSL_add_all_algorithms();#endif
2.2 創(chuàng)建CTX

??CTX是SSL會話環(huán)境,建立連接時使用不同的協(xié)議,其CTX也不一樣。創(chuàng)建CTX的相關(guān)OpenSSL函數(shù):

//客戶端、服務(wù)端都需要調(diào)用SSL_CTX_new();                       //申請SSL會話環(huán)境//若有驗證對方證書的需求,則需調(diào)用SSL_CTX_set_verify();                //指定證書驗證方式SSL_CTX_load_verify_location();      //為SSL會話環(huán)境加載本應(yīng)用所信任的CA證書列表//若有加載證書的需求,則需調(diào)用int SSL_CTX_use_certificate_file();      //為SSL會話加載本應(yīng)用的證書int SSL_CTX_use_certificate_chain_file();//為SSL會話加載本應(yīng)用的證書所屬的證書鏈int SSL_CTX_use_PrivateKey_file();       //為SSL會話加載本應(yīng)用的私鑰int SSL_CTX_check_private_key();         //驗證所加載的私鑰和證書是否相匹配
2.3 創(chuàng)建SSL套接字

??在創(chuàng)建SSL套接字之前要先創(chuàng)建Socket套接字,建立TCP連接。創(chuàng)建SSL套接字相關(guān)函數(shù):

SSL *SSl_new(SSL_CTX *ctx);          //創(chuàng)建一個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連接的基礎(chǔ)上,建立SSL連接。與普通流套接字建立連接的過程類似:Client使用函數(shù)SSL_connect()【類似于流套接字中用的connect()】發(fā)起握手,而Server使用函數(shù)SSL_ accept()【類似于流套接字中用的accept()】對握手進行響應(yīng),從而完成握手過程。兩函數(shù)原型如下:

int SSL_connect(SSL *ssl);int SSL_accept(SSL *ssl);

??握手過程完成之后,Client通常會要求Server發(fā)送證書信息,以便對Server進行鑒別。其實現(xiàn)會用到以下兩個函數(shù):

X509 *SSL_get_peer_certificate(SSL *ssl);  //從SSL套接字中獲取對方的證書信息X509_NAME *X509_get_subject_name(X509 *a); //得到證書所用者的名字
2.5 數(shù)據(jù)傳輸

??經(jīng)過前面的一系列過程后,就可以進行安全的數(shù)據(jù)傳輸了。在數(shù)據(jù)傳輸階段,需要使用SSL_read( )和SSL_write( )來代替普通流套接字所使用的read( )和write( )函數(shù),以此完成對SSL套接字的讀寫操作,兩個新函數(shù)的原型分別如下:

int SSL_read(SSL *ssl,void *buf,int num);            //從SSL套接字讀取數(shù)據(jù)int SSL_write(SSL *ssl,const void *buf,int num);     //向SSL套接字寫入數(shù)據(jù)
2.6 會話結(jié)束

??當(dāng)Client和Server之間的通信過程完成后,就使用以下函數(shù)來釋放前面過程中申請的SSL資源:

int SSL_shutdown(SSL *ssl);       //關(guān)閉SSL套接字void SSl_free(SSL *ssl);          //釋放SSL套接字void SSL_CTX_free(SSL_CTX *ctx);  //釋放SSL會話環(huán)境

3. SSL 和 TLS

??HTTPS 使用 SSL(Secure Socket Layer) 和 TLS(Transport LayerSecurity)這兩個協(xié)議。 SSL 技術(shù)最初是由瀏覽器開發(fā)商網(wǎng)景通信公司率先倡導(dǎo)的,開發(fā)過 SSL3.0之前的版本。目前主導(dǎo)權(quán)已轉(zhuǎn)移到 IETF(Internet Engineering Task Force,Internet 工程任務(wù)組)的手中。

??IETF 以 SSL3.0 為基準(zhǔn),后又制定了 TLS1.0、TLS1.1 和 TLS1.2。TSL 是以SSL 為原型開發(fā)的協(xié)議,有時會統(tǒng)一稱該協(xié)議為 SSL。當(dāng)前主流的版本是SSL3.0 和 TLS1.0。

??由于 SSL1.0 協(xié)議在設(shè)計之初被發(fā)現(xiàn)出了問題,就沒有實際投入使用。SSL2.0 也被發(fā)現(xiàn)存在問題,所以很多瀏覽器直接廢除了該協(xié)議版本。

4. Nebula中的SSL通訊實現(xiàn)

??Nebula框架同時支持SSL服務(wù)端應(yīng)用和SSL客戶端應(yīng)用,對openssl的初始化只需要初始化一次即可(SslInit()只需調(diào)用一次)。Nebula框架的SSL相關(guān)代碼(包括客戶端和服務(wù)端的實現(xiàn))都封裝在 SocketChannelSslImpl 這個類中。Nebula的SSL通信是基于異步非阻塞的socket通信,并且不使用openssl的BIO(因為沒有必要,代碼還更復(fù)雜了)。

??SocketChannelSslImpl是 SocketChannelImpl 的派生類,在SocketChannelImpl常規(guī)TCP通信之上增加了SSL通信層,兩個類的調(diào)用幾乎沒有差異。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()方法,實現(xiàn)非阻塞socket連接建立后繼續(xù)建立SSL連接,并收發(fā)數(shù)據(jù)
    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通道狀態(tài)基礎(chǔ)上增加SSL通道狀態(tài)
    bool m_bIsClientConnection;
    SSL* m_pSslConnection;    static SSL_CTX* m_pServerSslCtx;    //當(dāng)打開ssl選項編譯,啟動Nebula服務(wù)則自動創(chuàng)建
    static SSL_CTX* m_pClientSslCtx;    //默認(rèn)為空,當(dāng)打開ssl選項編譯并且第一次發(fā)起了對其他SSL服務(wù)的連接時(比如訪問一個https地址)創(chuàng)建};

??SocketChannelSslImpl類中帶override關(guān)鍵字的方法都是覆蓋基類SocketChannelImpl的同名方法,也是實現(xiàn)SSL通信與非SSL通信調(diào)用透明的關(guān)鍵。不帶override關(guān)鍵字的方法都是SSL通信相關(guān)方法,這些方法里有openssl的函數(shù)調(diào)用。不帶override的方法中有靜態(tài)和非靜態(tài)之分,靜態(tài)方法在進程中只會被調(diào)用一次,與具體Channel對象無關(guān)。SocketChannel外部不需要調(diào)用非靜態(tài)的ssl相關(guān)方法。

??因為是非阻塞的socket,SSL_do_handshake()和SSL_write()、SSL_read()返回值并不完全能判斷是否出錯,還需要SSL_get_error()獲取錯誤碼。SSL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE都是正常的。

??網(wǎng)上的大部分openssl例子程序是按順序調(diào)用openssl函數(shù)簡單實現(xiàn)同步ssl通信,在非阻塞IO應(yīng)用中,ssl通信要復(fù)雜許多。SocketChannelSslImpl實現(xiàn)的是非阻塞的ssl通信,從該類的實現(xiàn)上看整個通信過程并非完全線性的。下面的SSL通信圖更清晰地說明了Nebula框架中SSL通信是如何實現(xiàn)的:

HTTPS通信的C++實現(xiàn)

??SocketChannelSslImpl中的靜態(tài)方法在進程生命期內(nèi)只需調(diào)用一次,也可以理解成SSL_CTX_new()、SSL_CTX_free()等方法只需調(diào)用一次。更進一步理解SSL_CTX結(jié)構(gòu)體在進程內(nèi)只需要創(chuàng)建一次(在Nebula中分別為Server和Client各創(chuàng)建一個)就可以為所有SSL連接所用;當(dāng)然,為每個SSL連接創(chuàng)建獨立的SSL_CTX也沒問題(Nebula 0.4中實測過為每個Client創(chuàng)建獨立的SSL_CTX),但一般不這么做,因為這樣會消耗更多的內(nèi)存資源,并且效率也會更低。

??建立SSL連接時,客戶端調(diào)用SSL_connect(),服務(wù)端調(diào)用SSL_accept(),許多openssl的demo都是這么用的。Nebula中用的是SSL_do_handshake(),這個方法同時適用于客戶端和服務(wù)端,在兼具client和server功能的服務(wù)更適合用SSL_do_handshake()。注意調(diào)用SSL_do_handshake()前,如果是client端需要先調(diào)用SSL_set_connect_state(),如果是server端則需要先調(diào)用SSL_set_accept_state()。非阻塞IO中,SSL_do_handshake()可能需要調(diào)用多次才能完成握手,具體調(diào)用時機需根據(jù)SSL_get_error()獲取錯誤碼SSL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE判斷需監(jiān)聽讀事件還是寫事件,在對應(yīng)事件觸發(fā)時再次調(diào)用SSL_do_handshake()。詳細實現(xiàn)請參考SocketChannelSslImpl的Send和Recv方法。

??關(guān)閉SSL連接時先調(diào)用SSL_shutdown()正常關(guān)閉SSL層連接(非阻塞IO中SSL_shutdown()亦可能需要調(diào)用多次)再調(diào)用SSL_free()釋放SSL連接資源,最后關(guān)閉socket連接。SSL_CTX無須釋放。整個SSL通信順利完成,Nebula 0.4在開多個終端用shell腳本死循環(huán)調(diào)用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

??測試方法如下圖:

HTTPS通信的C++實現(xiàn)

??查看資源使用情況,SSL Server端的內(nèi)存使用一直在增長,疑似有內(nèi)存泄漏,不過pmap -d查看某一項anon內(nèi)存達到近18MB時不再增長,說明可能不是內(nèi)存泄漏,只是部分內(nèi)存被openssl當(dāng)作cache使用了。這個問題網(wǎng)上沒找到解決辦法。從struct ssl_ctx_st結(jié)構(gòu)體定義發(fā)現(xiàn)端倪,再從nginx源碼中發(fā)現(xiàn)了SSL_CTX_remove_session(),于是在SSL_free()之前加上SSL_CTX_remove_session()。session復(fù)用可以提高SSL通信效率,不過Nebula暫時不需要。

??這種測試方法把NebulaInterface作為SSL服務(wù)端,NebulaLogic作為SSL客戶端,同時完成了Nebula框架SSL服務(wù)端和客戶端功能測試,簡單的壓力測試。Nebula框架的SSL通信測試通過,也可以投入生產(chǎn)應(yīng)用,在后續(xù)應(yīng)用中肯定還會繼續(xù)完善。openssl真的難用,難怪被吐槽那么多,或許不久之后的Nebula版本將用其他ssl庫替換掉openssl。

5. 結(jié)束

??加上SSL支持的Nebula框架測試通過,雖然不算太復(fù)雜,但過程還是蠻曲折,耗時也挺長。這里把Nebula使用openssl開發(fā)SSL通信分享出來,希望對準(zhǔn)備使用openssl的開發(fā)者有用。如果覺得本文對你有用,別忘了到Nebula的 Github 碼云 給個star,謝謝。

<br/>


向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI