Web端即时通信技术-WebSocket

news2024/9/23 3:21:35

最近在做项目的时候,遇到了一个前端页面需要实时刷新的功能,一种方法是我们通过短轮询的方式,但这种方式虽然简单,但是无用的请求过多,占用资源,并且如果是对数据要求高较高的场景,就不适用了。

这个时候就要考虑应用长连接了,最开始想到的是,Http1.1以后支持的长连接,但是经过实践后发现,这里可能存在一个误解:Http协议是基于请求/响应模式的,因此客户端请求后只要服务端给了响应,本次Http请求就结束了,没有长连接这么一说,那么自然也就没有短连接这么一说了。也就是说,在这样一个HTTP连接中,可以发送多个Request,接收多个Response。但是Request和Response永远是相对应的,也就是说一个request只能有一个response。并且这个response也是被动的,不能主动发起。

所谓的HTTP长连接和短连接,其实本质上说的是TCP连接。TCP连接是一个双向的通道,它是可以保持一段时间不关闭的,因此TCP连接才有真正的长连接和短连接这么一说。

那么下面我们一起来看下WebSocket是如何实现的。

什么是WebSocket?

WebSocket是一种在单个TCP连接上进行全双工通信的协议,使得客户端与服务端之间的数据交换变得更加简单,允许服务端主动向客户端推送数据,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

这里先来区分下三者的含义:

  • 单工:数据传输只允许在一个方向上传输,只能一方发送数据,另一方来接受数据并发送;例如:对讲机。

  • 半双工:数据传输允许两个方向上的传输,但是同一时间内,只可以有一方发送或接收消息,并存在最大传输距离的限制。

  • 全双工:接口可以同时发送和接收数据,最大吞吐量可达到双倍速率,且消除了半双工的物理距离限制。

解决了什么问题?

客户端(浏览器)和服务器进行通信,只能由客户端发起ajax请求,才能进行通信,服务端无法主动向客户端推送消息。像我们常见的一些场景:体育赛事、聊天室、实时位置之类的场景时,客户端要获取服务器端的变化,就只能通过轮询(定时请求)的方式来了解服务器端有没有新的信息变化

轮询效率低,非常浪费资源(需要不断的发送请求,不停连接服务器)

WebSocket的出现,让服务器端可以主动向客户端发送信息,使得浏览器具备了实时双向通信的能力。

实现案例:

前端示例:

// websocket.js   
const WebSocket = require('ws')
const events = []
let latestTimestamp = Date.now()
const clients = new Set()//连接着的socket数组

const EventProducer = () => {
  const event = {
    id: Date.now(),
    timestamp: Date.now()
  }
  events.push(event)
  latestTimestamp = event.timestamp
  
  // 推送给所有连接着的socket
  clients.forEach(client => {
    client.ws.send(JSON.stringify(events.filter(event => event.timestamp > client.timestamp)))
    client.timestamp = latestTimestamp
  })
}

// 每10秒生成一个新的事件
setInterval(() => {
  EventProducer()
}, 10000)

// 启动socket服务器
const wss = new WebSocket.Server({ port: 8080 })
wss.on('connection', (ws, req) => {
  console.log('client connected')
  // 首次连接,推送现存事件
  ws.send(JSON.stringify(events))
  const client = {
    timestamp: latestTimestamp,
    ws,
  }
  clients.add(client)//将连接放入数组
  ws.on('close', _ => {
    clients.delete(client)
  })
})
var timestampRef={current:0}
    var eventsRef={current:[]}
     const ws = new WebSocket(`ws://localhost:8080/ws?timestamp=${timestampRef.current}`)
    ws.addEventListener('open', e => {
      console.log('successfully connected')
    })
    ws.addEventListener('close', e => {
      console.log('socket close')
    })
    ws.addEventListener('message', (ev) => {
      const latestEvents = JSON.parse(ev.data)
      if (latestEvents && latestEvents.length) {
        timestampRef.current = latestEvents[latestEvents.length - 1].timestamp
         //注意latestEvents数据是后端新生成的数据,前端需要自己拼上老数据
        eventsRef.current = [...eventsRef.current, ...latestEvents]
        console.log(eventsRef)
      }
    })

