简介

Restful API是基于请求-响应模式的单向通信,而 WebSocket 提供全双工通信渠道,允许客户端和服务器之间进行实时双向数据传输,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。WebSocket 通信协议于 2011 年被IETF 定为标准 RFC 6455,并由 RFC 7936 补充规范。WebSocket API 也被 W3C 定为标准。

定义

  1. 基于 TCP 的独立协议
    WebSocket 是一种独立的应用层协议,基于 TCP 传输层实现,专为全双工实时通信设计。
  2. HTTP 兼容的握手机制
    通过 HTTP/1.1 101 Switching Protocols 状态码完成握手,建立持久连接后,通信脱离 HTTP 约束,直接使用 WebSocket 协议传输数据。
  3. 双向连接的建立流程
    • 客户端发起握手:浏览器发送带有 Upgrade: websocket 头的 HTTP 请求。
    • 服务端确认升级:服务器验证请求后返回 101 响应,协议切换为 WebSocket。
    • 持久化双向通道:握手成功后,TCP 连接复用,客户端与服务端可随时主动发送数据,无需重复握手。

核心优势
✔ 低延迟双向通信 | ✔ 减少 HTTP 冗余开销 | ✔ 服务端主动推送 | ✔ 标准化(RFC 6455 + W3C API)

与HTTP的区别

传统 HTTP

  • HTTP 是基于请求/响应模型的,客户端发送请求,服务器返回响应,然后连接关闭(除非使用 Connection: keep-alive)。
  • 每次请求都需要重新建立连接(或复用连接),并且请求和响应是独立的。

WebSocket

  • WebSocket 是全双工的,客户端和服务器可以在任何时候互相发送数据。
  • 数据传输是通过 WebSocket 帧(frames)进行的,帧可以是文本帧(UTF-8 编码)或二进制帧。
  • WebSocket 的连接是持久的,直到客户端或服务器主动关闭连接

