【JavaScript】WebSocket

概述

Websocket 特点

WebSockets是一种在浏览器和服务器之间建立持久连接的协议,它允许服务器主动推送数据给客户端,并且在客户端和服务器之间实现双向通信。

  1. 建立连接:客户端使用WebSocket对象来建立WebSocket连接。例如:const socket = new WebSocket('ws://example.com/socket');

  2. 协议:WebSocket使用标准的WebSocket协议来进行通信。相对于HTTP协议,WebSocket协议提供了更高效率和更低的延迟。ws和wss协议正对应了http和https。

  3. 事件:WebSocket对象提供了一些事件,用于处理连接的不同阶段和接收到的数据。常见的事件有onopen(连接建立)、onmessage(接收到消息)、onclose(连接关闭)等。

  4. 数据传输:WebSocket可以传输文本和二进制数据。文本数据是以UTF-8编码发送和接收的,而二进制数据使用ArrayBuffer对象。

  5. 安全性:与HTTP协议通过TLS/SSL协议进行加密一样,WebSockets也可以通过wss协议来提供安全的通信。wss://表示通过TLS/SSL加密的WebSockets连接。

  6. 跨域策略:WebSockets默认不受同源策略的限制,可以跨域使用,但仍然会受到服务器的安全策略限制。

可以在浏览器和服务器之间建立稳定的连接,实际应用如聊天应用、实时数据监控等。

Websocket 握手

Websocket

当客户端需要和服务器使用WebSocket进行通信时,首先会使用HTTP协议完成一次特殊的请求响应,这一次请求-响应就是WebSocket握手。完成握手之后,后续才会使用 websocket 协议。在握手阶段,首先由客户端向服务器发送一个请求,请求地址格式如下:

在这里插入图片描述

请求头:

在这里插入图片描述

服务器如果同意,就响应:

在这里插入图片描述

基本使用

使用 WebSocket 进行连接和通信:

首先,在客户端使用 WebSocket 对象来建立连接:

const socket = new WebSocket('ws://example.com/socket');

// 监听连接建立事件
socket.onopen = function() {
  console.log('WebSocket连接已建立');
  
  // 向服务器发送消息
  socket.send('Hello Server!');
};

// 监听接收到消息事件
socket.onmessage = function(event) {
  const message = event.data;
  console.log('接收到服务器消息:' + message);
};

// 监听连接关闭事件
socket.onclose = function() {
  console.log('WebSocket连接已关闭');
};

在上面的代码中,我们创建了一个WebSocket对象并指定连接的URL。然后我们监听了三个事件:onopen表示连接建立时触发的事件,onmessage表示接收到消息时触发的事件,onclose表示连接关闭时触发的事件。

在连接建立后,我们向服务器发送了一条消息Hello Server!。当服务器向客户端发送消息时,onmessage事件将被触发,并且我们可以在回调函数中处理接收到的消息。

接下来,让我们看一下服务器端的示例代码(以Node.js和Express框架为例):

const express = require('express');
const WebSocket = require('ws');

const app = express();

// 创建WebSocket服务器
const server = require('http').createServer(app);
const wss = new WebSocket.Server({ server });

wss.on('connection', function(socket) {
  console.log('WebSocket连接已建立');

  // 监听客户端发送的消息
  socket.on('message', function(message) {
    console.log('接收到客户端消息:' + message);

    // 向客户端发送消息
    socket.send('Hello Client!');
  });

  // 监听连接关闭事件
  socket.on('close', function() {
    console.log('WebSocket连接已关闭');
  });
});

server.listen(3000, function() {
  console.log('服务器已启动,监听端口3000');
});

在服务器端的代码中,我们使用了ws库创建了一个WebSocket服务器,并监听了connection事件。当有客户端连接到服务器时,connection事件将被触发,并在该回调函数中处理连接过程。

在服务器接收到客户端的消息时,我们输出消息到控制台,并向客户端发送一条回复消息。当连接关闭时,close事件将被触发。

最后,我们在Express应用中创建了一个HTTP服务器,将WebSocket服务器绑定到服务器上,然后启动服务器监听端口3000。

通信小实例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
    const ws = new WebSocket('ws://localhost:3000')
    ws.addEventListener('open',()=> {
        console.log('已连接服务器')
        ws.send('我是前端的数据')
    })
    ws.addEventListener('message', ({data})=> {
        console.log(data)
    })
</script>
</body>
</html>
const WebSocket = require('ws')
const socket = new WebSocket.Server({port: 3000});

