Skip to content

Nest中使用Websocket

为什么需要 WebSocket?#

socket#

  1. 客户端连接服务器(TCP / IP),三次握手,建立了连接通道
  2. 客户端和服务器通过socket接口发送消息和接收消息,任何一端在任何时候,都可以向另一端发送任何消息
  3. 有一端断开了,通道销毁

http#

  1. 客户端连接服务器(TCP / IP),三次握手,建立了连接通道
  2. 客户端发送一个http格式的消息(消息头 消息体),服务器响应http格式的消息(消息头 消息体)
  3. 客户端或服务器断开,通道销毁

只能是客户端向服务器发送请求,服务器返回查询结果,HTTP协议做不到服务器主动向客户端推送信息

这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获取就非常麻烦

实时性的问题:

  1. 轮询
  2. 长连接

websocket#

专门用于解决实时传输的问题

  1. 客户端连接服务器(TCP / IP),三次握手,建立了连接通道
  2. 客户端发送一个http格式的消息(特殊格式),服务器也响应一个http格式的消息(特殊格式),称之为http握手
  3. 双发自由通信,通信格式按照websocket的要求进行
  4. 客户端或服务器断开,通道销毁

Nest使用websocket#

创建项目:

Terminal window
nest new nest-websocket -g -p pnpm

默认Nest使用的是Socket.io,我们安装相关包

pnpm add @nestjs/websockets @nestjs/platform-socket.io -S

创建模块:

Terminal window
nest g resource chat --no-spec

注意,我们需要选择websocket

image-20250214174845639

image-20250214174934397

默认会帮我生成xxx.gateway.ts文件,默认代码如下:

import {
WebSocketGateway,
SubscribeMessage,
MessageBody,
} from '@nestjs/websockets'
import { ChatService } from './chat.service'
import { CreateChatDto } from './dto/create-chat.dto'
import { UpdateChatDto } from './dto/update-chat.dto'
@WebSocketGateway()
export class ChatGateway {
constructor(private readonly chatService: ChatService) {}
@SubscribeMessage('createChat')
create(@MessageBody() createChatDto: CreateChatDto) {
return this.chatService.create(createChatDto)
}
@SubscribeMessage('findAllChat')
findAll() {
return this.chatService.findAll()
}
@SubscribeMessage('findOneChat')
findOne(@MessageBody() id: number) {
return this.chatService.findOne(id)
}
@SubscribeMessage('updateChat')
update(@MessageBody() updateChatDto: UpdateChatDto) {
return this.chatService.update(updateChatDto.id, updateChatDto)
}
@SubscribeMessage('removeChat')
remove(@MessageBody() id: number) {
return this.chatService.remove(id)
}
}

Nestjs已经把Socket.io相关的处理做了封装,我们可以简单的使用几个装饰器,就能实现WebSocket的效果

@WebSocketGateWay 声明这是一个处理 weboscket 的类。

@SubscribeMessage 指定处理的消息

@MessageBody 取出传过来的消息内容

我们先不用管自动生成的代码,自己简单测试一下就行,比如我们创建一个下面的方法:

@SubscribeMessage('chatMessage')
handleMessage(@MessageBody() msg: string): string {
console.log('后端接收---', msg);
return msg;
}

相当于指定处理消息是chatMessage,接收客户端传递过来的消息变量是msg

这样后端我们就非常简单的处理完成了。

接下来是前端界面,我们在界面上简单处理,当打开html页面窗口,就创建连接,然后发送消息。当然,这需要客户端通过Socket.io进行连接,我们在工程根目录创建public文件夹,里面创建index.html文件。由于这是一个静态文件,并且放在服务器后端,因此,为了让客户端能够访问到后端静态文件,我们需要在main.ts中处理,这点和express是一样的

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.useStaticAssets('public');
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
<html>
<head>
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
<script>
const socket = io('http://127.0.0.1:3000');
socket.on('connect', function () {
console.log('Connected');
socket.emit('chatMessage', 'hello world', (response) =>
console.log('chatMessage', response),
);
});
socket.on('disconnect', function () {
console.log('Disconnected');
});
</script>
</head>
<body></body>
</html>

从浏览器的连接信息中,就可以看出websocket比http协议请求多出了一些东西

image-20250214162745073

其中:

Terminal window
Upgrade: websocket
Connection: Upgrade

这两个是 WebSocket 的核心,相当于告诉服务器:注意,我发起的请求要用 WebSocket 协议,请帮我找到对应的助理处理,而不是原来的HTTP

sec-websocket-extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: KQlRrXlEM3r9qfET4dnP8g==
Sec-WebSocket-Version: 13

Sec-WebSocket-Extensions是用于协商本次连接要使用的 WebSocket 扩展。

Sec-WebSocket-Key 这个是浏览器随机生成的,并且与服务端响应头部的 Sec-WebSocket-Accept 是配套的,提供基本的防护,这里的“配套”指的是:Sec-WebSocket-Accept 是根据请求头部的 Sec-WebSocket-Key 计算而来

Sec-WebSocket-Version是告诉服务器所使用的 WebSocket Draft (协议版本)

然后服务器会返回下列东西,表示已经接受到请求, 成功建立了WebSocket

image-20250214163529327

其中:

Upgrade: websocket
Connection: Upgrade

就是告诉客户端即将升级的是 WebSocket 协议

Sec-WebSocket-Accept这个则是经过服务器确认,并且加密过后的Sec-WebSocket-Key

至此,HTTP 已经完成它所有工作了,接下来就是完全按照WebSocket协议进行

总结,WebSocket连接的过程是:

首先,客户端发起http请求,经过3次握手后,建立起TCP连接;

http请求里存放WebSocket支持的版本号等信息,如:UpgradeConnectionWebSocket-Version等;

然后,服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据;

最后,客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信。

我们当然也能把其他的方法,直接在客户端进行调用:

<html>
<head>
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
<script>
const socket = io('http://127.0.0.1:3000');
socket.on('connect', function () {
console.log('Connected');
socket.emit('chatMessage', 'hello world', (response) =>
console.log('chartMessage', response),
);
socket.emit('findAllChat', (response) =>
console.log('findAllChat', response),
);
socket.emit('findOneChat', 1, (response) =>
console.log('findOneChat', response),
);
socket.emit('createChat', { name: 'jack' }, (response) =>
console.log('createChat', response),
);
socket.emit('updateChat', { id: 2, name: 'rose' }, (response) =>
console.log('updateChat', response),
);
socket.emit('removeChat', 2, (response) =>
console.log('removeChat', response),
);
});
socket.on('disconnect', function () {
console.log('Disconnected');
});
</script>
</head>
<body></body>
</html>