猫耳 WebSocket 跨端优化实践

news2024/12/26 11:57:36

前言

在现代的移动应用程序中,长连接是一种不可或缺的能力,包括但不限于推送、实时通信、信令控制等常见场景。在猫耳FM的直播业务中,我们同样使用了 WebSocket 长连接作为我们实时通信的基础。

在我们推进用户体验优化的工作中,其中用户成功进入直播间的时间是我们优化的一个重点指标,其包含了房间信息接口的调用、长连接的建立、播放器拉流的首帧等。本文主要介绍我们在 WebSocket 长连接跨端统一和体验优化的思路和方案。

这里我们先简单介绍下 WebSocket,以及为什么我们选择了 WebSocket 而不是其他的协议作为我们持续迭代的方向。

WebSocket 是一种在 Web 应用程序和服务器之间建立持久、双向通信连接的通信协议。它允许客户端和服务器之间进行实时数据传输,而无需客户端不断地发起 HTTP 请求,为开发者提供了丰富的实时应用开发可能性。从最早猫耳 2016 年开始调研和迭代直播的业务,WebSocket 已经是一种相当成熟的方案,可以同时在 Web 和移动应用程序中使用。早在 2011 年 12 月 WebSocket 的协议标准被定稿 [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455),从 2012 年开始 WebSocket 逐渐被各大浏览器支持,包括当时的 Internet Explorer、Safari、Mozilla Firefox 和 Google Chrome 等。同时作为更早期诞生的 Socket.IO 为了兼容更早期的浏览器,在我们的场景下很多做法于这个时间点上就显得相当臃肿和没必要了。当时使用 WebSocket 的一个重点考虑我们还是可以同时在 Web 上直接使用,如果我们在客户端上引入其他的协议,在迭代过程中,我们就不得不考虑同时兼容和支持多个实时通信的通道,为了降低最初直播方案的复杂性,我们选择使用了 WebSocket 协议作为我们直播业务实时通信、信令传递的基础协议。

时间来到 2023 年前后,随着互联网技术的发展,我们当前又有了更多的选择作为实时消息传递的基础消息通道,类似 MQTT、gRPC 等上层协议其实都可以作为一个可靠的传递消息的方式,并且被大量的用户所验证。同时,在 Web 技术上也有类似 SSE、WebRTC、WebTransport 等方案可以作为我们上层消息传输的机制。作为一个普遍的考虑,这里我们的选择一定是一种“高级”协议,如果直接使用 TCP、UDP 等传输层协议,不可避免的我们要考虑更复杂的消息拆分、可靠性保证和安全等问题,这里我们秉持着多端统一、提高研发效率、减少维护成本的考虑,我们最后的选择尽可能还是一种相对成熟和可靠的方案,在这样的前提下,MQTT、gRPC 由于在 Web 上支持不佳或是本身就通过 WebSocket 包装的,我们就暂时不做深入讨论。同时,一些较新的 Web 方案,对比之下类似 SSE 是单向传输、WebRTC 更适合实时音视频通信的场景且不太能被 CDN 加速支持、WebTransport 太过于新,Safari 到现在为止还不支持,同时在消息顺序上需要外部进行额外保证,这样看下来 WebSocket 仍是我们当前最好的选择。

技术方案

