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

https://www.cnblogs.com/kklldog/archive/2020/03/05/core-for-websocket.htmlhtml


什么是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状态码表示“握手”成功。

git

//客户端请求
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的请求,把创建的连接统一进行管理,其实微软已经帮咱们简单的封装过了。github

新建一个asp.net core网站

新建WebsocketHandlerMiddleware中间件

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

在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一下。websocket

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

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

编写客户端界面

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

<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;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>

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

现代浏览器已经都支持websocket协议,JavaScript运行时内置了WebSocket类,咱们仅仅须要new一个Websocket对象出来就能够对websocket进行操做。asp.net

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 + '\r\n' + 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

相关文章
相关标签/搜索