热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

你可能不知道的浏览器实时通信方案

本文主要探讨现阶段浏览器端可行的实时通信方案,以及它们的发展历史。这里以sockjs作为切入点,这是一个流行的浏览器实时通信库,提供了’类Websocket’、一致性、跨平台的AP

本文主要探讨现阶段浏览器端可行的实时通信方案,以及它们的发展历史。

这里以sockjs作为切入点,这是一个流行的浏览器实时通信库,提供了’类Websocket’、一致性、跨平台的API,旨在浏览器和服务器之间创建一个低延迟、全双工、支持跨域的实时通信信道. 主要特点就是仿生Websocket,它会优先使用Websocket作为传输层,在不支持WebSocket的环境回退使用其他解决方案,例如XHR-Stream、轮询.

所以sockjs本身就是浏览器实时通信方案的编年史, 本文也是按照由新到老这样的顺序来介绍这些解决方案.

类似sockjs的解决方案还有
socket.io

如果你觉得文章不错,请不要吝惜你的点赞👍,鼓励笔者写出更精彩的文章

目录

  • WebSocket
  • XHR-streaming
  • EventSource
  • HtmlFile
  • Polling
  • Long polling
  • 扩展

WebSocket

WebSocket其实不是本文的主角,而且网上已经有很多教程,本文的目的是介绍WebSocket之外的一些回退方案,在浏览器不支持Websocket的情况下, 可以选择回退到这些方案.

在此介绍Websocket之前,先来了解一些HTTP的基础知识,毕竟WebSocket本身是借用HTTP协议实现的。

HTTP协议是基于TCP/IP之上的应用层协议,也就是说HTTP在TCP连接中进行请求和响应的,浏览器会为每个请求建立一个TCP连接,请求等待服务端响应,在服务端响应后关闭连接:

《你可能不知道的浏览器实时通信方案》

后来人们发现为每个HTTP请求都建立一个TCP连接,太浪费资源了,能不能不要着急关闭TCP连接,而是将它复用起来, 在一个TCP连接中进行多次请求。

这就有了HTTP持久连接(HTTP persistent connection, 也称为HTTP keep-alive), 它利用同一个TCP连接来发送和接收多个HTTP请求/响应。持久连接的方式可以大大减少等待时间, 双方不需要重新运行TCP握手,这对前端静态资源的加载也有很大意义:

《你可能不知道的浏览器实时通信方案》

Ok, 现在回到WebSocket, 浏览器端用户程序并不支持和服务端直接建立TCP连接,但是上面我们看到每个HTTP请求都会建立TCP连接, TCP是可靠的、全双工的数据通信通道,那我们何不直接利用它来进行实时通信? 这就是Websocket的原理!

我们这里通过一张图,通俗地理解一下Websocket的原理:

《你可能不知道的浏览器实时通信方案》

通过上图可以看到,WebSocket除最初建立连接时需要借助于现有的HTTP协议,其他时候直接基于TCP完成通信。这是浏览器中最靠近套接字的API,可以实时和服务端进行全双工通信. WebSocket相比传统的浏览器的Comet)(下文介绍)技术, 有很多优势:

  • 更强的实时性。基于TCP协议的全双工通信
  • 更高效。一方面是数据包相对较小,另一方面相比传统XHR-Streaming和轮询方式更加高效,不需要重复建立TCP连接
  • 更好的二进制支持。 Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容
  • 保持连接状态。 相比HTTP无状态的协议,WebSocket只需要在建立连接时携带认证信息,后续的通信都在这个会话内进行
  • 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等

它的接口也非常简单:

const ws = new WebSocket('ws://localhost:8080/socket');
// 错误处理
ws.Onerror= (error) => { ... }
// 连接关闭
ws.Onclose= () => { ... }
// 连接建立
ws.Onopen= () => {
// 向服务端发送消息
ws.send("ping");
}
// 接收服务端发送的消息
ws.Onmessage= (msg) => {
if(msg.data instanceof Blob) {
// 处理二进制信息
processBlob(msg.data);
} else {
// 处理文本信息
processText(msg.data);
}
}

