​细说websocket -Node篇​

news2024/12/22 13:42:17

一、WebSocket 协议

1. 概述

websocket协议允许不受信用的客户端代码在可控的网络环境中控制远程主机。该协议包含一个握手和一个基本消息分帧、分层通过TCP。简单点说,通过握手应答之后,建立安全的信息管道,这种方式明显优于前文所说的基于 XMLHttpRequest 的 iframe 数据流和长轮询。该协议包括两个方面,握手链接(handshake)和数据传输(data transfer)。

2. 握手连接

这部分比较简单,就像路上遇到熟人问好。

Client:嘿,大哥,有火没?(烟递了过去)
Server:哈,有啊,来~ (点上)
Client:火柴啊,也行!(烟点上,验证完毕)

握手连接中,client 先主动伸手:

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

客户端发了一串 Base64 加密的密钥过去,也就是上面你看到的 Sec-WebSocket-Key。 Server 看到 Client 打招呼之后,悄悄地告诉 Client 他已经知道了,顺便也打个招呼。

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

Server 返回了 Sec-WebSocket-Accept 这个应答,这个应答内容是通过一定的方式生成的。生成算法是:

mask  = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";  // 这是算法中要用到的固定字符串
accept = base64( sha1( key + mask ) );

key 和 mask 串接之后经过 SHA-1 处理,处理后的数据再经过一次 Base64 加密。分解动作:

1. t = "GhlIHNhbXBsZSBub25jZQ==" + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
   -> "GhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
2. s = sha1(t) 
   -> 0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 
      0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea
3. base64(s) 
   -> "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="

上面 Server 端返回的 HTTP 状态码是 101,如果不是 101 ,那就说明握手一开始就失败了~

下面就来个 demo,跟服务器握个手:

var crypto = require('crypto');

require('net').createServer(function(o){
    var key;
    o.on('data',function(e){
        if(!key){
            // 握手
            // 应答部分,代码先省略
            console.log(e.toString());
        }else{

        };
    });
}).listen(8000);

客户端代码:

var ws=new WebSocket("ws://127.0.0.1:8000");
ws.οnerrοr=function(e){
  console.log(e);
};

运行代码

上面当然是一串不完整的代码,目的是演示握手过程中,客户端给服务端打招呼。在控制台我们可以看到:

看起来很熟悉吧,其实就是发送了一个 HTTP 请求,这个我们在浏览器的 Network 中也可以看到:

但是 WebSocket协议 并不是 HTTP 协议,刚开始验证的时候借用了 HTTP 的头,连接成功之后的通信就不是 HTTP 了,不信你用 fiddler2 抓包试试,肯定是拿不到的,后面的通信部分是基于 TCP 的连接。

服务器要成功的进行通信,必须有应答,往下看:

//服务器程序
var crypto = require('crypto');
var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
require('net').createServer(function(o){
    var key;
    o.on('data',function(e){
        if(!key){
            //握手
            key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
            key = crypto.createHash('sha1').update(key + WS).digest('base64');
            o.write('HTTP/1.1 101 Switching Protocols\r\n');
            o.write('Upgrade: websocket\r\n');
            o.write('Connection: Upgrade\r\n');
            o.write('Sec-WebSocket-Accept: ' + key + '\r\n');
            o.write('\r\n');
        }else{
            console.log(e);
        };
    });
}).listen(8000);

关于crypto模块,可以看看官方文档,上面的代码应该是很好理解的,服务器应答之后,Client 拿到 Sec-WebSocket-Accept ,然后本地做一次验证,如果验证通过了,就会触发 onopen 函数。

//客户端程序
var ws=new WebSocket("ws://127.0.0.1:8000/");
ws.onopen=function(e){
    console.log("握手成功");
};

运行代码

可以看到

3. 数据帧格式

官方文档提供了一个结构图

  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+

第一眼瞟到这张图恐怕是要吐血,如果大学修改计算机网络这门课应该不会对这东西陌生,数据传输协议嘛,是需要定义字节长度及相关含义的。

