了解 WebSocket

了解 WebSocket

    • 轮询方式、
    • WebSocket
      • [为什么说 WebSocket 是基于 Http 协议的?](#为什么说 WebSocket 是基于 Http 协议的?)
      • [如何通过 `Sec-WebSocket-Key` 与 验证 `Sec-WebSocket-Accept`](#如何通过 Sec-WebSocket-Key 与 验证 Sec-WebSocket-Accept)
        • [验证 demo](#验证 demo)
    • [SpringBoot 中使用 WebSocket](#SpringBoot 中使用 WebSocket)
    • [前端使用 WebSocket](#前端使用 WebSocket)

轮询方式、

短轮询

实现方式:浏览器以指定的时间向服务器发出 HTTP 请求,服务器实时返回数据给浏览器。

长轮询

HTTP1.1

浏览器发送异步请求,服务端如果没有数据返回则在服务端进行阻塞,有数据返回则立马返回。超时则触发超时机制。

SSE

server-send event 服务器发送事件。

  • SSE 在服务器和客户端之间打开一个单向通道。服务器 -> 客户端。
  • 服务端响应的不再是一次性的数据包。而是 text/event-stream 类型的数据流信息
  • 服务器有数据变更时将数据流式传输到客户端。

WebSocket

WebSocket 是一种基于 TCP 连接进行全双工通信的协议,允许服务器主动向客户端推送信息,客户端也能实时接收服务器的响应。

全双工:允许数据在两个方向上同时传输。TCP 协议是全双工的。

半双工:允许数据在两个方向上传输,但是同一时间段内只允许一个方向上传输。

为什么说 WebSocket 是基于 Http 协议的?

建立全双工通信的关键步骤

  1. 客户端发起 握手请求 :客户端通过 HTTP 请求来开始握手过程,请求中包括 Connection:UpgradeUpgrade:websocketSec-WebSocket-Key:随机的Base64值 等特殊的请求头。
  2. 服务端响应 握手请求 :如果服务器支持 WebSokcet 并接受客户端的请求,它就会响应一个 HTTP 101 Switching Protocols 状态码并会提供 Sec-WebSocket-Accept 响应头信息。
  3. 握手成功,客户端与服务器之间就建立了一个 WebSocket 的连接。并且这个阶段就跟 http 无关了,可以实时双向传输数据。
  • 请求头

    • Upgrade :必须设置为 websocket,表示希望升级到 WebSocket 协议。

    • Sec-WebSocket-Key:一个随机生成的 16 字节的字符串,经过 Base64 编码,用于验证握手的安全性。

    • Sec-WebSocket-Version :指定 WebSocket 协议的版本,必须为 13

    • Sec-WebSocket-Protocol:可选字段,表示客户端希望使用的子协议列表。

    • Sec-WebSocket-Extensions:可选字段,表示客户端希望使用的扩展列表。

  • 响应头

  • HTTP/1.1 101 Switching Protocols:状态行,表示服务器接受了请求并将连接升级。

  • Connection :必须设置为 Upgrade,表示这是一个升级请求。

  • Upgrade :必须设置为 websocket,表示已成功升级到 WebSocket 协议。

  • Sec-WebSocket-Accept :服务器通过对 Sec-WebSocket-Key 进行 SHA-1 哈希计算并 Base64 编码后生成的值,用于验证握手的安全性。

  • Sec-WebSocket-Protocol:可选字段,表示服务器选择的子协议。

  • Sec-WebSocket-Extensions:可选字段,表示服务器选择的扩展。

如何通过 Sec-WebSocket-Key 与 验证 Sec-WebSocket-Accept

258EAFA5-E914-47DA-95CA-C5AB0DC85B11 这个是一个固定的 guid 是 WebSocket 协议规范的一部分,它在 RFC 6455 文档中定义。

  1. Sec-WebSocket-Key+ 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 凭借后转换为 字节序列
  2. 对这个字节序列进行 SHA-1的哈希计算 (不可逆)
  3. 再对加密后的字节序列进行 Base64编码
  4. 比较 Sec-WebSocket-Accept 是否一致,一致代表验证成功
验证 demo
java 复制代码
public static void main(String[] args) {
        try {
            // 客户端提供的 Sec-WebSocket-Key
            String webSocketKey = "q/cmokhHZFPydhuFxTTC/Q==";

            // 固定的 GUID
            String magicGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

            // 拼接字符串
            String concatenated = webSocketKey + magicGuid;

            // 计算 SHA-1 哈希值
            MessageDigest digest = MessageDigest.getInstance("SHA-1");
            byte[] hash = digest.digest(concatenated.getBytes());
            // 进行 Base64 编码
            String webSocketAccept = Base64.getEncoder().encodeToString(hash);

            // 输出计算结果
            System.out.println("编码后 Sec-WebSocket-Accept: " + webSocketAccept);

            // 比较计算结果与提供的 Sec-WebSocket-Accept
            String providedWebSocketAccept = "wPTfN8RfqGIiK9Wgk5jnefJSZA8=";
            if (webSocketAccept.equals(providedWebSocketAccept)) {
                System.out.println("Sec-WebSocket-Accept 标头有效。");
            } else {
                System.out.println("Sec-WebSocket-Accept 标头无效。");
            }
        } catch (NoSuchAlgorithmException e) {
        }
    }

SpringBoot 中使用 WebSocket

引入依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

增加 WebSocketConfig

java 复制代码
@Configuration
@EnableWebSocket  // 开启WebSocket支持
public class WebSocketConfig {

    /**
     * 自动将标注@ServerEndpoint的端点自动注册到WebSocket服务器中
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

修改 ServerEndpointConfig

自定义 WebSocket 服务器端点配置的类,像修改握手请求,设置子协议等。

java 复制代码
/**
 * 获取HttpSession,这样的话,ChatEndpoint类就能操作HttpSession
 */
public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {

    /**
     * 修改握手请求
     * @param serverEndpointConfig
     * @param request
     * @param response
     */
    @Override
    public void modifyHandshake(ServerEndpointConfig serverEndpointConfig, HandshakeRequest request, HandshakeResponse response) {
        // 获取 HttpSession 对象
        HttpSession httpSession = (HttpSession) request.getHttpSession();

        // 将 httpSession 对象保存起来,存到 ServerEndpointConfig 对象中
        // 在 ChatEndpoint 类的 onOpen 方法就能通过 EndpointConfig 对象获取在这里存入的数据
        serverEndpointConfig.getUserProperties().put(HttpSession.class.getName(), httpSession);
    }
}

定义 ServerEndpoint

@ServerEndpoint(value = "/chat", configurator = GetHttpSessionConfig.class)

用于定义 WebSocket 端点,设置 websocket 地址,设置端点的配置

@OnOpen

java 复制代码
@OnOpen
public void onOpen(Session session, EndpointConfig config){
    
}

建立 websocket 连接后,会触发标注@OnOpen 的方法

@OnClose

java 复制代码
@OnClose
public void onClose(Session session, EndpointConfig config){
    
}

关闭 websocket 连接时,会触发标注@OnClose 的方法

@OnMessage

java 复制代码
@OnMessage
public void onMessage(String message, EndpointConfig config) {
    
}

接受消息时,会触发标注@OnMessage 的方法

Session

jakarta.websocket.Session

Session 对象代表了服务器与客户端之间的一个单独的 WebSocket 连接,用来管理链接的生命周期,以及通过这个连接发送和接收数据。
可以向与之链接的对方发送消息。可以发送同步消息与异步消息

java 复制代码
 // 使用 getBasicRemote() 方法发送同步消息   
session.getBasicRemote().sendText(message);

 // 使用 getAsyncRemote() 方法发送异步消息   
session.getAsyncRemote().sendText(message);

样例

java 复制代码
package cn.edu.scau.websocket;

import cn.edu.scau.config.GetHttpSessionConfig;
import cn.edu.scau.model.dto.Message;
import cn.edu.scau.model.dto.OnlineAndOfflineMessage;
import cn.edu.scau.model.dto.OnlineUserMessage;
import cn.edu.scau.model.dto.PrivateChatMessage;
import cn.edu.scau.model.enums.MessageEnum;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import jakarta.servlet.http.HttpSession;
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

@ServerEndpoint(value = "/chat", configurator = GetHttpSessionConfig.class)
@Component
public class ChatEndpoint {
    // 保存在线的用户,key为用户名,value为 Session 对象
    private static final Map<String, Session> onlineUsers = new ConcurrentHashMap<>();

    private HttpSession httpSession;

    /**
         * 建立websocket连接后,被调用
         *
         * @param session Session
         */
    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
        String currentUser = (String) this.httpSession.getAttribute("currentUser");
        if (currentUser != null) {
            onlineUsers.put(currentUser, session);
        }
        Message onlineMessage = new Message();
        onlineMessage.setType(MessageEnum.ONLINE_MESSAGE);
        OnlineAndOfflineMessage onlineAndOfflineMessage = new OnlineAndOfflineMessage();
        onlineAndOfflineMessage.setUser(currentUser);
        onlineMessage.setData(onlineAndOfflineMessage);

        Message message = new Message();
        message.setType(MessageEnum.ONLINE_USER_MESSAGE);
        OnlineUserMessage onlineUserMessage = new OnlineUserMessage();
        onlineUserMessage.setOnlineUsers(getFriends());
        message.setData(onlineUserMessage);
        // 通知所有用户,当前用户上线了
        try {
            Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet();

            for (Map.Entry<String, Session> entry : entries) {
                // 获取到所有用户对应的 session 对象
                Session toSession = entry.getValue();
                // 使用 getBasicRemote() 方法发送同步消息
                toSession.getBasicRemote().sendText(JSON.toJSONString(message));
                if(!session.equals(toSession)){
                    toSession.getBasicRemote().sendText(JSON.toJSONString(onlineMessage));
                }
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

    private Set<String> getFriends() {
        return onlineUsers.keySet();
    }

    private void broadcastAllUsers(String... messages) {
        try {
            Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet();

            for (Map.Entry<String, Session> entry : entries) {
                // 获取到所有用户对应的 session 对象
                Session session = entry.getValue();

                for (String message : messages) {
                    // 使用 getBasicRemote() 方法发送同步消息
                    session.getBasicRemote().sendText(message);
                }
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

    /**
         * 浏览器发送消息到服务端时该方法会被调用,也就是私聊
         * 张三  -->  李四
         *
         * @param message String
         */
    @OnMessage
    public void onMessage(String message) {
        try {
            // 将消息推送给指定的用户
            Message msg = JSON.parseObject(message, Message.class);
            MessageEnum type = msg.getType();
            switch (type){
                case PRIVATE_CHAT_MESSAGE: {
                    PrivateChatMessage privateChatMessage = JSONObject.from(msg.getData()).to(PrivateChatMessage.class);
                    // 获取消息接收方的用户名
                    String toUser = privateChatMessage.getToUser();
                    Session session = onlineUsers.get(toUser);
                    session.getBasicRemote().sendText(message);
                }
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

    /**
         * 断开 websocket 连接时被调用
         *
         * @param session Session
         */
    @OnClose
    public void onClose(Session session) throws IOException {
        // 1.从 onlineUsers 中删除当前用户的 session 对象,表示当前用户已下线
        String currentUser = (String) this.httpSession.getAttribute("currentUser");
        if (currentUser != null) {
            Session remove = onlineUsers.remove(currentUser);
            if (remove != null) {
                remove.close();
            }
            session.close();
        }

        // 2.通知其他用户,当前用户已下线
        // 注意:不是发送类似于 xxx 已下线的消息,而是向在线用户重新发送一次当前在线的所有用户
        Message message = new Message();
        message.setType(MessageEnum.ONLINE_USER_MESSAGE);
        OnlineUserMessage onlineUserMessage = new OnlineUserMessage();
        onlineUserMessage.setOnlineUsers(getFriends());
        message.setData(onlineUserMessage);

        Message onlineMessage = new Message();
        onlineMessage.setType(MessageEnum.OFFLINE_MESSAGE);
        OnlineAndOfflineMessage onlineAndOfflineMessage = new OnlineAndOfflineMessage();
        onlineAndOfflineMessage.setUser(currentUser);
        onlineMessage.setData(onlineAndOfflineMessage);
        // 通知所有用户,当前用户上线了
        broadcastAllUsers(JSON.toJSONString(message),JSON.toJSONString(onlineMessage));
    }
}

前端使用 WebSocket

// 建立 WebSocket 链接

webSocket.value = new WebSocket('ws://127.0.0.1:7024/chat')

// 关闭链接时触发

onclose: ((this: WebSocket, ev: CloseEvent) => any) | null;

// 接受消息时触发

onmessage: ((this: WebSocket, ev: MessageEvent) => any) | null;

// 建立链接时触发

onopen: ((this: WebSocket, ev: Event) => any) | null;

WebSocket 也可以发送消息到服务端

样例

vue 复制代码
const init = async () => {
   // 创建 WebSocket 对象
  webSocket.value = new WebSocket('ws://127.0.0.1:7024/chat')

  webSocket.value.onopen = onOpen

  // 接收到服务端推送的消息后触发
  webSocket.value.onmessage = onMessage

  webSocket.value.onclose = onClose
}

const onOpen = () => {}


const onClose = () => {}


const onMessage = (event) => {}
相关推荐
学传打活5 天前
【边打字.边学昆仑正义文化】_进阶篇5 宇宙星系的运转(续1)
微信公众平台·1024程序员节·汉字·昆仑正义文化
fangzhanpeng16812 天前
快速掌握C#语言基础知识点(1.基础数据类型)
开发语言·c#·1024程序员节
无己心15 天前
面向多平台 AI 搜索引擎的适配层架构设计
前端·人工智能·搜索引擎·ai·1024程序员节
u01030552720 天前
AI驱动的安卓UI自动生成功能实现
人工智能·1024程序员节
u01030552721 天前
长株潭智能体赋能腾讯元器链接共享
1024程序员节
云贝贝贝22 天前
OceanBase认证体系介绍与备考指南
运维·服务器·oceanbase·1024程序员节
u01030552722 天前
Java图像处理实战指南
人工智能·1024程序员节
u01030552723 天前
Java AWT鼠标事件全解析
人工智能·1024程序员节
u01030552725 天前
Java实用类应用精讲
人工智能·1024程序员节
u0103055271 个月前
Java I/O核心操作实战
人工智能·1024程序员节