mock-socket模拟WebSocket接口实战

什么是 WebSocket?

以下是来自 DeepSeek 的回答

简单来说,WebSocket 是一种网络通信协议,它允许在单个、长期的连接上进行全双工(双向) 通信。这与我们熟悉的 HTTP 协议形成了鲜明对比。

一个生动的比喻:电话 vs 明信片
HTTP(如明信片): 你每次想获取或发送新信息,都需要寄出一张新的明信片(发起一个新的请求)。客户端问一句,服务器答一句,然后连接就关闭了。如果你想持续获取更新,你就得不停地寄明信片问“有更新吗?”,这被称为 “轮询”。这种方式效率低下,浪费资源。

WebSocket(如电话): 你首先拨通一个电话(发起一个 HTTP 握手请求),电话接通后(WebSocket 连接建立),你们之间就保持了一条持续的通信线路。双方可以随时说话,你来我往,无需挂断再重拨。这实现了真正的实时、低延迟的对话。

WebSocket 的核心特点
全双工通信
服务器和客户端可以同时、独立地向对方发送数据。客户端无需等待服务器的响应,反之亦然。

单一 TCP 连接
连接一旦建立,在整个会话期间都会保持打开状态。这消除了为每个数据交换建立新连接的开销。

低延迟
由于没有频繁建立连接和发送冗余 HTTP 头信息的开销,数据可以非常快速地在双方之间传递。

真正的实时性
服务器可以在有新数据时主动“推送” 给客户端,客户端无需反复询问。

WebSocket 是如何工作的?
WebSocket 连接的建立依赖于一次普通的 HTTP 请求,这个过程叫做“握手”。

握手(Handshake)

客户端(例如浏览器)发送一个特殊的 HTTP 请求,其中包含一个头信息 Upgrade: websocket。

这相当于客户端对服务器说:“你好,我们别用 HTTP 了,升级到 WebSocket 协议聊天吧?”

如果服务器支持 WebSocket,它会回复一个 HTTP 101 Switching Protocols 的响应,表示同意升级。

至此,握手完成,底层的 TCP 连接保持不变,但通信协议从 HTTP 切换到了 WebSocket。

数据交换

握手成功后,连接保持打开状态。

双方开始使用 WebSocket 协议定义的数据帧格式进行通信,可以发送文本或二进制数据。

连接关闭

当任何一方决定结束通信时,可以发送一个关闭帧来优雅地终止连接。

为什么需要 WebSocket?解决了什么问题?
在没有 WebSocket 之前,为了实现实时效果,开发者们使用了一些“曲线救国”的方法:

轮询: 客户端每隔几秒就向服务器发一个 HTTP 请求问:“有数据吗?” 无论服务器有没有新数据,都会响应。这种方式效率低,浪费带宽和服务器资源。

长轮询: 客户端发起一个请求,如果服务器没有新数据,就把这个请求挂起,直到有数据或超时才返回。客户端收到响应后立即再发起一个新的请求。这比普通轮询好一些,但依然复杂且对服务器有压力。

WebSocket 的出现,正是为了从根本上解决这些方法的低效和复杂性,为真正的实时双向通信提供了标准方案。

主要应用场景
WebSocket 非常适合需要高实时性和频繁数据交换的场景:

在线聊天/即时通讯:最典型的例子,消息即发即收。

多人在线游戏:玩家的每一个操作都需要实时同步给其他所有玩家。

实时数据仪表盘:股票行情、体育赛事比分、实时销售数据、服务器监控等。

协同编辑工具:如 Google Docs,多人同时编辑时,能看到彼此的改动。

在线直播弹幕和点赞。

代码示例(前端 JavaScript)
在浏览器中,WebSocket API 使用起来非常简单。

// 1. 创建 WebSocket 连接,连接到服务器端点
const socket = new WebSocket('ws://example.com/socket-server')

// 2. 监听连接打开事件
socket.onopen = function (event) {
  console.log('WebSocket 连接已建立')
  // 连接建立后,发送一条消息给服务器
  socket.send('你好,服务器!')
}

// 3. 监听来自服务器的消息
socket.onmessage = function (event) {
  console.log('收到服务器消息:', event.data)
  // 可以更新网页上的内容,例如将消息添加到聊天窗口
}

// 4. 监听连接关闭事件
socket.onclose = function (event) {
  console.log('WebSocket 连接已关闭')
}

// 5. 监听错误事件
socket.onerror = function (error) {
  console.error('WebSocket 错误:', error)
}

// 发送消息的函数
function sendMessage(message) {
  if (socket.readyState === WebSocket.OPEN) {
    socket.send(message)
  }
}

总结

特性 HTTP WebSocket
通信模式 半双工(请求-响应) 全双工(双向)
连接 短暂,每次请求后关闭 持久,单一长连接
开销 每次请求都包含完整的 HTTP 头 初始握手后,数据帧头很小
实时性 靠轮询模拟,延迟高 真正的实时,低延迟
数据流向 主要由客户端发起 服务器可以主动推送

