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,这个技术主要是为了实现服务端可以向客户端推送数据,为了解决实时性比较高的情况。

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

    <!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. 长轮询

    <!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流(以前用的挺多的)

    <!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>
    
    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.基础内容

<!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>
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的换算

// 可以使用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.具体代码模拟

// 引入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');
})

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值