WebSocket实现聊天室

更新时间: 2024-11-21 16:23:21

# 背景

我们公司的3d渲染之前一直在客户端,客户端渲染虽然服务器压力小,但是弊端还是很明显,对客户端硬件要求高,加载很慢,而且模型一大就会卡,因此,老板决定要把模型渲染挪到服务端去,调研过后决定用nodejs去实现,大概原理如下:

  1. nodejs 安装模拟浏览器环境的包 以及 threejs
  2. 渲染出画面以后用websocket推送给客户端
  3. 客户端鼠标的事件传给服务端
  4. 服务端的画面根据客户端鼠标事件发生改变,然后将最新的截图推送给客户端

万丈高楼平地起,前两周把nodejs基础课程刷完了,现在开始一步步实现我的想法,首先先实现一下nodejs的websocket

# WebSocket出现的原因

  1. Http协议发布RESR API的不足:
    每次请求响应完成之后,服务器与客户端之间的连接就断开了,如果客户端想要继续获取服务器的消息,必须再次向服务器发起请求。这显然无法适应对实时通信要求高的场景。

  2. 改善http的不足:Web通信领域出现了一些其他的解决方案,如轮询、长轮询、服务器推送事件,WebSocket

  • 轮询:就是重复发送新的请求到服务器。如果服务器没有新的数据,就发送适当的指示并关闭连接。然后客户端等待一段时间(比如间隔一秒),再发送另一个请求。这种实现方式相对比较简单,无需做过多的更改。但学点是轮询的间隔过长,会导致用户不能及时接收到更新的数据;轮询时间过短,会导致查询请求过多,增加服务器端的负担。

  • 长轮询:客户端发送一个请求到服务器,如果服务器端没有新的数据,就保持这个连接直到有数据。一旦服务器端有了数据(消息)给客户端。他就是用这个连接发送数据给客户端,接着关闭连接。

  • 服务器推送事件: Server Sent Events(SSE),SSE通常中庸一个连接处理多个消息(事件)。SSE还定义了一个专门的媒体类型,用于描述一个从服务端发送到客户端的简单格式。

  • WebSocket: 提供了一个真正的全双工链接。发起者是一个客户端,发送一个带特殊HTTP头的请求到服务端,通知服务器。该方案的有点事属于html5标准,已经被大多数浏览器支持,而且是真正的全双工,性能比较好,其缺点是实现起来比较复杂,需要对ws协议专门处理

# Node使用ws创建WebSocket服务器

  1. Node.js原生API没有提供对WebSocket的支持,需要安装第三方包才能使用WebSocket功能
  2. ws模块 :是一个用于支持WebSocket客户端和服务器的框架。它易于使用,功能强大,且不依赖于其他环境
  3. 安装ws: npm install ws 这个库在浏览器不起作用
    文档https://github.com/websockets/ws/blob/HEAD/doc/ws.md (opens new window)
    https://www.npmjs.com/package/ws#api-docs (opens new window)
  4. 创建WebSocket服务器
// 创建一个WebSocket服务器,在8080端口启动 
const WebSocket = require('ws')  
const server = new WebSocket.Server({port:8080})
1
2
3
  1. WebSocket.Server(options[, callback])方法中options对象所支持的参数
    |参数|作用| |----|---| |host|绑定服务器的主机名| |port|绑定服务器的端口号| |backlog|挂起链接队列的最大长度| |server|预先创建的node.js http/s服务器| |verifyClient|可用于验证传入连接的函数| |handelProtocols|可用于处理WebSocket子协议的函数| |path|仅接受此路径匹配的连接| |noServer|不启用服务器模式| |clientTracking|指定是否跟踪客户端| |perMessageDeflate|启用/禁用消息压缩| |maxPayload|允许的最大消息大小(以字节为单位)|

# 监听链接

ws通过connection事件来监听连接

