HTTP、HTTPS 和 WebSocket 协议和开发

目录

HTTP(超文本传输协议)

[HTTPS(HTTP Secure)](#HTTPS(HTTP Secure))

WebSocket

三者关系

[深入剖析 HTTP 的演进](#深入剖析 HTTP 的演进)

[1. HTTP/1.0 (1996) - "一次性邮差"](#1. HTTP/1.0 (1996) - “一次性邮差”)

[2. HTTP/1.1 (1997) - "可复用的邮差" - 目前仍最主流](#2. HTTP/1.1 (1997) - “可复用的邮差” - 目前仍最主流)

[3.HTTP/2 (2015) - "超级高速公路"](#3.HTTP/2 (2015) - “超级高速公路”)

[HTTP/1.1 vs HTTP/2 性能对比](#HTTP/1.1 vs HTTP/2 性能对比)

[WebSocket 再深入:它是如何"升级"的?](#WebSocket 再深入:它是如何“升级”的?)

[WebSocket 和Http/2](#WebSocket 和Http/2)

[1. 通信模型的根本差异(这是最重要的区别)](#1. 通信模型的根本差异(这是最重要的区别))

[2. 对"服务器推送"的误解澄清](#2. 对“服务器推送”的误解澄清)

[3. 如何选择?](#3. 如何选择?)

[开发中(SpringBoot Vue)](#开发中(SpringBoot Vue))

HTTP请求的接收和发送

SpringBoot后端

接收HTTP请求

表面现象:为什么我们不需要手动构建HTTP响应

发送HTTP请求(调用外部API)

1.使用RestTemplate

[2.WebClient 详细讲解](#2.WebClient 详细讲解)

Vue前端

[发送 + 接收HTTP请求](#发送 + 接收HTTP请求)

WebSocket完整通信

后端

前端

重新讲解WebSocket

用一个超形象的比喻理解WebSocket

[传统HTTP vs WebSocket](#传统HTTP vs WebSocket)

[WebSocket 实际开发详解(用WebSocketHandler)](#WebSocket 实际开发详解(用WebSocketHandler))

[第一步:Spring Boot后端设置](#第一步:Spring Boot后端设置)

[1. 添加依赖](#1. 添加依赖)

[2. 创建WebSocket配置(就像安装电话线路)](#2. 创建WebSocket配置(就像安装电话线路))

[3. 创建WebSocket处理器(就像接线员)](#3. 创建WebSocket处理器(就像接线员))

第二步:Vue前端使用WebSocket

[1. 创建WebSocket聊天组件](#1. 创建WebSocket聊天组件)

四个关键事件:

核心方法:

连接状态:

[WebSocket 实际开发详解(用@ServerEndpoint)](#WebSocket 实际开发详解(用@ServerEndpoint))

[第一步:Spring Boot后端配置](#第一步:Spring Boot后端配置)

[1. 添加依赖(与之前相同)](#1. 添加依赖(与之前相同))

[2. 关键配置:启用ServerEndpoint](#2. 关键配置:启用ServerEndpoint)

[3. 使用@ServerEndpoint创建WebSocket端点](#3. 使用@ServerEndpoint创建WebSocket端点)

[4. 业务服务示例(展示依赖注入)](#4. 业务服务示例(展示依赖注入))

第二步:Vue前端使用(与之前类似,但连接地址不同)

[@ServerEndpoint 核心注解说明](#@ServerEndpoint 核心注解说明)

四个核心注解:

重要特性:

两种方式对比总结

选择建议:


协议 特点 运行过程(简述) OSI 层 TCP/IP 层
HTTP 明文无状态请求-响应模型 1. 建立 TCP 连接 2. 客户端发送请求 3. 服务器返回响应 4. 关闭连接 应用层 应用层
HTTPS 加密身份验证完整性校验 1. 建立 TCP 连接 2. TLS 握手 (交换密钥) 3. 在加密通道内进行 HTTP 通信 应用层 应用层
WebSocket 全双工长连接低延迟 1. HTTP 握手 (Upgrade 请求) 2. 连接升级为 WebSocket 3. 双向持久通信,无需重复握手 应用层 应用层

HTTP(超文本传输协议)

所属层级: 应用层协议。它基于传输层的 TCP 协议。

特点:

  1. 明文传输:请求和响应的内容都是未加密的,容易被窃听和篡改。

  2. 无状态:服务器不记录每次请求之间的关联信息。每个请求都是独立的(通常使用 Cookie/Session 技术来弥补这一缺陷)。

  3. 请求-响应模型:通信总是由客户端(如浏览器)发起,服务器被动响应。服务器不会主动向客户端推送消息。

  4. 简单灵活 :传输的内容类型由 Content-Type 标头定义,可以传输任意类型的数据。

运行过程:

  1. 建立 TCP 连接:客户端(浏览器)首先与服务器的 80 端口建立一个可靠的 TCP 连接。

  2. 发送 HTTP 请求:客户端通过这个连接发送一个请求报文,包含:

    • 请求行(方法:GET/POST,URL,协议版本)

    • 请求头(Host, User-Agent, Accept 等)

    • 请求体(可选,如 POST 方法提交的表单数据)

  3. 服务器处理并返回响应:服务器解析请求,处理业务逻辑,然后返回一个响应报文,包含:

    • 状态行(状态码:200 OK,404 Not Found 等)

    • 响应头(Content-Type, Content-Length, Set-Cookie 等)

    • 响应体(请求的资源,如 HTML、图片、JSON 数据等)

  4. 关闭连接 :在 HTTP/1.0 中,每次请求-响应后都会关闭 TCP 连接。HTTP/1.1 引入了持久连接,允许在同一个连接上进行多次请求-响应,减少了建立连接的开销。

HTTPS(HTTP Secure)

所属层级: 应用层协议。它是在 HTTP 和 TCP 之间加入了一个安全层(SSL/TLS)。

特点:

  1. 加密传输:通过 SSL/TLS 协议对通信内容进行加密,防止数据被窃听。

  2. 身份验证:通过数字证书验证服务器的身份,防止中间人攻击。

  3. 完整性保护:通过消息认证码来校验数据在传输过程中是否被篡改。

  4. 本质还是 HTTP:在建立安全通道后,通信的内容和方式与 HTTP 完全一样。

运行过程:

  1. 建立 TCP 连接 :客户端连接到服务器的 443 端口。

  2. TLS 握手:这是 HTTPS 安全的核心。

    • 客户端 Hello:客户端向服务器发送支持的加密算法列表和一个随机数。

    • 服务器 Hello:服务器选择加密算法,发送自己的数字证书和一个随机数。

    • 验证证书:客户端验证证书的合法性(是否由可信机构颁发,域名是否匹配等)。

    • 生成会话密钥 :客户端用证书中的公钥加密一个预主密钥并发送给服务器。

    • 生成会话密钥 :服务器用自己的私钥解密得到预主密钥。此时,客户端和服务器都拥有了三个随机数(客户端随机数、服务器随机数、预主密钥),它们使用相同的算法生成唯一的会话密钥

    • 握手结束:双方交换加密完成的"Finished"消息,验证握手是否成功。

  3. 加密的 HTTP 通信:之后的整个 HTTP 请求和响应过程,都使用上一步生成的会话密钥进行加密和解密。

  4. 关闭连接

WebSocket

所属层级: 应用层协议。它同样基于 TCP,并借用了 HTTP 的握手过程。

特点:

  1. 全双工通信 :连接建立后,服务器和客户端可以同时向对方发送消息。

  2. 持久化长连接:只需一次握手,连接就会一直保持,避免了 HTTP 的频繁建立和断开连接的开销。

  3. 低延迟:由于没有频繁的握手和头部信息,通信效率极高,非常适合实时应用。

  4. 服务器主动推送:服务器可以随时主动向客户端发送数据,完美解决了 HTTP 轮询带来的性能问题。

运行过程:

  1. HTTP 握手请求:客户端首先发送一个特殊的 HTTP 请求,其头部包含:

    GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Sec-WebSocket-Version: 13

  • Upgrade: websocketConnection: Upgrade 表明客户端希望将协议升级为 WebSocket。

2. HTTP 握手响应 :服务器如果同意升级,会返回一个状态码为 101 Switching Protocols 的响应:

复制代码
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
  • Sec-WebSocket-Accept 是对客户端发送的 Key 计算后的结果,用于验证握手有效性。

3. WebSocket 数据传输 :握手成功后,TCP 连接保持,但通信协议从 HTTP 切换到了 WebSocket。此时,双方使用 WebSocket 定义的数据帧格式进行双向、低开销的数据传输。

4. 关闭连接:任何一方都可以发送一个关闭帧来优雅地终止连接。

三者关系

复制代码
+-------------------------------------------------------------------+
|                   应用层 (Application Layer)                      |
+-------------------+-------------------+---------------------------+
|      HTTP 1.x     |      HTTP/2       |       WebSocket           |
|    (明文,队头阻塞) |   (二进制,多路复用)  |    (全双工,持久连接)      |
+-------------------+-------------------+---------------------------+
|         HTTPS (HTTP + TLS/SSL) 安全的传输层                        |
+-------------------------------------------------------------------+
|                   传输层 (Transport Layer)                        |
|                           TCP                                     |
+-------------------------------------------------------------------+
|                   网络层 (Network Layer)                          |
|                           IP                                      |
+-------------------------------------------------------------------+

核心比喻:

  • TCP/IP:相当于高速公路系统。它负责把数据包从A点可靠地送到B点。
  • HTTP:相当于在高速公路上跑的邮递车。它负责运送具体的"信件"(网页、图片等),但每次送货都需要重新装货、发货、收货确认,效率较低。
  • WebSocket:相当于在高速公路上建立的一条专用管道。一旦建好,双方可以随时、持续地向管道里扔东西,效率极高。

深入剖析 HTTP 的演进

1. HTTP/1.0 (1996) - "一次性邮差"

特点:

  • 短连接:每次请求都需要建立一次 TCP 连接,收到响应后立即断开。想象成邮差每送一封信,都要回邮局一趟再送下一封。效率极低。

图示过程:

复制代码
客户端 (浏览器)             服务器
     |---- TCP连接 ---->|
     |--- HTTP请求1 --->|
     |<-- HTTP响应1 ----|
     |---- 断开连接 ---->
     |---- TCP连接 ---->|
     |--- HTTP请求2 --->|
     |<-- HTTP响应2 ----|
     |---- 断开连接 ---->

缺点:建立 TCP 连接是昂贵的(三次握手),频繁连接断开开销巨大。

2. HTTP/1.1 (1997) - "可复用的邮差" - 目前仍最主流

核心改进:

  • 持久连接 :默认保持 TCP 连接打开,可以在一个连接上发送多个请求和响应。用 Connection: keep-alive 头控制。邮差一次出门可以送多封信。

  • 管道化 :理论上,客户端可以连续发送多个请求,而不用等待上一个响应返回。但在实践中,队头阻塞 问题严重。

队头阻塞比喻:

就像一个单车道收费站(服务器),前面的车交费慢了,后面的车即使准备好了钱也得等着。

图示过程 (无管道化(默认模式)):

复制代码
客户端                      服务器
  |---- TCP连接 ----------->|
  |--- 请求1 -------------->|
  |<--------- 响应1 --------|
  |--- 请求2 -------------->|
  |<--------- 响应2 --------|
  |--- 请求3 -------------->|
  |<--------- 响应3 --------|
  |---- 断开连接 ----------->|

图示过程 (有管道化(Pipelining)但存在队头阻塞):

复制代码
客户端                      服务器
  |---- TCP连接 ----------->|
  |--- 请求1 -------------->|
  |--- 请求2 -------------->|  # 连续发送请求
  |--- 请求3 -------------->|
  |<--------- 响应1 --------|  # 响应1必须最先返回
  |<--------- 响应2 --------|  # 如果响应1处理慢,2和3就被堵住了
  |<--------- 响应3 --------|

HTTP/1.1 的其他问题:请求和响应头信息重复且未压缩,浪费带宽。

3.HTTP/2 (2015) - "超级高速公路"

HTTP/2 没有改变 HTTP 的语义(方法、状态码等),而是改变了数据格式和传输方式

核心改进:

  • 二进制分帧:不再是纯文本格式,而是被分解为更小的二进制帧。更容易机器的解析,也更紧凑。

  • 多路复用 :解决了队头阻塞问题。多个请求和响应可以在同一个连接上混杂传输,互不干扰。

多路复用比喻:

就像一个多车道高速公路。即使一辆车(请求1)在慢速行驶,其他车(请求2、3)也可以从旁边车道超车。

图示过程:

复制代码
客户端                              服务器
  |--------- TCP连接 --------------->|
  |---- 流1: 头帧 + 数据帧 ---------->|
  |---- 流2: 头帧 + 数据帧 ---------->|  # 多个流混杂在一起
  |---- 流3: 头帧 + 数据帧 ---------->|
  |<---- 流2: 数据帧 ----------------|  # 服务器可以优先返回准备好的流2
  |<---- 流1: 数据帧 ----------------|
  |<---- 流3: 数据帧 ----------------|
  • :一个虚拟通道,承载一个完整的请求-响应过程。每个流有唯一ID。

  • :数据通信的最小单位,每个帧都标明了它属于哪个流。

其他重要特性:

  • 头部压缩:使用 HPACK 算法压缩请求头,大大减少了冗余。

  • 服务器推送:服务器可以预测客户端需要哪些资源(如CSS、JS),在客户端请求之前就主动推送过去。

HTTP/1.1 vs HTTP/2 性能对比

想象一个网页,需要加载 HTML, CSS, JS, 图片1, 图片2。

HTTP/1.1 (有并发连接数限制,如6个):

复制代码
时间线 | [HTML] [CSS] [JS] [IMG1] [IMG2] ... | 排队等待,可能有多轮
  • 虽然连接复用,但响应必须按顺序返回,容易堵塞。

HTTP/2 (多路复用):

复制代码
时间线 | [HTML][CSS][JS][IMG1][IMG2]... | 所有资源几乎并行下载
  • 所有资源争抢同一个连接的"带宽",谁先准备好谁就先发送,效率极高。

现在所有的 HTTP 默认用的是哪个版本?

现状总结:

  • HTTP/1.1 是当前绝对的主流和保底选择。

  • HTTP/2 已在绝大多数现代网站和浏览器中普及,是性能优化的首选。

  • HTTP/3 (基于QUIC协议) 正在快速崛起,是未来方向。

协议版本 使用场景与现状 默认程度
HTTP/1.0 基本已被淘汰。仅在一些非常古老的客户端或嵌入式设备中可能出现。 绝非默认
HTTP/1.1 目前的绝对基准和保底协议 。所有客户端和服务器都100%支持。如果无法协商更新版本,一定会降级到 HTTP/1.1 功能上的"默认" (当其他版本不可用时)
HTTP/2 现代网站的默认性能选择 。超过一半的网站已支持。几乎所有现代浏览器都支持。需要 HTTPS 性能上的"默认" (对于现代网站)
HTTP/3 前沿和未来。由 Google 推动,基于 QUIC 协议(在 UDP 上而非 TCP)。能进一步解决延迟和队头阻塞问题。支持率正在快速增长(如 Cloudflare, Google 等服务已支持)。 正在成为下一代默认

WebSocket 再深入:它是如何"升级"的?

阶段一:HTTP 握手 ("敲门,对暗号")

复制代码
客户端 (敲门)  --- HTTP Request --->
    GET /chat HTTP/1.1
    Host: example.com
    Upgrade: websocket       # "我想升级成WebSocket"
    Connection: Upgrade
    Sec-WebSocket-Key: dGhl... # 暗号的一部分
    Sec-WebSocket-Version: 13

服务器 (核对暗号) <--- HTTP Response ---
    HTTP/1.1 101 Switching Protocols # "好的,升级成功!"
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPL... # 暗号的另一部分

阶段二:WebSocket 全双工通信 ("推开大门,自由交谈")

复制代码
客户端                     服务器
  |------- WebSocket 数据帧 ------->| # 客户端可以随时发消息
  |<------ WebSocket 数据帧 --------| # 服务器也可以随时主动发消息
  |<------ WebSocket 数据帧 --------|
  |------- WebSocket 数据帧 ------->|
  ... (连接持久,双向通信) ...

WebSocket 和Http/2

  • WebSocket 是为了提供持续的、双向的、对话式的通信通道。

  • HTTP/2 是为了高效地传输大量的 Web 资源(HTML, CSS, JS, 图片等),它优化的是请求-响应模型。

特性 WebSocket HTTP/2
通信模型 真正的、基于消息的全双工通道。连接建立后,双方平等,可随时主动发送消息。 增强的请求-响应模型。通信仍由客户端请求发起。服务器推送是对请求的"预测响应",并非真正的主动通信。
服务器主动推送 原生支持。服务器可以在任何业务逻辑需要时,主动向客户端发送消息。 有限的 Server Push 。服务器只能将资源推送到客户端缓存,无法触发客户端JavaScript代码执行。
数据格式 轻量级的帧结构。头部开销极小,适合高频、小数据量的消息交换。 二进制分帧层。虽然也是二进制,但头部经过HPACK压缩,仍是为传输Web资源优化。
连接与状态 有状态的。连接一旦建立,会一直保持,直到一方关闭。服务器知道哪些客户端在线。 无状态的。每个请求-响应在逻辑上仍是独立的(尽管在同一个TCP连接上)。
与 HTTP 的关系 借用 HTTP 进行初始握手,之后完全脱离 HTTP,使用自己的协议。 是 HTTP 语义的进化。它改变了数据的传输格式,但没有改变 HTTP 的方法、状态码等核心概念。
延迟 极低。一旦连接建立,消息可以立即往返,没有协议上的开销和延迟。 较低。相比 HTTP/1.1 大大降低,但由于仍是请求-响应模式,对于真正的实时应用,延迟高于 WebSocket。

解析关键区别

1. 通信模型的根本差异(这是最重要的区别)
  • WebSocket:像一个电话通话

    • 你拨号(HTTP 握手),对方接听(101 Switching Protocols)。

    • 之后,你们就建立了一条持续的连接,任何一方都可以随时说话,也可以同时听对方说。

    • 适用场景: 在线聊天、实时游戏、协同编辑、股票行情推送。任何需要服务器在数据产生时立即通知客户端的场景。

  • HTTP/2:像一个高效的邮递员

    • 你给他一张购物清单(请求),他可以用一辆大卡车(多路复用)把所有的货品(响应)一起运回来,效率很高。

    • 他甚至能预测你可能还需要酱油,于是把酱油也提前塞进你的包裹(Server Push)。

    • 但是,他不能主动敲门给你送一封你不知道的信。 所有送来的东西,都必须源于你最初的那张"购物清单"或基于它的预测。

    • 适用场景: 加载复杂的网页。它极大地优化了网页的加载性能

2. 对"服务器推送"的误解澄清

这是最关键的混淆点。

  • HTTP/2 Server Push:

    • 目的: 是为了减少延迟。当服务器收到对一个HTML页面的请求时,它可以"推送"这个页面所需的CSS和JS文件到客户端缓存,省去了客户端解析HTML后再去请求这些资源的时间。

    • 本质:对已知资源的缓存填充 。推送到浏览器的资源,不能被你的 JavaScript 代码直接接收和处理。它只是安静地待在缓存里,等你需要时直接读取。

    • 例子: 浏览器请求 index.html,服务器连同 style.cssapp.js 一起推送过来。

  • WebSocket 推送:

    • 目的: 是为了传递动态的、未知的业务数据

    • 本质:一条消息 。当服务器通过 WebSocket 发送数据时,会直接触发客户端的 onmessage 事件,你的 JavaScript 代码可以立即处理它。

    • 例子: 在聊天应用中,当另一个用户发送了一条消息,服务器通过 WebSocket 立即将它推送到你的浏览器,聊天窗口实时更新。

      // WebSocket 的推送可以直接被JS代码处理
      websocket.onmessage = function(event) {
      var message = event.data; // 直接拿到消息内容
      appendMessageToChatWindow(message);
      };

3. 如何选择?
  • 选择 HTTP/2

    • 你的主要目标是加快网站或Web应用的加载速度

    • 你不需要服务器主动向客户端发送实时业务消息。

    • 你的应用架构仍然是传统的"客户端请求,服务器响应"。

  • 选择 WebSocket

    • 你需要真正的、低延迟的双向通信

    • 你的应用功能依赖于服务器主动发起的事件(如聊天消息、实时通知、游戏状态同步、直播评论)。

开发中(SpringBoot Vue)

下面我们分别详细讲解。

  • HTTP请求的接收和发送
  • WebSocket消息的接收和发送

HTTP请求的接收和发送

SpringBoot后端

接收HTTP请求

1. 依赖配置

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

2. 接收HTTP请求的Controller

复制代码
@RestController
@RequestMapping("/api")
public class UserController {
    
    //  接收GET请求 - 获取用户列表
    @GetMapping("/users")
    public ResponseEntity<List<User>> getUsers() {
        List<User> users = userService.findAll();
        return ResponseEntity.ok(users); // 自动构建HTTP响应
    }
    
    //  接收POST请求 - 创建用户
    @PostMapping("/users")
    public ResponseEntity<User> createUser(@RequestBody User user) {
        // @RequestBody 自动将JSON请求体转换为Java对象
        User savedUser = userService.save(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
    }
    
    //  接收带路径参数的GET请求
    @GetMapping("/users/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        User user = userService.findById(id);
        return ResponseEntity.ok(user);
    }
    
    //  接收带查询参数的GET请求
    @GetMapping("/users/search")
    public ResponseEntity<List<User>> searchUsers(
            @RequestParam String keyword,
            @RequestParam(defaultValue = "0") int page) {
        List<User> users = userService.search(keyword, page);
        return ResponseEntity.ok(users);
    }
    
    //  接收PUT请求 - 更新用户
    @PutMapping("/users/{id}")
    public ResponseEntity<User> updateUser(
            @PathVariable Long id, 
            @RequestBody User user) {
        User updatedUser = userService.update(id, user);
        return ResponseEntity.ok(updatedUser);
    }
    
    //  接收DELETE请求
    @DeleteMapping("/users/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.noContent().build(); // 204 No Content
    }
}
表面现象:为什么我们不需要手动构建HTTP响应

这是Spring MVC框架的功劳

Spring MVC的工作流程:

复制代码
HTTP请求 -> DispatcherServlet -> @Controller/@RestController -> 返回对象 -> HttpMessageConverter -> JSON/XML -> HTTP响应

具体发生了什么:

写一个简单的Controller方法:

复制代码
@RestController
public class UserController {
    
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        return user; // 直接返回Java对象,不是HttpResponse!
    }
}
  • Spring MVC在背后帮你做了这些:

    1. 自动设置状态码:默认200 OK

    2. 自动设置Content-Type头application/json

    3. 自动将Java对象序列化为JSON(响应体)

    4. 自动处理异常,返回合适的错误码

这就是框架的价值:把重复的样板代码隐藏起来,让开发者专注于业务逻辑。

发送HTTP请求(调用外部API)

我们使用RestTemplate或WebClient来发送HTTP请求。

工具 时代 编程模型 底层实现 Spring Boot起步依赖
RestTemplate Spring 3.0+ 同步阻塞 可配置(默认JDK HttpURLConnection) spring-boot-starter-web
WebClient Spring 5.0+ 异步非阻塞 (Reactive) Reactor Netty spring-boot-starter-webflux

RestTemplate

  • 默认 :JDK的HttpURLConnection

  • 可替换为:Apache HttpClient、OkHttp等

    @Bean
    public RestTemplate restTemplate() {
    return new RestTemplate(new HttpComponentsClientHttpRequestFactory()); // 使用Apache HttpClient
    }

WebClient

  • 基于Reactor和Netty,使用事件驱动、非阻塞IO模型。

    // 简化的执行流程
    public Mono<ClientResponse> exchange() {
    return Mono.defer(() -> {
    // 1. 创建HttpClientRequest
    HttpClientRequest request = httpClient.request(httpMethod);

    复制代码
          // 2. 设置请求头、体等
          // 3. 发送请求并返回Mono<ClientResponse>
          return request.response();
      });

    }

1.使用RestTemplate

1.依赖配置

起步依赖spring-boot-starter-web (这是最常用的Web开发starter)

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

spring-boot-starter-web
├── spring-webmvc          # Spring MVC核心
├── spring-web             # Spring Web核心
├── jackson-databind       # JSON序列化
└── tomcat-embed-core      # 内嵌Tomcat

2.RestTemplate在Spring Boot中的使用方式:

方式1:手动配置Bean(推荐,可定制化)

复制代码
@Configuration
public class RestTemplateConfig {
    
    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        
        // 可选的定制配置
        restTemplate.setErrorHandler(new MyErrorHandler());
        // 设置超时时间等...
        
        return restTemplate;
    }
}

然后,在需要的地方注入并使用。

复制代码
@Service
public class UserService {
    
    @Autowired
    private RestTemplate restTemplate;
    
    // GET请求 - 获取用户信息
    public User getUserFromExternalAPI(Long userId) {
        String url = "https://jsonplaceholder.typicode.com/users/{id}";
        
        // 方法1:getForObject - 直接返回对象
        User user = restTemplate.getForObject(url, User.class, userId);
        
        // 方法2:getForEntity - 返回包含响应头的完整响应
        ResponseEntity<User> response = restTemplate.getForEntity(url, User.class, userId);
        if (response.getStatusCode() == HttpStatus.OK) {
            return response.getBody();
        }
        
        return null;
    }
    
    // POST请求 - 创建用户
    public User createUser(User user) {
        String url = "https://jsonplaceholder.typicode.com/users";
        
        // 方法1:postForObject
        User createdUser = restTemplate.postForObject(url, user, User.class);
        
        // 方法2:exchange - 最灵活的方法
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set("Authorization", "Bearer token123");
        
        HttpEntity<User> request = new HttpEntity<>(user, headers);
        ResponseEntity<User> response = restTemplate.exchange(
            url, HttpMethod.POST, request, User.class);
            
        return response.getBody();
    }
    
    // PUT、DELETE请求
    public void updateUser(User user) {
        String url = "https://jsonplaceholder.typicode.com/users/{id}";
        restTemplate.put(url, user, user.getId());
    }
    
    public void deleteUser(Long userId) {
        String url = "https://jsonplaceholder.typicode.com/users/{id}";
        restTemplate.delete(url, userId);
    }
}

方式2:直接使用(不推荐)

复制代码
// 可以直接new,但失去了Spring管理的优势
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject("http://example.com", String.class);
2.WebClient 详细讲解

1.配置依赖。起步依赖:spring-boot-starter-webflux

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

spring-boot-starter-webflux
├── spring-webflux         # Reactive Web核心
├── reactor-core           # Reactor响应式编程
└── netty                  # 底层网络库

2.配置Bean:

复制代码
@Configuration
public class WebClientConfig {
    
    @Bean
    public WebClient webClient() {
        return WebClient.builder()
            .baseUrl("https://jsonplaceholder.typicode.com")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();
    }
}

3.然后,在需要的地方注入并使用。

复制代码
@Service
public class ReactiveUserService {
    
    @Autowired
    private WebClient webClient;
    
    // 异步获取用户
    public Mono<User> getUserAsync(Long userId) {
        return webClient.get()
            .uri("/users/{id}", userId)
            .retrieve()
            .bodyToMono(User.class);
    }
    
    // 带错误处理的请求
    public Mono<User> getUserWithErrorHandling(Long userId) {
        return webClient.get()
            .uri("/users/{id}", userId)
            .retrieve()
            .onStatus(HttpStatus::is4xxClientError, response -> 
                Mono.error(new RuntimeException("Client error: " + response.statusCode()))
            )
            .onStatus(HttpStatus::is5xxServerError, response -> 
                Mono.error(new RuntimeException("Server error: " + response.statusCode()))
            )
            .bodyToMono(User.class);
    }
    
    // POST请求
    public Mono<User> createUser(User user) {
        return webClient.post()
            .uri("/users")
            .bodyValue(user)
            .retrieve()
            .bodyToMono(User.class);
    }
    
    // 同步调用(在需要阻塞的地方)
    public User getUserSync(Long userId) {
        return webClient.get()
            .uri("/users/{id}", userId)
            .retrieve()
            .bodyToMono(User.class)
            .block(); // 阻塞直到得到结果
    }
}

RestTemplate/WebClient主要就是为了在分布式系统中调用其他服务!

Vue前端

发送 + 接收HTTP请求

1. 安装依赖

复制代码
npm install axios

2. HTTP服务封装

复制代码
// src/services/api.js
import axios from 'axios';

// 创建axios实例
const apiClient = axios.create({
  baseURL: 'http://localhost:8080/api', // Spring Boot后端地址
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
});

export default {
  //  发送GET请求 + 接收响应
  getUsers() {
    return apiClient.get('/users')
      .then(response => {
        // 接收后端响应数据
        return response.data;
      })
      .catch(error => {
        console.error('获取用户列表失败:', error);
        throw error;
      });
  },
  
  //  发送POST请求 + 接收响应
  createUser(userData) {
    return apiClient.post('/users', userData)
      .then(response => {
        // 接收创建成功的用户数据
        return response.data;
      });
  },
  
  //  发送PUT请求 + 接收响应
  updateUser(id, userData) {
    return apiClient.put(`/users/${id}`, userData)
      .then(response => response.data);
  },
  
  //  发送DELETE请求
  deleteUser(id) {
    return apiClient.delete(`/users/${id}`);
  }
}

3. Vue组件中使用

复制代码
<template>
  <div>
    <!-- 发送请求的UI -->
    <button @click="loadUsers">加载用户</button>
    <button @click="createUser">创建用户</button>
    
    <!-- 接收并显示响应数据 -->
    <div v-if="loading">加载中...</div>
    <ul v-else>
      <li v-for="user in users" :key="user.id">
        {{ user.name }} - {{ user.email }}
        <button @click="deleteUser(user.id)">删除</button>
      </li>
    </ul>
  </div>
</template>

<script>
import apiService from '@/services/api.js';

export default {
  data() {
    return {
      users: [],      //  接收到的数据
      loading: false //  接收加载状态
    }
  },
  methods: {
    //  发送GET请求并接收响应
    async loadUsers() {
      this.loading = true;
      try {
        // 发送请求 + 接收响应
        this.users = await apiService.getUsers();
      } catch (error) {
        console.error('加载失败:', error);
      } finally {
        this.loading = false;
      }
    },
    
    //  发送POST请求并接收响应
    async createUser() {
      const newUser = {
        name: '张三',
        email: 'zhangsan@example.com'
      };
      
      try {
        // 发送请求 + 接收响应
        const createdUser = await apiService.createUser(newUser);
        console.log('创建成功:', createdUser);
        this.loadUsers(); // 重新加载列表
      } catch (error) {
        console.error('创建失败:', error);
      }
    },
    
    //  发送DELETE请求
    async deleteUser(userId) {
      try {
        await apiService.deleteUser(userId);
        this.loadUsers(); // 重新加载
      } catch (error) {
        console.error('删除失败:', error);
      }
    }
  },
  mounted() {
    this.loadUsers(); // 组件加载时自动获取数据
  }
}
</script>

WebSocket完整通信

后端

基本步骤:

  • 添加WebSocket依赖。
  • 配置WebSocket,比如注册WebSocket处理器或使用@ServerEndpoint注解。
  • 编写WebSocket处理器,处理连接、消息、关闭和错误。

1.在pom.xml中添加WebSocket依赖。

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

两种方式配置WebSocket:一种是实现WebSocketConfigurer,另一种是使用@ServerEndpoint注解。

方式一:

  1. 创建一个配置类启用WebSocket,并注册WebSocket端点。

    @Configuration
    @EnableWebSocket
    public class WebSocketConfig implements WebSocketConfigurer {

    复制代码
     @Override
     public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
         registry.addHandler(new MyWebSocketHandler(), "/ws")
                 .setAllowedOrigins("*"); // 允许跨域
     }

    }

2.实现WebSocket处理器,实现WebSocketHandler接口来处理WebSocket连接和消息。

复制代码
@Component
public class MyWebSocketHandler extends TextWebSocketHandler {
    
    private final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();
    
    //  接收连接建立
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.add(session);
        System.out.println("客户端连接: " + session.getId());
        
        //  主动发送欢迎消息
        session.sendMessage(new TextMessage("欢迎连接WebSocket!"));
    }
    
    //  接收客户端消息
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String clientMessage = message.getPayload();
        System.out.println("收到消息: " + clientMessage + " from " + session.getId());
        
        //  发送响应消息给这个客户端
        String response = "服务器回应: " + clientMessage;
        session.sendMessage(new TextMessage(response));
        
        //  广播消息给所有客户端
        broadcast("用户 " + session.getId() + " 说: " + clientMessage);
    }
    
    //  接收连接关闭
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        sessions.remove(session);
        System.out.println("客户端断开: " + session.getId());
    }
    
    //  主动发送消息给所有客户端(服务器推送)
    public void broadcast(String message) {
        for (WebSocketSession session : sessions) {
            try {
                if (session.isOpen()) {
                    session.sendMessage(new TextMessage(message));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    //  主动发送消息给特定客户端
    public void sendToUser(String sessionId, String message) {
        for (WebSocketSession session : sessions) {
            if (session.getId().equals(sessionId) && session.isOpen()) {
                try {
                    session.sendMessage(new TextMessage(message));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

方式二:

1.配置ServerEndpointExporter

复制代码
@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

2.然后,使用@ServerEndpoint注解定义一个端点。

复制代码
@Component
@ServerEndpoint("/my-websocket")
public class MyWebSocketEndpoint {

    @OnOpen
    public void onOpen(Session session) {
        System.out.println("Connected: " + session.getId());
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("Received: " + message);
        try {
            // 发送回复
            session.getBasicRemote().sendText("Echo: " + message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @OnClose
    public void onClose(Session session) {
        System.out.println("Closed: " + session.getId());
    }

    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
    }
}

前端

在Vue组件中,我们使用JavaScript原生的WebSocket API。

基本步骤:

  • 在组件创建时(mounted)建立WebSocket连接。

  • 监听WebSocket的open、message、close、error事件。

  • 在方法中发送消息。

  • 在组件销毁前(beforeUnmount)关闭WebSocket连接。

    <template>

    WebSocket聊天室

    复制代码
      <!-- 显示接收到的消息 -->
      <div class="message-container">
        <div v-for="(msg, index) in messages" :key="index" class="message">
          {{ msg }}
        </div>
      </div>
      
      <!-- 发送消息的输入框 -->
      <div class="input-area">
        <input v-model="inputMessage" @keyup.enter="sendMessage" placeholder="输入消息...">
        <button @click="sendMessage">发送</button>
        <button @click="connectWebSocket">连接</button>
        <button @click="disconnectWebSocket">断开</button>
      </div>
      
      <div>连接状态: {{ connectionStatus }}</div>
    </div>
    </template> <script> export default { data() { return { websocket: null, messages: [], // 接收到的消息列表 inputMessage: '', // 要发送的消息 connectionStatus: '未连接' } }, methods: { // 建立WebSocket连接 connectWebSocket() { // 创建WebSocket连接 this.websocket = new WebSocket('ws://localhost:8080/ws'); // 接收连接建立事件 this.websocket.onopen = (event) => { this.connectionStatus = '已连接'; this.messages.push('系统: 连接服务器成功'); console.log('WebSocket连接已建立'); }; // 接收服务器消息 this.websocket.onmessage = (event) => { // 接收到服务器发送的消息 this.messages.push('服务器: ' + event.data); console.log('收到消息:', event.data); }; // 接收连接关闭事件 this.websocket.onclose = (event) => { this.connectionStatus = '已断开'; this.messages.push('系统: 连接已断开'); console.log('WebSocket连接已关闭'); }; // 接收错误事件 this.websocket.onerror = (error) => { this.connectionStatus = '连接错误'; this.messages.push('系统: 连接发生错误'); console.error('WebSocket错误:', error); }; }, // 发送消息到服务器 sendMessage() { if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { this.websocket.send(this.inputMessage); this.messages.push('我: ' + this.inputMessage); this.inputMessage = ''; // 清空输入框 } else { alert('WebSocket未连接'); } }, // 断开连接 disconnectWebSocket() { if (this.websocket) { this.websocket.close(); } } }, mounted() { // 组件加载时自动连接 this.connectWebSocket(); }, beforeUnmount() { // 组件销毁前断开连接 this.disconnectWebSocket(); } } </script> <style scoped> .message-container { height: 300px; border: 1px solid #ccc; overflow-y: auto; margin-bottom: 10px; padding: 10px; } .message { margin: 5px 0; padding: 5px; background: #f5f5f5; border-radius: 4px; } .input-area { display: flex; gap: 10px; } </style>

重新讲解WebSocket

用一个超形象的比喻理解WebSocket

传统HTTP vs WebSocket

HTTP = 打电话

  • 你拨号 → 对方接听 → 你说一句 → 对方回一句 → 挂断

  • 每次要说新的话,都要重新拨号、接听、挂断

WebSocket = 微信视频通话

  • 拨通一次 → 保持连接 → 双方随时都能说话 → 还能同时说话 → 直到有人挂断

WebSocket 实际开发详解(用WebSocketHandler)

第一步:Spring Boot后端设置

1. 添加依赖
复制代码
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2. 创建WebSocket配置(就像安装电话线路)
复制代码
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 注册WebSocket处理器,"/ws"就是客户端的连接地址
        registry.addHandler(new MyWebSocketHandler(), "/ws")
                .setAllowedOrigins("*"); // 允许所有来源访问
    }
}
3. 创建WebSocket处理器(就像接线员)
复制代码
@Component
public class MyWebSocketHandler extends TextWebSocketHandler {
    
    // 保存所有在线的客户端连接
    private static final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();
    
    // 📞 当有客户端连接时(有人打来电话)
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.add(session);
        System.out.println("新客户端连接: " + session.getId());
        
        // 🎤 主动向这个客户端发送欢迎消息
        session.sendMessage(new TextMessage("欢迎连接!你是第" + sessions.size() +个用户"));
        
        // 📢 向所有客户端广播有新用户加入
        broadcast("系统消息: 有新用户加入聊天室");
    }
    
    // 💬 当收到客户端发来的消息时(对方说话了)
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String clientMessage = message.getPayload(); // 获取客户端发来的内容
        String clientId = session.getId();
        
        System.out.println("收到来自 " + clientId + " 的消息: " + clientMessage);
        
        // 🎤 给这个客户端单独回复
        String personalReply = "你说: " + clientMessage;
        session.sendMessage(new TextMessage(personalReply));
        
        // 📢 向所有其他客户端广播这条消息(群聊效果)
        String broadcastMsg = "用户" + clientId.substring(0, 6) + "说: " + clientMessage;
        broadcastToOthers(session, broadcastMsg);
    }
    
    // 📞 当客户端断开连接时(对方挂电话了)
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        sessions.remove(session);
        System.out.println("客户端断开: " + session.getId());
        
        // 📢 通知其他用户有人离开
        broadcast("系统消息: 有用户离开了聊天室");
    }
    
    // 📢 广播消息给所有客户端
    private void broadcast(String message) {
        for (WebSocketSession session : sessions) {
            try {
                if (session.isOpen()) {
                    session.sendMessage(new TextMessage(message));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    // 📢 广播给除指定session外的所有客户端
    private void broadcastToOthers(WebSocketSession excludeSession, String message) {
        for (WebSocketSession session : sessions) {
            if (!session.getId().equals(excludeSession.getId()) && session.isOpen()) {
                try {
                    session.sendMessage(new TextMessage(message));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    // 🎯 主动给特定用户发送消息(私聊)
    public void sendToUser(String targetSessionId, String message) {
        for (WebSocketSession session : sessions) {
            if (session.getId().equals(targetSessionId) && session.isOpen()) {
                try {
                    session.sendMessage(new TextMessage("私信: " + message));
                } catch (IOException e) {
                    e.printStackTrace();
                }
                break;
            }
        }
    }
}

第二步:Vue前端使用WebSocket

1. 创建WebSocket聊天组件
复制代码
<template>
  <div class="websocket-demo">
    <h2>💬 实时聊天室 (WebSocket)</h2>
    
    <!-- 连接状态显示 -->
    <div class="status" :class="connectionStatus">
      状态: {{ statusText }}
    </div>
    
    <!-- 连接控制按钮 -->
    <div class="controls">
      <button @click="connect" :disabled="isConnected">📞 连接</button>
      <button @click="disconnect" :disabled="!isConnected">❌ 断开</button>
    </div>
    
    <!-- 消息显示区域 -->
    <div class="messages">
      <div 
        v-for="(msg, index) in messages" 
        :key="index" 
        class="message"
        :class="{ 'my-message': msg.isMyMessage }"
      >
        {{ msg.content }}
      </div>
    </div>
    
    <!-- 消息发送区域 -->
    <div class="send-area">
      <input 
        v-model="inputMessage" 
        @keyup.enter="sendMessage"
        placeholder="输入消息..."
        :disabled="!isConnected"
      >
      <button @click="sendMessage" :disabled="!isConnected">📤 发送</button>
    </div>
    
    <!-- 在线用户列表 -->
    <div class="online-users" v-if="onlineUsers.length > 0">
      <h4>👥 在线用户</h4>
      <div v-for="user in onlineUsers" :key="user" class="user">
        {{ user }}
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'WebSocketChat',
  data() {
    return {
      websocket: null,           // WebSocket连接实例
      messages: [],              // 存储所有消息
      inputMessage: '',          // 输入框的消息
      connectionStatus: 'disconnected', // 连接状态
      onlineUsers: [],           // 在线用户列表
      myUserId: ''               // 我的用户ID
    }
  },
  computed: {
    // 计算属性:是否已连接
    isConnected() {
      return this.websocket && this.websocket.readyState === WebSocket.OPEN;
    },
    // 计算属性:状态显示文本
    statusText() {
      switch (this.connectionStatus) {
        case 'connected': return '🟢 已连接';
        case 'connecting': return '🟡 连接中...';
        case 'disconnected': return '🔴 未连接';
        case 'error': return '🔴 连接错误';
        default: return '未知状态';
      }
    }
  },
  methods: {
    // 📞 建立WebSocket连接
    connect() {
      this.connectionStatus = 'connecting';
      
      // 创建WebSocket连接,连接到后端的 /ws 端点
      this.websocket = new WebSocket('ws://localhost:8080/ws');
      
      // 🎧 监听连接成功事件
      this.websocket.onopen = (event) => {
        console.log('✅ WebSocket连接成功');
        this.connectionStatus = 'connected';
        this.addSystemMessage('连接服务器成功!');
      };
      
      // 🎧 监听收到消息事件(最重要的部分!)
      this.websocket.onmessage = (event) => {
        console.log('📨 收到服务器消息:', event.data);
        
        // 处理接收到的消息
        this.handleReceivedMessage(event.data);
      };
      
      // 🎧 监听连接关闭事件
      this.websocket.onclose = (event) => {
        console.log('❌ WebSocket连接关闭');
        this.connectionStatus = 'disconnected';
        this.addSystemMessage('连接已断开');
      };
      
      // 🎧 监听连接错误事件
      this.websocket.onerror = (error) => {
        console.error('💥 WebSocket错误:', error);
        this.connectionStatus = 'error';
        this.addSystemMessage('连接发生错误');
      };
    },
    
    // ❌ 断开WebSocket连接
    disconnect() {
      if (this.websocket) {
        this.websocket.close();
      }
    },
    
    // 📤 发送消息到服务器
    sendMessage() {
      if (this.inputMessage.trim() && this.isConnected) {
        // 发送消息
        this.websocket.send(this.inputMessage);
        
        // 在消息列表中显示自己发送的消息
        this.messages.push({
          content: `我: ${this.inputMessage}`,
          isMyMessage: true
        });
        
        // 清空输入框
        this.inputMessage = '';
        
        // 滚动到底部
        this.$nextTick(() => {
          this.scrollToBottom();
        });
      }
    },
    
    // 🎯 处理接收到的消息
    handleReceivedMessage(message) {
      // 根据消息内容进行不同的处理
      if (message.includes('系统消息')) {
        this.addSystemMessage(message);
      } else if (message.includes('欢迎连接')) {
        this.addSystemMessage(message);
      } else if (message.includes('你说:')) {
        // 服务器对个人消息的确认
        this.messages.push({
          content: `系统: ${message}`,
          isMyMessage: false
        });
      } else {
        // 其他用户的消息
        this.messages.push({
          content: message,
          isMyMessage: false
        });
      }
      
      // 滚动到底部
      this.$nextTick(() => {
        this.scrollToBottom();
      });
    },
    
    // 📝 添加系统消息
    addSystemMessage(message) {
      this.messages.push({
        content: `💡 ${message}`,
        isMyMessage: false
      });
    },
    
    // 📜 滚动到底部
    scrollToBottom() {
      const messagesContainer = this.$el.querySelector('.messages');
      if (messagesContainer) {
        messagesContainer.scrollTop = messagesContainer.scrollHeight;
      }
    }
  },
  
  // 组件挂载时自动连接
  mounted() {
    this.connect();
  },
  
  // 组件销毁前断开连接
  beforeUnmount() {
    this.disconnect();
  }
}
</script>

<style scoped>
.websocket-demo {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
  font-family: Arial, sans-serif;
}

.status {
  padding: 10px;
  border-radius: 5px;
  margin-bottom: 10px;
  text-align: center;
  font-weight: bold;
}

.status.connected {
  background-color: #d4edda;
  color: #155724;
}

.status.connecting {
  background-color: #fff3cd;
  color: #856404;
}

.status.disconnected, .status.error {
  background-color: #f8d7da;
  color: #721c24;
}

.controls, .send-area {
  display: flex;
  gap: 10px;
  margin-bottom: 15px;
}

.controls button, .send-area button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.controls button:disabled, .send-area button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.messages {
  height: 300px;
  border: 1px solid #ddd;
  border-radius: 5px;
  padding: 10px;
  overflow-y: auto;
  margin-bottom: 15px;
  background-color: #f9f9f9;
}

.message {
  margin: 8px 0;
  padding: 8px 12px;
  border-radius: 15px;
  max-width: 80%;
  word-wrap: break-word;
}

.message:not(.my-message) {
  background-color: #e3f2fd;
  align-self: flex-start;
}

.message.my-message {
  background-color: #c8e6c9;
  margin-left: auto;
  text-align: right;
}

.send-area input {
  flex: 1;
  padding: 8px 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.online-users {
  margin-top: 20px;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
  background-color: #f0f8ff;
}

.user {
  padding: 5px;
  border-bottom: 1px solid #e0e0e0;
}
</style>

四个关键事件:

  1. onopen - 连接建立时(电话接通)

  2. onmessage - 收到消息时(听到对方说话)

  3. onclose - 连接关闭时(对方挂断)

  4. onerror - 连接错误时(通话故障)

核心方法:

  • WebSocket.send() - 发送消息(说话)

  • WebSocket.close() - 关闭连接(挂断)

连接状态:

  • WebSocket.CONNECTING (0) - 连接中

  • WebSocket.OPEN (1) - 已连接

  • WebSocket.CLOSING (2) - 关闭中

  • WebSocket.CLOSED (3) - 已关闭

WebSocket 实际开发详解(用**@ServerEndpoint)**

方式 优点 缺点
WebSocketHandler 更Spring风格,依赖注入方便 代码相对繁琐
@ServerEndpoint 代码简洁,标准JSR-356 依赖注入需要特殊处理

第一步:Spring Boot后端配置

1. 添加依赖(与之前相同)
复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2. 关键配置:启用ServerEndpoint
复制代码
@Configuration
public class WebSocketConfig {
    
    /**
     * 这个Bean是必须的!
     * 它会自动扫描带有@ServerEndpoint注解的类并注册为WebSocket端点
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
3. 使用@ServerEndpoint创建WebSocket端点
复制代码
@Component
@ServerEndpoint("/ws/chat")  // 🔑 定义WebSocket连接路径
public class ChatWebSocketEndpoint {
    
    // 保存所有连接的会话
    private static final Map<String, Session> sessions = new ConcurrentHashMap<>();
    
    // 🔧 解决@ServerEndpoint中@Autowired为null的问题
    private static SomeService someService;
    
    @Autowired
    public void setSomeService(SomeService someService) {
        ChatWebSocketEndpoint.someService = someService;
    }
    
    // 📞 连接建立时调用
    @OnOpen
    public void onOpen(Session session) {
        String sessionId = session.getId();
        sessions.put(sessionId, session);
        
        System.out.println("🔗 新连接: " + sessionId + ", 当前在线: " + sessions.size());
        
        // 🎤 发送欢迎消息
        sendMessage(session, "欢迎加入聊天室!你的ID: " + sessionId);
        
        // 📢 广播用户上线通知
        broadcast("系统: 用户 " + sessionId.substring(0, 6) + " 加入聊天室", sessionId);
    }
    
    // 💬 收到客户端消息时调用
    @OnMessage
    public void onMessage(String message, Session session) {
        String sessionId = session.getId();
        System.out.println("📨 收到消息: " + message + " from " + sessionId);
        
        // 🎯 处理不同类型的消息
        if (message.startsWith("/name ")) {
            // 设置昵称
            handleSetName(message, session);
        } else {
            // 普通聊天消息
            String userNickname = getUserNickname(session);
            String broadcastMsg = userNickname + ": " + message;
            
            // 📢 广播消息给所有人(除了发送者)
            broadcast(broadcastMsg, sessionId);
            
            // 🎤 给发送者回执
            sendMessage(session, "我: " + message);
        }
    }
    
    // 📞 连接关闭时调用
    @OnClose
    public void onClose(Session session) {
        String sessionId = session.getId();
        sessions.remove(sessionId);
        
        System.out.println("❌ 连接关闭: " + sessionId + ", 剩余在线: " + sessions.size());
        
        // 📢 广播用户离开通知
        broadcast("系统: 用户 " + sessionId.substring(0, 6) + " 离开聊天室", sessionId);
    }
    
    // ⚠️ 发生错误时调用
    @OnError
    public void onError(Session session, Throwable error) {
        String sessionId = session.getId();
        System.err.println("💥 WebSocket错误 [" + sessionId + "]: " + error.getMessage());
        error.printStackTrace();
    }
    
    // 🎯 处理设置昵称
    private void handleSetName(String message, Session session) {
        try {
            String nickname = message.substring(6).trim(); // 提取昵称
            session.getUserProperties().put("nickname", nickname);
            sendMessage(session, "系统: 昵称已设置为: " + nickname);
        } catch (Exception e) {
            sendMessage(session, "系统: 设置昵称失败");
        }
    }
    
    // 🎯 获取用户昵称
    private String getUserNickname(Session session) {
        String nickname = (String) session.getUserProperties().get("nickname");
        return nickname != null ? nickname : session.getId().substring(0, 6);
    }
    
    // 📤 发送消息给单个客户端
    private void sendMessage(Session session, String message) {
        try {
            if (session.isOpen()) {
                session.getBasicRemote().sendText(message);
            }
        } catch (IOException e) {
            System.err.println("发送消息失败: " + e.getMessage());
        }
    }
    
    // 📢 广播消息给所有客户端(可排除指定会话)
    private void broadcast(String message, String excludeSessionId) {
        sessions.forEach((sessionId, session) -> {
            if (!sessionId.equals(excludeSessionId) && session.isOpen()) {
                try {
                    session.getBasicRemote().sendText(message);
                } catch (IOException e) {
                    System.err.println("广播消息失败: " + e.getMessage());
                }
            }
        });
    }
    
    // 🎯 主动发送消息给特定用户(可从其他业务类调用)
    public static void sendToUser(String targetSessionId, String message) {
        Session session = sessions.get(targetSessionId);
        if (session != null && session.isOpen()) {
            try {
                session.getBasicRemote().sendText("私信: " + message);
            } catch (IOException e) {
                System.err.println("发送私信失败: " + e.getMessage());
            }
        }
    }
    
    // 📊 获取在线用户数
    public static int getOnlineCount() {
        return sessions.size();
    }
    
    // 👥 获取所有在线会话ID
    public static Set<String> getOnlineUsers() {
        return sessions.keySet();
    }
}
4. 业务服务示例(展示依赖注入)
复制代码
@Service
public class SomeService {
    
    public String processMessage(String message) {
        // 这里可以处理业务逻辑,比如保存到数据库、调用其他服务等
        return "处理后的消息: " + message.toUpperCase();
    }
    
    // 主动推送消息到WebSocket
    public void pushNotification(String message) {
        // 可以在这里调用WebSocket的静态方法进行推送
        ChatWebSocketEndpoint.broadcastToAll("通知: " + message);
    }
}

第二步:Vue前端使用(与之前类似,但连接地址不同)

复制代码
<template>
  <div class="chat-room">
    <h2>💬 聊天室 (@ServerEndpoint方式)</h2>
    
    <div class="connection-info">
      <span>状态: {{ connectionStatus }}</span>
      <span>在线用户: {{ onlineCount }}</span>
    </div>
    
    <!-- 消息显示区域 -->
    <div class="messages-container">
      <div 
        v-for="(msg, index) in messages" 
        :key="index" 
        class="message"
        :class="{
          'system-message': msg.type === 'system',
          'my-message': msg.type === 'my',
          'other-message': msg.type === 'other'
        }"
      >
        {{ msg.content }}
      </div>
    </div>
    
    <!-- 消息发送区域 -->
    <div class="input-area">
      <input 
        v-model="inputMessage" 
        @keyup.enter="sendMessage"
        placeholder="输入消息... 输入 /name 昵称 来设置昵称"
        :disabled="!isConnected"
      >
      <button @click="sendMessage" :disabled="!isConnected">发送</button>
      <button @click="setNickname">设置昵称</button>
    </div>
    
    <!-- 在线用户列表 -->
    <div class="online-users">
      <h4>👥 在线用户 ({{ onlineUsers.length }})</h4>
      <div v-for="user in onlineUsers" :key="user" class="user-item">
        {{ user }}
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ChatRoom',
  data() {
    return {
      websocket: null,
      messages: [],
      inputMessage: '',
      connectionStatus: 'disconnected',
      onlineUsers: [],
      onlineCount: 0,
      myNickname: '用户' + Math.random().toString(36).substr(2, 5)
    }
  },
  computed: {
    isConnected() {
      return this.websocket && this.websocket.readyState === WebSocket.OPEN;
    }
  },
  methods: {
    // 连接WebSocket
    connect() {
      this.connectionStatus = 'connecting';
      
      // 🔑 注意:连接地址与@ServerEndpoint注解的路径一致
      this.websocket = new WebSocket('ws://localhost:8080/ws/chat');
      
      this.websocket.onopen = () => {
        this.connectionStatus = 'connected';
        this.addSystemMessage('连接成功!');
        
        // 连接成功后设置昵称
        this.setNickname();
      };
      
      this.websocket.onmessage = (event) => {
        this.handleReceivedMessage(event.data);
      };
      
      this.websocket.onclose = () => {
        this.connectionStatus = 'disconnected';
        this.addSystemMessage('连接已断开');
      };
      
      this.websocket.onerror = (error) => {
        this.connectionStatus = 'error';
        this.addSystemMessage('连接错误: ' + error);
      };
    },
    
    // 处理接收到的消息
    handleReceivedMessage(message) {
      if (message.includes('系统:') || message.includes('欢迎') || message.includes('昵称')) {
        this.messages.push({
          content: message,
          type: 'system'
        });
      } else if (message.startsWith('我:')) {
        this.messages.push({
          content: message,
          type: 'my'
        });
      } else {
        this.messages.push({
          content: message,
          type: 'other'
        });
        
        // 简单的在线用户数统计(根据消息内容判断)
        if (message.includes('加入聊天室')) {
          this.onlineCount++;
        } else if (message.includes('离开聊天室')) {
          this.onlineCount = Math.max(0, this.onlineCount - 1);
        }
      }
      
      this.scrollToBottom();
    },
    
    // 发送消息
    sendMessage() {
      if (this.inputMessage.trim() && this.isConnected) {
        this.websocket.send(this.inputMessage);
        this.inputMessage = '';
      }
    },
    
    // 设置昵称
    setNickname() {
      if (this.isConnected) {
        this.websocket.send('/name ' + this.myNickname);
      }
    },
    
    // 添加系统消息
    addSystemMessage(message) {
      this.messages.push({
        content: message,
        type: 'system'
      });
    },
    
    // 滚动到底部
    scrollToBottom() {
      this.$nextTick(() => {
        const container = this.$el.querySelector('.messages-container');
        if (container) {
          container.scrollTop = container.scrollHeight;
        }
      });
    },
    
    // 断开连接
    disconnect() {
      if (this.websocket) {
        this.websocket.close();
      }
    }
  },
  
  mounted() {
    this.connect();
  },
  
  beforeUnmount() {
    this.disconnect();
  }
}
</script>

<style scoped>
.chat-room {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  display: grid;
  grid-template-columns: 1fr 200px;
  grid-gap: 20px;
}

.connection-info {
  grid-column: 1 / -1;
  display: flex;
  justify-content: space-between;
  padding: 10px;
  background: #f5f5f5;
  border-radius: 5px;
}

.messages-container {
  grid-column: 1;
  height: 400px;
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 15px;
  overflow-y: auto;
  background: #fafafa;
}

.message {
  margin: 10px 0;
  padding: 8px 12px;
  border-radius: 8px;
  word-wrap: break-word;
}

.system-message {
  background: #fff3cd;
  color: #856404;
  text-align: center;
  font-style: italic;
}

.my-message {
  background: #d1ecf1;
  margin-left: 20%;
  text-align: right;
}

.other-message {
  background: #e2e3e5;
  margin-right: 20%;
}

.input-area {
  grid-column: 1;
  display: flex;
  gap: 10px;
}

.input-area input {
  flex: 1;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.input-area button {
  padding: 10px 15px;
  border: none;
  border-radius: 4px;
  background: #007bff;
  color: white;
  cursor: pointer;
}

.input-area button:disabled {
  background: #6c757d;
  cursor: not-allowed;
}

.online-users {
  grid-column: 2;
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 15px;
  background: white;
}

.online-users h4 {
  margin: 0 0 10px 0;
  color: #333;
}

.user-item {
  padding: 5px 0;
  border-bottom: 1px solid #f0f0f0;
}
</style>

@ServerEndpoint 核心注解说明

四个核心注解:

@OnOpen - 连接建立时

复制代码
@OnOpen
public void onOpen(Session session) {
    // session: 代表一个WebSocket连接
}

@OnMessage - 收到消息时

复制代码
@OnMessage
public void onMessage(String message, Session session) {
    // message: 客户端发送的消息内容
}

@OnClose - 连接关闭时

复制代码
@OnClose
public void onClose(Session session) {
    // 连接关闭清理工作
}

@OnError - 发生错误时

复制代码
@OnError
public void onError(Session session, Throwable error) {
    // 错误处理
}
重要特性:
  1. Session对象
  • 每个连接都有一个唯一的Session
  • 可以存储用户属性:session.getUserProperties().put("key", value)
  • 可以发送消息:session.getBasicRemote().sendText(message)
  1. 依赖注入问题解决

由于@ServerEndpoint实例由WebSocket容器创建,不是Spring管理的,所以需要特殊处理:

复制代码
@Component
@ServerEndpoint("/ws/chat")
public class MyEndpoint {
    
    private static SomeService someService;
    
    @Autowired
    public void setSomeService(SomeService someService) {
        MyEndpoint.someService = someService; // 静态变量保存
    }
}
  1. 路径参数支持

    @ServerEndpoint("/ws/chat/{roomId}/{userId}")
    public class ChatEndpoint {

    复制代码
     @OnOpen
     public void onOpen(Session session, 
                       @PathParam("roomId") String roomId,
                       @PathParam("userId") String userId) {
         // 可以获取路径参数
         System.out.println("房间: " + roomId + ", 用户: " + userId);
     }

    }

两种方式对比总结

特性 WebSocketHandler @ServerEndpoint
代码风格 Spring风格 JSR-356标准风格
依赖注入 直接使用@Autowired 需要静态变量中转
配置方式 实现接口+注册 注解+ServerEndpointExporter
Session管理 手动管理集合 手动管理集合
适用场景 复杂业务逻辑 简单实时通信

选择建议:

  • 新手/简单项目 :推荐@ServerEndpoint,代码更简洁

  • 复杂业务/需要深度Spring集成 :推荐WebSocketHandler

相关推荐
GilgameshJSS5 小时前
STM32H743-ARM例程29-HTTP
c语言·arm开发·stm32·单片机·http
l1t5 小时前
在DuckDB中使用http(s)代理
数据库·网络协议·http·xlsx·1024程序员节·duckdb
2301_803554525 小时前
Http学习
网络协议·学习·http
KAI丶15 小时前
【Https】Received fatal alert: internal_error
https·1024程序员节
jfqqqqq16 小时前
使用pem和key文件给springboot开启https服务
网络协议·http·https
Tony Bai17 小时前
【Go 网络编程全解】13 从 HTTP/1.1 到 gRPC:Web API 与微服务的演进
开发语言·网络·http·微服务·golang
易ლ拉罐19 小时前
【计算机网络】HTTP协议(二)——超文本传输协议
网络·计算机网络·http·1024程序员节
BIBI20491 天前
HTTP 请求方法指南:GET, POST, PUT, PATCH, DELETE 区别
网络·网络协议·http
fenglllle1 天前
http trailer 与 http2
http·wireshark·1024程序员节