服务端代码(基于SpringBoot):

WebSocketConfig

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Resource
    private HttpAuthHandler handler;

    @Resource
    private WebSocketInterceptor interceptor;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(handler, "/hello")
                .addInterceptors(interceptor)
                .setAllowedOrigins("*");
    }
}

HttpAuthHandler

@Component
public class HttpAuthHandler extends TextWebSocketHandler {

    /**
     * 建立成功事件
     * @param session
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        super.afterConnectionEstablished(session);
    }

    /**
     * 接受消息事件
     * @param session
     * @param message
     * @throws Exception
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        Object token = session.getAttributes().get("token");
        System.out.println("server 接收到 " + token + " 发送的 " + payload);
        session.sendMessage(new TextMessage("server 发送给 " + token + " 消息 " + payload + " " +    LocalDateTime.now().toString()));
    }

    /**
     * 断开连接时
     * @param session
     * @param status
     * @throws Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        super.afterConnectionClosed(session, status);
    }
}

WebSocketInterceptor

public class WebSocketInterceptor implements HandshakeInterceptor {

    /**
     * 握手前
     *
     * @param request
     * @param response
     * @param wsHandler
     * @param attributes
     * @return
     * @throws Exception
     */
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        System.out.println("握手开始");
        // 获得请求参数
        Map<String, String> paramMap = HttpUtil.decodeParamMap(request.getURI().getQuery(), StandardCharsets.UTF_8);
        String uid = paramMap.get("token");
        if (StrUtil.isNotBlank(uid)) {
            // 放入属性域
            attributes.put("token", uid);
            System.out.println("用户 token " + uid + " 握手成功!");
            return true;
        }
        System.out.println("用户登录已失效");
        return false;
    }

    /**
     * 握手后
     *
     * @param request
     * @param response
     * @param wsHandler
     * @param exception
     */
    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
        System.out.println("握手完成");
    }
}

测试:

上边简单实现了一个webSocket通信。实际的东西还有很多,比如webSocket扩展,心跳检测,数据加密,身份认证等知识点。但自己也需要再去研究,所以先不做介绍了。

到这里,基本上使用应该是没问题了,下面我们来继续来继续探讨下有关其实现方面的细节。

实现原理

WebSocket是位于应用层的一个应用层协议,它需要依赖HTTP协议进行一次握手,握手成功后,数据就直接从TCP通道传输,与HTTP无关了。也就是分为握手阶段和数据传输阶段,即:HTTP握手+双工的TCP连接。

下面我们分别来看一下这两个阶段的具体实现原理。

一、握手阶段

客户端发送消息:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Version: 13

在HTTP Header中设置Upgrade字段,其字段值为websocket,并在Connection字段提示Upgrade,服务端若支持WebSocket协议,并同意握手,可以返回如下结构:


HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Version: 13

我们来详细讨论WebSocket握手的细节,发现除了设置Upgrade之外,还需要设置其他的Header字段。

  • Sec-WebSocket-Key:必传,由客户端随机生成的 16 字节值, 然后做 base64 编码, 客户端需要保证该值是足够随机, 不可被预测的 (换句话说, 客户端应使用熵足够大的随机数发生器), 在 WebSocket 协议中, 该头部字段必传, 若客户端发起握手时缺失该字段, 则无法完成握手

  • Sec-WebSocket-Version:必传, 指示 WebSocket 协议的版本, RFC 6455 的协议版本为 13, 在 RFC 6455 的 Draft 阶段已经有针对相应的 WebSocket 实现, 它们当时使用更低的版本号, 若客户端同时支持多个 WebSocket 协议版本, 可以在该字段中以逗号分隔传递支持的版本列表 (按期望使用的程序降序排列), 服务端可从中选取一个支持的协议版本

  • Sec-WebSocket-Protocol :可选, 客户端发起握手的时候可以在头部设置该字段, 该字段的值是一系列客户端希望在与服务端交互时使用的子协议 (subprotocol), 多个子协议之间用逗号分隔, 按客户端期望的顺序降序排列, 服务端可以根据客户端提供的子协议列表选择一个或多个子协议

  • Sec-WebSocket-Extensions:可选, 客户端在 WebSocket 握手阶段可以在头部设置该字段指示自己希望使用的 WebSocket 协议拓展

