websocket学习笔记1

1. 知识模块一

1.1. websocket与http对比

1.1.1. http协议

主要关注:客户端->服务器(获取资源)

特点:

  • 无状态协议,每个请求都是独立的,请求应答模式,服务端无法主动给客户端推送消息,半双工(同一刻数据传输只能是单项的,还有单工和全双工)。
  • http受浏览器同源策略影响,需要保证协议、主机名、端口号一致,否则会出现跨域问题(为了安全)。
  • 适合获取资源、下载文件,但不适合实时性要求高的需求。

1.1.2.websocket协议

双向通信(全双工协议),每次不需要重新建立连接,可以一致相互通信,适合长通信。

1.1.3.关系

都是通信协议,websocket是建立在http基础之上的,第一次websocket握手是基于http的,底层传输都依靠TCP。

1.2.不用websocket以前是如何实现双向通信的

Comet,这个技术主要是为了实现服务端可以向客户端推送数据,为了解决实时性比较高的情况。

javascript 复制代码
import express from "express";
import cors from "cors";

const app = express();
// 解决跨域问题
app.use(cors());

// 轮询,短轮询()


// 接口
app.get('/clock',function(req,res){
    res.send(new Date().toLocaleDateString());
})

// 通过node命令启动时,修改后并不会重新执行
// 通过nodeman启动可以在改变后自动执行
app.listen(3000,function(){
    console.log('server start 3000');
})
  1. 轮询

    clock-1.html

    html 复制代码
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    
    <body>
        <div id="clock"></div>
    
        <script>
            setInterval(() => {
             	// 创建请求
                const xhr = new XMLHttpRequest();
                
                // 访问请求,异步
                xhr.open('GET','http://localhost:3000/clock',true);
                xhr.onload = function () {
                    console.log(xhr.responseText);
                    clock.innerHTML = xhr.responseText;
                }
                // 发送请求
                xhr.send();
            }, 1000)//每隔一秒
        </script>
    </body>
    
    </html>

    存在问题:

    • 竞速问题:无法保证请求的先后顺序,可能会出现多个请求返回的时候同时修改资源,会导致一些不可预测的问题。
    • 频繁的网络请求,请求数目过多,会导致网络带宽的消耗,增加服务端和客户端的消耗。
    • http在发送请求的时候,会增加http报文(鉴权、内容类型),增加额外的数据消耗
    • 实时性比较低,如果服务端1s内变了三次,而客户端每隔1s发送一次请求。

    优点:

    • 容易实现,适合轻量级、低并发。
  2. 长轮询

    html 复制代码
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    
    <body>
        <div id="clock"></div>
    
        <script>
            // 客户端发送请求后,服务端相应后,我就发下一个请求
            function longPolling() {
                const xhr = new XMLHttpRequest();
                xhr.open('GET', 'http://localhost:3000/clock', true);
                xhr.onload = function () {
                    console.log(xhr.responseText);
                    clock.innerHTML = xhr.responseText;
                    longPolling();
                }
                xhr.send();
            }
            longPolling()
        </script>
    </body>
    
    </html>
    1. 想解决短轮询的问题,希望实时性更强,但是实时性强了的同时,也会造成频繁的网络请求(实时性强了,但是要求服务端的并发能力必须强)。
    2. 连接堆叠问题,这些链接都在服务端中保持打开,会占用服务端资源。
  3. iframe流(以前用的挺多的)

    html 复制代码
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        <div id="clock"></div>\
        <!-- 目前谷歌的document.domain跨域方法已经噶了 -->
        <script>
            document.domain = 'localhost'
        </script>
        <iframe src="http://localhost:3000/clock" frameborder="0"></iframe>
    </body>
    </html>
    javascript 复制代码
    import express from "express";
    import cors from "cors";
    
    const app = express();
    // 解决跨域问题
    app.use(cors());
    
    // 接口
    app.get('/clock', function (req, res) {
        // res.end或者res.send请求结束后会断开
        // res.write方法不会结束本次的响应
        setInterval(() => {
            res.write(`
            <script>
                document.domain = 'localhost'
                parent.document.getElementById('clock').innerHTML = "${new Date().toDateString()}"
            </script>
            `);
        })
    })
    
    // 通过node命令启动时,修改后并不会重新执行
    // 通过nodeman启动可以在改变后自动执行
    app.listen(3000, function () {
        console.log('server start 3000');
    })

    创建之后一直保持链接,会出现跨域问题

    可以保证实时性,而且不用客户频繁发送请求 。

    缺点:单向通信。

  4. sse EventSource(写法已经比较接近websocket了)

    html提供的,单向通信,客户端可以监控服务端推送的事件,只能推送文本类型的数据,适合小数据,需要做额外的处理。

    缺点:单向,客户端无法给服务端传递数据。

  5. websocket

    优势:

    1. 双向绑定
    2. 持久链接,可以一直握手
    3. 发送的消息增加帧非常小
    4. 支持多种数据格式
    5. 天生支持跨域

