序
在項目中需要訪問 https 加密的網頁,為了保證並發性,需要用到非阻塞的 socket,搜索發現,這種使用場景的相關介紹不是很多,所以這裡記錄一下使用的過程。
在項目中,所使用的 ssl 庫是老牌 sll 庫 —— openssl。所使用的 io多路復用 技術是 epoll。
整體流程與訪問非加密網站類似,不同之處在於有一下幾點:
建立連接
首先,打開 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 直到讀取失敗為止。否則,就會導致在緩沖區中的數據沒有完全讀取的情況。