likes
comments
collection
share

⚡一文搞懂实时通信,建议来看看,WebSocket和SSE都有

作者站长头像
站长
· 阅读数 15

前言

一直对实时通信功能蛮感兴趣的。之前在上家公司做聊天功能的时候,以为终于可以实操下websocket了,谁知道too young too simple,后端没搞过websocket也不想调研,最后采取的方案是轮询,然后这个实时通信功能就搁置了。

直到最近公司的项目迭代中有一个服务端实时推送消息给前端的功能,不由得又勾起了我对长连接的兴趣。

⚡一文搞懂实时通信,建议来看看,WebSocket和SSE都有

长连接的前世今生

技术来源于实际需求。

对于实时性方面的需求,经过技术不断的迭代,目前分为三大块:长轮询、SSE 和 websocket

长轮询

长轮询采用的还是HTTP协议,为了实现不断接收到服务端返回的数据,有两种方法

  • 通过Ajax向服务器发送数据,并在获取到数据后再次发送请求。因为请求会被“挂起”,所以也被称为“挂起式请求”。

  • 固定通过setInterval每隔2s发送一次请求,页面卸载/切除后台,清除定时器

优点:

  • 学习成本低,实现简单。

  • 应用场景较广。

  • 对服务器压力较小。

缺点:

  • 需要不断地发送请求和关闭连接,会增加网络流量和延迟。

  • 会占用服务器IO资源。

  • 在高并发情况下,会导致大量的挂起式请求,可能影响服务器性能。

SSE

SSEServer-Send Events)是HTML5规范中新增的一种实现实时推送、单向通信的技术,是基于 HTTP协议中的持久连接。

下面是一个使用SSE实现实时推送的示例代码:

var source = new EventSource(“/api/sse”);
// 监听连接是否建立
source.onopen = function(event){ console.log(event) }
// 接收消息
source.onmessage = function (event) {
    console.log(“Got data:”, event.data);
    //处理返回的数据//…
};
// 监听错误信息
source.onerror= function(event){ console.log(event.readyState) }
// 断开SSE连接
source.close();

在服务器端,我们需要启用SSE并持续向客户端发送消息。下面是Node.js服务端代码:

app.get(‘/api/sse’, function(req, res) {
    res.writeHead(200, 
        {‘Content-Type’: ‘text/event-stream’,
Cache-Control’: ‘no-cache’,
Connection’: ‘keep-alive’}
    );
    //持续发送消息
    setInterval(function() {
        var data = generateNewData();
        res.write(‘data: ‘ + JSON.stringify(data) + ‘\n\n’);}, 2000);
    });    
})

SSE通信过程

⚡一文搞懂实时通信,建议来看看,WebSocket和SSE都有

优点:

  • 学习成本低,实现简单。

  • 性能更好,因为客户端和服务器之间的连接是持久的,减少了网络流量和延迟。

  • 可以自动重连,保证了可靠性。

  • 单向通信(服务端 -> 客户端)

缺点:

  • 不支持跨域请求(需要使用CORS解决)。

  • 需要浏览器和服务器的支持,可能存在兼容性问题。

websocket

WebSocket协议是基于TCP协议的,与HTTP协议有一定的关联。

  • 关联的点:WebSocket协议使用HTTP协议进行握手,但一旦握手成功,通信就会转换为基于TCP的持久连接,不需要像HTTP那样频繁地建立和断开连接。

WebSocket能够实现实时双向通信,允许服务器主动向客户端发送数据。客户端也能主动向服务端发送数据。

Websocket 和 EventSource都是windows自带的吗?

不是。是浏览器环境提供的。

