ossl-guide-quic-multi-stream
名稱
ossl-guide-quic-multi-stream - OpenSSL 指南:撰寫簡易多串流 QUIC 應用程式
簡介
此頁面將介紹撰寫簡易 QUIC 多串流應用程式所需的一些重要概念。它假設您對 QUIC 及其在 OpenSSL 中的使用方式有基本了解。請參閱 ossl-guide-quic-introduction(7) 和 ossl-guide-quic-client-block(7)。
QUIC 串流
在 QUIC 多串流應用程式中,我們將 QUIC「連線」和 QUIC「串流」的概念分開。連線物件代表用戶端和伺服器之間連線的整體詳細資料,包括所有協商和設定的參數。我們在 OpenSSL 應用程式中使用 SSL 物件來表示該連線(稱為連線 SSL 物件)。它是由應用程式呼叫 SSL_new(3) 所建立。
連線本身可以有 0 個或多個串流與其關聯(儘管有 0 個串流的連線可能不太有用,因此您通常至少會有一個)。串流用於在兩個對等方之間傳送和接收資料。每個串流也由 SSL 物件所代表。串流在邏輯上獨立於與同一個連線關聯的所有其他串流。保證串流上傳送的資料會按照在該串流中傳送的順序傳遞。不同串流之間則不適用此規則,例如,如果應用程式先在串流 1 上傳送資料,然後再在串流 2 上傳送更多資料,則遠端對等方可能會在收到串流 1 上傳送的資料之前收到串流 2 上傳送的資料。
連線 SSL 物件完成交握後(即 SSL_connect(3) 已傳回 1),串流 SSL 物件會由應用程式呼叫 SSL_new_stream(3) 或 SSL_accept_stream(3) 所建立(請參閱下方的「建立新串流」)。
與大多數 OpenSSL 物件一樣,相同的執行緒規則也適用於 SSL 物件(請參閱 ossl-guide-libraries-introduction(7))。特別是,大多數 OpenSSL 函式都是執行緒安全的,但 SSL 物件並非如此。這表示您可以同時使用代表一個串流的 SSL 物件,以及另一個執行緒使用同一個連線上的不同串流的另一個 SSL 物件。但您不能同時在兩個不同的執行緒上使用同一個 SSL 物件(除非有額外的應用程式層級鎖定)。
預設串流
連線 SSL 物件也可以(選擇性地)與串流關聯。此串流稱為預設串流。當應用程式呼叫 SSL_read_ex(3)、SSL_read(3)、SSL_write_ex(3) 或 SSL_write(3) 並傳遞連線 SSL 物件作為參數時,會自動建立預設串流並與 SSL 物件關聯。
如果用戶端應用程式先呼叫 SSL_write_ex(3) 或 SSL_write(3),則(預設)預設串流將會是用戶端發起的雙向串流。如果用戶端應用程式先呼叫 SSL_read_ex(3) 或 SSL_read(3),則伺服器發起的第 1 個串流將會用作預設串流(無論它是雙向或單向)。
此行為可透過預設串流模式控制。請參閱 SSL_set_default_stream_mode(3) 以取得進一步的詳細資料。
建議新的多串流應用程式完全不使用預設串流,而應為每個使用的串流使用一個獨立的串流 SSL 物件。這需要呼叫 SSL_set_default_stream_mode(3) 並將模式設定為 SSL_DEFAULT_STREAM_MODE_NONE。
建立新串流
端點可透過呼叫 SSL_new_stream(3) 建立新串流。這會建立一個本機發起的串流。為此,您必須傳遞 QUIC 連線 SSL 物件作為參數。您也可以指定您要雙向或單向串流。
此函式會傳回一個新的 QUIC 串流 SSL 物件,用於在該串流上傳送和接收資料。
對等方也可能會發起串流。應用程式可以使用函式 SSL_get_accept_stream_queue_len(3) 來判斷對等方已發起且等待應用程式處理的串流數。應用程式可以呼叫 SSL_accept_stream(3) 來為遠端發起的串流建立新的 SSL 物件。如果對等方尚未發起任何串流,則此呼叫會封鎖,直到有一個串流可用(如果連線物件處於封鎖模式,請參閱 SSL_set_blocking_mode(3))。
在使用預設串流時,OpenSSL 會阻止接受新串流。若要覆寫此行為,您必須呼叫 SSL_set_incoming_stream_policy(3) 將政策設定為 SSL_INCOMING_STREAM_POLICY_ACCEPT。請參閱說明頁面以取得進一步的詳細資料。如果已停用預設串流(如上文 "預設串流" 所述),則這並不相關。
任何串流都可以是雙向或單向。如果是單向,則發起者可以寫入串流,但無法從串流讀取,而對等方則相反。您可以透過呼叫 SSL_get_stream_type(3) 來判斷 SSL 物件代表哪種類型的串流。請參閱說明頁面以取得進一步的詳細資料。
使用串流傳送和接收資料
一旦您擁有串流SSL物件(如果使用預設串流,則包含連線SSL物件),您就可以使用SSL_write_ex(3)、SSL_write(3)、SSL_read_ex(3)或SSL_read(3)函式透過串流傳送和接收資料。請參閱手冊頁面以取得進一步的詳細資訊。
如果其中一個函式未傳回成功碼,您應該呼叫SSL_get_error(3)以找出有關錯誤的進一步詳細資訊。在封鎖模式中,這將是致命錯誤(例如SSL_ERROR_SYSCALL或SSL_ERROR_SSL),或者當嘗試從串流讀取資料且對等方已表示串流已結束(即在串流上已發出「FIN」訊號)時,會發生SSL_ERROR_ZERO_RETURN。這表示對等方將不會在該串流上傳送更多資料。請注意,與TLS應用程式相比,QUIC應用程式的SSL_ERROR_ZERO_RETURN的解釋略有不同。在TLS中,當連線已由對等方關閉時,就會發生這種情況。在QUIC中,這只告訴您目前的串流已由對等方結束。它不會告訴您任何有關基礎連線的資訊。如果對等方已結束串流,則不會再在串流上接收更多資料,但是應用程式仍可以在串流的傳送端也結束之前,傳送資料給對等方。這可以透過應用程式呼叫SSL_stream_conclude(3)來完成。在呼叫SSL_stream_conclude(3)之後,嘗試在串流上傳送更多資料會發生錯誤。
也可以透過呼叫SSL_stream_reset(3)異常地放棄串流。
一旦不再需要串流物件,就應該透過呼叫SSL_free(3)釋放它。應用程式不應該在串流上呼叫SSL_shutdown(3),因為這只對連線層級SSL物件有意義。釋放串流會自動向對等方發出STOP_SENDING訊號。
串流和連線
給定串流物件,可以透過呼叫SSL_get0_connection(3)取得對應於連線的SSL物件。多執行緒限制適用,因此在使用傳回的連線物件時應小心。特別是,如果您在不同的執行緒中處理每個串流物件,並從該執行緒內呼叫SSL_get0_connection(3),則您必須小心不要呼叫任何使用連線物件的函式,同時其他執行緒之一也在使用該連線物件(SSL_accept_stream(3)和SSL_get_accept_stream_queue_len(3)例外,它們是執行緒安全的)。
串流物件不會從其父項SSL連線物件繼承所有設定和值。因此,與整個連線相關的某些函式呼叫在串流上將無法運作。例如,函式SSL_get_certificate(3)可用於在使用連線SSL物件呼叫時取得對等憑證的控制權。在使用串流SSL物件呼叫時,它會傳回NULL。
簡單多串流QUIC客戶端範例
此區段將提供各種原始碼範例,說明如何撰寫一個簡單的多串流QUIC客戶端應用程式,該應用程式會連線到伺服器,傳送一些HTTP/1.0要求給伺服器,並讀回回應。請注意,QUIC上的HTTP/1.0並非標準,且不會受到實際伺服器的支援。這僅供示範用途。
我們將建立在ossl-guide-quic-client-block(7)頁面上涵蓋的簡單封鎖QUIC客戶端範例程式碼上,並且假設您已熟悉它。我們只會說明簡單封鎖QUIC客戶端和多串流QUIC客戶端之間的差異。儘管範例程式碼使用封鎖SSL物件,但您也可以使用非封鎖SSL物件。請參閱ossl-guide-quic-client-non-block(7)以取得有關撰寫非封鎖QUIC客戶端的更多資訊。
此範例多串流QUIC客戶端的完整原始碼可在OpenSSL原始碼散佈的demos/guide
目錄中的quic-multi-stream.c
檔案中取得。它也可在線上取得,網址為https://github.com/openssl/openssl/blob/master/demos/guide/quic-multi-stream.c。
停用預設串流
如上文"THE DEFAULT STREAM"中所述,我們將遵循建議,為多串流客戶端停用預設串流。為執行此操作,我們呼叫SSL_set_default_stream_mode(3)函式,並傳入我們的連線SSL物件和值SSL_DEFAULT_STREAM_MODE_NONE。
/*
* We will use multiple streams so we will disable the default stream mode.
* This is not a requirement for using multiple streams but is recommended.
*/
if (!SSL_set_default_stream_mode(ssl, SSL_DEFAULT_STREAM_MODE_NONE)) {
printf("Failed to disable the default stream mode\n");
goto end;
}
建立要求串流
針對此範例的目的,我們將建立兩個不同的串流,以傳送兩個不同的HTTP要求給伺服器。為了示範的目的,第一個串流將是雙向串流,而第二個串流將是單向串流
/*
* We create two new client initiated streams. The first will be
* bi-directional, and the second will be uni-directional.
*/
stream1 = SSL_new_stream(ssl, 0);
stream2 = SSL_new_stream(ssl, SSL_STREAM_FLAG_UNI);
if (stream1 == NULL || stream2 == NULL) {
printf("Failed to create streams\n");
goto end;
}
將資料寫入串流
一旦串流建立成功,我們就可以開始寫入資料。在此範例中,我們將在每個串流上傳送不同的 HTTP 要求。為了避免重複過多程式碼,我們撰寫一個簡單的輔助函式,用於將 HTTP 要求傳送至串流
int write_a_request(SSL *stream, const char *request_start,
const char *hostname)
{
const char *request_end = "\r\n\r\n";
size_t written;
if (!SSL_write_ex(stream, request_start, strlen(request_start), &written))
return 0;
if (!SSL_write_ex(stream, hostname, strlen(hostname), &written))
return 0;
if (!SSL_write_ex(stream, request_end, strlen(request_end), &written))
return 0;
return 1;
}
我們假設字串 request1_start 和 request2_start 包含適當的 HTTP 要求。然後,我們可以呼叫上述輔助函式,以在兩個串流上傳送要求。為了簡化起見,此範例會依序執行此動作,先寫入 stream1,然後在成功後寫入 stream2。請記住,我們的用戶端會進行封鎖,因此這些呼叫只會在成功完成後才會傳回。實際應用程式不需要依序或按任何特定順序執行這些寫入動作。例如,我們可以啟動兩個執行緒(每個串流一個),並同時將要求寫入每個串流。
/* Write an HTTP GET request on each of our streams to the peer */
if (!write_a_request(stream1, request1_start, hostname)) {
printf("Failed to write HTTP request on stream 1\n");
goto end;
}
if (!write_a_request(stream2, request2_start, hostname)) {
printf("Failed to write HTTP request on stream 2\n");
goto end;
}
從串流讀取資料
在此範例中,stream1 是雙向串流,因此,一旦我們傳送要求,我們就可以嘗試從伺服器讀取回應。在此,我們只是重複呼叫 SSL_read_ex(3),直到該函式失敗(表示發生問題或對等方已將串流標示為已結束)。
printf("Stream 1 data:\n");
/*
* Get up to sizeof(buf) bytes of the response from stream 1 (which is a
* bidirectional stream). We keep reading until the server closes the
* connection.
*/
while (SSL_read_ex(stream1, buf, sizeof(buf), &readbytes)) {
/*
* 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.
*/
fwrite(buf, 1, readbytes, stdout);
}
/* In case the response didn't finish with a newline we add one now */
printf("\n");
在像這樣的封鎖應用程式中,對 SSL_read_ex(3) 的呼叫將立即成功傳回已可用的資料,或封鎖並等待更多資料可用並在資料可用時傳回,或失敗並傳回 0 回應碼。
一旦我們離開上述 while 迴圈,我們就知道對 SSL_read_ex(3) 的最後一次呼叫傳回 0 回應碼,因此我們呼叫 SSL_get_error(3) 函式以找出更多詳細資料。由於這是封鎖應用程式,因此這將傳回 SSL_ERROR_SYSCALL 或 SSL_ERROR_SSL,表示基本問題,或傳回 SSL_ERROR_ZERO_RETURN,表示串流已結束,且沒有更多資料可供讀取。必須小心區分串流層級的錯誤(例如串流重設)和連線層級的錯誤(例如連線關閉)。SSL_get_stream_read_state(3) 函式可用於區分這些不同的情況。
/*
* Check whether we finished the while loop above normally or as the
* result of an error. The 0 argument to SSL_get_error() is the return
* code we received from the SSL_read_ex() call. It must be 0 in order
* to get here. Normal completion is indicated by SSL_ERROR_ZERO_RETURN. In
* QUIC terms this means that the peer has sent FIN on the stream to
* indicate that no further data will be sent.
*/
switch (SSL_get_error(stream1, 0)) {
case SSL_ERROR_ZERO_RETURN:
/* Normal completion of the stream */
break;
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(stream1)) {
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. Skip SSL_shutdown() */
goto end;
default:
printf("Unknown stream failure\n");
break;
}
break;
default:
/* Some other unexpected error occurred */
printf ("Failed reading remaining data\n");
break;
}
接受傳入串流
我們在上面建立的 stream2 物件是一個單向串流,所以不能用來接收伺服器傳來的資料。在這個假設的範例中,我們假設伺服器會啟動一個新的串流,把我們要求的資料傳送回來。為了做到這一點,我們呼叫 SSL_accept_stream(3)。由於這是一個封鎖應用程式,所以它會無限期地等待,直到新的串流抵達,而且我們可以接受它為止。如果發生錯誤,它會傳回 NULL。
/*
* In our hypothetical HTTP/1.0 over QUIC protocol that we are using we
* assume that the server will respond with a server initiated stream
* containing the data requested in our uni-directional stream. This doesn't
* really make sense to do in a real protocol, but its just for
* demonstration purposes.
*
* We're using blocking mode so this will block until a stream becomes
* available. We could override this behaviour if we wanted to by setting
* the SSL_ACCEPT_STREAM_NO_BLOCK flag in the second argument below.
*/
stream3 = SSL_accept_stream(ssl, 0);
if (stream3 == NULL) {
printf("Failed to accept a new stream\n");
goto end;
}
現在我們可以從串流中讀取資料,方法與上面針對 stream1 所做的一樣。我們在此不重複說明。
清除串流
一旦我們使用完串流,我們可以簡單地呼叫 SSL_free(3) 來釋放它們。如果我們想要告知對等端我們不會再傳送任何資料給它們,我們可以選擇呼叫 SSL_stream_conclude(3)。不過我們在這個範例中沒有這麼做,因為我們假設 HTTP 應用程式協定會提供足夠的資訊,讓對等端知道我們已經傳送完請求資料。
我們不應該對串流物件呼叫 SSL_shutdown(3) 或 SSL_shutdown_ex(3),因為這些呼叫不應該用於串流。
SSL_free(stream1);
SSL_free(stream2);
SSL_free(stream3);
另請參閱
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)
版權
版權所有 2023 The OpenSSL Project Authors。保留所有權利。
根據 Apache License 2.0(「授權」)授權。您只能在遵守授權的條件下使用此檔案。您可以在原始程式碼散佈中的 LICENSE 檔案中取得一份副本,或在 https://www.openssl.org/source/license.html 取得。