--- 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` 應用 一如往常所做,到 `檔案` » `新增` » `專案` 並且 給專案一個名子。 ![VS2017 Web專案建立畫面](https://i.imgur.com/Qtf088W.png) 然後 樣板選擇 `Web 應用程式`, framework 選 `ASP.NET Core 2.1` ![ASP.NETCore Web專案詳細建立畫面](https://i.imgur.com/fvGqM3n.png) 完成後,您應該得到在方案資料節中的專案。 我們將開始撰寫我們的`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 初始 與 用戶端函式庫安裝](/images/npm_init_and_add_client_lib_zh-tw.png "Optional title") 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">&nbsp;</div> <div class="row"> <div class="col-6">&nbsp;</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">&nbsp;</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); }, }); ``` 這個程式碼前後展示了串流連結非同步關係,在我們點擊按鈕後啟動串流連接。 這是串流的輸出結果: ![成果展示](https://i.imgur.com/4tSXzNE.png) ![成果展示 GIF動畫](https://i.imgur.com/1lpwNfp.gif) 上圖式我修改了`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`,克服了將資料從伺服器傳輸到用戶端一次傳輸所有內容的問題。 當你認為你的資料很大或者你想要一些使用者體驗,而不是通過展示無盡的`載入圈圈/等待沙漏`阻塞用戶端時,我會建議你選擇串流。