浏览器、浏览器环境、v8引擎、jsCore引擎、 windows对象、node中的global对象 分别有什么联系?

  1. 浏览器:浏览器是用于访问和呈现Web页面的应用程序。不同的浏览器有不同的实现和功能,但它们都遵循Web标准,Web标准组成了浏览器的环境。

  2. 浏览器环境提供了一系列的功能和API(例如DOM操作、AJAX、Canvas等),使开发者能够在浏览器中开发丰富的Web应用程序。浏览器环境负责解析和执行网页内容,并提供与浏览器窗口、页面元素、网络通信等相关的API供开发者使用。

  3. V8引擎:V8是一种高性能的JavaScript引擎,由Google开发并用于Chrome浏览器。V8引擎负责解析和执行JavaScript代码,并提供了JavaScript运行环境的基本功能。

  4. JSCore引擎:苹果浏览器(Safari)的JavaScript引擎是WebKit JavaScript引擎,在WebKit中,JavaScript引擎被称为JavaScriptCore(JSC),它负责解析和执行JavaScript代码。

  5. Window对象:Window对象是浏览器环境中的全局对象。在浏览器中,JavaScript代码可以直接访问和操作Window对象,它提供了一系列与浏览器窗口、页面和浏览器功能相关的属性和方法。

  6. global对象:在Node.js环境中,global是一个类似于Window对象的全局对象。它是Node.js中的顶级作用域,在所有模块中都可以访问。Node.js中的JavaScript代码可以直接访问和操作global对象,它提供了一些与Node.js运行时环境相关的属性和方法(例如模块加载、文件系统访问等)。

看到十个字以上就头疼的~~~ ,请看下图:

⚡一文搞懂实时通信,建议来看看,WebSocket和SSE都有

优点:

  • 实时性:WebSocket提供了持久的连接,允许服务器主动推送数据给客户端,实现了实时性的双向通信。

  • 较低的延迟:由于WebSocket建立了持久连接,避免了多次握手和断开连接的开销,使得数据传输的延迟较低。

  • 更小的数据传输量:WebSocket使用了较小的数据帧头部,减少了数据传输的开销。

  • 更好的兼容性:WebSocket协议得到了广泛支持,现代浏览器都提供了对WebSocket的原生支持。

缺点:

  • 较高的服务器资源消耗:WebSocket需要保持持久连接,对服务器的资源消耗较大,特别是在大规模并发连接的情况下。

  • 需要较新的浏览器支持:较旧的浏览器版本可能不支持WebSocket,这会限制应用程序的兼容性。

  • 长连接可能导致代理服务器限制:某些代理服务器可能会限制长时间的连接,这可能会影响WebSocket的可用性。

在项目中使用的小技巧

心跳

主要有三个优点

  1. 长时间空闲连接:在一些场景中,WebSocket连接可能会长时间处于空闲状态,没有数据传输。为了防止底层网络设备或中间代理等组件关闭空闲连接,使用心跳可以发送定期的消息来维持连接的活跃性。

  2. 断线重连:如果WebSocket连接断开,心跳可以作为检测连接断开的信号。当检测到连接断开时,客户端可以尝试重新连接服务器,以恢复通信。

  3. 节省资源和带宽:WebSocket心跳通常是一个简短的消息,与正常数据传输相比较小。这样可以减少不必要的数据传输和资源消耗,同时节省带宽和网络资源。

建立和断开连接的时机

场景:在app端使用的时候,页面打开时建立长连接,切出后台时(回到手机桌面/切到其他app)断开长连接。因为长连接一直开着,会浪费服务端性能。

uniapp举例:

建立连接

onShow(){
    ...doSth
}

断开连接:

onHide(){
    ...doSth
}

下面是白嫖党的福音,笔者的项目源码区域。不想慢慢看文章内容的,可以直接收藏吃灰。

⚡一文搞懂实时通信,建议来看看,WebSocket和SSE都有

如何在项目中使用websocket

主要介绍在uniapp和原生H5中的具体使用方法

1, uniapp中

封装一个websocket.js

// 心跳间隔、重连websocket间隔,5秒
import store from '@/store'
import constants from '@/configs/constants'

const interval = 5000
// 重连最大次数
const maxReconnectMaxTime = 5

