哼哧哼哧半年,優化改進了一個運維開發web平台。
本文記錄SignalR在react/golang 技術棧的生產小實踐。
一. 背景
有個前后端分離的運維開發web平台, 后端會間隔1分鍾同步一次數據,現在需要將最新一次同步的時間推送到web前端。 說到[web服務端推送],立馬想到SignalR。
- signalr是微軟推出的實時通信標准框架,內部封裝了 websocket、服務端發送事件、長輪詢,一開始雙方發起協商, 確定即將要用的實時傳輸方式(優先websocket)。
- signalr 有兩種通信模型:大部分應用都使用高級的hub模型。
- 持久連接API : 提供低級的連接ID,這里的連接表示用於發送單個接收者、分組或廣播消息的簡單端點。
- hubs: 遠程過程調用, 雙端調用,就像函數就在本地。
- signalR提供了管理實例、連接、失連, 分組管控的API。
本例就是react寫signalr客戶端,golang寫signalr服務端,盲猜有對應的輪子。
二. 擼起袖子干
果然, signalr的作者David Fowler實現了node、go版本, 這位老哥是.NET技術棧如雷貫耳的大牛。
但是他的倉庫很久不更了,某德國大佬在此基礎上fork新的github倉庫。
本例主要你演示 服務端向客戶端推送,最關鍵的一個概念是集線器Hub,其實也就是RPC領域常說的客戶端代理。
服務端在baseUrl
上建立signalr的監聽地址; 客戶端連接並注冊receive
事件;
服務端在適當時候通過hubServer
向HubClients發送數據。
三. go服務端
-
添加golang pgk:
go get github.com/philippseith/signalr -
定義客戶端集線器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方法推送了數據。
- 初始化客戶端集線器, 並在特定地址監聽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")
- 利用
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請求。