本文不会深入解析Websocket的协议细节,有兴趣的读者可以看下列文章:

  • WebSocket
  • WebSocket 浅析
  • 阮一峰:WebSocket 教程

如果不考虑低版本IE,基本上WebSocket不会有什么兼容性上面的顾虑. 下面列举了Websocket一些常见的问题, 当无法正常使用Websocket时,可以利用sockjs或者socket.io这些方案回退到传统的Comet技术方案.

  1. 浏览器兼容性。

    • IE10以下不支持
    • Safari 下不允许使用非标准接口建立连接
  2. 心跳. WebSocket本身不会维护心跳机制,一些Websocket实现在空闲一段时间会自动断开。所以sockjs这些库会帮你维护心跳
  3. 一些负载均衡或代理不支持Websocket。
  4. 会话和消息队列维护。这些不是Websocket协议的职责,而是应用的职责。sockjs会为每个Websocket连接维护一个会话,且这个会话里面会维护一个消息队列,当Websocket意外断开时,不至于丢失数据

XHR-streaming

XHR-Streming, 中文名称‘XHR流’, 这是WebSocket的最佳替补方案. XHR-streaming的原理也比较简单:服务端使用分块传输编码(Chunked transfer encoding)的HTTP传输机制进行响应,并且服务器端不终止HTTP响应流,让HTTP始终处于持久连接状态,当有数据需要发送给客户端时再进行写入数据

没理解?没关系,我们一步一步来, 先来看一下正常的HTTP请求处理是这样的:

// Node.js代码
const http = require('http')
const server = http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/plain', // 设置内容格式
'Content-Length': 11, // 设置内容长度
})
res.end('hello world') // 响应
})

客户端会立即接收到响应:

《你可能不知道的浏览器实时通信方案》

那么什么是分块传输编码呢?

在HTTP/1.0之前, 响应是必须作为一整块数据返回客户端的(如上例),这要求服务端在发送响应之前必须设置Content-Length, 浏览器知道数据的大小后才能确定响应的结束时间。这让服务器响应动态的内容变得非常低效,它必须等待所有动态内容生成完,再计算Content-Length, 才可以发送给客户端。如果响应的内容体积很大,需要占用很多内存空间.

HTTP/1.1引入了Transfer-Encoding: chunked;报头。 它允许服务器发送给客户端应用的数据可以分为多个部分, 并以一个或多个块发送,这样服务器可以发送数据而不需要提前计算发送内容的总大小

有了分块传输机制后,动态生成内容的服务器就可以维持HTTP长连接, 也就是说服务器响应流不结束,TCP连接就不会断开.

现在我们切换为分块传输编码模式, 且我们不终止响应流,看会有什么情况:

const http = require('http')
const server = http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/plain'
// 'Content-Length': 11, // 🔴将Content-Length报头去掉,Node.js默认就是使用分块编码传输的
})
res.write('hello world')
// res.end() // 🔴不终止输出流
})

我们会发现请求会一直处于Pending状态(绿色下载图标),除非出现异常、服务器关闭或显式关闭连接(比如设置超时机制),请求是永远不会终止的。但是即使处于Pending状态客户端还是可以接收数据,不必等待请求结束:

《你可能不知道的浏览器实时通信方案》

基于这个原理我们再来创建一个简单的ping-pong服务器:

const server = http.createServer((req, res) => {
if (req.url === '/ping') {
// ping请求
if (pendingRespOnse== null) {
res.writeHead(500);
res.write('session not found');
res.end();
return;
}
res.writeHead(200)
res.end()
// 给客户端推流
pendingResponse.write('pong\n');
} else {
// 保存句柄
res.writeHead(200, {
'Content-Type': 'text/plain',
});
res.write('welcome to ping\n');
pendingRespOnse= res
}
});

测试一下,在另一个窗口访问/ping路径:

《你可能不知道的浏览器实时通信方案》

Ok! 这就是XHR-Streaming!

