SignalR 在 React/Go 技術(shù)棧的實踐
本文轉(zhuǎn)載自微信公眾號「精益碼農(nóng)」,作者有態(tài)度的馬甲 。轉(zhuǎn)載本文請聯(lián)系精益碼農(nóng)公眾號。
一.背景
有個前后端分離的運維開發(fā)web平臺, 后端會間隔5分鐘同步一次數(shù)據(jù),現(xiàn)在需要將最新一次同步的時間推送到web前端。
說到[web服務(wù)端推送],立馬想到SignalR,(我頭腦中一直有技術(shù)體系, 但一直沒實踐過。)
SignalR是微軟推出的實時通信標(biāo)準(zhǔn)框架,內(nèi)部封裝了 websocket、服務(wù)端發(fā)送事件、長輪詢, 可以算是實時通信的大殺器,傳送門。
實際編碼就是react寫SignalR客戶端,golang寫SignalR服務(wù)端,盲猜有對應(yīng)的輪子。
二.擼起袖子干
果然, signalr的作者David Fowler實現(xiàn)了node、go版本, 這位老哥是.NET技術(shù)棧如雷貫耳的大牛:
但是他的倉庫很久不更了,某德國大佬在此基礎(chǔ)上開了新github倉庫[1]繼續(xù)支持。
SignalR的基本交互原理:
(1) signalR提供了一組API, 用于創(chuàng)建從服務(wù)端到客戶端的遠(yuǎn)程過程調(diào)用(RPC),這個調(diào)用的具體體現(xiàn)是 :從服務(wù)端.NET 代碼調(diào)用位于客戶端的javascript 代碼。
(2) signalr提供了管理實例、連接、失連, 分組管控的API。
這里面最關(guān)鍵的一個概念是集線器Hub,其實也就是RPC領(lǐng)域常說的客戶端代理。
服務(wù)端在baseUrl上建立signalr的監(jiān)聽地址;
客戶端連接并注冊receive事件;
服務(wù)端在適當(dāng)時候通過hubServer向HubClients發(fā)送數(shù)據(jù)。
go服務(wù)端
(1) 添加golang pgk:go get github.com/philippseith/signalr
(2) 定義客戶端集線器hub,這里要實現(xiàn)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")
- }
- // 客戶端調(diào)用的函數(shù), 本例不用
- func (h *AppHub) Send(message string) {
- h.Clients().All().Send("receive", time.Now().Format("2006/01/02 15:04:05") )
- }
(3) 初始化集線器, 并在特定地址監(jiān)聽signalr請求。
這個庫將signalr監(jiān)聽服務(wù)抽象為獨立的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在合適業(yè)務(wù)代碼位置向web客戶端推送數(shù)據(jù)。
- 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客戶端需要監(jiān)聽的JavaScript事件名。
react客戶端
前端菜雞,跟著官方示例琢磨了好幾天。
(1) 添加@microsoft/signalr 包
(2) 在組件掛載事件componentDidMount初始化signalr連接
實際也就是向服務(wù)端baseUrl建立HubConnection,注冊receive事件,等待服務(wù)端推送。
- 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客戶端與服務(wù)器協(xié)商 傳輸方式http://localhost:9598/realtime/negotiate?negotiateVersion=1,
返回可用的傳輸方式和連接標(biāo)識ConnectionId。
- {
- "connectionId": "hkSNQT-pGpZ9E6tuMY9rRw==",
- "availableTransports": [{
- "transport": "WebSockets",
- "transferFormats": ["Text", "Binary"]
- }, {
- "transport": "ServerSentEvents",
- "transferFormats": ["Text"]
- }]
- }
(2) web客戶端利用上面的ConnectionId向特定的服務(wù)器地址/realtime連接,建立傳輸通道,默認(rèn)優(yōu)先websocket。
以上網(wǎng)絡(luò)交互,大部分會通過SignalR框架自動完成。
源碼:Github Demo[2]
引用鏈接
[1] Github倉庫: https://github.com/philippseith/signalr
[2] Github Demo: https://github.com/zaozaoniao/SignalR-apply-to-react-and-golang