修身养性,知行合一

  • 首页
  • 爱码
    • 系统
    • 数据库
    • JavaScript
    • CSharp
    • Python
  • 生活
    • 文化
    • 美食
  • 杂谈
  • 关于
修身养性,知行合一
码字,杂谈
  1. 首页
  2. 爱码
  3. CSharp
  4. ASP.NET
  5. 正文

.net core 中使用 websocket

2021年2月23日 5316点热度 4人点赞 5条评论

感觉放了好长时间的假期。贴个 websocket 的简单示例。

整体文件结构

- 项目(WebSockets.Test)
|-- Extensions
|      |-- SocketsExtension.cs
|-- Handlers
|      |-- WebSocketMessageHandler.cs
|-- SocketsManager
|      |-- SocketsHandler.cs
|      |-- SocketsManager.cs
|      |-- SocketsMiddleware.cs
|-- Program.cs
|-- Startup.cs

大体需要的文件是这些,这是最基本的示例,可以按需自行修改。

1、创建保存 WebSocket 的类

该类用于保存所有 WebSocket。

// 文件:SocketsManager/SocketsManager.cs

using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;

namespace WebSockets.Test.SocketsManager
{
    public class SocketsManager
    {
        private readonly ConcurrentDictionary<string, WebSocket> _connections =
            new ConcurrentDictionary<string, WebSocket>();

        /// <summary>
        /// 获取所有 sockets 的字典集合
        /// </summary>
        /// <returns></returns>
        public ConcurrentDictionary<string, WebSocket> GetAllConnections()
        {
            return _connections;
        }

        /// <summary>
        ///     获取指定 id 的 socket
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public WebSocket GetSocketById(string id)
        {
            return _connections.FirstOrDefault(x => x.Key == id).Value;
        }

        /// <summary>
        /// 根据 socket 获取其 id
        /// </summary>
        /// <param name="socket"></param>
        /// <returns></returns>
        public string GetId(WebSocket socket)
        {
            return _connections.FirstOrDefault(x => x.Value == socket).Key;
        }

        /// <summary>
        /// 删除指定 id 的 socket,并关闭该链接
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public async Task RemoveSocketAsync(string id)
        {
            _connections.TryRemove(id, out var socket);
            await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "socket connection closed",
                CancellationToken.None);
        }

        /// <summary>
        /// 添加一个 socket
        /// </summary>
        /// <param name="socket"></param>
        public void AddSocket(WebSocket socket)
        {
            _connections.TryAdd(CreateId(), socket);
        }

        /// <summary>
        /// 创建 id
        /// </summary>
        /// <returns></returns>
        private string CreateId()
        {
            return Guid.NewGuid().ToString("N");
        }
    }
}

2、创建管理和操作 WebSocket 的基类

该类旨在处理 socket 的连接和断连,以及接收和发送消息,属于基类。

// 文件: SocketsManager/SocketsHandle.cs

using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace WebSockets.Test.SocketsManager
{
    public abstract class SocketsHandler
    {
        protected SocketsHandler(SocketsManager sockets)
        {
            Sockets = sockets;
        }

        public SocketsManager Sockets { get; set; }

        /// <summary>
        /// 连接一个 socket
        /// </summary>
        /// <param name="socket"></param>
        /// <returns></returns>
        public virtual async Task OnConnected(WebSocket socket)
        {
            await Task.Run(() => { Sockets.AddSocket(socket); });
        }

        /// <summary>
        /// 断开指定 socket
        /// </summary>
        /// <param name="socket"></param>
        /// <returns></returns>
        public virtual async Task OnDisconnected(WebSocket socket)
        {
            await Sockets.RemoveSocketAsync(Sockets.GetId(socket));
        }

        /// <summary>
        /// 发送消息给指定 socket
        /// </summary>
        /// <param name="socket"></param>
        /// <param name="message"></param>
        /// <returns></returns>
        public async Task SendMessage(WebSocket socket, string message)
        {
            if (socket.State != WebSocketState.Open) return;

            await socket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(message)),
                WebSocketMessageType.Text, true, CancellationToken.None);
        }

        /// <summary>
        /// 发送消息给指定 id 的 socket
        /// </summary>
        /// <param name="id"></param>
        /// <param name="message"></param>
        /// <returns></returns>
        public async Task SendMessage(string id, string message)
        {
            await SendMessage(Sockets.GetSocketById(id), message);
        }

        /// <summary>
        /// 给所有 sockets 发送消息
        /// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        public async Task SendMessageToAll(string message)
        {
            foreach (var connection in Sockets.GetAllConnections()) await SendMessage(connection.Value, message);
        }

        /// <summary>
        /// 接收到消息
        /// </summary>
        /// <param name="socket"></param>
        /// <param name="result"></param>
        /// <param name="buffer"></param>
        /// <returns></returns>
        public abstract Task Receive(WebSocket socket, WebSocketReceiveResult result,
            byte[] buffer);
    }
}