服务端若支持 WebSocket 协议, 并同意与客户端握手, 则应返回 101 的 HTTP 状态码, 表示同意协议升级, 同时应设置 Upgrade 字段并将值设置为 websocket, 并将 Connection 字段的值设置为 Upgrade, 这些都是与标准 HTTP Upgrade 机制完全相同的, 除了这些以外, 服务端还应设置与 WebSocket 相关的头部字段:

  • Sec-WebSocket-Accept:必传, 客户端发起握手时通过 | Sec-WebSocket-Key | 字段传递了一个将随机生成的 16 字节做 base64 编码后的字符串, 服务端若接收握手, 则应将该值与 WebSocket 魔数 (Magic Number) "258EAFA5-E914-47DA- 95CA-C5AB0DC85B11" 进行字符串连接, 将得到的字符串做 SHA-1 哈希, 将得到的哈希值再做 base64 编码, 最终的值便是该字段的值

  • Sec-WebSocket-Protocol:可选, 若客户端在握手时传递了希望使用的 WebSocket 子协议, 则服务端可在客户端传递的子协议列表中选择其中支持的一个, 服务端也可以不设置该字段表示不希望或不支持客户端传递的任何一个 WebSocket 子协议

  • Sec-WebSocket-Extensions:可选, 与 Sec-WebSocket-Protocol 字段类似, 若客户端传递了拓展列表, 服务端可从中选择其中一个作为该字段的值, 若服务端不支持或不希望使用这些扩展, 则不设置该字段

  • Sec-WebSocket-Version:必传, 服务端从客户端传递的支持的 WebSocket 协议版本中选择其中一个, 若客户端传递的所有 WebSocket 协议版本对服务端来说都不支持, 则服务端应立即终止握手, 并返回 HTTP 426 状态码, 同时在 Header 中设置 | Sec-WebSocket-Version | 字段向客户端指示自己所支持的 WebSocket 协议版本列表

二、数据传输阶段

通信的数据是基于帧(frame)的,可以传输文本数据,也可以直接传输二进制数据,效率高,当然,开发者相应的也需要考虑封包、拆包、编号等细节。

三、优缺点

优点:

  • 节约带宽
    不停地轮询服务端数据这种方式,使用的是http协议,head信息很大,有效数据占比低, 而使用WebSocket方式,头信息很小,有效数据占比高。

  • 实时性
    考虑到服务器压力,使用轮询方式不可能很短的时间间隔,否则服务器压力太多,所以轮询时间间隔都比较长,好几秒,设置十几秒。而WebSocket是由服务器主动推送过来,实时性是最高的。

  • 压缩效果好

  • 可以支持扩展

缺点:

  • 不兼容低版本IE

总结

WebSocket是HTML5开始提供的一种独立在单个TCP连接上进行全双工通讯的有状态协议,并且还能支持二进制帧、扩展协议、部分自定义的自协议、压缩等特性。

目前看来,WebSocket可以完美替代AJAX轮询和Comet,但是某些场景还是不能替代SSE,WebSocket和SSE各有所长。

关注公众号,获取更多Java知识。

 

 

 

 

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

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

相关文章

汽车ECU软件开发之应用层软件与底层软件

随着汽车产品日渐趋向电子化&#xff0c;人们对各类先进的安全系统、高级驾驶辅助系统的接受程度日益提升。而原本昂贵的盲点检测、自动紧急制动等功能的成本下滑&#xff0c;使其快速获得了市场青睐。富士奇美拉研究所&#xff08;Fuji Chimera Research Institute&#xff09…