export default class WS {
  constructor(options) {
    // 配置
    this.options = options
    // WS实例
    this.socketTask = null

    // 正常关闭
    this.normalCloseFlag = false
    // 重新连接次数
    this.reconnectTime = 1
    // 重新连接Timer
    this.reconnectTimer = null
    // 心跳Timer
    this.heartTimer = null

    // 发起连接
    this.initWS()

    // 关闭WS
    this.close = () => new Promise((resolve) => {
      // 正常关闭状态
      this.normalCloseFlag = true
      // 关闭websocket
      this.socketTask.close()
      // 关闭心跳定时器
      clearInterval(this.heartTimer)
      // 关闭重连定时器
      clearTimeout(this.reconnectTimer)

      resolve(true)
    })
  }

  initWS() {
    const url = store.getters.envUrl || ''
    this.socketTask = uni.connectSocket({
      url,
      protocols: [store.getters.token],
      success() { }
    })
    // 监听WS
    this.watchWS()
  }

  watchWS() {
    // 监听 WebSocket 连接打开事件
    this.socketTask.onOpen(() => {
      console.log('websocket连接成功!')
      // 连接成功
      this.options.onConnected()
      // 重置连接次数
      this.reconnectTime = 1
      // 发送心跳
      this.onHeartBeat()
      // 监听消息
      this.onMessage()
      // 关闭Toast
      uni.hideLoading()
    })

    // 监听websocket 错误
    this.socketTask.onError(() => {
      // 关闭并重连
      this.socketTask.close()
    })

    // 监听 WebSocket 连接关闭事件
    this.socketTask.onClose((res) => {
      console.log('websocket连接关闭')
      // 连接错误,发起重连接
      if (!this.normalCloseFlag) {
        this.onDisconnected(res)
      }
    })
  }

  // 监听消息
  onMessage() {
    // 监听websocket 收到消息
    this.socketTask.onMessage((res) => {
      // 收到消息
      if (res.data) {
        this.options.onMessage(res)
      } else {
        console.log('未监听到消息:原因:', JSON.stringify(res))
      }
    })
  }

  // 断开连接
  onDisconnected(res) {
    console.log('websocket断开连接,原因:', JSON.stringify(res))
    // 关闭心跳
    clearInterval(this.heartTimer)
    // 全局Toast提示,防止用户继续发送
    uni.showLoading({ title: '消息收取中…' })
    // 尝试重新连接
    this.onReconnect()
  }

  // 断线重连
  onReconnect() {
    clearTimeout(this.reconnectTimer)
    if (this.reconnectTime < maxReconnectMaxTime) {
      this.reconnectTimer = setTimeout(() => {
        console.log(`第【${this.reconnectTime}】次重新连接中……`)
        this.initWS()
        this.reconnectTime++
      }, interval)
    } else {
      uni.hideLoading()
      uni.showModal({
        title: '温馨提示',
        content: '服务器开小差啦~',
        showCancel: false,
        confirmText: '我知道了',
        success: () => {
          console.log('关闭弹窗')
        }
      })
    }
  }

  /** @心跳 * */
  onHeartBeat() {
    this.heartTimer = setInterval(() => {
      this.socketTask.send({
        data: 'heart:测试ing',
        success() {
          console.log('心跳发送成功!')
        }
      })
    }, interval)
  }
}

项目中的使用方法(就是vue2写法)
import WS from './websocket'

let ws = null
export default {
    onShow() {
      this.connectSocket()
      // 正常获取你页面里的数据
      this.getData()
    },
    onHide() {
      this.closeSocket()
      this.closeSessionDec()
    },
    methods:{
     closeSocket() {
        if (!this.$store.getters.token) return
        if (!ws) return
        return ws.close()
      },
      // 开始建立连接
      connectSocket() {
        if (!this.$store.getters.token) return
        ws = new WS({
          // 连接websocket所需参数
          data: {},
          // 监听接收到服务器消息
          onMessage: (e) => {
            console.log('收到消息', e)
            // 监听服务端推送给你的新数据,然后更新到页面上
            // ... doSth
          }
        })
      },
    }

}

要点解析:

1, protocols

protocolsuniapp给我们提供的,它主要的目的是为了提供token给后端,去鉴权

因为笔者实际开发了uniapp转的APP端APP端内嵌的H5端,这里的protocols也是为了两端统一,后端写一套逻辑就行啦。

this.socketTask = uni.connectSocket({ url, protocols: [store.getters.token], success() { } })

