@(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^~