Bagikan melalui


Dukungan WebSockets di .NET

Protokol WebSocket memungkinkan komunikasi dua arah antara klien dan host jarak jauh. System.Net.WebSockets.ClientWebSocket mengekspos kemampuan untuk membuat koneksi WebSocket melalui jabat tangan pembuka, itu dibuat dan dikirim oleh metode ConnectAsync.

Perbedaan dalam HTTP/1.1 dan HTTP/2 WebSockets

WebSocket melalui HTTP/1.1 menggunakan satu koneksi TCP, oleh karena itu dikelola oleh header di seluruh koneksi, untuk informasi selengkapnya, lihat RFC 6455. Pertimbangkan contoh berikut tentang cara membuat WebSocket melalui HTTP/1.1:

Uri uri = new("ws://corefx-net-http11.azurewebsites.net/WebSocket/EchoWebSocket.ashx");

using ClientWebSocket ws = new();
await ws.ConnectAsync(uri, default);

var bytes = new byte[1024];
var result = await ws.ReceiveAsync(bytes, default);
string res = Encoding.UTF8.GetString(bytes, 0, result.Count);

await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client closed", default);

Pendekatan yang berbeda harus diambil dengan HTTP/2 karena sifat multiplexing-nya. WebSocket dibuat per aliran, untuk informasi selengkapnya, lihat RFC 8441. Dengan HTTP/2 dimungkinkan untuk menggunakan satu koneksi untuk beberapa aliran soket web bersama dengan aliran HTTP biasa dan memperluas penggunaan jaringan HTTP/2 yang lebih efisien ke WebSocket. Ada kelebihan beban khusus ConnectAsync(Uri, HttpMessageInvoker, CancellationToken) yang menerima HttpMessageInvoker untuk memungkinkan penggunaan kembali koneksi terkumpul yang ada:

using SocketsHttpHandler handler = new();
using ClientWebSocket ws = new();
await ws.ConnectAsync(uri, new HttpMessageInvoker(handler), cancellationToken);

Menyiapkan versi dan kebijakan HTTP

Secara default, ClientWebSocket menggunakan HTTP/1.1 untuk mengirim jabat tangan pembuka dan memungkinkan penurunan tingkat. Dalam .NET 7, soket web melalui HTTP/2 tersedia. Ini dapat diubah sebelum memanggil ConnectAsync:

using SocketsHttpHandler handler = new();
using ClientWebSocket ws = new();

ws.Options.HttpVersion = HttpVersion.Version20;
ws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;

await ws.ConnectAsync(uri, new HttpMessageInvoker(handler), cancellationToken);

Opsi yang tidak kompatibel

ClientWebSocket memiliki properti System.Net.WebSockets.ClientWebSocketOptions yang dapat disiapkan pengguna sebelum koneksi dibuat. Namun, ketika HttpMessageInvoker disediakan, ia juga memiliki sifat-sifat ini. Untuk menghindari ambiguitas, dalam hal ini, properti harus diatur pada HttpMessageInvoker, dan ClientWebSocketOptions harus memiliki nilai default. Jika tidak, jika ClientWebSocketOptions diubah, kelebihan beban ConnectAsync akan melemparkan ArgumentException.

using HttpClientHandler handler = new()
{
    CookieContainer = cookies;
    UseCookies = cookies != null;
    ServerCertificateCustomValidationCallback = remoteCertificateValidationCallback;
    Credentials = useDefaultCredentials
        ? CredentialCache.DefaultCredentials
        : credentials;
};
if (proxy is null)
{
    handler.UseProxy = false;
}
else
{
    handler.Proxy = proxy;
}
if (clientCertificates?.Count > 0)
{
    handler.ClientCertificates.AddRange(clientCertificates);
}
HttpMessageInvoker invoker = new(handler);
using ClientWebSocket cws = new();
await cws.ConnectAsync(uri, invoker, cancellationToken);

Kompresi

Protokol WebSocket mendukung deflate per pesan seperti yang didefinisikan dalam RFC 7692. Ini dikendalikan oleh System.Net.WebSockets.ClientWebSocketOptions.DangerousDeflateOptions. Saat ada, opsi dikirim ke server selama fase jabat tangan. Jika server mendukung per-message-deflate dan opsi diterima, instans ClientWebSocket akan dibuat dengan pemadatan diaktifkan secara default untuk semua pesan.

using ClientWebSocket ws = new()
{
    Options =
    {
        DangerousDeflateOptions = new WebSocketDeflateOptions()
        {
            ClientMaxWindowBits = 10,
            ServerMaxWindowBits = 10
        }
    }
};

Penting

Sebelum menggunakan kompresi, perlu diketahui bahwa mengaktifkannya membuat aplikasi tunduk pada jenis serangan KRIMINAL/PELANGGARAN, untuk informasi selengkapnya, lihat CRIME dan BREACH. Sangat disarankan untuk menonaktifkan pemadatan saat mengirim data yang berisi rahasia dengan menentukan bendera DisableCompression untuk pesan tersebut.

Keep-Alive strategi