2, 回调

封装基本离不开回调

上文new WS({ onMessage:()=>{} })中,在实例化WS类时,传入了一个onMessage函数,目的是为了在监听到服务端推送给客户端的最新消息。

用回调的写法,可以在websocket.js中通过this.options.onMessage(res)把最新消息传递给你任意的vue页面,便于vue页面里的业务开发。

3, 生命周期

此处的生命周期涉及到性能优化方面的问题。

做移动端的长连接,肯定离不开是否切出后台的业务场景。

比如你在逛淘宝的时候,来了一条微信。那么你打开微信页面时,淘宝其实就算切出了后台,因为淘宝已经不在你的可视区域了。

onShow生命周期可以判断你当前的页面是否在可视区域,这时候建立websocket连接

onHide生命周期可以判断你当前的页面是否脱离了可视区域,这时候断开websocket连接

如果你在切出后台却不断开长连接,那么你的长连接是会一直在连接状态的,这里很消耗性能

4, 我想让关闭socket变为同步行为,可以吗?

可以,上文的ws.close()封装了一层,返回的是promise

可以通过async + await变为同步。比如,把tab切换的时候,关闭长连接再重新调一个其他接口。

async switchTab(){
  await ws.close()
  getData()
} 

2, 原生H5页面中

封装一个websocket.js

// 心跳间隔、重连websocket间隔,5秒
const interval = 5000
// 重连最大次数
const maxReconnectMaxTime = 5

function SocketTask(ws) {
  this.ws = ws
  this.close = () => {
    this.ws.close()
  }
  this.onopen = (fn) => {
    this.ws.onopen = fn
  }
  this.onerror = (fn) => {
    this.ws.onerror = fn
  }
  this.onclose = (fn) => {
    this.ws.onclose = fn
  }
  this.onmessage = (fn) => {
    this.ws.onmessage = fn
  }
  this.send = (data) => {
    this.ws.send(data)
  }
}
import store from '@/store'
import jsb from '@/utils/webview-jsbridge'
export default class WS {
  constructor(options) {
    // 配置
    this.options = options
    // WS实例
    this.socketTask = null

    // 正常关闭
    this.normalCloseFlag = false
    // 重新连接次数
    this.reconnectTime = 1
    // 重新连接Timer
    this.reconnectTimer = null
    // 心跳Timer
    this.heartTimer = null

    // 发起连接
    this.initWS()

    // 关闭WS
    this.close = () => new Promise((resolve) => {
      // 正常关闭状态
      this.normalCloseFlag = true
      // 关闭websocket
      this.socketTask.close()
      // 关闭心跳定时器
      clearInterval(this.heartTimer)
      // 关闭重连定时器
      clearTimeout(this.reconnectTimer)

      resolve(true)
    })
  }

  async initWS() {
    let env = ''
    let url = ''
    await uni.getEnv(function (res) {
      console.log('当前环境:' + JSON.stringify(res))
      env = JSON.stringify(res)
    })
    if (env.includes('h5')) {
      url = 'wss://你的域名/api/webSocket/1'
    } else {
      let res = await jsb.getAppEnvUrl()
      url = res.envUrl
    }

    console.log('看这里看这里url:', url)
    // this.options.data 连接websocket所需参数

    let ws = new WebSocket(url, [store.getters['user/token']])
    this.socketTask = new SocketTask(ws)
    // 监听WS
    this.watchWS()
  }

  watchWS() {
    // 监听 WebSocket 连接打开事件
    this.socketTask.onopen(() => {
      console.log('websocket连接成功!')
      // 连接成功
      this.options.onConnected()
      // 重置连接次数
      this.reconnectTime = 1
      // 发送心跳
      this.onHeartBeat()
      // 监听消息
      this.onMessage()
      // 关闭Toast
      // uni.hideLoading()
    })

    // 监听websocket 错误
    this.socketTask.onerror(() => {
      // 关闭并重连
      this.socketTask.close()
    })

    // 监听 WebSocket 连接关闭事件
    this.socketTask.onclose((res) => {
      console.log('websocket连接关闭')
      // 连接错误,发起重连接
      if (!this.normalCloseFlag) {
        this.onDisconnected(res)
      }
    })
  }

