为什么有HTTP还要有WebSocket?

摘要:从一次"用HTTP轮询实现聊天室导致服务器崩溃"的架构失败出发,深度剖析HTTP在实时通信场景的致命缺陷。通过短轮询的资源浪费、长轮询的线程占用、以及WebSocket全双工通信的优势对比,揭秘为什么HTTP的请求-响应模式无法满足实时推送、为什么WebSocket能用1个连接替代1000次HTTP请求、以及协议升级的握手细节。配合性能压测数据和资源占用对比,给出聊天、游戏、协同编辑等实时场景的最佳方案。


💥 翻车现场

周一下午,哈吉米上线了一个在线聊天室。

技术方案:"用HTTP短轮询,前端每秒请求一次新消息。"

// 前端代码
setInterval(() => {
    axios.get('/api/message/new').then(resp => {
        if (resp.data.length > 0) {
            showMessages(resp.data);
        }
    });
}, 1000);  // 每秒1次

上线第一天

在线用户:1000人

服务器压力:
- QPS:1000(每秒1000个请求)
- 数据库QPS:1000(每次都查数据库)
- CPU:40%
- 内存:2GB

运行正常 ✅

上线第三天(活动推广):

在线用户:10000人

服务器压力:
- QPS:10000(每秒1万个请求)
- 数据库QPS:10000(数据库扛不住)
- CPU:100%
- 内存:8GB
- 响应时间:从50ms飙升到5秒

问题:
- 服务器崩溃
- 数据库连接池耗尽
- 用户大量掉线

哈吉米:"卧槽,1万人在线就崩了?"

紧急扩容到10台服务器后,勉强支撑。

技术总监:"这个架构有问题!轮询太浪费资源了,改成WebSocket!"
哈吉米:"WebSocket和HTTP有啥区别?"

南北绿豆和阿西噶阿西来了。

南北绿豆:"HTTP是请求-响应模式,无法实时推送,只能轮询。"
阿西噶阿西:"WebSocket是全双工通信,天生为实时推送设计。"
哈吉米:"???"
南北绿豆:"来,我给你对比HTTP和WebSocket的区别。"


🤔 HTTP的致命缺陷:无法服务器主动推送

HTTP的请求-响应模式

阿西噶阿西在白板上画了一个图。

HTTP通信模式:

客户端                    服务器
  |  1. 请求(我要数据)      |
  |------------------------>|
  |                         |
  |  2. 响应(给你数据)      |
  |<------------------------|
  |                         |

特点:
- 必须客户端发起请求
- 服务器被动响应
- 服务器不能主动推送

问题:
如果服务器有新消息,怎么通知客户端?
→ 无法主动通知 ❌
→ 只能客户端轮询

短轮询的资源浪费

场景:1万人在线聊天

每秒请求:
1万个用户 × 1次/秒 = 1万次请求

实际有新消息的比例:
假设每秒只有100个用户收到新消息(1%)

资源浪费:
- 无效请求:9900次(99%)
- 数据库查询:1万次(大部分查询结果为空)
- HTTP连接:1万次建立/关闭(短连接)

服务器资源:
- 线程池:200个线程
- 每个请求占用1个线程50ms
- 并发:1万次/秒 × 0.05秒 = 500个并发线程
- 线程池爆满,请求排队 ❌

资源消耗图

时间线(1秒内):

T0: 10000个请求进来
    → 200个线程处理
    → 9800个请求排队

T0.05: 前200个请求处理完
    → 继续处理后200个请求
    → 9600个排队

T0.50: 处理完10000个请求
    → 其中9900个返回空(无新消息)

T1.0: 下一波10000个请求进来
    → 循环...

问题:
- 99%的请求是无效的
- 服务器忙于处理无效请求
- 数据库压力大

南北绿豆:"看到了吗?短轮询99%的资源都浪费了!"


🤔 WebSocket的优势

WebSocket的通信模式

WebSocket通信模式:

