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

来源:https://www.cnblogs.com/kklldog/archive/2020/03/05/core-for-websocket.html
-Advertisement-
Play Games

什麼是Websocket 我們在傳統的客戶端程式要實現實時雙工通訊第一想到的技術就是socket通訊,但是在web體系是用不了socket通訊技術的,因為http被設計成無狀態,每次跟伺服器通訊完成後就會斷開連接。 在沒有websocket之前web系統如果要做雙工通訊往往使用http long p ...


什麼是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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文源碼: "GitHub·點這裡" || "GitEE·點這裡" 一、併發編程簡介 1、基礎概念 程式 與電腦系統操作有關的電腦程式、規程、規則,以及可能有的文件、文檔及數據。 進程 進程是電腦中的程式,關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎 ...
  • 1. calendar處理日期 calendar模塊第一了Calendar類,其中封裝了一些值的計算,如給定的一個月或一年中的周日期。另外,TextCalendar和HTMLCalendar類可以生成經過預格式化的輸出。 1.1 格式化示例 prmonth()方法是一個簡單的函數,可以生成月的格式化 ...
  • 本文根據Jin Liang的《Getting Started with Machine Learning》ver 0.96而寫,基本上就是翻譯,但並不是逐字翻譯。 第一部分 機器學習基礎 01.概述 應用:自動駕駛,面部識別,垃圾郵件識別,信用卡欺詐識別,機器驗傷,銷售預測,語音識別,機器人。 深度 ...
  • 系列文章索引: 1. "Spring事務Transactional和動態代理(一) JDK代理實現" 2. "Spring事務Transactional和動態代理(二) cglib動態代理" 3. "Spring事務Transactional和動態代理(三) 事務失效的場景" 什麼是代理 理設計模式 ...
  • @2020.3.4 Python語法入門-變數和基本數據類型-練習一 習題 #1、病毒程式需要定期將監控到的數據寫入日誌文件,請記錄下日誌文件路徑C:\a\b\c\adhsvc.dll.system32,方便後期處理 #2、病毒程式在上傳文件時,發送的報頭數據里需要包含文件信息:文件名a.txt、大 ...
  • 1. Java和C++最大的不同在於Java採用的指針模型可以消除重寫記憶體和損壞數據的可能性。 2. Java沒有C++的多重繼承,取而代之的是更簡單的介面概念。 3. 從一開始Java就設計能防範各種攻擊,包括:運行時堆棧溢出、破壞自己的進程空間之外的記憶體、未經授權的讀寫文件。 4. javac程 ...
  • 獸人,金幣,布爾…… 看這麼多次都膩了。最起碼我們得把他們簡化一點。 簡介 使用 與 (AND) 操作符時,如果第一個條件 (在 AND 左邊那個) 為假,第二個條件 (右邊那個) 永遠不會執行。 你可以好好利用這點! 這些代碼有可能出錯: enemy = hero.findNearestEnemy ...
  • PhalApi是一款國人製作的PHP純後端框架。它的開發相當簡單,同時也具備文檔生成等特色功能。下麵,我通過簡單的幾點,讓你可以快速入門使用該框架的開發。 建議使用PHPStorm作為IDE,代碼提示相當完全。由於PHP的熱更新特性,修改過的PHP文件保存後立即生效,無需編譯,無需重啟伺服器。 什麼 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...