Pada .NET 8 dan versi sebelumnya, satu-satunya strategi Keep-Alive yang tersedia adalah PONG yang tidak diminta. Strategi ini cukup untuk menjaga koneksi TCP dasar tetap aktif. Namun, dalam kasus ketika host jarak jauh menjadi tidak responsif (misalnya, server jarak jauh mengalami crash), satu-satunya cara untuk mendeteksi situasi tersebut dengan Unsolicited PONG adalah dengan bergantung pada batas waktu TCP.

.NET 9 memperkenalkan strategi PING/PONG Keep-Alive yang telah diinginkan sejak lama, melengkapi pengaturan KeepAliveInterval yang ada dengan pengaturan KeepAliveTimeout baru. Dimulai dengan .NET 9, strategi Keep-Alive dipilih sebagai berikut:

  1. Keep-Alive MATI, jika
    • KeepAliveInterval TimeSpan.Zero atau Timeout.InfiniteTimeSpan
  2. PONG yang tidak diminta, jika
    • KeepAliveInterval adalah TimeSpanterbatas positif, -AND-
    • KeepAliveTimeout TimeSpan.Zero atau Timeout.InfiniteTimeSpan
  3. PING/PONG, jika
    • KeepAliveInterval adalah TimeSpanterbatas positif, -AND-
    • KeepAliveTimeout adalah positif terbatas TimeSpan

Nilai KeepAliveTimeout default adalah Timeout.InfiniteTimeSpan, sehingga perilaku Keep-Alive default tetap konsisten di antara versi .NET.

Jika Anda menggunakan ClientWebSocket, nilai ClientWebSocketOptions.KeepAliveInterval default adalah WebSocket.DefaultKeepAliveInterval (biasanya 30 detik). Itu berarti, ClientWebSocket memiliki Keep-Alive AKTIF secara bawaan, dengan PONG yang tidak diminta sebagai strategi default.

Jika Anda ingin beralih ke strategi PING/PONG, menggantikan ClientWebSocketOptions.KeepAliveTimeout sudah memadai.

var ws = new ClientWebSocket();
ws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(20);
await ws.ConnectAsync(uri, cts.Token);

Untuk WebSocketdasar , Keep-Alive NONAKTIF secara default. Jika Anda ingin menggunakan strategi PING/PONG, baik WebSocketCreationOptions.KeepAliveInterval maupun WebSocketCreationOptions.KeepAliveTimeout perlu disetel.

var options = new WebSocketCreationOptions()
{
    KeepAliveInterval = WebSocket.DefaultKeepAliveInterval,
    KeepAliveTimeout = TimeSpan.FromSeconds(20)
};
var ws = WebSocket.CreateFromStream(stream, options);

Jika strategi PONG yang tidak diminta digunakan, bingkai PONG digunakan sebagai heartbeat searah. Mereka dikirim secara teratur dengan interval KeepAliveInterval, terlepas dari apakah titik akhir jarak jauh berkomunikasi atau tidak.

Jika strategi PING/PONG aktif, bingkai PING dikirim setelah waktu KeepAliveInterval berlalu sejak komunikasi terakhir dari titik akhir jarak jauh. Setiap bingkai PING berisi token bilangan bulat untuk dipasangkan dengan respons PONG yang diharapkan. Jika tidak ada respons PONG yang tiba setelah KeepAliveTimeout berlalu, titik akhir jarak jauh dianggap tidak responsif, dan koneksi WebSocket secara otomatis dibatalkan.

var ws = new ClientWebSocket();
ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(10);
ws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(10);
await ws.ConnectAsync(uri, cts.Token);

// NOTE: There must be an outstanding read at all times to ensure
// incoming PONGs are processed
var result = await _webSocket.ReceiveAsync(buffer, cts.Token);

Jika batas waktu berlalu, ReceiveAsync yang luar biasa akan melemparkan OperationCanceledException:

System.OperationCanceledException: Aborted
 ---> System.AggregateException: One or more errors occurred. (The WebSocket didn't receive a Pong frame in response to a Ping frame within the configured KeepAliveTimeout.) (Unable to read data from the transport connection: The I/O operation has been aborted because of either a thread exit or an application request..)
 ---> System.Net.WebSockets.WebSocketException (0x80004005): The WebSocket didn't receive a Pong frame in response to a Ping frame within the configured KeepAliveTimeout.
   at System.Net.WebSockets.ManagedWebSocket.KeepAlivePingHeartBeat()
...

Lanjutkan Membaca untuk Memproses PONGs

Nota

Saat ini, WebSocket HANYA memproses bingkai masuk ketika ada ReceiveAsync yang sedang menunggu.

Penting

Jika Anda ingin menggunakan Keep-Alive batas waktu, penting bahwa respons PONG segera diproses. Bahkan jika endpoint jarak jauh hidup dan mengirim respons PONG dengan benar, tetapi WebSocket tidak memproses bingkai masuk, mekanisme Keep-Alive dapat mengeluarkan pembatalan "positif palsu." Masalah ini dapat terjadi jika bingkai PONG tidak pernah diambil dari aliran transportasi sebelum batas waktu berlalu.

Untuk menghindari merusak koneksi yang baik, pengguna disarankan untuk mempertahankan pembacaan yang tertunda di semua WebSocket yang telah dikonfigurasi dengan Waktu Habis Keep-Alive.