likes
comments
collection
share

websocket原理及前后端使用方法

作者站长头像
站长
· 阅读数 59

前言

在项目中有个监听机器人实时位置的需求,通过传统的http协议只能通过定时器来调用获取,当机器人位置不动时,接口还是在一直发送请求,资源浪费验证。后改为WebSocket实时监听位置变动,互相沟通所消耗的请求头变小,大大提升平台性能。由于项目中使用后端是java开发,为了弄清ws的整套流程,便自己用node.js来试着搭了一下,本文后端基于@nestjs相关插件搭建后端服务,具体搭建过程略(可阅读官网nest.nodejs.cn/), 主要讲使用@nestjs搭建ws服务和前端如何使用ws。

WebSocket 原理

先来看看 WebSocket 的请求头示例

websocket原理及前后端使用方法

在 WebSocket 协议中,建立连接时需要进行一次“握手”。这个握手过程与HTTP协议类似,但使用的是 WebSocket 协议的特定头部格式。在客户端发送 WebSocket 请求时,其请求头部包括一个特殊的 "Upgrade" 标识,表示希望升级连接为 WebSocket。同时还需要包含一个 "Sec-WebSocket-Key",它是随机生成的一段字符串,用于在服务器端验证请求的合法性。

当服务器收到这个 WebSocket 请求时,会进行一次握手确认,响应头部包含一个 "Upgrade" 标识,并指明应用的协议是 WebSocket。响应头部还包含一个 "Sec-WebSocket-Accept",这是一个根据客户端请求头部 的 "Sec-WebSocket-Key" 计算的特殊字符串。如果服务器验证通过,那么连接就被升级为 WebSocket 协议。

在建立连接之后,客户端和服务器之间可以进行任意的双向通信。WebSocket 协议支持文本和二进制数据的传输,因此可以在浏览器端实现实时聊天、游戏、文件传输等各种应用。在传输数据时,WebSocket 协议使用了一种特殊的分帧格式,可以确保数据的完整性和顺序性。

WebSocket 协议通过建立长连接,可以实现多种实时应用,它具有低延迟、高并发、易于实现的特点。在实际应用中,WebSocket 协议已经被广泛地应用于在线游戏、即时通讯、股票行情等各个领域。

WebSocket 的其他特点包括:

  1. 建立在 TCP 协议之上,服务器端的实现比较容易。
  2. 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  3. 数据格式比较轻量,性能开销小,通信高效。
  4. 可以发送文本,也可以发送二进制数据。
  5. 没有同源限制,客户端可以与任意服务器通信。
  6. 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

WebSocket 与 HTTP 的区别 相同点: 都是一样基于TCP的,都是可靠性传输协议。都是应用层协议。 联系: WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的。 下面一张图说明了 HTTP 与 WebSocket 的主要区别:

websocket原理及前后端使用方法 不同点:

1、 WebSocket 是双向通信协议,模拟 Socket 协议,可以双向发送或接受信息,而 HTTP 是单向的; 2、 WebSocket 是需要浏览器和服务器握手进行建立连接的,而 http 是浏览器发起向服务器的连接。 3、 虽然 HTTP/2 也具备服务器推送功能,但 HTTP/2 只能推送静态资源,无法推送指定的信息。

接下来我们通过实战来具体实现前后端ws的流程

基于@nestjs搭建ws服务

安装依赖

npm i --save @nestjs/websockets @nestjs/platform-socket.io
npm i ws

新建ws.gateway.ts文件

1、装饰器 @WebSocketGateway()里端口指定为17030

指定了两个方法hello,订阅的消息是 ‘hello'

import {
    ConnectedSocket,  
    MessageBody,
    SubscribeMessage,
    WebSocketGateway,
  } from '@nestjs/websockets';
  import * as WebSocket from 'ws';
  
  @WebSocketGateway(17030)
  export class WsStartGateway {
  
    @SubscribeMessage('hello')  // 监听客户端发出的 join 事件
    hello(@MessageBody() data: string,  // 获取客户端发送的数据
    @ConnectedSocket() client: WebSocket,)  // 获取当前客户端的 WebSocket 连接实例
    : string {
      console.log(data,"hello打印")
      // client.send(JSON.stringify({ event: 'tmp', data: '这里是个临时信息' }));
      return data;
    }
  }