在考虑优化之前,由于我们之前在 Android 和 iOS 客户端上也是使用的是 WebSocket (wss://),首先需要明确的就是当前有什么问题。

根据我们的埋点的数据,在我们直连国内 BGP 线路服务器建连的情况下,完成 WebSocket 握手的时长(90 分位,下文同)为 500ms ~ 600ms,DNS + TCP + TLS + HTTP Upgrade,相比于我们一次正常的 https 请求取得响应的时间明显慢了很多,这个也是我们优化的一个最终目标,由于 WebSocket 在 HTTP/1.1 上不可避免的需要重新建立 HTTP 连接并进行一次 Upgrade,目前可以做的优化相对有限,可能在 DNS 和 TLS 过程中可以有一些优化。其实我们早期还尝试通过订阅、取消订阅指令来直接复用同一个连接,但后来为了方便做负载均衡以及高可用性等原因线上已经不再支持这块逻辑。

要做好这次优化,我们这边有几点考虑:

  1. 统一多端代码,在日志中能输出有效和明确的错误信息

  2. 支持自定义 DNS 过程,包括 httpdns、DNS 缓存的策略和 ipv6 的策略等

  3. 支持 http proxy,方便测试调试

  4. 后续迭代方便,可以持续跟进支持 WebSocket 协议标准

在考虑多端统一并且成熟可靠的选择不多,我们主要对比下我们使用过的一些可以跨端的方案,这里主要提使用过的原因是我们能大致理解其核心的逻辑并作出一定的修改:

  • [websocketpp](https://github.com/zaphoyd/websocketpp)一个支持 client/server 的库,我们主要用于 PC 上建立 server 和 Web 进行本地交互

  • [libwebsockets](https://github.com/warmcat/libwebsockets)在用户连麦等场景中曾作为和服务端的进行信令传递的方案

  • [cronet](https://chromium.googlesource.com/chromium/src/+/HEAD/components/cronet/)来自 Chromium 的网络栈,基本上可以认为是 Google Chrome 浏览器中负责网络通信部分的上层封装

    这里特别提到 cronet 的原因是它虽然不直接支持 WebSocket,但是其代码库完整包含了 WebSocket 协议的实现,同时也是目前我们在客户端上已经使用中的网络库的底层实现

由于 websocketpp 已经很久没更新了,我们这里主要对比下我们目前 libwebsockets 和 cronet 实现的方案要考虑的一些问题:

主要代码语言DNS系统 Wi-Fi http proxy 设置接入成本持续迭代
libwebsocketsC目前仅支持简单的策略,需要修改代码进行完整的 DNS 逻辑控制外部控制设置Android 上需要单独实现 JNI 层,支持 Java/Kotlin 中调用。同时自定义 DNS 过程都在持续迭代中,支持较新的标准
cronetC++ / iOS Objective-C / Android Java JNI已修改,并在线上稳定运行
支持国内的各种商用 httpdns 服务、海外 Google DoH 等
内部集成支持需要实现 cronet 适配层代码,代码生成相关接入层代码

通过简单的对比,其实我们更倾向使用 cronet 作为持续后续迭代的方案,不仅更适合我们在移动端中集成,且有足够的经验来优化它。同时,在 Android 上我们将逻辑的代码合入 native 层中,还能进一步优化网络 IO、TLS 等关键性能。

这里特别要提到我们之前使用的 cronet 方案是来自B站移动端基础架构优化过的修改版本,在一些特定的场景给予了我们业务更高的自由度,比如请求优先级等。常见的在 iOS 上原生支持自定义 DNS 过程其实都是一个比较取巧的方案,特别是在处理 https 请求的一些过程上,我们是用了 cronet 之后,才考虑 iOS 上也支持这块。由于这里我们是使用的修改版 cronet 方案,这些问题已经都被优化的足够好了,我们仅需要考虑如何启用其中 WebSocket 的部分并提供给客户端使用。另外需要考虑的一点是 Android 上的 cronet 实际上它的 Java 层的初始化的配置和 native 层中使用的 Cronet C API 接口实际上不能直接兼容,这块其实是在更早猫耳 Android 落地播放器使用 cronet 的作为 http 传输方案的时候已经得到了解决,并用于实验不同协议的播放效果,参见 [猫耳 Android 播放框架开发实践],而我们 iOS 和 PC 上直接使用的就是 Cronet C API 的包装,这样保证了多端全局的配置、埋点信息都是一致的且最大化了网络的性能和端上可观测的能力。

确定了方向,我们其实要做的事情也很简单,因为在 Chromium 中 WebSocket 在某种程度上也是基于 http 已有过程的一种延伸,之前很多已有的优化方案可以直接对 WebSocket 生效,包括自定义 DNS 过程等。

这里我们仅将 `net/websockets` 中相关逻辑对客户端进行了适配:

  • 支持主动发送 ping 包。Web 标准中的 WebSocket 实现实际上没有 ping 的动作,只能被动响应服务端的 ping 包

  • 移除 Origin 的请求头和相关校验。Web 标准中的 WebSocket 相对于普通的 http 请求有一些源站策略的差别,这里我们在客户端和 native 中实际上使用不到

增加 cronet 适配层代码,这里简单贴下 native 接口 idl,Android 中 jni 的适配也是依样画葫芦:

// Counterpart of UrlRequestCallback for websocket.
[Abstract]
interface WebSocketCallback {
  /**
   * The message type invoked by {@code OnMessage()}.
   * 目前只能收到 text 或者 binary 两种. continuation 已被合并处理.
   */
  enum MESSAGE_TYPE {
    CONTINUATION = 0,
    TEXT = 1,
    BINARY = 2,
  };

  OnAddChannelResponse(WebSocket request, UrlResponseInfo info, string extensions);
  OnMessage(WebSocket request, MESSAGE_TYPE type, Buffer buffer);
  OnDropChannel(WebSocket request, bool was_clean, uint16 code, string reason);
  OnFailed(WebSocket request, Error error);
  OnCanceled(WebSocket request);
};

/**
 * Controls an Websocket request.
 * Initialized by InitWithParams().
 * Note: All methods must be called on the Executor passed to InitWithParams().
 */
interface WebSocket {

  // see https://www.iana.org/assignments/websocket/websocket.xhtml#opcode.
  enum OPCODE {
    CONTINUATION_FRAME = 0,
    TEXT_FRAME = 1,
    BINARY_FRAME = 2,
    CONNECTION_CLOSE_FRAME = 8,
    PING_FRAME = 9,
    PONG_FRAME = 10,
  };

  [Sync]
  InitWithParams(Engine engine,
                 string url,
                 WebSocketParams params,
                 WebSocketCallback callback,
                 Executor executor) => (RESULT result);

  [Sync]
  Start() => (RESULT result);

  [Sync]
  ReadFrames() => (RESULT result);

  [Sync]
  SendFrame(bool fin, OPCODE op_code, Buffer buffer) => (RESULT result);

  [Sync]
  Send(OPCODE op_code, Buffer buffer) => (RESULT result);

  [Sync]
  Close(uint16 code, string reason) => (RESULT result);

  Cancel();

  [Sync]
  IsDone() => (bool done);
};

// 请求参数.
struct WebSocketParams {
  /**
   * Array of HTTP headers for this request.
   */
  array<HttpHeader> request_headers;
};

其中大部分的通用的参数都可以复用已有的 CronetURLRequest 的部分。

整体的架构和调用时机大致是这样的,关系图中有标星的位置是我们这次进行新加或调整的地方:

图片

调用时机:

图片

同时,我们对 WebSocket 的消息在内部进行了处理,由于协议支持消息是分片传输,为了简化业务上消息处理的过程,将消息做了合并,最终一起回调给上层业务处理。

最后在端上再进行一次封装,已有的重试、心跳等机制这里就不进行讨论了,我们将端上业务中的和 native 代码中 libwebsockets 的 WebSocket 都换成了 cronet 的实现,最终上线后收获了非常可观的收益:

在建连速度方面,Android 优化了 ~150ms,iOS 优化了 ~250ms,其中我们 Android 端上早在之前就已经落地通过 httpdns 的方式(包括 DNS 的缓存策略的优化等)优化 okhttp 建连的过程,iOS 之前的 WebSocket 实现 [SocketRocket](https://github.com/facebookincubator/SocketRocket) 依赖系统的 DNS,建连时长相比于 Android 会慢 50ms+,现在也和普通 HTTP 请求统一了 DNS 的策略,收益会更明显点,符合我们的预期,最终也是补完了我们端上业务中最后一块不支持自定义 DNS 过程的缺口。在失败率方面,也和之前基本上一致,且上报上来的错误信息更具有可读性,后续进行分析和监控都将更加清晰。下图展示了不同版本间客户端 WebSocket 连接失败的错误信息,这里我们从 6.1.0 版本开始切换到 cronet 的实现,可以看到新版本的信息更加直观和明确,极大地方便了研发人员定位具体问题。

图片

随着 WebSocket 的实现切换为 cronet,在我们客户端本身的网络诊断中也可以更加完整和准确反馈实际的 DNS 结果、错误信息和网络质量等信息。

未来展望

前阵子 Node.js 22 的发布,其原生支持的 WebSocket 客户端也旨在提供一个更加标准化的接口和使用的方式,说明了当前 WebSocket 仍能作为一种足够优秀的方案被广大开发者所认可。

值得一提的是,随着互联网技术的发展,WebSocket 也不是一成不变,WebSocket over HTTP/2 [RFC 8441](https://www.rfc-editor.org/rfc/rfc8441)和 WebSocket over HTTP/3 [RFC 9220](https://www.rfc-editor.org/rfc/rfc9220.html) 也被相继定稿,其中 WebSocket over HTTP/2 也开始逐渐被各大浏览器、网关、开源库支持,相对于 HTTP/1.1 中的 Upgrade 机制,在 HTTP/2 和 HTTP/3 中 WebSocket 的握手有了相当程度的简化,换句话说就是可以更快。目前由于支持 WebSocket over HTTP/2 的服务端组件还不够广泛,暂时没有在用户侧进行验证,不过我们接入的 cronet 版本已经支持,仅需要先有一次 HTTP/2 的连接,确认服务端支持后就可以正常启用,在我们的测试环境中也可以观测到更好的效果。除此之外,由于我们使用的修改后的 cronet 也支持配置下发域名通配符等方式强制使用 QUIC 通过 UDP 进行传输而无需与普通 TCP 的 HTTP 请求进行竞速,目前仅用于在播放和下载场景中做一些实验,后续也有望在 WebSocket over HTTP/3 的演进中得到更可观收益。

-End-

作者丨腾袭、三七、nazimai、叔于田、阿司、浩哥

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1709684.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

利用AI办公工具类API,大幅提高办公效率

AI办公工具类API是一项革命性的技术&#xff0c;利用人工智能的力量为办公场景提供了许多创新的解决方案。借助AI办公工具类API&#xff0c;用户可以实现自动化的文档处理、语音转文字、图像识别、数据分析等多种功能&#xff0c;大大提高了办公效率和工作质量。此外&#xff0…

LiveGBS流媒体平台GB/T28181用户手册-国标级联:添加上级平台、选择通道、推送通道级联会话、搜索、删除

LiveGBS流媒体平台GB/T28181用户手册-国标级联:添加上级平台、选择通道、推送通道级联会话、搜索、删除 1、国标级联1.1、添加上级平台1.2、注册状态1.3、选择通道1.4、推送通道1.5、级联会话1.6、搜索1.7、删除 2、搭建GB28181视频直播平台 1、国标级联 1.1、添加上级平台 点…

【golang学习之旅】go mod tidy

系列文章 【golang学习之旅】报错&#xff1a;a declared but not used 【golang学习之旅】Go 的基本数据类型 【golang学习之旅】深入理解字符串string数据类型 目录 系列文章go mod tidy的作用 go mod tidy的作用 把项目所依赖的包添加到go.mod文件中去掉go.mod文件中项目不…

使用 RT 矩阵进行 3D 点云变换详解(基于 PCL 和 Eigen 库)

在 3D 点云处理中&#xff0c;RT 矩阵是一个常用的工具&#xff0c;用于对点云进行旋转和平移操作。本文将详细介绍 RT 矩阵的概念&#xff0c;并通过一个示例程序演示如何基于 PCL 和 Eigen 库将一帧点云进行矩阵变换再输出。 本教程的示例代码和点云数据可在 GitHub 下载。 什…

告别裸奔,聊聊主流消息队列的认证和鉴权!

大家好&#xff0c;我是君哥。 我们在使用消息队列时&#xff0c;经常关注的是消息队列收发消息的功能。但好多时候需要对客户端有一定的限制&#xff0c;比如只有持有令牌的客户端才能访问集权&#xff0c;不允许 Producer 发送消息到某一个 Topic&#xff0c;或者某一个 Top…

网工必备的几种远程工具,教你使用

号主&#xff1a;老杨丨11年资深网络工程师&#xff0c;更多网工提升干货&#xff0c;请关注公众号&#xff1a;网络工程师俱乐部 下午好&#xff0c;我的网工朋友。 干网工这行&#xff0c;工具是必备的&#xff0c;不会用工具赋能工作的网工不是好网工&#xff01; 拥有一套…

Matplotlib 实践指南:图形样式、风格与标记探索

目录 前言 第一点&#xff1a;导入模块 第二点&#xff1a;创建二维图 第三点&#xff1a;创建统计图 总结 前言 Matplotlib 是一个强大的数据可视化库&#xff0c;可用于创建各种类型的图形。在本文中&#xff0c;我们将研究如何在 Matplotlib 中设置图形的颜色、风格和标记…

深入了解Nginx(一):Nginx核心原理

一、Nginx核心原理 本节为大家介绍Nginx的核心原理,包含Reactor模型、Nginx的模块化设计、Nginx的请求处理阶段. &#xff08;本文源自微博客,且已获得授权&#xff09; 1.1、Reactor模型 Nginx对高并发IO的处理使用了Reactor事件驱动模型。Reactor模型的基本组件包含时间收集…

redis数据类型set,zset

华子目录 Set结构图相关命令sdiff key1 [key2]sdiffstore destination key1 [key2...]sinter key1 [key2...]sinterstore destination key1 [key2...]sunion key1 [key2...]sunionstore destination key1 [key2...]smove source destination memberspop key [count]sscan key c…

51驱动DY-SV20F语音播放模块

51驱动DY-SV20F语音播放模块 简介模块特征电气参数工作模式配置原理图代码结果图 简介 DY-SV20F 是一款一对一分段触发控制播放器&#xff0c;支持 MP3,WAV 解码格式&#xff1b; 可分段触发 9 首曲目&#xff1b;低电平触发&#xff1b;3.7-5VDC 宽电压供电&#xff0c;直驱 …

【安装笔记-20240520-Windows-自定义 WSL2 安装位置】

安装笔记-系列文章目录 安装笔记-20240520-Windows-自定义 WSL2 安装位置 文章目录 安装笔记-系列文章目录安装笔记-20240520-Windows-自定义 WSL2 安装位置 前言一、软件介绍名称&#xff1a;WSL&#xff08;适用于 Linux 的 Windows 子系统&#xff09;主页官方介绍 二、安装…

Java面试八股之有哪些线程安全的集合类

Java中有哪些线程安全的集合类 在Java中&#xff0c;并非所有的集合类都是线程安全的&#xff0c;但在多线程环境下&#xff0c;确保集合操作的线程安全性至关重要。以下是几个典型的线程安全集合类&#xff1a; Vector: 类似于ArrayList&#xff0c;但它是线程安全的。它通过…

快团团帮卖团长如何修改供货大团长复制帮卖团的信息?

一、功能说明 在复制帮卖团中&#xff0c;帮卖团长可以选择&#xff1a;①修改团购内容 ②同步大团长的团购内容 二、具体操作步骤 点击“编辑后帮卖”&#xff0c;在团购设置中设置开启/关闭“同步大团长内容” 开启“同步大团长内容”后&#xff0c;大团长修改图文后&#xf…

微信小程序毕业设计-校园综合服务系统项目开发实战(附源码+论文)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;微信小程序毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计…

Incogniton流覽器使用代理詳細教程

作為一款用於多帳戶管理的反檢測流覽器&#xff0c;Incogniton可以與Mac和Windows系統相容並且試用體驗良好。這篇入門級教程會幫你瞭解如何在Incogniton中使用代理。運用Incogniton&#xff0c;你可以通過虛擬流覽器配置檔代替多臺電腦&#xff0c;同時確保數據安全和私密。這…

【JAVA |图书管理系统】JAVA实现图书管理系(附完整代码)

✨✨谢谢大家捧场&#xff0c;祝屏幕前的小伙伴们每天都有好运相伴左右&#xff0c;一定要天天开心哦&#xff01;✨✨ &#x1f388;&#x1f388;作者主页&#xff1a; &#x1f388;丠丠64-CSDN博客&#x1f388; ✨✨ 帅哥美女们&#xff0c;我们共同加油&#xff01;一起…

天津企业采购云管平台需要考虑哪些?选择哪家好?

随着天津上云企业的增加&#xff0c;云管理需求也逐步增加。因此采购云管平台是非常必要的。那天津企业采购云管平台需要考虑哪些&#xff1f;选择哪家好&#xff1f; 天津企业采购云管平台需要考虑哪些&#xff1f; 【回答】&#xff1a;天津企业采购云管平台需要考虑的因素比…

xcode依赖包package已经安装,但是提示No such module ‘Alamofire‘解决办法

明明已经通过xcode自带的swift包管理器安装好了依赖包&#xff0c;但是却还是提示&#xff1a;No such module&#xff0c;这个坑爹的xcode&#xff0c;我也只能说服气&#xff0c;但是无奈&#xff0c;没办法攻打苹果总部&#xff0c;只能自己想解决办法了 No such module Ala…

企业融资新渠道:一文详解动产抵押

在当今瞬息万变的商业环境中&#xff0c;资金是企业发展的血液。面对融资难题&#xff0c;动产抵押作为一种灵活高效的融资方式&#xff0c;越来越受到企业的青睐。本文将为您全面解析动产抵押的概念、流程、优势及注意事项&#xff0c;助力您的企业解锁融资新途径。 什么是动…

西储大学数据集学习

数据集下载地址&#xff1a;CWRU凯斯西储大学轴承数据数据集——附&#xff1a;下载链接_西储大学轴承数据集下载-CSDN博客 最近研究故障诊断&#xff0c;先对使用比较多的西储大学数据集研究。以资料【1】中的内容展开研究。 1、轴承的结构 轴承分为外圈、内圈、保持架和滚珠…