3、创建 WebSocket 的中间件

// 文件:SocketsManager/SocketsMiddleware.cs

using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace WebSockets.Test.SocketsManager
{
    public class SocketsMiddleware
    {
        private readonly RequestDelegate _next;

        public SocketsMiddleware(RequestDelegate next, SocketsHandler handler)
        {
            _next = next;
            Handler = handler;
        }

        private SocketsHandler Handler { get; }

        public async Task InvokeAsync(HttpContext context)
        {
            if (context.WebSockets.IsWebSocketRequest)
            {
                // 转换当前连接为一个 ws 连接
                var socket = await context.WebSockets.AcceptWebSocketAsync();
                await Handler.OnConnected(socket);

                // 接收消息的 buffer
                var buffer = new byte[1024 * 4];
                // 判断连接类型,并执行相应操作
                while (socket.State == WebSocketState.Open)
                {
                    // 这句执行之后,buffer 就是接收到的消息体,可以根据需要进行转换。
                    var result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
                    switch (result.MessageType)
                    {
                        case WebSocketMessageType.Text:
                            await Handler.Receive(socket, result, buffer);
                            break;
                        case WebSocketMessageType.Close:
                            await Handler.OnDisconnected(socket);
                            break;
                        case WebSocketMessageType.Binary:
                            break;
                        default:
                            throw new ArgumentOutOfRangeException();
                    }
                }
            }
            else
            {
                await _next(context);
            }
        }
    }
}

4、创建 WebSocket 管理子类

可以创建多个,用于个性化设置,主要是上面设置了接收的抽象方法,所以必须要重写 Receive 方法。如果不需要的话,其实把基类的抽象去掉,直接在基类中写也可以。

为了展示效果,添加了加入和离开时的消息提示。同时接收到的消息直接转发给所有人。

// 文件: Handlers/WebSocketMessageHandler.cs

using System.Net.WebSockets;
using System.Text;
using System.Threading.Tasks;
using WebSockets.Test.SocketsManager;

namespace WebSockets.Test.Handlers
{
    public class WebSocketMessageHandler : SocketsHandler
    {
        public WebSocketMessageHandler(SocketsManager.SocketsManager sockets) : base(sockets)
        {
        }

        public override async Task OnConnected(WebSocket socket)
        {
            await base.OnConnected(socket);
            var socketId = Sockets.GetId(socket);
            await SendMessageToAll($"{socketId}已加入");
        }

        public override async Task OnDisconnected(WebSocket socket)
        {
            await base.OnDisconnected(socket);
            var socketId = Sockets.GetId(socket);
            await SendMessageToAll($"{socketId}离开了");
        }

        public override async Task Receive(WebSocket socket, WebSocketReceiveResult result, byte[] buffer)
        {
            var socketId = Sockets.GetId(socket);
            var message = $"{socketId} 发送了消息:{Encoding.UTF8.GetString(buffer, 0, result.Count)}";
            await SendMessageToAll(message);
        }
    }
}

5、创建注入扩展

直接在 Startup.cs 中写也无不可,但这是好习惯,将每个注入内容单独写到文件。

// 文件:Extensions/SocketsExtension.cs

using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using WebSockets.Test.SocketsManager;

namespace WebSockets.Test.Extensions
{
    public static class SocketsExtension
    {
        public static IServiceCollection AddWebSocketManager(this IServiceCollection services)
        {
            services.AddTransient<SocketsManager.SocketsManager>();
            var exportedTypes = Assembly.GetEntryAssembly()?.ExportedTypes;
            if (exportedTypes == null) return services;

            foreach (var type in exportedTypes)
                if (type.GetTypeInfo().BaseType == typeof(SocketsHandler))
                    services.AddSingleton(type);

            return services;
        }