备注:websock和http不要用一个端口号,不然会报错 2、把WsStartGateway放到AppModule的providers里

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { WsStartGateway } from './webSocket/ws.gateway'
@Module({
  imports: [
    ConfigModule.forRoot(),
    MongooseModule.forRoot(process.env.MONGODB_URL),
    WsStartGateway
  ],
  controllers: [AppController],
  providers: [AppService],
})

3、获取WebSocket对象 在WsStarGateway里新增加一个消息订阅方法. 方法里接收 @ConnectedSocket() client: WebSocket,这个client就是与客户端的连接对象(可以用它来给客户端发送消息)。

@SubscribeMessage('hello22')
    hello2(
      @MessageBody() data: string,   // 获取客户端发送的数据
      @ConnectedSocket() client: WebSocket,  // 获取当前客户端的 WebSocket 连接实例
    ): string {
      console.log('收到消息 client:', client);
      client.send(JSON.stringify({ event: 'tmp', data: '这里是个临时信息' }));
      return data;
    }

自定义适配器(WebSocketAdapter

1、新建一个ws.adapter.ts文件,继承WebSocketAdapter

import * as WebSocket from 'ws';
import { WebSocketAdapter, INestApplicationContext } from '@nestjs/common';
import { MessageMappingProperties } from '@nestjs/websockets'; // MessageMappingProperties是NestJS的一个装饰器,用于定义消息映射功能的属性。这个装饰器用于指定客户端可以发送的事件名称。
import { Observable, fromEvent, EMPTY } from 'rxjs'; // rxjs 采用流来处理异步和事件的工具库
import { mergeMap, filter } from 'rxjs/operators';

export class WsAdapter implements WebSocketAdapter {
  constructor(private app: INestApplicationContext) {}

  create(port: number, options: any = {}): any {
    console.log('ws create');
    return new WebSocket.Server({ port, ...options });
  }

  bindClientConnect(server, callback: Function) {
    console.log('ws bindClientConnect, server:\n', server);
    server.on('connection', callback);
  }

  bindMessageHandlers(   // 内部方法,它用于绑定消息处理程序到指定的事件上。
    client: WebSocket,
    handlers: MessageMappingProperties[],
    process: (data: any) => Observable<any>,
  ) {
    console.log('[waAdapter]有新的连接进来');
    fromEvent(client, 'message') // fromEvent是一个RxJS的操作符,它将事件(比如DOM事件)转换成Observable
      .pipe(
        mergeMap((data) =>
          this.bindMessageHandler(client, data, handlers, process),
        ),
        filter((result) => result),
      )
      .subscribe((response) => { 
        // console.log(response,"response")
        // client.send(JSON.stringify(response))
      });
  }

  bindMessageHandler(
    client: WebSocket,
    buffer,
    handlers: MessageMappingProperties[], // 指定客户端可以发送的事件名称
    process: (data: any) => Observable<any>,
  ): Observable<any> {
    let message = null;
    try {
      message = JSON.parse(buffer.data);
      console.log(message,'message');
      client.send(JSON.stringify('我是服务端发送的数据'))
    } catch (error) {
      console.log('ws解析json出错', error);
      return EMPTY;
    }
    const messageHandler = handlers.find((handler) => {
      return handler.message === message.event;
    });
    if (!messageHandler) {
      return EMPTY;
    }
    return process(messageHandler.callback(message.data));
  }

  close(server) {
    console.log('ws server close');
    server.close();
  }
}

在bindMessageHandler方法中,会将传来的json消息解析,然后发送到对应的处理器中。

这里就是发给gateway进行处理。

判断依据是message.event,即event字段。

2、在main.ts里使用这个适配器

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { WsAdapter } from './ws/ws.adapter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useWebSocketAdapter(new WsAdapter(app)); // 使用我们的适配器
  await app.listen(3600);
}
bootstrap();

到此ws后端服务就搭建完成,运行npm run start启动

备注:后端服务安装依赖时,@nestjs对应依赖库需版本匹配,不然会报错

下面是我搭建后端服务对应的package.json