  // 监听消息
  onMessage() {
    // 监听websocket 收到消息
    this.socketTask.onmessage((res) => {
      // 收到消息
      if (res.data) {
        try {
          let data = JSON.parse(res.data)
          this.options.onMessage({
            ...data,
            data: data.heart ? data : JSON.parse(data.data),
          })
        } catch (error) {
          console.log(error)
        }
      } else {
        console.log('未监听到消息:原因:', res)
      }
    })
  }

  // 断开连接
  onDisconnected(res) {
    console.log('websocket断开连接,原因:', JSON.stringify(res))
    // 关闭心跳
    clearInterval(this.heartTimer)
    // 全局Toast提示,防止用户继续发送
    // uni.showLoading({ title: '消息收取中…' })
    // 尝试重新连接
    this.onReconnect()
  }

  // 断线重连
  onReconnect() {
    clearTimeout(this.reconnectTimer)
    if (this.reconnectTime < maxReconnectMaxTime) {
      this.reconnectTimer = setTimeout(() => {
        console.log(`第【${this.reconnectTime}】次重新连接中……`)
        this.initWS()
        this.reconnectTime++
      }, interval)
    } else {
    }
  }

  /** @心跳 * */
  onHeartBeat() {
    this.heartTimer = setInterval(() => {
      this.socketTask.send(`heart:${this.options.data.userId}`)
    }, interval)
  }
}

在原生H5里的使用

在H5里的使用和上面uniapp项目中的使用方法里中的方式大同小异,就不过多赘述啦

要点解析:

1,SocketTask函数

SocketTask函数是我为了和uniapp端的js写法保持统一,手戳的一个中转站~~~

小伙伴们直接用就行

2,websocket.js里 在也有个new WebSocket?

此处的WebSocket是浏览器环境提供的,上文有提到。而你的vue页里的new websocket其实是引用了一个类,这个类是websocket.js本身。

H5端的这段代码

new WebSocket(url, [store.getters['user/token']])

等于 uniapp的这段代码

uni.connectSocket({ url, protocols: [store.getters.token], success() { } })

3,jsb是啥玩意?

我上文有介绍,这里业务场景是uniapp端有一个长连接,通过webview内嵌的h5页面也有长连接。

jsb是jsbridge的意思,实现APP与H5之间通信的技术,允许原生代码和H5页面之间相互调用和传递数据

说白了 这里就是通过uniapp自带的@messageuni.postMessage,然后封装了一波。小伙伴要是没这个业务,就忽略它。

本文的重点是websocket长连接

// APP端
<web-view ref="webview" class="webview" @message="handlePostMessage"></web-view>
handlePostMessage(data){
   // data是H5传递过来的数据
}

// H5端
uni.postMessage({
      data: {
        action,
        payload,
      },
    })

4,重点说下H5中怎么自己实现uniapp里的onShowonHide生命周期

使用visibilitychange, 简单来说onShowonHide本质上就是监听页面是否在可视区域里。

onMounted(() => {
    init()
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'visible') {
        console.log('浏览器的当前页签onShow时,do something')
        init()
      } else {
        console.log('浏览器的当前页签onHide时,do something')
        // 切出后台
        closeSocket()
      }
    })
  })
  onBeforeUnmount(() => {
    // 页面卸载-针对路由切换
    closeSocket()
    document.removeEventListener('visibilitychange', () => {})
  })

完结

这篇文章本该发出来啦,只是一直没时间搞,都用来做其他的事了。这里把开发过程中的笔记和遇到的一些知识点整理了一下,希望对小伙伴有帮助。

websocket开发实时通信,项目提测后给我的感觉就是

卧槽,听着好牛逼的技术 -> 嗯?这么简单 -> 还是有点细节的 -> 学到的,常规操作吧

欢迎转载,但请注明来源。 最后,希望小伙伴们给我个免费的点赞,祝大家心想事成,平安喜乐。

⚡一文搞懂实时通信,建议来看看,WebSocket和SSE都有