Web SSH 的原理与在 ASP.NET Core SignalR 中的实现

这篇具有很好参考价值的文章主要介绍了Web SSH 的原理与在 ASP.NET Core SignalR 中的实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

有个项目,需要在前端有个管理终端可以 SSH 到主控机的终端,如果不考虑用户使用 vim 等需要在控制台内现实界面的软件的话,其实使用 Process 类型去启动相应程序就够了。而这次的需求则需要考虑用户会做相关设置。

原理

这里用到的原理是伪终端。伪终端(pseudo terminal)是现代操作系统的一个功能,他会模拟一对输入输出设备来模拟终端环境去执行相应的进程。伪终端通常会给相应的进程提供例如环境变量或文件等来告知他在终端中运行,这样像 vim 这样的程序可以在最后一行输出命令菜单或者像 npm / pip 这样的程序可以打印炫酷的进度条。通常在我们直接创建子进程的时候,在 Linux 上系统自带了 openpty 方法可以打开伪终端,而在 Windows 上则等到 Windows Terminal 推出后才出现了真正的系统级伪终端。下面付一张来自微软博客的伪终端原理图,Linux 上的原理与之类似。

Web SSH 的原理与在 ASP.NET Core SignalR 中的实现

基本设计

建立连接与监听终端输出

Web SSH 的原理与在 ASP.NET Core SignalR 中的实现

监听前端输入

graph TD; A[终端窗口收到键盘事件] --> B[SignalR 发送请求]; B --> C[后台转发到对应终端]

超时与关闭

graph TD; A[当 SignalR 发送断开连接或终端超时] --> B[关闭终端进程];

依赖库

portable_pty

这里用到这个 Rust 库来建立终端,这个库是一个独立的进程,每次建立连接都会运行。这里当初考虑过直接在 ASP.NET Core 应用里调用 vs-pty(微软开发的,用在 vs 里的库,可以直接在 vs 安装位置复制一份),但是 vs-pty 因为种种原因在 .NET 7 + Ubuntu 22.04 的环境下运行不起来故放弃了。

xterm.js

这个是前端展示终端界面用的库,据说 vs code 也在用这个库,虽然文档不多,但是用起来真的很简单。

SignalR

这个不多说了,咱 .NET 系列 Web 实时通信选他就没错。

代码

废话不多讲了,咱还是直接看代码吧,这里代码还是比较长的,我节选了一些必要的代码。具体 SignalR 之类的配置,还请读者自行参考微软官方文档。文章来源地址https://www.toymoban.com/news/detail-722139.html

  1. main.rs 这个 Rust 代码用于建立伪终端并和 .NET 服务通信,这里使用了最简单的 UDP 方式通信。
