我就不相信把面筋吃下来我会上场的时候啥都不知道。
Q:能不能从头开始讲细一点连接协议、连接线程池,这是由什么问题引申出来的?
单机单用户的流程:等待连接、接收数据、处理业务、返回结果、关闭连接
一个服务器,第二个客户必须等第一个完全处理完,走完一个流程。但是当接收数据的时候,CPU是空闲的。所以有了同时处理多个客户端的诉求:创建一个新线程去处理它,可以让多个客户端同时处理通信消息。但是线程带来的问题:每个线程占用1MB栈内存,那么创建和销毁的成本太高了。同时线程切换开销巨大。且需要有资源竞争。
C10K问题是指服务器如何支持10,000个并发连接(concurrent 10,000 connections)。这个问题的提出是为了应对互联网用户数量爆炸性增长带来的服务器性能挑战。
第二次进化:一个线程管理所有的连接------Reactor模式的核心:一个线程轮询所有连接,哪个连接有事件就处理哪个,没有事件的连接不占用线程。
结果接着带来新的问题:IO线程不能阻塞。因此提供方案:分离IO线程和业务线程。这里就引入了连接线程池的含义:Boss/IO线程池:处理网络收发,数量少;Worker/业务线程池:处理业务逻辑,数量可配置。
连接协议的根源:TCP的流式特性,没有消息边界,因此可能切分消息不得当,比如HelloWorld可能切成Hell ow orld。这个叫粘包、半包问题。解决方案是定义应用层协议。
常见的连接协议设计方式:定长消息 特殊分隔符 长度字段。
连接协议的本质:在TCP流式字节流之上,定义消息的边界和格式,让收发双方能正确解析。
A:"这个问题要从服务端并发处理的演进说起。
最开始,我们用的是阻塞IO + 一连接一线程模型。但随着连接数增长到成千上万,线程开销太大------1万连接需要1万线程,内存占用10GB+,CPU都在做上下文切换。这就是著名的C10K问题。
为了解决这个问题,业界提出了Reactor模式,用少量线程通过非阻塞IO + 事件驱动来管理海量连接。Netty、Nginx、Redis都是这个思路。
但Reactor模式下,IO线程如果执行业务逻辑(查数据库、调接口),会阻塞事件循环,导致其他连接饿死。所以必须把IO和业务分离:IO线程只负责收发数据,业务逻辑丢给专门的业务线程池。这就形成了我们常说的连接线程池模型。
至于连接协议,是因为TCP是流式协议,没有消息边界。如果不用协议界定消息,接收方无法区分'Hello'和'World'是一个消息还是两个。所以必须在TCP之上定义应用层协议------比如用长度字段、分隔符、或固定长度------来编码和解码消息。
所以这两个概念本质上是在解决同一个核心问题:如何高效、正确地处理成千上万的并发网络连接。"
Q:模块拆解和模块之间的通信是怎么做的?
一、模块间怎么通信?------ 三大模式
模式1:同步通信(RPC / HTTP)
工作原理:A 调用 B,A 一直等 B 返回结果。
text
订单服务 ──HTTP/RPC──▶ 库存服务
│ │
│ ◀──返回结果── │
▼
继续执行
技术选型:
技术 适用场景 特点
HTTP/REST 对外 API、简单场景 简单、通用、性能一般
gRPC 内部服务、高性能场景 高性能、强类型、多语言
Dubbo Java 生态内部 服务治理完善
Feign Spring Cloud 生态 声明式、易用
优点:实时、直观、容易理解
缺点:耦合强、链路长、一个慢全链慢
模式2:异步通信(消息队列)
工作原理:A 发消息到 MQ 就返回,B 慢慢消费。
text
订单服务 ──消息──▶ 消息队列 ──推送──▶ 库存服务
│ │
│ 立即返回 │ 处理
▼ ▼
继续执行 扣减库存
技术选型:
技术 特点 适用场景
Kafka 高吞吐、持久化 日志、大数据、流处理
RocketMQ 事务消息、可靠 金融、订单(阿里)
RabbitMQ 功能丰富、易用 一般业务场景
Pulsar 存储计算分离 云原生场景
优点:解耦、削峰、异步
缺点:复杂度高、延迟增加、数据一致性问题
模式3:事件驱动(Event-Driven)
工作原理:服务发布"事件",感兴趣的服务订阅。
text
┌─────────────┐
│ 事件总线 │
└─────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 积分服务 │ │ 物流服务 │ │ 报表服务 │
│ 订阅订单事件 │ │ 订阅订单事件 │ │ 订阅订单事件 │
└─────────────┘ └─────────────┘ └─────────────┘
与消息队列的区别:事件驱动是"发布-订阅"模式,事件源不关心谁来消费。
三种模式对比
维度 同步 RPC 消息队列 事件驱动
实时性 高 中 中
解耦程度 低 高 最高
复杂度 低 中 高
数据一致性 容易(事务) 难(最终一致) 难(最终一致)
典型场景 查询、实时操作 削峰、异步处理 跨服务联动
二、通信协议细节(面试深挖点)
HTTP 通信的完整流程
订单服务 ──────────────────────────────────▶ 库存服务
│
├─ 1. DNS 解析(域名→IP)
├─ 2. TCP 三次握手
├─ 3. TLS 握手(HTTPS)
├─ 4. 发送 HTTP 请求
│ POST /stock/deduct
│ Content-Type: application/json
│ {"skuId": 123, "count": 1}
├─ 5. 服务端处理
├─ 6. 返回 HTTP 响应(200 OK)
└─ 7. TCP 四次挥手
性能问题:每个请求都要握手、挥手 → 慢!
优化方案:连接池 + Keep-Alive
gRPC 的优势
基于 HTTP/2:多路复用、头部压缩、服务端推送
Protobuf 序列化:比 JSON 小 3-10 倍,快 5-10 倍
四种调用模式:一元、服务端流、客户端流、双向流
消息队列的关键配置
注意:消费者必须幂等(同一个消息处理多次,结果一致),因为 MQ 可能重试。
A:"模块拆解决定架构质量,模块通信决定系统性能。我们分两步来看。
第一步,模块拆解。我们主要按业务边界纵向拆分,比如把订单、用户、库存、支付拆成独立服务。拆分的核心原则是高内聚低耦合------一个模块只做一类事情,模块之间尽量减少依赖。同时配合横向分层,网关做接入、业务服务做逻辑、数据层做存储。
第二步,模块通信。我们根据场景选择三种模式:
-
同步调用:用 gRPC 或 HTTP,适合实时查询。关键优化是用连接池复用 TCP 连接,避免频繁握手。
-
异步消息:用 RocketMQ 或 Kafka,适合解耦和削峰。比如订单创建后,积分、物流、报表都订阅同一个事件。
-
事件驱动:更彻底的解耦,服务只发布事件,不关心谁消费。
选型原则是:强一致性场景用同步(比如扣库存前必须查库存),弱一致性场景用异步(比如发积分慢一点没事)。
通信的细节上,我们用了连接池、长连接、protobuf 序列化来优化性能,同时要求所有消息消费者实现幂等,防止重试导致重复扣款。"
Q:为什么用websocket?
- HTTP的天然缺陷(痛点场景)
场景: 做一个股票行情、聊天室、游戏、协同文档。
HTTP方式的问题:
单向性:只有客户端能发起请求,服务端不能主动推送数据。
解决方案的代价:
短轮询:客户端每隔1秒请求一次。→ 99%的请求无数据,浪费带宽、CPU、连接数。
长轮询:请求挂起20秒,有数据才返回。→ 依然有HTTP头开销(每次几百字节),连接频繁重建。
数据对比:
HTTP短轮询(1秒间隔):1小时 = 3600次请求,3600个HTTP头。
WebSocket:1次握手,后续每帧仅2-14字节开销。
- WebSocket解决了什么
核心特性:
全双工:客户端和服务端可以随时互相发送数据。
持久连接:一次握手(HTTP Upgrade),长期保持。
极低开销:无HTTP头、无Cookie重复发送。
服务端推送:实时性从"秒级"降到"毫秒级"。
典型代码对比(伪代码):javascript
javascript
// HTTP长轮询:客户端要主动问
function poll() {
fetch('/updates').then(() => setTimeout(poll, 1000));
}
// WebSocket:服务端能主动说
ws.onmessage = (event) => {
updateUI(event.data); // 服务端主动推来数据
};
面试官深挖:为什么不用其他方案?
SSE vs WebSocket
特性 SSE(Server-Sent Events) WebSocket
方向 服务端→客户端(单向) 双向
协议 HTTP(兼容性好) ws/wss(独立协议)
断线重连 自动支持 需自己实现
二进制 不支持(只能文本) 支持
复杂度 简单 中等
回答策略:
如果只需要服务端推送(如股票行情、通知),SSE更简单,甚至不用考虑心跳重连。
如果需要双向通信(如聊天、游戏、协同编辑),WebSocket更合适。
实战坑点(面试官最爱问)
问:"WebSocket连接断了怎么办?"
答:
应用层必须实现心跳机制(每30秒发Ping/Pong)。
监听onclose和onerror,实现指数退避重连。
注意:不要重连太频繁(如每次1秒),会加重服务端负担。
问:"WebSocket和HTTP2复用TCP连接,区别在哪?"
答:
HTTP/2虽然支持服务端推送(Server Push),但推送的是资源(如CSS、JS),不是任意数据,且客户端无法主动告诉服务端"我准备好了"。
WebSocket是全双工消息通道,适合实时业务逻辑交互。
HTTP/2的多路复用是针对多个HTTP请求,WebSocket是一个长期存在的消息流。
问:"WebSocket怎么鉴权?和HTTP共用Session吗?"
答:
升级握手时是HTTP请求,可以携带Cookie或Header中的Token。
最佳实践:握手时验证Token,成功后把连接和用户ID绑定到内存Map或Redis。
注意:WebSocket连接建立后,就脱离了HTTP上下文,Session可能过期。建议无状态鉴权(每次消息都带Token,或握手时发放临时连接ID)。
什么时候不该用WebSocket?(展示你的判断力)
面试官陷阱: "所有实时场景都用WebSocket?"
正确回答:
❌ 普通API请求:REST + HTTP/2足够了,WebSocket增加复杂度。
❌ 低频数据更新(如每分钟刷新一次):轮询或SSE更简单。
❌ 需要CDN缓存:WebSocket无法走CDN(连接是持久的)。
❌ 只做日志上报:用UDP或HTTP异步批量上报即可。
技术实现细节(加分项)
WebSocket协议帧结构
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+-------------------------------+-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+---------------------------------------------------------------+
关键点:
FIN位:是否最后一帧(支持分片传输大消息)
Opcode:文本、二进制、Ping/Pong、Close
Masking:客户端→服务端必须掩码(防止缓存投毒攻击)
Nginx配置WebSocket
nginx
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 60s; # 空闲超时,要和心跳匹配
}
总结:面试回答模板
当被问到"为什么用WebSocket"时,按这个结构回答:
场景铺垫:"我们当时做的是XXX(实时行情/聊天/协同),需要服务端主动推送。"
对比其他方案:"HTTP轮询开销大、延迟高,长轮询依然有头开销,SSE只支持单向。"WebSocket优势:"全双工、低延迟、低开销,一次握手持续通信。"
补充坑点:"但我们特别注意心跳、重连、鉴权问题,也评估过并非所有场景都适合。"
顺便秀技术:"底层基于Netty实现,支持百万级连接,用了自定义协议和心跳检测。"
一句话总结:"WebSocket解决了HTTP无法服务端主动推送、且每次请求头开销大的问题,适用于低延迟、高频率的双向实时通信场景。"