websocket原理及前后端使用方法
前言
在项目中有个监听机器人实时位置的需求,通过传统的http协议只能通过定时器来调用获取,当机器人位置不动时,接口还是在一直发送请求,资源浪费验证。后改为WebSocket实时监听位置变动,互相沟通所消耗的请求头变小,大大提升平台性能。由于项目中使用后端是java开发,为了弄清ws的整套流程,便自己用node.js来试着搭了一下,本文后端基于@nestjs相关插件搭建后端服务,具体搭建过程略(可阅读官网nest.nodejs.cn/), 主要讲使用@nestjs搭建ws服务和前端如何使用ws。
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 的其他特点包括:
- 建立在 TCP 协议之上,服务器端的实现比较容易。
- 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
- 数据格式比较轻量,性能开销小,通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
- 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
WebSocket 与 HTTP 的区别 相同点: 都是一样基于TCP的,都是可靠性传输协议。都是应用层协议。 联系: WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的。 下面一张图说明了 HTTP 与 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、前端控制台打印结果
2、后端服务控制台打印结果
后端接口集成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()向客户端发送数据
对应前端部分初始化时调用方法
const open = () => {
console.log('socket连接成功')
//首次刷新页面数据
socket.send(
JSON.stringify({
event: 'currentSocket',
devId: 'socket连接成功',
}),
)
setInterval(() => {
heartbeat()
}, 4000);
}
最后在客户端,我们可以实时监听服务端返回的数据了
引用:
转载自:https://juejin.cn/post/7374249487077163017