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

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

- 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類別庫

### Step5:在wwwroot下新增trackchat.js(www)
安裝完Signalr後,接著要在Client實作註冊Signalr監聽,前端若有註冊EventPush Class Method,Web Server則可以將所收到Plc資料廣播出去給前端。

- 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

```=
<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標籤上。

```=
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());
}
```
### 測試

## 情境二、在Web Client按要求TrackintMap,將要求訊息傳送至外部PLC(Web Client -> PLC)
### Step1:至Web Server View的index.html新增一個Button按鈕
第一步當然是至Web Client撰寫一個觸發按鈕,在此我們新增一個要求鋼捲追蹤訊息的Button element。並給予id=btn_reqtrack。

```=
<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一樣)。

```=
// 控制項元素
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

## [Sample Code](https://drive.google.com/file/d/1Ikw0h7Tzc2ZOkWOJSjU3uUbp33ynnXMO/view?usp=sharing)
