openssl 使用非阻塞 bio

linux

在项目中须要访问 https 加密的网页,为了保证并发性,须要用到非阻塞的 socket,搜索发现,这种使用场景的相关介绍不是不少,因此这里记录一下使用的过程。并发

在项目中,所使用的 ssl 库是老牌 sll 库 —— openssl。所使用的 io多路复用 技术是 epoll。socket

核心流程

总体流程与访问非加密网站相似,不一样之处在于有一下几点:tcp

  1. 在 socket 创建 tcp 链接以后,须要绑定 socket 句柄在 SSL 中
  2. 读取,发送数据,使用 SSL 库的方法,替代 linux 系统调用
  3. 关闭链接前,须要先执行 SSL 关闭流程

创建链接函数

首先,打开 socket 句柄,而后设置必要的属性
网站

 

1 int sock_fd = -1;
2 int flags = -1;
3 sock_fd = socket(AF_INET, SOCK_STREAM, 0);
4 flags = fcntl(sockfd, F_GETFL, 0);
5 fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

 

而后,将句柄加入 epoll 的管理加密

1 epoll_event ev;
2 ev.events = EPOLLIN | EPOLLOUT | EPOLLET
3 ev.data.ptr = your_ev_info;
4 epoll_ctl(epfd, EPOLL_CTL_ADD, url_item->sockfd, &ev);

如今,能够开始真正的链接过程了,与普通的 tcp 链接同样,调用 connect 系统调用。在非阻塞 io 中,须要经过 connect 的返回值和 errno 来判断链接状态,采起不一样的策略url

 1 struct sockaddr_in serv_addr;
 2 
 3 if (connect(sock_fd, (sockaddr *) & serv_addr, sizeof (sockaddr)) < 0) {
 4     // 没有马上链接成功,须要判断 errno
 5     if (errno != EINPROGRESS && errno != EINTR) {
 6         // 失败了, 从epoll里面干掉
 7         epoll_ctl(epfd, EPOLL_CTL_DEL, sock_fd, NULL);
 8     }
 9 } else {
10     // 马上成功了
11     prepare_connect_ssl(your_ev_info);
12 }

若是没有马上链接成功,在成功后,会触发 epoll,咱们须要在 your_ev_info 中,须要保存如今的状态,以便在  epoll_wait 以后,经过状态来决定须要调用的函数。这些属于 epoll 的细节了,在此不展开说。spa

假设,如今已经链接成功,则开始作 SSL 握手以前的准备工做。code

1 SSL_CTX *ssl_ctx;
2 SSL *ssl;
3 
4 ssl_ctx = SSL_CTX_new(TLSv1_method());
5 ssl = SSL_new(url_item->ssl_ctx);
6 SSL_set_mode(url_item->ssl, SSL_MODE_ENABLE_PARTIAL_WRITE);
7 
8 // 绑定 SSL 和 socket 句柄
9 SSL_set_fd(ssl, sock_fd);

这一步之因此和后面的 SSL 握手过程分开,是由于 SSL 握手在非阻塞io 的状况下,有可能会被调用屡次,而这部分只须要一次调用便可。

如今开始 SSL 握手

 1 int ssl_conn_ret = SSL_connect(ssl);
 2 if (1 == ssl_conn_ret) {
 3     // 开始和对端交互
 4 } else if (-1 == ssl_conn_ret) {
 5     // 没有马上握手成功,须要经过错误码来判断如今的状态
 6     int ssl_conn_err = SSL_get_error(ssl, ssl_conn_ret);
 7     if (SSL_ERROR_WANT_READ == ssl_conn_err || 
 8              SSL_ERROR_WANT_WRITE == ssl_conn_err) {
 9          //须要更多时间来进行握手
10     }
11 } else {
12     // 链接失败了,作必要处理
13     if (0 != ssl_conn_ret) {
14         SSL_shutdown(ssl);
15     }
16     SSL_free(ssl);
17     SSL_CTX_free(ssl_ctx);
18 }

在没有马上握手成功的时候,须要在 epoll 触发后,在次调用此段代码,来继续握手的过程。

至此,创建链接的过程就完成了。

发送与读取数据

因为发送与读取数据都有可能没有彻底完成咱们所指定的长度,因此须要判断对应返回值,来决定是否继续发送或读取

 

 

1 // 发送数据
2 int ret = SSL_write(ssl, buf + last_write_pos, buf_len - last_write_pos);
3 
4 // 读取数据
5 int ret = SSL_read(ssl, buf + last_read_pos, buf_len - last_read_pos);

 

关闭链接

 

// 关闭 ssl 链接
SSL_shutdown(ssl);
SSL_free(ssl);
SSL_CTX_free(ssl_ctx);

// 而后关闭 socket
close(sock_fd);

要点记录

在使用过程当中,总体流程是十分顺利的。一个最重要的点是关于 openssl 与 epoll 的边缘触发配合的问题。

当须要使用 epoll 的边缘触发时,必定要注意,SSL_read 最多只会读取一个完整的加密段,因此,当一次能够读取的数据量大于此值时,须要循环调用 SSL_read 直到读取失败为止。不然,就会致使在缓冲区中的数据没有彻底读取的状况。

相关文章
相关标签/搜索