# websocket 及 socket.io 简介

websocket 是一种在单个 TCP 连接上进行全双工通信的协议。

websocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

以前很多网站为了实现推送技术,所用的技术都是轮询,即浏览器需要不断向服务器发送请求,这样导致过多占用带宽资源和后端资源,而且如果想占用少,那么必然要提高轮询的时间间隔,同时也降低了请求的响应及时性,从任何角度它都不利于我们网站的性能要求。

websocket 在 html5 的标准下应运而生,到现在已不再是一个新的东西了,各个语言都有针对 websocket 协议的适配,在这之中,属 nodejs 阵营的 socket.io 最为出色.

socket.io 在这里不做过多介绍,官方文档里有关于它的详细 api 和例子可供参考,接下来就将 socket.io 集成到 Nest 框架中.

# Nest 集成 socket.io

在 Nest 里,监听 websocket 协议的模块叫做网关 (Gateway)

# 安装必要模块

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

# 创建 socketio网关

h
$ nest g ga socketio/socketio
t
import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets';
@WebSocketGateway()
export class SocketIoGateway {
  @SubscribeMessage('message')
  handleMessage(client: any, payload: any): string {
    return 'Hello world!';
  }
}

# 修改配置项

简单的一条命令就创建好了,它默认监听我们 HTTP 服务监听的端口,我们可以修改端口号也可以修改 path , namespace 等参数,相应地能做什么修改可以参考 socket.io 的 options 选项

接下来让我们补齐它的配置吧

t
@WebSocketGateway({
    path: "/ws",
    transports: ["websocket"],
    cors: {
        origin: "*"
    }
})

@WebSocketGateway 装饰器可以设置 socket.io 的配置,这里我们暂时不修改它的端口号,只设置 path , transports 及跨域 cos

这里设置 transportswebsocket , 意为使用 websocket 协议

为了访问原生的 Server 实例,可以使用 @WebSocketServer 装饰器,这里的 Server 可以从 socket.io 导入

t
....
import { Server, Socket } from 'socket.io';
@WebSocketGateway({
    path: "/ws",
    transports: ["websocket"],
    cors: {
        origin: "*"
    }
})
export class SocketIoGateway implements OnGatewayDisconnect,OnGatewayConnection,OnGatewayInit{
    @WebSocketServer()
    server: Server;
    
    .....
}

OnGatewayInit , OnGatewayDisconnect , OnGatewayConnection 是 3 个有用的生命周期钩子,分别是实例初始化,客户端断开,客户端连接的接口

# 搭建简单的聊天室应用

使用 Nest 框架集成 socket.io 完成之后让我们搭建一个简单的聊天室应用,这里采用 socket.ioGet started 项目 (前端代码几乎一样)

# 部署前端代码

HTML 部分 index.html

