<

1.创建项目 #

nest new nest-ws
npm install --save @nestjs/websockets @nestjs/platform-socket.io

2.客户端连接 #

2.1. message.module.ts #

src/message/message.module.ts

// 从 @nestjs/common 导入 Module 装饰器
import { Module } from '@nestjs/common';
// 从本地文件导入 MessageGateway,这个类负责处理 WebSocket 事件
import { MessageGateway } from './message.gateway';
// 使用 @Module 装饰器定义一个模块
@Module({
  // 在 providers 数组中注册 MessageGateway,表示该模块提供 MessageGateway 服务
  providers: [MessageGateway],
})
// 定义并导出 MessageModule 类,代表消息模块
export class MessageModule { }

2.2. message.gateway.ts #

src/message/message.gateway.ts

// 导入 WebSocketGateway 和 WebSocketServer 装饰器,用于声明 WebSocket 网关
import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
// 导入 Socket.IO 的 Server 类型,用于定义 server 实例
import { Server } from 'socket.io';
// 使用 @WebSocketGateway 装饰器声明一个 WebSocket 网关类
@WebSocketGateway()
export class MessageGateway {
    // 使用 @WebSocketServer 装饰器来注入 Socket.IO 的 Server 实例
    @WebSocketServer()
    server: Server;  // Server 实例,用于处理 WebSocket 连接和事件
}

2.3. app.module.ts #

src/app.module.ts

import { Module } from '@nestjs/common';
+import { MessageModule } from './message/message.module';
@Module({
+ imports: [MessageModule],
+ controllers: [],
+ providers: [],
})
+export class AppModule { }

2.4. main.ts #

src/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);
+ app.useStaticAssets('public');
  await app.listen(3000);
}
bootstrap();

2.5. index.html #

public/index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>聊天室</title>
    <link href="https://static.docs-hub.com/bootstrapmin_1726934364785.css" rel="stylesheet">
    <script src="https://static.docs-hub.com/jquery360min_1726934373776.js"></script>
    <script src="https://static.docs-hub.com/socketiomin_1726934381484.js"></script>
</head>

<body>
    <script>
        const socket = io('http://localhost:3000');
        socket.on('connect', () => {
            console.log('已连接到服务器');
        });
    </script>
</body>

</html>

2.6. .eslintrc.js #

.eslintrc.js

module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    project: 'tsconfig.json',
    tsconfigRootDir: __dirname,
    sourceType: 'module',
  },
  plugins: ['@typescript-eslint/eslint-plugin'],
  extends: [
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
  ],
  root: true,
  env: {
    node: true,
    jest: true,
  },
  ignorePatterns: ['.eslintrc.js'],
  rules: {
    '@typescript-eslint/interface-name-prefix': 'off',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-explicit-any': 'off',
+   'linebreak-style': ['error', 'auto'],
  },
};

3.用户登录 #

3.1. message.gateway.ts #

src/message/message.gateway.ts

