@(blogs)
使用openssl進行加密通訊時,一般是先創建socket鏈接,而後使用SSL_XXX系列函數在普通socket之上創建安全鏈接,而後發送和接收數據。openssl的這些函數能夠支持底層的socket是非阻塞模式的。但當將openssl和libuv進行結合時,會遇到一些問題:緩存
解決這兩個問題的思路是同樣的,將openssl看作是一個數據過濾器,可參考這篇文章。安全
在和libuv結合時,openssl不能直接對socket進行讀寫,由於對socket的讀寫操做已經被libuv徹底封裝了。不過openssl能夠經過BIO進行讀寫數據。也就是說,須要準備兩個BIO,一個用於存儲openssl加密好的數據,一個用於存儲接收到的加密數據以備openssl解密。這個操做直接調用下面這個函數便可完成:socket
void SSL_set_bio(SSL *ssl, BIO *rbio, BIO *wbio);
設置好這兩個BIO以後,SSL_XXX系列函數的全部操做都是針對這兩個BIO,再也不直接和socket打交道。這樣對socket的操做就能夠委託給libuv了。tcp
對於寫數據到socket,直接將數據丟給libuv就能夠了。但讀數據的時候會略微麻煩一些。在建立安全鏈接的時候openssl須要屢次「握手」操做,也就是須要朝socket讀寫幾回數據。這個過程須要在libuv的read_cb函數裏處理。也就是說在libuv的read_cb函數須要區分要讀的數據是「握手」時的數據仍是真正通訊讀取的數據。這個判斷經過函數
int SSL_is_init_finished(SSL *ssl);
函數實現,也就是判斷openssl是否完成了安全鏈接的初始化。測試
對於前面提到的第二個問題,openssl提供瞭解決這個問題的機制。SSL_XXX系列函數的返回值能夠經過加密
int SSL_get_error(const SSL *ssl, int ret);
來獲取其具體的含義,其中兩個重要的返回結果是SSL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE。在調用SSL_connect,SSL_read和SSL_write時,openssl可能須要讀取更多的數據或者發送數據,這兩個返回值代表openssl的意圖。注意:這三個函數都有可能返回這兩個值。也就是說在讀數據的時候可能須要寫數據,在寫數據的時候可能須要讀數據。code
囉囉嗦嗦說了這麼多,上代碼纔是王道。如下代碼只是示意,並不能直接編譯運行^v^。
首先,聲明變量:blog
SSL *ssl; SSL_ctx *ssl_ctx; BIO *read_bio; BIO *write_bio; uv_tcp_t *con
在libuv的on_connect_cb函數中初始化openssl並開始「握手」。內存
void on_connect_cb(uv_connect_t *req, int status) { //設置數據讀取的回調函數 uv_read_start((uv_stream_t*)con, on_alloc_cb, on_read_cb); ssl = SSL_new(ssl_ctx); read_bio = BIO_new(BIO_s_mem()); write_bio = BIO_new(BIO_s_mem()); SSL_set_bio(ssl, read_bio, write_bio); SSL_set_connect_state(ssl); // 這是個客戶端鏈接 int ret = SSL_connect(ssl); // 開始握手。這個函數僅僅是將數據寫如了BIO緩存,並無發送到socket上。 write_bio_to_socket(); // 若是有,將wirte BIO中的數據寫入socket。(具體定義見後面代碼) if (ret != 1) { // connect出錯了,看看具體什麼問題。 int err = SSL_get_error(ssl, ret); if (err == SSL_ERROR_WANT_READ) { // 在read回調函數中讀取數據 } else if (err == SSL_ERROR_WANT_WRITE) { write_bio_to_socket(); // 將write BIO中的數據發送出去 } } }
真正的重頭戲是在on_read_cb中。
void read_cb(uv_stream_t* stream, ssize_t nread, const uv_buf_t *buf) { if (nread == UV_EOF) { // 已經讀完了全部的數據 read_data_after_handshake(); return; } else { // 讀取數據到BIO中。buf中的數據是加密數據,將其放到BIO中,讓openssl將其解碼。 BIO_write(read_bio, buf -> base, nread); if (!SSL_is_init_finished(ssl)) { // 咱們尚未完成ssl的初始化,繼續進行握手。 int ret = SSL_connect(ssl); write_bio_to_socket(); if (ret != 1) { int err = SSL_get_error(ssl, ret); if (err == SSL_ERROR_WANT_READ) { // 在read回調函數中讀取數據 } else if (err == SSL_ERROR_WANT_WRITE) { write_bio_to_socket(); } } else { // 握手完成,發送數據。 send_data_after_handshake(); } } else { // ssl已經初始化好了, 咱們能夠從BIO中讀取已經解密的數據。 read_data_after_handshake(); } } free(buf -> base); }
下面來看看write_bio_to_socket()的實現,這個函數很簡單,就是將write_bio中的數據丟給libuv進行發送。
void write_bio_to_socket() { char buf[1024]; int hasread = BIO_read(write_bio, buf, sizeof(buf)); if (hasread <= 0) { // 無數據可寫。 return; } uv_write_t *wreq = (uv_write_t*)malloc(sizeof(uv_write_t)); char *tmp = malloc(hasread); memcpy(tmp, buf, hasread); uv_buf_t *bufs = (uv_buf_t*)malloc(sizeof(uv_buf_t) * 1); bufs[0].base = tmp; bufs[0].len = hasread; uv_write(wreq, (uv_stream_t*)con, bufs, 1, on_write_cb); // 記得在on_write_cb中釋放這裏分配的內存。 }
BIO_read有可能一次讀取不完write_bio中的數據,因此這個地方須要一個循環屢次調用BIO_read直到數據所有讀完。這裏爲了簡單就只讀一次了^v^。
send_data_after_handshake函數也很簡單,就是將須要發送的數據寫入wirte_bio中而後丟給libuv發送,還須要處理有數據要讀取的狀況。
void send_data_after_handshake() { int ret = SSL_write(ssl, data, data_len); // data中存放了要發送的數據 if (ret > 0) { // 寫入socket write_bio_to_socket(); } else if (ret == 0) { // 鏈接關閉了?? uv_close((uv_handle_t*)con, on_close_cb); } else { // 須要讀取或寫入數據。 int err = SSL_get_error(client -> ssl, ret); if (err == SSL_ERROR_WANT_READ) { // 在read回調中處理(其實若是有數據要讀時什麼都不要,等read回調就好了。。。) } else if (err == SSL_ERROR_WANT_WRITE) { write_bio_to_socket(); } } }
最後是read_data_after_handshake,這個函數將openlls解密好的數據讀取出來,同時還須要處理在讀取數據的時候須要寫入數據的問題。
void read_data_after_handshake() { char buf[1024]; memset(buf, '\0', sizeof(buf)); int ret = SSL_read(ssl, buf, sizeof(buf)); if (ret < 0) { int err = SSL_get_error(client -> ssl, ret); if (err == SSL_ERROR_WANT_READ) { // 在read回調函數中讀取數據 } else if (err == SSL_ERROR_WANT_WRITE) { // 有數據要寫,將write BIO中的數據發送出去 write_bio_to_socket(); } } // 解密好的數據就存放在buf中了。固然,這個地方也可能須要屢次調用SSL_read來說全部數據都讀出來。 }
以上就是所有的示例代碼了。
關於這個openssl和libuv結合使用的思路尚未進行嚴格的測試,我也只是在工程中初步測試了一下能夠走通。對於一些細節的處理還不是很到位。這裏只是提供了一個libuv和openssl結合的思路,若是有任何問題,歡迎指正。^v^~