angular + express 實現websocket通訊

耳機大神發表於2023-09-21

最近需要實現一個功能,後端透過TCP協議連線雷達硬體的控制器,前端透過websocket連線後端,當控制器觸發訊息的時候,把資訊通知給所以前端;

第一個思路是單獨寫一個後端服務用來實現websocket,除錯成功了,後來又發現一個外掛express-ws,於是決定改變思路,研究了下,最終程式碼如下,希望幫助更多的朋友,不再害怕websocket

 首先寫一個前端websocket服務。這裡我選擇放棄單例模式,採用誰呼叫誰負責銷燬的思路

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { LoginService } from '../login/login.service';
import { environment } from 'src/environments/environment';

export class WsConnect {
  ws!:WebSocket;
  sendWs!:(msg:string)=>void;
  closeWs!:()=>void;
  result!:Observable<any>
}
@Injectable({providedIn:"root"})
export class WebsocketService {

  origin = window.location.origin.replace('http', 'ws');

  constructor(
    private loginService: LoginService
  ) { }

  getUrl(path:string){
    return `${this.origin}${path}`;
  }
  connect(path:string):WsConnect{
    let url = this.getUrl(path);
    let ws = new WebSocket(url, this.loginService.userInfo.jwt);  // 在這裡放入jwt資訊,目前沒有找到其它地方可以放。有些網友建議先放入地址,然後在nginx裡重新放入header,我覺得不夠接地氣
    return {
      ws,
      sendWs:function(message:string){
        ws.send(message);
      },
      closeWs:function(){
        ws.close();
      },
      result:new Observable(
        observer => {
            ws.onmessage = (event) => { observer.next(event.data)};//接收資料
            ws.onerror =  (event) => {console.log("ws連線錯誤:",event);observer.error(event)};//發生錯誤
            ws.onclose =  (event) => {console.log("ws連線斷開:",event); observer.complete() };//結束事件
            ws.onopen =  (event) => { console.log("ws連線成功:",event);};//結束事件
        }
      )
    }
  }
}

然後在元件裡呼叫

import { Component, OnDestroy, OnInit } from '@angular/core';

