上一次我已经讲了在webapi主机上面加入websocket中间件。
这次就更进一步,搭建一个websocket局域网聊天室。
传送门-->webapi添加添加websocket中间件
聊天室
websocket通信其实和win32api里面的消息循环差不多,只不过一个消息来自操作系统,一个来自网络。
但核心都是一个阻塞的while循环,在循环中处理各种消息。
由于搭建聊天室代码多一点,我就把中间件单独写一个类,而不是用lambadu委托在program里面直接写了。
- 聊天室成员
游客对象可以是单纯的WebSocket
对象,也可以是更复杂的对象。但我们还需要为游客命名等,所以采用自定义一个游客类来代表游客信息。
//RoomVisitor.cs
public class RoomVisitor
{
/// <summary>
/// 网络连接
/// </summary>
public WebSocket Web { get; set; }
/// <summary>
/// 名字
/// </summary>
public string Name { get; set; }
/// <summary>
/// ID
/// </summary>
public string Id { get; set; }
}
我暂时就考虑这些字段。其实还有,比如前面已经添加了身份认证的中间件,可以取得其他信息。但是目前为了简单,先不考虑认证的问题。不考虑会员,只考虑游客。
- 聊天室对象
在初始阶段。我们先不考虑从那么复杂,不考虑多少个聊天室的情况,就考虑单聊天室。这样我们就可以使用单例模式进行依赖注入。
/// <summary>
/// 聊天室
/// </summary>
public class WebSocketChatRoom
{
/// <summary>
/// 成员
/// </summary>
public ConcurrentDictionary<string, RoomVisitor> clients=new ConcurrentDictionary<string, RoomVisitor>();
public WebSocketChatRoom()
{
}
}
//program.cs
//添加聊天室服务
builder.Services.AddSingleton<WebSocketChatRoom>();
聊天室功能
为了不那么单调,我们只定义聊天室三个最简单的功能。
- 上线通知
- 发言广播
- 下线通知
消息循环条件
在win32api中消息循环是while(有消息)
,直到接收到窗体关闭消息,退出循环,程序未关闭。
websocket类似。需要while(client.CloseStatus.HasValue==false)
,连接未关闭时一直循环。
当然,这是个阻塞式循环
上线通知和下线通知
这两个功能倒是简单。在websocket连接建立时和连接关闭时广播一条消息就行。
也就是在进入消息循环前和退出消息循环后。
//游客加入聊天室
var visitor = new RoomVisitor() { Id= System.Guid.NewGuid().ToString("N"), Name = $"游客_{clients.Count + 1}", Web = client };
while(client.CloseStatus.HasValue==false)
{
//...
}
//广播游客退出
CascadeMeaasge(visitor,$"{visitor.Name}退出聊天室");
clients.TryRemove(visitor.Id, out RoomVisitor v);
//关闭连接...
await client.CloseAsync(
client.CloseStatus!.Value,
client.CloseStatusDescription,
CancellationToken.None);
发言广播
在收到消息时,遍历聊天室成员,依次转发这条消息。
当然,还可以加一些处理。比如添加发言人名字,时间等。
- 首先要定义广播方法,CascadeMeaasge
public void CascadeMeaasge(RoomVisitor visitor, string message)
{
foreach (var other in clients)
{
//不对发言者广播
if (visitor!=null)
{
if (other.Key == visitor.Id)
{
continue;
}
}
//转发内容
var buffer = Encoding.UTF8.GetBytes(message);
if (other.Value.Web.State==WebSocketState.Open)
{
other.Value.Web.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
}
}
}
- 收到消息时广播转发
//消息缓冲区。每个连接分配400字节,100个汉字的内存
var minimumBuffer=new byte[400];
while(!client.CloseStatus.HasValue)
{
WebSocketReceiveResult result = await client.ReceiveAsync(new ArraySegment<byte>(minimumBuffer), CancellationToken.None);
//广播游客发言
if (result.MessageType == WebSocketMessageType.Text)
{
CascadeMeaasge(visitor, $"{visitor.Name}: "+UTF8Encoding.UTF8.GetString(minimumBuffer,0, result.Count));
}
}
聊天室的核心功能就完成了,实在是没几行代码。
添加中间件处理websocket连接
websocket连接很多,不一定都是要进入聊天室。我们使用/chat
路径来路由到聊天室。
- 中间件定义
//WebSocketChatRoomMiddleware.cs
public class WebSocketChatRoomMiddleware
{
private readonly RequestDelegate _next;
public WebSocketChatRoomMiddleware(RequestDelegate next, Func<WebSocketChatRoom> handler)
{
_next = next;
Handler = handler;
}
public Func<WebSocketChatRoom> Handler { get; }
public async Task InvokeAsync(HttpContext context)
{
await _next(context);
if (context.Request.Path=="/chat")
{
WebSocket client = await context.WebSockets.AcceptWebSocketAsync();
await Handler().HandleContext(context, client);
}
}
}
我们使用MapWhen
单独给websocket连接做一个管道分支,区别于http处理流程。只有websocket连接才会进入这个分支。
注意授权中间件,如果没做,可以删掉。
//WebSocketChatRoomMiddleware.cs
public static class WebSocketChatRoomMiddlewareExtensions
{
public static WebApplication UseWebSocketChatRoomMiddleware(this WebApplication builder)
{
//建立websocket分支
builder.MapWhen(c => c.WebSockets.IsWebSocketRequest, appbuilder =>
{
//授权
appbuilder.Use(async (context, next) =>
{
if (context.User.Identity.IsAuthenticated)
await next(context);
else
context.Response.StatusCode = StatusCodes.Status403Forbidden;
})
//连接
.UseMiddleware<WebSocketChatRoomMiddleware>(new Func<WebSocketChatRoom>(() =>
{
return appbuilder.ApplicationServices.GetRequiredService<WebSocketChatRoom>();
}));
});
return builder;
}
}
- 添加中间件
//program.cs
//调用了此终结点才能判断连接请求是不是ws请求,会让ws连接的context.WebSockets.IsWebSocketRequest变成true
app.UseWebSockets();
//使用我们定义的中间件
app.UseWebSocketChatRoomMiddleware();
聊天测试
客户端就直接用api请求工具吧,懒得写了。我使用了ApiPost
来测试。
我定义了三个游客来测试广播功能
-
游客定义
-
测试步骤
- 游客1、游客2、游客3依次连接聊天室。查看加入聊天室的广播信息。
- 游客1、游客2、游客3依次发言。查看发言广播。
- 游客1、游客2、游客3依次退出聊天室。查看退出聊天室广播信息。