客户端                    服务器
  |  1. 握手(升级协议)      |
  |------------------------>|
  |  2. 握手成功             |
  |<------------------------|
  |                         |
  |  3. 保持连接             |
  |========================>|  ← 持久连接
  |                         |
  |  4. 客户端消息           |
  |------------------------>|
  |                         |
  |  5. 服务器主动推送       |
  |<------------------------|  ← 服务器主动推送
  |                         |
  |  6. 客户端消息           |
  |------------------------>|
  |                         |
  |  7. 服务器推送           |
  |<------------------------|
  |                         |

特点:
- 全双工(双向同时通信)
- 持久连接(不频繁建立/关闭)
- 服务器可主动推送(核心优势)

资源占用对比

短轮询(HTTP)

1万用户在线:
- 每秒请求:1万次
- 建立连接:1万次/秒(短连接)
- 或占用连接:1万个(Keep-Alive)
- 数据库查询:1万次/秒
- 无效请求:99%

服务器资源:
- 线程:200个(处理请求)
- CPU:100%(处理大量请求)
- 内存:4GB(请求对象、连接)
- 数据库连接:200个(连接池满)

WebSocket

1万用户在线:
- 建立连接:1万个(长连接,只建立一次)
- 每秒请求:100次(只有真正的消息)
- 数据库查询:0次(消息从内存推送)

服务器资源:
- 线程:20个(NIO,一个线程管理多个连接)
- CPU:20%(只处理真实消息)
- 内存:800MB(连接对象)
- 数据库连接:10个(几乎不查库)

性能对比(1万用户在线)

指标HTTP短轮询WebSocket提升
每秒请求数10000100(只有真实消息)100倍
无效请求比例99%0%-
线程数2002010倍
CPU使用率100%20%5倍
内存占用4GB800MB5倍
数据库QPS100000-

哈吉米:"卧槽,WebSocket的资源占用是HTTP的1/5?"

南北绿豆:"对!WebSocket才是实时通信的正确方案。"


🎯 WebSocket的实现原理

协议升级握手

HTTP请求(升级为WebSocket):

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket              ← 关键:请求升级
Connection: Upgrade             ← 关键:升级连接
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==  ← 随机密钥
Sec-WebSocket-Version: 13

服务器响应:

HTTP/1.1 101 Switching Protocols  ← 状态码101:协议切换
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=  ← 根据Key计算

握手成功后:
连接从HTTP协议切换到WebSocket协议
后续通信不再是HTTP请求/响应
而是WebSocket帧(Frame)

WebSocket帧格式

WebSocket帧(二进制格式):

┌──────────────────────────────┐
│ FIN(1bit) + Opcode(4bit)      │ ← 帧类型(文本/二进制/关闭)
├──────────────────────────────┤
│ Mask(1bit) + Payload Len(7bit)│ ← 数据长度
├──────────────────────────────┤
│ Masking Key(4字节)            │ ← 掩码(客户端 → 服务器需要)
├──────────────────────────────┤
│ Payload Data                 │ ← 实际数据
└──────────────────────────────┘

开销:
最小2字节(无掩码、数据小于126字节)

对比HTTP:
HTTP头:250字节
WebSocket帧头:2-6字节

开销减少:98%

消息推送对比

HTTP轮询

1分钟内收到1条消息:

请求次数:60次(每秒1次)
数据传输:
- 请求:60 × 250字节 = 15KB(HTTP头)
- 响应(无消息):59 × 200字节 = 11.8KB
- 响应(有消息):1 × 250字节 = 0.25KB
- 总计:27KB

有效数据:50字节(消息内容)
开销:27KB / 50字节 = 540倍

WebSocket

1分钟内收到1条消息:

连接次数:1次(握手,后续保持连接)
数据传输:
- 握手:HTTP头约400字节(一次性)
- 消息推送:2字节(帧头) + 50字节(消息) = 52字节
- 总计:452字节

有效数据:50字节
开销:452字节 / 50字节 = 9倍

对比HTTP:
27KB vs 452字节

流量节省:98.3%

🎯 为什么HTTP做不到服务器推送?

HTTP的单向性

HTTP的设计

HTTP/1.1的限制:
1. 必须客户端发起请求
2. 服务器被动响应
3. 一问一答(半双工)

