ASP.NET Core 聊天室實現(SingalR)
文章目錄
本篇文章的開發環境:
- VS2017
- .NET Core 2.2
- js客戶端 @aspnet/signalr 1.1.4
SignalR
SignalR
是一個開源的庫,一個構建實時應用的框架。實時應用可以在服務器中將內容實時推送到客戶端中,特別適合聊天應用,股票交易,新聞推送,遊戲等等非常多的應用場景。
機制
SignalR
的機制其實很像遠程過程調用,即RPC
,因爲SignalR區分服務端和客戶端,服務端和客戶端分別都要註冊相應的函數,然後服務端調用在客戶端註冊的函數從而實現可以從服務端向客戶端推送消息。
中心
SignalR
的中心可以理解爲是一組通訊接口,在服務端中心上定義的方法將可以被客戶端調用,而在客戶端中心上的定義的方法將可以被服務端調用。
組和用戶
SignalR
中組的概念可以理解爲微信羣或者QQ羣,如果我們把不同的連接id添加到一個組裏,那麼我們可以往這個組裏推送信息,那麼就只有組裏的人能收到推送。
SignalR
中的用戶又是什麼?假設你有多臺設備,手機,筆記本,平板等。每臺設備都連接到了SignalR
的應用當中,那麼當SignalR
以用戶標識爲參數來推送那麼你所有連接到應用並且有你的標識的設備就都會收到推送。
傳輸方式
SignalR
支持以下的通訊協議:
- WebSockets
- 服務器發送的事件
- 長輪詢
SignalR
會自動選擇在客戶端和服務端之間最好的連接方式。最差的情況就是採用長輪詢了。
所以該框架本身會幫我們把具體的連接實現隱藏起來,從而提供給使用者完全一致的使用體驗。
下面廢話不多說,直接上代碼。
SignalR代碼示例
添加中間件
首先我們需要在Setup.cs
,ConfigureServices
方法中添加SignalR
的中間件:
注意:asp.net core 中已經包含了SignalR的服務端的庫,但是js之類的客戶端庫需要自己通過npm進行下載。
services.AddSignalR();
然後在Configure
方法中設置SignalR
的路由:
app.UseSignalR(configure =>
{
configure.MapHub<ChatHub>("/api/chat");
});
之後我們就可以通過/api/chat
連接到SignalR服務端。
定義中心
下面我們來定義一箇中心,這需要繼承Hub來創建中心,並向中心添加方法,客戶端可以調用標識符爲public
的方法:
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
客戶端代碼
由於大部分的場景都是使用的瀏覽器,所以我們這裏就使用js的客戶端(還有java,.net客戶端),可以通過npm命令下載@aspnet/signalr
這個包
npm install @aspnet/signalr
下載後我們需要把路徑node_modules\@aspnet\signalr\dist\browser
中的signalr.js
文件複製到前端項目的輸出目錄。
然後就可以正常編寫js代碼了。
下面的代碼就是在在客戶端連接到服務端中心,並且定義一個客戶端的中心方法:
//創建SignalR連接對象
const connection = new signalR.HubConnectionBuilder()
.withUrl("/api/chatHub")
.build();
//定義方法
connection.on("ReceiveMessage", function (user, message) {
//收到推送的具體實現,user,message就是服務器推送來的信息,可以更新顯示到前端頁面中。
});
//連接到服務器中心
connection.start().then(function(){
//連接成功
}).catch(function (err) {
//連接異常err
});
好了,那該做的工作都做好了,那麼這個聊天室具體如何工作呢?
我們先來考慮一下前端頁面發送信息如何處理,假設我們在前端頁面輸入一段信息,按發送按鈕發送出去,前端代碼如下:
document.getElementById("sendButton").addEventListener("click", function (event) {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection.invoke("SendMessage", user, message).catch(function (err) {
//出現異常
});
});
我們可以看到這裏給sendButton
綁定了一個單擊事件,事件內容就是獲取輸入的用戶名和信息,然後調用我們在上一段代碼中的connection
對象,該對象有一個invoke
方法,第一個參數就是服務器上的SignalR
中心所定義的方法SendMessage
,之後的參數對應的就是服務器中心上的參數列表。這裏實際上就是調用了服務器上所定義的方法。
那我們看到服務器上面這個方法,就是這一段:
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
可以看到服務器上的SendMessage
方法是對所有的已連接到服務器上SignalR
的客戶端都調用客戶端的ReceiveMessage
方法,並且傳遞兩個參數,一個用戶名一個信息。在客戶端中心中定義的ReceiveMessage
將會被執行,並且傳入兩個參數。
所以這就實現了在一個客戶端點擊發送按鈕,所有的客戶端都收到了推送。
SignalR進階內容
點對點發送
上面的例子中是客戶端點擊發送按鈕後就推送到所有的客戶端去了。我們知道推送到單個客戶端也是可能能夠實現的,那麼如何實現呢?
我們定義在服務器上的中心是需要繼承Hub
,而Hub
中包含了三個屬性,分別是
類型 | 屬性名稱 | 作用 |
---|---|---|
IHubCallerClients | Clients | 客戶端選擇 |
HubCallerContext | Context | 連接上下文,連接id等 |
IGroupManager | Groups | 組管理 |
所以也就是我們上面看到的我們能夠訪問Clients
的原因,要實現點對點發送,我們就可以用
Clients.Client(connectionId).SendAsync("ReceiveMessage", user, message);
來實現指向特定連接id來進行發送。
注意:連接id是不能夠在客戶端代碼中獲取的,微軟也不建議暴露連接id,所以一般我們可以在用戶登陸時在服務端獲取連接id,手動把用戶名或者其他用戶標識關聯該連接id。連接id可以在Hub類中的Context屬性中獲取。
其他客戶端選擇方式
上面的是點對點的客戶端選擇,如果我們看Hub
類裏其實還有很多的選擇方式:
屬性或方法名稱 | 作用 |
---|---|
Caller | 調用者 |
Others | 除了調用者以外 |
OthersInGroup | 除了指定組以外 |
All | 所有連接的客戶端 |
AllExcept | 除了一組指定的連接id以外 |
Client | 指定某一連接id |
Clients | 指定多個連接id |
Group | 指定某一組 |
GroupExcept | 指定某一組,並且除開指定的多個連接id |
Groups | 指定多個組 |
User | 指定某一用戶 |
Users | 指定多個用戶 |
服務端推送
上面的例子我們是聊天室所以是由客戶端觸發,並且發送到客戶端的。那麼如果想實現服務端自動推送該如何實現呢?
我們可以通過構造函數注入的方式來獲取一個IHubContext<ChatHub>
實例,如下:
public class SomeController : Controller
{
private readonly IHubContext<ChatHub> _chatHub;
public SomeController(IHubContext<ChatHub> chatHub)
{
_chatHub = chatHub;
}
}
然後可以在控制器中調用_chatHub
:
public async Task<IActionResult> Index()
{
await _hubContext.Clients.All.SendAsync("ReceiveMessage", "user", "someMessage");
return View();
}
強類型中心
我們之前所定義的中心是繼承Hub
這個類,我們調用客戶端上註冊的方法是通過SendAsync("ReceiveMessage", user, message);
方法指定一個客戶端上方法的名稱來進行調用的,這樣有可能會因爲方法拼寫錯誤而造成方法調用失敗。
所以爲了代替Hub
,我們可以引入強類型中心Hub<T>
來實現,把客戶端的方法放到一個接口中:
public interface IChatClient
{
ReceiveMessage(user,message);
}
然後再繼承Hub<IChatClient>
:
public class ChatHub : Hub<IChatCilent>
{
public async Task SendMessage(string user, string message)
{
await Clients.All.ReceiveMessage(user, message);
}
}
這樣經過泛型來啓用編譯時來對名稱進行檢查可以防止因爲拼寫錯誤而導致的很多問題。
組
如何創建組?我們留意到Hub
類中有一個IGroupManager
類型的名爲Groups
的屬性,通過該屬性可以添加某個連接id到組,或者從組裏移除某連接id。
添加某連接id到組:
await Groups.AddToGroupAsync(ConnectionId, groupName);
移除某連接id:
await Groups.RemoveFromGroupAsync(ConnectionId, groupName);
對某個組推送信息,可以按照前面的客戶端選擇來選擇組然後發送。
注意:重新建立連接後不會保留組成員的身份(可能因爲連接id不一樣),所以重新連接後需要重新加入組。
自定義類型參數
微軟推薦採用自定義類型的參數作爲服務端和客戶端之間數據的交換。我們之前定義的中心方法是採用的兩個形參,如果以後服務端增加一個參數,三個形參,那麼客戶端如果還沒修改過來繼續傳遞兩個實參過來,那麼就會報錯。
而如果用自定義類型如下:
public class ChatParam
{
User{get;set;}
Message{get;set;}
Other{get;set;}
}
就算後面服務端繼續添加了一個參數,如果客戶端繼續發送兩個參數過來,那麼第三個參數也只是null
,而不會報錯,保證了系統的向後兼容性。
跨域
如果請求存在跨域,那麼需要在Setup.cs
裏開啓允許跨域請求客戶端才能夠連接上SignalR
。
總結
上面就是SignalR
這個實時框架的一些使用入門了,總的來說該框架隱藏了很多的細節,而且學習曲線很平滑,挺輕量的一個庫。性能方面我沒有做過多的測試所以性能方面的表現就不太清楚了。
喜歡這篇文章的話關注我的公衆號吧,有空就分享一下技術文章,哈哈哈。