2. 知识模块二

2.1.基础内容

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>

    <!-- 客户端 -->
    <script>
        // 与服务端提供的一个websocket服务相关联
        const ws = new WebSocket('ws://localhost:3000');
        // 给服务端发送消息
        ws.onopen = function(){
            console.log('Connection opend');
            ws.send('hello server');// 给服务端发送消息
        }

        // 监控服务端的数据
        ws.onmessage = function(e){
            console.log('服务端相应的数据:' + e.data);
        }

        // http各种header的使用

        // websocket怎么实现握手、数据长什么样的、怎么通信的
        // 协议的表示方式
    
        // 请求行:GET ws://localhost:3000 HTTP/1.1
        // Connection:Upgrade
        // Sec-Websocket-Key:用于保证是安全的websocket链接,防止恶意连接,用于握手
        // Sec-Websoeckt-Version:版本

        // 握手成功后服务端会返回一个Sec-Websocket-Accept,是根据key算出来的
        // Upgrade:websocket,表示升级成什么协议

    </script>
</body>
</html>
javascript 复制代码
import express, { response } from 'express';
import http from 'http';
import { WebSocketServer } from 'ws';

const app = express();
const server = http.createServer(app); // http服务

const wss = new WebSocketServer({server});

// 监控连接成功
wss.on('connection',(ws)=>{
    console.log('Connection opend');

    // 给客户端发送消息
    ws.send('hello client');

    // 第一个参数可以为
    // close、error、message、open、ping、pong、upgrade、unexpected-response
    ws.on('message', function(message){
        console.log("客户端数据:"+message);
    })
})

// 监控端口
server.listen(3000)

2.2. key和accept的换算

javascript 复制代码
// 可以使用wireshark抓包软件,分析协议信息
// key-> P2P2F9kEf/wg18RKzXM8eA== ,握手的时候创建一个随机的key
// 服务端通过key加上
// const number = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
// 然后经历sha1算法计算生成accept,
// accept-> adAEOXRx506qcgqahbjvIHPI1Sk= ,服务端要相应一个值
import crypto from 'crypto'

const number = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const WebsocketKey = 'P2P2F9kEf/wg18RKzXM8eA=='; // key是随机值

const WebsocketAccept = crtpto
		 .createHash('sha1')
		 .update(websocketKey + number)
		 .digest('base64');

2.3.具体握手过程

2.3.1.三次握手:

  1. 第一次握手:建立连接,客户端A发送SYN=1、随机产生Seq=client_isn的数据包到服务器B,等待服务器确认。
  2. 第二次握手:服务器B收到请求后确认联机(可以接受数据),发起第二次握手请求,ACK=(A的Seq+1)、SYN=1,随机产生Seq=client_isn的数据包到A。
  3. 第三次握手:A收到后检查ACK是否正确,若正确,A会在发送确认包ACK=服务器B的Seq+1、ACK=1,服务器B收到后确认Seq值与ACK值,若正确,则建立连接。