use portable_pty::{self, native_pty_system, CommandBuilder, PtySize};
use std::{io::prelude::*, sync::Arc};
use tokio::net::UdpSocket;
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = std::env::args().collect::<Vec<_>>();
    // 启动一个终端
    let pty_pair = native_pty_system().openpty(PtySize {
        rows: args.get(2).ok_or("NoNumber")?.parse()?,
        cols: args.get(3).ok_or("NoNumber")?.parse()?,
        pixel_width: 0,
        pixel_height: 0,
    })?;
    // 执行传进来的命令
    let mut cmd = CommandBuilder::new(args.get(4).unwrap_or(&"bash".to_string()));
    if args.len() > 5 {
        cmd.args(&args[5..]);
    }
    let mut proc = pty_pair.slave.spawn_command(cmd)?;
    // 绑定输入输出
    let mut reader = pty_pair.master.try_clone_reader()?;
    let mut writer = pty_pair.master.take_writer()?;
    // 绑定网络
    let main_socket = Arc::new(UdpSocket::bind("localhost:0").await?);
    let recv_socket = main_socket.clone();
    let send_socket = main_socket.clone();
    let resize_socket = UdpSocket::bind("localhost:0").await?;
    // 连接到主服务后发送地址
    main_socket
        .connect(args.get(1).ok_or("NoSuchAddr")?)
        .await?;
    main_socket
        .send(&serde_json::to_vec(&ClientAddr {
            main: main_socket.local_addr()?.to_string(),
            resize: resize_socket.local_addr()?.to_string(),
        })?)
        .await?;
    // 读取终端数据并发送
    let read = tokio::spawn(async move {
        loop {
            let mut buf = [0; 1024];
            let n = reader.read(&mut buf).unwrap();
            if n == 0 {
                continue;
            }
            println!("{:?}", &buf[..n]);
            send_socket.send(&buf[..n]).await.unwrap();
        }
    });
    // 接收数据并写入终端
    let write = tokio::spawn(async move {
        loop {
            let mut buf = [0; 1024];
            let n = recv_socket.recv(&mut buf).await.unwrap();
            if n == 0 {
                continue;
            }
            println!("{:?}", &buf[..n]);
            writer.write_all(&buf[..n]).unwrap();
        }
    });
    // 接收调整窗口大小的数据
    let resize = tokio::spawn(async move {
        let mut buf = [0; 1024];
        loop {
            let n = resize_socket.recv(&mut buf).await.unwrap();
            if n == 0 {
                continue;
            }
            let size: WinSize = serde_json::from_slice(buf[..n].as_ref()).unwrap();
            pty_pair
                .master
                .resize(PtySize {
                    rows: size.rows,
                    cols: size.cols,
                    pixel_width: 0,
                    pixel_height: 0,
                })
                .unwrap();
        }
    });
    // 等待进程结束
    let result = proc.wait()?;
    write.abort();
    read.abort();
    resize.abort();
    if 0 == result.exit_code() {
        std::process::exit(result.exit_code() as i32);
    }
    return Ok(());
}
/// 窗口大小
#[derive(serde::Deserialize)]
struct WinSize {
    /// 行数
    rows: u16,
    /// 列数
    cols: u16,
}
/// 客户端地址
#[derive(serde::Serialize)]
struct ClientAddr {
    /// 主要地址
    main: String,
    /// 调整窗口大小地址
    resize: String,
}
  1. SshPtyConnection.cs 这个代码用于维持一个后台运行的 Rust 进程,并管理他的双向通信。
    public class SshPtyConnection : IDisposable
    {
        /// <summary>
        /// 客户端地址
        /// </summary>
        private class ClientEndPoint
        {
            public required string Main { get; set; }
            public required string Resize { get; set; }
        }
        /// <summary>
        /// 窗口大小
        /// </summary>
        private class WinSize
        {
            public int Cols { get; set; }
            public int Rows { get; set; }
        }
        /// <summary>
        /// SignalR 上下文
        /// </summary>
        private readonly IHubContext<SshHub> _hubContext;
        /// <summary>
        /// 日志记录器
        /// </summary>
        private readonly ILogger<SshPtyConnection> _logger;
        /// <summary>
        /// UDP 客户端
        /// </summary>
        private readonly UdpClient udpClient;
        /// <summary>
        /// 最后活动时间
        /// </summary>
        private DateTime lastActivity = DateTime.UtcNow;
        /// <summary>
        /// 是否已释放
        /// </summary>
        private bool disposedValue;
        /// <summary>
        /// 是否已释放
        /// </summary>
        public bool IsDisposed => disposedValue;
        /// <summary>
        /// 最后活动时间
        /// </summary>
        public DateTime LastActivity => lastActivity;
        /// <summary>
        /// 取消令牌
        /// </summary>
        public CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource();
        /// <summary>
        /// 窗口大小
        /// </summary>
        public event EventHandler<EventArgs> Closed = delegate { };
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="hubContext"></param>
        /// <param name="logger"></param>
        /// <exception cref="ArgumentNullException"></exception>
        public SshPtyConnection(IHubContext<SshHub> hubContext, ILogger<SshPtyConnection> logger)
        {
            _hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            lastActivity = DateTime.Now;
            udpClient = new(IPEndPoint.Parse("127.0.0.1:0"));
        }
        /// <summary>
        /// 开始监听
        /// </summary>
        /// <param name="connectionId">连接 ID</param>
        /// <param name="username">用户名</param>
        /// <param name="height">行数</param>
        /// <param name="width">列数</param>
        public async void StartAsync(string connectionId, string username, int height, int width)
        {
            var token = CancellationTokenSource.Token;
            _logger.LogInformation("process starting");
            // 启动进程
            using var process = Process.Start(new ProcessStartInfo
            {
                FileName = OperatingSystem.IsOSPlatform("windows") ? "PtyWrapper.exe" : "pty-wrapper",
                // 这里用了 su -l username,因为程序直接部署在主控机的 root 下,所以不需要 ssh 只需要切换用户即可,如果程序部署在其他机器上,需要使用 ssh
                ArgumentList = { udpClient.Client.LocalEndPoint!.ToString() ?? "127.0.0.1:0", height.ToString(), width.ToString(), "su", "-l", username }
            });
            // 接收客户端地址
            var result = await udpClient.ReceiveAsync();
            var clientEndPoint = await JsonSerializer.DeserializeAsync<ClientEndPoint>(new MemoryStream(result.Buffer), new JsonSerializerOptions
            {
                PropertyNameCaseInsensitive = true
            });
            if (clientEndPoint == null)
            {
                CancellationTokenSource.Cancel();
                return;
            }
            process!.Exited += (_, _) => CancellationTokenSource.Cancel();
            var remoteEndPoint = IPEndPoint.Parse(clientEndPoint.Main);
            udpClient.Connect(remoteEndPoint);
            var stringBuilder = new StringBuilder();
            // 接收客户端数据,并发送到 SignalR,直到客户端断开连接或者超时 10 分钟
            while (!token.IsCancellationRequested && lastActivity.AddMinutes(10) > DateTime.Now && !(process?.HasExited ?? false))
            {
                try
                {
                    lastActivity = DateTime.Now;
                    var buffer = await udpClient.ReceiveAsync(token);
                    await _hubContext.Clients.Client(connectionId).SendAsync("WriteDataAsync", Encoding.UTF8.GetString(buffer.Buffer));
                    stringBuilder.Clear();
                }
                catch (Exception e)
                {
                    _logger.LogError(e, "ConnectionId: {ConnectionId} Unable to read data and send message.", connectionId);
                    break;
                }
            }
            // 如果客户端断开连接或者超时 10 分钟,关闭进程
            if (process?.HasExited ?? false) process?.Kill();
            if (lastActivity.AddMinutes(10) < DateTime.Now)
            {
                _logger.LogInformation("ConnectionId: {ConnectionId} Pty session has been closed because of inactivity.", connectionId);
                try
                {
                    await _hubContext.Clients.Client(connectionId).SendAsync("WriteErrorAsync", "InactiveTimeTooLong");
                }
                catch (Exception e)
                {
                    _logger.LogError(e, "ConnectionId: {ConnectionId} Unable to send message.", connectionId);
                }
            }
            if (token.IsCancellationRequested)
            {
                _logger.LogInformation("ConnectionId: {ConnectionId} Pty session has been closed because of session closed.", connectionId);
                try
                {
                    await _hubContext.Clients.Client(connectionId).SendAsync("WriteErrorAsync", "SessionClosed");
                }
                catch (Exception e)
                {
                    _logger.LogError(e, "ConnectionId: {ConnectionId} Unable to send message.", connectionId);
                }
            }
            Dispose();
        }
        /// <summary>
        /// 接收 SignalR 数据,并发送到客户端
        /// </summary>
        /// <param name="data">数据</param>
        /// <returns></returns>
        /// <exception cref="AppException"></exception>
        public async Task WriteDataAsync(string data)
        {
            if (disposedValue)
            {
                throw new AppException("SessionClosed");
            }
            try
            {
                lastActivity = DateTime.Now;
                await udpClient.SendAsync(Encoding.UTF8.GetBytes(data));
            }
            catch (Exception e)
            {
                CancellationTokenSource.Cancel();
                Dispose();
                throw new AppException("SessionClosed", e);
            }
        }
        /// <summary>
        /// 回收资源
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    CancellationTokenSource.Cancel();
                    udpClient.Dispose();
                }
                disposedValue = true;
                Closed(this, new EventArgs());
            }
        }
        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }
  1. SshService 这段代码用于管理 SshPtyConnection 和 SignalR 客户端连接之间的关系
    public class SshService : IDisposable
    {
        private bool disposedValue;
        private readonly IHubContext<SshHub> _hubContext;
        private readonly ILoggerFactory _loggerFactory;
        private Dictionary<string, SshPtyConnection> _connections;

        public SshService(IHubContext<SshHub> hubContext, ILoggerFactory loggerFactory)
        {
            _hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext));
            _connections = new Dictionary<string, SshPtyConnection>();
            _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
        }

        /// <summary>
        /// 创建终端连接
        /// </summary>
        /// <param name="connectionId">连接 ID</param>
        /// <param name="username">用户名</param>
        /// <param name="height">行数</param>
        /// <param name="width">列数</param>
        /// <returns></returns>
        /// <exception cref="InvalidOperationException"></exception>
        public Task CreateConnectionAsync(string connectionId, string username, int height, int width)
        {
            if (_connections.ContainsKey(connectionId))
                throw new InvalidOperationException();
            var connection = new SshPtyConnection(_hubContext, _loggerFactory.CreateLogger<SshPtyConnection>());
            connection.Closed += (sender, args) =>
            {
                _hubContext.Clients.Client(connectionId).SendAsync("WriteErrorAsync", "SessionClosed");
                _connections.Remove(connectionId);
            };
            _connections.Add(connectionId, connection);
            // 运行一个后台线程
            connection.StartAsync(connectionId, username, height, width);
            return Task.CompletedTask;
        }
        /// <summary>
        /// 写入数据
        /// </summary>
        /// <param name="connectionId">连接 ID</param>
        /// <param name="data">数据</param>
        /// <exception cref="AppException"></exception>
        public async Task ReadDataAsync(string connectionId, string data)
        {
            if (_connections.TryGetValue(connectionId, out var connection))
            {
                await connection.WriteDataAsync(data);
            }
            else
                throw new AppException("SessionClosed");
        }
        /// <summary>
        /// 关闭连接
        /// </summary>
        /// <param name="connectionId">连接 ID</param>
        /// <exception cref="AppException"></exception>
        public Task CloseConnectionAsync(string connectionId)
        {
            if (_connections.TryGetValue(connectionId, out var connection))
            {
                connection.Dispose();
            }
            else
                throw new AppException("SessionClosed");
            return Task.CompletedTask;
        }
        /// <summary>
        /// 回收资源
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    foreach (var item in _connections.Values)
                    {
                        item.Dispose();
                    }
                }
                disposedValue = true;
            }
        }

        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }
  1. WebSsh.vue 这段代码是使用 vue 展示终端窗口的代码
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { WebLinksAddon } from 'xterm-addon-web-links';
import { SearchAddon } from 'xterm-addon-search';
import { WebglAddon } from 'xterm-addon-webgl';
import * as signalR from '@microsoft/signalr';
import 'xterm/css/xterm.css';
const termRef = ref<HTMLElement | null>(null);
// 创建 xterm 终端
const term = new Terminal();
// 定义 SignalR 客户端
const connection = new signalR.HubConnectionBuilder()
  .withUrl('/hubs/ssh', {
    accessTokenFactory: () => localStorage.getItem('token'),
  } as any)
  .build();
