如何使用websocket

如何使用websocket

之前看到过一个面试题:吃饭点餐的小程序里,同一桌的用户点餐菜单如何做到的实时同步?
答案就是:使用websocket使数据变动时服务端实时推送消息给其他用户。
最近在我们自己的项目中我也遇到了类似问题:后端需要调用第三方接口然后异步得到结果,前端却不知道具体的回调时间,只能反复轮询,后来找了找资料,想要达到服务端主动推送消息,也许需要使用websocket。
 

参考:
websocket 学习–简单使用,nodejs搭建websocket服务器
一文吃透 WebSocket
比第一个文章更加深入地实现:NodeJS 落地 WebSocket 实践
主参考(必看)
 

学习前的疑惑:

  1. 服务端广播消息时如何具体推送到相关用户?
  2. 代码书写中针对websocket的网络协议有没有什么不安全的行为,如何避免传输中信息泄露。
  3. 长连接涉及的断联和重传行为如何解决

1、什么是websocket

webSocket是一种网络应用层协议,它是基于TCP连接上进行全双工通信的协议,在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接, 并进行双向数据传输,也就是说可以达到服务端主动向客户端推送数据的效果。

WebSocket连接的过程是:

websocket首先通过HTTP协议把TCP连接好,然后通过Upgrade字段进行协议转化,收到服务器的101 Switching Protocols应答后,后续的TCP消息就通过websocket协议解析。

首先,WebSocket需要一个握手过程,在这里它利用了HTTP本身协议升级的特性。经过3次握手后,服务器和客户端建立起TCP连接,然后一方发起一个http get请求,请求头里存放WebSocket支持的版本号等信息,如:Upgrade、Connection、WebSocket-Version等;

  • “Connection: Upgrade”,表示要求协议“升级”;
  • “Upgrade: websocket”,表示要“升级”成 WebSocket 协议。
  • Sec-WebSocket-Key:一个 Base64 编码的 16 字节随机数,作为简单的认证密钥;
  • Sec-WebSocket-Version:协议的版本号,当前必须是 13。
    然后,服务器收到客户端的握手请求后,就不会走普通的 HTTP 处理流程,而是构造一个特殊的“101 Switching Protocols”响应报文,通知客户端,接下来就不用 HTTP 了,全改用 WebSocket 协议通信。;
  • Sec-WebSocket-Accept:响应报文响应头,具体是把请求头里Sec-WebSocket-Key的值+某一个UUID,计算一番传给客户端,然后客户端验证后连接成功。
    最后,客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信。

2、简单的demo

服务器采用epress+ws简单构建。 然后node express-server.js启动。

// express-server.js

var WebSocketServer = require('ws').Server;

wss = new WebSocketServer({ port: 9999 });
wss.on('connection', function (ws) {
  console.log('client connected');
  ws.on('message', function (message) {
    console.log(message);
    ws.send('服务端接收到请求后,发送给客户端的数据' + message);
  });
  ws.on('close', () => {
    console.log('close');
  });
});

服务端使用react脚手架直接启一个,页面上将这个封装好的测试连接组件显示出来。

import { useRef, useState } from 'react';
import { Button } from 'antd';

export default function Index() {
  const ws = useRef(null);
  const startWs = () => {
    if ('WebSocket' in window) {
      // 初始化一个 WebSocket 对象,参数指明url
      ws.current = new WebSocket('ws://localhost:9999');

      // WebSocket 连接时候触发
      ws.current.onopen = () => {
        // 使用 send() 方法发送数据
        ws.current.send('客户端发送的数据');
        console.log('数据发送中...');
      };

      /**
       * 接收服务端数据时触发
       * @param {[{type:string,number:number}]} evt.data
       * @param {string} evt.data.type a :a+1,b:b+1
       * @param {number} evt.data.number
       */
      ws.current.onmessage = (evt) => {
        let received_msg = evt.data;
        console.log('数据已接收...', received_msg);
      };

      // 断开 web socket 连接成功触发事件
      ws.current.onclose = () => {
        // 关闭 websocket
        console.log('连接已关闭...');
      };
    } else {
      // 浏览器不支持 WebSocket
      console.log('您的浏览器不支持 WebSocket!');
    }
  };

  return (
    <>
      <Button onClick={startWs}>测试WS连接</Button>
      <div>
        {Object.keys(data).map((key) => (
          <h2>
            {key} : {data[key]}
            <br />
          </h2>
        ))}
      </div>
    </>
  );
}

