.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

相關文章
相關標籤/搜索