---
tags: C#, Asp.net Core, SignalR, 翻譯文章
---
# 於`Asp.Net Core SignalR` 上的串流
原文:Streaming in `ASP.NET Core SignalR`
文章出處:<https://coderethinked.com/streaming-in-asp-net-core-signalr>
原文作者: Karthik Chintala
原文發表時間: 2018/08/30
譯者 : Cymon Dez
翻譯時間 : 2018/09/02
確認範例內容可執行 : 2018/09/03
目錄
[TOC]
<!-- markdownlint-disable MD001 MD026 -->
## 前言
隨著`ASP.NET Core 2.1`的發佈,SignalR開始支援串流內容(streaming content)。在此文章中,我們將會看到如何在`ASP.NET Core SignalR`中使用串流傳輸資料
## 什麼是串流 ?
我們來看看引述自 webopedia.com 的串流定義
>Streaming or media streaming is a technique for transferring data so that it can be processed as a steady and continuous stream. – [webopedia\.com](https://www.webopedia.com/TERM/S/streaming.html)
## 串流的使用時機 ?
在資料與伺服器有一些延遲的情況下,而我們不需等待內容到達。 對於這種情況,我們可以使用資料串流。
當我們不想一次獲取所有資料時,這也很有用,因為這可能非常耗時。 因此,我們將以 區塊 或 片段 的形式將資料從伺服器發送到用戶端。
---
現在,我們開始使用 **Visual Studio** 建立 `ASP.NET Core SignalR`
## 建立專案 `ASP.NET Core` 應用
一如往常所做,到 `檔案` » `新增` » `專案` 並且 給專案一個名子。

然後 樣板選擇 `Web 應用程式`, framework 選 `ASP.NET Core 2.1`

完成後,您應該得到在方案資料節中的專案。
我們將開始撰寫我們的`Hubs`。(SignalR Hub 觀念,請參閱<https://docs.microsoft.com/zh-tw/aspnet/core/signalr/hubs?view=aspnetcore-2.1>)
## 中樞(Hub) 設置
不同於一般的SignalR方法(method),串流方法(stream methods)的資料塊(chunks of data)可用時,會隨時間推移傳輸內容。
在專案中創建一個名為`StreamHub.cs`或其他的C# 文件()。最好將它添加到資料夾中。
從`Hub`類繼承並在文件中加入名稱空間`Microsoft.AspNetCore.SignalR`:
```csharp
using System.Threading.Tasks;
using System.Threading.Channels;
using Microsoft.AspNetCore.SignalR;
namespace CodeRethinked.SignalRStreaming
{
public class StreamHub :Hub
{
}
}
```
現在,在類別中中建立一個方法(method),回傳型別為`ChannelReader <T>`,其中T是回傳的串流資料型別。
回傳型別`ChannelReader`為構成串流中樞方法(streaming hub)的關鍵。
以下是串流資料傳輸的程式碼:
```csharp
public ChannelReader<int> DelayCounter(int delay)
{
var channel = Channel.CreateUnbounded<int>();
_ = WriteItems(channel.Writer, 20, delay); // _ 為C#7.0的語法,_ (底線字元)作為捨棄變數
return channel.Reader;
}
private async Task WriteItems(ChannelWriter<int> writer, int count, int delay)
{
for (var i = 0; i < count; i++)
{
//For every 5 items streamed, add twice the delay
if (i % 5 == 0)
delay = delay * 2;
await writer.WriteAsync(i);
await Task.Delay(delay);
}
writer.TryComplete(); // 串流已傳輸完成並且關閉用戶端
}
```
+ `DelayCounter`是我們的串流方法(streaming method),具有一個來自用戶端的參數 `delay`作為延遲時間。
+ `WriteItems`是一個私有方法,且回傳Task。(一個私有的非同步方法)
+ `WriteItems`的最後一行是`.TryComplete()`,代表串流已傳輸完成並且關閉用戶端。
## 在專案中設定 SignalR
以下的設定原理請參見`Asp.Net Core`的`Middleware`(譯者編按:或者去上保哥開的課,會學到更多)
1. 移至 類別`Startup`並找到`ConfigureServices`方法(method)並在最後添加以下幾行(如果你可以自己設定,請跳過這個)
```csharp
services.AddSignalR();
```
2. 我們還需要為*signalR*串流添加路由(route)。 現在,類別`Startup`中的`Configure`方法並加入以下內容
```csharp
app.UseSignalR(routes =>
{
routes.MapHub<StreamHub>("/streamHub");
});
```
## 加入 SignalR 用戶端函式庫( client library )
在用戶端加入`signalR.js`。
從Visual Studio啟動 套件管理器主控台 (`檢視(V)` » `其他視窗(E)` » `套件管理器主控台(O)`),並使用以下命令導航到專案資料夾
```shell
cd CodeRethinked.SignalRStreaming
```
執行 `npm init` 來建立檔案 `package.json`
```shell
npm init -y
```
安裝 signalR 用戶端函式庫,忽略所有的警告訊息。
```shell
npm install @aspnet/signalr
```

npm install會將signalR用戶端函式庫下載到`node_modules`資料夾下的子資料夾`@aspnet\signalr`。
`node_modules`資料夾不會顯示於Visual Studio 的方案總管中。(使用`資料夾檢視`時才看得到)
## 從 node_modules 複製 signalR
將`signalr.js`從`<project_folder>\node_modules\@aspnet\signalr\dist\browser`複製到`wwwroot\lib\signalr`的資料夾中。
或者...
您也可以使用`Microsoft Library Manager`(libman.json)為您恢復它。
如果你不知道libman.json是什麼。 查看關於Libman的[這篇文章](http://coderethinked.com/libman-microsoft-library-manager-for-client-side-libraries)或[官方文章](https://docs.microsoft.com/zh-tw/aspnet/core/client-side/libman/libman-vs?view=aspnetcore-2.1)。
所以,你的Libman添加下載的signalR應該是這樣的。
```json
{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"provider": "filesystem",
"library": "node_modules/@aspnet/signalr/dist/browser/signalr.js",
"destination": "wwwroot/lib/signalr"
}
]
}
```
一旦你將`libman.json`存檔,我們的`signalr.js`將在可在`lib`的`SignalR`資料夾中使用。
## 使用串流的 HTML
將以下HTML內容複製到`Index.chtml`。出於本文的目的,我將刪除`Index.cshtml`中的現有HTML並加入以下內容:
```htmlmixed=
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="container">
<div class="row"> </div>
<div class="row">
<div class="col-6"> </div>
<div class="col-6">
<input type="button" id="streamButton" value="Start Streaming" />
</div>
</div>
<div class="row">
<div class="col-12">
<hr />
</div>
</div>
<div class="row">
<div class="col-6"> </div>
<div class="col-6">
<ul id="messagesList"></ul>
</div>
</div>
</div>
<script src="~/lib/signalr/signalr.js"></script>
<script src="~/js/signalrstream.js"></script>
```
Notice we have `signalrstream.js` at the end. Let’s add the js file to stream the content.
注意我們最後有`signalrstream.js`。 讓我們添加js檔案來完成串流傳輸內容。
## JavaScript setup
在 `wwwroot\js`中,建立一個新的檔案 `signalrstream.js`,並加入下列程式碼於該js檔中:
```javascript=
// signalrstream.js
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var connection = new signalR.HubConnectionBuilder()
.withUrl("/streamHub")
.build();
document.getElementById("streamButton")
.addEventListener("click", (event) => __awaiter(this, void 0, void 0, function* () {
try {
connection.stream("DelayCounter", 500)
.subscribe({
next: (item) => {
var li = document.createElement("li");
li.textContent = item;
document.getElementById("messagesList").appendChild(li);
},
complete: () => {
var li = document.createElement("li");
li.textContent = "Stream completed";
document.getElementById("messagesList").appendChild(li);
},
error: (err) => {
var li = document.createElement("li");
li.textContent = err;
document.getElementById("messagesList").appendChild(li);
},
});
}
catch (e) {
console.error(e.toString());
}
event.preventDefault();
}));
(() => __awaiter(this, void 0, void 0, function* () {
try {
yield connection.start();
}
catch (e) {
console.error(e.toString());
}
}))();
```
由於`ASP.NET SignalR`現在使用ES6 功能,但並非所有瀏覽器都支持ES6功能。因此,為了使其在所有瀏覽器中均能正常工作,建議使用諸如babel之類的轉發器。
與傳統的signalR不同,我們現在有不同的語法來創建連接(`signalrstream.js`中的片段):
```javascript=
var connection = new signalR.HubConnectionBuilder()
.withUrl("/streamHub")
.build();
```
對於常規signalR連接,我們將使用`.on`方法添加監聽器(listener),但這是串流,我們必須為串流方法傳遞兩個參數。
+ Hub方法名稱:我們的中樞(Hub)名稱是`DelayCounter`
+ Hub方法的參數:在我們的例子中,參數是串流的延遲時間。
`connection.stream`將有訂閱方法來訂閱事件。 我們將連接下一個完整和錯誤事件,並在`messagesList`元素中顯示消息。
(`signalrstream.js`中的片段)
```javascript=
connection.stream("DelayCounter", 500)
.subscribe({
next: (item) => {
var li = document.createElement("li");
li.textContent = item;
document.getElementById("messagesList").appendChild(li);
},
complete: () => {
var li = document.createElement("li");
li.textContent = "Stream completed";
document.getElementById("messagesList").appendChild(li);
},
error: (err) => {
var li = document.createElement("li");
li.textContent = err;
document.getElementById("messagesList").appendChild(li);
},
});
```
這個程式碼前後展示了串流連結非同步關係,在我們點擊按鈕後啟動串流連接。
這是串流的輸出結果:


上圖式我修改了`StreamHub`類別的內容,使gif圖像中的數量達到10,這樣它不需要跑這麼長了。
注意串流傳輸時項目6-10的延遲是因為我們將每5個項目的延遲量增加了一倍。這可以被認為是僅在可用時(available)傳輸串流資料。因此,第6項會在可用時進行串流傳輸。
因此,如果要將大量的資料發送到用戶端,則轉而使用串流傳輸而不是立即發送資料。
## 原始碼下載
原始碼放在[github](https://github.com/karthikchintala1/CodeRethinked.SignalRStreaming)上。我移除了`npm_modules` 使專案輕量化,請自行使用npm重新安裝相關模組並建置/啟動專案。
```shell
npm install
```
## 重點提示
串流並不是新技術,但它現在是signalR中的一個很棒的功能。 串流式傳輸將保持非常酷的使用者體驗,而且我們的服務器也不會有那些高附載( high bars )於高峰時段。
大多數開發人員都知道SignalR無法傳輸大量資料的局限性。
使用`ASP.NET Core SignalR`,克服了將資料從伺服器傳輸到用戶端一次傳輸所有內容的問題。
當你認為你的資料很大或者你想要一些使用者體驗,而不是通過展示無盡的`載入圈圈/等待沙漏`阻塞用戶端時,我會建議你選擇串流。