HTTP
协议的通信只能由客户端发起,要实现实时双向数据通信,一般情况下会使用WebSocket
。websoket
是一种 client
与 service
保存长链接通讯的一种技术。在nestjs
中有两个现成支持的 WebSocket
平台:socket.io
和 ws
,它们都是基于websocket封装的框架。nestjs
默认支持socket.io
,而如果使用ws
则需要切换适配器。
socket.io
在websocket的基础上集成了丰富强大的功能,优化了我们构建复杂应用的过程。
下面我们使用nestjs
和react
基于socket.io
来一起开发WebSocket
应用。
初始化nestjs项目
全局安装nestjs脚手架@nestjs/cli
npm install -g @nestjs/cli
创建一个nest项目
nest new nest_websocket
安装依赖
pnpm i @nestjs/websockets @nestjs/platform-socket.io socket.io
开发
创建个 websocket 模块,新建两个文件
// web-socket.gateway.ts 文件
import {
WebSocketGateway,
SubscribeMessage,
MessageBody,
} from '@nestjs/websockets';
@WebSocketGateway()
export class WSGateway {
constructor() {}
@SubscribeMessage('sendMessage')
sendMessage(@MessageBody() data: { message: string }) {
return data;
}
}
Nest
中的网关是一个用 @WebSocketGateway()
装饰器注释的类。
创建网关后,在模块中注册它。
// web-socket.module.ts 文件
import { Module } from '@nestjs/common';
import { WSGateway } from './web-socket.gateway';
@Module({
providers: [WSGateway],
})
export class WebSocketModule {}
在app.module
中引入
// app.module.ts 文件
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { WebSocketModule } from './web-socket/web-socket.module';
@Module({
imports: [WebSocketModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
// main.ts 文件
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);
await app.listen(3000);
}
bootstrap();
客户端
然后用react写一下客户端代码
安装依赖
socket.io-client
是socket.i
专为前端设计的库
pnpm i socket.io-client
书写组件
import io from "socket.io-client";
const socket = io("ws://localhost:3000");
socket.on("connect", function () {
console.log("连接成功");
socket.emit(
"sendMessage",
{
message: "Hello world",
},
(data) => {
console.log(data);
}
);
});
socket.on("disconnect", function () {
console.log("断开连接");
});
export default function Demo() {
return <></>;
}
前后端联调
把服务跑起来
pnpm start
前端项目也跑起来
pnpm dev
浏览器访问前端地址 http ://localhost:5173
这时候发现出现跨域问题,怎么解决呢
@WebSocketGateway()
可以通过传参将受支持的配置选项传递给装饰器
配置一下跨域问题,如下代码
// web-socket.gateway.ts 文件
import {
WebSocketGateway,
SubscribeMessage,
MessageBody,
} from '@nestjs/websockets';
@WebSocketGateway({
cors: {
origin: '*',
}, // 配置一下跨域问题
})
export class WSGateway {
constructor() {}
@SubscribeMessage('sendMessage')
sendMessage(@MessageBody() data: { message: string }) {
return data;
}
}
结果如下,连接成功,
再捋一下流程
nestJs
中通过@SubscribeMessage()
装饰器订阅传入消息,如下方代码,它将订阅“sendMessage”消息,并收到的数据返回。
@SubscribeMessage('sendMessage')
sendMessage(@MessageBody() data: { message: string }) {
return data;
}
客户端中, 通过订阅connect
,在连接成功后,客户端触发“sendMessage”消息,发送数据,然后获取到服务端返回的消息并打印
import io from "socket.io-client";
const socket = io("ws://localhost:3000");
socket.on("connect", function () {
console.log("连接成功");
socket.emit(
"sendMessage",
{
message: "Hello world",
},
(data) => {
console.log(data);
}
);
});
socket.on("disconnect", function () {
console.log("断开连接");
});
一般来说,每个网关都会监听与 HTTP 服务器相同的端口,也可以手动修改端口,通过将参数传递给 @WebSocketGateway(80)
装饰器来修改此默认行为,其中 80
是选定的端口号。
例如我们可以修改一下端口号(如下图改成3001),这时候前端也需要修改为同样的端口地址。
import {
WebSocketGateway,
SubscribeMessage,
MessageBody,
} from '@nestjs/websockets';
@WebSocketGateway(3001, { // 修改端口号为3001
cors: {
origin: '*',
}, // 配置一下跨域问题
})
export class WSGateway {
constructor() {}
@SubscribeMessage('sendMessage')
sendMessage(@MessageBody() data: { message: string }) {
return data;
}
}
上面代码中,服务端订阅了“sendMessage”消息,并在接收到消息后返回数据。
@SubscribeMessage('sendMessage')
sendMessage(@MessageBody() data: { message: string }) {
return data;
}
上面代码可以修改为返回一个包含event
和data
两个属性的对象
@SubscribeMessage('sendMessage')
sendMessage(@MessageBody() data: { message: string }) {
return {
event: 'myEvent',
data: data,
};
}
修改后,原先的前端代码中,回调函数不会触发了。
socket.emit(
"sendMessage",
{
message: "Hello world",
},
(data) => {
console.log(data);
}
);
前端代码需要增加订阅“myEvent”事件,这样就能获取到后端返回的数据
socket.on("myEvent", function (data) {
console.log(data);
});
我们也可以注入实例使用 socket.io
的api,通过使用 @ConnectedSocket()
装饰器。
import {
WebSocketGateway,
SubscribeMessage,
MessageBody,
ConnectedSocket,
} from '@nestjs/websockets';
import { Server } from 'socket.io';
@WebSocketGateway({
cors: {
origin: '*',
}, // 配置一下跨域问题
})
export class WSGateway {
constructor() {}
@SubscribeMessage('sendMessage')
sendMessage(
@MessageBody() data: { message: string },
@ConnectedSocket() server: Server,
) {
server.emit('myEvent', { message: data.message });
return;
}
}
前端还是订阅“myEvent”事件获取数据打印
也可以用@WebSocketServer
注入实例
import {
WebSocketGateway,
SubscribeMessage,
MessageBody,
WebSocketServer,
} from '@nestjs/websockets';
import { Server } from 'socket.io';
@WebSocketGateway({
cors: {
origin: '*',
}, // 配置一下跨域问题
})
export class WSGateway {
constructor() {}
@WebSocketServer()
server: Server;
@SubscribeMessage('sendMessage')
sendMessage(@MessageBody() data: { message: string }) {
this.server.emit('myEvent', { message: data.message });
return;
}
}
结果也是一样的。
简单例子
通过上面的示例,我们可以做一个简单的聊天室
服务端代码,使用OnGatewayConnection
, OnGatewayDisconnect
生命周期钩子。
-
OnGatewayConnection
强制实现handleConnection()
方法,将特定于库的客户端实例作为参数。 -
OnGatewayDisconnect
强制实现handleDisconnect()
方法,将特定于库的客户端实例作为参数。
服务端订阅“userMessage”消息接收数据,并通过“chatMessage”事件向前端发送数据。
import {
WebSocketGateway,
SubscribeMessage,
WebSocketServer,
MessageBody,
OnGatewayConnection,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({
cors: {
origin: '*',
},
})
export class WSGateway implements OnGatewayConnection, OnGatewayDisconnect {
constructor() {}
@WebSocketServer()
server: Server;
handleConnection(client: Socket) {
this.server.emit('chatMessage', {
message: client.id + ' 上线了',
});
}
handleDisconnect(client: Socket) {
this.server.emit('chatMessage', {
message: client.id + ' 离线了',
});
}
@SubscribeMessage('userMessage')
sendMessage(@MessageBody() data: { id: string; message: string }) {
this.server.emit('chatMessage', {
message: data.id + ': ' + data.message,
});
}
}
前端代码
import { useState, useRef } from "react";
import io, { Socket } from "socket.io-client";
import "./index.scss";
export default function Demo() {
const [inputValue, setInputValue] = useState<string>("");
const [messageList, setMessageList] = useState<{ message: string }[]>([]);
const socketRef = useRef<Socket | null>(null);
function connect() {
socketRef.current = io("ws://localhost:3000");
socketRef.current.on("connect", function () {
socketRef.current!.on(
"chatMessage",
function (data: { message: string }) {
setMessageList((messageList) => [
...messageList,
{ message: data.message },
]);
}
);
});
}
function disconnect() {
if (socketRef.current) {
socketRef.current.disconnect();
}
}
const sendMessage = () => {
if (socketRef.current) {
socketRef.current.emit("userMessage", {
id: socketRef.current.id,
message: inputValue,
});
setInputValue("");
}
};
return (
<div className="page-content">
<div className="message-content">
{messageList.map((e) => (
<p className="message-item" key={e.message}>
{e.message}
</p>
))}
<button className="button" onClick={connect}>
上线
</button>
<button className="button" onClick={disconnect}>
下线
</button>
</div>
<div className="input-wrap">
<input
type="text"
className="input"
value={inputValue}
onChange={(event) => setInputValue(event.target.value)}
/>
<button className="button" onClick={sendMessage}>
发送
</button>
</div>
</div>
);
}
效果如下
房间
socket.io 有 room 功能。sockets可以join
和 leave
房间。它可用于向一部分客户端广播事件。
加入
可以调用join
以将socket订阅到给定的频道(加入对应的房间)
io.on("connection", (socket) => {
socket.join("some room");
});
离开
离开房间通过调用leave
socket.leave("some room");
向房间发送消息
使用to
或in
向特定的房间发消息
io.to("some room").emit("some event");
io.in("some room").emit("some event");
也可以同时发射到多个房间
io.to("room1").to("room2").to("room3").emit("some event");
在这种情况下,将执行“联合”:至少在其中一个房间中的每个socket都将获得一次事件(即使socket在两个或更多房间中)。
使用房间做个聊天功能
利用socket.io 的 room 功能简单做一个聊天的功能
服务端代码如下
import {
WebSocketGateway,
SubscribeMessage,
WebSocketServer,
MessageBody,
ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({
cors: {
origin: '*',
},
})
export class RoomWSGateway {
constructor() {}
@WebSocketServer()
server: Server;
@SubscribeMessage('joinRoom')
joinRoom(
@MessageBody() data: { room: string },
@ConnectedSocket() client: Socket,
) {
client.join(data.room);
this.server.to(data.room).emit('roomMessage', {
message: '欢迎' + client.id + '用户加入' + data.room,
});
}
@SubscribeMessage('leaveRoom')
leaveRoom(
@MessageBody() data: { room: string },
@ConnectedSocket() client: Socket,
) {
client.leave(data.room);
this.server
.to(data.room)
.emit('roomMessage', { message: client.id + '用户离开了' + data.room });
}
@SubscribeMessage('sendMessage')
sendMessage(
@MessageBody() data: { room: string; id: string; message: string },
) {
this.server.to(data.room).emit('roomMessage', {
message: data.id + ': ' + data.message,
});
}
}
前端代码
import { useState, useRef } from "react";
import io, { Socket } from "socket.io-client";
import "./index.scss";
export default function Room() {
const [inputValue, setInputValue] = useState<string>("");
const [messageList, setMessageList] = useState<{ message: string }[]>([]);
const socketRef = useRef<Socket | null>(null);
const room = useRef<string>("");
function joinRoom(roomName: string) {
socketRef.current = io("ws://localhost:3000");
socketRef.current.on("connect", function () {
if (socketRef.current) {
socketRef.current.emit("joinRoom", { room: roomName });
room.current = roomName;
socketRef.current.on("roomMessage", (data) => {
setMessageList((messageList) => [
...messageList,
{ message: data.message },
]);
});
}
});
}
function disconnect() {
if (socketRef.current) {
socketRef.current.emit("leaveRoom", { room: room.current });
}
}
const sendMessage = () => {
if (socketRef.current) {
socketRef.current.emit("sendMessage", {
room: room.current,
id: socketRef.current.id,
message: inputValue,
});
setInputValue("");
}
};
return (
<div className="page-content">
<div className="message-content">
{messageList.map((e) => (
<p className="message-item" key={e.message}>
{e.message}
</p>
))}
<button className="button" onClick={() => joinRoom("roomA")}>
加入房间A
</button>
<button className="button" onClick={() => joinRoom("roomB")}>
加入房间B
</button>
<button className="button" onClick={disconnect}>
离开房间
</button>
</div>
<div className="input-wrap">
<input
type="text"
className="input"
value={inputValue}
onChange={(event) => setInputValue(event.target.value)}
/>
<button className="button" onClick={sendMessage}>
发送
</button>
</div>
</div>
);
}
如此,同一个房间的人可以收到房间内人发的消息,而无法收到其他不同的房间的消息。