总而言之,WebSocket 是为现代 Web 应用对实时性、低延迟的高要求而设计的通信协议,是构建互动式、实时 Web 应用的基石技术。

简而言之,就是不用再向服务器发起轮询请求(在吗?有消息吗?),只用建立一次链接,之后就可以高效的双向通信。

因此,在实际开发中,前端页面要显示某个设备(如无人机,车辆)在地图上的实时位置,WebSocke(以下简称 ws),就显得很有用了,节省了大量资源和性能开销。

那么,作为一个 webGis 前端开发者在构建 cesium 应用的时候,后端的 ws 接口还没有准备好的时候,前端开发怎么模拟 ws 接口呢?首先容易想到的是 mockjs,很遗憾,mockjs 无法直接模拟,它的核心功能是拦截 ajax 请求并生成模拟数据。

mock-socket 介绍和使用

这里就要介绍 mock-socket 这个包了,这是它的 GitHub 主页

mock-socket

mock-socket 的简单使用

主页有安装和 hello world 的起步,这里就不赘述了

如何模拟设备的定位信息呢?

定位数据的准备

这里给出一个 json 数据的简单示例

{
  "locations": [
    {
      "time": "2025-01-01 10:01:00",
      "location": {
        "type": "Point",
        "coordinates": [117.627915, 40.608494, 731.15]
      }
    },
    {
      "time": "2025-01-01 10:02:00",
      "location": {
        "type": "Point",
        "coordinates": [117.627915, 40.608494, 731.15]
      }
    },
    {
      "time": "2025-01-01 10:03:00",
      "location": {
        "type": "Point",
        "coordinates": [117.627915, 40.608494, 731.31]
      }
    },
    {
      "time": "2025-01-01 10:04:00",
      "location": {
        "type": "Point",
        "coordinates": [117.627915, 40.608494, 731.48]
      }
    },
    {
      "time": "2025-01-01 10:05:00",
      "location": {
        "type": "Point",
        "coordinates": [117.627915, 40.608494, 731.59]
      }
    }
  ]
}

引入 mock-socket 实例

首先创建启动的函数,在启动的函数里面处理准备的模拟数据 json,把组织好的数据每五秒进行一次广播

import { Server } from 'mock-socket'

// mock-socket 的 Server 实例;复用以保证多次启动不会重复创建
let server = null

// 定时广播的计时器句柄
let broadcastTimer = null

// 启动本地位置 WS 模拟服务器(若已启动则返回停止函数)
export const startMockLocationWsServer = async () => {
  if (server) {
    return stopMockLocationWsServer
  }

  // 从 public 目录加载模拟位置数据
  const response = await fetch('/geojson/mockLocation.json')
  if (!response.ok) {
    throw new Error(`加载 mockLocation.json 失败: ${response.status}`)
  }
  const json = await response.json()
  // 期望结构:{ locations: Array<FeatureLike> }
  const locations = Array.isArray(json?.locations) ? json.locations : []
  if (locations.length === 0) {
    throw new Error('mockLocation.json 中未找到有效的 locations 数组')
  }

  // 创建本地 WS 服务,并维护一个已连接 socket 的集合
  server = new Server(MOCK_WS_URL)
  const sockets = new Set()

  server.on('connection', (socket) => {
    sockets.add(socket)
    const handleClose = () => sockets.delete(socket)
    socket.addEventListener?.('close', handleClose)
    socket.onclose = handleClose
  })

  let index = 0
  // 广播函数:按序从 locations 取一条记录并推送给所有连接的客户端
  const broadcast = () => {
    try {
      const record = locations[index % locations.length]
      const payload = {
        type: 'location',
        index: index % locations.length,
        time: record.time,
        lng: record.location.coordinates[0],
        lat: record.location.coordinates[1],
        height: record.location.coordinates[2],
      }
      const data = JSON.stringify(payload)
      sockets.forEach((s) => {
        try {
          s.send(data)
        } catch {}
      })
      index += 1
    } catch {}
  }

  // 立即推送一条,随后每 5s 推送一次
  broadcast()
  broadcastTimer = window.setInterval(broadcast, 5000)

  return stopMockLocationWsServer
}

停止的函数

export const stopMockLocationWsServer = () => {
  if (broadcastTimer != null) {
    clearInterval(broadcastTimer)
    broadcastTimer = null
  }
  if (server) {
    try {
      server.stop?.()
    } catch {}
    server = null
  }
}

可以看到,控制台已经按照预期的每五秒钟打印出位置信息

记得 cesium 组件在卸载时候一并关闭 ws

onBeforeUnmount(() => {
  try {
    ws?.close?.()
  } catch {}
})