ossl-guide-quic-client-non-block
名稱
ossl-guide-quic-client-non-block - OpenSSL 指南:撰寫非封鎖 QUIC 簡易用戶端
非封鎖 QUIC 簡易用戶端範例
此頁面將建立在 ossl-guide-quic-client-block(7) 頁面開發的範例上,說明如何撰寫非封鎖 QUIC 簡易用戶端。在此頁面中,我們將修改該示範程式碼,使其支援非封鎖功能。
此非封鎖 QUIC 用戶端範例的完整原始碼可在 OpenSSL 原始碼散佈的 demos/guide 目錄中的 quic-client-non-block.c 檔案中取得。也可以在線上取得,網址為 https://github.com/openssl/openssl/blob/master/demos/guide/quic-client-non-block.c。
正如我們在先前的範例中所見,OpenSSL QUIC 應用程式總是使用非封鎖 socket。然而,儘管如此,SSL 物件仍具有封鎖行為。當 SSL 物件具有封鎖行為時,表示當您嘗試在尚未有資料時從中讀取資料,它會等待(封鎖),直到有資料可用為止。同樣地,如果 SSL 物件目前無法寫入時,它會在寫入時等待。這可以簡化程式碼的開發,因為您不必擔心在這些情況下該怎麼辦。程式碼執行只會停止,直到它可以繼續為止。然而,在許多情況下,您不想要這種行為。您的應用程式可能需要在 SSL 物件無法讀取/寫入時執行其他任務,而不是停止並等待,例如更新 GUI 或對其他連線或串流執行作業。
我們稍後將在這個教學課程中看到如何變更 SSL 物件,使其具有非封鎖行為。對於非封鎖 SSL 物件,如果目前無法讀取或寫入,函式(例如 SSL_read_ex(3) 或 SSL_write_ex(3))將立即傳回非致命錯誤。
由於此頁面建立在 ossl-guide-quic-client-block(7) 頁面開發的範例上,我們假設您已熟悉該範例,我們只說明此範例有何不同。
在等待 socket 時執行工作
在非封鎖應用程式中,當我們想要讀取或寫入 SSL 物件,但目前無法執行時,您需要執行工作。事實上,這就是使用非封鎖 SSL 物件的重點,也就是讓應用程式有機會執行其他工作。無論應用程式必須執行什麼工作,它還必須準備好回來並定期重試先前嘗試的作業,以查看它現在是否可以完成。理想情況下,它只會在某些情況下執行此操作,例如在重試嘗試中可能會成功的情況,但這不一定是這樣。它可以隨時重試。
請注意,您務必重試上一次嘗試的相同作業。您無法開始新的作業。例如,如果您嘗試寫入文字「Hello World」,而作業因為SSL物件目前無法寫入而失敗,則您在重試作業時無法嘗試寫入其他文字。
在此示範應用程式中,我們將建立一個模擬執行其他工作的輔助函式。事實上,為了簡化起見,它除了等待基礎 Socket 的狀態變更或逾時(SSL物件的狀態可能已變更)之外,不會執行任何動作。我們將呼叫函式wait_for_activity()
。
static void wait_for_activity(SSL *ssl)
{
fd_set wfds, rfds;
int width, sock, isinfinite;
struct timeval tv;
struct timeval *tvp = NULL;
/* Get hold of the underlying file descriptor for the socket */
sock = SSL_get_fd(ssl);
FD_ZERO(&wfds);
FD_ZERO(&rfds);
/*
* Find out if we would like to write to the socket, or read from it (or
* both)
*/
if (SSL_net_write_desired(ssl))
FD_SET(sock, &wfds);
if (SSL_net_read_desired(ssl))
FD_SET(sock, &rfds);
width = sock + 1;
/*
* Find out when OpenSSL would next like to be called, regardless of
* whether the state of the underlying socket has changed or not.
*/
if (SSL_get_event_timeout(ssl, &tv, &isinfinite) && !isinfinite)
tvp = &tv;
/*
* Wait until the socket is writeable or readable. We use select here
* for the sake of simplicity and portability, but you could equally use
* poll/epoll or similar functions
*
* NOTE: For the purposes of this demonstration code this effectively
* makes this demo block until it has something more useful to do. In a
* real application you probably want to go and do other work here (e.g.
* update a GUI, or service other connections).
*
* Let's say for example that you want to update the progress counter on
* a GUI every 100ms. One way to do that would be to use the timeout in
* the last parameter to "select" below. If the tvp value is greater
* than 100ms then use 100ms instead. Then, when select returns, you
* check if it did so because of activity on the file descriptors or
* because of the timeout. If the 100ms GUI timeout has expired but the
* tvp timeout has not then go and update the GUI and then restart the
* "select" (with updated timeouts).
*/
select(width, &rfds, &wfds, NULL, tvp);
}
如果您熟悉如何為 TLS 在 OpenSSL 中撰寫非封鎖應用程式(請參閱 ossl-guide-tls-client-non-block(7)),則您應該注意到 QUIC 應用程式和 TLS 應用程式的工作方式之間有一個重要的差異。對於 TLS 應用程式,如果我們嘗試讀取或寫入SSL物件的內容,而我們收到「重試」回應(SSL_ERROR_WANT_READ 或 SSL_ERROR_WANT_WRITE),則我們可以假設這是因為 OpenSSL 嘗試讀取或寫入基礎 Socket,而 Socket 發出「重試」訊號。對於 QUIC,情況並非如此。OpenSSL 可能會因為 SSL_read_ex(3) 或 SSL_write_ex(3)(或類似)呼叫而發出重試訊號,這表示串流的狀態。這完全獨立於基礎 Socket 是否需要重試。
若要判斷 OpenSSL 目前是否想要為 QUIC 應用程式讀取或寫入基礎 Socket,我們必須呼叫 SSL_net_read_desired(3) 和 SSL_net_write_desired(3) 函式。
對於 QUIC,定期呼叫 I/O 函式(或呼叫 SSL_handle_events(3) 函式)也很重要,以確保 QUIC 連線保持正常。對於非封鎖應用程式而言,這特別重要,因為當應用程式離開執行其他工作時,您可能會讓SSL物件閒置一段時間。可以使用 SSL_get_event_timeout(3) 函式來判斷下次我們需要呼叫 I/O 函式(或呼叫 SSL_handle_events(3))的截止時間。
使用 SSL_get_event_timeout(3) 找出 OpenSSL 必須再次呼叫的下一個截止期限的替代方案是使用「執行緒輔助」模式。在「執行緒輔助」模式中,OpenSSL 會產生一個額外的執行緒,該執行緒會定期自動呼叫 SSL_handle_events(3),這表示應用程式可以安全地讓連線處於閒置狀態,因為連線仍會保持在正常狀態。請參閱下方的 "建立 SSL_CTX 和 SSL 物件" 以取得有關此項目的更多詳細資訊。
在此範例中,我們使用 select
函式檢查 Socket 的可讀性/可寫性,因為它非常容易使用,而且可在大多數作業系統上使用。不過,您可以使用任何其他類似的函式來執行相同的工作。select
會等到基礎 Socket(s) 的狀態變為可讀/可寫,或者在逾時之前才會傳回。
處理來自 OpenSSL I/O 函式的錯誤
已設定為非封鎖行為的 QUIC 應用程式需要準備好處理從 OpenSSL I/O 函式傳回的錯誤,例如 SSL_read_ex(3) 或 SSL_write_ex(3)。錯誤可能是串流的致命錯誤(例如因為串流已重設或因為基礎連線已失敗),或是非致命錯誤(例如因為我們嘗試從串流讀取資料,但對等方尚未傳送任何資料給該串流)。
SSL_read_ex(3) 和 SSL_write_ex(3) 會傳回 0 以表示錯誤,而 SSL_read(3) 和 SSL_write(3) 會傳回 0 或負值以表示錯誤。 SSL_shutdown(3) 會傳回負值以表示錯誤。
如果發生錯誤,應用程式應該呼叫 SSL_get_error(3) 以找出已發生的錯誤類型。如果錯誤是非致命錯誤且可以重試,則 SSL_get_error(3) 會傳回 SSL_ERROR_WANT_READ 或 SSL_ERROR_WANT_WRITE,具體取決於 OpenSSL 是否想要從串流讀取或寫入串流,但無法執行。請注意,呼叫 SSL_read_ex(3) 或 SSL_read(3) 仍會產生 SSL_ERROR_WANT_WRITE。類似地,呼叫 SSL_write_ex(3) 或 SSL_write(3) 可能會產生 SSL_ERROR_WANT_READ。
可能會發生的另一種類型的非致命錯誤是 SSL_ERROR_ZERO_RETURN。這表示 EOF (檔案結束),如果您嘗試從 SSL 物件讀取資料,但對等方已表示它不會在串流上傳送任何更多資料,則可能會發生這種情況。在這種情況下,您可能仍想寫入資料到串流,但您不會收到任何更多資料。
可能發生的致命錯誤為 SSL_ERROR_SYSCALL 和 SSL_ERROR_SSL。這些錯誤表示串流不再可用。例如,這可能是因為串流已被對等方重設,或因為底層連線已失敗。您可以參閱 OpenSSL 錯誤堆疊以取得進一步的詳細資料(例如,透過呼叫 ERR_print_errors(3) 來列印已發生錯誤的詳細資料)。您也可以參閱 SSL_get_stream_read_state(3) 的傳回值,以判斷錯誤是發生在串流本機,還是底層連線也已失敗。傳回值 SSL_STREAM_STATE_RESET_REMOTE 表示串流已被對等方重設,而 SSL_STREAM_STATE_CONN_CLOSED 表示底層連線已關閉。
在我們的示範應用程式中,我們將撰寫一個函式來處理來自 OpenSSL I/O 函式的這些錯誤
static int handle_io_failure(SSL *ssl, int res)
{
switch (SSL_get_error(ssl, res)) {
case SSL_ERROR_WANT_READ:
case SSL_ERROR_WANT_WRITE:
/* Temporary failure. Wait until we can read/write and try again */
wait_for_activity(ssl);
return 1;
case SSL_ERROR_ZERO_RETURN:
/* EOF */
return 0;
case SSL_ERROR_SYSCALL:
return -1;
case SSL_ERROR_SSL:
/*
* Some stream fatal error occurred. This could be because of a
* stream reset - or some failure occurred on the underlying
* connection.
*/
switch (SSL_get_stream_read_state(ssl)) {
case SSL_STREAM_STATE_RESET_REMOTE:
printf("Stream reset occurred\n");
/*
* The stream has been reset but the connection is still
* healthy.
*/
break;
case SSL_STREAM_STATE_CONN_CLOSED:
printf("Connection closed\n");
/* Connection is already closed. */
break;
default:
printf("Unknown stream failure\n");
break;
}
/*
* If the failure is due to a verification error we can get more
* information about it from SSL_get_verify_result().
*/
if (SSL_get_verify_result(ssl) != X509_V_OK)
printf("Verify error: %s\n",
X509_verify_cert_error_string(SSL_get_verify_result(ssl)));
return -1;
default:
return -1;
}
}
此函式將代表連線的 SSL 物件,以及來自失敗的 I/O 函式的傳回碼作為引數。在發生非致命錯誤的情況下,它會等到 I/O 作業重試可能會成功為止(透過使用我們在前一節中開發的 wait_for_activity()
函式)。在發生非致命錯誤(EOF 除外)時傳回 1,在發生 EOF 時傳回 0,或是在發生致命錯誤時傳回 -1。
建立 SSL_CTX 和 SSL 物件
為了連線到伺服器,我們必須為此建立 SSL_CTX 和 SSL 物件。執行此作業的大部分步驟與封鎖式用戶端相同,並在 ossl-guide-quic-client-block(7) 頁面上說明。我們在此不重複這些資訊。
一個主要的差異是我們必須將 SSL 物件設為非封鎖模式(預設為封鎖模式)。為執行此作業,我們使用 SSL_set_blocking_mode(3) 函式
/*
* The underlying socket is always nonblocking with QUIC, but the default
* behaviour of the SSL object is still to block. We set it for nonblocking
* mode in this demo.
*/
if (!SSL_set_blocking_mode(ssl, 0)) {
printf("Failed to turn off blocking mode\n");
goto end;
}
雖然我們在此開發的示範應用程式並未使用它,但在開發 QUIC 應用程式時可以使用「執行緒輔助模式」。通常,在撰寫 OpenSSL QUIC 應用程式時,重要的是定期對連線 SSL 物件呼叫 SSL_handle_events(3)(或任何 I/O 函式)以維持連線處於正常狀態。請參閱 "等待 socket 時執行工作" 以取得關於此項的更多討論。在撰寫非封鎖式 QUIC 應用程式時,特別需要記住這一點,因為在使用非封鎖模式時,通常會讓 SSL 連線物件閒置一段時間。透過使用「執行緒輔助模式」,OpenSSL 會建立一個獨立的執行緒來自動執行此作業,這表示應用程式開發人員不需要處理這個面向。為執行此作業,我們必須在建構 SSL_CTX 時使用 OSSL_QUIC_client_thread_method(3),如下所示
ctx = SSL_CTX_new(OSSL_QUIC_client_thread_method());
if (ctx == NULL) {
printf("Failed to create the SSL_CTX\n");
goto end;
}
執行握手
與封鎖式 QUIC 用戶端的示範相同,我們使用 SSL_connect(3) 函式來與伺服器執行握手。由於我們使用的是非封鎖式 SSL 物件,因此在等待伺服器回應我們的握手訊息時,呼叫此函式很可能會失敗,並出現非致命錯誤。在這種情況下,我們必須在稍後重試相同的 SSL_connect(3) 呼叫。在此示範中,我們在迴圈中執行此作業
/* Do the handshake with the server */
while ((ret = SSL_connect(ssl)) != 1) {
if (handle_io_failure(ssl, ret) == 1)
continue; /* Retry */
printf("Failed to connect to server\n");
goto end; /* Cannot retry: error */
}
我們會持續呼叫 SSL_connect(3),直到它提供給我們成功的回應。否則,我們使用我們先前建立的 handle_io_failure()
函式來找出我們下一步應該做什麼。請注意,我們不預期在此階段會發生 EOF,因此會以與致命錯誤相同的方式處理此類回應。
傳送和接收資料
與封鎖 QUIC 範例一樣,我們使用 SSL_write_ex(3) 函式將資料傳送至伺服器。與上述 SSL_connect(3) 一樣,由於我們使用非封鎖 SSL 物件,此呼叫可能會傳回非致命錯誤。在這種情況下,我們應再次嘗試完全相同的 SSL_write_ex(3) 呼叫。請注意,參數必須完全相同,亦即指向緩衝區的指標相同,且長度相同。您不得嘗試在重試時傳送不同的資料。確實存在一種選用模式 (SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER),它會設定 OpenSSL 以允許緩衝區在一次重試到下一次重試之間變更。但是,在這種情況下,您仍必須重試完全相同的資料,即使包含該資料的緩衝區位置可能會變更。請參閱 SSL_CTX_set_mode(3) 以取得進一步詳細資料。與 TLS 教學課程 (ossl-guide-tls-client-block(7)) 一樣,我們分為三個區塊撰寫請求。
/* Write an HTTP GET request to the peer */
while (!SSL_write_ex(ssl, request_start, strlen(request_start), &written)) {
if (handle_io_failure(ssl, 0) == 1)
continue; /* Retry */
printf("Failed to write start of HTTP request\n");
goto end; /* Cannot retry: error */
}
while (!SSL_write_ex(ssl, hostname, strlen(hostname), &written)) {
if (handle_io_failure(ssl, 0) == 1)
continue; /* Retry */
printf("Failed to write hostname in HTTP request\n");
goto end; /* Cannot retry: error */
}
while (!SSL_write_ex(ssl, request_end, strlen(request_end), &written)) {
if (handle_io_failure(ssl, 0) == 1)
continue; /* Retry */
printf("Failed to write end of HTTP request\n");
goto end; /* Cannot retry: error */
}
在寫入時,我們不預期會看到 EOF 回應,因此我們將此情況視為致命錯誤處理。
從伺服器讀取回應的方式類似
do {
/*
* Get up to sizeof(buf) bytes of the response. We keep reading until
* the server closes the connection.
*/
while (!eof && !SSL_read_ex(ssl, buf, sizeof(buf), &readbytes)) {
switch (handle_io_failure(ssl, 0)) {
case 1:
continue; /* Retry */
case 0:
eof = 1;
continue;
case -1:
default:
printf("Failed reading remaining data\n");
goto end; /* Cannot retry: error */
}
}
/*
* OpenSSL does not guarantee that the returned data is a string or
* that it is NUL terminated so we use fwrite() to write the exact
* number of bytes that we read. The data could be non-printable or
* have NUL characters in the middle of it. For this simple example
* we're going to print it to stdout anyway.
*/
if (!eof)
fwrite(buf, 1, readbytes, stdout);
} while (!eof);
/* In case the response didn't finish with a newline we add one now */
printf("\n");
這次的主要差異在於,當嘗試從伺服器讀取資料時,我們收到 EOF 回應是有效的。這會在伺服器在回應中傳送所有資料後關閉連線時發生。
在此範例中,我們只會列印從伺服器回應中收到的所有資料。我們會持續執行迴圈,直到我們遇到致命錯誤,或收到 EOF (表示正常結束)。
關閉連線
與 QUIC 封鎖範例一樣,我們必須在完成連線後關閉連線。
即使我們已在上述讀取的串流上收到 EOF,這並未告知我們基礎連線的狀態。我們的範例應用程式將透過 SSL_shutdown(3) 初始化連線關閉程序。
由於我們的應用程式初始化關閉,因此我們可能會預期 SSL_shutdown(3) 傳回 0,然後我們應持續呼叫它,直到我們收到傳回值 1 (表示我們已成功完成關閉)。由於我們使用非封鎖 SSL 物件,我們可能會預期必須重試此操作多次。如果 SSL_shutdown(3) 傳回負面結果,則我們必須呼叫 SSL_get_error(3) 以找出下一步該做什麼。我們使用先前開發的 handle_io_failure() 函式來執行此動作
/*
* Repeatedly call SSL_shutdown() until the connection is fully
* closed.
*/
while ((ret = SSL_shutdown(ssl)) != 1) {
if (ret < 0 && handle_io_failure(ssl, ret) == 1)
continue; /* Retry */
}
最後清理
與阻擋 QUIC 範例一樣,我們的連線完成後,我們必須釋放它。此範例執行此步驟的方式與阻擋範例相同,因此我們在此不重複說明。
進一步閱讀
請參閱 ossl-guide-quic-client-block(7),以閱讀有關如何撰寫阻擋 QUIC 範例的教學課程。請參閱 ossl-guide-quic-multi-stream(7),以了解如何撰寫多串流 QUIC 範例。
另請參閱
ossl-guide-introduction(7)、ossl-guide-libraries-introduction(7)、ossl-guide-libssl-introduction(7)、ossl-guide-quic-introduction(7)、ossl-guide-quic-client-block(7)、ossl-guide-quic-multi-stream(7)
版權
版權所有 2023 The OpenSSL Project Authors。保留所有權利。
根據 Apache 授權條款 2.0(「授權條款」)授權。您不得使用此檔案,除非符合授權條款。您可以在原始程式碼散佈中的 LICENSE 檔案或 https://www.openssl.org/source/license.html 取得一份副本。