likes
comments
collection
share

WebSocket 的入门与使用,保证让您看懂!

作者站长头像
站长
· 阅读数 5

一、消息推送的常用方式

  相信大家都在网页上聊过天,就像下面这样。

WebSocket 的入门与使用,保证让您看懂!

  当对面收到消息并回复的时候,这个消息会在没有刷新页面的情况下自动的弹出来,那这是如何做到的呢?我们知道前后端交互大部分场景都是使用的HTTP协议进行的。前端发起请求,后端收到请求再返回所需数据,但是上面的场景是服务器主动推送给我们消息,似乎又跟这个交互模式相矛盾,那到底是怎么样进行通信的呢?

(一)轮询

  浏览器以指定时间间隔向服务器发送HTTP请求,服务器实时返回数据给浏览器。

WebSocket 的入门与使用,保证让您看懂!

  显然这种方式不能够适用上面的场景,它不仅有延迟还很消耗资源。

(二)长论询

  浏览器发出请求,服务器端接收到请求后会阻塞请求直到有数据或者超时才返回。

WebSocket 的入门与使用,保证让您看懂!

  这种方式其实也不太合适上面的场景,假如你给你的女神发消息,结果女神第二天才回你消息,这不一值超时一直重发请求嘛。

(三)WebSocket

  WebSocket是一种在基于TCP连接上进行全双工通信的协议,全双工就是允许数据在两个方向上同时传输数据。

WebSocket 的入门与使用,保证让您看懂!

  没错,这就很适合上面的场景,网页聊天使用的就是 WebSocket 协议,或者说这种通信。那么浏览器与服务器是如何从 HTTP 协议切换到WebSocket协议的呢?

握手阶段(Handshake):

  • 客户端发起一个 HTTP 请求,这个请求包含一个特殊的头部字段 Upgrade: websocket,还有一些其他的 WebSocket 相关的头部字段,例如 Connection: UpgradeSec-WebSocket-Key 等。
  • 服务器收到这个带有特殊头部字段的请求后,如果支持 WebSocket 协议,就会进行协议升级。服务器返回的响应中包含了状态码 101(Switching Protocols),还包括一些与 WebSocket 握手相关的头部字段,例如 Upgrade: websocketConnection: Upgrade
  • 一旦客户端收到带有状态码 101 的响应,说明握手成功,此时连接升级完成,浏览器与服务器之间的通信将升级到 WebSocket 协议。

请求:

GET xxxxxxxx
Host:xxxxxx
Connection:Upgrade 
Upgrade:websocket
Sec-WebSocket-Version:xx
Sec-WebSocket-Key:xxx
xxxxxxxxxxx

响应:

HTTP/1.1 101 Switching Protocols
Connection:Upgrade 
Upgrade:websocket
Sec-WebSocket-Accept:xx
xxxxxxxxxxx

WebSocket 通信阶段:

  • 一旦握手成功,浏览器和服务器之间的通信就切换到了 WebSocket 协议。此时,双方可以通过 WebSocket 协议进行实时双向通信,而不再依赖于传统的请求-响应模型。

二、WebSocket 的协议格式

  学习一个协议,那得先了解它的协议格式。WebSocket的协议格式在RFC 6455: The WebSocket Protocol (rfc-editor.org) 文档中有详细的阐述(第5.2章节),这里我截取了里面的一部分,值得注意的是WebSocket是应用层协议,不是传输层协议。

WebSocket 的入门与使用,保证让您看懂!

  • FIN(1 bit):表示是否要关闭websocket

  • RSV1、2、3(每个 1 bit):保留位

  • opcode(4 bit):描述当前这个websocket数据帧是什么类型

    • %x1:文本数据
    • %x2:二进制
  • MASK(1 bit):是否开启掩码操作。

  • payload length(7 bits, 7+16 bits, or 7+64 bits):载荷长度,也就是数据报上要携带的具体数据的大小。当payload length < 126,此时是模式 1(7bit);如果 7 bit 的值是 126,此时是模式 2(7+16 bits)。如果 7 个 bit 的值是 127,此时是模式 3(7+64 bits)。

  • Masking-key: 0 or 4 bytes,如果屏蔽位设置为0(也就是 MASK 为0),则此字段不存在。这个字段用于对从客户端发送到服务器或从服务器发送到客户端的数据进行掩码处理,其目的是在数据传输过程中增加一定的安全性。

  • Payload Data:存储了实际的数据信息。

二、WebSocket API

(一)客户端 API

  1. 创建 websocket 对象(内置的)