是的,就上面两个代码,就能测试一个最简单的ws连接是什么样的。
打开浏览器的控制台 network也可以看到ws的传输报文。可以看到httpCode:101,而requestHeaders里包含几个升级到websocket的请求头,后续我们就可以利用这些特性进行连接的鉴权。

request
Sec-WebSocket-Key: 是随机的字符串,用于后续校验。
Origin: 请求源
Upgrade: websocket
Connection: Upgrade\

response
Sec-WebSocket-Accept: 用匹配寻找客户端连接的值,计算公式为toBase64(sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )
这里的258EAFA5-E914-47DA-95CA-C5AB0DC85B11 为魔术字符串,为常量。

若计算不正确,或者没有返回该字段,则websocket连接不能建立成功。

3、连接的鉴权和安全行为

首先https的连接必须采用wss的连接方式(防止中间人)。
websocket本身是不支持http封装好的cookie 、headers等信息传递方式的,但是它在升级协议的那个请求还是http协议,因此我们可以手动实现一个类似cookie鉴权。
数据传输时的鉴权采取了基于信道建立时鉴权的方案,用户第一次认证后,回传给客户端一个类似token的令牌,用户在每一次使用websocket进行数据传输时,则需要回传这个token到服务端进行验证。


// 参考https://www.npmjs.com/package/ws
import { createServer } from 'http';
import { WebSocketServer } from 'ws';

const server = createServer();
const wss = new WebSocketServer({ noServer: true });

wss.on('connection', function connection(ws, request, client) {
  ws.on('message', function message(data) {
    console.log(`Received message ${data} from user ${client}`);
  });
});

server.on('upgrade', function upgrade(request, socket, head) {
  // This function is not defined on purpose. Implement it with your own logic.
  authenticate(request, function next(err, client) {
    if (err || !client) {
      socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
      socket.destroy();
      return;
    }

    wss.handleUpgrade(request, socket, head, function done(ws) {
      wss.emit('connection', ws, request, client);
    });
  });
});

server.listen(8080);

4、开发过程中需要注意的心跳检测、断线重连机制。

长时间的保持连接是比较浪费网络资源的,因此对于很久没有响应的通道最好进行一个心跳检测,间隔一定时间发送一个心跳包,保持通道连接。
服务端发送心跳包可以直接使用封装好的方法:ws.ping('', false, true);
客服端这边发送心跳包需要自己定义一个定时器,假设保活时间为10分钟一次,收不到服务器返回的回应包就把连接挂了,其他的情况保持通道。

function heartCheck (ws) {
  const timeout = 10*60*1000; // 间隔10分钟发送一次。
  const serverTimeout = 5000; // 5秒内若还没有响应则关闭连接。
  let timer = null; // 心跳包的发送定时器。
  let serverTimer = null; // 服务器端响应定时器时间。

  // OnMessage接收消息则清除定时器。
  const resetServerTimer = () => {
    clearTimeout(resetTimer);
  };

  // 删除全部定时器(正常关闭链接或者没有收到心跳包)  
  const resetTimer = () => {
    clearTimeout(timer);
    clearTimeout(serverTimer);
    ws.close();
  };


  const start = () => {
    timer = setTimeout(()=>{
      //这里发送一个心跳,后端收到后,返回一个心跳消息,
      //onmessage拿到返回的心跳就说明连接正常
      ws.send("ping");

      serverTimer = (() => {
        resetTimer();
      },serverTimeout)
    },timeout)
  };

  return {resetServerTimer,resetTimer,start};
}


 

