什么是 WebSocket?

WebSocket 是一种网络通信协议,它实现了客户端(通常是浏览器)与服务器之间的全双工、双向、长连接的通信通道。它解决了传统 HTTP 协议在实时通信场景下的痛点:

  • HTTP 的短板:HTTP 协议是无状态、单向的。客户端发起请求,服务器响应后连接就关闭。要实现实时数据推送(如聊天消息、实时行情),只能通过低效的“轮询”(客户端不断向服务器发送请求询问是否有新数据),这浪费了大量带宽和服务器资源。
  • WebSocket 的优势:WebSocket 在初次握手后,会建立一个持久化的长连接。在这个连接上,服务器和客户端可以随时、主动地向对方发送数据,实现了真正的实时、低延迟、高效的双向通信。

WebSocket 的通信原理

WebSocket 的通信过程可以分为两个阶段:握手连接数据传输

1. 握手连接 (Handshake)

WebSocket 并非凭空建立连接,它巧妙地利用了 HTTP 协议来完成初始握手,以此兼容现有的网络基础设施(如防火墙、代理服务器)。

  • 客户端请求 (HTTP Upgrade Request):客户端首先发送一个标准的 HTTP 请求,但这个请求包含特殊的头信息,表明它希望将协议升级(Upgrade) 为 WebSocket。

    GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // 一个随机生成的Base64编码的密钥
    Sec-WebSocket-Version: 13
    Origin: http://example.com

    • Upgrade: websocketConnection: Upgrade:表明客户端希望升级协议。
    • Sec-WebSocket-Key:一个随机密钥,用于安全验证。
    • Sec-WebSocket-Version:指定协议版本(13是当前最广泛使用的版本)。
  • 服务器响应 (HTTP Switching Protocols):服务器如果支持 WebSocket,会返回一个特殊的 HTTP 响应(状态码 101)。

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= // 由客户端的Key计算得出的值

    • 状态码 101 表示协议切换成功。
    • Sec-WebSocket-Accept 是服务器使用标准算法对客户端的 Sec-WebSocket-Key 处理后的结果,用于验证连接的有效性,防止意外的跨协议攻击。

一旦握手成功,TCP连接保持不变,但通信协议从此从 HTTP 切换到了 WebSocket 协议。

2. 数据传输 (Data Transfer)

握手完成后,连接保持打开状态,进入全双工通信阶段。

  • 数据帧 (Frames):数据被分割成一个个的“帧(Frame)”进行传输。WebSocket 协议定义了如何封装数据帧,帧头包含操作码(Opcode)来指明这是文本数据、二进制数据、还是控制帧(如连接关闭、心跳ping/pong)。
  • 双向通信:在此通道上,服务器可以不再等待客户端请求,直接主动推送(Push)数据给客户端。客户端也可以随时发送数据给服务器。所有通信都在同一个TCP连接上完成,开销极小(帧头只有2-10字节),效率远高于HTTP。

3. 连接关闭

任何一方都可以发起关闭连接的请求,发送一个关闭帧(Close Frame),另一方回应后,连接终止。


WebSocket 的使用场景

WebSocket 适用于所有需要高实时性、低延迟的Web应用:

  1. 实时聊天应用:最经典的场景。如微信网页版、在线客服系统、群聊工具,消息需要瞬间送达。
  2. 多人在线游戏:网页游戏需要实时同步玩家位置、状态和动作。
  3. 实时数据推送
    • 金融财经:股票、期货的实时价格变动、K线图。
    • 体育博彩:实时赔率更新。
    • 监控系统:实时服务器性能指标、日志流。
  4. 协同工具:如在线文档(Google Docs)、协同绘图工具,实时看到他人的编辑光标和操作。
  5. 物联网 (IoT):实时显示和控制智能设备的状态。

使用 Spring MVC 实现一个简单的 WebSocket 应用

我们将使用 Spring 框架提供的 spring-websocket 模块来实现一个简单的广播式聊天室。

1. 添加依赖 (Maven)
pom.xml 中添加依赖:


<!-- WebSocket -->
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-websocket</artifactId>
   <version>5.3.23</version> <!-- 请使用你的Spring版本 -->