原因:
HTTP是为浏览器浏览网页设计的:
1. 用户点击链接 → 浏览器发请求
2. 服务器返回HTML → 浏览器渲染
3. 交互模式:用户主动,服务器被动

问题:
- 服务器有新数据,无法通知浏览器
- 只能浏览器轮询

尝试过的方案

方案1:短轮询

问题:
- 99%请求无效
- 资源浪费
- 延迟(最多1秒)

方案2:长轮询

问题:
- 服务器挂起大量连接
- 占用线程(1万连接 = 1万线程 = 10GB内存)
- 仍然是HTTP(有HTTP头开销)

方案3:SSE(Server-Sent Events)

优点:
- 服务器可以推送

缺点:
- 单向(只能服务器 → 客户端)
- 客户端不能发消息(还要额外的HTTP接口)

结论

HTTP的本质限制:
- 请求-响应模式(半双工)
- 无法服务器主动推送(单向)

需要:
- 全双工通信(双向同时通信)
- 服务器主动推送
- 低开销(减少HTTP头)

解决方案:
WebSocket(为实时通信设计的协议)

南北绿豆:"HTTP不是不好,而是不是为实时通信设计的。"


🎯 WebSocket vs HTTP的本质区别

通信模型

HTTP:
单工/半双工(一问一答)

时间轴:
T1: 客户端发请求 → 服务器处理 → 返回响应
T2: 客户端发请求 → 服务器处理 → 返回响应
T3: 客户端发请求 → 服务器处理 → 返回响应

特点:
- 同一时刻,只有一个方向在通信
- 必须等响应回来,才能发下一个请求


WebSocket:
全双工(双向同时通信)

时间轴:
T1: 客户端发消息 → 服务器接收
    同时:服务器推送 → 客户端接收
    
T2: 客户端发消息 → 服务器接收
    同时:服务器推送 → 客户端接收

特点:
- 双向同时通信
- 互不干扰

连接模式

HTTP:
短连接(或Keep-Alive有超时)

流程:
建立连接 → 请求 → 响应 → 关闭连接
(或Keep-Alive保持60秒,超时关闭)

问题:
- 频繁建立/关闭连接
- Keep-Alive有超时限制


WebSocket:
长连接(持久保持)

流程:
握手 → 连接建立 → 保持通信 → 主动关闭

特点:
- 一次握手,永久连接(除非主动关闭)
- 心跳保活
- 不超时

协议开销

发送1000条消息(每条50字节):

HTTP:
每条消息:
- HTTP头:250字节
- 数据:50字节
- 总计:300字节

1000条消息:
300字节 × 1000 = 300KB


WebSocket:
握手:
- HTTP头:400字节(一次性)

每条消息:
- 帧头:2字节
- 数据:50字节
- 总计:52字节

1000条消息:
400字节(握手) + 52字节 × 1000 = 52.4KB


对比:
HTTP:300KB
WebSocket:52.4KB

流量节省:(300 - 52.4) / 300 = 82.5%

🎯 改用WebSocket后的性能

代码实现

服务端

@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
    
    // 存储所有在线用户的WebSocket连接
    private static final Map<Long, WebSocketSession> ONLINE_USERS = new ConcurrentHashMap<>();
    
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        Long userId = getUserId(session);
        ONLINE_USERS.put(userId, session);
        
        log.info("用户{}上线,当前在线:{}", userId, ONLINE_USERS.size());
    }
    
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) {
        // 接收客户端消息
        ChatMessage chatMsg = JSON.parseObject(message.getPayload(), ChatMessage.class);
        
        // 保存消息到数据库
        messageService.save(chatMsg);
        
        // 实时推送给目标用户
        WebSocketSession targetSession = ONLINE_USERS.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);
        ONLINE_USERS.remove(userId);
        
        log.info("用户{}下线", userId);
    }
}

前端

// WebSocket客户端
const ws = new WebSocket('ws://localhost:8080/chat');

ws.onopen = function() {
    console.log('连接成功');
};