5、多个服务端的测试demo,如何推送消息到具体用户

// 服务端代码
var WebSocketServer = require('ws').Server;
const { createServer } =  require('http');

wss = new WebSocketServer({ noServer:true });
const server = createServer();

// 鉴权行为函数
const checkAuth = () => new Promise((res,rej)=>{
  res(true)
})

let obj = {a:1,b:2}
let timer = null;

// 定时器定时更新数据
function setImmediateFun (){
    timer = setImmediate(() => {
      obj = {a:obj.a+1,b:obj.b+1};
    },3000)          
}

setImmediateFun();


wss.on('connection', async function (ws,req,client) {

  // 检测协议升级时的鉴权 
  // 因为无法修改返回状态码以及返回token故采用ws.on("upgrade",(ws, req) => void)
  // if (headers['upgrade'] && headers['sec-websocket-key']) {
  //   // 请求用户服务身份验证
  //   const authorized = await checkAuth(headers.authToken);
  //   if(authorized){
  //     // 升级成功,生成一个token给客户端,状态码为101
  //   }else{
  //     // 连接失败,返回403
  //   }
  // }

  // open发送第一条消息
  ws.on('open', function (ws) {
    console.log("connect successfully");
    ws.send(JOSN.stringify(obj));
  });

  // 响应那边传来的数据,更新新数据
  ws.on('message', function (message) {
    ws.send(JSON.stringify(obj));

    wss.clients.forEach((client) => {
      // console.log(client)
      if (client.readyState === 1) {
        client.send(JSON.stringify(obj));
      }
    });
  });

  // 关闭连接
  ws.on('close', () => {
    console.log('close');
    clearInterval(timer)
  });
});

server.on('upgrade', async function upgrade(request, socket, head) {
  if (await checkAuth(request.headers.authToken)) {
    // 升级成功,生成一个token给客户端,状态码为101
    wss.handleUpgrade(request, socket, head, function done(ws) {
      wss.emit('connection', ws, request);
    });
  }else{
    // 连接失败,返回403
    socket.write('HTTP/1.1 403 Unauthorized\r\n\r\n');
    socket.destroy();
    return;
  }
});

server.listen(9999);
// 客户端代码
import { useRef, useState } from 'react';
import { Button } from 'antd';