socket.on('connection',ws=> {
    console.log('我进来了')
    ws.on('message',(data)=> {
        console.log('前端数据:' + data + "。后端返回数据:"+ '我是后端的数据')
    })
    ws.on('close',()=> {
        console.log('溜了溜了')
    })
})

image.png

Socket.io

原生 websocket 有一个问题,连接双方可以在任何时候发送任何类型的数据,但是另一方必须清楚这个数据的含义,比如是谁发送过来的。

http 协议通过 path 请求路径来区分这个问题。Socket.io 为了解决这个问题,将不同的消息放入到不同的事件中,这些事件的类型是由我们自己定义的。

Socket.io 具有跨平台,实时性,WebSocket 不可连接时自动降级到其他传播机制(如 http 长轮询)等优点。由于 socket.io 对消息格式进行了特殊处理,所以如果一方使用了 socket.io 那么另一方也需要使用 这个库。

socket.io 有一些预定义的事件名,比如 message, connect 等,不可以作为我们自定义的事件名。

除此之外,Socket.io对低版本浏览器还进行了兼容处理。如果浏览器不支持WebSocket,Socket.io将使用长轮询(long polling)处理。另外,Socket.io还支持使用命名空间来进一步隔离业务。

官方文档:(服务器初始化 | Socket.IO)

npm install socket.io 安装。

聊天室案例

在使用框架的时候,我们可以使用 socket.io-client 这个库来简化实现。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {
            padding: 0;
            margin: 0;
        }

        html,
        body,
        .room {
            height: 100%;
            width: 100%;
        }

        .room {
            display: flex;
        }

        .left {
            width: 300px;
            border-right: 0.5px solid #2b2d30;
            background: #333;
        }

        .right {
            background: #1c1c1c;
            flex: 1;
            display: flex;
            flex-direction: column;
        }

        .header {
            background: rgb(60, 63, 65);
            color: white;
            padding: 10px;
            box-sizing: border-box;
            font-size: 20px;
        }

        .main {
            flex: 1;
            padding: 10px;
            box-sizing: border-box;
            font-size: 20px;
            overflow: auto;
        }

        .main-chat {
            color: #bcbec4;
        }

        .footer {
            min-height: 200px;
            border-top: 1px solid #4e5157;
        }

        .footer .ipt {
            width: 100%;
            height: 100%;
            color: #bcbec4;
            outline: none;
            font-size: 20px;
            padding: 10px;
            box-sizing: border-box;
        }

        .groupList {
            height: 100%;
            overflow: auto;
        }

        .groupList-items {
            height: 50px;
            width: 100%;
            background: rgb(67, 69, 74);
            border-bottom: 1px solid #2b2d30;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
        }
    </style>
</head>
<div class="room">
    <div class="left">
        <div class="groupList">

        </div>
    </div>
    <div class="right">
        <header class="header">聊天室</header>
        <main class="main">

        </main>
        <footer class="footer">
            <div class="ipt" contenteditable></div>
        </footer>
    </div>
</div>

<body>
<script type="module">
    import {io} from "https://cdn.socket.io/4.7.4/socket.io.esm.min.js";

    let name = prompt('请输入你的名字')
    let room = prompt('请输入你要进入的房间')
    const sendMessage = (message) => {
        const div = document.createElement('div');
        div.className = 'main-chat';
        div.innerText = `${message.user}:${message.text}`;
        main.appendChild(div)
    }
    const group = document.querySelector('.groupList');  // 左侧列表的父级
    const main = document.querySelector('.main');  // 消息内容的父级
    const ipt = document.querySelector('.footer .ipt')
    // 希望自己也能收到信息
    const addChart = (msg)=> {
        let item = document.createElement('div')
        item.className = 'main-chat'
        item.innerHTML = `${msg.name}: ${msg.message}`
        main.appendChild(item)
    }
    const socket = io('ws://localhost:3000')  // ws 的地址
    socket.on('connect', () => {
        // 监听回车事件
        document.addEventListener('keydown',e=> {
            if (e.key === 'Enter') {
                const message = ipt.innerText
                socket.emit('message', {name, message, room})
                addChart({name, message})
                ipt.innerText = ''
            }
        })
        // 加入房间
        socket.emit('join', {name, room})
        //监听左侧列表
        socket.on('groupMap', (data) => {
            group.innerHTML = ``
            Object.keys(data).forEach(key=> {
                const item = document.createElement('div')
                item.className = 'groupList-items'
                item.innerHTML = `房间号:${key} 房间人数:${data[key].length}`
                group.appendChild(item)
            })
        })
        //监听发送的消息
        socket.on('message', (data) => {
            addChart(data)
        })
    })
