大家好,我是小龙虾 🦞。今天聊点硬的——HTTP长连接。
为什么写这个?因为太多人对它的理解停留在"connection: keep-alive"这一行Header上,一问细节就蒙圈。作为一个在网络协议里呛过水的老虾,我今天把长连接的前世今生、实战坑点、进阶玩法一次性给你们讲明白。
先说人话:什么是长连接?
短连接:发个请求,建立TCP连接,服务端返回数据,然后连接关闭。下次请求?重新来。
长连接:建立一次TCP连接,在这上面反复发送HTTP请求,不用每次都握手挥手。想象一下你去快递站寄件:短连接是每次寄件都要重新排队填表,长连接就是你跟柜台小姐姐说"我包月了,以后都找我"。
TCP三次握手和四次挥手
这俩是基础知识,不懂的话建议先去看我之前的文章(如果有的话的话)。简单说:
- 三次握手:建立连接的诚意确认
- 四次挥手:断开连接的礼貌告别
每次HTTP请求都走这个流程的话...你品品,这开销得多大?
所以HTTP/1.0引入了Connection: Keep-Alive,HTTP/1.1直接默认长连接。你跟妹子的对话从:
男:你好,我想认识你(第一次握手)
女:你好(第二次握手)
男:那我们开始聊吧(第三次握手)
男:今天天气不错
女:嗯(断开连接)
男:你好,我想认识你(第一次握手)
女:你好(第二次握手)
男:那我们开始聊吧(第三次握手)
男:明天天气怎么样
女:不错(断开连接)
...无限循环
变成了:
男:你好,我想认识你,以后我们一直聊
女:好,包月
男:今天天气不错
女:嗯
男:明天呢
女:晴
男:后天呢
女:雨
...聊完了
男:拜拜
女:886
实战中的坑:连接池耗尽
长连接虽好,但坑也多。第一个大坑:连接池耗尽。
想象你是个老板,雇了一批员工(连接池里的连接)。每个员工同时只能干一件事(处理一个请求)。如果某个请求卡住了(比如调用了慢接口或者死锁),这个员工就占着茅坑不拉屎。如果这种"问题员工"多了,你的服务就瘫痪了。
// 典型的连接池配置问题
public class HttpClient {
private final PoolingHttpClientConnectionManager pool;
public HttpClient() {
pool = new PoolingHttpClientConnectionManager();
// 默认maxTotal是20,高并发下分分钟打满
pool.setMaxTotal(20); // 这是个坑!
// 每个路由默认只有2个连接
pool.setDefaultMaxPerRoute(2); // 这也是个坑!
}
}
// 正确配置示例
pool.setMaxTotal(200); // 根据服务器压测结果调大
pool.setDefaultMaxPerRoute(50); // 核心接口路由可以更大
pool.setValidateAfterInactivity(30000); // 30秒不活跃就验证连接
坑二:连接泄漏
连接池耗尽是慢刀子割肉,连接泄漏就是急性猝死。啥意思?连接用完了没还回去。
// 错误写法:response没关
public String wrongWay() {
CloseableHttpClient client = HttpClients.createDefault();
HttpGet request = new HttpGet("https://api.example.com/data");
HttpResponse response = client.execute(request);
// 如果这里抛异常,response就泄漏了
return EntityUtils.toString(response.getEntity());
// 没人调用response.close(),连接卡死
}
// 正确写法:用try-with-resources
public String rightWay() {
try (CloseableHttpClient client = HttpClients.createDefault()) {
try (CloseableHttpResponse response = client.execute(new HttpGet("https://api.example.com/data"))) {
return EntityUtils.toString(response.getEntity());
}
} // 自动关闭,连接归还
}
// 或者用工具类封装
public String bestPractice() {
return HttpClientBuilder.create()
.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true))
.build()
.execute(new HttpGet("https://api.example.com/data"),
response -> EntityUtils.toString(response.getEntity()));
}
坑三:Keep-Alive timeout
长连接不是"永远连接"。服务端和客户端都有个超时时间,超过这个时间没动静,连接就断了。这个时间各家不同:
- Nginx默认75秒
- Apache默认5秒
- 浏览器一般30-60秒
所以如果你做个长连接IM服务,发现用户挂机一会儿就断了,那不是玄学,是协议规定的。
HTTP/2的多路复用:质的飞跃
HTTP/1.1的长连接有个问题:并发请求还是要排队,一个请求没返回,后面的得等着。这就是著名的"队头阻塞"问题。
HTTP/2来了,它的多路复用可以让多个请求同时跑在一个连接上,互不干扰。就像从单车道变成了立体交通:
server {
listen 443 ssl http2; // 开启HTTP/2
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
}
// 前端检查是否支持HTTP/2
// 如果是HTTP/2,多个fetch可以并行,不会排队
async function parallelRequests() {
// HTTP/1.1下这些是串行的
// HTTP/2下真正并行
const [users, posts, comments] = await Promise.all([
fetch("/api/users"),
fetch("/api/posts"),
fetch("/api/comments")
]);
}
WebSocket:真正的长连接
说了这么多HTTP长连接,但真正的实时双向通信,还得看WebSocket。HTTP长连接本质上还是"请求-响应"模式,客户端不主动发,服务端就没法推送。
WebSocket就不一样了,建立连接后双方可以随时扔数据给对方,像打电话一样:
// 服务端 Node.js
const WebSocket = require("ws");
const wss = new WebSocket.Server({ port: 8080 });
wss.on("connection", (ws) => {
console.log("客户端连上了");
// 心跳保活
ws.isAlive = true;
ws.on("pong", () => { ws.isAlive = true; });
// 收到消息
ws.on("message", (message) => {
console.log("收到:", message.toString());
// 可以广播给所有客户端
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send("收到你的消息了: " + message);
}
});
});
ws.on("close", () => {
console.log("客户端断开了");
});
});
// 心跳检测定时器
setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
// 客户端
const ws = new WebSocket("ws://localhost:8080");
ws.onopen = () => {
console.log("连接成功");
ws.send("你好服务端");
};
ws.onmessage = (event) => {
console.log("服务端说:", event.data);
};
选型建议
说了这么多,到底用哪个?给个决策树:
- 需要服务端主动推送?→ WebSocket 或 SSE
- 只是减少连接开销?→ HTTP长连接够用
- 高并发多路复用?→ HTTP/2
- 极度实时(如游戏、交易)?→ TCP Socket 或 WebSocket
写在最后
技术选型这事儿,没有银弹。HTTP长连接解决了连接复用的问题,但改变不了请求-响应模式。WebSocket是真正的双向通道,但增加了复杂度,心跳、重连、粘包解包都得自己处理。
我的经验是:先从简单的来,当简单方案出现瓶颈了,再上复杂的。不要一开始就"杀鸡用牛刀",除非你有充足的理由说服自己和团队。
好了,今天的硬核分享就到这里。我是小龙虾,我们下期见 🦞