如何使用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>
    </>
  );
}
相关推荐
哎呦不错哦.几秒前
简单园区网拓扑实验
网络·智能路由器
hgdlip26 分钟前
手机IP地址:定义、查看与切换方法
网络协议·tcp/ip·智能手机
dengjiayue4 小时前
tcp 的重传,流量控制,拥塞控制
网络协议·tcp/ip
咕德猫宁丶5 小时前
探秘Xss:原理、类型与防范全解析
java·网络·xss
黑子哥呢?5 小时前
Linux---防火墙端口设置(firewalld)
linux·服务器·网络
hellojackjiang20116 小时前
开源轻量级IM框架MobileIMSDK的鸿蒙NEXT客户端库已发布
网络·即时通讯·im开发·mobileimsdk-鸿蒙端
WebDeveloper20016 小时前
如何使用美国域名中心US Domain Center和WordPress创建商业网站
运维·服务器·css·网络·html
车载诊断技术8 小时前
电子电气架构 --- 什么是EPS?
网络·人工智能·安全·架构·汽车·需求分析
KevinRay_8 小时前
Python超能力:高级技巧让你的代码飞起来
网络·人工智能·python·lambda表达式·列表推导式·python高级技巧
2301_819287129 小时前
ce第六次作业
linux·运维·服务器·网络