let websocket = new WebSocket(ws://ip地址/访问路径);
  1. 事件处理方法
事件方法名称描述
连接建立onopen当 WebSocket 连接成功建立时触发。
消息接收onmessage当接收到来自服务器的消息时触发。
连接关闭onclose当 WebSocket 连接关闭时触发。
错误发生onerror当 WebSocket 连接发生错误时触发。

示例:

<script>
    let ws = new WebSocket("ws://localhost:8080/xxxx");
    //当 WebSocket 连接成功建立时触发。
    ws.onopen = function(){
        //发送数据
        ws.send(data);
    }
    //当接收到来自服务器的消息时触发,res.data 是服务器返回的消息
    ws.onmessage = function(res){

    }
	//当 WebSocket 连接关闭时触发。
    ws.onclose = function(){
        
    }
    
    //当 WebSocket 连接发生错误时触发。
    ws.onerror = function(event){

    }
</script>
  1. 发送数据send(data): 将数据发送到服务器。

(二)服务端 API

  实现WebSocket的方式有很多种,比如原生jdk注解、Spring封装等,本篇演示的是Spring封装的用法。jdk注解式可以参考:在 Spring Boot 中整合、使用 WebSocket - spring 中文网 (springdoc.cn)

  演示如下👇

三、WebSocket 演示

(一)服务端

  1. 创建一个 SpringBoot 项目。
  2. 引入如下依赖。
<!--    WebSocket API    -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
  1. 写一个demo类继承TextWebSocketHandler类,并重写其中一些方法。TextWebSocketHandler 是一个具体的抽象类,它实现了 WebSocketHandler 接口,并专门用于处理文本消息(Text Messages)。WebSocket协议允许在客户端和服务器之间传输文本消息,而 TextWebSocketHandler 封装了处理这些文本消息的逻辑。通过继承 TextWebSocketHandler,你可以专注于处理文本消息的逻辑,而无需实现整个WebSocketHandler 接口的所有方法。
package com.example.websocket.component;

import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

@Component
public class TestWebSocket extends TextWebSocketHandler {


    /**
     *
     * @param session WebSocketSession的会话
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception 	  {
        //在 websocket 连接建立成功后,被自动调用
        System.out.println("TestWebSocket连接成功!");
    }


    /**
     *
     * @param session WebSocketSession的会话
     * @param message 收到的消息
     * @throws Exception
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        //这个方法是在 websocket 收到消息的时候,被自动调用的
        System.out.println("TestWebSocket收到消息:" + message.toString());
        //session是一个会话,里面就记录了通信双方是谁。
        //发送给客户端
        session.sendMessage(message);
    }

    /**
     *
     * @param session WebSocketSession的会话
     * @param exception 对象记录的异常信息
     * @throws Exception
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        //连接出现异常的时候,被自动调用
        System.out.println("TestWebSocket连接异常!");
    }

    /**
     *
     * @param session WebSocketSession的会话
     * @param status 关闭的状态
     * @throws Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        //连接关闭后自动调用
        System.out.println("TestWebSocket关闭连接!");
    }
}

  1. 配置路由信息,创建一个配置类来实现 WebSocketConfigurer接口。
package com.example.websocket.config;

import com.example.websocket.component.TestWebSocket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;


@Configuration
@EnableWebSocket //启动 WebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private TestWebSocket testWebSocket;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        //通过这个方法,把刚才创建好的 Handler 类注册到具体的路径上
        //当浏览器 websocket 中的请求路径是"/test" 的时候,就会调用到 TestWebSocket 里的方法。
        registry.addHandler(testWebSocket,"/test");
    }
}

(二)客户端

  自己手写一个简单的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>
    <input type="text" id="message">
    <button id="send-button">发送消息</button>

    <script>
        let websocket = new WebSocket("ws://localhost:8080/test");

        //当 WebSocket 连接成功建立时触发。
        websocket.onopen = function () {
            //发送数据
            console.log("websocket连接成功!");
        }

        //当 接收到来自服务器的消息时触发,res.data 是服务器返回的消息
        websocket.onmessage = function (res) {
            console.log("websocket收到消息!" + res.data);
        }

        //当 WebSocket 连接关闭时触发。
        websocket.onclose = function () {
            console.log("websocket连接断开!");
        }

        //当 WebSocket 连接发生错误时触发。
        websocket.onerror = function (event) {
            console.log("websocket连接异常!");
        }

        //发送消息
        let sendButton = document.querySelector('#send-button');
        let message = document.querySelector('#message');
        sendButton.onclick = function(){
            console.log("websocket发送消息"+message.value);
            websocket.send(message.value);
        }
    </script>
</body>
</html>

WebSocket 的入门与使用,保证让您看懂!

  启动项目,并访问:

WebSocket 的入门与使用,保证让您看懂!

WebSocket 的入门与使用,保证让您看懂!

  成功!

(三)WebSocketSession 与 TextMessage

  WebSocketSession 通常包含有关连接的信息,例如连接的 ID、协议版本、URI、和其他与连接相关的属性。通过 WebSocketSession,我们可以在服务器端处理 WebSocket连接,接收来自客户端的消息,发送消息到客户端,关闭连接等操作。

下面是常用的方法:

方法名描述
String getId()获取 WebSocket 连接的唯一标识符。
URI getUri()获取 WebSocket 连接的 URI。
boolean isOpen()检查 WebSocket 连接是否打开。
void sendMessage(WebSocketMessage<?> message)发送 WebSocket 消息到客户端。
void close()关闭 WebSocket 连接。
void setTextMessageSizeLimit(int messageSizeLimit)设置文本消息的大小限制。
int getTextMessageSizeLimit()获取文本消息的大小限制。
void close(CloseStatus status)使用指定的关闭状态关闭 WebSocket 连接。

  TextMessage用于表示文本消息的类,它是 WebSocketMessage 接口的一个实现,用于在 WebSocket 通信中传递文本数据。

方法名描述
String getPayload()获取文本消息的内容。
byte[] asBytes()将文本消息的内容转换为字节数组。
String toString()返回 TextMessage 对象的字符串表示。

(四)实现群发

  如何实现群发呢?或者说如何让服务器群发给所有连接的客户端?其实只需要将所有的websocketsession存起来,下面就给一个简单的demo,这里演示的是Map。如果客户端比较多,可以使用 Redis 来存。下面是一个工具类,用来操作Map

public class WebSocketMap {
    //用来存储会话信息
    public static final Map<Integer,WebSocketSession> map = new ConcurrentHashMap<>();

    /**
     * 添加会话
     */
    public static WebSocketSession put(Integer id, WebSocketSession session){
        return map.put(id,session);
    }

    /**
     * 删除会话,并返回删除的会话,但是没有断开连接!
     */
    public static WebSocketSession remove(Integer id){
        return map.remove(id);
    }

    /**
     * 删除并断开连接
     * @param id
     */
    public static void removeAndClose(Integer id){
        //在缓存中删除会话
        WebSocketSession webSocketSession = remove(id);
        //保证会话是连接的,再进行删除
        if(webSocketSession != null && webSocketSession.isOpen()){
            try {
                //断开连接
                webSocketSession.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    /**
     * 获取会话
     */
    public static WebSocketSession get(Integer id){
        return map.get(id);
    }

    /**
     * 给指定用户发送消息
     */
    public static void send(Integer id,String msg) throws IOException {
        //获取指定用户
        WebSocketSession session = map.get(id);
        if(session != null && session.isOpen()){
            //发送消息
            session.sendMessage(new TextMessage(msg));
        }else {
            //处理错误
            System.out.println("session为null或者连接已断开");
        }
    }

    /**
     * 群发功能
     */
    public static void sendAll(String msg) throws IOException {
        //遍历每一个会话
        for(WebSocketSession session : map.values()){
            if(session != null && session.isOpen()){
                session.sendMessage(new TextMessage(msg));
            }
        }
    }
}

(五)获取 HTTP 的 HttpSession 数据

  升级为WebSocket协议后,不能直接获取 HTTPSession了,但是可以利用WebSocketSession来获取HttpSessionWebSocketSession对象提供了一个getAttributes()方法,该方法返回一个Map,这个Map就是HttpSession存储的键值对。

前提:注册一个拦截器。

@Configuration  
@EnableWebSocket  
public class WebSocketConfig implements WebSocketConfigurer {  
  
@Resource  
private WebSocketComponent webSocketComponent;  
  
    @Override  
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {  
        //通过这个方法,把刚才创建好的 Handler 类注册到具体的路径上
        //当浏览器 websocket 中的请求路径是"/test" 的时候,就会调用到 TestWebSocket 里的方法。
        registry.addHandler(webSocketComponent,"/test")  
        //注册这个特定的 HttpSession 拦截器,就可以把 HttpSession 中的键值对也添加到 websocketsession 中来。  
        .addInterceptors(new HttpSessionHandshakeInterceptor());  
    }  
}

注册后,直接获取 HttpSession中的值。

  HttpSessionHandshakeInterceptor的作用是在WebSocket握手阶段进行拦截,它会在WebSocket连接建立之前的握手过程中执行一些逻辑。在这个过程中,它可以访问原始的HTTP请求和响应对象,从中提取HttpSession,并将其属性复制到WebSocketSession中。

  获取拦截的数据:


@Component
public class TestWebSocket extends TextWebSocketHandler {
    //......................
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        //这个方法是在 websocket 收到消息的时候,被自动调用的

        //getAttributes() 的返回值是 Map,这个Map就是 HttpSession 装的键值对
        Map<String, Object> map = session.getAttributes();
        //假如在前面的登录业务中已经存了“user”这个session,这样就得到了 HttpSession 中的 User 对象。
        User user = (User) map.get("user");
    }
    //......................
}