</dependency>
<!-- 为了简化,使用内置的STOMP代理。生产环境建议用RabbitMQ或ActiveMQ -->
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-messaging</artifactId>
   <version>5.3.23</version>
</dependency>

2. 启用 WebSocket 支持
创建一个Java配置类 WebSocketConfig

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 // 启用WebSocket消息代理
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) {
      // 注册一个STOMP端点,客户端将使用它来连接到我们的WebSocket服务器
      // withSockJS() 提供了降级方案,在不支持WS的浏览器中使用其他方式模拟
      registry.addEndpoint("/ws").withSockJS();
  }

  @Override
  public void configureMessageBroker(MessageBrokerRegistry registry) {
      // 设置消息代理的前缀
      // 以/app开头的消息将被路由到@MessageMapping注解的方法
      registry.setApplicationDestinationPrefixes("/app");
      
      // 以/topic开头的消息将被路由到消息代理,再广播给所有连接的客户端
      // (内置的简单内存消息代理)
      registry.enableSimpleBroker("/topic");
  }
}

3. 创建消息控制器
创建一个普通的Spring MVC控制器来处理消息:

 import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

@Controller
public class ChatController {

   // 处理所有发送到 `/app/chat` 的消息
   @MessageMapping("/chat")
   
   // 将方法的返回值广播给所有订阅了 `/topic/messages` 的客户端
   @SendTo("/topic/messages")
   public ChatMessage sendMessage(ChatMessage message) {
       // 这里可以添加业务逻辑,如保存到数据库
       return message; // 直接将接收到的消息广播出去
   }
}


**4. 创建消息实体类**

public class ChatMessage {
   private String from;
   private String text;

   // 必须有无参构造函数
   public ChatMessage() {
   }

   public ChatMessage(String from, String text) {
       this.from = from;
       this.text = text;
   }
   // Getter and Setter 方法
   public String getFrom() { return from; }
   public void setFrom(String from) { this.from = from; }
   public String getText() { return text; }
   public void setText(String text) { this.text = text; }
}

5. 前端客户端 (HTML + JavaScript)
创建一个 index.html 页面:

<!DOCTYPE html>
<html>
<head>
   <title>WebSocket Chat</title>
   <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
</head>
<body>
   <div id="messageArea"></div>
   <form>
       <input type="text" id="messageInput" placeholder="Type a message..." />
       <button type="button" onclick="sendMessage()">Send</button>
   </form>

   <script>
       // 建立连接
       const socket = new SockJS('/ws'); // 连接到我们注册的端点
       const stompClient = Stomp.over(socket);

       stompClient.connect({}, function (frame) {
           console.log('Connected: ' + frame);
           // 订阅广播地址,当服务器向/topic/messages发送消息时,这里的回调函数会被触发
           stompClient.subscribe('/topic/messages', function (messageOutput) {
               showMessage(JSON.parse(messageOutput.body));
           });
       });

       function sendMessage() {
           const from = "User"; // 在实际应用中,这应该是登录的用户名
           const text = document.getElementById('messageInput').value;
           // 向服务器发送消息,目的地是 /app/chat
           stompClient.send("/app/chat", {}, JSON.stringify({'from': from, 'text': text}));
           document.getElementById('messageInput').value = '';
       }

       function showMessage(message) {
           const messageArea = document.getElementById('messageArea');
           const messageElement = document.createElement('p');
           messageElement.textContent = message.from + ": " + message.text;
           messageArea.appendChild(messageElement);
       }
   </script>
</body>
</html>

总结与运行

  1. 将以上代码整合到你的Spring Boot或Spring MVC项目中。
  2. 启动应用服务器。
  3. 在浏览器中打开 http://localhost:8080/index.html(端口号根据你的配置调整)。
  4. 打开多个浏览器窗口,在一个窗口中发送消息,所有其他窗口都会实时收到广播的消息。

这个例子使用了 STOMP 子协议,它是在 WebSocket 之上提供了一个更高级的、基于帧的消息格式,类似于 HTTP,使得在客户端和服务器之间传递消息变得更加简单和结构化。

在这里插入图片描述

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