那么Ajax怎么接收这些数据呢? ①一种做法是在XMLHttpRequestonreadystatechange事件处理器中判断readyState是否等于XMLHttpRequest.LOADING;②另外一种做法是在xhr.onprogress事件处理器中处理。下面是ping客户端实现:

function listen() {
const xhr = new XMLHttpRequest();
xhr.Onprogress= () => {
// 注意responseText是获取服务端发送的所有数据,如果要获取未读数据,则需要进行划分
console.log('progress', xhr.responseText);
}
xhr.open('POST', HOST);
xhr.send(null);
}
function ping() {
const xhr = new XMLHttpRequest();
xhr.open('POST', HOST + '/ping');
xhr.send(null);
}
listen();
setInterval(ping, 5000);

慢着,不要高兴得太早😰. 如果运行上面的代码会发现onprogress并没有被正常的触发, 具体原因笔者也没有深入研究,我发现sockjs的服务器源码里面会预先写入2049个字节,这样就可以正常触发onprogress事件了:

const server = http.createServer((req, res) => {
if (req.url === '/ping') {
// ping请求
// ...
} else {
// 保存句柄
res.writeHead(200, {
'Content-Type': 'text/plain',
});
res.write(Array(2049).join('h') + '\n');
pendingRespOnse= res
}
});

最后再图解一下XHR-streaming的原理:

《你可能不知道的浏览器实时通信方案》

总结一下XHR-Streaming的特点:

  • 利用分块传输编码机制实现持久化连接(persistent connection): 服务器不关闭响应流,连接就不会关闭
  • 单工(unidirectional): 只允许服务器向浏览器单向的推送数据

通过XHR-Streaming,可以允许服务端连续地发送消息,无需每次响应后再去建立一个连接, 所以它是除了Websocket之外最为高效的实时通信方案. 但它也并不是完美无缺

比如XHR-streaming连接的时间越长,浏览器会占用过多内存,而且在每一次新的数据到来时,需要对消息进行划分,剔除掉已经接收的数据. 因此sockjs对它进行了一点优化, 例如sockjs默认只允许每个xhr-streaming连接输出128kb数据,超过这个大小时会关闭输出流,让浏览器重新发起请求.

EventSource

了解了XHR-Streaming, 就会觉得EventSource并不是什么新鲜玩意: 它就是上面讲的XHR-streaming, 只不过浏览器给它提供了标准的API封装和协议, 你抓包一看和XHR-streaming没有太大的区别:

《你可能不知道的浏览器实时通信方案》

上面可以看到请求的Accepttext/event-stream, 且服务端写入的数据都有标准的约定, 即载荷需要这样组织:

const data = `data: ${payload}\r\n\r\n`

EventSource的API和Websocket类似, 实例:

const evtSource = new EventSource('sse.php');
// 连接打开
evtSource.Onopen= () => {}
// 接受消息
evtSource.Onmessage= function(e) {
// do something
// ...
console.log("message: " + e.data)
// 关闭流
evtSource.close()
}
// 异常
evtSource.Onerror= () => {}

因为是标准的,浏览器调试也比较方便,不需要借助第三方抓包工具:

《你可能不知道的浏览器实时通信方案》

HtmlFile

这是一种古老的‘秘术’😂,虽然我们可能永远都不会再用到它,但是它的实现方式比较有意思(类似于JSONP这种黑科技), 所以还是值得讲一下。

HtmlFile的另一个名字叫做永久帧(forever-frame), 顾名思义, 浏览器会打开一个隐藏的iframe,这个iframe会请求一个分块传输编码的html文件(Transfer-Encoding: chunked), 和XHR-Streaming一样,这个请求永远都不会结束,服务器会不断在这个文档上输出内容。这里面的要点是现代浏览器都会增量渲染html文件,所以服务器可以通过添加script标签在客户端执行某些代码,先来看个抓包的实例:

《你可能不知道的浏览器实时通信方案》

从上图可以看出:

  • ① 这里会给服务器传递一个callback,通过这个callback将数据传递给父文档
  • ② 服务器每当有新的数据,就向文档追加一个标签,script的代码就是将数据传递给callback。利用浏览器会被下载边解析HTML文档的特性,新增的script会马上被执行