通俗点,客户端跟服务端说我们结婚吧,服务端给客户端说好的我们结婚吧,然后服务端和客户端结婚了。

2.3.2.websocket数据帧格式:

  • FIN:1个比特,如果是1,表示这是消息(message)的最后一个分片(fragment),如果是0,表示不是消息(message)的最后一个分片(fragment)。

  • RSV1、RSV2、RSV1:各占1个比特,一般情况全为0.当客户端、服务端协商采用websocket扩展时,这三个标志位可以非0,且值的含义由拓展进行定义。如果出现非零的值,且并没有采用websocket拓展,连接出错。

  • Opcode:4个比特。操作代码,决定了应该如何解析后续的数据载荷(data payload)。如果操作代码是不认识的,那么接收端应该断开连接

    • %x1:表示这是一个文本帧。
    • %x2:表示这是一个二进制帧。
    • %x2:表示这是一个二进制帧。
    • %x3-7:保留的操作代码,用于后续定义的非控制帧。
    • %x8:表示连接断开
    • %x9:表示这是一个ping操作
    • %xA:表示这是一个pong操作
    • %xB-F:保留的操作代码,用于后续定义的控制帧
  • Mask:1个比特。表示是否要对数据载荷进行掩码操作

    • 从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作,如果服务端接收到的数据没有进行掩码操作,服务端需要断开连接。

    • 如果Mask是1,那么在 Masking-key中会定义一个掩码键(masking key),并用这个掩码键来对数据载荷进行反掩码。所有客户端发送到服务端的数据帧,Mask都是1。

  • Payload length:表示数据载荷的长度,单位是字节,由7位/7+16位/7+64位

    • Payload length=x为0~125:数据的长度为x字节。
    • Payload length=x为126:后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度。
    • Payload length=x为127:后续8个字节代表一个64位的无符号整数(最高位为0),该无符号整数的值为数据的长度。
    • 如果Payload length占用了多个字节的话,Payload length的二进制表达采用网络序(big endian,重要的位在前)
  • Masking-key:0或4字节(32位),所有从客户端传到服务端的数据帧,数据载荷都进行了掩码操作,Mask为1,且携带了4字节的Masking-key。如果Mask为0,则没有Masking-key。载荷数据的长度不包括mask key的长度

  • Payload data:(x+y)字节

    • 载荷数据:包括了拓展数据、应用数据。其中拓展数据x字节,应用数据y字节

2.3.3.具体代码模拟