export default function Index() {
  const ws_current = useRef(null);
  let ws = ws_current.current;
  const [data, setData] = useState({ a: 0, b: 0 });
  const startWs = () => {
    if ('WebSocket' in window) {
      // 初始化一个 WebSocket 对象,参数指明url
      ws = new WebSocket('ws://localhost:9999');

      // WebSocket 连接时候触发
      ws.onopen = () => {
        console.log('连接成功');
      };

      /**
       * 接收服务端数据时触发
       * @param {[{type:string,number:number}]} evt.data
       * @param {string} evt.data.type a :a+1,b:b+1
       * @param {number} evt.data.number
       */
      ws.onmessage = (evt) => {
        let received_msg = evt.data;
        received_msg = JSON.parse(received_msg);
        console.log(received_msg)
        setData(received_msg);

        console.log('数据已接收...', received_msg);
      };

      // 断开 web socket 连接成功触发事件
      ws.onclose = () => {
        // 关闭 websocket
        console.log('连接已关闭...');
      };
    } else {
      // 浏览器不支持 WebSocket
      console.log('您的浏览器不支持 WebSocket!');
    }
  };

  const sendMessage = () => {
    ws?.send('send message')
  }

  return (
    <>
      <Button onClick={startWs}>测试WS连接</Button>
      <Button onClick={sendMessage}>连接成功后发送信号获得响应数据</Button>
      <div>
        {Object.keys(data).map((key) => (
          <h2>
            {key} : {data[key]}
            <br />
          </h2>
        ))}
      </div>
    </>
  );
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

网络层DoS

网络层是OSI参考模型中的第三层&#xff0c;介于传输层和数据链路层之间&#xff0c;其目的 是实现两个终端系统之间数据的透明传送&#xff0c;具体功能包括&#xff1a;寻址和路由选择、连 接的建立、保持和终止等。位于网络层的协议包括ARP 、IP和ICMP等。下面就 ICMP为例&…

Android13新特性之预测返回手势

Android14新特性之预测返回手势 简介 Android 14引入了对预测性返回手势的支持&#xff0c;这意味着开发者可以通过系统提供的额外动画和API来实现定制化的动画效果。这一更新使得应用程序可以在用户执行返回手势时展示一个动画预览&#xff0c;例如在应用程序前显示Home屏幕…

FRP内网穿透如何避免SSH暴力破解(二)——指定地区允许访问

背景 上篇文章说到&#xff0c;出现了试图反复通过FRP的隧道&#xff0c;建立外网端口到内网服务器TCP链路的机器人&#xff0c;同时试图暴力破解ssh。这些连接造成了流量的浪费和不必要的通信开销。考虑到服务器使用者主要分布在A、B、C地区和国家&#xff0c;我打算对上一篇…

cleanmymacX和腾讯柠檬哪个好用

很多小伙伴在使用Mac时&#xff0c;会遇到硬盘空间不足的情况。遇到这种情况&#xff0c;我们能做的就是清理掉一些不需要的软件或者一些占用磁盘空间较大的文件来腾出空间。我们可以借助一些专门的清理工具&#xff0c;本文中我们来推荐几款好用的Mac知名的清理软件。并且将Cl…

Page 257~258 11.2.6处理“鼠标移动”消息

鼠标在当前窗口上移动&#xff0c;哪怕不单机&#xff0c;也会造成操作系统向该窗口发送消息。鼠标移动消息是WM_MOUSEMOVE,将它加入窗口过程函数的switch-case中&#xff1a; 我们自行编写的回调函数如下&#xff1a; 之前编写的OnPaint()函数也需要修改一下&#xff1a; 几点…

清华系2B模型杀出,性能吊打LLaMA-13B

2 月 1 日&#xff0c;面壁智能与清华大学自然语言处理实验室共同开源了系列端侧语言大模型 MiniCPM&#xff0c;主体语言模型 MiniCPM-2B 仅有 24 亿&#xff08;2.4B&#xff09;的非词嵌入参数量。 在综合性榜单上与 Mistral-7B 相近&#xff0c;在中文、数学、代码能力表现…

c++多态(2)-- 虚函数

我们在多态(1)中说到&#xff0c;多态就是使用父类指针访问子类函数&#xff0c;可以使得代码更加的简便。并且举了一个喂食动物的例子加以说明&#xff0c;我们使用代码进行展示。 enum class _ANIMALS_TYPE {CAT,DOG,ANIMAL_COUNT };class Animal { public:Animal(_ANIMALS_…

2024.2.6

1.现有无序序列数组为23,24,12,5,33,5347&#xff0c;请使用以下排序实现编程 函数1:请使用冒泡排序实现升序排序 函数2:请使用简单选择排序实现升序排序 函数3:请使用快速排序实现升序排序 函数4:请使用插入排序实现升序排序 #include<stdio.h> #include<string.h&g…

Linux操作系统基础(一):操作系统概述

文章目录 操作系统概述 一、计算机分类 二、计算机组成 三、操作系统概述 四、操作系统分类 操作系统概述 一、计算机分类 计算机一般分为个人计算机&#xff08;笔记、台式机&#xff09;与 企业级服务器&#xff08;1U、2U、机柜、塔式、刀片&#xff09;两种形式。 二…

日本的便宜服务器有哪些?

年底之际&#xff0c;无非是云服务器优惠的黄金时期&#xff0c;对于个人用户和独立开发者来说&#xff0c;无论你是搭建个人网站还是个人博客&#xff0c;现在都是行动的好时机。那么&#xff0c;对于这时要入手日本服务器的用户&#xff0c;该怎么找便宜厂商呢&#xff1f;这…

Shell脚本系列| SSH分发公钥方法 - expect脚本的使用

ssh原理&#xff1a;在SSH安全协议的原理中&#xff0c; 是一种非对称加密与对称加密算法的结合。用于确保远程登录和其他网络服务的会话安全&#xff0c;通过非对称加密、会话加密、多重验证机制等手段&#xff0c;保护数据传输的机密性和完整性。 ssh登录有2种方法&#xff1…

P2957

题目描述 The cows enjoy mooing at the barn because their moos echo back, although sometimes not completely. Bessie, ever the excellent secretary, has been recording the exact wording of the moo as it goes out and returns. She is curious as to just how mu…

Qt Windows和Android使用MuPDF预览PDF文件

文章目录 1. Windows MuPDF编译2. Android MuPDF编译3. 引用 MuPDF 库4. 解析本地PDF文件 1. Windows MuPDF编译 使用如下命令将MuPDF的源码克隆到本地 git clone --recursive git://git.ghostscript.com/mupdf.git直接用VS&#xff0c;打开 mupdf/platform/win32/mupdf.sln …

基于Skywalking开发分布式监控(二)

续上篇&#xff0c;上一篇主要是讲了为啥选skywalking&#xff0c;以及怎么有针对性改造SW Agent&#xff0c;现在我们继续看看如何构建自定义Trace跟踪链 要对SW Agent插件做适当剪裁&#xff0c;原来包括customize插件在内SW 8.9有100多个插件&#xff0c;如果没有作用也就罢…

【网络技术】【Kali Linux】Nmap 嗅探(一)简单扫描

一、实验环境 本次实验进行简单的Nmap扫描&#xff0c;实验使用 Kali Linux 虚拟机和 Ubuntu Linux 虚拟机完成&#xff0c;主机操作系统为 Windows 11&#xff0c;虚拟化平台选择 Oracle VM VirtualBox&#xff0c;如下图所示。 二、实验步骤 1、相关配置 Kali Linux 虚拟机…

windows中的apache改成手动启动的操作步骤

使用cmd解决安装之后开机自启的问题 services.msc 0. 这个命令是打开本地服务找到apache的服务名称 2 .通过服务名称去查看服务的状态 sc query apacheapache3.附加上关掉和启动的命令&#xff08;换成是你的服务名称&#xff09; 关掉命令 sc stop apacheapache启动命令 …

ChatGPT 3.5与4.0:深入解析技术进步与性能提升的关键数据

大家好&#xff0c;欢迎来到我的博客&#xff01;今天我们将详细比较两个引人注目的ChatGPT版本——3.5和4.0&#xff0c;通过一些关键数据来深入解析它们之间的差异以及4.0版本的技术进步。 1. 模型规模与参数 ChatGPT 3.5&#xff1a; 参数数量&#xff1a;约1.7亿个模型层数…

07-Java桥接模式 ( Bridge Pattern )

Java桥接模式 摘要实现范例 桥接模式&#xff08;Bridge Pattern&#xff09;是用于把抽象化与实现化解耦&#xff0c;使得二者可以独立变化 桥接模式涉及到一个作为桥接的接口&#xff0c;使得实体类的功能独立于接口实现类&#xff0c;这两种类型的类可被结构化改变而互不影…

CSS之盒子模型

盒子模型 01-选择器 结构伪类选择器 基本使用 作用&#xff1a;根据元素的结构关系查找元素。 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IE…

12 选择排序和堆排序

选择排序 基本思想 每一次从待排序的数据元素中选出最小(或最大)的一个元素&#xff0c;存放在序列的起始位置&#xff0c;直到全部待排序的数据元素排完 直接选择排序 在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素若它不是这组元素的最后一个(第一个)元素…