感觉放了好长时间的假期。贴个 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>
测试效果:
有了上面的示例,一个最简单的聊天室模型已经可以实现了。
文章评论
SocketsMiddleware的构造方法有两个参数,为啥 app.Map(path, x => x.UseMiddleware(socket)) 这里只传了一个?
@toy 因为第一个参数是 next 委托,你看源码就可以看到,它调用了一个 `CreateDelegate` 方法,如果参数只有一个,默认在第一个参数的地方添加上这个委托。
这个怎么设置访问路径
@FateDong 路径在 startup.cs 的 Configure 里面设置的,这个是固定的。你说的是这个吗?
为啥我报错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'.”