+import { ConnectedSocket, MessageBody, SubscribeMessage, WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
+import { Server, Socket } from 'socket.io';

@WebSocketGateway()
export class MessageGateway {
    @WebSocketServer()
    server: Server;
+
+   @SubscribeMessage('userJoined')
+   handleUserJoined(@MessageBody() data: { username: string }, @ConnectedSocket() client: Socket) {
+       client.data.username = data.username;
+       this.server.emit('userJoined', { username: data.username });
+   }
}

3.2. index.html #

public/index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>聊天室</title>
+   <link href="https://static.docs-hub.com/bootstrapmin_1726934364785.css" rel="stylesheet">
+   <script src="https://static.docs-hub.com/jquery360min_1726934373776.js"></script>
+   <script src="https://static.docs-hub.com/socketiomin_1726934381484.js"></script>
</head>

<body>
+   <div class="container">
+       <h1 class="mt-5 text-center">聊天室</h1>
+       <div id="loginForm" class="my-4">
+           <div class="mb-3">
+               <label for="username" class="form-label">用户名</label>
+               <input type="text" class="form-control" id="username" placeholder="请输入用户名">
+           </div>
+           <button id="loginBtn" class="btn btn-primary">登录</button>
+       </div>
+       <div id="chatWindow" class="d-none">
+           <div class="card">
+               <div class="card-header">
+                   聊天消息
+                   <span class="float-end">
+                       当前用户: <strong id="currentUsername"></strong>
+                   </span>
+               </div>
+               <div class="card-body" id="messages" style="height: 300px; overflow-y: scroll;">
+
+               </div>
+               <div class="card-footer">
+                   <div class="input-group">
+                       <input type="text" class="form-control" id="messageInput" placeholder="输入消息">
+                       <button class="btn btn-primary" id="sendMessageBtn">发送</button>
+                   </div>
+               </div>
+           </div>
+       </div>
+   </div>
    <script>
+       $('#loginBtn').on('click', () => {
+           const username = $('#username').val();
+           if (!username) {
+               alert('请输入用户名');
+               return;
+           }
+           $('#currentUsername').text(username);
+           $('#chatWindow').removeClass('d-none');
+           $('#loginForm').hide();
+           const socket = io();
+           socket.on('userJoined', (data) => {
+               const messageElement = $('<div>').text(`系统消息: ${data.username} 加入了聊天室`);
+               $('#messages').append(messageElement);
+           });
+           socket.on('connect', () => {
+               console.log('已连接到服务器');
+               socket.emit('userJoined', { username });
+           });
        });
    </script>
</body>

</html>

4.发送消息 #

4.1. message.gateway.ts #

src/message/message.gateway.ts

import { ConnectedSocket, MessageBody, SubscribeMessage, WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway()
export class MessageGateway {
    @WebSocketServer()
    server: Server;
    @SubscribeMessage('userJoined')
    handleUserJoined(@MessageBody() data: { username: string }, @ConnectedSocket() client: Socket) {
        client.data.username = data.username;
        this.server.emit('userJoined', { username: data.username });
    }
+   @SubscribeMessage('createMessage')
+   handleMessage(@MessageBody() createMessageDto: { username: string, message: string }) {
+       this.server.emit('message', createMessageDto);
+   }
}

4.2. index.html #

public/index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>聊天室</title>
    <link href="https://static.docs-hub.com/bootstrapmin_1726934364785.css" rel="stylesheet">
    <script src="https://static.docs-hub.com/jquery360min_1726934373776.js"></script>
    <script src="https://static.docs-hub.com/socketiomin_1726934381484.js"></script>
</head>

<body>
    <div class="container">
        <h1 class="mt-5 text-center">聊天室</h1>
        <div id="loginForm" class="my-4">
            <div class="mb-3">
                <label for="username" class="form-label">用户名</label>
                <input type="text" class="form-control" id="username" placeholder="请输入用户名">
            </div>
            <button id="loginBtn" class="btn btn-primary">登录</button>
        </div>
        <div id="chatWindow" class="d-none">
            <div class="card">
                <div class="card-header">
                    聊天消息
                    <span class="float-end">
                        当前用户: <strong id="currentUsername"></strong>
                    </span>
                </div>
                <div class="card-body" id="messages" style="height: 300px; overflow-y: scroll;">

                </div>
                <div class="card-footer">
                    <div class="input-group">
                        <input type="text" class="form-control" id="messageInput" placeholder="输入消息">
                        <button class="btn btn-primary" id="sendMessageBtn">发送</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script>
+       let username = '';
+       let socket = null;
        $('#loginBtn').on('click', () => {
+           username = $('#username').val();
            if (!username) {
                alert('请输入用户名');
                return;
            }
            $('#currentUsername').text(username);
            $('#chatWindow').removeClass('d-none');
            $('#loginForm').hide();
+           socket = io();
            socket.on('userJoined', (data) => {
                const messageElement = $('<div>').text(`系统消息: ${data.username} 加入了聊天室`);
                $('#messages').append(messageElement);
            });
            socket.on('connect', () => {
                console.log('已连接到服务器');
                socket.emit('userJoined', { username });
            });
+           socket.on('message', (messageData) => {
+               const messageElement = $('<div>').text(`${messageData.username}: ${messageData.message}`);
+               $('#messages').append(messageElement);
+           });
+       });
+       $('#sendMessageBtn').on('click', () => {
+           const message = $('#messageInput').val();
+           if (message && socket && username) {
+               const messageData = { username, message };
+               socket.emit('createMessage', messageData);
+               $('#messageInput').val('');
+           }
        });
    </script>
</body>
</html>

5.私聊 #

5.1. create-message.dto.ts #

src/message/create-message.dto.ts

export class CreateMessageDto {
    username: string
    message: string
    recipient?: string
}

5.2. message.gateway.ts #

src/message/message.gateway.ts

import { ConnectedSocket, MessageBody, SubscribeMessage, WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
+import { CreateMessageDto } from './create-message.dto';

@WebSocketGateway()
export class MessageGateway {
    @WebSocketServer()
    server: Server;
    @SubscribeMessage('userJoined')
    handleUserJoined(@MessageBody() data: { username: string }, @ConnectedSocket() client: Socket) {
        client.data.username = data.username;
        this.server.emit('userJoined', { username: data.username });
    }
    @SubscribeMessage('createMessage')
+   handleMessage(@MessageBody() createMessageDto: CreateMessageDto) {
+       const { username, message, recipient } = createMessageDto;
+       if (recipient) {
+           const recipientSocket = Array.from(this.server.sockets.sockets.values())
+               .find((socket) => socket.data.username === recipient);
+           if (recipientSocket) {
+               recipientSocket.emit('message', { username, message });
+           }
+       } else {
+           this.server.emit('message', createMessageDto);
+       }
    }
}

5.3. index.html #

public/index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>聊天室</title>
    <link href="https://static.docs-hub.com/bootstrapmin_1726934364785.css" rel="stylesheet">
    <script src="https://static.docs-hub.com/jquery360min_1726934373776.js"></script>
    <script src="https://static.docs-hub.com/socketiomin_1726934381484.js"></script>
</head>

<body>
    <div class="container">
        <h1 class="mt-5 text-center">聊天室</h1>
        <div id="loginForm" class="my-4">
            <div class="mb-3">
                <label for="username" class="form-label">用户名</label>
                <input type="text" class="form-control" id="username" placeholder="请输入用户名">
            </div>
            <button id="loginBtn" class="btn btn-primary">登录</button>
        </div>
        <div id="chatWindow" class="d-none">
            <div class="card">
                <div class="card-header">
                    聊天消息
                    <span class="float-end">
                        当前用户: <strong id="currentUsername"></strong>
                    </span>
                </div>
                <div class="card-body" id="messages" style="height: 300px; overflow-y: scroll;">

                </div>
                <div class="card-footer">
                    <div class="input-group">
                        <input type="text" class="form-control" id="messageInput" placeholder="输入消息">
                        <button class="btn btn-primary" id="sendMessageBtn">发送</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script>
        let username = '';
        let socket = null;
        $('#loginBtn').on('click', () => {
            username = $('#username').val();
            if (!username) {
                alert('请输入用户名');
                return;
            }
            $('#currentUsername').text(username);
            $('#chatWindow').removeClass('d-none');
            $('#loginForm').hide();
            socket = io();
            socket.on('userJoined', (data) => {
                const messageElement = $('<div>').text(`系统消息: ${data.username} 加入了聊天室`);
                $('#messages').append(messageElement);
            });
            socket.on('connect', () => {
                console.log('已连接到服务器');
                socket.emit('userJoined', { username });
            });
            socket.on('message', (messageData) => {
                const messageElement = $('<div>').text(`${messageData.username}: ${messageData.message}`);
                $('#messages').append(messageElement);
            });
        });
        $('#sendMessageBtn').on('click', () => {
            const message = $('#messageInput').val();
            if (message && socket && username) {
+               let recipient = null;
+               let actualMessage = message;
+               const atIndex = message.indexOf('@');
+               if (atIndex !== -1) {
+                   const endOfUsername = message.indexOf(' ', atIndex);
+                   const recipient = message.substring(atIndex + 1, endOfUsername);
+                   actualMessage = message.substring(endOfUsername + 1);
+               }
+               socket.emit('createMessage', { username, message: actualMessage, recipient });
                $('#messageInput').val('');
            }
        });
    </script>
</body>
</html>

6.群聊 #

6.1. create-room.dto.ts #

src/message/create-room.dto.ts

export class CreateRoomDto {
    roomName: string;
}

6.2. join-room.dto.ts #

src/message/join-room.dto.ts

export class JoinRoomDto {
    room: string
    username: string
}

6.3. create-message.dto.ts #

src/message/create-message.dto.ts

export class CreateMessageDto {
    username: string
    message: string
    recipient?: string
+   room?: string;
}

6.4. message.gateway.ts #

src/message/message.gateway.ts

import { ConnectedSocket, MessageBody, SubscribeMessage, WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { CreateMessageDto } from './create-message.dto';
+import { CreateRoomDto } from './create-room.dto';
+import { JoinRoomDto } from './join-room.dto';
@WebSocketGateway()
export class MessageGateway {
    @WebSocketServer()
    server: Server;
+   private rooms: Set<string> = new Set();
+
    @SubscribeMessage('userJoined')
    handleUserJoined(@MessageBody() data: { username: string }, @ConnectedSocket() client: Socket) {
        client.data.username = data.username;
        this.server.emit('userJoined', { username: data.username });
    }
    @SubscribeMessage('createMessage')
    handleMessage(@MessageBody() createMessageDto: CreateMessageDto) {
+       const { username, message, recipient, room } = createMessageDto;
        if (recipient) {
            const recipientSocket = Array.from(this.server.sockets.sockets.values())
+               .find((socket) => {
+                   return socket.data.username === recipient
+               });
            if (recipientSocket) {
                recipientSocket.emit('message', { username, message });
            }
+       } else if (room) {
+           this.server.to(room).emit('message', { username, message });
        } else {
            this.server.emit('message', createMessageDto);
        }
    }
+
+   @SubscribeMessage('createRoom')
+   handleCreateRoom(@MessageBody() createRoomDto: CreateRoomDto) {
+       const { roomName } = createRoomDto;
+       if (!this.rooms.has(roomName)) {
+           this.rooms.add(roomName);
+           this.server.emit('roomList', Array.from(this.rooms));
+       }
+   }
+   @SubscribeMessage('joinRoom')
+   handleJoinRoom(@MessageBody() data: JoinRoomDto, @ConnectedSocket() client: Socket) {
+       const { room, username } = data;
+       client.join(room);
+       client.data.username = username;
+       this.server.to(room).emit('userJoinedRoom', { username: client.data.username, room });
+   }
+
+   @SubscribeMessage('requestRooms')
+   handleRequestRooms(@ConnectedSocket() client: Socket) {
+       client.emit('roomList', Array.from(this.rooms));
+   }
}

6.5. index.html #

public/index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>聊天室</title>
    <link href="https://static.docs-hub.com/bootstrapmin_1726934364785.css" rel="stylesheet">
    <script src="https://static.docs-hub.com/jquery360min_1726934373776.js"></script>
    <script src="https://static.docs-hub.com/socketiomin_1726934381484.js"></script>
</head>

<body>
    <div class="container">
        <h1 class="mt-5 text-center">聊天室</h1>
        <div id="loginForm" class="my-4">
            <div class="mb-3">
                <label for="username" class="form-label">用户名</label>
                <input type="text" class="form-control" id="username" placeholder="请输入用户名">
            </div>
            <button id="loginBtn" class="btn btn-primary">登录</button>
        </div>
+       <div id="roomSection" class="d-none">
+           <h3>房间列表</h3>
+           <ul id="roomList" class="list-group mb-3">
+           </ul>
+           <div class="mb-3">
+               <label for="roomName" class="form-label">创建房间</label>
+               <input type="text" class="form-control" id="roomName" placeholder="请输入房间名">
+           </div>
+           <button id="createRoomBtn" class="btn btn-success">创建房间</button>
+       </div>
        <div id="chatWindow" class="d-none">
            <div class="card">
                <div class="card-header">
                    聊天消息
                    <span class="float-end">
                        当前用户: <strong id="currentUsername"></strong>
+                       <span id="currentRoomInfo"> | 房间: <strong id="currentRoom"></strong></span>
                    </span>
                </div>
                <div class="card-body" id="messages" style="height: 300px; overflow-y: scroll;">

                </div>
                <div class="card-footer">
                    <div class="input-group">
                        <input type="text" class="form-control" id="messageInput" placeholder="输入消息">
                        <button class="btn btn-primary" id="sendMessageBtn">发送</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script>
        let username = '';
        let socket = null;
+       let room = '';
        $('#loginBtn').on('click', () => {
            username = $('#username').val();
            if (!username) {
                alert('请输入用户名');
                return;
            }
            $('#currentUsername').text(username);
+           $('#roomSection').removeClass('d-none');
            $('#loginForm').hide();
+           socket = io('http://localhost:3000');
            socket.on('userJoined', (data) => {
                const messageElement = $('<div>').text(`系统消息: ${data.username} 加入了聊天室`);
                $('#messages').append(messageElement);
            });
            socket.on('connect', () => {
                console.log('已连接到服务器');
+               socket.emit('requestRooms');
                socket.emit('userJoined', { username });
            });
            socket.on('message', (messageData) => {
+               console.log('messageData', messageData);
                const messageElement = $('<div>').text(`${messageData.username}: ${messageData.message}`);
                $('#messages').append(messageElement);
            });
+           socket.on('roomList', (rooms) => {
+               $('#roomList').empty();
+               rooms.forEach((room) => {
+                   const roomElement = $('<li>').addClass('list-group-item').text(room);
+                   roomElement.on('click', () => joinRoom(room));
+                   $('#roomList').append(roomElement);
+               });
+           });
        });
+       function joinRoom(roomName) {
+           room = roomName;
+           $('#roomSection').addClass('d-none');
+           $('#chatWindow').removeClass('d-none');
+           $('#currentRoom').text(roomName);
+           $('#currentRoomInfo').show();
+           socket.emit('joinRoom', { room, username });
+       }
        $('#sendMessageBtn').on('click', () => {
            const message = $('#messageInput').val();
+           if (!room) {
+               alert('请先加入房间');
+               return;
+           }
+           if (message && socket && username && room) {
                let recipient = null;
                let actualMessage = message;
                const atIndex = message.indexOf('@');
                if (atIndex !== -1) {
                    const endOfUsername = message.indexOf(' ', atIndex);
+                   recipient = message.substring(atIndex + 1, endOfUsername);
                    actualMessage = message.substring(endOfUsername + 1);
                }
+               console.log({ username, message: actualMessage, recipient, room });
+               socket.emit('createMessage', { username, message: actualMessage, recipient, room });
                $('#messageInput').val('');
            }
        });
+       $('#createRoomBtn').on('click', () => {
+           const roomName = $('#roomName').val();
+           if (roomName) {
+               socket.emit('createRoom', { roomName });
+               $('#roomName').val('');
+           }
+       });
+       window.addEventListener('beforeunload', () => {
+           if (socket) {
+               socket.disconnect();
+           }
+       });
    </script>
</body>
</html>

1. WebSocket #

什么是 WebSocket? #

WebSocket 是一种全双工的通信协议,允许客户端和服务器之间建立持久连接,以实现实时、低延迟的双向数据传输。与传统的 HTTP 请求-响应模型不同,WebSocket 使得客户端和服务器都可以随时发送和接收数据,而无需反复建立和关闭连接。

1.1 WebSocket 的工作原理 #

  1. 连接建立

    • WebSocket 连接是通过一个初始的 HTTP 请求(称为“握手”请求)建立的。客户端通过发送一个带有特殊头部的 HTTP 请求来请求 WebSocket 连接。
    • 服务器收到请求后,如果同意建立 WebSocket 连接,会返回一个 101 状态码,表示协议切换成功。之后,这个 HTTP 连接会被升级为 WebSocket 连接,客户端和服务器可以进行双向通信。
  2. 双向通信

    • 在 WebSocket 连接建立后,客户端和服务器之间的通信不再遵循传统的 HTTP 请求-响应模式。服务器和客户端可以在任意时间向对方发送数据,且数据是即时传输的。
  3. 持续连接

    • WebSocket 连接是持续的,除非客户端或服务器主动断开,否则这个连接会一直保持有效。这种持续的双向通信非常适合需要频繁数据更新的应用场景,如聊天、在线游戏、股票行情等。
  4. 数据格式

    • WebSocket 发送的数据可以是文本数据(通常为 JSON 格式)或二进制数据(如 ArrayBuffer、Blob 等)。
    • WebSocket 协议支持轻量的帧(frame)结构,在传输数据时不需要每次都携带完整的 HTTP 头部信息,这使得它比传统的长轮询(long polling)等技术更加高效。

1.2 WebSocket 的优势 #

  1. 全双工通信:客户端和服务器可以同时发送和接收消息,无需等待对方响应。

  2. 低延迟:由于 WebSocket 是持久连接,一旦连接建立,数据可以即时传输,无需每次都建立新的连接,避免了 HTTP 请求的开销。

  3. 减少带宽消耗:WebSocket 数据帧的头部非常小,相比于每次都发送完整的 HTTP 请求和响应,WebSocket 协议的开销更低。

  4. 实时性强:WebSocket 允许实时通信,特别适合需要实时更新的应用,如聊天应用、在线游戏、股票行情推送等。

1.3 WebSocket 和 HTTP 的区别 #

特性 WebSocket HTTP
通信方式 双向(全双工) 单向(请求-响应)
连接方式 持久连接 短连接,每次请求重新建立
数据帧开销 轻量,头部较小 每次请求需携带完整的 HTTP 头部
适用场景 实时通信、低延迟场景 适用于一次性请求-响应的场景
数据发送方向 客户端和服务器都可以主动发送数据 服务器只能响应客户端的请求

1.4 WebSocket 的应用场景 #

  1. 即时通讯:如聊天应用(WhatsApp、微信)、支持多人在线的聊天室。
  2. 在线游戏:游戏客户端和服务器需要频繁交换实时数据,比如多人在线游戏中的玩家动作、状态更新等。
  3. 实时金融数据:如股票、加密货币交易平台,可以通过 WebSocket 实时推送价格变化、订单成交等数据。
  4. 协作工具:如多人在线文档编辑,所有用户的操作实时同步。
  5. 物联网(IoT):传感器和服务器之间的实时数据交换,如智能家居系统。
  6. 视频流媒体:虽然 WebSocket 通常不直接用于传输大规模的视频数据,但在控制视频播放、实时聊天和互动等场景中非常有用。

1.5 WebSocket 客户端和服务器的示例 #

1.5.1 客户端(浏览器端)的 WebSocket 示例 #

// 创建 WebSocket 连接
const socket = new WebSocket('ws://localhost:8080');

// 连接成功时的回调
socket.onopen = function (event) {
  console.log('WebSocket 连接已打开');
  // 发送消息到服务器
  socket.send('Hello Server');
};

// 收到服务器消息时的回调
socket.onmessage = function (event) {
  console.log('服务器消息:', event.data);
};

// 连接关闭时的回调
socket.onclose = function (event) {
  console.log('WebSocket 连接已关闭');
};

// 连接出错时的回调
socket.onerror = function (error) {
  console.log('WebSocket 错误:', error);
};

1.5.2 服务器端的 WebSocket 示例(基于 Node.js 和 ws 库) #

const WebSocket = require('ws');

// 创建 WebSocket 服务器,监听端口 8080
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  console.log('客户端已连接');

  // 监听客户端发送的消息
  ws.on('message', (message) => {
    console.log('收到客户端消息:', message);

    // 发送消息给客户端
    ws.send('Hello Client');
  });

  // 监听连接关闭事件
  ws.on('close', () => {
    console.log('客户端已断开连接');
  });
});

1.6 WebSocket 与其他技术的对比 #

  1. 与 HTTP 长轮询

    • 长轮询是一种模拟“实时”通信的技术,客户端通过定期发送 HTTP 请求来获取服务器的更新。相比之下,WebSocket 是真正的实时双向通信,不需要频繁发送请求,效率更高。
  2. 与 Server-Sent Events (SSE)

    • SSE 只支持服务器向客户端的单向推送,无法实现客户端向服务器的双向通信。而 WebSocket 支持双向通信,适用于更多场景。

1.7 小结 #

WebSocket 是一种强大的通信协议,适用于需要实时、低延迟的应用场景。它提供了全双工的通信模型,并且能够显著减少网络通信的开销,是现代网络应用(如在线游戏、实时聊天、金融市场等)中不可或缺的技术。

2.Socket.IO #

Socket.IO 是一个基于事件驱动的实时双向通信库,常用于实现服务器与客户端之间的实时数据传输。它通常用于像聊天室、实时数据更新、在线游戏等需要即时通信的场景。

2.1 主要特点: #

  1. 实时双向通信:客户端和服务器之间可以进行双向通信,服务器可以主动向客户端发送消息,客户端也可以向服务器发送数据。

  2. 跨平台支持:Socket.IO 支持各种平台(浏览器、Node.js、Android、iOS 等)之间的通信,且自动处理不同的传输协议。

  3. 自动降级:当浏览器不支持 WebSocket 时,Socket.IO 会自动降级到其他传输方式(如长轮询)。

  4. 基于事件的模型:通信是通过事件触发机制完成的,用户可以自定义事件来处理特定的逻辑。例如,客户端可以监听 message 事件,服务器可以触发这个事件并发送消息。

  5. 房间和命名空间:支持房间(Rooms)和命名空间(Namespaces),可以实现复杂的通信逻辑,比如将用户分配到不同的房间,实现组播功能。

2.2 使用流程: #

2.3 示例: #

// 服务器端 (Node.js)
const io = require('socket.io')(3000);
io.on('connection', (socket) => {
  console.log('用户连接了');
  socket.on('message', (data) => {
    console.log('接收到消息:', data);
    io.emit('message', data); // 广播消息给所有客户端
  });
});

// 客户端 (浏览器)
const socket = io('http://localhost:3000');
socket.on('connect', () => {
  console.log('已连接到服务器');
});
socket.on('message', (data) => {
  console.log('收到消息:', data);
});

Socket.IO 提供了便捷的 API 来处理实时通信,使开发者可以轻松地构建实时应用。

3.@nestjs/websockets #

@nestjs/websockets 是 NestJS 框架提供的一个用于构建基于 WebSocket 的实时通信应用的模块。NestJS 是一个基于 Node.js 的框架,受 Angular 的启发,使用 TypeScript 开发,结构化、模块化程度较高,非常适合构建服务器端应用程序。

通过 @nestjs/websockets,你可以轻松地将 WebSocket 集成到 NestJS 应用中,并创建具备实时数据传输能力的应用程序。

3.1. 安装依赖 #

在使用 @nestjs/websockets 之前,需要确保已经安装了相应的依赖:

npm install --save @nestjs/websockets @nestjs/platform-socket.io

@nestjs/platform-socket.io 是 WebSocket 的 Socket.IO 适配器,用于在 NestJS 中使用 Socket.IO。

3.2. 创建 WebSocket 网关(Gateway) #

在 NestJS 中,WebSocket 通过 "网关" (Gateway) 进行处理。网关是监听和响应 WebSocket 客户端请求的核心组件。

示例代码

import { WebSocketGateway, SubscribeMessage, MessageBody } from '@nestjs/websockets';
import { WebSocketServer } from 'ws';

@WebSocketGateway()
export class ChatGateway {
  @WebSocketServer() server;

  @SubscribeMessage('message')
  handleMessage(@MessageBody() data: string): void {
    this.server.emit('message', data); // 将消息广播给所有客户端
  }
}

3.3. 使用 Socket.IO 适配器 #

虽然 @nestjs/websockets 可以直接与 ws (WebSocket 库) 集成,但在实际应用中,NestJS 通常通过 Socket.IO 来处理 WebSocket 通信。Socket.IO 提供了更高层次的功能,如房间(rooms)、命名空间(namespaces)等。

示例

@WebSocketGateway({ namespace: 'chat' })
export class ChatGateway {
  @WebSocketServer() server;

  @SubscribeMessage('message')
  handleMessage(@MessageBody() data: string): void {
    this.server.to('some-room').emit('message', data); // 向特定房间广播消息
  }
}

3.4. WebSocket 与 HTTP 的结合 #

NestJS 是一个强大的全栈框架,允许将 HTTP 与 WebSocket 无缝结合在一起。例如,你可以在同一个服务中处理 HTTP 请求和 WebSocket 消息,复用服务和逻辑。

3.5. 生命周期钩子 #

NestJS 允许你通过 WebSocket 生命周期钩子来处理客户端的连接和断开事件。

示例

@WebSocketGateway()
export class ChatGateway {
  @WebSocketServer() server;

  handleConnection(client: any, ...args: any[]) {
    console.log(`Client connected: ${client.id}`);
  }

  handleDisconnect(client: any) {
    console.log(`Client disconnected: ${client.id}`);
  }
}

3.6. 总结 #

这使得 @nestjs/websockets 成为一个功能强大且灵活的工具,适合构建实时聊天、游戏、在线协作等需要高效实时通信的应用程序。

4.@WebSocketGateway #

@WebSocketGateway 是 NestJS 提供的一个装饰器,用于创建 WebSocket 网关。它允许我们在 NestJS 应用中轻松集成和管理 WebSocket 通信功能。

4.1 主要功能: #

@WebSocketGateway 装饰器用于声明一个类为 WebSocket 网关,NestJS 会将其转换为能够处理 WebSocket 事件的类。通过它,你可以处理客户端的连接、消息传输和断开连接等事件。

4.2 基本用法: #

import { WebSocketGateway, WebSocketServer, SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway() // 声明这是一个 WebSocket 网关
export class MyGateway {
  @WebSocketServer()
  server: Server; // 引用 Socket.IO 的 Server 实例

  // 监听 'message' 事件
  @SubscribeMessage('message')
  handleMessage(@MessageBody() data: any, @ConnectedSocket() client: Socket) {
    console.log('收到消息:', data);
    this.server.emit('message', data); // 广播消息给所有连接的客户端
  }

  // 处理客户端连接
  handleConnection(client: Socket) {
    console.log('客户端已连接', client.id);
  }

  // 处理客户端断开连接
  handleDisconnect(client: Socket) {
    console.log('客户端断开连接', client.id);
  }
}

4.3关键概念: #

  1. @WebSocketGateway()

    • 装饰类,用于声明这个类是一个 WebSocket 网关。
    • 你可以通过传递参数来指定自定义的配置,例如端口号或协议:
      @WebSocketGateway(3001, { namespace: '/chat' }) // 在 /chat 命名空间上监听端口 3001
      
  2. @WebSocketServer

    • 使用 @WebSocketServer 装饰一个类属性来引用 Socket.IOServer 实例。通过它可以直接与所有连接的客户端进行交互,例如广播消息、管理连接等。
  3. @SubscribeMessage()

    • 用于处理来自客户端的特定事件消息。@SubscribeMessage('event_name') 会监听来自客户端名为 'event_name' 的事件,并处理相关逻辑。
    • 事件处理函数会接受 @MessageBody()@ConnectedSocket() 等参数:
      • @MessageBody():获取消息内容。
      • @ConnectedSocket():获取当前连接的客户端 Socket 实例。
  4. 生命周期钩子方法

    • handleConnection(client: Socket):当客户端连接时触发,通常用于初始化或发送欢迎消息。
    • handleDisconnect(client: Socket):当客户端断开连接时触发,通常用于清理资源或记录日志。

4.4 配置选项: #

@WebSocketGateway() 支持多种配置,如下:

@WebSocketGateway({
  namespace: '/chat',  // 设置命名空间
  cors: {              // 配置跨域
    origin: '*',
  },
})

4.5 应用场景: #

  1. 实时聊天应用:多个客户端之间能够通过 WebSocket 连接实时发送和接收消息。
  2. 在线通知系统:当某些事件发生时,立即通过 WebSocket 向客户端推送消息。
  3. 多人在线游戏:游戏状态的实时更新和广播。
  4. 实时数据流:如股票市场、社交媒体更新等,需要推送实时数据的场景。

通过 @WebSocketGateway,NestJS 提供了一种简单而强大的方式来处理 WebSocket 通信,使得开发者可以很容易地构建实时应用。

5.@WebSocketServer #

@WebSocketServer 是 NestJS 提供的一个装饰器,用于在 WebSocket 网关中注入 Socket.IO 的 Server 实例。它允许你直接访问并控制 WebSocket 服务器,进而可以与所有连接的客户端进行通信,如广播消息、发送私信、管理房间等。

5.1 主要功能: #

  1. 访问 Socket.IOServer 实例:通过 @WebSocketServer 装饰器,你可以在网关类中访问 Socket.IO 的核心 Server 实例,从而控制连接的客户端和 WebSocket 事件。
  2. 广播消息:你可以通过 Server 实例将消息发送给所有连接的客户端或特定的客户端。
  3. 管理房间和命名空间:你可以使用 Server 实例来创建房间或命名空间,并在这些房间或命名空间中进行消息广播或管理连接。

5.2 基本用法: #

import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
import { Server } from 'socket.io';

@WebSocketGateway()
export class ChatGateway {
  @WebSocketServer()
  server: Server;  // 注入 Socket.IO 的 Server 实例

  // 广播消息给所有客户端
  broadcastMessage(message: string) {
    this.server.emit('message', { text: message });
  }
}

5.3 关键点: #

  1. 注入 Server 实例@WebSocketServer 装饰器会将 Socket.IO 的 Server 实例注入到类的属性中。这个实例允许你控制整个 WebSocket 服务器,比如向所有客户端广播消息或向特定房间发送消息。

  2. 消息广播: 使用 this.server.emit() 方法可以向所有连接的客户端发送消息。例如,你可以将某个事件的数据广播给所有用户:

    this.server.emit('event_name', data);
    

    这会向所有连接的客户端发送名为 event_name 的事件和数据。

  3. 向特定客户端发送消息: 如果你想向特定的客户端发送消息,可以通过 Socket 实例中的 id 来定位客户端:

    this.server.to(socketId).emit('event_name', data);
    

    这样只会向特定的客户端发送消息。

  4. 房间和命名空间@WebSocketServer 允许你管理房间(Rooms)和命名空间(Namespaces):

    • 房间:房间是一组客户端连接,房间中的消息只会广播给特定组内的客户端。使用 join()leave() 可以让客户端加入或离开房间。

      // 客户端加入房间
      client.join('room1');
      
      // 向房间内所有客户端广播消息
      this.server.to('room1').emit('message', { text: 'Hello Room 1' });
      
    • 命名空间:命名空间用于分隔 WebSocket 的事件处理逻辑,可以为每个命名空间指定特定的路由或逻辑。

      @WebSocketGateway({ namespace: '/chat' })
      

5.4 示例:广播和房间管理 #

import { WebSocketGateway, WebSocketServer, SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway()
export class ChatGateway {
  @WebSocketServer()
  server: Server;

  // 处理用户连接
  handleConnection(client: Socket) {
    console.log(`用户 ${client.id} 已连接`);
  }

  // 处理用户断开连接
  handleDisconnect(client: Socket) {
    console.log(`用户 ${client.id} 已断开`);
  }

  // 监听 "message" 事件
  @SubscribeMessage('message')
  handleMessage(@MessageBody() message: string, @ConnectedSocket() client: Socket) {
    // 向所有客户端广播消息
    this.server.emit('message', { user: client.id, text: message });
  }

  // 客户端加入房间
  @SubscribeMessage('joinRoom')
  handleJoinRoom(@MessageBody() room: string, @ConnectedSocket() client: Socket) {
    client.join(room);
    this.server.to(room).emit('message', { user: '系统', text: `用户 ${client.id} 加入了房间 ${room}` });
  }

  // 客户端离开房间
  @SubscribeMessage('leaveRoom')
  handleLeaveRoom(@MessageBody() room: string, @ConnectedSocket() client: Socket) {
    client.leave(room);
    this.server.to(room).emit('message', { user: '系统', text: `用户 ${client.id} 离开了房间 ${room}` });
  }
}

5.5 解释: #

5.6 适用场景: #

  1. 实时聊天:你可以使用 @WebSocketServer 来管理多个聊天房间,广播消息给房间内的所有用户。
  2. 游戏:多人在线游戏中,可以用房间来分组玩家,并且用 Server 实例管理实时的游戏状态。
  3. 通知系统:可以向所有用户或者特定用户组广播重要通知。

通过 @WebSocketServer,NestJS 为你提供了强大的控制权来管理 WebSocket 连接、房间和消息广播,使得构建复杂的实时应用变得更加简单和高效。

6.@SubscribeMessage #

@SubscribeMessage 是 NestJS 中用于 WebSocket 的一个装饰器,它专门用来监听和处理来自客户端的特定事件。当客户端发送某个特定事件时,带有 @SubscribeMessage 装饰器的方法会被触发并处理该事件。

6.1 主要功能: #

@SubscribeMessage 可以用来监听客户端发送的自定义事件,并根据接收到的数据执行相应的逻辑操作。事件的处理逻辑通常包括接收消息内容、对消息进行处理,然后再通过 WebSocket 发送响应回客户端。

6.2 基本用法: #

import { WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets';
import { Socket } from 'socket.io';

@WebSocketGateway()
export class ChatGateway {

  // 使用 @SubscribeMessage 监听 'message' 事件
  @SubscribeMessage('message')
  handleMessage(@MessageBody() data: string, @ConnectedSocket() client: Socket): string {
    console.log(`收到的消息内容: ${data}`);
    return `服务器响应: ${data}`;
  }
}

6.3 详细解释: #

  1. @SubscribeMessage('事件名')

    • 装饰一个方法来监听特定的 WebSocket 事件,例如 message
    • 当客户端发送该事件时,方法会被自动调用并处理事件数据。
  2. @MessageBody()

    • 使用 @MessageBody() 可以从客户端传来的消息中提取数据。在上面的例子中,它提取了 data,这是客户端发送的消息内容。
    • 这个参数通常是消息的主体数据。
  3. @ConnectedSocket()

    • 通过 @ConnectedSocket() 可以获取到当前连接的 Socket 实例,它代表了当前的客户端连接。
    • 可以通过 Socket 实例向特定客户端发送消息、加入房间等操作。

6.4 方法的返回值: #

6.5 示例:处理房间内的消息 #

import { WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets';
import { Socket } from 'socket.io';

@WebSocketGateway()
export class RoomGateway {

  // 监听 'joinRoom' 事件,客户端请求加入房间时触发
  @SubscribeMessage('joinRoom')
  handleJoinRoom(@MessageBody() room: string, @ConnectedSocket() client: Socket) {
    client.join(room); // 客户端加入指定的房间
    client.emit('joinedRoom', `你已加入房间 ${room}`);
  }

  // 监听 'sendMessage' 事件,客户端发送消息时触发
  @SubscribeMessage('sendMessage')
  handleSendMessage(@MessageBody() data: { room: string, message: string }, @ConnectedSocket() client: Socket) {
    const { room, message } = data;
    // 向特定房间内的所有客户端广播消息
    client.to(room).emit('receiveMessage', message);
  }
}

6.6 解释: #

  1. handleJoinRoom:当客户端发送 joinRoom 事件时,服务器端接收到房间名并将该客户端加入指定的房间。之后,服务器会给该客户端发送一条确认信息,告诉它已经成功加入房间。
  2. handleSendMessage:当客户端在房间内发送消息时,服务器会监听 sendMessage 事件,并将消息广播到该房间内的所有客户端。

6.7 应用场景: #

6.8 小结: #

这个装饰器非常适合实时应用场景,如聊天、在线游戏等需要频繁通信的应用。

7.@MessageBody #

@MessageBody 是 NestJS 提供的一个参数装饰器,专门用于从 WebSocket 事件中提取消息主体(即客户端发送的数据)。当客户端通过 WebSocket 向服务器发送事件时,服务器可以通过 @MessageBody 获取这次事件携带的数据内容。

7.1 主要功能: #

@MessageBody 允许你从客户端发来的 WebSocket 消息中直接获取传递的数据。在事件处理函数中,使用该装饰器能够轻松获取消息内容并进行处理。

7.2 基本用法: #

import { WebSocketGateway, SubscribeMessage, MessageBody } from '@nestjs/websockets';

@WebSocketGateway()
export class ChatGateway {

  // 监听 'message' 事件,并通过 @MessageBody 获取消息内容
  @SubscribeMessage('message')
  handleMessage(@MessageBody() data: string): string {
    console.log(`接收到的消息: ${data}`);
    return `服务器收到: ${data}`; // 返回数据给客户端
  }
}

7.3 解释: #

  1. @SubscribeMessage('message')

    • 监听客户端发送的名为 message 的事件。
    • 当客户端通过 WebSocket 发送 message 事件时,服务器端的 handleMessage 方法会被调用。
  2. @MessageBody()

    • @MessageBody() 用来提取客户端发送的消息内容。比如,客户端发送了 message: 'Hello'@MessageBody() 就会将 'Hello' 提取出来并作为参数传递给 handleMessage 方法。
    • 在上面的例子中,data 变量包含了客户端发送的消息。

7.4 客户端发送数据示例: #

// 客户端通过 WebSocket 发送消息
socket.emit('message', 'Hello Server');

7.5 复杂数据处理: #

除了简单的字符串,@MessageBody() 也可以处理复杂的数据类型,比如对象、数组等。在实际应用中,通常会通过对象结构发送消息数据。

7.5.1 示例:处理对象数据 #

import { WebSocketGateway, SubscribeMessage, MessageBody } from '@nestjs/websockets';

@WebSocketGateway()
export class ChatGateway {

  // 处理带有对象数据的事件
  @SubscribeMessage('sendMessage')
  handleSendMessage(@MessageBody() data: { username: string, message: string }): string {
    console.log(`用户 ${data.username} 发送了消息: ${data.message}`);
    return `服务器已收到来自 ${data.username} 的消息`;
  }
}

7.5.2 客户端发送对象数据: #

// 客户端发送带有对象数据的消息
socket.emit('sendMessage', { username: 'Alice', message: 'Hello!' });

在上面的例子中:

7.6 使用 @MessageBody() 结合 DTO: #

为了确保传入的数据结构符合预期,可以结合 DTO(数据传输对象)来使用 @MessageBody(),从而保证数据类型的安全性和一致性。

7.6.1 示例:使用 DTO #

import { WebSocketGateway, SubscribeMessage, MessageBody } from '@nestjs/websockets';
import { IsString } from 'class-validator';

// 定义 DTO 来约束消息结构
export class SendMessageDto {
  @IsString()
  username: string;

  @IsString()
  message: string;
}

@WebSocketGateway()
export class ChatGateway {

  // 监听事件并使用 DTO 验证数据
  @SubscribeMessage('sendMessage')
  handleSendMessage(@MessageBody() data: SendMessageDto): string {
    console.log(`用户 ${data.username} 发送了消息: ${data.message}`);
    return `服务器已收到来自 ${data.username} 的消息`;
  }
}

在这种情况下,@MessageBody() 会将客户端发来的数据绑定到 SendMessageDto 类型的对象中,从而确保数据符合预期的格式。

7.7 使用场景: #

7.8 小结: #

8.@ConnectedSocket #

@ConnectedSocket 是 NestJS 提供的一个参数装饰器,用于在 WebSocket 网关中获取当前连接的客户端 Socket 实例。通过这个装饰器,服务器可以访问客户端的连接信息,从而执行与该客户端相关的操作,比如向特定客户端发送消息、管理房间、处理断开连接等操作。

8.1 主要功能: #

  1. 获取客户端的 Socket 实例:通过 @ConnectedSocket,你可以获取当前连接的客户端 Socket,并利用它执行与该客户端相关的操作,如发送消息、获取客户端的 ID 等。
  2. 管理客户端连接:你可以通过 Socket 实例来管理客户端连接的状态,比如加入或离开房间、断开连接等。
  3. 访问客户端的唯一 ID:每个客户端的 Socket 实例都有一个唯一的 id,可以通过它来识别不同的客户端。

8.2 基本用法: #

import { WebSocketGateway, SubscribeMessage, ConnectedSocket } from '@nestjs/websockets';
import { Socket } from 'socket.io';

@WebSocketGateway()
export class ChatGateway {

  // 监听 'message' 事件,并获取客户端的 Socket 实例
  @SubscribeMessage('message')
  handleMessage(@ConnectedSocket() client: Socket): string {
    console.log(`客户端 ID: ${client.id}`); // 输出客户端的 ID
    return `你好,客户端 ${client.id}`;  // 返回消息给客户端
  }
}

8.3 解释: #

  1. @SubscribeMessage('message'):监听来自客户端的 message 事件。
  2. @ConnectedSocket():装饰 client 参数,用于获取当前发送 message 事件的客户端的 Socket 实例。在 client 中,你可以访问与该客户端连接相关的所有信息。
  3. client.id:每个客户端连接时都会分配一个唯一的 id,可以通过 client.id 访问该客户端的标识符。

8.4 Socket 实例的常见用法: #

8.5 结合 @ConnectedSocket 和 @MessageBody 使用: #

通常,@ConnectedSocket@MessageBody 会一起使用,前者用于获取当前客户端的连接信息,后者用于获取客户端发送的消息内容。

8.5.1 示例:处理房间中的消息 #

import { WebSocketGateway, SubscribeMessage, ConnectedSocket, MessageBody } from '@nestjs/websockets';
import { Socket } from 'socket.io';

@WebSocketGateway()
export class RoomGateway {

  // 客户端加入房间
  @SubscribeMessage('joinRoom')
  handleJoinRoom(@MessageBody() room: string, @ConnectedSocket() client: Socket) {
    client.join(room);  // 将客户端加入指定的房间
    client.emit('joinedRoom', `你已加入房间 ${room}`);
  }

  // 处理房间内的消息
  @SubscribeMessage('sendMessage')
  handleSendMessage(@MessageBody() message: string, @ConnectedSocket() client: Socket) {
    const rooms = Object.keys(client.rooms);  // 获取客户端所在的房间
    const room = rooms[1]; // 默认房间在第二个位置(第一个是自身连接 ID)

    if (room) {
      client.to(room).emit('receiveMessage', message);  // 广播消息到房间
    } else {
      client.emit('error', '你尚未加入任何房间');
    }
  }
}

8.6 解释: #

  1. handleJoinRoom

    • 通过 @MessageBody() 获取客户端请求加入的房间名,通过 @ConnectedSocket() 获取客户端 Socket 实例。
    • 使用 client.join(room) 将客户端加入指定的房间,并返回确认消息给客户端。
  2. handleSendMessage

    • 使用 @MessageBody() 获取客户端发送的消息,通过 @ConnectedSocket() 获取客户端的 Socket 实例。
    • 通过 client.rooms 获取该客户端当前所在的所有房间。
    • 将消息广播给同一房间内的所有其他客户端。

8.7 应用场景: #

  1. 私信功能:可以通过 Socket 实例向某个特定客户端发送消息,从而实现私聊。

    client.emit('privateMessage', { message: 'Hello!' });
    
  2. 房间管理:使用 Socket 实例将客户端加入或移出房间,实现多人聊天或游戏房间功能。

    client.join('gameRoom1');
    client.leave('gameRoom1');
    
  3. 断开连接:服务器可以主动断开某个客户端的连接。

    client.disconnect();
    

8.8 小结: #