{
  "name": "img-server",
  "version": "0.0.1",
  "description": "",
  "author": "",
  "license": "UNLICENSED",
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:nodemon": "nodemon --config nodemon-debug.json",
    "start:prod": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  },
  "dependencies": {
    "@nestjs/common": "^9.0.0",
    "@nestjs/config": "^3.2.2",
    "@nestjs/core": "^9.0.0",
    "@nestjs/jwt": "^10.2.0",
    "@nestjs/mongoose": "^10.0.6",
    "@nestjs/passport": "^10.0.3",
    "@nestjs/platform-express": "^9.0.0",
    "@nestjs/platform-socket.io": "^9.2.0",
    "@nestjs/swagger": "^7.3.1",
    "@nestjs/websockets": "^9.2.0",
    "axios": "^0.26.0",
    "express": "^4.17.2",
    "ldapjs": "^2.3.1",
    "minio": "7.0.28",
    "moment": "^2.29.4",
    "mongoose": "^6.1.5",
    "passport": "^0.5.2",
    "passport-jwt": "^4.0.0",
    "passport-local": "^1.0.0",
    "swagger-ui-express": "^4.3.0",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.2",
    "rxjs": "^7.2.0"
  },
  "devDependencies": {
    "@nestjs/cli": "^9.0.0",
    "@nestjs/schematics": "^9.0.0",
    "@nestjs/testing": "^9.0.0",
    "@types/express": "^4.17.13",
    "@types/jest": "27.0.2",
    "@types/node": "^16.0.0",
    "@types/passport-jwt": "^3.0.6",
    "@types/supertest": "^2.0.11",
    "@typescript-eslint/eslint-plugin": "^5.0.0",
    "@typescript-eslint/parser": "^5.0.0",
    "eslint": "^8.0.1",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "jest": "^27.2.5",
    "nodemon": "^2.0.15",
    "prettier": "^2.3.2",
    "source-map-support": "^0.5.20",
    "supertest": "^6.1.3",
    "ts-jest": "^27.0.3",
    "ts-loader": "^9.2.3",
    "ts-node": "^10.0.0",
    "tsconfig-paths": "^3.10.1",
    "typescript": "^4.3.5"
  },
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "ts"
    ],
    "rootDir": "src",
    "testRegex": ".*\\.spec\\.ts$",
    "transform": {
      "^.+\\.(t|j)s$": "ts-jest"
    },
    "collectCoverageFrom": [
      "**/*.(t|j)s"
    ],
    "coverageDirectory": "../coverage",
    "testEnvironment": "node"
  }
}

基于vue搭建前端服务

前提已搭建好一个前端服务,接下来我们直接在前端调用ws服务

1、ws连接及初始哈

// ws初始化
const init = () => {
  // ip(端口)
  const pattern = /((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}(:\d{2,4})?/
  
  const url = window.location.href.match(pattern)[0]
  //判断当前浏览器是否支持ws
  if (typeof WebSocket === 'undefined') {
    console.log('您的浏览器不支持socket')
  } else {
    let protocol = window.location.protocol
    let wsValue = 'ws'
    if (protocol.indexOf('https') !== -1) {
      wsValue = 'wss'
    }
    socket && socket.close()
    // 实例化socket
    socket = new WebSocket(`${wsValue}://${url}`)
    // 监听socket连接
    socket.onopen = open
    // 监听socket错误信息
    socket.onerror = error
    // 监听socket消息
    socket.onmessage = getMessage
    socket.onclose = wsClose
  }
}

2、对应监听,发送保活相关方法

onMounted(() => {
  init()
})
let socket = null
// ws初始化
const init = () => {
  // ip(端口) 
  //判断当前浏览器是否支持ws
  if (typeof WebSocket === 'undefined') {
    console.log('您的浏览器不支持socket')
  } else {
  
    socket && socket.close()
    // 实例化socket
    socket = new WebSocket(`ws://127.0.0.1:17030`)
    // 监听socket连接
    socket.onopen = open
    // 监听socket错误信息
    socket.onerror = error
    // 监听socket消息
    socket.onmessage = getMessage
    socket.onclose = wsClose
  }
}
//ws相关方法
const heartbeat = () => {  // 保活心跳
  console.log('socket保活心跳')
  //首次刷新页面数据
  // socket.send(
  //   JSON.stringify({
  //     method: 'heartbeat',
  //     devId: 'socket保活心跳',
  //   }),
  // )
  socket.send(JSON.stringify({
   event: 'hello',
   data: 'socket保活心跳'
  }))
}
const open = () => {
  console.log('socket连接成功')
  //首次刷新页面数据
  socket.send(
    JSON.stringify({
      method: 'selectRobotChange',
      devId: 'socket连接成功',
    }),
  )
  setInterval(() => {
    heartbeat()
  }, 4000);
}
const error = () => {
  console.log('连接错误')
}
const wsClose = () => {
  console.log('连接关闭')
  init()
}
const getMessage = msg => {
  console.log(msg, 'msg')
}

下面是最终运行结果

1、前端控制台打印结果

websocket原理及前后端使用方法

2、后端服务控制台打印结果

websocket原理及前后端使用方法

后端接口集成websocket服务

1、首先我们封装一个ws.module.ts

import { Module } from '@nestjs/common';
import { WsStartGateway } from './ws.gateway'

@Module({
  providers: [WsStartGateway],
  exports:[WsStartGateway]
})
export class wsModule {}

2、在其他接口服务调用

在application.module.ts引入

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UsersController } from './application.controller';
import { applicationService } from './application.service';
import { applicationSchema } from './schemas/application.schema';
import { MinioService } from 'src/minioSchema/minioSchema.service';
import { wsModule } from '../webSocket/ws.module'
@Module({
  imports: [
    wsModule,
    MongooseModule.forFeature([{ name: 'application', schema: applicationSchema }]),
  ],
  controllers: [UsersController],
  providers: [applicationService,MinioService],
  exports: [applicationService,MinioService],
})
export class applicationModule {}