Request Headers

  • 典型的 HTTP 请求头包含以下字段:

    GET /path HTTP/1.1
    Host: example.com
    Connection: keep-alive
    Accept: */*
    User-Agent: Mozilla/5.0
    
  • WebSocket 的初始握手请求头是基于 HTTP 的,但包含一些特殊的字段:

    GET /chat HTTP/1.1
    Host: example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Sec-WebSocket-Version: 13
    
    • Upgrade: websocket:表示客户端希望升级到 WebSocket 协议。
    • Connection: Upgrade:表示客户端希望升级连接。
    • Sec-WebSocket-Key:一个随机生成的 Base64 编码字符串,用于握手验证。
    • Sec-WebSocket-Version:指定 WebSocket 协议的版本(通常是 13)。

Request Body

传统 HTTP 请求体

  • 在 HTTP 请求中,请求体通常用于传输数据(如 POSTPUT 请求中的表单数据、JSON 数据等)

    {
      "name": "John",
      "age": 30
    }
    

WebSocket 请求体

  • WebSocket 的初始握手请求没有请求体,因为握手是通过请求头完成的。
  • 一旦握手成功,WebSocket 会切换到二进制帧协议,后续的数据传输不再使用 HTTP 的请求体格式,而是通过 WebSocket 帧(frames)来传输数据。
  • WebSocket 的初始握手是基于 HTTP 的,但一旦握手成功,协议会从 HTTP 切换到 WebSocket 协议(ws://wss://)。
  • 切换后的 WebSocket 协议不再使用 HTTP 的请求/响应模型,而是使用帧(frames)来传输数据。

Response Headers

  • 典型的 HTTP 响应头如下:

    HTTP/1.1 200 OK
    Content-Type: text/html
    Content-Length: 1234
    
    • 响应头包含状态码(如 200 OK)、内容类型(Content-Type)、内容长度(Content-Length)等信息。
  • WebSocket 的握手响应头如下:

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
    
    • 101 Switching Protocols:表示服务器同意升级到 WebSocket 协议。
    • Upgrade: websocketConnection: Upgrade:表示连接已升级。
    • Sec-WebSocket-Accept:服务器根据客户端的 Sec-WebSocket-Key 计算出的值,用于验证握手。
特性 传统 HTTP WebSocket
请求头 包含方法、路径、Host 等 包含 UpgradeSec-WebSocket-Key
请求体 用于传输数据(如 JSON、表单数据) 无(握手阶段无请求体)
响应头 包含状态码、Content-Type 等 包含 101 Switching Protocols
响应体 包含实际数据(如 HTML、JSON) 无(握手阶段无响应体)
数据传输方式 请求/响应模型,非持久连接 全双工,持久连接,基于帧传输
协议 HTTP/HTTPS WebSocket(ws://wss://

Java的Servlet规范从3.1开始支持WebSocket,所以,必须选择支持Servlet 3.1或更高规范的Servlet容器,才能支持WebSocket。最新版本的Tomcat、Jetty等开源服务器均支持WebSocket。

入门案例

聊天室

一个 Spring Boot 集成 WebSocket 的经典案例,实现一个简单的实时聊天应用。

项目结构

chat-demo/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/
│   │   │       └── example/
│   │   │           └── chatdemo/
│   │   │               ├── config/
│   │   │               │   └── WebSocketConfig.java
│   │   │               ├── controller/
│   │   │               │   └── ChatController.java
│   │   │               ├── model/
│   │   │               │   └── Message.java
│   │   │               └── ChatDemoApplication.java
│   │   └── resources/
│   │       ├── static/
│   │       │   └── index.html
│   │       └── application.yaml
│   └── test/
└── pom.xml

添加依赖

在pom.xml中添加WebSocket和前端相关依赖:

<!-- Spring Boot Starter Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Spring Boot Starter WebSocket -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

WebSocket配置类

创建WebSocket配置类WebSocketConfig.java

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 启用简单的内存消息代理,前缀为/topic的消息将发送到消息代理
        config.enableSimpleBroker("/topic");
        // 设置应用程序目的地前缀为/app
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 注册STOMP端点,客户端将使用它连接到WebSocket服务器
        registry.addEndpoint("/ws").withSockJS();
    }
}
  • @EnableWebSocketMessageBroker 启用WebSocket消息代理

消息模型

创建消息模型Message.java

public class Message {
    private String from;
    private String content;
    @Override
    public String toString() {
        return "Message{" +
                "from='" + from + '\'' +
                ", content='" + content + '\'' +
                '}';
    }

    // 构造方法、getter和setter    
}

控制器

创建聊天控制器ChatController.java

import com.example.chatdemo.model.Message;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

@Controller
public class ChatController {

    // 处理来自客户端的消息
    @MessageMapping("/chat.sendMessage")
    @SendTo("/topic/public")
    public Message sendMessage(Message chatMessage) {
        return chatMessage;
    }

    // 处理用户加入的通知
    @MessageMapping("/chat.addUser")
    @SendTo("/topic/public")
    public Message addUser(Message chatMessage) {
        chatMessage.setContent("用户 " + chatMessage.getFrom() + " 加入了聊天室!");
        return chatMessage;
    }
}

1、@MessageMapping 定义消息接收端点。监听客户端发送到特定目的地的STOMP消息。类似Spring MVC 中的@RequestMapping,但专用于WebSocket消息

@MessageMapping("/chat.sendMessage") // 监听/app/chat.sendMessage
public void handleMessage(Message message, SimpMessageHeaderAccessor headers) {
    // message: 自动从JSON反序列化的对象
    // headers: 包含STOMP帧的头部信息(如sessionId)
}

支持的参数类型:

参数类型 用途
@Payload Message 消息体(自动反序列化)
@Header("custom") String 获取特定STOMP头
SimpMessageHeaderAccessor 完整访问消息头
Principal 认证用户信息

2、@SendTo 指定消息发送目的地。指定方法返回值的发送目的地,将返回值自动序列化(如JSON)并发送到指定主题/队列。

关键特性

  • 动态路径支持:可使用SpEL表达式(如@SendTo("/topic/{message.type}")
  • 自动广播:发送到代理(如/topic)的消息会被所有订阅该目的地的客户端接收
  • 返回值处理:非void返回值才会触发发送
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public") // 返回值发送到/topic/public
public Message broadcastMessage(Message message) {
    return message; // 自动转为JSON发送
}

前端页面

src/main/resources/static/index.html创建前端页面:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>WebSocket 聊天演示</title>
    <script src="https://unpkg.com/sockjs-client@1.5.0/dist/sockjs.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
        }

        #login-container, #chat-container {
            max-width: 600px;
            margin: 0 auto;
        }

        #chat-container {
            display: none;
        }

        #message-container {
            border: 1px solid #ccc;
            height: 300px;
            overflow-y: scroll;
            padding: 10px;
            margin-bottom: 10px;
        }

        #message-form {
            display: flex;
        }

        #message-input {
            flex-grow: 1;
            padding: 8px;
        }

        button {
            padding: 8px 15px;
            background-color: #4CAF50;
            color: white;
            border: none;
            cursor: pointer;
        }

        #username-display {
            margin-bottom: 10px;
            font-weight: bold;
            color: #4CAF50;
        }
    </style>
</head>
<body>
<div id="login-container">
    <h1>欢迎加入聊天室</h1>
    <form id="login-form">
        <input type="text" id="username-input" placeholder="请输入您的用户名" required/>
        <button type="submit">进入聊天室</button>
    </form>
</div>

<div id="chat-container">
    <h1>Spring Boot WebSocket 聊天演示</h1>
    <div id="username-display"></div>
    <div id="message-container"></div>
    <form id="message-form">
        <input type="text" id="message-input" placeholder="输入消息..." autocomplete="off"/>
        <button type="submit">发送</button>
    </form>
</div>

<script>
    let stompClient = null;
    let username = null;

    // 登录处理
    document.getElementById('login-form').addEventListener('submit', function (e) {
        e.preventDefault();
        username = document.getElementById('username-input').value.trim();

        if (username) {
            connect();
            document.getElementById('login-container').style.display = 'none';
            document.getElementById('chat-container').style.display = 'block';
            document.getElementById('username-display').textContent = '您当前的用户名: ' + username;
        }
    });

    function connect() {
        const socket = new SockJS('/ws');
        stompClient = Stomp.over(socket);

        stompClient.connect({}, function (frame) {
            console.log('Connected: ' + frame);

            // 订阅公共频道
            stompClient.subscribe('/topic/public', function (message) {
                showMessage(JSON.parse(message.body));
            });

            // 通知用户加入
            stompClient.send("/app/chat.addUser", {},
                JSON.stringify({from: username, content: ''})
            );
        });
    }

    // 发送消息
    document.getElementById('message-form').addEventListener('submit', function (e) {
        e.preventDefault();
        const messageInput = document.getElementById('message-input');
        const messageContent = messageInput.value.trim();

        if (messageContent && stompClient) {
            const chatMessage = {
                from: username,
                content: messageContent
            };

            stompClient.send("/app/chat.sendMessage", {},
                JSON.stringify(chatMessage)
            );
            messageInput.value = '';
        }
    });

    // 显示消息
    function showMessage(message) {
        const messageContainer = document.getElementById('message-container');
        const messageElement = document.createElement('div');

        // 区分系统消息和普通消息
        if (message.content.includes('加入了聊天室')) {
            messageElement.style.color = '#888';
            messageElement.style.fontStyle = 'italic';
        } else if (message.from === username) {
            messageElement.style.color = '#4CAF50';
            messageElement.innerHTML = `<strong>我:</strong> ${message.content}`;
        } else {
            messageElement.innerHTML = `<strong>${message.from}:</strong> ${message.content}`;
        }

        messageContainer.appendChild(messageElement);
        messageContainer.scrollTop = messageContainer.scrollHeight;
    }
</script>
</body>
</html>
  • SockJS提供了浏览器兼容性支持

  • STOMP协议简化了WebSocket通信

    // ❌ 原始WebSocket(需要手动处理消息格式)
    websocket.onmessage = function(event) {
        const data = JSON.parse(event.data); // 需要自己解析
        console.log(data);
    };
    
    // ✅ STOMP(自动解析消息格式)
    stompClient.subscribe('/topic/public', function(message) {
        console.log(JSON.parse(message.body)); // STOMP已处理好消息体
    });
    

主应用类

ChatDemoApplication.java:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ChatDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(ChatDemoApplication.class, args);
    }
}

运行和测试

  1. 启动Spring Boot应用
  2. 打开浏览器访问 http://localhost:8080
  3. 打开多个浏览器窗口模拟多个用户
  4. 发送消息测试实时通信功能

YOLO