前言
最近在做通信模块时,遇到一个棘手的问题:怎么判断网络到底是断了还是对方没发数据?更进一步,怎么区分对方是正常关闭连接还是网线被拔了?
这个问题看似简单,实际上涉及TCP协议的很多细节。折腾了几天,把这块彻底搞清楚了,记录一下。
一、问题背景
先明确我们要解决的几个问题:
- 对方静默(没发数据)vs 网络断开------怎么区分?
- 主动断开(正常close)vs 意外断开(拔网线、断电)------怎么区分?
- 如何设计一个可靠的断线检测机制?
要搞清楚这些,得先理解TCP连接的本质。
二、TCP连接的本质------没你想的那么"实时"
很多人以为TCP是"实时连接"的,其实不是。TCP连接本质上是两端维护的一组状态信息,中间的网络设备(路由器、交换机)根本不知道有这个连接存在。
这意味着什么?
正常情况:
[主机A] <---> [路由器] <---> [主机B]
│ │
└── 双方都记录连接状态 ───────┘
拔掉主机B的网线:
[主机A] <---> [路由器] X [主机B]
│ │
└── A不知道B已经断了!────────┘
关键点:如果双方都不发数据,即使网线断了,A也感知不到!这就是问题的根源。
三、recv()返回值的真相
先看最基本的检测手段------recv()函数的返回值:
cpp
ssize_t ret = recv(sockfd, buf, sizeof(buf), 0);
返回值含义:
| 返回值 | 含义 | 说明 |
|---|---|---|
| > 0 | 收到数据 | 正常情况 |
| = 0 | 对方关闭连接 | 对方调用了close()或shutdown() |
| -1 | 出错 | 需要看errno判断具体原因 |
注意 :recv()=0只能检测主动关闭,检测不了拔网线这种情况!
因为正常关闭时会发FIN包:
主动关闭流程:
A B
│ │
│ ←─── FIN ───────────────── │ B调用close()
│ ──── ACK ─────────────────→ │
│ │
此时A端recv()返回0
但如果是拔网线,FIN包根本发不出来,A端啥也收不到。
四、四种断线检测机制
4.1 机制一:TCP Keepalive(系统级心跳)
Linux内核自带的保活机制,不用自己写心跳代码。
原理:连接空闲一段时间后,内核自动发探测包,对方必须回应ACK。
cpp
#include <sys/socket.h>
#include <netinet/tcp.h>
int enable_keepalive(int sockfd)
{
int keepalive = 1; // 开启keepalive
int keepidle = 10; // 空闲10秒后开始探测
int keepinterval = 3; // 探测间隔3秒
int keepcount = 3; // 探测3次无响应则认为断开
if (setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE,
&keepalive, sizeof(keepalive)) < 0) {
perror("SO_KEEPALIVE");
return -1;
}
if (setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE,
&keepidle, sizeof(keepidle)) < 0) {
perror("TCP_KEEPIDLE");
return -1;
}
if (setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPINTVL,
&keepinterval, sizeof(keepinterval)) < 0) {
perror("TCP_KEEPINTVL");
return -1;
}
if (setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPCNT,
&keepcount, sizeof(keepcount)) < 0) {
perror("TCP_KEEPCNT");
return -1;
}
return 0;
}
探测流程:
空闲10秒后:
A ─── Keepalive探测包 ───→ B
A ←─── ACK ────────────── B (正常)
网线断开情况:
A ─── Keepalive探测包 ───→ X (无响应)
等待3秒...
A ─── Keepalive探测包 ───→ X (无响应)
等待3秒...
A ─── Keepalive探测包 ───→ X (无响应)
探测3次失败,内核标记连接断开,recv()返回-1,errno=ETIMEDOUT
优点:内核实现,开销小,不占应用层带宽
缺点:
- 检测时间较长(上面配置要10+3×3=19秒)
- 某些NAT设备会拦截或修改keepalive包
- 无法区分"对方静默"和"网络断开"(还是得等探测超时)
4.2 机制二:应用层心跳(推荐)
自己实现心跳包,更灵活可控。
cpp
#include <sys/time.h>
#include <thread>
#include <atomic>
#include <chrono>
// 心跳包定义
#pragma pack(push, 1)
struct HeartbeatPacket {
uint8_t type; // 0x01=请求, 0x02=响应
uint32_t seq; // 序列号
int64_t timestamp; // 时间戳(ms)
};
#pragma pack(pop)
class HeartbeatManager {
private:
int m_sockfd;
std::atomic<bool> m_running{false};
std::atomic<int64_t> m_lastRecvTime{0}; // 最后收到数据的时间
std::atomic<int64_t> m_lastHeartbeatAck{0}; // 最后收到心跳响应的时间
std::atomic<uint32_t> m_seq{0};
int m_heartbeatInterval = 3000; // 心跳间隔(ms)
int m_timeoutThreshold = 10000; // 超时阈值(ms)
std::thread m_sendThread;
public:
HeartbeatManager(int sockfd) : m_sockfd(sockfd) {
m_lastRecvTime = getCurrentTimeMs();
m_lastHeartbeatAck = getCurrentTimeMs();
}
static int64_t getCurrentTimeMs() {
auto now = std::chrono::system_clock::now();
auto duration = now.time_since_epoch();
return std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
}
// 发送心跳请求
int sendHeartbeat() {
HeartbeatPacket pkt;
pkt.type = 0x01;
pkt.seq = ++m_seq;
pkt.timestamp = getCurrentTimeMs();
ssize_t ret = send(m_sockfd, &pkt, sizeof(pkt), MSG_NOSIGNAL);
if (ret != sizeof(pkt)) {
return -1;
}
return 0;
}
// 处理收到的心跳包
int handleHeartbeat(const HeartbeatPacket& pkt) {
if (pkt.type == 0x01) {
// 收到心跳请求,回复响应
HeartbeatPacket resp;
resp.type = 0x02;
resp.seq = pkt.seq;
resp.timestamp = getCurrentTimeMs();
send(m_sockfd, &resp, sizeof(resp), MSG_NOSIGNAL);
} else if (pkt.type == 0x02) {
// 收到心跳响应
m_lastHeartbeatAck = getCurrentTimeMs();
int64_t rtt = getCurrentTimeMs() - pkt.timestamp;
// 可以统计RTT用于网络质量评估
}
return 0;
}
// 更新最后收到数据的时间(收到任何数据都调用)
void updateRecvTime() {
m_lastRecvTime = getCurrentTimeMs();
}
// 检查连接状态
enum ConnectionState {
STATE_NORMAL, // 正常
STATE_IDLE, // 对方静默(没发数据,但心跳正常)
STATE_DISCONNECTED // 网络断开
};
ConnectionState checkState() {
int64_t now = getCurrentTimeMs();
int64_t silentTime = now - m_lastRecvTime;
int64_t heartbeatTime = now - m_lastHeartbeatAck;
// 心跳超时,网络断开
if (heartbeatTime > m_timeoutThreshold) {
return STATE_DISCONNECTED;
}
// 心跳正常但没收到业务数据,对方静默
if (silentTime > m_timeoutThreshold && heartbeatTime < m_timeoutThreshold) {
return STATE_IDLE;
}
return STATE_NORMAL;
}
// 启动心跳线程
void start() {
m_running = true;
m_sendThread = std::thread([this]() {
while (m_running) {
sendHeartbeat();
std::this_thread::sleep_for(
std::chrono::milliseconds(m_heartbeatInterval));
}
});
}
void stop() {
m_running = false;
if (m_sendThread.joinable()) {
m_sendThread.join();
}
}
};
这个方案能区分:
- 心跳正常 + 没收到业务数据 = 对方静默(STATE_IDLE)
- 心跳超时 = 网络断开(STATE_DISCONNECTED)
4.3 机制三:send()探测
往socket写数据也能检测断线,但有坑。
cpp
int probe_connection(int sockfd)
{
char probe = 0;
// 发送0字节数据
ssize_t ret = send(sockfd, &probe, 0, MSG_NOSIGNAL);
if (ret < 0) {
if (errno == EPIPE || errno == ECONNRESET) {
printf("连接已断开\n");
return -1;
}
}
return 0;
}
坑在这里:
send()成功只表示数据进了发送缓冲区,不表示对方收到了- TCP有重传机制,拔网线后第一次send可能还是成功的
- 要等到重传超时(可能几十秒到几分钟),send才会返回错误
所以send()探测检测不及时,只能作为辅助手段。
4.4 机制四:select/poll/epoll超时检测
用I/O多路复用设置超时,这是实际工程中最常用的方式。
cpp
#include <sys/epoll.h>
#include <errno.h>
class ConnectionMonitor {
private:
int m_epollfd;
int m_sockfd;
int m_timeout; // 超时时间(ms)
public:
ConnectionMonitor(int sockfd, int timeout_ms)
: m_sockfd(sockfd), m_timeout(timeout_ms)
{
m_epollfd = epoll_create1(0);
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLERR | EPOLLHUP | EPOLLRDHUP;
ev.data.fd = sockfd;
epoll_ctl(m_epollfd, EPOLL_CTL_ADD, sockfd, &ev);
}
~ConnectionMonitor() {
close(m_epollfd);
}
enum EventType {
EVENT_DATA, // 有数据可读
EVENT_TIMEOUT, // 超时(对方静默)
EVENT_CLOSED, // 对方主动关闭
EVENT_ERROR // 连接异常
};
EventType wait() {
struct epoll_event events[1];
int nfds = epoll_wait(m_epollfd, events, 1, m_timeout);
if (nfds < 0) {
return EVENT_ERROR;
}
if (nfds == 0) {
// 超时,对方没发数据
return EVENT_TIMEOUT;
}
uint32_t ev = events[0].events;
// EPOLLRDHUP: 对方关闭连接(调用close或shutdown)
if (ev & EPOLLRDHUP) {
return EVENT_CLOSED;
}
// EPOLLERR或EPOLLHUP: 连接异常
if (ev & (EPOLLERR | EPOLLHUP)) {
return EVENT_ERROR;
}
// EPOLLIN: 有数据
if (ev & EPOLLIN) {
return EVENT_DATA;
}
return EVENT_ERROR;
}
};
关键事件说明:
| 事件 | 含义 | 触发场景 |
|---|---|---|
| EPOLLIN | 有数据可读 | 正常收到数据 |
| EPOLLRDHUP | 对方关闭写端 | 对方调用close()或shutdown(WR) |
| EPOLLHUP | 连接挂起 | 本端或对端异常 |
| EPOLLERR | 错误 | 连接出错 |
注意 :EPOLLRDHUP需要在add时显式指定,否则不会触发。
五、区分主动断开和意外断开
这是最难的部分。先看两种断开的区别:
主动断开(正常关闭)
对方执行close()或程序正常退出:
A B
│ │
│ ←─── FIN ───────────────── │ B关闭
│ ──── ACK ─────────────────→ │
│ ──── FIN ─────────────────→ │ A收到后也关闭
│ ←─── ACK ───────────────── │
│ │
检测方式:recv()返回0,或触发EPOLLRDHUP
意外断开(拔网线、断电、进程崩溃)
对方没机会发FIN:
A B
│ │
│ ──── 数据/心跳 ──────→ X │ 网络中断
│ 等待ACK...超时重传 │
│ 重传多次后放弃 │
│ │
检测方式:
1. 发送超时(send失败,errno=ETIMEDOUT)
2. Keepalive探测超时
3. 应用层心跳超时
完整检测方案
cpp
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/tcp.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <thread>
#include <atomic>
#include <mutex>
#include <functional>
class TcpConnectionDetector {
public:
enum DisconnectType {
DISCONNECT_NONE, // 未断开
DISCONNECT_GRACEFUL, // 主动断开(对方正常关闭)
DISCONNECT_TIMEOUT, // 超时断开(可能是网络问题或对方静默)
DISCONNECT_RESET, // 连接重置(对方进程崩溃等)
DISCONNECT_NETWORK_ERROR // 网络错误(拔网线等)
};
using DisconnectCallback = std::function<void(DisconnectType, const std::string&)>;
private:
int m_sockfd;
int m_epollfd;
std::atomic<bool> m_running{false};
std::thread m_monitorThread;
// 心跳相关
std::atomic<int64_t> m_lastRecvTime{0};
std::atomic<int64_t> m_lastSendTime{0};
int m_heartbeatInterval = 3000; // ms
int m_heartbeatTimeout = 10000; // ms
// 回调
DisconnectCallback m_callback;
std::mutex m_mutex;
// 获取当前时间(ms)
static int64_t now() {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
// 获取socket错误
int getSocketError() {
int error = 0;
socklen_t len = sizeof(error);
getsockopt(m_sockfd, SOL_SOCKET, SO_ERROR, &error, &len);
return error;
}
// 分析断开类型
DisconnectType analyzeDisconnect(int recvRet, int err, uint32_t events) {
// recv返回0: 对方主动关闭
if (recvRet == 0) {
return DISCONNECT_GRACEFUL;
}
// EPOLLRDHUP: 对方关闭写端
if (events & EPOLLRDHUP) {
return DISCONNECT_GRACEFUL;
}
// 连接重置
if (err == ECONNRESET) {
return DISCONNECT_RESET;
}
// 超时类错误:通常是网络问题
if (err == ETIMEDOUT || err == EHOSTUNREACH || err == ENETUNREACH) {
return DISCONNECT_NETWORK_ERROR;
}
// 其他错误
if (events & (EPOLLERR | EPOLLHUP)) {
int sockErr = getSocketError();
if (sockErr == ECONNRESET) {
return DISCONNECT_RESET;
}
if (sockErr == ETIMEDOUT) {
return DISCONNECT_NETWORK_ERROR;
}
return DISCONNECT_NETWORK_ERROR;
}
return DISCONNECT_TIMEOUT;
}
// 监控线程主函数
void monitorLoop() {
struct epoll_event events[4];
char buf[1024];
while (m_running) {
// 计算下次心跳的等待时间
int64_t elapsed = now() - m_lastSendTime;
int waitTime = std::max(100, m_heartbeatInterval - (int)elapsed);
int nfds = epoll_wait(m_epollfd, events, 4, waitTime);
if (!m_running) break;
// 检查心跳超时
if (now() - m_lastRecvTime > m_heartbeatTimeout) {
notifyDisconnect(DISCONNECT_NETWORK_ERROR, "心跳超时");
break;
}
// 超时,发送心跳
if (nfds == 0) {
sendHeartbeat();
continue;
}
if (nfds < 0) {
if (errno != EINTR) {
notifyDisconnect(DISCONNECT_NETWORK_ERROR, strerror(errno));
break;
}
continue;
}
// 处理事件
for (int i = 0; i < nfds; i++) {
uint32_t ev = events[i].events;
// 先检查错误事件
if (ev & (EPOLLERR | EPOLLHUP | EPOLLRDHUP)) {
DisconnectType type = analyzeDisconnect(-1, 0, ev);
std::string reason;
if (ev & EPOLLRDHUP) {
reason = "对方关闭连接";
} else if (ev & EPOLLHUP) {
reason = "连接挂起";
} else {
reason = "连接错误: " + std::to_string(getSocketError());
}
notifyDisconnect(type, reason);
m_running = false;
break;
}
// 有数据可读
if (ev & EPOLLIN) {
ssize_t ret = recv(m_sockfd, buf, sizeof(buf), MSG_DONTWAIT);
if (ret > 0) {
m_lastRecvTime = now();
// 这里处理收到的数据,包括心跳响应
processData(buf, ret);
} else if (ret == 0) {
// 对方主动关闭
notifyDisconnect(DISCONNECT_GRACEFUL, "对方正常关闭连接");
m_running = false;
break;
} else {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
DisconnectType type = analyzeDisconnect(-1, errno, 0);
notifyDisconnect(type, strerror(errno));
m_running = false;
break;
}
}
}
}
}
}
void sendHeartbeat() {
uint8_t heartbeat[] = {0xAA, 0x55, 0x01, 0x00}; // 简单心跳包
ssize_t ret = send(m_sockfd, heartbeat, sizeof(heartbeat), MSG_NOSIGNAL);
if (ret > 0) {
m_lastSendTime = now();
} else if (ret < 0 && errno != EAGAIN) {
// 发送失败
DisconnectType type = analyzeDisconnect(-1, errno, 0);
notifyDisconnect(type, "发送失败: " + std::string(strerror(errno)));
m_running = false;
}
}
void processData(const char* data, size_t len) {
// 处理业务数据,这里简化处理
// 实际应该解析协议,识别心跳响应等
}
void notifyDisconnect(DisconnectType type, const std::string& reason) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_callback) {
m_callback(type, reason);
}
}
public:
TcpConnectionDetector(int sockfd) : m_sockfd(sockfd) {
m_lastRecvTime = now();
m_lastSendTime = now();
// 创建epoll
m_epollfd = epoll_create1(0);
// 添加socket
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLERR | EPOLLHUP | EPOLLRDHUP;
ev.data.fd = sockfd;
epoll_ctl(m_epollfd, EPOLL_CTL_ADD, sockfd, &ev);
// 设置TCP Keepalive作为兜底
int keepalive = 1;
int keepidle = 30;
int keepinterval = 5;
int keepcount = 3;
setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPINTVL, &keepinterval, sizeof(keepinterval));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPCNT, &keepcount, sizeof(keepcount));
}
~TcpConnectionDetector() {
stop();
close(m_epollfd);
}
void setCallback(DisconnectCallback cb) {
std::lock_guard<std::mutex> lock(m_mutex);
m_callback = std::move(cb);
}
void setHeartbeatInterval(int ms) { m_heartbeatInterval = ms; }
void setHeartbeatTimeout(int ms) { m_heartbeatTimeout = ms; }
void start() {
m_running = true;
m_monitorThread = std::thread(&TcpConnectionDetector::monitorLoop, this);
}
void stop() {
m_running = false;
if (m_monitorThread.joinable()) {
m_monitorThread.join();
}
}
// 获取断开类型的字符串描述
static const char* getDisconnectTypeStr(DisconnectType type) {
switch (type) {
case DISCONNECT_NONE: return "未断开";
case DISCONNECT_GRACEFUL: return "正常关闭";
case DISCONNECT_TIMEOUT: return "超时";
case DISCONNECT_RESET: return "连接重置";
case DISCONNECT_NETWORK_ERROR: return "网络错误";
default: return "未知";
}
}
};
六、使用示例
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
// 创建socket并连接(示例代码,实际使用需要错误处理)
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
inet_pton(AF_INET, "192.168.1.100", &addr.sin_addr);
if (connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("connect");
return -1;
}
printf("连接成功\n");
// 创建连接检测器
TcpConnectionDetector detector(sockfd);
// 设置断开回调
detector.setCallback([](TcpConnectionDetector::DisconnectType type,
const std::string& reason) {
printf("\n===== 连接断开 =====\n");
printf("类型: %s\n", TcpConnectionDetector::getDisconnectTypeStr(type));
printf("原因: %s\n", reason.c_str());
switch (type) {
case TcpConnectionDetector::DISCONNECT_GRACEFUL:
printf("处理建议: 对方正常关闭,可以清理资源\n");
break;
case TcpConnectionDetector::DISCONNECT_RESET:
printf("处理建议: 对方进程可能崩溃,考虑重连\n");
break;
case TcpConnectionDetector::DISCONNECT_NETWORK_ERROR:
printf("处理建议: 网络异常,检查网线/路由,尝试重连\n");
break;
case TcpConnectionDetector::DISCONNECT_TIMEOUT:
printf("处理建议: 超时,可能对方静默或网络拥塞\n");
break;
default:
break;
}
});
// 设置参数
detector.setHeartbeatInterval(3000); // 3秒发一次心跳
detector.setHeartbeatTimeout(10000); // 10秒无响应判定断开
// 启动检测
detector.start();
// 主线程可以做其他事情
printf("按Enter键退出...\n");
getchar();
detector.stop();
close(sockfd);
return 0;
}
七、各种断线场景的检测结果
实测不同断线场景的检测效果:
| 场景 | 检测方式 | 检测时间 | 返回类型 |
|---|---|---|---|
| 对方调用close() | recv()=0 / EPOLLRDHUP | 立即 | GRACEFUL |
| 对方进程被kill -9 | recv()=0 / EPOLLRDHUP | 立即 | GRACEFUL |
| 对方进程崩溃(段错误) | ECONNRESET | 立即 | RESET |
| 拔网线 | 心跳超时 | 心跳超时时间 | NETWORK_ERROR |
| 对方断电 | 心跳超时 | 心跳超时时间 | NETWORK_ERROR |
| 网络拥塞丢包 | 心跳超时(可能误判) | 心跳超时时间 | TIMEOUT |
说明:
- kill -9虽然强制杀进程,但内核还是会发FIN包,所以和正常close()一样
- 段错误等异常退出,内核会发RST包,触发ECONNRESET
- 拔网线、断电这种物理断开,只能靠心跳超时检测
八、工程实践建议
8.1 参数调优
cpp
// 对实时性要求高的场景(如遥控机器人)
detector.setHeartbeatInterval(500); // 500ms心跳
detector.setHeartbeatTimeout(2000); // 2秒超时
// 对带宽敏感的场景(如低带宽卫星链路)
detector.setHeartbeatInterval(10000); // 10秒心跳
detector.setHeartbeatTimeout(35000); // 35秒超时
8.2 重连策略
cpp
class ReconnectManager {
int m_maxRetries = 5;
int m_baseDelay = 1000; // 基础延迟1秒
public:
void reconnect(TcpConnectionDetector::DisconnectType type) {
int retries = 0;
while (retries < m_maxRetries) {
// 指数退避
int delay = m_baseDelay * (1 << retries);
delay = std::min(delay, 30000); // 最大30秒
printf("等待%d毫秒后重连...\n", delay);
std::this_thread::sleep_for(std::chrono::milliseconds(delay));
if (tryConnect()) {
printf("重连成功\n");
return;
}
retries++;
}
printf("重连失败,放弃\n");
}
bool tryConnect() {
// 实现连接逻辑
return false;
}
};
8.3 双向心跳
单向心跳有个问题:如果A→B的链路断了,但B→A正常,A会一直发心跳但收不到响应。
更好的方案是双向心跳:
cpp
// A发心跳请求,B必须回复心跳响应
// B也发心跳请求,A必须回复心跳响应
// 这样双向都能检测到断线
九、总结
检测TCP断线其实就三板斧:
- recv()返回值:检测主动关闭
- epoll事件:EPOLLRDHUP检测关闭,EPOLLERR/EPOLLHUP检测异常
- 心跳机制:检测意外断开(拔网线等)
区分断开类型:
recv()=0→ 主动关闭ECONNRESET→ 进程崩溃- 心跳超时 → 网络断开
实际工程中,建议应用层心跳 + TCP Keepalive双保险,前者响应快,后者作为兜底。
以上代码在Ubuntu 20.04 + GCC 9.3测试通过,有问题欢迎评论区讨论。
参考资料:
- TCP/IP详解 卷1:协议
- Linux man pages: tcp(7), socket(7), epoll(7)
- Stevens, UNIX网络编程 卷1