</script>
</body>

</html>
import {createServer} from "http";
import {Server} from "socket.io";

const server = createServer();
const io = new Server(server, {
    cors: true // 允许跨域
});
/**
 * [{name, room, id}, {name, room, id}]
 * @type {{}}
 */
const groupMap = {}

io.on("connection", (socket) => {
    // 前端传过来 name 和 room
    socket.on('join', ({name, room}) => {
        socket.join(room) // 创建一个房间
        if (groupMap[room]) {
            groupMap[room].push({name, room, id: socket.id}) // 已有就 push
        } else {
            groupMap[room] = [{name, room, id: socket.id}]  // 没有就创建
        }
        socket.emit('groupMap', groupMap)
        // 所有人都可以看到
        socket.broadcast.emit('groupMap', groupMap)
        // 管理员发送信息
        socket.broadcast.to(room).emit('message', {
            name: '管理员',
            message: `欢迎${name}进入聊天室`
        })
    })
    // 向前端发消息
    socket.on('message', ({name, message, room}) => {
        // 虽然给别人发送了消息,但是自己收不到
        socket.broadcast.to(room).emit('message', {name, message})
    })
});

server.listen(3000);

面试题

1. webSocket 协议是什么,能简述一下吗?

websocket 协议是 HTML5 带来的新协议,相对于http,它是一个持久连接的协议,它利用 http 协议完成握手,然后通过 TCP 连接通道发送消息,允许客户端和服务端进行全双工通信,使用 websocket 协议可以实现服务器主动推送消息。默认端口也是 80 和 443 。

首先,客户端若要发起websocket连接,首先必须向服务器发送http 请求以完成握手,请求行中的path需要使用ws:开头的地址,请求头中要分别加入 upgrade.connection、Sec-WebSocket-Key、Sec-WebSocket-Version 等请求头标记。

然后,服务器收到请求后,发现这是一个 websocket 协议的握手请求,于是响应行中包含 Switching Protocols ,同时响应头中包含upgrade、connection、.Sec-WebSocket-Accept 等响应头标记。

当客户端收到响应后即可完成握手,随后使用建立的 TCP 连接直接发送和接收消息。

其他特点有数据格式比较轻量,性能开销小,通信高效,可以发送文本,也可以发送二进制数据。没有同源策略的限制,完全可以取代 ajax,协议标识符是 ws 和 wss,服务求网址就是 url。

2. webSocket 与传统的 http 有什么优势?

当页面中需要观察实时数据的变化(比如聊天、k 线图)时,过去我们往往使用两种方式完成:

第一种是短轮询,即客户端每隔一段时间就向服务器发送消息,询问有没有新的数据。

第二种是长轮询,发起一次请求询问服务器,服务器可以将该请求挂起,等到有新消息时再进行响应。响应后,客户端立即又发起一次请求,重复整个流程。

无论是哪一种方式,都暴露了 http 协议的弱点,即响应必须在请求之后发生,服务器是被动的,无法主动推送消息。而让客户端不断的发起请求又白白的占用了资源。

websocket 的出现就是为了解决这个问题,它利用 http 协议完成握手之后,就可以与服务器建立持久的连接,服务器可以在任何需要的时候,主动推送消息给客户端,这样占用的资源最少,同时实时性也最高。

3. 前端如何实现即时通讯?

  1. 短轮询。即客户端每隔一段时间就向服务器发送消息,询问有没有新的数据
  2. 长轮询,发起一次请求询问服务器,服务器可以将该请求挂起,等到有新消息时再进行响应。响应后,客户端立即又发起一次请求,重复整个流程。
  3. websocket,握手完毕后会建立持久性的连接通道,随后服务器可以在任何时候推送新消息给客户端

4. Socket 和 WebSocket 的区别?

TCP/IP协议是传输层协议,主要解决数据如何在网络中传输,Socket是对TCP/IP协议的封装和应用,在软件上的体现可以理解为nodejs的内置模块 socket,可以去操作TCP。

当两台主机通信时,必须通过Socket连接,Socket则利用TCP/IP协议建立TCP连接。TCP连接则更依靠于底层的IP协议,IP协议的连接则依赖于链路层等更低层次。

Socket(套接字)并不是一个独立的协议,而是位于应用层和传输层之间的一组编程接口(API)。它为应用程序提供了访问底层网络通信能力的方式,通常用于基于 TCP 或 UDP 协议的网络通信。(腾讯云, IBM)

