WebSocket是什么?和HTTP是什么区别?长轮询是什么?服务器推送是什么?
文章目录
- 💥 翻车现场
- 🤔 方案1:短轮询(Polling)
- 原理
- 时序图
- 🤔 方案2:长轮询(Long Polling)
- 原理
- 代码实现
- 长轮询时序图
- 🤔 方案3:SSE(Server-Sent Events)服务器推送
- 原理
- 代码实现
- 🤔 方案4:WebSocket(推荐⭐⭐⭐⭐⭐)
- 原理
- WebSocket握手流程
- 握手时序图
- WebSocket代码实现
- 📊 4种方案对比
- 性能对比(1000个用户在线聊天)
- 资源占用对比
- 流量对比(发送1条消息"hello")
- 🎯 何时用什么方案?
- 选型建议
- 技术栈选择
- 🎓 面试标准答案
- 题目:WebSocket和HTTP有什么区别?
- 题目:长轮询是什么?
- 🎉 结束语
摘要:从一次"实现聊天室功能的技术选型"出发,深度剖析WebSocket、HTTP长轮询、SSE服务器推送的实现原理与性能对比。通过短轮询到长轮询到WebSocket的演进过程、以及协议升级的握手流程,揭秘为什么聊天室必须用WebSocket、为什么长轮询会浪费资源、以及SSE为什么只能单向推送。配合时序图展示消息推送流程,给出不同实时通信场景的最佳选型。
💥 翻车现场
周二下午,哈吉米接到了一个需求。
产品经理:“我们要做一个在线客服聊天功能,用户和客服实时对话。”
哈吉米:“好的!”(内心:怎么实现实时通信?)
第一版:短轮询
// 前端每1秒请求一次新消息
setInterval(() => {
axios.get('/api/message/new').then(resp => {
if (resp.data.length > 0) {
// 显示新消息
showMessages(resp.data);
}
});
}, 1000); // 每秒请求一次
上线第一天:
问题:
1. 服务器QPS暴增(1000个用户 × 1次/秒 = 1000 QPS)
2. 大部分请求都是"没有新消息"(浪费资源)
3. 消息延迟(最多1秒延迟)
4. 用户体验差
哈吉米:“短轮询太浪费了,有没有更好的方案?”
南北绿豆和阿西噶阿西来了。
南北绿豆:“实时通信有4种方案:短轮询、长轮询、SSE、WebSocket。”
阿西噶阿西:“来,我逐个给你讲。”
🤔 方案1:短轮询(Polling)
原理
客户端每隔一段时间(如1秒)请求一次服务器
流程:
1. 客户端:请求新消息
2. 服务器:查询数据库,返回结果(可能为空)
3. 等待1秒
4. 重复步骤1
时序图
优点:
- ✅ 实现简单
缺点:
- ❌ 大量无效请求(浪费服务器资源)
- ❌ 消息延迟(最多1秒)
- ❌ 频繁建立/关闭连接
🤔 方案2:长轮询(Long Polling)
原理
客户端请求后,服务器不立即返回:
- 如果有新消息 → 立即返回
- 如果没有新消息 → 挂起请求,等待(如30秒)
- 超时或有新消息 → 返回
好处:
- 减少无效请求
- 消息实时性好(几乎立即返回)
代码实现
服务端:
@RestController
public class MessageController {
// 用于通知有新消息
private final Map<Long, CountDownLatch> waitingClients = new ConcurrentHashMap<>();
/**
* 长轮询接口
*/
@GetMapping("/api/message/longPolling")
public Result longPolling(@RequestParam Long userId,
@RequestParam Long lastMessageId) {
// 1. 先查询是否有新消息
List<Message> messages = messageService.getNewMessages(userId, lastMessageId);
if (!messages.isEmpty()) {
// 有新消息,立即返回
return Result.ok(messages);
}
// 2. 没有新消息,挂起请求
CountDownLatch latch = new CountDownLatch(1);
waitingClients.put(userId, latch);
try {
// 等待30秒(超时)或被唤醒(有新消息)
boolean hasNew = latch.await(30, TimeUnit.SECONDS);
if (hasNew) {
// 有新消息,查询并返回
messages = messageService.getNewMessages(userId, lastMessageId);
return Result.ok(messages);
} else {
// 超时,返回空
return Result.ok(Collections.emptyList());
}
} catch (InterruptedException e) {
return Result.error("请求中断");
} finally {
waitingClients.remove(userId);
}
}
/**
* 发送消息时,唤醒等待的客户端
*/
@PostMapping("/api/message/send")
public Result sendMessage(@RequestBody Message message) {
// 保存消息
messageService.save(message);
// 唤醒等待的客户端
CountDownLatch latch = waitingClients.get(message.getToUserId());
if (latch != null) {
latch.countDown(); // 唤醒
}
return Result.ok();
}
}
前端:
// 长轮询(递归调用)
function longPolling() {
axios.get('/api/message/longPolling', {
params: {
userId: 10086,
lastMessageId: lastMsgId
},
timeout: 35000 // 超时35秒(比服务器超时长)
}).then(resp => {
if (resp.data.length > 0) {
// 显示新消息
showMessages(resp.data);
lastMsgId = resp.data[resp.data.length - 1].id;
}
// 立即发起下一次请求
longPolling();
}).catch(err => {
// 出错后,等待1秒再重试
setTimeout(longPolling, 1000);
});
}
// 启动长轮询
longPolling();
长轮询时序图
优点:
- ✅ 减少无效请求(有消息才返回)
- ✅ 实时性好(消息立即返回)
缺点:
- ❌ 服务器要维护大量挂起的连接
- ❌ 占用线程资源(每个请求一个线程)
- ❌ 仍然是HTTP请求(有HTTP头开销)
🤔 方案3:SSE(Server-Sent Events)服务器推送
原理
SSE:
- 基于HTTP
- 单向通信(服务器 → 客户端)
- 持久连接
- 文本协议
代码实现
服务端:
@RestController
public class SseController {
/**
* SSE接口
*/
@GetMapping(value = "/api/message/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter sse(@RequestParam Long userId) {
SseEmitter emitter = new SseEmitter(0L); // 0表示不超时
// 异步发送消息
CompletableFuture.runAsync(() -> {
try {
while (true) {
// 查询新消息
List<Message> messages = messageService.getNewMessages(userId);
if (!messages.isEmpty()) {
// 推送消息
emitter.send(messages);
}
// 休息1秒
Thread.sleep(1000);
}
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
}
前端:
// SSE客户端
const eventSource = new EventSource('/api/message/sse?userId=10086');
eventSource.onmessage = function(event) {
const messages = JSON.parse(event.data);
showMessages(messages);
};
eventSource.onerror = function(error) {
console.error('SSE错误', error);
eventSource.close();
};
优点:
- ✅ 单向推送场景很方便
- ✅ 基于HTTP(兼容性好)
- ✅ 自动重连
缺点:
- ❌ 只能服务器推送(单向)
- ❌ 不能客户端发消息
适用场景:
- 实时通知
- 股票行情
- 新闻推送
🤔 方案4:WebSocket(推荐⭐⭐⭐⭐⭐)
原理
WebSocket:
- 全双工通信(客户端 ↔ 服务器)
- 持久连接
- 二进制/文本协议
- 基于TCP,先通过HTTP握手,再升级协议
WebSocket握手流程
HTTP升级为WebSocket:
客户端请求:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket ← 关键:请求升级协议
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL... ← 随机字符串
Sec-WebSocket-Version: 13
服务器响应:
HTTP/1.1 101 Switching Protocols ← 状态码101:协议切换
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0s... ← 根据客户端Key计算
握手完成后:
连接从HTTP协议切换到WebSocket协议
后续通信不再是HTTP,而是WebSocket帧
握手时序图
WebSocket代码实现
服务端(Spring Boot):
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new ChatWebSocketHandler(), "/chat")
.setAllowedOrigins("*");
}
}
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
// 存储所有连接的用户
private static final Map<Long, WebSocketSession> SESSIONS = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
// 连接建立
Long userId = getUserId(session);
SESSIONS.put(userId, session);
System.out.println("用户" + userId + "连接成功");
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
// 接收客户端消息
String payload = message.getPayload();
ChatMessage chatMsg = JSON.parseObject(payload, ChatMessage.class);
// 推送给目标用户
WebSocketSession targetSession = SESSIONS.get(chatMsg.getToUserId());
if (targetSession != null && targetSession.isOpen()) {
targetSession.sendMessage(new TextMessage(JSON.toJSONString(chatMsg)));
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
// 连接关闭
Long userId = getUserId(session);
SESSIONS.remove(userId);
System.out.println("用户" + userId + "断开连接");
}
}
前端:
// WebSocket客户端
const ws = new WebSocket('ws://localhost:8080/chat');
// 连接打开
ws.onopen = function() {
console.log('WebSocket连接成功');
// 发送消息
ws.send(JSON.stringify({
toUserId: 10087,
content: '你好'
}));
};
// 接收消息
ws.onmessage = function(event) {
const message = JSON.parse(event.data);
showMessage(message);
};
// 连接关闭
ws.onclose = function() {
console.log('WebSocket连接关闭');
};
// 错误处理
ws.onerror = function(error) {
console.error('WebSocket错误', error);
};
📊 4种方案对比
性能对比(1000个用户在线聊天)
| 方案 | 请求次数/秒 | 服务器压力 | 实时性 | 双向通信 |
|---|---|---|---|---|
| 短轮询 | 1000 | 极高 | 差(1秒延迟) | ✅ |
| 长轮询 | 33(平均30秒一次) | 高(挂起连接) | 好(立即返回) | ✅ |
| SSE | 1(建立连接) | 中 | 好 | ❌ 单向 |
| WebSocket | 1(建立连接) | 低 | 极好 | ✅ 双向 |
资源占用对比
短轮询:
1000个用户:
- 每秒1000个HTTP请求
- 每个请求:建立连接 + 查询数据库 + 关闭连接
- 数据库QPS:1000
- 网络带宽:1000 × 500字节(HTTP头) = 500KB/s
长轮询:
1000个用户:
- 同时挂起1000个HTTP连接
- 1000个线程(每个连接一个线程)
- 线程栈空间:1000 × 1MB = 1GB
- 消息到达时,立即返回
WebSocket:
1000个用户:
- 1000个WebSocket连接(长连接)
- 使用NIO(一个线程管理多个连接)
- 线程数:10-20个(不是1000个)
- 内存占用:低
- 消息推送:立即(双向通信)
流量对比(发送1条消息"hello")
HTTP短轮询:
HTTP请求:
GET /api/message/new?userId=10086 HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0
Accept: application/json
Cookie: ...
(约300字节HTTP头)
HTTP响应:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 50
{"code":0,"data":[{"content":"hello"}]}
总流量:约400字节
WebSocket:
WebSocket帧:
[Frame Header: 2-6字节][Payload: "hello"]
总流量:约10字节
流量节省:(400 - 10) / 400 = 97.5%
🎯 何时用什么方案?
选型建议
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 在线聊天 | WebSocket | 双向、实时、低延迟 |
| 游戏 | WebSocket | 实时性要求高 |
| 协同编辑 | WebSocket | 双向实时同步 |
| 实时通知 | SSE | 单向推送即可 |
| 股票行情 | SSE | 单向推送 |
| 简单轮询 | 短轮询 | 实时性要求不高 |
| 兼容性要求高 | 长轮询 | 兼容老浏览器 |
技术栈选择
Spring生态:
WebSocket:
- Spring WebSocket
- STOMP协议(消息代理)
- SockJS(兼容方案)
SSE:
- Spring MVC的SseEmitter
Netty:
- 高性能WebSocket服务器
- 适合百万连接
🎓 面试标准答案
题目:WebSocket和HTTP有什么区别?
答案:
核心区别:
| 特性 | HTTP | WebSocket |
|---|---|---|
| 通信方式 | 请求-响应(半双工) | 双向通信(全双工) |
| 连接 | 短连接(或Keep-Alive) | 长连接 |
| 协议 | 应用层协议 | 基于TCP,先HTTP握手再升级 |
| 开销 | 每次请求有HTTP头(300-500字节) | 握手后只有2-6字节帧头 |
| 实时性 | 差(轮询) | 好(推送) |
| 服务器推送 | ❌ 不支持(需要轮询) | ✅ 支持 |
| 浏览器支持 | ✅ 所有 | ✅ 现代浏览器 |
工作流程:
HTTP:
- 客户端发起请求
- 服务器响应
- 关闭连接(或Keep-Alive)
WebSocket:
- HTTP握手(协议升级)
- 升级为WebSocket协议
- 持久连接,双向通信
- 任意一方可主动发送消息
适用场景:
- WebSocket:聊天、游戏、协同编辑
- HTTP:普通API、文件下载
题目:长轮询是什么?
答案:
长轮询(Long Polling):
原理:
- 客户端发起请求
- 服务器不立即返回,挂起请求
- 有新数据或超时 → 返回
- 客户端收到响应后,立即发起下一次请求
优缺点:
- ✅ 减少无效请求
- ✅ 实时性好
- ❌ 服务器压力大(挂起大量连接)
- ❌ 占用线程(每个连接一个线程)
vs 短轮询:
- 短轮询:每秒请求一次,大部分返回空
- 长轮询:有消息才返回,减少无效请求
vs WebSocket:
- 长轮询:仍然是HTTP,有HTTP头开销
- WebSocket:升级为专用协议,开销小
🎉 结束语
一周后,哈吉米把聊天室改成了WebSocket。
哈吉米:“用WebSocket后,1000个用户在线,服务器CPU才用20%!”
南北绿豆:“对,WebSocket是实时通信的最佳方案,双向、低延迟、低开销。”
阿西噶阿西:“记住:聊天、游戏用WebSocket,单向推送用SSE,简单场景用HTTP。”
哈吉米:“还有长轮询虽然比短轮询好,但仍然不如WebSocket。”
南北绿豆:“对,理解了这4种方案的区别,就知道如何选型了!”
记忆口诀:
短轮询频繁请求浪费资源大
长轮询挂起连接等新消息
SSE服务器推送单向通信
WebSocket双向全双工最优选
HTTP请求响应,WebSocket持久连接