import { WebsocketService, WsConnect } from '../utils/websocket-client.service'; @Component({ selector:
'app-car-measure', templateUrl: './car-measure.component.html', styleUrls: ['./car-measure.component.scss'] }) export class CarMeasureComponent implements OnInit , OnDestroy{ connect!:WsConnect; constructor(public wsService:WebsocketService) { } ngOnInit() { this.connectServer(); } connectServer(){ this.connect = this.wsService.connect('/websocket/carMeasure') this.connect.result.subscribe( (data:any) => { //接收到服務端發來的訊息 console.log("伺服器訊息:",data); setTimeout(() => { this.connect.sendWs("這是從客戶端發出的訊息"); }, 5000); } ) } ngOnDestroy() { this.connect.closeWs(); // 這個方法時把整個ws銷燬,而不是取消訂閱哦,所以有需要的同學可以考慮取消訂閱的方案 } }

後端引入express-ws,封裝一個可呼叫的檔案,部分程式碼借鑑了網上的程式碼,做了一些改善

//websocket.js
const express = require('express');
const router = express.Router();
const expressWs = require('express-ws')
// 初始化
let WS = null;
// 宣告一個通道類
let channels = null;
let pathList = [
    '/websocket/carMeasure',
    '/path2'
]
function initWebSocket(app) {
    WS = expressWs(app) //混入app, wsServer 儲存所有已連線例項
    // 建立通道
    channels = new channel(router)
    pathList.forEach(path=>{
        channels.createChannel(path)
        // channels.createChannel('/carMeasure/websocket/carSize')
    })
    app.use(router)
}
// 通道類
class channel {
    router;
    constructor(props) {
        this.router = props;
    }
    createChannel(path) {
        // 建立通道
        this.router.ws( path, (ws, req) => {
            //把自定義資訊加入到socket裡面取,expressws會自動放入到從WS.getWss().clients,
            // 並且會自動根據活動使用者刪除或者增加客戶端
            ws['wsPath'] = path;
            ws['userId'] = req.userInfo._id;
            ws['roleId'] = req.userInfo.role;
            ws.on('message', (msg) => getMsg(msg, path))
            ws.on('close', (code) => close(code, path))
            ws.on('error', (e) => error(e, path))
        })
    }
}
/**
 * 
 * @param {*} msg 訊息內容
 * @param {String} from 訊息來源
 */
// 監聽訊息
let getMsg = (msg, from) => {
    console.log(msg, from);
    // SendMsgAll({path:'/path2', data: msg })
}
// 傳送訊息
let sendMsg = (client, data) => {
    if (!client) return
    client.send(JSON.stringify(data))
}
let close = (code) => {
    console.log('關閉連線', code);
}
let error = (e) => {
    console.log('error: ', e);
}
// 群發
/**
 * 
 * @param {String} path 需要傳送的使用者來源 路由,預設全部
 * @param {*} data 傳送的資料
 */
function sendMsgToClients(clients,data){
    clients.forEach((client)=> {
        if (client._readyState == 1) {
            sendMsg(client, data)
        }
    })
}

function sendMsgToAll(data = "") {
    let allClientsList = Array.from(WS.getWss().clients)
    sendMsgToClients(allClientsList,data)
}

function sendMsgToPath(data = "", path = '') {
    let allClientsList = Array.from(WS.getWss().clients).filter((ws)=>ws['wsPath'] == path)
    sendMsgToClients(allClientsList,data)
}
function sendMsgToId(data = "", userId = '') {
    let allClientsList = Array.from(WS.getWss().clients).filter((ws)=>ws['userId'] == userId)
    sendMsgToClients(allClientsList,data)
}
function sendMsgToRole(data = "", roleId = '') {
    let allClientsList = Array.from(WS.getWss().clients).filter((ws)=>ws['roleId'] == roleId)
    sendMsgToClients(allClientsList,data)
}
module.exports = {
    initWebSocket,
    sendMsgToAll,
    sendMsgToPath,
    sendMsgToId,
    sendMsgToRole,
}

然後再app.js裡面呼叫就可以了

const {initWebSocket} = require('./public/utils/websocket')
initWebSocket(app)

其中涉及到了許可權驗證的問題,也可以直接驗證jwt

app.use((req,res,next) => {
  if(!whiteList.some(item =>  req.url.startsWith(item))) {
      let httpJwt= req.headers['jwt'];
      let wsJwt= req.headers['sec-websocket-protocol'];  // 這裡驗證websocket的身份資訊,其它程式碼
      utils.verifyToken(httpJwt || wsJwt).then(res => {   //utils.verifyToken封裝了jwt的驗證
          req["userInfo"] = res;                        //放入一些資訊,方便後續操作
          next()
      }).catch(e => {
          console.error(e);
          res.status(401).send('invalid token')
      })
  } else {
      next()
  }
})

萬事具備,最後一步就是等待硬體裝置的觸發了,其它tcp客戶端的程式碼就不放出來干擾大家了,就是粗暴的呼叫即可

var {sendMsgToPath} = require('../public/utils/websocket');
sendMsgToPath(JSON.stringify(result), this.carMeasurePath);   // 注意websocket或者tcp的傳輸都只能用字串或者blob

 

另外注意要配置nginx代理,nginx的配置各位應該都清楚吧,這裡就不多說了,注意的是這裡有幾個可選擇的地方,一個是前端,可以把ws服務做成單例,另一個是後端路由其實可以寫在http的路由檔案裡,還有一個是對後端ws client的使用,利用了express-ws自身的方法,當然也可以自己寫物件來蒐集clients (不太建議)

想了以下還是放出來給小白,這裡是proxy.config.json

{
    "/api": {
        "target": "http://localhost:3000",
        "secure": false,
        "logLevel": "debug",
        "changeOrigin": true,
        "pathRewrite": {
            "^/api": "/"
        }
    },
    "/websocket":{
      "target": "http://localhost:3000",
      "secure": false,
      "ws": true
    }
}

畢竟講究的是手把手把你教會,不會也得會,這裡是放入伺服器的nginx.cong

worker_processes  1;

events {
    worker_connections  1024;
}

http {
    server {
        listen 80;
        server_name  localhost;
        client_max_body_size 20M;
        underscores_in_headers on;

        include /etc/nginx/mime.types;


        gzip on;
        gzip_static on;
        gzip_min_length 1000;
        gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;

        location  /{
            root  /usr/share/nginx/html;
            index  index.html index.htm;
            try_files $uri $uri/ /index.html;
        }

        location /api {
            rewrite  ^/api/(.*)$ /$1 break;
            proxy_pass http://localhost:3000;

        }
         location /websocket {
            proxy_set_header   X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header   Host      $http_host;
            proxy_set_header X-NginX-Proxy true;
            proxy_pass http://localhost:3000;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
        }
    }
}

 

最後,祝大家工作順利,請記住 ‘耳機大神’ 永遠陪著你

 

相關文章