• .NET Core 基于Websocket的在线聊天室


    什么是Websocket

    我们在传统的客户端程序要实现实时双工通讯第一想到的技术就是socket通讯,但是在web体系是用不了socket通讯技术的,因为http被设计成无状态,每次跟服务器通讯完成后就会断开连接。
    在没有websocket之前web系统如果要做双工通讯往往使用http long polling技术。http long polling 每次往服务器发送请求后,服务端不会立刻返回信息来结束请求,而是一直挂着直到有数据需要返回,或者等待超时了才会返回。客户端在结束上一次请求后立刻再发送一次请求,如此反复。http long polling虽然能实现web系统的双工通讯,但是有个很大的问题,就是基于http协议客户端每次发送请求都需要携带巨大的头部。在并发交互少量数据的时候非常不划算,对服务器资源的消耗也是巨大的。
    websocket很好的改善了以上问题。它基于tcp重新设计了一套协议,同时又兼容http,默认跟http一样使用80/443端口。websocket链接建立本质上就是一次http请求,直接使用http协议的upgrade头来标识这是一次websocket请求,服务端回复101状态码表示“握手”成功。

    //客户端请求
    GET / HTTP/1.1
    Upgrade: websocket
    Connection: Upgrade
    Host: example.com
    Origin: http://example.com
    Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
    Sec-WebSocket-Version: 13
    
    //服务端响应
    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
    Sec-WebSocket-Location: ws://example.com/
    

    使用asp.net core来处理websocket

    上面我们简单的了解了websocket,那么如何来使用asp.net core处理websocket呢?因为websocket的握手就是一次http请求,那么我们就可以使用一个middleware来拦截websocket的请求,把建立的链接统一进行管理,其实微软已经帮我们简单的封装过了。

    新建一个asp.net core网站

    新建WebsocketHandlerMiddleware中间件

    这个中间件就是我们管理websocket链接的入口,我们调用context.WebSockets.AcceptWebSocketAsync()方法把请求转换为websocket链接。

    在Invoke方法内接收websocket链接

          public async Task Invoke(HttpContext context)
            {
                if (context.Request.Path == "/ws")
                {
                    if (context.WebSockets.IsWebSocketRequest)
                    {
                        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);
                }
            }
    
    

    在Hanle方法等待客户端的消息

            private async Task Handle(WebsocketClient webSocket)
            {
                WebsocketClientCollection.Add(webSocket);
                _logger.LogInformation($"Websocket client added.");
               
                WebSocketReceiveResult result = null;
                do
                {
                    var buffer = new byte[1024 * 1];
                    result = await webSocket.WebSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
                    if (result.MessageType == WebSocketMessageType.Text && !result.CloseStatus.HasValue)
                    {
                        var msgString = Encoding.UTF8.GetString(buffer);
                        _logger.LogInformation($"Websocket client ReceiveAsync message {msgString}.");
                        var message = JsonConvert.DeserializeObject<Message>(msgString);
                        message.SendClientId = webSocket.Id;
                        MessageRoute(message);
                    }
                }
                while (!result.CloseStatus.HasValue);
                WebsocketClientCollection.Remove(webSocket);
                _logger.LogInformation($"Websocket client closed.");
            }
    

    在MessageRoute方法内对客户端的消息进行转发

    对客户端的消息定义几个标准的action,对不同的action进行特定的处理,比如加入房间、离开房间、在房间内广播消息等。

    private void MessageRoute(Message message)
            {
                var client = WebsocketClientCollection.Get(message.SendClientId);
                switch (message.action)
                {
                    case "join":
                        client.RoomNo = message.msg;
                        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.GetRoomClients(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":
                        var roomNo = client.RoomNo;
                        client.RoomNo = "";
                        client.SendMessageAsync($"{message.nick} leave room {roomNo} success .");
                        _logger.LogInformation($"Websocket client {message.SendClientId} leave room {roomNo}");
                        break;
                    default:
                        break;
                }
            }
    

    新建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> GetRoomClients(string roomNo)
            {
                var client = _clients.Where(c => c.RoomNo == roomNo);
                return client.ToList();
            }
        }
    

    在Startup中使用中间件

    有了上面的中间件,我们需要use一下。

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

    到此我们的服务端基本完成了,下面进行客户端html跟JavaScript的编写。

    编写客户端界面

    修改index.cshtml来实现一个简单的聊天室ui。

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

    使用JavaScript来处理websocket链接及消息

    现代浏览器已经都支持websocket协议,JavaScript运行时内置了WebSocket类,我们仅仅需要new一个Websocket对象出来就可以对websocket进行操作。

    
    var server = 'ws://localhost:5000'; //如果开启了https则这里是wss
    
    var WEB_SOCKET = new WebSocket(server + '/ws');
    
    WEB_SOCKET.onopen = function (evt) {
        console.log('Connection open ...');
        $('#msgList').val('websocket connection opened .');
    };
    
    WEB_SOCKET.onmessage = function (evt) {
        console.log('Received Message: ' + evt.data);
        if (evt.data) {
            var content = $('#msgList').val();
            content = content + '
    ' + evt.data;
    
            $('#msgList').val(content);
        }
    };
    
    WEB_SOCKET.onclose = function (evt) {
        console.log('Connection closed.');
    };
    
    $('#btnJoin').on('click', function () {
        var roomNo = $('#txtRoomNo').val();
        var nick = $('#txtNickName').val();
        if (roomNo) {
            var msg = {
                action: 'join',
                msg: roomNo,
                nick: nick
            };
            WEB_SOCKET.send(JSON.stringify(msg));
        }
    });
    
    $('#btnSend').on('click', function () {
        var message = $('#txtMsg').val();
        var nick = $('#txtNickName').val();
        if (message) {
            WEB_SOCKET.send(JSON.stringify({
                action: 'send_to_room',
                msg: message,
                nick: nick
            }));
        }
    });
    
    $('#btnLeave').on('click', function () {
        var nick = $('#txtNickName').val();
        var msg = {
            action: 'leave',
            msg: '',
            nick: nick
        };
        WEB_SOCKET.send(JSON.stringify(msg));
    });
    
    

    运行

    至此我们的聊天室已经搭建完成了,运行一下看看效果。我们启动两个页面,进行聊天。
    可以看到我们的消息被实时的转发出去了,good job !

    源码

    源码已上传github
    CoreWebsocketChatRoom

    关注我的公众号一起玩转技术

  • 相关阅读:
    [总结]并查集
    一些麻烦的语法知识
    P1496 找筷子
    P1314 [NOIP2011 提高组] 聪明的质监员
    HDU 1232 -畅通工程(并查集)
    POJ 1611 -The Suspects (并查集)
    方框(HPU暑期第四次积分赛)
    HDU 2191
    B
    HDU 1009
  • 原文地址:https://www.cnblogs.com/kklldog/p/core-for-websocket.html
Copyright © 2020-2023  润新知