.NET Core中WebSocket的使用详解

一、WebSocket是什么

初次接触WebSocket,大家都会问:我们已经有了HTTP协议,为什么还需要WebSocket?
因为HTTP协议中通信只能由客户端发起,而WebSocket协议中服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,实现了浏览器与服务器全双工通信(full-duplex),WebSocket属于服务器推送技术的一种。
WebSocket是HTML5的一种新协议,它使用JavaScript调用浏览器的API发出一个WebSocket请求至服务器,复用HTTP的握手通道经过一次握手和服务器建立了TCP通讯,因为它本质上是一个TCP连接,所以数据传输的稳定性强和数据传输量比较小。

二、WebSocket的优势

  1. 建立在TCP协议之上,服务器端的实现比较容易
  2. 与HTTP协议有着良好的兼容性,默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易被屏蔽能通过各种HTTP代理服务器。
  3. 数据格式比较轻量,性能开销小。连接创建后,ws客户端和服务器端进行数据交换时,协议控制的数据包头部较小。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。而HTTP协议每次通信都需要携带完整的头部。
  4. 可以发送文本,也可以发送二进制数据。
  5. 没有同源限制,客户端可以与任意服务器通信。
  6. 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
  7. 支持扩展。ws协议定义了扩展,用户可以扩展协议或者实现自定义的子协议。(比如支持自定义压缩算法等)

三、在.NET Core中利用WebSocket实现简易在线聊天室

因为WebSocket复用了HTTP的握手通道与服务器建立连接,所以WebSocket的握手就是一次http请求,因此我们就可以使用一个middleware来识别并拦截WebSocket请求,把客户端与服务器建立的WebSocket连接统一进行管理,其实微软已经帮我们简单的封装过了。

1、创建Core框架的Web项目

2、新建WebsocketClientCollection类对客户端与服务器建立的WebSocket连接进行统一管理 

public class WebsocketClientCollection
{
    private static List<WebsocketClient> _clients = new List<WebsocketClient>();

    public static void Add(WebsocketClient client)
    {
        _clients.Add(client);
    }

    public static void Remove(WebsocketClient client)
    {
        _clients.Remove(client);
    }

    public static WebsocketClient Get(string clientId)
    {
        var client = _clients.FirstOrDefault(c => c.Id == clientId);

        return client;
    }

    public static List<WebsocketClient> GetAll()
    {
        return _clients;
    }

    public static List<WebsocketClient> GetClientsByRoomNo(string roomNo)
    {
        var client = _clients.Where(c => c.RoomNo == roomNo);
        return client.ToList();
    }
}

3、新建WebsocketHandlerMiddleware并识别和接收WebSocket请求
WebsocketHandlerMiddleware就是我们管理WebSocket连接的入口,我们可以在Invoke()方法中先用context.WebSockets.IsWebSocketRequest来识别WebSocket请求,然后调用context.WebSockets.AcceptWebSocketAsync()方法把请求转换为WebSocket连接。 

public async Task Invoke(HttpContext context)
{
    if (context.Request.Path == "/ws")
    {
        //仅当网页执行new WebSocket("ws://localhost:5000/ws")时,后台会执行此逻辑
        if (context.WebSockets.IsWebSocketRequest)
        {
            //后台成功接收到连接请求并建立连接后,前台的webSocket.onopen = function (event){}才执行
            WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
            string clientId = Guid.NewGuid().ToString(); ;
            var wsClient = new WebsocketClient
            {
                Id = clientId,
                WebSocket = webSocket
            };
            try
            {
                await Handle(wsClient);
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Echo websocket client {0} err .", clientId);
                await context.Response.WriteAsync("closed");
            }
        }
        else
        {
            context.Response.StatusCode = 404;
        }
    }
    else
    {
        await next(context);
    }
}

4、在Handle()方法中循环接收客户端发送到后台的消息

private async Task Handle(WebsocketClient websocketClient)
{
    WebsocketClientCollection.Add(websocketClient);
    logger.LogInformation($"Websocket client added.");

    WebSocketReceiveResult clientData = null;
    do
    {
        var buffer = new byte[1024 * 1];
        //客户端与服务器成功建立连接后,服务器会循环异步接收客户端发送的消息,收到消息后就会执行Handle(WebsocketClient websocketClient)中的do{}while;直到客户端断开连接
        //不同的客户端向服务器发送消息后台执行do{}while;时,websocketClient实参是不同的,它与客户端一一对应
        //同一个客户端向服务器多次发送消息后台执行do{}while;时,websocketClient实参是相同的
        clientData = await websocketClient.WebSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
        if (clientData.MessageType == WebSocketMessageType.Text && !clientData.CloseStatus.HasValue)
        {
            var msgString = Encoding.UTF8.GetString(buffer);
            logger.LogInformation($"Websocket client ReceiveAsync message {msgString}.");
            var message = JsonConvert.DeserializeObject<Message>(msgString);
            message.SendClientId = websocketClient.Id;
            HandleMessage(message);
        }
    } while (!clientData.CloseStatus.HasValue);
    //关掉使用WebSocket连接的网页/调用webSocket.close()后,与之对应的后台会跳出循环
    WebsocketClientCollection.Remove(websocketClient);
    logger.LogInformation($"Websocket client closed.");
}

