SignalR 在 React-Go 技術棧的實踐

哼哧哼哧半年,優化改進了一個運維開發 web 平臺。
本文記錄 SignalR 在 react/golang 技術棧的生產小實踐。

有個前後端分離的運維開發 web 平臺, 後端會間隔 5 分鐘同步一次數據,現在需要將最新一次同步的時間推送到 web 前端。

說到 [web 服務端推送],立馬想到 SignalR,(我頭腦中一直有技術體系, 但一直沒實踐過。)

SignalR 是微軟推出的實時通信標準框架,內部封裝了 websocket、服務端發送事件、長輪詢, 可以算是實時通信的大殺器,傳送門。

實際編碼就是 react 寫 SignalR 客戶端,golang 寫 SignalR 服務端,盲猜有對應的輪子。

擼起袖子幹

果然, signalr 的作者 David Fowler 實現了 node、go 版本, 這位老哥是. NET 技術棧如雷貫耳的大牛:

但是他的倉庫很久不更了,某德國大佬在此基礎上開了新 github 倉庫 [1] 繼續支持。

SignalR 的基本交互原理:

(1) signalR 提供了一組 API, 用於創建從服務端到客戶端的遠程過程調用 (RPC),這個調用的具體體現是 :從服務端. NET 代碼調用位於客戶端的 javascript 代碼。

(2) signalr 提供了管理實例、連接、失連, 分組管控的 API。

這裏面最關鍵的一個概念是集線器 Hub,其實也就是 RPC 領域常說的客戶端代理。
服務端在baseUrl上建立 signalr 的監聽地址;
客戶端連接並註冊receive事件;

服務端在適當時候通過hubServer向 HubClients 發送數據。

go 服務端

(1) 添加 golang pgk:go get github.com/philippseith/signalr

(2) 定義客戶端集線器 hub,這裏要實現HubInterface接口的幾個方法, 你還可以爲集線器添加一些自定義方法。

package services

import (
 "github.com/philippseith/signalr"
 log "github.com/sirupsen/logrus"
 "time"
)

type AppHub struct{
  signalr.Hub
}

func (h *AppHub) OnConnected(connectionID string) {
 // fmt.Printf("%s connected\n", connectionID)
 log.Infoln(connectionID," connected\n" )
}

func (h *AppHub) OnDisconnected(connectionID string) {
 log.Infoln(connectionID," disconnected\n")
}

// 客戶端調用的函數, 本例不用
func (h *AppHub) Send(message string) {
 h.Clients().All().Send("receive", time.Now().Format("2006/01/02 15:04:05") )
}

(3) 初始化集線器, 並在特定地址監聽 signalr 請求。

這個庫將 signalr 監聽服務抽象爲獨立的 hubServer

shub := services.AppHub{}

sHubSrv,err:= signalr.NewServer(context.TODO(),
  signalr.UseHub(&shub), // 這是單例hub
  signalr.KeepAliveInterval(2*time.Second),
  signalr.Logger(kitlog.NewLogfmtLogger(os.Stderr)true))
 sHubSrv.MapHTTP(mux, "/realtime")

(4) 利用sHubServer在合適業務代碼位置向 web 客戶端推送數據。

if clis:= s.sHubServer.HubClients(); clis!= nil {
    c:= clis.All()
    if  c!= nil {
     c.Send("receive",ts.Format("2006/01/02 15:04:05"))
    }
   }

注意:上面的receive方法是後面 react 客戶端需要監聽的 JavaScript 事件名。

react 客戶端

前端菜雞,跟着官方示例琢磨了好幾天。

(1) 添加 @microsoft/signalr 包

(2) 在組件掛載事件componentDidMount初始化 signalr 連接

實際也就是向服務端baseUrl建立HubConnection,註冊receive事件,等待服務端推送。

import React from 'react';
import {
  JsonHubProtocol,
  HubConnectionState,
  HubConnectionBuilder,
  HttpTransportType,
  LogLevel,
} from '@microsoft/signalr';

class Clock extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        message:'',
        hubConnection: null,
      };
    }
  
    componentDidMount() {
      const connection = new HubConnectionBuilder()
        .withUrl(process.env.REACT_APP_APIBASEURL+"realtime"{
        })
        .withAutomaticReconnect()
        .withHubProtocol(new JsonHubProtocol())
        .configureLogging(LogLevel.Information)
        .build();
 
    // Note: to keep the connection open the serverTimeout should be
    // larger than the KeepAlive value that is set on the server
    // keepAliveIntervalInMilliseconds default is 15000 and we are using default
    // serverTimeoutInMilliseconds default is 30000 and we are using 60000 set below
        connection.serverTimeoutInMilliseconds = 60000;
 
    // re-establish the connection if connection dropped
        connection.onclose(error ={
            console.assert(connection.state === HubConnectionState.Disconnected);
            console.log('Connection closed due to error. Try refreshing this page to restart the connection', error);
        });
    
        connection.onreconnecting(error ={
            console.assert(connection.state === HubConnectionState.Reconnecting);
            console.log('Connection lost due to error. Reconnecting.', error);
        });
    
        connection.onreconnected(connectionId ={
            console.assert(connection.state === HubConnectionState.Connected);
            console.log('Connection reestablished. Connected with connectionId', connectionId);
        });
        
        this.setState({ hubConnection: connection})

        this.startSignalRConnection(connection).then(()={
              if(connection.state === HubConnectionState.Connected) {
                connection.invoke('RequestSyncTime').then(val ={
                  console.log("Signalr get data first time:",val);
                  this.setState({ message:val })
                })
              }
        }) ;

        connection.on('receive'res ={
          console.log("SignalR get hot res:", res)
            this.setState({
              message:res
            });
        });
    }
  
    startSignalRConnection = async connection ={
      try {
          await connection.start();
          console.assert(connection.state === HubConnectionState.Connected);
          console.log('SignalR connection established');
      } catch (err) {
          console.assert(connection.state === HubConnectionState.Disconnected);
          console.error('SignalR Connection Error: ', err);
          setTimeout(() => this.startSignalRConnection(connection), 5000);
      }
    };
  
    render() {
      return (
        <div style={{width: '300px',float:'left',marginLeft:'10px'}} >
          <h4>最新同步完成時間: {this.state.message}  </h4>
        </div>
      );
    }
  }

export  default  Clock;

(3) 將該 react 組件插入到 web 前端頁面

效果分析

最後的效果如圖:

效果分析:

(1) web 客戶端與服務器協商 傳輸方式http://localhost:9598/realtime/negotiate?negotiateVersion=1,
返回可用的傳輸方式和連接標識ConnectionId

{
    "connectionId""hkSNQT-pGpZ9E6tuMY9rRw==",
    "availableTransports"[{
        "transport""WebSockets",
        "transferFormats"["Text""Binary"]
    }{
        "transport""ServerSentEvents",
        "transferFormats"["Text"]
    }]
}

(2) web 客戶端利用上面的ConnectionId向特定的服務器地址/realtime連接,建立傳輸通道,默認優先 websocket。

以上網絡交互,大部分會通過 SignalR 框架自動完成。

源碼:Github Demo[2]

引用鏈接

[1] Github 倉庫: https://github.com/philippseith/signalr
[2] Github Demo: https://github.com/zaozaoniao/SignalR-apply-to-react-and-golang

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/O8ysCDsIG8mjarv_A31BIw