MySQL算术/比较/逻辑/位/运算符与正则全解

文章目录1. 算术运算符加法与减法运算符乘法与除法运算符求模&#xff08;求余&#xff09;运算符2. 比较运算符等号运算符安全等于运算符不等于运算符非符号类型的运算符空运算符非空运算符最小值运算符最大值运算符BETWEEN AND运算符IN运算符NOT IN运算符LIKE运算符ESCAPEREG…

〖金融帝国实验室〗(Capitalism Lab)v8.2.00官方重大版本更新(2023.01.17正式发布)

〖金融帝国实验室〗&#xff08;CapLab&#xff09;v8.2.00正式发布&#xff01; ————————————— ※v8.2.00更新内容摘要&#xff1a; 1.新增生活服务业&#xff1a;快餐店、比萨店、蛋糕店和咖啡店。 2.可以通过在农场周围购买土地并耕耘来实现产能扩容。 3.…

九龙证券|重磅会议召开,工信部强调推进民爆行业高质量发展

工信部强调推动民爆职业高质量开展&#xff0c;民爆概念股午后拉升。 工业和信息化部举行 民爆职业安全出产视频会议 据工信部网站&#xff0c;1月17日&#xff0c;工业和信息化部举行民爆职业安全出产视频会议。 工业和信息化部副部长徐晓兰表明&#xff0c;部党组对安全出…

【Linux操作系统】环境变量和命令行参数

文章目录一.环境变量1.什么是环境变量&#xff1f;2.windows下配置PATH环境变量的原理3.一览常见的环境变量4.指令方式如何查看环境变量5.PATH-存放系统默认搜索路径的环境变量6.两种方法使运行我们写的可执行程序不带路径二.环境变量和本地变量三.和环境变量相关的命令1.echo:…

元组的操作

1 元组的定义&#xff1a; 元组就是用一对小括号括起来的多个元素的有序集合&#xff0c;个元素之间用逗号隔开。 2 元组的创建&#xff1a; 2.1 使用&#xff08;&#xff09;创建元组对象。例&#xff1a;lt(12,34,"one") 2.2 使用内置方法tuple创建元组对象。…

mysql 为什么说加上了索引,查询速度就快了呢?(文末附视频)

一天&#xff0c;老板跟我们提出说网站访问的速度太慢&#xff0c;要求我们尽快优化一下。我赶快去查询了一下&#xff0c;发现有几条 SQL 查询的时间异常&#xff0c;我相信这个时候很多小伙伴的大脑里浮现出来的第一个解决办法就是&#xff1a;加索引。 那么到底什么是索引呢…

字符加密-C语言实现

任务描述 请编写程序将一串5个字符组成的英文字符串译成密码&#xff0c;规则为&#xff1a;例如&#xff0c;将“China”译成密码&#xff0c;密码规律是&#xff1a;用原来的字母后面第4个字母代替原来的字母。例如&#xff0c;字母A后面第4个字母是“E”&#xff0c;用“E”…

虚拟电厂负荷控制系统三维可视化监控 | 数字孪生

随着国家“双碳”及“构建以新能源为主体的新型电力系统”等目标的提出&#xff0c;清洁化、数字化越来越成为电力系统面临的迫切需求&#xff0c;负控系统的发展对电力营销现代化建设具有重要的意义。负控管理系统是一个着眼于全面加强电力信息管理的&#xff0c;集负荷控制、…

FMC子卡设计方案原理图:FMC550-基于ADRV9002双窄带宽带射频收发器FMC子卡

FMC550-基于ADRV9002双窄带宽带射频收发器FMC子卡一、产品概述 ADRV9002 是一款高性能、高线性度、高动态范围收发器&#xff0c;旨在针对性能与功耗系统进行优化。该设备是可配置的&#xff0c;非常适合要求苛刻、低功耗、便携式和电池供电的设备。ADRV9002 的工作频率为 …

我的2022年度总结

