序
在项目中需要访问 https 加密的网页,为了保证并发性,需要用到非阻塞的 socket,搜索发现,这种使用场景的相关介绍不是很多,所以这里记录一下使用的过程。
在项目中,所使用的 ssl 库是老牌 sll 库 —— openssl。所使用的 io多路复用 技术是 epoll。
核心流程
整体流程与访问非加密网站类似,不同之处在于有一下几点:
- 在 socket 建立 tcp 连接之后,需要绑定 socket 句柄在 SSL 中
- 读取,发送数据,使用 SSL 库的方法,替代 linux 系统调用
- 关闭连接前,需要先执行 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 来判断连接状态,采取不同的策略
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 的细节了,在此不展开说。
假设,现在已经连接成功,则开始做 SSL 握手之前的准备工作。
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 直到读取失败为止。否则,就会导致在缓冲区中的数据没有完全读取的情况。