.NET Core 基於Websocket的在線聊天室

作者:Agile.Zhou(kklldog)
出處:
http://www.cnblogs.com/kklldog/

什麼是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;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進行操作。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章