今天是腊月廿七&#xff0c;又到了写年度总结的时候了&#xff0c;这是写年度总结的第七个年头&#xff0c;希望可以一直坚持写下去。今年是新冠疫情的第三年&#xff0c;疫情管控结束了&#xff0c;同时疫情流行也开始了。我在北京刚全面放开的时候就阳了。用了差不多一周多的…

Go 语言就是在“面向未来”学习(文末附视频)

你好&#xff0c;欢迎学习 Go 语言&#xff01; 我们知道&#xff0c;Go 程序设计语言是由 Robert Griesemer、Rob Pike、Ken Thompson 主持&#xff0c;于 2007 年末开始开发的全新高级编程语言。在 2009 年 11 月开源&#xff0c;并于 2012 年发布了第一个稳定版本&#xff…

基于OpenCv的人脸识别,翻车了居然识别错误。

前言 我们身边的人脸识别有车站检票&#xff0c;监控人脸&#xff0c;无人超市&#xff0c;支付宝人脸支付&#xff0c;上班打卡&#xff0c;人脸解锁手机。 人脸检测是人脸识别系统组成的关键部分之一&#xff0c;其目的是检测出任意给定图片中的包含的一个或多个人脸&#xf…

23种设计模式(十)——原型模式【对象创建】

原型模式 文章目录 原型模式意图什么时候使用原型真实世界类比原型模式的实现原型模式的优缺点亦称:克隆、Clone、Prototype 意图 原型模式属于对象的创建模式。通过给出一个原型对象来指明所有创建对象的类型,然后用复制这个原型对象的方法创建出更多同类型的对象。这就是原…

Tailscale 开源版中文部署指南

目前国家工信部在大力推动三大运营商发展 IPv6&#xff0c;对家用宽带而言&#xff0c;可以使用的 IPv4 公网 IP 会越来越少。有部分地区即使拿到了公网 IPv4 地址&#xff0c;也是个大内网地址&#xff0c;根本不是真正的公网 IP&#xff0c;访问家庭内网的资源将会变得越来越…

大模型算法演进

分布式训练系统包括多个组成部分&#xff0c;如AI集群架构、通信机制、并行技术等。基于这些内容&#xff0c;我们可以设计出不同的大模型算法。下面我们简单讲述下大模型的发展脉络&#xff0c;大模型的结构以及SOTA大模型算法。 大模型发展脉络 随着2017年Transformer结构的提…

中国区域创新能力指数《中国区域创新能力报告》

一、《中国区域创新能力评价报告》2002-2021年 《中国区域创新能力报告》旨在对中国各省、直辖市、自治区的创新能力做一个客观、动态和全面的评价。该报告通过大量的数字和科学的分析框架&#xff0c;对中国区域创新总的能力做了一个较全面的分析&#xff0c;并对各省、直辖市…

与 AI 生成的历史人物聊天是怎样的体验? #Hello History

采用 AI 技术的聊天机器人正在被越来越多的人熟知。近期&#xff0c;一款能够与历史人物对话的软件出现&#xff0c;与古今中外的历史人物对话成为现实&#xff01;会有哪些令人惊叹的强大功能?Hello HistoryHello History 是一款基于人工智能的应用&#xff0c;用户可以与历史…

Redis6学习笔记【part1】安装与启动

redis6学习笔记 1.NoSQL数据库 NoSQL Not Only SQL&#xff0c;不仅仅是SQL&#xff0c;泛指非关系型数据库。NoSQL 不依赖业务逻辑方式存储&#xff0c;而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。 特点&#xff1a;不遵循SQL标准、不支持ACID&…

力扣sql简单篇练习(一)

力扣sql简单篇练习(一) 1 大的国家 1.1 题目内容 1.1.1 基本题目信息 1.1.2 示例输入输出 1.2 示例sql语句 SELECT name,population,area FROM World WHERE area>3000000 OR population>250000001.3 运行截图 2 组合两个表 2.1 题目内容 2.1.1 基本题目信息 2.1.2…