基于webapi的websocket聊天室(一)

上一次我已经讲了在webapi主机上面加入websocket中间件。
这次就更进一步,搭建一个websocket局域网聊天室。
传送门-->webapi添加添加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来测试。
我定义了三个游客来测试广播功能

  • 游客定义

image

  • 测试步骤

  1. 游客1、游客2、游客3依次连接聊天室。查看加入聊天室的广播信息。
  2. 游客1、游客2、游客3依次发言。查看发言广播。
  3. 游客1、游客2、游客3依次退出聊天室。查看退出聊天室广播信息。
    image

image

image

image

image

posted @ 2024-05-11 22:06  ggtc  阅读(258)  评论(0编辑  收藏  举报
//右下角目录