javascript 复制代码
// 引入node内的tcp模块,可以接收原始的tcp消息
import net from 'net';
import crypto from 'crypto';
const server = net.createServer(function (socket) { //每个人都会产生一个socket
    // 接收二进制信息
    socket.once('data', function (data) {
        // 将二进制信息转化为字符串
        data = data.toString();
        // 如果升级为websocket协议
        // console.log(data);
        // GET / HTTP/1.1
		// Host: localhost:3000
		// Connection: Upgrade
		// Pragma: no-cache
		// Cache-Control: no-cache
		// User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) 						//AppleWebKit/537.36 (KHTML, like Gecko) 
        //Chrome/117.0.0.0 Safari/537.36
		// Upgrade: websocket
		// Origin: http://127.0.0.1:5500
		// Sec-WebSocket-Version: 13
		// Accept-Encoding: gzip, deflate, br
		// Accept-Language: zh-CN,zh;q=0.9
		// Sec-WebSocket-Key: 1tIB0I01z9xlRZt89EDUxw==
		// Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
        if (data.match(/Upgrade: websocket/)){
            // 报文是以换行来分割的
            let rows = data.split('\r\n');
            // 解析出请求头
            const headers = rows.slice(1,-2).reduce((memo,row)=>{
                let [key,value] = row.split(': ')
                // 改成小写
                memo[key.toLowerCase()] = value;
                return memo;
            },{});
            const number = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
            let websocketKey = headers['sec-websocket-key'];
            let websocketAccept = crypto.createHash('sha1').update(websocketKey + number).digest('base64');

            // 相应报文
            let response = [
                'HTTP/1.1 101 Switching Protocols',
                'Upgrade: websocket',
                `Sec-Websocket-Accept: ${websocketAccept}`,
                'Connection: Upgrade',
                '\r\n'
            ].join('\r\n');

            // 表示websocket建立连接成功
            socket.write(response);

            // 继续解析 后续发来的websocket数据
            socket.on('data', function(buffers) {
                // 解析websocket的格式

                // 一、客户端发消息过来,先判断消息是否结束了
                // 第一个字节(1个字节是8个位,如何获取第一位是不是1)
                // 位运算:
                // 1、按位或,有一个为1即为1
                // 0000 1111
                // 1111 0000
                //--------------
                // 1111 1111
                // 2、按位与,都是1才是1
                // 0000 1111
                // 1111 1111
                // -------------
                // 0000 1111
                // 3、异或,相同为0不同为1
                // 0000 0111
                // 1000 0110
                //--------------
                // 1000 0001
                const FIN = ((buffers[0] & 0b10000000) === 0b10000000); //表示完成了
                console.log(FIN); //true

                // 二、判断发送数据的格式
                // 1表示的是文本,由于前四位不需要所以为0000 1111
                const OPCOED = (buffers[0] & 0b00001111);
                console.log(OPCOED); // 1

                // 三、计算masked,由于第一位数已经使用完,这里开始使用第二位
                const MASKED = ((buffers[1] & 0b10000000) === 0b10000000);
                console.log(MASKED); //true

                // 四、计算payload_len
                const PAYLOAD_LEN = ((buffers[1] & 0b01111111));
                console.log(PAYLOAD_LEN); // 12

                // 五、获取掩码,掩码的长度是4个字节
                const MASK_KEY = buffers.slice(2,6);

                // 六、获取真正的数据内容,这个内容是被掩码过的,需要用掩码做异或操作(相同为0不同为1)
                const PAYLOAD = buffers.slice(6);
                for (let i = 0 ; i<PAYLOAD.length; i++){
                    // 如果数据有多个字节但是掩码是4个字节时
                    PAYLOAD[i] = PAYLOAD[i]^MASK_KEY[i%4];
                }
                console.log(PAYLOAD.toString()); // hello server

                // 以上内容为客户端给服务端发送消息流程。
                // 服务端如果想给客户端发送消息,按照一样的格式发送即可(服务端给客户端发送消息是不用加掩码的)
                 

            })
        }
    })
})


server.listen(3000, function() {
    console.log('server start 3000');
})
相关推荐
曹天骄2 小时前
100个用户的聊天系统:轮询 vs WebSocket 综合对比
网络·websocket·网络协议
刘婉晴5 小时前
【信息安全工程师备考笔记】第三章 密码学基本理论
笔记·安全·密码学
jjw_zyfx7 小时前
成熟的前端vue vite websocket,Django后端实现方案包含主动断开websocket连接的实现
前端·vue.js·websocket
晓数7 小时前
【硬核干货】JetBrains AI Assistant 干货笔记
人工智能·笔记·jetbrains·ai assistant
我的golang之路果然有问题8 小时前
速成GO访问sql,个人笔记
经验分享·笔记·后端·sql·golang·go·database
lwewan8 小时前
26考研——存储系统(3)
c语言·笔记·考研
搞机小能手8 小时前
六个能够白嫖学习资料的网站
笔记·学习·分类
nongcunqq9 小时前
爬虫练习 js 逆向
笔记·爬虫
汐汐咯9 小时前
终端运行java出现???
笔记
无敌小茶11 小时前
Linux学习笔记之环境变量
linux·笔记