FIN      1bit 表示信息的最后一帧,flag,也就是标记符
RSV 1-3  1bit each 以后备用的 默认都为 0
Opcode   4bit 帧类型,稍后细说
Mask     1bit 掩码,是否加密数据,默认必须置为1 (这里很蛋疼)
Payload  7bit 数据的长度
Masking-key      1 or 4 bit 掩码
Payload data     (x + y) bytes 数据
Extension data   x bytes  扩展数据
Application data y bytes  程序数据

每一帧的传输都是遵从这个协议规则的,知道了这个协议,那么解析就不会太难了,下面我就直接拿了次碳酸钴同学的代码。

4. 数据帧的解析和编码

数据帧的解析代码:

 decodeDataFrame Function

数据帧的编码:

 encodeDataFrame Function

有些童鞋可能没有明白,应该解析哪些数据。这的解析任务主要是服务端处理,客户端送过去的数据是二进制流形式的,比如: 

var ws = new WebSocket("ws://127.0.0.1:8000/"); 
ws.onopen = function(){ 
  ws.send("握手成功"); 
};

运行代码

Server 收到的信息是这样的:

一个放在Buffer格式的二进制流。而当我们输出的时候解析这个二进制流:

//服务器程序
var crypto = require('crypto');
var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
require('net').createServer(function(o){
    var key;
    o.on('data',function(e){
        if(!key){
            //握手
            key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
            key = crypto.createHash('sha1').update(key + WS).digest('base64');
            o.write('HTTP/1.1 101 Switching Protocols\r\n');
            o.write('Upgrade: websocket\r\n');
            o.write('Connection: Upgrade\r\n');
            o.write('Sec-WebSocket-Accept: ' + key + '\r\n');
            o.write('\r\n');
        }else{
            // 输出之前解析帧
            console.log(decodeDataFrame(e));
        };
    });
}).listen(8000);

那输出的就是一个帧信息十分清晰的对象了:

5. 连接的控制

上面我买了个关子,提到的Opcode,没有详细说明,官方文档也给了一张表:

 |Opcode  | Meaning                             | Reference |
-+--------+-------------------------------------+-----------|
 | 0      | Continuation Frame                  | RFC 6455  |
-+--------+-------------------------------------+-----------|
 | 1      | Text Frame                          | RFC 6455  |
-+--------+-------------------------------------+-----------|
 | 2      | Binary Frame                        | RFC 6455  |
-+--------+-------------------------------------+-----------|
 | 8      | Connection Close Frame              | RFC 6455  |
-+--------+-------------------------------------+-----------|
 | 9      | Ping Frame                          | RFC 6455  |
-+--------+-------------------------------------+-----------|
 | 10     | Pong Frame                          | RFC 6455  |
-+--------+-------------------------------------+-----------|

decodeDataFrame 解析数据,得到的数据格式是:

{
    FIN: 1,
    Opcode: 1,
    Mask: 1,
    PayloadLength: 4,
    MaskingKey: [ 159, 18, 207, 93 ],
    PayLoadData: '握手成功'
}

那么可以对应上面查看,此帧的作用就是发送文本,为文本帧。因为连接是基于 TCP 的,直接关闭 TCP 连接,这个通道就关闭了,不过 WebSocket 设计的还比较人性化,关闭之前还跟你打一声招呼,在服务器端,可以判断frame的Opcode:

var frame=decodeDataFrame(e);
console.log(frame);
if(frame.Opcode==8){
    o.end(); //断开连接
}

客户端和服务端交互的数据(帧)格式都是一样的,只要客户端发送 ws.close(), 服务器就会执行上面的操作。相反,如果服务器给客户端也发送同样的关闭帧(close frame):

o.write(encodeDataFrame({
    FIN:1,
    Opcode:8,
    PayloadData:buf
}));

客户端就会相应 onclose 函数,这样的交互还算是有规有矩,不容易出错。

二、注意事项

1. WebSocket URIs

很多人可能只知道 ws://text.com:8888,但事实上 websocket 协议地址是可以加 path 和 query 的。

ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]

如果使用的是 wss 协议,那么 URI 将会以安全方式连接。 这里的 wss 大小写不敏感。

2. 协议中"多余"的部分(吐槽)

握手请求中包含Sec-WebSocket-Key字段,明眼人一下就能看出来是websocket连接,而且这个字段的加密方式在服务器也是固定的,如果别人想黑你,不会太难。