let isClosed = false;
// 监听键盘事件并发送到后端
term.onData((data) => {
  if (isClosed) {
    return;
  }
  connection.invoke('ReadDataAsync', data).then((result) => {
    if (result.code == 400) {
      isClosed = true;
      term.write('SessionClosed');
    }
  });
});
// 监听后端数据回传
connection.on('WriteDataAsync', (data) => {
  term.write(data);
});
// 监听后端终端关闭
connection.on('WriteErrorAsync', () => {
  isClosed = true;
  term.write('SessionClosed');
});
// 加载插件
const fit = new FitAddon();
term.loadAddon(fit);
term.loadAddon(new WebLinksAddon());
term.loadAddon(new SearchAddon());
term.loadAddon(new WebglAddon());

onMounted(async () => {
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  term.open(termRef.value!);
  fit.fit();
  // 启动 SignalR 客户端
  await connection.start();
  // 创建终端
  connection.invoke('CreateNewTerminalAsync', term.rows, term.cols);
});
</script>

<template>
  <div ref="termRef" class="xTerm"></div>
</template>

<style scoped>
</style>
  1. SshHub.cs 这个文件是 SignalR 的 Hub 文件,用来做监听的。
    [Authorize]
    public class SshHub : Hub<ISshHubClient>
    {
        private readonly SshService _sshService;
        private readonly ILogger<SshHub> _logger;

        public SshHub(SshService sshService, ILogger<SshHub> logger)
        {
            _sshService = sshService ?? throw new ArgumentNullException(nameof(sshService));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }
        /// <summary>
        /// 创建一个新的终端
        /// </summary>
        /// <param name="height"></param>
        /// <param name="width"></param>
        /// <returns></returns>
        public async Task<BaseResponse> CreateNewTerminalAsync(int height = 24, int width = 80)
        {
            try
            {
                var username = Context.User?.FindFirst("preferred_username")?.Value;
                if (username == null)
                {
                    return new BaseResponse
                    {
                        Code = 401,
                        Message = "NoUsername"
                    };
                }
                if (!Context.User?.IsInRole("user") ?? false)
                {
                    username = "root";
                }
                _logger.LogInformation($"{username}");
                await _sshService.CreateConnectionAsync(Context.ConnectionId, username, height, width);
                return new BaseResponse();
            }
            catch (InvalidOperationException)
            {
                return new BaseResponse() { Code = 500, Message = "TerminalAlreadyExist" };
            }
            catch (Exception e)
            {
                _logger.LogError(e, "ConnectionId: {ConnectionId} No such pty session.", Context.ConnectionId);
                return new BaseResponse() { Code = 500, Message = "UnableToCreateTerminal" };
            }
        }
        /// <summary>
        /// 读取输入数据
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        public async Task<BaseResponse> ReadDataAsync(string data)
        {
            try
            {
                await _sshService.ReadDataAsync(Context.ConnectionId, data);
                return new BaseResponse();
            }
            catch (Exception e)
            {
                _logger.LogError(e, "ConnectionId: {ConnectionId} No such pty session.", Context.ConnectionId);
                return new BaseResponse { Message = "NoSuchSeesion", Code = 400 };
            }
        }
    }
    /// <summary>
    /// 客户端接口
    /// </summary>
    public interface ISshHubClient
    {
        /// <summary>
        /// 写入输出数据
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        Task WriteDataAsync(string data);
        /// <summary>
        /// 写入错误数据
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        Task WriteErrorAsync(string data);
    }