5、在HandleMessage()方法中对客户端发送到后台的消息进行解析并处理,最后推送处理结果到客户端

private void HandleMessage(Message message)
{
    var client = WebsocketClientCollection.Get(message.SendClientId);
    switch (message.action)
    {
        case "join":
            client.RoomNo = message.roomNo;
            client.SendMessageAsync($"{message.nick} join room {client.RoomNo} success .");
            logger.LogInformation($"Websocket client {message.SendClientId} join room {client.RoomNo}.");
            break;
        case "send_to_room":
            if (string.IsNullOrEmpty(client.RoomNo))
            {
                break;
            }
            var clients = WebsocketClientCollection.GetClientsByRoomNo(client.RoomNo);
            clients.ForEach(c =>
            {
                c.SendMessageAsync(message.nick + " : " + message.msg);
            });
            logger.LogInformation($"Websocket client {message.SendClientId} send message {message.msg} to room {client.RoomNo}");
            break;
        case "leave":
            #region 通过把连接的RoomNo置空模拟关闭连接
            var roomNo = client.RoomNo;
            client.RoomNo = "";
            #endregion

            #region 后台关闭连接
            //client.WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
            //WebsocketClientCollection.Remove(client); 
            #endregion

            client.SendMessageAsync($"{message.nick} leave room {roomNo} success .");
            logger.LogInformation($"Websocket client {message.SendClientId} leave room {roomNo}");
            break;
        default:
            break;
    }
}

6、在startup中配置中间件

app.UseWebSockets(new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromSeconds(60),
    ReceiveBufferSize = 1 * 1024
});
app.UseMiddleware<WebsocketHandlerMiddleware>(); 

 7、修改index.cshtml来实现一个简单的聊天室UI

<div style="margin-bottom:5px;">
    room no: <input type="text" id="txtRoomNo" value="99999" /> 
    <button id="btnJoin">join room</button> 
    <button id="btnLeave">leave room</button> 
    <button id="btnDisConnect">DisConnect</button>
</div>
<div style="margin-bottom:5px;">
    nick name: <input type="text" id="txtNickName" value="batman" />
</div>
<div style="height:300px;width:600px">
    <textarea style="height:100%;width:100%" id="msgList"></textarea>
    <div style="text-align: right">
        <input type="text" id="txtMsg" value="" />  <button id="btnSend">send</button>
    </div>
</div>

8、使用JavaScript来处理WebSocket连接并与服务器进行通信
 现代浏览器已经都支持WebSocket协议,JavaScript运行时也内置了WebSocket类,我们仅仅需要new一个WebSocket对象出来就可以利用他与后台进行双工通信。

var server = "ws://localhost:5000";//若开启了https则这里是wss
var webSocket = new WebSocket(server + "/ws");
//前台向后台发送连接请求,后台成功接收并建立连接后才会触发此事件
webSocket.onopen = function (event) {
    console.log("Connection opened...");
    $("#msgList").val("WebSocket connection opened");
};

//后台向前台发送消息,前台成功接收后会触发此事件
webSocket.onmessage = function (event) {
    console.log("Received message: " + event.data);
    if (event.data) {
        var content = $('#msgList').val();
        content = content + '\r\n' + event.data;
        $('#msgList').val(content);
    }
};

//后台关闭连接后/前台关闭连接后都会触发此事件
webSocket.onclose = function (event) {
    console.log("Connection closed...");
    var content = $('#msgList').val();
    content = content + '\r\nWebSocket connection closed';
    $('#msgList').val(content);
};

$('#btnJoin').on('click', function () {
    var roomNo = $('#txtRoomNo').val();
    var nick = $('#txtNickName').val();
    if (!roomNo) {
        alert("请输入RoomNo");
        return;
    }
    var msg = {
        action: 'join',
        roomNo: roomNo,
        nick: nick
    };
    if (CheckWebSocketConnected(webSocket)) {
        webSocket.send(JSON.stringify(msg));
    }
});

$('#btnSend').on('click', function () {
    var message = $('#txtMsg').val();
    var nick = $('#txtNickName').val();
    if (!message) {
        alert("请输入发生的内容");
        return;
    }
    if (CheckWebSocketConnected(webSocket)) {
        webSocket.send(JSON.stringify({
            action: 'send_to_room',
            msg: message,
            nick: nick
        }));
    }
});

$('#btnLeave').on('click', function () {
    var nick = $('#txtNickName').val();
    var msg = {
        action: 'leave',
        roomNo: '',
        nick: nick
    };
    if (CheckWebSocketConnected(webSocket)) {
        webSocket.send(JSON.stringify(msg));
    }
});