// 只要有WebSocket连接到服务器,就会触发connection事件,req对象可以用来获取客户端的信息,如ip,端口号  
// 获取所有已连接的客户端信息,则可以使用server.clients数据集  
server.on('connection',function connection(ws, req) {
    const ip = req.socket.remoteAddress  
    const port = req.socket.remotePort  
    const clientName = ip + port 
})
1
2
3
4
5
6
7

# 发送数据

ws通过send()方法来发送数据

/**
 * send(data [,options][,callback])
 *      data:发送的数据  
 *      options对象:
 *          (1)compress: 指定数据是否需要压缩。默认为true  
 *           (2) binary: 指定数据是否通过二进制传送,默认是自动检测  
 *          (3)mask: 指定是否应遮罩数据  
 *          (4)fin: 指定数据是否为消息的最后一个片段,默认为true  
 */
server.on('connection',function connection(ws, req){
    const ip = req.socket.remoteAddress  
    const port = req.socket.remotePort  
    const clientName = ip + port 

    ws.send('Welcome '+clientName)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 接收数据

ws通过message事件来接收数据。当客户端有消息发送给服务器时,服务器就能触发该消息

server.on('connection',function connection(ws, req){
    const ip = req.socket.remoteAddress  
    const port = req.socket.remotePort  
    const clientName = ip + port 

    ws.send('Welcome '+clientName)  

    ws.on('message', function incoming(message){
        console.log('recived: %s from %s', message, clientName)
        server.clients.forEach(function each(client) {
            if(client.readyState === WebSocket.OPEN) {
                client.send(clientName + "->" + message)
            }
        })
    }) 
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 准备的状态

ws中WebSocket类具有以下4种准备状态

状态 含义
CONNECTION 0 连接还没有打开
OPEN 1 连接已经打开,可以通信了
CLOSING 2 连接正在关闭
CLOSED 3 连接已经关闭

# 服务器端 server.js

// 创建一个websocket服务器,在3000端口启动  
const WebSocket = require('ws')  
const server = new WebSocket.Server({port:3000})  

server.on('open', () => {
    console.log('connected')
})

server.on('connection', (ws, req) => {
    const ip = req.socket.remoteAddress
    const port = req.socket.remotePort  
    const clientName = ip + port 

    console.log('%s is connected', clientName)

    ws.send('Welcome '+ clientName)  

    ws.on('message', (message) => {
        console.log('received: %s from %s', message, clientName)
        server.clients.forEach((client) => {
            if(client.readyState === WebSocket.OPEN) {
                client.send(clientName + " -> " + message)
            }
        })
    })
})

server.on('close', () => {
    console.log('disconnected')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 客户端(使用了vue)

<template>
  <div class="main">
    <h3>WebSocket 聊天室:</h3>
    <div id="responseTest"></div>
    <input type="text" v-model="message" style="width:300px;">
    <input type="button" value="发送消息" @click="send(message)">
  </div>
</template>

<script setup>
  import {ref, onMounted} from "vue"

  const message = ref('')
  let socket = null

  onMounted(() => {
    if(!window.WebSocket) {
      window.WebSocket = window.MozWebSocket
    }

    if(window.WebSocket) {
      socket = new WebSocket("ws://127.0.0.1:3000/")
      socket.onmessage = function(event) {
        console.log(event)
        var ta = document.getElementById('responseTest')
        ta.innerHTML += '<br>' + event.data
      }

      socket.onopen = function(event) {
        var ta = document.getElementById('responseTest')
        ta.innerHTML = '连接开启'
      }

      socket.onclose = function(event) {
        var ta = document.getElementById('responseTest')
        ta.innerHTML += '<br>' + '连接关闭'
      }
    }else{
      alert('连接没有开启')
    }
  })

  function send(message) {
    if(!window.WebSocket) {
      return 
    }
    if(socket.readyState === WebSocket.OPEN) {
      socket.send(message)
    }else{
      alert('连接没有开启')
    }
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53