HTTP的不足------半双工
-
HTTP 三大核心基础特性
(1)无状态
HTTP 协议本身不保存客户端与服务端的通信记录,每次请求都是独立、隔离的。服务端无法识别两次请求是否来自同一个用户,不会自动留存会话数据;若要实现登录、用户身份识别,必须借助 Cookie、Session、Token 等额外机制手动维护状态。
(2)短连接(默认短连接,HTTP/1.1 默认长连接仅复用 TCP 通道)
一次完整流程:客户端发起请求 → 服务端处理并返回完整响应 → TCP 连接立即断开。
即便 HTTP/1.1 通过 Connection: keep-alive 支持连接复用,也只是短暂保持通道,闲置一段时间后仍会主动断开;不存在永久持续的通信链路,每次数据交互都需要重新建立 / 复用连接,开销较高。
(3)半双工通信
半双工指通信双方拥有收发能力,但同一时间仅允许一方发送数据,另一方只能接收,无法同时双向传输。
HTTP 采用「请求 - 响应」固定模型,强制单向交互:
只能由客户端主动发起请求,服务端收到请求后才能返回响应;在无客户端请求的前提下,服务端没有任何渠道主动向客户端下发数据。
-
HTTP 面向实时业务的核心痛点
- 无法主动推送:服务端产生新消息、实时数据时,不能主动下发给前端,只能等待客户端来拉取;
- 实时性差:消息存在延迟,数据更新完全依赖客户端请求频率;
- 资源浪费:若要降低延迟,只能提高请求频次,产生大量无效空请求,消耗带宽与服务端性能;
- 不支持双向交互:聊天、协同编辑、实时对战等双方频繁互发消息的场景完全无法原生实现。
- 前端实时通信技术迭代演进路线
为弥补 HTTP 无法实时推送的短板,行业基于原有协议逐步迭代出四类方案,复杂度、实时性依次提升:- 短轮询:最简单的兼容方案,前端定时循环发起 HTTP 请求,主动拉取最新数据;
- 长轮询:优化短轮询无效请求问题,前端发起请求后服务端阻塞挂起连接,有新数据 / 超时才返回,收到响应后前端立刻重连;
- SSE(Server-Sent Events):基于 HTTP 流式长连接,专门实现服务端单向持续推送,浏览器原生支持自动重连,仅支持后端发、前端收;
- WebSocket:全新独立通信协议,通过一次 HTTP 握手升级为 TCP 持久长连接,实现全双工双向通信,客户端与服务端可随时主动发送数据,是实时交互场景最优解
一、轮询
1.1 轮询
客户端定时向后端发送 HTTP 请求,后端无论有无新数据,立刻返回响应,客户端拿到结果后再次定时请求。
这种方式实现简单,但效率较低,客户端未更新时产生的是无效请求。
1.2 长轮询
客户端发起请求,后端不立即响应,而是hang住挂起连接,直到后端有新数据、或超时,才返回结果,客户端收到响应后,立刻再次发起新请求,保持持续连接。
解决短轮询频繁无效请求的问题,同时可支持弹性扩展,但保持链接需消耗更多资源。
1.3 实现方法
实现方法为前端递归轮询,有消息则获取并直接进行下一次监听,后端消息发生变化时返回推送,否则到达超时时间则返回空消息。
Vue前端轮询请求:
javascript
<template>
<div>
<h3>长轮询最新消息:{{ longMsg }}</h3>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import axios from 'axios'
const longMsg = ref('')
let pollingFlag = true
// 递归持续长轮询
function startLongPoll() {
if (!pollingFlag) return
axios.get('http://localhost:8080/poll/long').then(res => {
// 收到新消息更新页面
if (res.data.newMsg) {
longMsg.value = res.data.data
}
// 立刻再次发起请求,保持长连接循环
startLongPoll()
}).catch(err => {
console.error('长轮询异常,3s后重连', err)
setTimeout(startLongPoll, 3000)
})
}
onMounted(() => {
startLongPoll()
})
// 组件卸载停止轮询
onUnmounted(() => {
pollingFlag = false
})
</script>
Java后端
java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/poll")
public class LongPollController {
// 存储消息
private String message = "默认消息";
// 标记是否有新消息
private volatile boolean hasNew = false;
// 模拟后台推送新消息
@GetMapping("/setMsg")
public String setMsg(String content) {
this.message = content;
this.hasNew = true;
return "已推送新消息";
}
@GetMapping("/long")
public Map<String, Object> longPoll() throws InterruptedException {
// 阻塞等待,最多等待5秒超时
long waitMax = 5000;
long start = System.currentTimeMillis();
// 无新消息则循环等待
while (!hasNew) {
TimeUnit.MILLISECONDS.sleep(200);
// 超时直接返回空标记
if (System.currentTimeMillis() - start > waitMax) {
return Map.of("code", 200, "data", "", "newMsg", false);
}
}
// 有新消息,重置标记并返回
hasNew = false;
return Map.of(
"code", 200,
"data", message,
"newMsg", true
);
}
}
1.4 评价与适用场景
短轮询
优点:实现简单、兼容性好、服务端逻辑简单
缺点:请求量大、无效请求多、占用带宽、延迟高
场景:简单心跳、低实时性场景(简单消息刷新、简单状态同步)
长轮询
优点:减少无效请求、实时性优于短轮询、基于 HTTP 无兼容性问题
缺点:服务端连接挂起占用资源、超时重连、并发压力大
场景:早期 IM 聊天、简单消息通知、银行通知、小程序实时状态
二、SSE
2.1 Server-Sent Events
SSE 全称 Server-Sent Events,是一套完全基于标准 HTTP 协议的浏览器实时推送规范,不需要像 WebSocket 那样进行协议升级握手,仅依靠普通 GET 请求即可建立长连接。
核心通信方向为单向通信:只允许服务端主动向客户端持续下发数据流,客户端没有任何接口、机制向服务端发送消息,天生只适用于服务端下发数据的场景。
SSE 能够持续分段推送数据,依靠固定 MIME 类型标识流传输,后端必须在响应头设置:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
text/event-stream:告诉浏览器当前响应是持续文本流,不要一次性缓存完整响应再返回,收到一段就立刻交给前端解析;
Cache-Control: no-cache:关闭浏览器缓存,避免推送数据被缓存拦截;
Connection: keep-alive:维持 TCP 长连接,持续传输消息。
同时 SSE 规定了固定消息分段格式,每条消息由多行标识组成,以空行 \n\n 作为一条消息结束分隔符,支持三类关键字段:
- data::承载实际传输的数据(字符串 / JSON),最核心字段;
- event::自定义事件名称,前端可单独监听不同类型业务消息;
- id:事件唯一 ID,用于断点续传;
- retry::单位毫秒,指定断线后浏览器自动重连的间隔时间。
标准消息示例:
event: notice
id: 1001
data: {"title":"系统公告","content":"服务器已完成升级"}
2.2 实现方法
后端:设置响应头 Content-Type: text/event-stream,使用SseEmitter持续推送流数据(该对象会根据请求自动对应地址,Tomcat自动封装上下文),使用SSE推送的Java代码如下:
java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/api/sse")
public class SseController {
// 定时线程模拟业务实时数据
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
@GetMapping("/connect")
public SseEmitter connect() {
// 设置连接超时 60s,0L=永不超时,emitter 对象是一对一绑定单个浏览器连接的,前端发起请求自动封装上下文
SseEmitter emitter = new SseEmitter(60000L);
// 连接关闭/异常回调
emitter.onCompletion(() -> System.out.println("SSE 连接关闭"));
emitter.onError((e) -> {
System.err.println("SSE异常:" + e.getMessage());
emitter.complete();
});
emitter.onTimeout(() -> System.out.println("SSE连接超时"));
// 每秒推送一条实时消息
executor.scheduleAtFixedRate(() -> {
try {
String data = "当前服务时间:" + System.currentTimeMillis();
// 1. 创建一条标准SSE消息构建器
SseEmitter.SseEventBuilder event = SseEmitter.event()
.name("message") // 自定义事件类型标识
.data(data); // 要推送给前端的业务数据
// 2. 把这条消息通过当前客户端长连接发送出去
emitter.send(event);
} catch (IOException e) {
emitter.complete();
}
}, 0, 1, TimeUnit.SECONDS);
return emitter;
}
}
前端:使用浏览器原生API EventSource(url) 对接 SSE 服务,发起一条GET 长连接 HTTP 请求,持续接收后端 text/event-stream 流式数据,可指定具体事件与后端匹配,vue前端接收代码如下:
javascript
<template>
<div class="sse-box">
<h3>SSE实时消息推送</h3>
<div v-for="item in msgList" :key="item">{{ item }}</div>
<button @click="closeSse">手动断开连接</button>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const msgList = ref([])
let eventSource = null
// 初始化SSE连接
const openSse = () => {
const url = 'http://localhost:8080/api/sse/connect'
eventSource = new EventSource(url)
// 监听后端name="message"事件
eventSource.addEventListener('message', e => {
msgList.value.push(e.data)
})
// 连接出错,浏览器会自动重试
eventSource.onerror = err => {
console.log('SSE连接异常', err)
}
}
// 手动关闭
const closeSse = () => {
if (eventSource) {
eventSource.close()
console.log("已关闭SSE")
}
}
onMounted(() => openSse())
// 页面销毁关闭连接,防止内存泄漏
onUnmounted(() => closeSse())
</script>
出于安全性,浏览器限制脚本内发起的跨源 HTTP 请求,强制实施同源策略,只有满足下面三点全部相同,才属于同源,接口请求不受限制:
- 协议相同:http /https
- 域名 / IP 相同:localhost / 127.0.0.1 / 业务域名
- 端口相同:8080、5173、3000
只要任意一项不同,就是跨域请求,浏览器会拦截后端返回的数据,控制台报 CORS 跨域错误。
前后端项目通常配置为跨域场景
Vue 前端地址:http://localhost:5173
SpringBoot 后端地址:http://localhost:8080
二者端口不一致,属于典型跨域,不配置 CORS 前端拿不到后端数据。
CORS = Cross-Origin Resource Sharing 跨域资源共享 ,是一套后端配置规则,让后端主动告诉浏览器:允许某个前端域名跨域访问我的接口。
CORS 由后端响应头实现,WebMvcConfigurer 是 Spring 提供的简化配置方式,不用手动写响应头
后端配置如下:
java
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final JwtInterceptor jwtInterceptor;
/**
* CORS 配置:允许前端开发服务器(localhost:5173)跨域访问所有 /api/** 接口。
* allowCredentials=true 支持携带 Cookie;生产环境应限制 allowedOrigins。
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:5173")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
/**
* 注册 JWT 拦截器:
* - 拦截生成与查询接口(需要登录)
* - 排除认证接口(注册/登录本身不需要 Token)
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/api/generate", "/api/task/**", "/api/user/**")
.excludePathPatterns("/api/auth/**");
}
}
2.3 评价与适用场景
优点:
- 轻量、带宽开销极低;
- 浏览器原生EventSourceAPI,无需第三方库;
- 底层内置断线自动重连、支持断点续传(Last-Event-ID);
- 基于标准 HTTP,网关、防火墙、Nginx 友好,跨域配置和普通接口一致,无 WebSocket 特殊代理配置。
缺点:
- 严格单向通信:只能服务端→客户端推送,前端无法发送数据;
- 老旧 IE 等浏览器不兼容;
- 单条连接仅支持下行,无法双向交互;
- 不支持二进制消息,只能传utf-8文本。
适用场景:
监控大屏、实时日志、股票行情、系统公告、文件导出 / 任务进度推送等仅后端主动下发数据的业务。
SseEmitter 的长连接是基于异步非阻塞 Web 模型,不占用 Tomcat 核心工作线程,同时一条 TCP 连接复用全程,不反复新建请求,所以服务端承压能力远高于长轮询。
三、WebSocket
3.1 全双工通信WebSocket
WebSocket 是一套独立的双向通信协议,不基于普通 HTTP 做持续流,依靠一次 HTTP 握手完成协议升级,切换为专用 ws/wss 长连接通道,通信开销小,但开发复杂度较高。
3.2 实现方法
前端:原生 WebSocket 对象
后端通过ServerEndpoint注解绑定url,建立连接、接受消息、连接关闭等事件都通过注解绑定进行操作,语法为
@ServerEndpoint("/ws/chat") // 修饰类,绑定url,前端使用new WebSocket("ws://ip:端口/ws/chat")自动匹配
@OnOpen // 客户端成功建立连接时触发
@OnMessage // 收到前端发送的文本/二进制消息触发
@OnClose // 连接正常关闭触发
@OnError // 连接发生异常、断线报错触发
具体使用方法如下:
引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
实现代码
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
// websocket管理类
@Configuration
public class WebSocketConfig {
// 自动扫描@ServerEndpoint注解的websocket端点
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
@Component
@ServerEndpoint("/ws/chat") // 前端连接地址 ws://localhost:8080/ws/chat
public class WebSocketEndpoint {
// 存储所有在线客户端session
private static final CopyOnWriteArraySet<Session> SESSION_SET = new CopyOnWriteArraySet<>();
private Session session;
// 连接建立成功
@OnOpen
public void onOpen(Session session) {
this.session = session;
SESSION_SET.add(session);
sendMsgAll("有新用户上线,当前在线人数:" + SESSION_SET.size());
}
// 接收前端发送的消息
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("收到前端消息:" + message);
// 广播给所有客户端
sendMsgAll("服务端收到消息:" + message);
}
// 连接关闭
@OnClose
public void onClose() {
SESSION_SET.remove(this.session);
sendMsgAll("用户下线,当前在线人数:" + SESSION_SET.size());
}
// 连接异常
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
// 群发消息
private void sendMsgAll(String msg) {
for (Session s : SESSION_SET) {
try {
s.getBasicRemote().sendText(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
前端使用new WebSocket(url)创建websocket连接,使用后端类似方法处理事件,Vue代码实例如下:
javascript
<template>
<div class="ws-demo">
<h3>WebSocket聊天室</h3>
<!-- 消息列表 -->
<div class="msg-box">
<div v-for="(item, idx) in msgList" :key="idx">{{ item }}</div>
</div>
<!-- 发送消息 -->
<input v-model="sendText" placeholder="输入消息" />
<button @click="sendMsg">发送</button>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const msgList = ref([])
const sendText = ref('')
let ws = null
// 初始化WebSocket连接
const initWs = () => {
// ws协议,后端地址
const url = 'ws://localhost:8080/ws/chat'
ws = new WebSocket(url)
// 连接成功回调
ws.onopen = () => {
console.log('WebSocket连接成功')
}
// 接收后端推送消息
ws.onmessage = (event) => {
console.log('收到后端消息', event.data)
msgList.value.push(event.data)
}
// 连接关闭
ws.onclose = () => {
console.log('连接关闭,可做重连逻辑')
}
// 连接报错
ws.onerror = (err) => {
console.error('WebSocket异常', err)
}
}
// 向服务端发送消息
const sendMsg = () => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert('连接未就绪')
return
}
ws.send(sendText.value)
sendText.value = ''
}
// 页面挂载建立连接
onMounted(() => {
initWs()
})
// 页面销毁关闭连接,防止内存泄漏
onUnmounted(() => {
if (ws) ws.close()
})
</script>
3.3 评价与适用场景
优点:真正实时、双向通信、极低延迟、无冗余请求
缺点:协议特殊、部分防火墙拦截、开发复杂度高 (每个客户端Session独立,若A=>socket1,B=>socket2,A的消息要给B展示,则需要在socket内部增加通信机制,所以开发复杂度高真不是说说而已)
场景:聊天室、直播弹幕、游戏联机、协同编辑、实时对战、智能设备实时通信
四、总结
| 对比维度 | 短轮询 | 长轮询 | SSE | WebSocket |
|---|---|---|---|---|
| 通信模式 | 单向拉取(客户端主动) | 单向拉取(客户端主动) | 单向推送(服务端主动) | 全双工双向通信 |
| 底层协议 | HTTP 短连接 | HTTP 挂起长连接 | HTTP 流式长连接(text/event-stream) | 独立 WS 协议(HTTP 握手升级) |
| 服务端线程模型 | 同步、用完即放 | 同步阻塞(高并发耗线程) | 异步非阻塞(不占业务线程) | 异步非阻塞(高性能) |
| 传参方式 | GET/POST 任意、灵活 | GET/POST 任意、灵活 | 仅支持 GET,不可传请求体 | 握手HTTP传参,连接后自由收发 |
| 数据格式支持 | 任意 HTTP 数据 | 任意 HTTP 数据 | 仅 UTF-8 文本/JSON,不支持二进制 | 文本 + 二进制(文件/流/图片) |
| 带宽开销 | 极高(大量无效请求头) | 中(减少无效请求) | 极低(单长连接持续推送) | 极低(自定义帧、无冗余头) |
| 实时性 | 差(固定间隔延迟) | 中(接近实时) | 高 | 极高(毫秒级) |
| 开发复杂度 | 极低 | 低 | 中 | 较高(需管理会话、广播、重连) |
| 核心优点 | 最简单、兼容所有设备 | 解决无效请求、比短轮询实时 | 轻量、自动重连、服务端压力小 | 双向通信、低延迟、支持二进制 |
| 核心缺点 | 无效请求多、浪费带宽 | 阻塞线程、并发压力大 | 单向、仅GET、无二进制 | 连接独立、需要手动管理会话集群 |
| 最佳适用场景 | 低频刷新、简单状态、老旧设备 | 简单消息通知、老项目兼容 | 大屏数据、日志、进度条、公告推送 | 聊天、弹幕、协同编辑、实时交互 |
web通信的三种方式没有谁完全高于谁的说法,本质上都是trade-off。
实时性要求不高,要求兼容性好的场景可使用轮询,同时需要请求体的消息SSE也无法完成;
只有服务器向前端推送可使用SSE,真正需要双向通信的,才考虑WebSocket。