        public static IApplicationBuilder MapSockets(this IApplicationBuilder app, PathString path,
            SocketsHandler socket)
        {
            return app.Map(path, x => x.UseMiddleware<SocketsMiddleware>(socket));
        }
    }
}

6、配置 Startup.cs

将上面的内容注入到启动项中即可。

在 ConfigureServices 中添加:

services.AddWebSocketManager();

然后在 Configure 中添加:

app.UseWebSockets();
app.MapSockets("/ws", serviceProvider.GetService<WebSocketMessageHandler>());

即可。

如果提示 serviceProvider 找不到,在 Configure 的参数中添加:

IServiceProvider serviceProvider

即可。

完整的内容如下:

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddWebSocketManager();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    app.UseWebSockets();
    // 配置路径
    app.MapSockets("/ws", serviceProvider.GetService<WebSocketMessageHandler>());

    app.UseStaticFiles();
}

7、测试

以上内容已经完成,现在可以跑起来。为了测试,编写一个最简单的页面。

<!DOCTYPE html>
<html lang="zh-cn">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket web client</title>
</head>

<body>
    <h1>WebSocket Web Client</h1>
    <br />

    <input type="text" placeholder="enter your message" id="message">
    <button id="sendBtn">Send</button>
    <ul id="messageList"></ul>

    <script>
        // 根据实际地址和端口进行修改,其他内容无需修改
        const uri = "ws://localhost:5000/ws";
        socket = new WebSocket(uri);
        socket.onopen = function (e) {
            console.log("websocket estabished!");
        }

        socket.onclose = function (e) {
            console.log('websocket closed!');
        }

        socket.onmessage = function (e) {
            appendItem(list, e.data);
            console.log(e.data);
        }

        const list = document.getElementById("messageList");
        const btn = document.getElementById("sendBtn");
        btn.addEventListener("click", function () {
            console.log("sending message~~~");

            var messgae = document.getElementById("message");
            socket.send(message.value)
        })

        function appendItem(list, message) {
            const li = document.createElement("li");
            li.appendChild(document.createTextNode(message));
            list.appendChild(li);
        }
    </script>
</body>

</html>

测试效果:

有了上面的示例,一个最简单的聊天室模型已经可以实现了。

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: .net websocket
最后更新:2021年2月23日

jeremyjone

这个人很懒,什么都没留下

打赏 点赞
< 上一篇
下一篇 >

文章评论

  • toy

    SocketsMiddleware的构造方法有两个参数,为啥 app.Map(path, x => x.UseMiddleware(socket)) 这里只传了一个?

    2021年4月7日
    回复
    • jeremyjone

      @toy 因为第一个参数是 next 委托,你看源码就可以看到,它调用了一个 `CreateDelegate` 方法,如果参数只有一个,默认在第一个参数的地方添加上这个委托。

      2021年4月14日
      回复
  • FateDong

    这个怎么设置访问路径

    2022年1月12日
    回复
    • jeremyjone

      @FateDong 路径在 startup.cs 的 Configure 里面设置的,这个是固定的。你说的是这个吗?

      2022年1月13日
      回复
  • 乐

    为啥我报错System.Exception:“Could not resolve a service of type 'Microsoft.Extensions.DependencyInjection.ServiceProvider' for the parameter 'serviceProvider' of method 'Configure' on type 'WebSockets.Test.Startup'.”

    2023年2月23日
    回复
  • 取消回复

    文章目录
    • 整体文件结构
    • 1、创建保存 WebSocket 的类
    • 2、创建管理和操作 WebSocket 的基类
    • 3、创建 WebSocket 的中间件
    • 4、创建 WebSocket 管理子类
    • 5、创建注入扩展
    • 6、配置 Startup.cs
    • 7、测试
    最新 热点 随机
    最新 热点 随机
    node-sass 的安装 解决端口被占的问题 vue3 组件 Props 的声明方式 给 div 添加选中状态 请求的取消 rgb 颜色小数兼容问题
    C#读取Excel的内容和图片及图片位置 IIS Express 通过IP访问的方法和坑 MySQL升级之路(5.6-8.0) .net core 3.x使用mysql EntityFramework 如何写一个组件级别的全局状态管理 Vim使用指南

    (っ•̀ω•́)っ✎⁾⁾ 开心每一天

    COPYRIGHT © 2021 jeremyjone.com. ALL RIGHTS RESERVED.

    THEME KRATOS MADE BY VTROIS

    京ICP备19012859号-1

    京公网安备 11010802028585号