---
# System prepended metadata

title: 從零到有Akka整合Dotnet Core Web建置程控系統-Akka透過Web Socket(Signalr)與Web頁面溝通
tags: [Akka, Web, DotNet Core]

---

###### 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)
