第一步:安装WebScokets库

首先需要安装Microsoft.AspNetCore.WebSockets这个库

第二步:.NET Core与中间件

需要在Startup类的Configure方法中添加WebSocket中间件:

app.UseWebSockets();

更一般地,我们可以配置以下两个配置,其中,KeepAliveInterval表示向客户端发送Ping帧的时间间隔;ReceiveBufferSize表示接收数据的缓冲区大小: 

var webSocketOptions = new WebSocketOptions()
{
    KeepAliveInterval = TimeSpan.FromSeconds(120),
    ReceiveBufferSize = 4 * 1024
};
app.UseWebSockets(webSocketOptions);

那么怎么接收一个来自客户端的请求呢?

首先,我们需要判断下请求的地址,这是客户端和服务端约定好的地址,默认为/,这里我们以/ws为例;接下来,我们需要判断当前的请求上下文是否为WebSocket请求,通过context.WebSockets.IsWebSocketRequest来判断。当这两个条件同时满足时,我们就可以通过context.WebSockets.AcceptWebSocketAsync()方法来得到WebSocket对象,这样就表示“握手”完成,这样我们就可以开始接收或者发送消息啦。

if (context.Request.Path == "/ws")
{
    if (context.WebSockets.IsWebSocketRequest)
    {
        WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
        //TODO
    }
});

一旦建立了Socket连接,客户端和服务端之间就可以开始通信,这是我们从Socket中收获的经验,这个经验同样适用于WebSocket。这里分别给出WebSocket发送和接收消息的实现,并针对代码做简单的分析。

private async Task SendMessage<TEntity>(WebSocket webSocket, TEntity entity)
{
    var Json = JsonConvert.SerializeObject(entity);
    var bytes = Encoding.UTF8.GetBytes(Json);

    await webSocket.SendAsync(
        new ArraySegment<byte>(bytes),
        WebSocketMessageType.Text,
        true,
        CancellationToken.None
    );
}

这里我们提供一个泛型方法,它负责对消息进行序列化并转化为byte[],最终调用SendAsync()方法发送消息。与之相对应地,客户端会在onmessage()回调中就会接受到消息,这一点我们放在后面再说。WebSocket接收消息的方式,和传统的Socket非常相似,我们需要将字节流循环读取到一个缓存区里,直至所有数据都被接收完。下面给出基本的代码示例:

