Azure Function SignalR连接Angular失败及Token过期刷新视图问题
问题:SignalR WebSocket连接失败及Token过期自动刷新需求
我在使用@aspnet/signalr-client时遇到WebSocket连接失败问题,刚接触相关技术,不清楚错误来源,错误信息如下:
SignalR connection error: Error: WebSocket failed to connect comes from. The connection could not be found on the server, either the endpoint may not be a SignalR endpoint, the connection ID is not present on the server, or there is a proxy blocking WebSockets. If you have multiple servers check that sticky sessions are enabled.
同时我希望实现SignalR Token过期时自动刷新Angular视图,以下是我的相关配置:
Negotiate函数的function.json
{ "disabled": false, "bindings": [ { "authLevel": "anonymous", "type": "httpTrigger", "direction": "in", "name": "req", "methods": ["get"] }, { "type": "http", "direction": "out", "name": "res" }, { "type": "signalRConnectionInfo", "name": "connectionInfo", "userId": "", "hubName": "measurements", "direction": "in" } ] }
Negotiate的index.js
module.exports = async function (context, req, connectionInfo) { context.res.body = connectionInfo; };
local.settings.json
{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "DefaultEndpointsProtocol=https;AccountName=mcdashboardfunctionsprd;AccountKey=????", "AzureWebJobsCosmosDBConnectionString": "AccountEndpoint=https://prd-cosmos-db-account.documents.azure.com:443/;AccountKey=???", "AzureSignalRConnectionString": "Endpoint=https://signalr-dashboard-prd.service.signalr.net;AccessKey=?????;Version=1.0;", "FUNCTIONS_WORKER_RUNTIME": "node" }, "Host": { "LocalHttpPort": 7071, "CORS": "*" } }
注:已将密钥替换为??
Angular环境变量配置
// The list of file replacements can be found in `angular.json`. export const environment = { authentication: { stsServerUrl: 'https://dev-sf-ztc2f.us.auth0.com', clientId: 'GoPIRYXScZWIMLxqfVuj2p4OwhSabh0l', }, bff: { serverUrl: 'http://localhost:7071', basePath: '/api', }, googleAnalyticsTagId: `G-TLMFNVQ6XY`, instrumentationKey: '', }; /* * For easier debugging in development mode, you can import the following file * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. * * This import should be commented out in production mode because it will have a negative impact * on performance if an error is thrown. */ // import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
Angular端SignalR服务(signalr.service.ts)
import * as signalR from '@microsoft/signalr'; import { BehaviorSubject, Observable, from, of } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { delay, map, retryWhen, switchMap, take } from 'rxjs/operators'; import { environment } from '@env/environment'; @Injectable() export class SignalRService { private hubConnection: signalR.HubConnection; private connectionAttempts = 0; private negociateUrl!: string; connection!: signalR.HubConnection; hubMessage: BehaviorSubject<string>; constructor(private http: HttpClient) { this.negociateUrl = `${environment.bff.serverUrl}${environment.bff.basePath}/negociate`; this.hubMessage = new BehaviorSubject<string>(null); } private setSignalrClientMethods(): void { this.connection.on('DisplayMessage', (message: string) => { this.hubMessage.next(message); }); } public async initiateSignalrConnection(): Promise<void> { try { this.connection = this.hubConnectionBuilder( this.negociateUrl, { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets, } ); await this.connection.start(); this.setSignalrClientMethods(); console.log( `SignalR connection success! connectionId: ${this.connection.connectionId}` ); } catch (error) { console.log(`SignalR connection error: ${error}`); } } private hubConnectionBuilder(url: string, options: any = null) { return new signalR.HubConnectionBuilder() .withUrl(url, options) .withAutomaticReconnect({ nextRetryDelayInMilliseconds: (retryContext) => { if (retryContext.elapsedMilliseconds < 6000) { // If we've been reconnecting for less than 60 seconds so far, // wait between 0 and 10 seconds before the next reconnect attempt. return Math.random() * 1000; } else { // If we've been reconnecting for more than 60 seconds so far, stop reconnecting. return null; } }, }) .configureLogging( environment.production ? signalR.LogLevel.None : signalR.LogLevel.Debug ) .build(); } public hubReconnection() { this.connection = this.hubConnectionBuilder(this.negociateUrl); if (this.connection.state === signalR.HubConnectionState.Disconnected) { console.log(`Connection lost due to error. Reconnecting.....`); this.connection.onreconnecting((error: any) => { console.log(`Connection lost due to error "${error}". Reconnecting.`); }); this.connection.onreconnected((connectionId: any) => { console.log( `Connection reestablished. Connected with connectionId "${connectionId}".` ); }); } else { console.log(this.connection.state); } } public getHubConnection = (): Observable<any> => { this.connectionAttempts++; if ( (this.hubConnection !== undefined && this.isConnected()) || this.connectionAttempts > 1 ) { return of(true); } // TODO save token in local storage and handle token expiration // const accessToken = localStorage.getItem('socketsToken') // const url = localStorage.getItem('socketsUrl') // if(accessToken && url && !this.isConnected()) { // console.log('Using local storage') // return this.startConnection(url, accessToken) // } return this.getAccessToken(); }; public isConnected() { if (this.hubConnection) console.log('state', this.hubConnection.state); return this.hubConnection && this.hubConnection.state !== 'Disconnected'; } public getAccessToken() { if (this.isConnected()) { return of(true); } return this.http .post( `${environment.bff.serverUrl}${environment.bff.basePath}/negociate`, {} ) .pipe( map((resp) => { return this.startConnection(resp['url'], resp['accessToken']); }) ); } public startConnection(url, accessToken) { const options = { accessTokenFactory: () => accessToken, }; this.hubConnection = this.hubConnectionBuilder(url, options); this.hubConnection.serverTimeoutInMilliseconds = 1000 * 60 * 10; this.hubConnection.keepAliveIntervalInMilliseconds = 1000 * 60 * 10; return from(this.hubConnection.start()); } public getRealtimeData(eventName): Observable<any> { return this.getHubConnection().pipe( switchMap(() => { return this.onEvent(eventName).pipe( retryWhen((errors) => errors.pipe(delay(1000), take(10))) ); }) ); } public onEvent(eventName): Observable<any> { const that = this; return new Observable((observer) => { this.hubConnection.on(eventName, (data) => { return observer.next({ name: data.process_time, value: data.sensor_value, }); }); // Function executed when the unsubscribe is called return function () { if (that.hubConnection) that.hubConnection.off(eventName); }; }); } public closeConnection() { this.hubConnection.stop().then(() => { console.log('connection closed'); }); } }
Home组件(home.component.ts)
import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import { Observable, tap } from 'rxjs'; import { SignalRService } from '@app/core/signalr/signalr.service'; import { ClockService } from './clock.service'; @Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'], encapsulation: ViewEncapsulation.None, }) export class HomeComponent implements OnInit { dayIcon = 'assets/images/icon-day.svg'; nightIcon = 'assets/images/icon-night.svg'; themeIcon = this.dayIcon; clock: Observable<Date>; measurementCollection = 'real'; hubMessage: string; constructor( private clockService: ClockService, private signalrService: SignalRService ) { this.hubMessage = ''; } ngOnInit(): void { this.setClock(this.measurementCollection); this.signalrService.getHubConnection().pipe( tap((value: any) => console.log(value)) ) this.signalrService .initiateSignalrConnection() .then((reason: any) => { console.log(reason); }) .catch((err) => { console.error(err); }); this.signalrService.hubReconnection() } toggleTheme() { const bodyEl = document.getElementsByTagName('body')[0]; bodyEl.classList.toggle('light-theme'); this.themeIcon = this.themeIcon === this.dayIcon ? this.nightIcon : this.dayIcon; } setClock(collection) { this.clock = this.clockService.getClock(collection); } toggleSimulated() { this.measurementCollection = this.measurementCollection === 'real' ? 'simulated' : 'real'; this.setClock(this.measurementCollection); } }
启动截图
我已被这个问题困扰两周,希望能找到解决方案。
内容的提问来源于stack exchange,提问作者Serge
相关产品推荐
相关产品推荐