再就是那个 MaskingKey 掩码,既然强制加密了(Mask为1表示加密,加密方式就是 MaskingKey 与 PayLoadData 进行异或处理),还有必要让开发者处理这个东西么?直接封装到内部不就行了?

3. 与 TCP 和 HTTP 之间的关系

WebSocket协议是一个基于TCP的协议,就是握手链接的时候跟HTTP相关(发了一个HTTP请求),这个请求被Server切换到(Upgrade)websocket协议了。websocket把 80 端口作为默认websocket连接端口,而websocket的运行使用的是443端口。

搜集整理学习路线&笔记icon-default.png?t=N4P3https://mp.weixin.qq.com/s/KQx_eIwdjCj3QdErxKb7ZQ

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

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

相关文章

中国汽车品牌出海简直“泰裤辣”!

世界上有五种辣:微辣、中辣、特辣、变态辣,还有一种叫“泰裤辣”!随着中国汽车品牌出海的步伐加快,泰国市场成为了一个备受关注的目标。无论是泰国对汽车产业的激励政策,还是泰国当地对促进汽车消费的力度,…

三星不愿成为炮灰,加速抢占美光的市场,美国的愿望落空

在美光被中国实施安全审查后,美国曾要求韩国芯片不要趁机扩大销售,抢占美光留下的市场人,然而由于全球芯片行业的衰退,三星等韩国企业担忧成为炮灰而加速抢占美光的市场。 一、韩国芯片不好过 三星等韩国芯片企业如此做&#xff0…

yolov5s融合ECA、CA、Transformer等组件开发构建海洋场景下远红外目标检测模型,并进行实验对比分析

海洋海面远红外场景下的目标检测在我之前的文章中已经有过相关的实践了,感兴趣的话可以自行移步阅读即可。 《红外海洋目标检测实践,基于目标检测模型识别红外海洋目标》 《基于YOLO开发构建红外场景下无人机航拍车辆实例分割检测识别分析系统》 《红…

CAD软件外包开发的技术难点

CAD软件的开发具有一定的复杂性和挑战性,需要复杂的数学和几何计算,同时需要兼顾高性能,这对软件开发者有比较高的要求。今天和大家分享这方面的知识,希望对大家有所帮助。北京木奇移动技术有限公司,专业的软件外包开发…

Centos升级docker后启动容器报错“unknown or invalid runtime name”

前言 操作系统为centosdocker原来的版本为1.31.1,升级后为20结尾有升级docker的教程听说图片能凑字数,就截图贴了 插播一个问题记录 加入后面出现无法强制删除镜像文件时,或者报错(无法创建容器) 按照如下解决 1. 报…

多语言跨境商城源码系统选择定制开发|多语言电商源码

多语言跨境商城源码选择 欢迎名片沟通 多语言跨境商城源码系统应当具有符合自身需求的语言和环境,具体应有:多语言支持、界面设计、支付方式、物流配送、安全性、性能和稳定性、可扩展性等等。具体如下: 多语言支持——具备语言切换…

13.生命周期函数

小程序中的生命周期函数分为两种 应用生命周期函数:整个应用的生命周期函数页面生命后期函数 目录 1 应用生命周期函数 1.1 初始化时 onLanch 1.2 隐藏小程序时 onHide 1.3 显示小程序时 onShow 2 页面生命周期函数 2.1 初始化时 onLoad 2.2 页面显…

C语言写网络爬虫总体思路

使用C语言编写爬虫可以实现网络数据的快速获取和处理,适用于需要高效处理海量数据的场景。与其他编程语言相比,C语言具有较高的性能和灵活性,可以进行底层操作和内存管理,适合处理较复杂的网络请求和数据处理任务。 但是&#xf…

OpenAI网站3月份独立访问量突破8.47亿,增长54%;10分钟搭建自己可免费商用的ChatGPT环境

🦉 AI新闻 🚀 OpenAI网站3月份独立访问量突破8.47亿,增长54% 摘要:据营销机构VezaDigital的调查数据显示,今年3月份共有8.47亿用户访问了OpenAI的网站,比2月份增长了54%。OpenAI是爆火聊天机器人ChatGPT的…

苹果手机备份软件iMazing2023免费版下载