var buffer =new ArraySegment<byte>(new byte[bufferSize);
var result = await webSocket.ReceiveAsync(buffer, CancellationToken.None);
while (!result.EndOfMessage)
{
    result = await webSocket.ReceiveAsync(buffer, default(CancellationToken));
}
var json = Encoding.UTF8.GetString(buffer.Array);
json = json.Replace("\0", "").Trim();
return JsonConvert.DeserializeObject<TEntity>(json, new JsonSerializerSettings()
{
    DateTimeZoneHandling = DateTimeZoneHandling.Local
});

 虽然不大清楚,为什么这里反序列化后的内容中会有大量的\0,以及这个全新的类型ArraySegment到底是个什么鬼,不过程序员的一生无非都在纠结这样两个问题,“it works” 和 “it doesn't works",就像人生里会让你纠结的无非是”她喜欢你“和”她不喜欢我“这样的问题。

即示例中的代码都是写在app.Use()方法中的,这样会使我们的Startup类显得臃肿,而熟悉OWIN或者http://ASP.NET Core的朋友,就会知道Startup类是一个非常重要的东西,我们通常会在这里配置相关的组件。在http://ASP.NET Core中,我们可以通过Configure()方法来为IApplicationBuilder增加相关组件,这种组件通常被称为中间件。那么,什么是中间件呢?

preview

 从这张图中可以看出,中间件实际上是指在HTTP请求管道中处理请求和响应的组件,每个组件都可以决定是否要将请求传递给下一个组件,比如身份认证、日志记录就是最为常见的中间件。在http://ASP.NET Core中,我们通过app.Use()方法来定义一个Func类型的参数,所以,我们可以简单地认为,在http://ASP.NET Core中,Func就是一个中间件,而通过app.Use()方法,这些中间件会根据注册的先后顺序组成一个链表,每一个中间件的输入是上一个中间件的输出,每一个中间件的输出则会成为下一个中间件的输入。简而言之,每一个RequestDelegate对象不仅包含了自身对请求的处理,而且包含了后续中间件对请求的处理,我们来看一个简单的例子:

app.Use(async (context,next)=>
{
    await context.Response.WriteAsync("这是第一个中间件\r\n");
    await next();
});

app.Use(async (context,next)=>
{
    await context.Response.WriteAsync("这是第二个中间件\r\n");
    await next();
});

app.Use(async (context,next)=>
{
    await context.Response.WriteAsync("这是第三个中间件\r\n");
    await next();
});

 通过Postman或者任意客户端发起请求,我们就可以得到下面的结果,现在想象一下,如果我们在第一种中间件中不调用next()会怎么样呢?答案是中间件之间的链路会被打断,这意味着后续的第二个、第三个中间件都不会被执行。什么时候我们会遇到这种场景呢?当我们的认证中间件认为一个请求非法的时候,此时我们不应该让用户访问后续的资源,所以直接返回403对该请求进行拦截。在大多数情况下,我们需要让请求随着中间件的链路传播下去,所以,对于每一个中间件来说,除了完成自身的处理逻辑以外,还至少需要调用一次next(),以保证下一个中间件会被调用,这其实和职责链模式非常相近,可以让数据在不同的处理管道中进行传播。

 OK,这里我们继续遵从这个约定,将整个聊天室相关的逻辑写到一个中间件里,这样做的好处是,我们可以将不同的WebSocket互相隔离开,同时可以为我们的Startup类”减负“。事实证明,这是一个正确的决定,在开发基于WebSocket的弹幕功能时,我们就是用这种方式开发了新的中间件。这里,我们给出的是WebSocketChat中间件中最为关键的部分,详细的代码我已经放在Github上啦,大家可以参考WebSocketChat类,其基本原理是:使用一个字典来存储每一个聊天室中的会话(Socket),当用户打开或者关闭一个WebSocket连接时,会向服务器端发送一个事件(Event),这样客户端中持有的用户列表将被更新,而根据发送的消息,可以决定这条消息是被发给指定联系人还是群发:

public async Task Invoke(HttpContext context)
{
    if (!IsWebSocket(context))
    {
        await _next.Invoke(context);
        return;
    }

    var userName = context.Request.Query["username"].ToArray()[0];
    var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    while (webSocket.State == WebSocketState.Open)
    {
         var entity = await Receiveentity<MessageEntity>(webSocket);
         switch (entity.Type)
         {
             case MessageType.Chat:
                  await HandleChat(webSocket, entity);
                  break;
             case MessageType.Event:
                  await HandleEvent(webSocket, entity);
                  break;
         }
    }

    await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Close", default(CancellationToken));
}

其中,HandleEvent负责对事件进行处理,HandleChat负责对消息进行处理。当有用户加入聊天室的时候,首先会向所有客户端广播一条消息,告诉大家有新用户加入了聊天室,与此同时,为了让大家可以和新用户进行通信,必须将新的用户列表推送到客户端。同理,当有用户离开聊天室的时候,服务器端会有类似的事件推送到客户端。事件同样是基于消息来实现的,不过这两种采用的数据结构不同,具体大家可以通过源代码来了解。发送消息就非常简单啦,给指定用户发送消息是通过用户名来找WebSocket对象,而群发消息就是遍历字典中的所有WebSocket对象,这一点我们不再详细说啦!

Vue驱动的客户端

在实现服务端的WebSocket以后,我们就可以着手客户端的开发啦!这里我们采用原生的WebSocket API来开发相关功能。具体来讲,我们只需要实例化一个WebSocket类,并设置相应地回调函数就可以了,我们一起来看下面的例子:

var username = "PayneQin"
var websocket = new WebSocket("ws://localhost:8002/ws?username=" + username);

这里我们使用/s这个路由来访问WebSocket,相应地,在服务端代码中我们需要判断context.Request.Path,WebSocket在握手阶段是基于HTTP协议的,所以我们可以以QueryString的形式给后端传递一个参数,这里我们需要一个用户名,它将作为服务端存储WebSocket时的一个键。一旦建立了WebSocket,我们就可以通过回调函数来监听服务器端的响应,或者是发送消息给服务器端。主要的回调函数有onopen、onmessage、onerror和onclose四个,基本使用方法如下:

websocket.onopen = function () {
    console.log("WebSocket连接成功");
};

websocket.onmessage = function (event) {
    console.log("接收到服务端消息:" + event.data)
};

websocket.onerror = function () {
    console.log("WebSocket连接发生错误");
};

websocket.onclose = function () {
console.log("WebSocket连接关闭");
};

Logo

致力于链接即构和开发者,提供实时互动和元宇宙领域的前沿洞察、技术分享和丰富的开发者活动,共建实时互动世界。

更多推荐