最后还是用流程图描述一下:

《你可能不知道的浏览器实时通信方案》

除了IE6、7以下不支持,大部分浏览器都支持这个方案,当浏览器不支持XHR-streaming时,可以作为最佳备胎。

Polling

轮询是最粗暴(或者说最简单),也是效率最低下的‘实时’通信方案,这种方式的原理就是定期向服务器发起请求, 拉取最新的消息队列:

《你可能不知道的浏览器实时通信方案》

这种轮询方式比较合适服务器的信息定期更新的场景,如天气和股票行情信息。举个例子股票信息每隔5分钟更新一次,这时候客户端定期轮询, 且轮询间隔和服务端更新频率保持一致是一种理想的方式。

但是如果追求实时性,轮询会导致一些严重的问题:

  • 资源浪费。比如轮询的间隔小于服务器信息更新的频率,这会浪费很多HTTP请求, 消耗宝贵的CPU时间和带宽
  • 容易导致请求轰炸。比如当服务器负载比较高时,第一个请求还没处理完成,这时候第二、第三个请求接踵而来,无用的额外请求对服务端进行了轰炸。

Long polling

还有一种优化的轮询方法,称为长轮询(Long Polling),sockjs就是使用这种轮询方式, 长轮询指的是浏览器发送一个请求到服务器,服务器只有在有可用的新数据时才响应

《你可能不知道的浏览器实时通信方案》

客户端向服务端发起一个消息获取请求,服务端会将当前的消息队列返回给客户端,然后关闭连接。当消息队列为空时,服务端不会立即关闭连接,而是等待指定的时间间隔,如果在这个时间间隔内没有新的消息,则由客户端主动超时关闭连接

另外一个要点是,客户端的轮询请求只有在上一个请求连接关闭后才会重新发起。这就解决了上文的请求轰炸问题。服务端可以控制客户端的请求时序,因为在服务端未响应之前,客户端不会发送额外的请求(在超时期间内)。

扩展

  • WebRTC 这是浏览器的实时通信技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。
  • metetor DDP DDP(Distributed Data Protocol), 这是一个’有状态的’实时通信协议,这个是Meteor框架的基础, 它就是使用这个协议来进行客户端和服务端通信. 他只是一个协议,而不是通信技术,比如它的底层可以基于Websocket、XHR-Streaming、长轮询甚至是WebRTC
  • 程序员怎么会不知道C10K 问题呢? – 池建强- Medium

