SignalR 在React/GO技術棧的生產應用


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

一. 背景

有個前后端分離的運維開發web平台, 后端會間隔1分鍾同步一次數據,現在需要將最新一次同步的時間推送到web前端。 說到[web服務端推送],立馬想到SignalR。


  1. signalr是微軟推出的實時通信標准框架,內部封裝了 websocket、服務端發送事件、長輪詢,一開始雙方發起協商, 確定即將要用的實時傳輸方式(優先websocket)。
  2. signalr 有兩種通信模型:大部分應用都使用高級的hub模型。
  • 持久連接API : 提供低級的連接ID,這里的連接表示用於發送單個接收者、分組或廣播消息的簡單端點。
  • hubs: 遠程過程調用, 雙端調用,就像函數就在本地。
  1. signalR提供了管理實例、連接、失連, 分組管控的API。

本例就是react寫signalr客戶端,golang寫signalr服務端,盲猜有對應的輪子。

二. 擼起袖子干

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

但是他的倉庫很久不更了,某德國大佬在此基礎上fork新的github倉庫

本例主要你演示 服務端向客戶端推送,最關鍵的一個概念是集線器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")
}

// 客戶端能調用的函數,  客戶端會是這樣: connection.invoke('RequestSyncTime',"msg");
func (h *AppHub)  RequestSyncTime(message string) {
	h.Clients().All().Send("receive", time.Now().Format("2006/01/02 15:04:05") )
}

上面定義了RequestSyncTime 方法,可以由客戶端rpc; 同時向客戶端receive方法推送了數據。

  1. 初始化客戶端集線器, 並在特定地址監聽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")
  1. 利用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 => {  //   RequestSyncTime  是服務端定義的函數,客戶端遠程過程調用
                  console.log("Signalr get data first time:",val);
                  this.setState({ message:val })
                })
              }
        }) ;

        connection.on('receive', res => {                   // 客戶端注冊的receive 函數
          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) 客戶端與服務器發起post協商請求
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。

wss://api.rosenbridge.qa.xxxx.com/realtime?id=hkSNQT-pGpZ9E6tuMY9rRw==

服務端的h.Clients().All().Send("receive", time.Now().Format("2006/01/02 15:04:05") )產生了如下的傳輸數據:

{“type”:1,“target”:“receive”, "arguments":[2021/10/18 11:13:28]}

websocket 請求是基於http Get請求握手后,要求升級協議達到的長連接,數據傳遞在[消息]標簽頁, 我們整體看到是ws:get請求。

Github Demo


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM