序linux
在項目中須要訪問 https 加密的網頁,爲了保證併發性,須要用到非阻塞的 socket,搜索發現,這種使用場景的相關介紹不是不少,因此這裏記錄一下使用的過程。併發
在項目中,所使用的 ssl 庫是老牌 sll 庫 —— openssl。所使用的 io多路複用 技術是 epoll。socket
總體流程與訪問非加密網站相似,不一樣之處在於有一下幾點:tcp
創建鏈接函數
首先,打開 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 直到讀取失敗爲止。不然,就會致使在緩衝區中的數據沒有徹底讀取的狀況。