在application.controller.ts调用对应方法

import { WsStartGateway } from '../webSocket/ws.gateway'

export class UsersController {
  constructor(
    private readonly applicationService: applicationService,
    private readonly MinioService: MinioService,
    private readonly response: ResponseService,
    private readonly wsStartGateway: WsStartGateway,
  ) {}

 // 获取用户main应用列表
 @Get('list')
 @ApiQuery({ name: 'pageSize', type: Number})
 @ApiQuery({ name: 'pageNo', type: Number})
 @ApiOperation({ summary: '获取项目列表' })
 async findByUsername(@Request() req: Req) {
   const { pageSize, pageNo } = req.query;
   let row = await this.applicationService.findByPage({
     pageSize: Number(pageSize),
     pageNo: Number(pageNo),
   })
   let total = await this.applicationService.findAll(); 
   console.log(999999)
   let index = 0
   setInterval(()=>{
    this.wsStartGateway.sendMessage(`我是实时传的数据${index}`) // 调用ws中的服务方法
    index = index +1 
   },3000)
   
   return this.response.success('查询成功', { row,total });
 }
}

在ws.gateway.ts服务中我们定义对应的方法

import {
    ConnectedSocket,  
    MessageBody,
    SubscribeMessage,
    WebSocketGateway,
    WebSocketServer
  } from '@nestjs/websockets';
  import * as WebSocket from 'ws';
  let clientD = null
  @WebSocketGateway(17030)
  export class WsStartGateway {
    @SubscribeMessage('currentSocket')  // 监听客户端发出的 join 事件
    currentSocket(@MessageBody() data: string,  // 获取客户端发送的数据
    @ConnectedSocket() client: WebSocket,)  // 获取当前客户端的 WebSocket 连接实例
    : string {
      console.log('连接成功')
      clientD = client
     // client.send(JSON.stringify({ event: 'tmp', data: '这里是个临时信息' }));
      return data;
    }
    @SubscribeMessage('hello')  // 监听客户端发出的 join 事件
    hello(@MessageBody() data: string,  // 获取客户端发送的数据
    @ConnectedSocket() client: WebSocket,)  // 获取当前客户端的 WebSocket 连接实例
    : string {
      // console.log(data,"hello打印")
      // client.send(JSON.stringify({ event: 'tmp', data: '这里是个临时信息' }));
      return data;
    }
  
    @SubscribeMessage('sendMessage')
    sendMessage(
      @MessageBody() data: string   // 获取客户端发送的数据
    ): string {
      
      setTimeout(() => {
        console.log('收到消息 client:',data,clientD.send);
        clientD.send(JSON.stringify({ event: 'tmp', data: data}));
      }, 1000);     
      return data;
    }
  }

备注:这里发送信息给客户端,需要获取当前客户端的 WebSocket 连接实例,我们在最初连接ws服务时,就全局赋值了这个实例clientD,所以在接口中使用方法时,可直接使用

clientD.send()向客户端发送数据

websocket原理及前后端使用方法

对应前端部分初始化时调用方法

const open = () => {
  console.log('socket连接成功')
  //首次刷新页面数据
  socket.send(
    JSON.stringify({
      event: 'currentSocket',
      devId: 'socket连接成功',
    }),
  )
  setInterval(() => {
    heartbeat()
  }, 4000);
}

最后在客户端,我们可以实时监听服务端返回的数据了

websocket原理及前后端使用方法

引用:

nest.nodejs.cn/

cn.rx.js.org/

www.vitejs.net/guide/

转载自:https://juejin.cn/post/7374249487077163017
评论
请登录