$("#btnDisConnect").on("click", function () {
    if (CheckWebSocketConnected(webSocket)) {
        //部分浏览器调用close()方法关闭WebSocket时不支持传参
        //webSocket.close(001, "closeReason");
        webSocket.close();
    }
});

9、至此我们的聊天室已经搭建完成了,项目运行之后我们启动两个页面,进入相同的房间号就能聊天了 

四、常见问题解答

1、Error during WebSocket handshake: Unexpected response code: 404
当VS设置使用IIS Express启动,但IIS没安装WebSocket时,会出现这个错误,解决方法有两个:①IIS安装WebSocket,②设置为项目自托管启动。

本文借鉴了https://www.cnblogs.com/kklldog/p/core-for-websocket.html,在此基础上对代码做了完善并加上了自己的理解,如果觉得本文对您有帮助的话,请点赞、评论鼓励下,谢谢。

  • 23
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 20
    评论
使用 .NET Core 可以很方便地实现 WebSocket 通信,以下是一个简单的示例: 1. 创建 WebSocket 控制器 在 .NET Core ,可以通过继承 `Microsoft.AspNetCore.Mvc.ControllerBase` 来创建控制器。我们可以创建一个 `WebSocketController`,代码如下: ```csharp using Microsoft.AspNetCore.Mvc; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; namespace YourNamespace.Controllers { [ApiController] [Route("[controller]")] public class WebSocketController : ControllerBase { private readonly WebSocketManager _webSocketManager; public WebSocketController(WebSocketManager webSocketManager) { _webSocketManager = webSocketManager; } [HttpGet("{id}")] public async Task Get(string id) { if (HttpContext.WebSockets.IsWebSocketRequest) { var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); _webSocketManager.AddSocket(id, webSocket); await Receive(webSocket, async (result, buffer) => { if (result.MessageType == WebSocketMessageType.Text) { var message = Encoding.UTF8.GetString(buffer, 0, result.Count); await _webSocketManager.SendMessageAsync(id, message); } else if (result.MessageType == WebSocketMessageType.Close) { await _webSocketManager.RemoveSocketAsync(id); } }); } else { HttpContext.Response.StatusCode = 400; } } private async Task Receive(WebSocket webSocket, Action<WebSocketReceiveResult, byte[]> handleMessage) { var buffer = new byte[1024 * 4]; while (webSocket.State == WebSocketState.Open) { var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); handleMessage(result, buffer); } } } } ``` 在上面的代码,我们首先判断 HTTP 请求是否是 WebSocket 请求,如果是,则接受 WebSocket 连接,并将 WebSocket 加入到 `WebSocketManager` 。然后,我们使用 `Receive` 方法处理接收到的消息,并使用 `SendMessageAsync` 方法发送消息给客户端。如果收到关闭消息,我们将 WebSocket 从 `WebSocketManager` 移除。 2. 创建 WebSocket 管理器 我们需要一个 WebSocket 管理器来管理所有的 WebSocket 连接。我们可以创建一个 `WebSocketManager` 类,代码如下: ```csharp using System.Collections.Concurrent; using System.Net.WebSockets; using System.Text; using System.Threading.Tasks; namespace YourNamespace { public class WebSocketManager { private readonly ConcurrentDictionary<string, WebSocket> _sockets = new ConcurrentDictionary<string, WebSocket>(); public void AddSocket(string id, WebSocket socket) { _sockets.TryAdd(id, socket); } public async Task SendMessageAsync(string id, string message) { if (_sockets.TryGetValue(id, out WebSocket socket)) { var bytes = Encoding.UTF8.GetBytes(message); await socket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None); } } public async Task RemoveSocketAsync(string id) { _sockets.TryRemove(id, out WebSocket socket); await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by the WebSocketManager", CancellationToken.None); } } } ``` 在上面的代码,我们使用 `ConcurrentDictionary` 来存储所有的 WebSocket 连接。我们提供了 `AddSocket` 方法来添加 WebSocket 连接,`SendMessageAsync` 方法来发送消息给指定的 WebSocket 连接,`RemoveSocketAsync` 方法来移除指定的 WebSocket 连接。 3. 注册 WebSocket 控制器和 WebSocket 管理器 最后,我们需要在 `Startup.cs` 注册 WebSocket 控制器和 WebSocket 管理器,代码如下: ```csharp using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; namespace YourNamespace { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSingleton<WebSocketManager>(); services.AddControllers(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } } } ``` 在上面的代码,我们首先注册 `WebSocketManager` 类型的单例依赖,然后使用 `AddControllers` 方法来注册控制器。最后,我们使用 `MapControllers` 方法将控制器映射到路由。 现在,我们可以使用浏览器或其他 WebSocket 客户端连接到 `/WebSocket/{id}` 路径,并发送和接收消息了。
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

changuncle

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值