SignalR 前后端开发一把梭

1,258 阅读4分钟

哼哧哼哧半年,优化改进了一个运维开发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") )   // 远程过程调用客户端receive方法
}

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 => {  //   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