OpenSSL

密碼編譯和 SSL/TLS 工具包

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_SYSCALLSSL_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_startrequest2_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_SYSCALLSSL_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 取得。