欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

.NET Core中WebSocket的使用详解 原创

程序员文章站 2022-06-18 13:49:35
一、WebSocket是什么 初次接触WebSocket,大家都会问:我们已经有了HTTP协议,为什么还需要WebSocket? 因为HTTP协...

一、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项目

.NET Core中WebSocket的使用详解
                    原创

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

.NET Core中WebSocket的使用详解
                    原创

四、常见问题解答

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

.NET Core中WebSocket的使用详解
                    原创

 

.NET Core中WebSocket的使用详解
                    原创

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