【愚公系列】2022年11月 .NET CORE工具案例-.NET 7中的WebTransport通信
文章目錄
- 前言
- 1.技術(shù)背景
- 2.QUIC相關(guān)概念
- 3.HTTP/3.0
- 一、WebTransport
- 1.WebTransport概念
- 2.WebTransport在js中的使用
- 3.WebTransport在.NET 7中的使用
- 3.1 創(chuàng)建項(xiàng)目
- 3.2 偵聽HTTP/3端口
- 3.3 獲取WebTransport會(huì)話
- 3.4 監(jiān)聽WebTransport請(qǐng)求
- 4.WebTransport在JavaScript中使用
- 4.1 創(chuàng)建 WebTransport 實(shí)例
- 4.2 通信測(cè)試
- 二、WebTransport通信完整源碼
- 1.服務(wù)端
- 2.客戶端
- 3.效果
前言
1.技術(shù)背景
如今對(duì)于網(wǎng)絡(luò)進(jìn)行實(shí)時(shí)通訊的要求越來越高,相關(guān)于網(wǎng)絡(luò)進(jìn)行實(shí)時(shí)通訊技術(shù)應(yīng)運(yùn)而生,如WebRTC,QUIC,HTTP3,WebTransport,WebAssembly,WebCodecs等。
2.QUIC相關(guān)概念
QUIC相關(guān)文章請(qǐng)看:https://blog.csdn.net/aa2528877987/article/details/127744186
3.HTTP/3.0
- 基于QUIC協(xié)議:天然復(fù)用QUIC的各種優(yōu)勢(shì);
- 新的HTTP頭壓縮機(jī)制:QPACK,是對(duì)HTTP/2中使用的HPACK的增強(qiáng)。在QPACK下,HTTP頭可以在不同的QUIC流中不按順序到達(dá)。
一、WebTransport
1.WebTransport概念
WebTransport 是一個(gè)協(xié)議框架,該協(xié)議使客戶端與遠(yuǎn)程服務(wù)器在安全模型下通信,并且采用安全多路復(fù)用傳輸技術(shù)。最新版的WebTransport草案中,該協(xié)議是基于HTTP3的,即WebTransport可天然復(fù)用QUIC和HTTP3的特性。
WebTransport 是一個(gè)新的 Web API,使用 HTTP/3 協(xié)議來支持雙向傳輸。它用于 Web 客戶端和 HTTP/3 服務(wù)器之間的雙向通信。它支持通過 不可靠的 Datagrams API 發(fā)送數(shù)據(jù),也支持可靠的 Stream API 發(fā)送數(shù)據(jù)。
WebTransport 支持三種不同類型的流量:數(shù)據(jù)報(bào)(datagrams) 以及單向流和雙向流。
WebTransport 的設(shè)計(jì)基于現(xiàn)代 Web 平臺(tái)基本類型(比如 Streams API)。它在很大程度上依賴于 promise,并且可以很好地與 async 和 await 配合使用。
2.WebTransport在js中的使用
W3C的文檔關(guān)于支持WebTransport的后臺(tái)服務(wù)進(jìn)行通信:https://www.w3.org/TR/webtransport/
3.WebTransport在.NET 7中的使用
3.1 創(chuàng)建項(xiàng)目
新建一個(gè).NET Core 的空項(xiàng)目,修改 csproj 文件
<Project Sdk="Microsoft.NET.Sdk.Web"><PropertyGroup><TargetFramework>net7.0</TargetFramework><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings><AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel><!-- 1.Turn on preview features --><EnablePreviewFeatures>True</EnablePreviewFeatures></PropertyGroup><ItemGroup><!-- 2.Turn on the WebTransport AppContext switch --><RuntimeHostConfigurationOption Include="Microsoft.AspNetCore.Server.Kestrel.Experimental.WebTransportAndH3Datagrams" Value="true" /></ItemGroup></Project>3.2 偵聽HTTP/3端口
要設(shè)置 WebTransport 連接,首先需要配置 Web 主機(jī)并通過 HTTP/3 偵聽端口:
// 配置端口 builder.WebHost.ConfigureKestrel((context, options) => {// 配置web站點(diǎn)端口options.Listen(IPAddress.Any, 5551, listenOptions =>{listenOptions.UseHttps();listenOptions.Protocols = HttpProtocols.Http1AndHttp2;});// 配置webtransport端口options.Listen(IPAddress.Any, 5552, listenOptions =>{listenOptions.UseHttps(certificate);listenOptions.UseConnectionLogging();listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;}); });3.3 獲取WebTransport會(huì)話
app.Run(async (context) => {var feature = context.Features.GetRequiredFeature<IHttpWebTransportFeature>();if (!feature.IsWebTransportRequest){return;}var session = await feature.AcceptAsync(CancellationToken.None); });await app.RunAsync();3.4 監(jiān)聽WebTransport請(qǐng)求
var stream = await session.AcceptStreamAsync(CancellationToken.None);var inputPipe = stream.Transport.Input; var outputPipe = stream.Transport.Output;4.WebTransport在JavaScript中使用
4.1 創(chuàng)建 WebTransport 實(shí)例
傳入服務(wù)地址并創(chuàng)建 WebTransport 實(shí)例, transport.ready 完成,此時(shí)連接就可以使用了。
const url = 'https://localhost:5002'; const transport = new WebTransport(url);await transport.ready;4.2 通信測(cè)試
// Send two Uint8Arrays to the server. const stream = await transport.createSendStream(); const writer = stream.writable.getWriter(); const data1 = new Uint8Array([65, 66, 67]); const data2 = new Uint8Array([68, 69, 70]); writer.write(data1); writer.write(data2); try {await writer.close();console.log('All data has been sent.'); } catch (error) {console.error(`An error occurred: ${error}`); }二、WebTransport通信完整源碼
1.服務(wù)端
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable enableusing System.Net; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core;var builder = WebApplication.CreateBuilder(args);// 生成密鑰 var certificate = GenerateManualCertificate(); var hash = SHA256.HashData(certificate.RawData); var certStr = Convert.ToBase64String(hash);// 配置端口 builder.WebHost.ConfigureKestrel((context, options) => {// 配置web站點(diǎn)端口options.Listen(IPAddress.Any, 5551, listenOptions =>{listenOptions.UseHttps();listenOptions.Protocols = HttpProtocols.Http1AndHttp2;});// 配置webtransport端口options.Listen(IPAddress.Any, 5552, listenOptions =>{listenOptions.UseHttps(certificate);listenOptions.UseConnectionLogging();listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;}); });var app = builder.Build();// 使可index.html訪問 app.UseFileServer();app.Use(async (context, next) => {// 配置/certificate.js以注入證書哈希if (context.Request.Path.Value?.Equals("/certificate.js") ?? false){context.Response.ContentType = "application/javascript";await context.Response.WriteAsync($"var CERTIFICATE = '{certStr}';");}// 配置服務(wù)器端應(yīng)用程序else{//判斷是否是WebTransport請(qǐng)求var feature = context.Features.GetRequiredFeature<IHttpWebTransportFeature>();if (!feature.IsWebTransportRequest){await next(context);}//獲取判斷是否是WebTransport請(qǐng)求會(huì)話var session = await feature.AcceptAsync(CancellationToken.None);if (session is null){return;}while (true){ConnectionContext? stream = null;IStreamDirectionFeature? direction = null;// 等待消息stream = await session.AcceptStreamAsync(CancellationToken.None);if (stream is not null){direction = stream.Features.GetRequiredFeature<IStreamDirectionFeature>();if (direction.CanRead && direction.CanWrite){//單向流_ = handleBidirectionalStream(session, stream);}else{//雙向流_ = handleUnidirectionalStream(session, stream);}}}} });await app.RunAsync();static async Task handleUnidirectionalStream(IWebTransportSession session, ConnectionContext stream) {var inputPipe = stream.Transport.Input;// 將流中的一些數(shù)據(jù)讀入內(nèi)存var memory = new Memory<byte>(new byte[4096]);while (!stream.ConnectionClosed.IsCancellationRequested){var length = await inputPipe.AsStream().ReadAsync(memory);var message = Encoding.Default.GetString(memory[..length].ToArray());await ApplySpecialCommands(session, message);Console.WriteLine("RECEIVED FROM CLIENT:");Console.WriteLine(message);} }static async Task handleBidirectionalStream(IWebTransportSession session, ConnectionContext stream) {var inputPipe = stream.Transport.Input;var outputPipe = stream.Transport.Output;// 將流中的一些數(shù)據(jù)讀入內(nèi)存var memory = new Memory<byte>(new byte[4096]);while (!stream.ConnectionClosed.IsCancellationRequested){var length = await inputPipe.AsStream().ReadAsync(memory);// 切片以僅保留內(nèi)存的相關(guān)部分var outputMemory = memory[..length];// 處理特殊命令await ApplySpecialCommands(session, Encoding.Default.GetString(outputMemory.ToArray()));// 對(duì)數(shù)據(jù)內(nèi)容進(jìn)行一些操作outputMemory.Span.Reverse();// 將數(shù)據(jù)寫回流await outputPipe.WriteAsync(outputMemory);memory.Span.Fill(0);} }static async Task ApplySpecialCommands(IWebTransportSession session, string message) {switch (message){case "Initiate Stream":var stream = await session.OpenUnidirectionalStreamAsync();if (stream is not null){await stream.Transport.Output.WriteAsync(new("Created a new stream from the client and sent this message then closing the stream."u8.ToArray()));}break;case "Abort":session.Abort(256 /*No error*/);break;default:break; // in all other cases the string is not a special command} }// Adapted from: https://github.com/wegylexy/webtransport // We will need to eventually merge this with existing Kestrel certificate generation // tracked in issue #41762 static X509Certificate2 GenerateManualCertificate() {X509Certificate2 cert;var store = new X509Store("KestrelSampleWebTransportCertificates", StoreLocation.CurrentUser);store.Open(OpenFlags.ReadWrite);if (store.Certificates.Count > 0){cert = store.Certificates[^1];// rotate key after it expiresif (DateTime.Parse(cert.GetExpirationDateString(), null) >= DateTimeOffset.UtcNow){store.Close();return cert;}}// generate a new certvar now = DateTimeOffset.UtcNow;SubjectAlternativeNameBuilder sanBuilder = new();sanBuilder.AddDnsName("localhost");using var ec = ECDsa.Create(ECCurve.NamedCurves.nistP256);CertificateRequest req = new("CN=localhost", ec, HashAlgorithmName.SHA256);// Adds purposereq.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection{new("1.3.6.1.5.5.7.3.1") // serverAuth}, false));// Adds usagereq.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));// Adds subject alternate namesreq.CertificateExtensions.Add(sanBuilder.Build());// Signusing var crt = req.CreateSelfSigned(now, now.AddDays(14)); // 14 days is the max duration of a certificate for thiscert = new(crt.Export(X509ContentType.Pfx));// Savestore.Add(cert);store.Close();return cert; }2.客戶端
<!DOCTYPE html> <html> <head><meta charset="utf-8" /><title>WebTransport Test Page</title> </head> <body><script src="certificate.js"></script><div id="panel"><h1 id="title">WebTransport Test Page</h1><h3 id="stateLabel">Ready to connect...</h3><div><label for="connectionUrl">WebTransport Server URL:</label><input id="connectionUrl" value="https://127.0.0.1:5552" disabled /><p>Due to the need to synchronize certificates, you cannot modify the url at this time.</p><button id="connectButton" type="submit" onclick="connect()">Connect</button></div><div id="communicationPanel" hidden><div><button id="closeStream" type="submit" onclick="closeActiveStream()">Close active stream</button><button id="closeConnection" type="submit" onclick="closeConnection()">Close connection</button><button id="createBidirectionalStream" type="submit" onclick="createStream('bidirectional')">Create bidirectional stream</button><button id="createUnidirectionalStream" type="submit" onclick="createStream('output unidirectional')">Create unidirectional stream</button></div><h2>Open Streams</h2><div id="streamsList"><p id="noStreams">Empty</p></div><div><h2>Send Message</h2><textarea id="messageInput" name="text" rows="12" cols="50"></textarea><button id="sendMessageButton" type="submit" onclick="sendMessage()">Send</button></div></div></div><div id="communicationLogPanel"><h2 style="text-align: center;">Communication Log</h2><table style="overflow: auto; max-height: 1000px;"><thead><tr><td>Timestamp</td><td>From</td><td>To</td><td>Data</td></tr></thead><tbody id="commsLog"></tbody></table></div><script>let connectionUrl = document.getElementById("connectionUrl");let messageInput = document.getElementById("messageInput");let stateLabel = document.getElementById("stateLabel");let commsLog = document.getElementById("commsLog");let streamsList = document.getElementById("streamsList");let communicationPanel = document.getElementById("communicationPanel");let noStreamsText = document.getElementById("noStreams");let session;let connected = false;let openStreams = [];async function connect() {if (connected) {alert("Already connected!");return;}communicationPanel.hidden = true;stateLabel.innerHTML = "Ready to connect...";session = new WebTransport(connectionUrl.value, {serverCertificateHashes: [{algorithm: "sha-256",value: Uint8Array.from(atob(CERTIFICATE), c => c.charCodeAt(0))}]});stateLabel.innerHTML = "Connecting...";await session.ready;startListeningForIncomingStreams();setConnection(true);}async function closeConnection() {if (!connected) {alert("Not connected!");return;}for (let i = 0; i < openStreams.length; i++) {await closeStream(i);}await session.close();setConnection(false);openStreams = [];updateOpenStreamsList();}function setConnection(state) {connected = state;communicationPanel.hidden = !state;let msg = state ? "Connected!" : "Disconnected!";stateLabel.innerHTML = msg;addToCommsLog("Client", "Server", msg);}function updateOpenStreamsList() {streamsList.innerHTML = "";for (let i = 0; i < openStreams.length; i++) {streamsList.innerHTML += '<div>' +`<input type="radio" name = "streams" value = "${i}" id = "${i}" >` +`<label for="${i}">${i} - ${openStreams[i].type}</label>` +'</div >';}noStreamsText.hidden = openStreams.length > 0;}async function closeActiveStream() {let streamIndex = getActiveStreamIndex();await closeStream(streamIndex);}async function closeStream(index) {let stream = openStreams[index].stream;if (!stream) {return;}// close the writable part of a stream if it existsif (stream.writable) {await stream.writable.close();}// close the readable part of a stream if it existsif (stream.cancelReaderAndClose) {await stream.cancelReaderAndClose();}// close the stream if it can be closed manuallyif (stream.close) {await stream.close();}// remove from the list of open streamsopenStreams.splice(index, 1);updateOpenStreamsList();}async function startListeningForIncomingStreams() {try {let streamReader = session.incomingUnidirectionalStreams.getReader();let stream = await streamReader.read();if (stream.value && confirm("New incoming stream. Would you like to accept it?")) {startListeningForIncomingData(stream.value, openStreams.length, "input unidirectional");// we don't add to the stream list here as the only stream type that we can receive is// input. As we can't send data over these streams, there is no point in showing them to the user}} catch {alert("Failed to accept incoming stream");}}async function startListeningForIncomingData(stream, streamId, type) {let reader = isBidirectional(type) ? stream.readable.getReader() : stream.getReader();// define a function that we can use to abort the reading on this streamvar closed = false;stream.cancelReaderAndClose = () => {console.log(reader);reader.cancel();reader.releaseLock();closed = true;}// read loop for the streamtry {while (true) {let data = await reader.read();let msgOut = "";data.value.forEach(x => msgOut += String.fromCharCode(x));addToCommsLog("Server", "Client", `RECEIVED FROM STREAM ${streamId}: ${msgOut}`);}} catch {alert(`Stream ${streamId} ${closed ? "was closed" : "failed to read"}. Ending reading from it.`);}}async function createStream(type) {if (!connected) {return;}let stream;switch (type) {case 'output unidirectional':stream = await session.createUnidirectionalStream();break;case 'bidirectional':stream = await session.createBidirectionalStream();startListeningForIncomingData(stream, openStreams.length, "bidirectional");break;default:alert("Unknown stream type");return;}addStream(stream, type);}function addStream(stream, type) {openStreams.push({ stream: stream, type: type });updateOpenStreamsList();addToCommsLog("Client", "Server", `CREATING ${type} STREAM WITH ID ${openStreams.length}`);}async function sendMessage() {if (!connected) {return;}let activeStreamIndex = getActiveStreamIndex();if (activeStreamIndex == -1) {alert((openStreams.length > 0) ? "Please select a stream first" : "Please create a stream first");}let activeStreamObj = openStreams[activeStreamIndex];let activeStream = activeStreamObj.stream;let activeStreamType = activeStreamObj.type;let writer = isBidirectional(activeStreamType) ? activeStream.writable.getWriter() : activeStream.getWriter();let msg = messageInput.value.split("").map(x => (x).charCodeAt(0));await writer.write(new Uint8Array(msg));writer.releaseLock();addToCommsLog("Client", "Server", `SENDING OVER STREAM ${activeStreamIndex}: ${messageInput.value}`);}function isBidirectional(type) {return type === "bidirectional";}function getActiveStream() {let index = getActiveStreamIndex();return (index === -1) ? null : openStreams[index].stream;}function getActiveStreamIndex() {let allStreams = document.getElementsByName("streams");for (let i = 0; i < allStreams.length; i++) {if (allStreams[i].checked) {return i;}}return -1;}function addToCommsLog(from, to, data) {commsLog.innerHTML += '<tr>' +`<td>${getTimestamp()}</td>` +`<td>${from}</td>` +`<td>${to}</td>` +`<td>${data}</td>`'</tr>';}function getTimestamp() {let now = new Date();return `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}.${now.getMilliseconds()}`;}</script> </body> </html> <style>html,body {background-color: #459eda;padding: 0;margin: 0;height: 100%;}body {display: flex;flex-direction: row;}#panel {background-color: white;padding: 12px;margin: 0;border-top-right-radius: 12px;border-bottom-right-radius: 12px;border-right: #0d6cad 5px solid;max-width: 400px;min-width: 200px;flex: 1;min-height: 1200px;overflow: auto;}#panel > * {text-align: center;}#communicationLogPanel {padding: 24px;flex: 1;}#communicationLogPanel > * {color: white;width: 100%;}#connectButton {background-color: #2e64a7;}#messageInput {max-width: 100%;}#streamsList {max-height: 400px;overflow: auto;}input {padding: 6px;margin-bottom: 8px;margin-right: 0;}button {background-color: #459eda;border-radius: 5px;border: 0;text-align: center;color: white;padding: 8px;margin: 4px;width: 100%;} </style>3.效果
總結(jié)
以上是生活随笔為你收集整理的【愚公系列】2022年11月 .NET CORE工具案例-.NET 7中的WebTransport通信的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ArcGIS专题制图(一):制图中如何给
- 下一篇: java 图片合成 工具类_Java实现