// 接收服务器推送
ws.onmessage = function(event) {
    const message = JSON.parse(event.data);
    showMessage(message);  // 立即显示
};

// 发送消息
function sendMessage(content) {
    ws.send(JSON.stringify({
        toUserId: 10087,
        content: content
    }));
}

性能对比(1万用户在线)

HTTP短轮询

服务器资源:
- 线程数:200
- CPU:100%
- 内存:4GB
- QPS:10000
- 数据库QPS:10000

能支撑:1万用户(已接近极限)

WebSocket

服务器资源:
- 线程数:20(NIO,一个线程管理多个连接)
- CPU:15%
- 内存:800MB
- QPS:100(只有真实消息)
- 数据库QPS:100(只有消息保存)

能支撑:10万用户 ✅

性能提升:
- CPU:100% → 15%(提升6.7倍)
- 内存:4GB → 800MB(减少80%)
- QPS:10000 → 100(减少99%)
- 支撑人数:1万 → 10万(提升10倍)

🎯 什么时候必须用WebSocket?

场景对比

场景推荐方案原因
在线聊天WebSocket实时双向通信
多人游戏WebSocket低延迟、高频推送
协同编辑WebSocket实时同步
股票行情WebSocket或SSE服务器推送
实时通知WebSocket或SSE服务器推送
普通APIHTTP请求-响应即可
文件下载HTTP单向传输
RESTful APIHTTP通用性好

不适合WebSocket的场景

场景1:低频交互

场景:
用户1小时才发1条消息

分析:
- WebSocket:保持1小时连接,占用资源
- HTTP:发送时才请求,不占用资源

推荐:
低频场景用HTTP更省资源

场景2:对外开放API

场景:
第三方接入你的服务

问题:
- WebSocket需要保持连接
- 第三方语言可能不支持WebSocket
- HTTP更通用

推荐:
对外API用HTTP(RESTful)

🎓 面试标准答案

题目:为什么有HTTP还要有WebSocket?

答案

HTTP的局限(实时通信场景):

  1. 无法服务器主动推送

    • 请求-响应模式
    • 必须客户端发起
    • 服务器被动响应
  2. 只能轮询(资源浪费)

    • 短轮询:99%请求无效
    • 长轮询:占用大量线程
  3. 半双工(效率低)

    • 一问一答
    • 不能双向同时通信
  4. 协议开销大

    • 每次请求都有HTTP头(250字节)
    • 对于小消息,开销是数据的10倍

WebSocket的优势

  1. 全双工通信

    • 双向同时通信
    • 客户端和服务器都能主动发送
  2. 服务器主动推送

    • 核心优势
    • 有新数据立即推送
  3. 持久连接

    • 一次握手,长期保持
    • 不频繁建立/关闭
  4. 低开销

    • 握手后只有2-6字节帧头
    • 流量节省80%+

性能对比

  • CPU:降低80%
  • 内存:减少80%
  • QPS:减少99%
  • 支撑人数:提升10倍

适用场景

  • WebSocket:聊天、游戏、协同编辑
  • HTTP:普通API、文件下载

结论

  • HTTP和WebSocket不是竞争关系
  • 是互补关系
  • HTTP适合请求-响应,WebSocket适合实时推送

🎉 结束语

一周后,哈吉米把聊天室改成了WebSocket。

哈吉米:"改成WebSocket后,1万用户在线,服务器CPU从100%降到15%,还能再支撑10万人!"

南北绿豆:"对,WebSocket天生为实时通信设计,HTTP是为浏览网页设计的。"

阿西噶阿西:"记住:实时推送用WebSocket,请求-响应用HTTP,各司其职。"

哈吉米:"还有WebSocket的协议开销只有HTTP的2%,流量节省巨大。"

南北绿豆:"对,理解了HTTP和WebSocket的区别,就知道什么场景用什么协议了!"


记忆口诀

HTTP请求响应半双工,服务器无法主动推
轮询浪费九成九资源,线程占用内存高
WebSocket全双工通信,服务器主动推送强
持久连接低开销,实时场景是首选
HTTP适合普通API,WebSocket适合实时通信