Mazing 则是完全符合用户的使用习惯,可以非常方便地选择一个文件夹即可导入,或者直接像文件管理器那样,用鼠标拖放文件即可完成音乐的传输,简单至极!这点 iTunes 是做不到的。iMazing 可以帮助你轻松快速在本地备份自己…

数据中心低压配电系统能耗分析与PUE计算

本篇内容是同为科技(TOWE)整编的现代化数据中心能耗及PUE的基本概念解读与分析,PUE作为数据中心机房建设节能技术衡量工具,正确理解PUE的涵义,采取针对措施降低PUE值,寻找节能高效与成本合理的平衡点&#…

[2]PCB设计实验|电路板布局布线要求及规律|电抗和电容的关系|8:30~9:30

目录 1.为什么要学习电路板设计? 2.电路板设计目标分析 3.电路设计水平分级与实训目标 4.硬件设计不等于AD使用 5.元件布局纲要 5.1元件布局纲要-机械结构考虑 5.2元件布局纲要-发热器件处理 6.布局和布线的注意事项 6.1布局时的“五个分开” 6.2元件布局…

已解决:如何从别人的仓库那里克隆到自己的仓库,并修改代码并提交。

一、场景 拉取项目代码后,如果要共同开发一个项目的自动化代码,此时需要把自己写的代码部分提交到代码仓库。 可以用pycharm把修改的代码push到代码仓库 二、操作方法 1.从别人的仓库那里点击fork,将仓库克隆到自己的仓库。 2.在pychar…

Vue.js 中的虚拟 DOM 是什么?如何使用虚拟 DOM?

Vue.js 中的虚拟 DOM 是什么?如何使用虚拟 DOM? 什么是虚拟 DOM? 虚拟 DOM(Virtual DOM)是一种用于优化页面渲染性能的技术。它是由React框架首先引入并广泛应用的,后来也被Vue.js和其他一些框架所采纳。…

springboot整合mybatisplus使用多数据源

本篇文章主要讲解的是使用多数据源&#xff0c;至于springboot与mybatisplus的整合这里只做简单介绍 springboot整合mybatisplus&#xff08;非本文重点&#xff09; 在pom文件中引入如下的依赖 <dependency><groupId>com.baomidou</groupId><artifactI…

RabbitMQ的基本概念

目录 1、MQ 的基本概念 1.1 MQ概述 1.2 MQ 的优势和劣势 1.3 MQ 的优势 1. 应用解耦 2. 异步提速 3. 削峰填谷 小结: 1.4 MQ 的劣势 1.5 常见的 MQ 产品 1.6 RabbitMQ 简介 1.7 JMS 1、MQ 的基本概念 1.1 MQ概述 MQ全称 Message Queue&#xff08;消息队列&#…

深入理解vue插槽

我们都知道vue的插槽及使用&#xff0c;一下是探究他的背后&#xff0c;不对的地方欢迎指正 父组件中我们在子组件中嵌套插槽&#xff0c;在子组件中我们使用template模板写下对应的插槽 实际上父组件中经过编译传递给子组件的插槽是函数 此图为下面示例中子组件中的输出 d…

VSCode--Config

1. basic 1.1 调整字体 1.2 调整 remote login 输入框都在 TERMINAL 中实现 1.3 界面设置成中文 安装插件&#xff1a; 然后配置即可。 2.Linux 2.1 Install 2.1.1 offline Install vscode server 问题描述 内网开发&#xff0c;vscode 自身通过代理安装完 remote 插件后…

【算法系列之二叉树IV】leetcode450.删除二叉搜索树中的节点

701.二叉搜索树中的插入操作 力扣题目链接 给定二叉搜索树&#xff08;BST&#xff09;的根节点 root 和要插入树中的值 value &#xff0c;将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 &#xff0c;新值和原始二叉搜索树中的任意节点值都不同。 注…

shell之免交互

一.免交互&#xff08;Here Document&#xff09;介绍 使用I/O重定向的方式将命令列表提供给交互式程序或命令&#xff0c;比如 ftp、cat 或 read 命令。是标准输入的一种替代品可以帮助脚本开发人员不必使用临时文件来构建输入信息&#xff0c;而是直接就地生产出一个“文件”…