【愚公系列】2022年12月 .NET CORE工具案例-.NET 7中的WebTransport通信

举报
愚公搬代码 发表于 2022/12/30 23:02:50 2022/12/30
【摘要】 前言 1.技术背景如今对于网络进行实时通讯的要求越来越高,相关于网络进行实时通讯技术应运而生,如WebRTC,QUIC,HTTP3,WebTransport,WebAssembly,WebCodecs等。 2.QUIC相关概念QUIC相关文章请看:https://blog.csdn.net/aa2528877987/article/details/127744186 3.HTTP/3.0基...

前言

1.技术背景

如今对于网络进行实时通讯的要求越来越高,相关于网络进行实时通讯技术应运而生,如WebRTC,QUIC,HTTP3,WebTransport,WebAssembly,WebCodecs等。

2.QUIC相关概念

QUIC相关文章请看:https://blog.csdn.net/aa2528877987/article/details/127744186

3.HTTP/3.0

  • 基于QUIC协议:天然复用QUIC的各种优势;
  • 新的HTTP头压缩机制:QPACK,是对HTTP/2中使用的HPACK的增强。在QPACK下,HTTP头可以在不同的QUIC流中不按顺序到达。

一、WebTransport

1.WebTransport概念

WebTransport 是一个协议框架,该协议使客户端与远程服务器在安全模型下通信,并且采用安全多路复用传输技术。最新版的WebTransport草案中,该协议是基于HTTP3的,即WebTransport可天然复用QUIC和HTTP3的特性。

WebTransport 是一个新的 Web API,使用 HTTP/3 协议来支持双向传输。它用于 Web 客户端和 HTTP/3 服务器之间的双向通信。它支持通过 不可靠的 Datagrams API 发送数据,也支持可靠的 Stream API 发送数据。

WebTransport 支持三种不同类型的流量:数据报(datagrams) 以及单向流和双向流。

WebTransport 的设计基于现代 Web 平台基本类型(比如 Streams API)。它在很大程度上依赖于 promise,并且可以很好地与 async 和 await 配合使用。

2.WebTransport在js中的使用

W3C的文档关于支持WebTransport的后台服务进行通信:https://www.w3.org/TR/webtransport/
在这里插入图片描述

let transport = new WebTransport("https://x.x.x.x");
await transport.ready;
let stream = await transport.createBidirectionalStream();
let encoder = new TextEncoder();
let writer = stream.writable.getWriter();
await writer.write(encoder.encode("Hello, world! What's your name?"))
writer.close();
console.log(await new Response(stream.readable).text());

3.WebTransport在.NET 7中的使用

3.1 创建项目

新建一个.NET Core 的空项目,修改 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端口

要设置 WebTransport 连接,首先需要配置 Web 主机并通过 HTTP/3 侦听端口:

// 配置端口
builder.WebHost.ConfigureKestrel((context, options) =>
{
    // 配置web站点端口
    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会话

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 监听WebTransport请求

var stream = await session.AcceptStreamAsync(CancellationToken.None);

var inputPipe = stream.Transport.Input;
var outputPipe = stream.Transport.Output;

4.WebTransport在JavaScript中使用

4.1 创建 WebTransport 实例

传入服务地址并创建 WebTransport 实例, transport.ready 完成,此时连接就可以使用了。

const url = 'https://localhost:5002';
const transport = new WebTransport(url);
 
await transport.ready;

4.2 通信测试

// 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.服务端

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable enable

using 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站点端口
    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}';");
    }

    // 配置服务器端应用程序
    else
    {
        //判断是否是WebTransport请求
        var feature = context.Features.GetRequiredFeature<IHttpWebTransportFeature>();

        if (!feature.IsWebTransportRequest)
        {
            await next(context);
        }
        //获取判断是否是WebTransport请求会话
        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;

    // 将流中的一些数据读入内存
    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;

    // 将流中的一些数据读入内存
    var memory = new Memory<byte>(new byte[4096]);
    while (!stream.ConnectionClosed.IsCancellationRequested)
    {
        var length = await inputPipe.AsStream().ReadAsync(memory);

        // 切片以仅保留内存的相关部分
        var outputMemory = memory[..length];

        // 处理特殊命令
        await ApplySpecialCommands(session, Encoding.Default.GetString(outputMemory.ToArray()));

        // 对数据内容进行一些操作
        outputMemory.Span.Reverse();

        // 将数据写回流
        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 expires
        if (DateTime.Parse(cert.GetExpirationDateString(), null) >= DateTimeOffset.UtcNow)
        {
            store.Close();
            return cert;
        }
    }
    // generate a new cert
    var 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 purpose
    req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection
    {
        new("1.3.6.1.5.5.7.3.1") // serverAuth
    }, false));
    // Adds usage
    req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));
    // Adds subject alternate names
    req.CertificateExtensions.Add(sanBuilder.Build());
    // Sign
    using var crt = req.CreateSelfSigned(now, now.AddDays(14)); // 14 days is the max duration of a certificate for this
    cert = new(crt.Export(X509ContentType.Pfx));

    // Save
    store.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 exists
            if (stream.writable) {
                await stream.writable.close();
            }

            // close the readable part of a stream if it exists
            if (stream.cancelReaderAndClose) {
                await stream.cancelReaderAndClose();
            }

            // close the stream if it can be closed manually
            if (stream.close) {
                await stream.close();
            }

            // remove from the list of open streams
            openStreams.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 stream
            var closed = false;
            stream.cancelReaderAndClose = () => {
                console.log(reader);
                reader.cancel();
                reader.releaseLock();
                closed = true;
            }

            // read loop for the stream
            try {
                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.效果

在这里插入图片描述

在这里插入图片描述

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。