参考文献

  1. Windows Command-Line: Introducing the Windows Pseudo Console (ConPTY)
  2. portable_pty - Rust
  3. xterm.js
  4. 教程:使用 TypeScript 和 Webpack 开始使用 ASP.NET Core SignalR

到了这里,关于Web SSH 的原理与在 ASP.NET Core SignalR 中的实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • ASP.NET Core SignalR 系列(二)- 中心(服务端)

    本章将和大家分享 ASP.NET Core SignalR 中的中心(服务端)。 本文大部分内容摘自微软官网:https://learn.microsoft.com/zh-cn/aspnet/core/signalr/hubs?view=aspnetcore-7.0 废话不多说,我们直接来看一个Demo,Demo的目录结构如下所示: 本Demo的Web项目为ASP.NET Core Web 应用程序( 目标框架为.NET 7.0

    2024年02月13日
    浏览(33)
  • ASP.NET Core SignalR 系列(四)- 中心筛选器

    本章将和大家分享 ASP.NET Core SignalR 中的中心筛选器。 本文大部分内容摘自微软官网:https://learn.microsoft.com/zh-cn/aspnet/core/signalr/hub-filters?view=aspnetcore-7.0 废话不多说,下面我们直接进入本章主题。 中心筛选器: 在 ASP.NET Core 5.0 或更高版本中可用。 允许在客户端调用中心方法之

    2024年02月16日
    浏览(28)
  • 【ASP.NET Core】使用SignalR推送服务器日志

    一个多月前接手了一个产线机器人项目,上位机以读写寄存器的方式控制机器人,服务器就是用 ASP.NET Core 写的 Web API。由于前一位开发者写的代码质量问题,导致上位机需要16秒才能启动。经过我近一个月的改造,除了保留业务逻辑代码,其他的基本重写。如今上位机的启动

    2024年02月03日
    浏览(43)
  • ASP.NET Core SignalR 系列(三)- JavaScript 客户端

    本章将和大家分享 ASP.NET Core SignalR 中的 JavaScript 客户端。ASP.NET Core SignalR JavaScript 客户端库使开发人员能够调用服务器端SignalR中心代码。 本文大部分内容摘自微软官网:https://learn.microsoft.com/zh-cn/aspnet/core/signalr/javascript-client?view=aspnetcore-7.0tabs=visual-studio 废话不多说,下面我们

    2024年02月15日
    浏览(33)
  • 微信小程序如何使用原生Websocket与Asp.Net Core SignalR 通信

    如题,这可能算是.net 做小程序的服务端时,绕不开的一个问题,老生常谈了。同样的问题,我记得我2018/19年的一个项目的解决方案是: 修改官方的SignalR.js的客户端 :把里面用到浏览器的Websocket改成微信小程序的官方api的。目前网上也有不少这样的方案,已经改好开源了;

    2024年02月08日
    浏览(42)
  • ASP.NET Core 中的两种 Web API

    ASP.NET Core 有两种创建 RESTful Web API 的方式: 基于 Controller,使用完整的基于ControllerBase的基类定义接口endpoints。 基于 Minimal APIs,使用Lambda表达式定义接口 endpoints。 基于 Controller 的 Web API 可以使用构造函数注入,或者属性注入,遵循面向对象模式。 基于 Minimal APIs 的 Web API 通

    2024年02月09日
    浏览(32)
  • 微信小程序如何使用原生Websocket api与Asp.Net Core SignalR 通信

    如题,这可能算是.net 做小程序的服务端时,绕不开的一个问题,老生常谈了。同样的问题,我记得我2018/19年的一个项目的解决方案是: 修改官方的SignalR.js的客户端 :把里面用到浏览器的Websocket改成微信小程序的官方api的。目前网上也有不少这样的方案,已经改好开源了;

    2024年02月09日
    浏览(65)
  • 服务端使用ASP.NET Core SignalR与Vue3(TypeScript与JavaScript)前端建立通信(以进度条为例)

    1. ASP.NET Core           ASP.NET Core 是一个跨平台、高性能及开源的框架,用于生成基于云且连接互联网的新式应用程式。 官方文档:ASP.NET documentation | Microsoft Learn  2.  ASP.NET Core SignalR         ASP.NET Core SignalR 是开源库,用于服务端与客户端建立实时通信,可以自动管理连接

    2024年02月06日
    浏览(36)
  • ASP.NET Core Web API 流式返回,实现ChatGPT逐字显示

    🏆作者:科技、互联网行业优质创作者 🏆专注领域:.Net技术、软件架构、人工智能、数字化转型、DeveloperSharp、微服务、工业互联网、智能制造 🏆欢迎关注我(Net数字智慧化基地),里面有很多 高价值 技术文章, 是你刻苦努力也积累不到的经验 ,能助你快速成长。升职

    2024年02月22日
    浏览(43)
  • ASP.NET Core 的 Web Api 实现限流 中间件

    Microsoft.AspNetCore.RateLimiting  中间件提供速率限制(限流)中间件。 它是.NET 7 以上版本才支持的中间件,刚看了一下,确实挺好用,下面给大家简单介绍一下: RateLimiterOptionsExtensions 类提供下列用于限制速率的扩展方法:​​​​​ 固定窗口限制器 滑动窗口限制器 令牌桶限

    2024年01月17日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包