l
<!DOCTYPE html>
<html>
  <head>
    <title>Socket.IO chat</title>
    <style>
      body { margin: 0; padding-bottom: 3rem; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
      #form { background: rgba(0, 0, 0, 0.15); padding: 0.25rem; position: fixed; bottom: 0; left: 0; right: 0; display: flex; height: 3rem; box-sizing: border-box; backdrop-filter: blur(10px); }
      #input { border: none; padding: 0 1rem; flex-grow: 1; border-radius: 2rem; margin: 0.25rem; }
      #input:focus { outline: none; }
      #form > button { background: #333; border: none; padding: 0 1rem; margin: 0.25rem; border-radius: 3px; outline: none; color: #fff; }
      #messages { list-style-type: none; margin: 0; padding: 0; }
      #messages > li { padding: 0.5rem 1rem; }
      #messages > li:nth-child(odd) { background: #efefef; }
    </style>
  </head>
  <body>
    <ul id="messages"></ul>
    <form id="form" action="">
      <input id="input" autocomplete="off" /><button>Send</button>
    </form>
  </body>
</html>

JS 部分

l
<script type="text/javascript" src="https://cdn.socket.io/4.5.0/socket.io.min.js"></script>
<script type="text/javascript">
const socket = io("ws://localhost:9000",{path:"/ws",transports:["websocket"]});
socket.on("connect",()=>{
  console.log("连接成功:",socket.id);
  socket.emit("message","你好啊",(res)=>{
    console.log(res);
  });
});
socket.on("disconnect",(reason)=>{
  console.log("已断开连接:",reason);
});
socket.on('response_message', function(msg) {
  let item = document.createElement('li');
  item.textContent = msg;
  messages.appendChild(item);
  window.scrollTo(0, document.body.scrollHeight);
});
const form = document.getElementById('form');
const input = document.getElementById('input');
form.addEventListener('submit', function(e) {
  e.preventDefault();
  if (input.value) {
    socket.emit('chat_message', input.value,(res)=>{
      console.log(res);
    });
    input.value = '';
  }
});
</script>

SocketIoGateway

t
import {
    SubscribeMessage,
    WebSocketGateway,
    OnGatewayDisconnect,
    OnGatewayConnection,
    OnGatewayInit,
    MessageBody,
    ConnectedSocket,
    WsResponse,
    WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({
    path: "/ws",
    transports: ["websocket"],
    cors: {
        origin: "*"
    }
})
export class SocketIoGateway implements OnGatewayDisconnect,OnGatewayConnection,OnGatewayInit{
    // 这里的 server 即 socket.io 的 server 实例
    @WebSocketServer()
    server: Server;
    @SubscribeMessage('message')
    handleMessage(@ConnectedSocket() client: Socket, @MessageBody() data: any): string {
        return `Hello ${data}!`;
    }
    @SubscribeMessage('chat_message')
    handleChatMessage(@ConnectedSocket() client: Socket,@MessageBody() data:any): string{
        // 向所有人广播
        this.server.emit("response_message",data)
        return "发送成功!";
    }
    // 断开链接
    handleDisconnect(client: Socket){
        console.log("断开链接:",client.id);
    }
    // 建立链接
    handleConnection(client: Socket){
        console.log("建立链接:",client.id);
    }
    // 初始化完成
    afterInit(server:Server){
        console.log("server 初始化完成");
    }
}

以上便是所有代码了,我们可以使用浏览器打开 2 个 index.html 进行测试

# 测试简单聊天室

首先启动我们的 Nest 框架

h
$ npm run dev
.....
[Nest] 14440  - 2022/09/01 14:20:58     LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +739ms
[Nest] 14440  - 2022/09/01 14:20:58     LOG [InstanceLoader] TypeOrmModule dependencies initialized +0ms
[Nest] 14440  - 2022/09/01 14:20:58     LOG [InstanceLoader] OptionsModule dependencies initialized +1ms
server 初始化完成
[Nest] 14440  - 2022/09/01 14:20:58     LOG [WebSocketsController] SocketIoGateway subscribed to the "message" message +20ms
[Nest] 14440  - 2022/09/01 14:20:58     LOG [WebSocketsController] SocketIoGateway subscribed to the "chat_message" message +1ms
[Nest] 14440  - 2022/09/01 14:20:58     LOG [RoutesResolver] AppController {/api}: +1ms
[Nest] 14440  - 2022/09/01 14:20:58     LOG [RouterExplorer] Mapped {/api/hello, GET} (version: 1) route +3ms
[Nest] 14440  - 2022/09/01 14:20:58     LOG [RouterExplorer] Mapped {/api/hello, GET} (version: 2) route +0ms
[Nest] 14440  - 2022/09/01 14:20:58     LOG [RouterExplorer] Mapped {/api/mysql_host, GET} route +1ms
[Nest] 14440  - 2022/09/01 14:20:58     LOG [RoutesResolver] OptionsController {/api/options}: +0ms
[Nest] 14440  - 2022/09/01 14:20:58     LOG [RouterExplorer] Mapped {/api/options, GET} route +0ms
[Nest] 14440  - 2022/09/01 14:20:58     LOG [RouterExplorer] Mapped {/api/options/set, POST} route +1ms
[Nest] 14440  - 2022/09/01 14:20:58     LOG [NestApplication] Nest application successfully started +3ms
server listen on 9000!
建立链接: ZlExP3YVjbZRFHEJAAAB
建立链接: de8IBLbBg_sQacOZAAAD

接下来可以欢快地聊天了

聊天

请我喝杯[咖啡]~( ̄▽ ̄)~*

一个放羊娃 微信支付

微信支付

一个放羊娃 支付宝

支付宝