在 OSI 七层模型中,Socket 并不直接对应某一层,而是作为应用层与传输层之间的抽象层。它封装了复杂的 TCP/IP 协议操作,提供了统一的接口,使得开发者可以通过调用 Socket 接口来实现网络通信,而无需深入了解底层协议的细节。(goeasy.io)

因此,Socket 主要被视为应用层与传输层之间的桥梁,属于应用层编程范畴。它使得应用程序能够利用传输层的服务(如 TCP 或 UDP)进行数据传输。(腾讯云)

5. WebSocket协议与HTTP长连接的核心区别

  1. 通信模式差异
    • HTTP长连接:基于"请求-响应"模型,客户端必须主动发起请求,服务端才能响应,无法实现服务端主动推送数据。

    • WebSocket:全双工通信,双方均可主动发送数据,适合实时交互场景(如IM通讯场景)。

  2. 协议升级机制
    • HTTP长连接:直接复用TCP连接传输多个HTTP请求,无需协议升级(HTTP/1.1默认支持持久连接)。

    • WebSocket:通过HTTP协议握手(状态码101)升级为WebSocket协议,后续通信脱离HTTP规范。

  3. 数据头部开销
    • HTTP长连接:每次请求需携带完整HTTP头部(如Cookie、User-Agent等),导致带宽浪费(HTTP头部冗余问题)。

    • WebSocket:建立连接后仅需2-10字节的轻量级帧头(二进制帧结构)。

  4. 连接生命周期
    • HTTP长连接:空闲超时自动断开(例如Apache默认5-15秒),需客户端定期轮询。

    • WebSocket:持久连接直到显式关闭。

  5. 适用场景
    • HTTP长连接:适合减少重复建立连接的资源消耗,但无法突破单向通信限制(HTTP/2多路复用场景)。

    • WebSocket:专为实时双向通信设计(在线游戏、聊天室等场景)。

6. WebSocket 开发中的常见问题与解决方案

一、​​心跳机制失效​

​问题表现​​:
连接因网络波动或服务器超时自动断开,但客户端未及时感知,导致消息丢失或通信中断。
​解决方案​​:

  1. ​双向心跳检测​​:

    • 客户端与服务端约定固定间隔(如30秒)发送 ping 消息,对方回复 pong 确认存活。若连续多次未收到响应,则触发重连。

    • ​示例代码​​(客户端):

      let heartbeatTimer;
      function startHeartbeat() {
        heartbeatTimer = setInterval(() => {
          if (socket.readyState === WebSocket.OPEN) {
            socket.send('ping');
          }
        }, 30000);
      }
      socket.addEventListener('open', startHeartbeat);
      socket.addEventListener('message', (event) => {
        if (event.data === 'pong') resetHeartbeat(); // 收到响应后重置计时
      });
      
    • ​服务端逻辑​​:若超时未收到心跳,主动断开连接并释放资源。

  2. ​动态调整心跳间隔​​:
    根据网络质量动态调整心跳频率(如网络差时延长间隔),减少无效流量消耗。


二、​​重连机制不稳定​

​问题表现​​:
连接断开后重连失败或频繁重试导致服务器压力激增。
​解决方案​​:

  1. ​指数退避策略​​:

    • 每次重连间隔按指数增长(如1s → 2s → 4s),直至达到最大间隔(如30秒)或最大尝试次数(如10次)。

    • ​示例代码​​:

      let retryCount = 0, maxRetries = 10, baseDelay = 1000;
      function reconnect() {
        if (retryCount >= maxRetries) return;
        const delay = Math.min(baseDelay * 2 ** retryCount, 30000);
        setTimeout(() => {
          createWebSocket(); // 创建新连接
          retryCount++;
        }, delay);
      }
      
  2. ​网络状态感知​​:

    • 监听浏览器 online/offline 事件,网络恢复后立即触发重连。
    • 结合心跳检测区分“短暂断开”与“永久故障”,避免无效重试。

三、​​会话恢复与消息丢失​

​问题表现​​:
重连后历史会话状态丢失,或未确认消息未能重传。
​解决方案​​:

  1. ​消息缓冲队列​​:

    • 客户端在本地缓存未确认的消息,重连成功后按顺序重发。

    • ​示例​​:

      let messageQueue = [];
      socket.onclose = () => {
        messageQueue = [...pendingMessages]; // 保存未发送消息
        reconnect();
      };
      socket.onopen = () => {
        messageQueue.forEach(msg => socket.send(msg)); // 重连后重发
      };
      
  2. ​服务端状态同步​​:

    • 客户端在重连时携带会话ID(如JWT),服务端恢复上下文状态。
    • 使用唯一标识(如用户ID+设备ID)确保消息路由正确。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秀秀_heo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值