推荐阅读
  • 在分析和解决 Keepalived VIP 漂移故障的过程中,我们发现主备节点配置如下:主节点 IP 为 172.16.30.31,备份节点 IP 为 172.16.30.32,虚拟 IP 为 172.16.30.10。故障表现为监控系统显示 Keepalived 主节点状态异常,导致 VIP 漂移到备份节点。通过详细检查配置文件和日志,我们发现主节点上的 Keepalived 进程未能正常运行,最终通过优化配置和重启服务解决了该问题。此外,我们还增加了健康检查机制,以提高系统的稳定性和可靠性。 ... [详细]
  • 秒建一个后台管理系统?用这5个开源免费的Java项目就够了
    秒建一个后台管理系统?用这5个开源免费的Java项目就够了 ... [详细]
  • Java Socket 关键参数详解与优化建议
    Java Socket 的 API 虽然被广泛使用,但其关键参数的用途却鲜为人知。本文详细解析了 Java Socket 中的重要参数,如 backlog 参数,它用于控制服务器等待连接请求的队列长度。此外,还探讨了其他参数如 SO_TIMEOUT、SO_REUSEADDR 等的配置方法及其对性能的影响,并提供了优化建议,帮助开发者提升网络通信的稳定性和效率。 ... [详细]
  • 本文介绍了如何利用Struts1框架构建一个简易的四则运算计算器。通过采用DispatchAction来处理不同类型的计算请求,并使用动态Form来优化开发流程,确保代码的简洁性和可维护性。同时,系统提供了用户友好的错误提示,以增强用户体验。 ... [详细]
  • Nginx 反向代理配置与应用指南
    本文详细介绍了 Nginx 反向代理的配置与应用方法。首先,用户可以从官方下载页面(http://nginx.org/en/download.html)获取最新稳定版 Nginx,推荐使用 1.14.2 版本。下载并解压后,通过双击 `nginx.exe` 文件启动 Nginx 服务。文章进一步探讨了反向代理的基本原理及其在实际应用场景中的配置技巧,包括负载均衡、缓存管理和安全设置等,为用户提供了一套全面的实践指南。 ... [详细]
  • 利用爬虫技术抓取数据,结合Fiddler与Postman在Chrome中的应用优化提交流程
    本文探讨了如何利用爬虫技术抓取目标网站的数据,并结合Fiddler和Postman工具在Chrome浏览器中的应用,优化数据提交流程。通过详细的抓包分析和模拟提交,有效提升了数据抓取的效率和准确性。此外,文章还介绍了如何使用这些工具进行调试和优化,为开发者提供了实用的操作指南。 ... [详细]
  • 本文回顾了作者初次接触Unicode编码时的经历,并详细探讨了ASCII、ANSI、GB2312、UNICODE以及UTF-8和UTF-16编码的区别和应用场景。通过实例分析,帮助读者更好地理解和使用这些编码。 ... [详细]
  • 零拷贝技术是提高I/O性能的重要手段,常用于Java NIO、Netty、Kafka等框架中。本文将详细解析零拷贝技术的原理及其应用。 ... [详细]
  • php更新数据库字段的函数是,php更新数据库字段的函数是 ... [详细]
  • 基于Web的Kafka管理工具Kafkamanager首次访问Web界面的详细配置指南(附图解)
    首次访问Kafkamanager Web界面时,需要对Kafka集群进行配置。这一过程相对简单,用户只需依次点击【Cluster】>【Add Cluster】,按照提示完成相关设置即可。本文将通过图文并茂的方式,详细介绍每一步的配置步骤,帮助用户快速上手Kafkamanager。 ... [详细]
  • 浏览器作为我们日常不可或缺的软件工具,其背后的运作机制却鲜为人知。本文将深入探讨浏览器内核及其版本的演变历程,帮助读者更好地理解这一关键技术组件,揭示其内部运作的奥秘。 ... [详细]
  • 本文探讨了如何通过编程手段在Linux系统中禁用硬件预取功能。基于Intel® Core™微架构的应用性能优化需求,文章详细介绍了相关配置方法和代码实现,旨在帮助开发人员有效控制硬件预取行为,提升应用程序的运行效率。 ... [详细]
  • Spring框架中枚举参数的正确使用方法与技巧
    本文详细阐述了在Spring Boot框架中正确使用枚举参数的方法与技巧,旨在帮助开发者更高效地掌握和应用枚举类型的数据传递,适合对Spring Boot感兴趣的读者深入学习。 ... [详细]
  • Python 伦理黑客技术:深入探讨后门攻击(第三部分)
    在《Python 伦理黑客技术:深入探讨后门攻击(第三部分)》中,作者详细分析了后门攻击中的Socket问题。由于TCP协议基于流,难以确定消息批次的结束点,这给后门攻击的实现带来了挑战。为了解决这一问题,文章提出了一系列有效的技术方案,包括使用特定的分隔符和长度前缀,以确保数据包的准确传输和解析。这些方法不仅提高了攻击的隐蔽性和可靠性,还为安全研究人员提供了宝贵的参考。 ... [详细]
  • 在处理 XML 数据时,如果需要解析 `` 标签的内容,可以采用 Pull 解析方法。Pull 解析是一种高效的 XML 解析方式,适用于流式数据处理。具体实现中,可以通过 Java 的 `XmlPullParser` 或其他类似的库来逐步读取和解析 XML 文档中的 `` 元素。这样不仅能够提高解析效率,还能减少内存占用。本文将详细介绍如何使用 Pull 解析方法来提取 `` 标签的内容,并提供一个示例代码,帮助开发者快速解决问题。 ... [详细]
author-avatar
白开水冲清茶
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有