###### tags: `Akka` `Web` `DotNet Core` # 從零到有Akka整合Dotnet Core Web建置程控系統-Akka透過Web Socket(Signalr)與Web頁面溝通 此章討論Akka在Web啟用後,該如何與Web做互動?我們選用Dot Net Core Signalr機制去做Web與Akka的訊息傳遞。 我們沿用上一篇做的在Web啟用Akka系統,在已建置好的Plc做一個實際操作,接收外部Plc的Tracking Map訊息顯示在Web Client頁面上,以及透過Web Client頁面按鈕點擊發送要求Tracking Map要求給外部PLC。 ## 情境一、接收Tracking Map訊息顯示在Web(PLC訊息 -> Web Client) ### Step1:定義Event,在ExternalSys類別庫新增Event 目前要實做接收Plc鋼捲Track Map資料顯示至Web,首先我們需先在AkkaSys系統下設計Event事件。 我們根據功能去分類Event,在此我們建立一個Track Event介面讓Web Server去引用實做。為了測試方便我們先假設TrackMap訊息資料型態為一般字串string。 ![](https://i.imgur.com/swHncH4.png) - Event資料夾 - Tracking資料夾 - ITrackingEventPusher Class - ITrackingEventPusher Class ```= public interface ITrackingEventPusher { // 更新Web Client TrackMap頁面 void UpdateTrackingMap(string trkMap); } ``` ### Step2:在Web Server建至Web Socket(Signalr) 在AkkaSys定義好通訊功能介面後,接著在Web Server建立Web Socket,在Dot Net Core Solution又稱做Signalr(Hub)。 <font color="#f00">在此我們使用xxxChannel名稱去定義Hub Class Name,若Web Client將資料透過Web Server送至AkkaSys就會使用到此Class方法,因此裡面需注入SysAkkaManager。 而由AkkaSys將資料訊息推到前端去則用xxxxPusher去定義Class Name。</font> ![](https://i.imgur.com/HL8QD0M.png) - Hubs資料夾 - TrackingChannel Class : Web Client -> Plc ```= public class TrackingChannel : Hub { private readonly ISysAkkaManager _akkaManager; public TrackingChannel(ISysAkkaManager akkaManager) { _akkaManager = akkaManager; } } ``` - TrackingEventPusher Class : Plc -> Web Client 宣告IHubContext,並將TrackingChannel載入泛型,接著實做 ITrackingEventPusher Method。當收到Plc鋼捲追蹤廣播全部Client。 ```= public class TrackingEventPusher : ITrackingEventPusher { private readonly IHubContext<TrackingChannel> _trackHubContext; public TrackingEventPusher(IHubContext<TrackingChannel> trackContext) { _trackHubContext = trackContext; } public void UpdateTrackingMap(string trkMap) { _trackHubContext.Clients.All.SendAsync("UpdateTrackingMap", trkMap); } } ``` ### Step3:在Web Server設置Starup 此不我們要在Starup設置Signalr與剛剛實做的TrackingEventPusher做DI相關設置。並在Dot Net Core Middleware設置Signalr的Channel Path Name。 - 在Starup裡的ConfigureServices設置Signalr DI設置 ```= // Signalr services.AddSignalR(); ``` - 在Starup裡的ConfigureServices設置ITrackingEventPusher對應實做Class DI設置 ```= services.AddScoped<ITrackingEventPusher, TrackingEventPusher>(); ``` - 在Starup裡的Configure設置Hub Url Path 在Starup裡的UseEndpoints內設置Hub Path ```= app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); // Hub Path endpoints.MapHub<TrackingChannel>("/trackChannel"); }); ``` ### Step4:在Web Server用戶端程式碼安裝Signalr lib 至Web Server wwwroot資料夾加入用戶端程式庫,新增Signalr類別庫 ![](https://i.imgur.com/Z54YWOv.png) ### Step5:在wwwroot下新增trackchat.js(www) 安裝完Signalr後,接著要在Client實作註冊Signalr監聽,前端若有註冊EventPush Class Method,Web Server則可以將所收到Plc資料廣播出去給前端。 ![](https://i.imgur.com/i74r1YQ.png) - trackchat內容 ```= // 建立Connection var connection = new signalR.HubConnectionBuilder().withUrl("/trackChannel").build(); // Connect BackEnd and FrontEnd Init connection.start().then(function () { // Connect Sucess }).catch(function (err) { return console.error(err.toString()); }); // Register Hub Method (Akka->FrontEnd) connection.on("UpdateTrackingMap", function (trkMap) { // Action }); ``` ### Step6:在Web Server View資料夾撰寫index.html 我們在index.hmtl檔案裡撰寫一段html.code,新增一個標籤元件,並賦予ID=messageList,並在下面引用Script ![](https://i.imgur.com/8nMJHwj.png) ```= <div class="row"> <div class="col-6"> <ul id="messagesList"></ul> </div> </div> <script src="~/js/signalr/dist/browser/signalr.js"></script> <script src="~/js/hubs/trackchat.js"></script> ``` ### Step7:View撰寫完後接著至trackchat.js寫收到TrakingMap資訊時顯示到html上。 到trackchat.js檔把剛剛在view撰寫的標籤tag顯示寫進所註冊監聽的UpdateTrackingMap中。當監聽收到Web Server推播訊息顯示在ul標籤上。 ![](https://i.imgur.com/i74r1YQ.png) ```= connection.on("UpdateTrackingMap", function (trkMap) { // 顯示資料 var encodedMsg = trkMap; var li = document.createElement("li"); li.textContent = encodedMsg; document.getElementById("messagesList").appendChild(li); }); ``` ### Step8:在PlcRcvEdit注入ITrackingEventPusher 完成Web Client後,接下來要將Web所實做的TrackingEventPusher注入到PlcRcvEdit建構子中。在此直接在建構子多加一個引入介面ITrackingEventPusher。並宣告內部ITrackingEventPusher物件變數,去接取DI所注入的實體Class。當建構子多加一個引入介面後,記得也須同步更改Web Server Starup AkkaDIService的PlcRcvEdit設置。 在以上設置好後,因為在系統接收職責,PlcRcv定義為接收外部Plc第一接口人驗證資料正確性後傳至PlcRcvEdit,在將訊息解析成系統ORM Model後才會在傳到Web Server推波。故PlcRcv也需做處理將資料轉給PlcRcvEdit這段程式碼。 - PlcRcvEdit ```= public class PlcRcvEdit : BaseActor { private readonly ILog _log; // 宣告ITrackingEventPusher private readonly ITrackingEventPusher _trackingEventPusher; // 建構子新增ITrackingEventPusher輸入參數 public PlcRcvEdit(ILog log, ITrackingEventPusher trackingEventPusher) : base(log){ _log = log; _trackingEventPusher = trackingEventPusher; // 接收PlcRcv訊息 Receive<byte[]>(message => ProTcpRcvMsg(message)); } // 當接收到資料後,會透過Signlr廣播至前端顯示 private void ProTcpRcvMsg(byte[] msg) { var trkMap = System.Text.Encoding.Default.GetString(msg); _trackingEventPusher.UpdateTrackingMap(trkMap); } } ``` - 更改Web Service AkkaDIService 補ITrackingEventPusher注入至PlcRcvEdit ```= _service.AddScoped(p => { var trkMapEventPuhser = p.GetService<ITrackingEventPusher>(); return new PlcRcvEdit(GetLog<PlcRcvEdit>(p, "PlcRcvEditLog") , trkMapEventPuhser); }); ``` - PlcRcvEdit ```= private readonly ISysAkkaManager _akkaManager; private readonly ILog _log; private readonly IActorRef _plcRcvEditActor; public PlcRcv(ISysAkkaManager akkaManager, AkkaSocketIP akkaSysIp, ILog log) : base(akkaSysIp, log) { _akkaManager = akkaManager; _plcRcvEditActor = akkaManager.GetActor(nameof(PlcRcvEdit)); _log = log; } protected override void TcpReceivedData(Tcp.Received msg) { _log.I("接收到訊息"," [Info] Handle_Tcp_Received. message=" + msg.ToString()); _log.I("接收到訊息"," [Info] Count=" + msg.Data.Count.ToString()); // 將訊息傳至PlcRcvEdit _plcRcvEditActor.Tell(msg.Data.ToArray()); } ``` ### 測試 ![](https://i.imgur.com/Xk3BJnn.png) ## 情境二、在Web Client按要求TrackintMap,將要求訊息傳送至外部PLC(Web Client -> PLC) ### Step1:至Web Server View的index.html新增一個Button按鈕 第一步當然是至Web Client撰寫一個觸發按鈕,在此我們新增一個要求鋼捲追蹤訊息的Button element。並給予id=btn_reqtrack。 ![](https://i.imgur.com/8nMJHwj.png) ```= <button id="btn_reqtrack" class="btn-success" data-toggle="tooltip" title="要求鋼捲追蹤">要求鋼捲追蹤訊息</button> ``` ### Step2:至Web Server wwwroot資料夾新增viewjs資料夾並新增一個index.js檔 我們遺棄上述的trackchat.js,並在wwwroot新增一個viewjs資料夾,接著新增index.js檔案。 <font color="#f00">因目前Dot Net Core前端部分不像Angular, Vue有成熟的框加架構,在此例直接使用傳統JQuery寫法。 除了公用js方法,View建議檔案分類基本上就是一個view對應一個js檔</font> 因此我們將剛剛trackat.js的檔案內容直接複製到index.js裡,宣告剛剛在index.html的按鈕元件並註冊Click事件。 註冊完Button Click事件後,撰寫signlar invoke,調用trackingChannel的ReqTrackScan Method(第一參數輸入名稱要與Method Name一樣)。 ![](https://i.imgur.com/9ZAzA2H.png) ```= // 控制項元素 var _btn_reqtrack; // 取得 dom 上所需元素 function GetElement() { _btn_reqtrack = $('#btn_reqtrack'); } // Connect BackEnd and FrontEnd Init connection.start().then(function () { // Connect Sucess GetElement(); // Invoke 調用要求鋼捲追蹤Channel Method _btn_reqtrack.on('click', (e) => { connection.invoke('ReqTrackScan','Req Track Map'); }); }).catch(function (err) { return console.error(err.toString()); }); ``` ### Step3:修改Web Server裡index.html引入的js檔 將index.html裡的trackchat.js引用換成index.js ```= <script src="~/viewjs/index.js"></script> ``` ### Step4:實做TrackingChannel裡ReqTrackScan方法 在TrackingChannel新增ReqTrackScan方法,<font color="#f00">Method名稱要與js檔Invoke調用的名稱一樣!</font> 當Web Server收到Web Client透過Signalr的資料時,會去Akka Manatger調用PlcSndEdit Actor,並將資料轉丟(Tell)過去。 ```= public void ReqTrackScan(string msg) { _akkaManager.GetActor(nameof(PlcSndEdit)).Tell(msg); } ``` ### Step5:實做ExternalSys類別庫裡PlcSndEdit的接收與PlcSnd發送 最後一個步驟需完成Akka Actor資料的轉拋,我們需先在PlcSndEdit註冊接收型別以及設定響應事件。當Receive註冊完響應資料型別後,接著設置一個ProClientMsg的Method。當PlcSndEdit Actor接收到string型別資料後會去使用ProClientMsg方法。處理內容流程單純將資料轉拋給PlcSnd Actor。 - PlcSndEdit ```= Receive<string>(message => ProClientMsg(message)); ``` ```= private void ProClientMsg(string msg) { _log.I("發送Client訊息", msg); _plcSndActor.Tell(msg); } ``` 相對地來說,若PlcSnd要接收PlcSndEdit的string型別資料,一樣須註冊接收string型別,並實做對應響應方法。在此的響應方法流程就是將資料轉拋出去給外部Plc。 - PlcSnd ```= Receive<string>(message => ProSndEditMsg(message)); ``` ```= // 訊息處裡 private void ProSndEditMsg(string msg) { _log.I("發送訊息至PLC", msg); _connection.Tell(Tcp.Write.Create(ByteString.FromString(msg))); } ``` - Step6:Demo ![](https://i.imgur.com/bWuQJWP.png) ## [Sample Code](https://drive.google.com/file/d/1Ikw0h7Tzc2ZOkWOJSjU3uUbp33ynnXMO/view?usp=sharing) ![](https://i.imgur.com/fT9f5n2.png)