添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

为了理解 TCP keepalive的作用。我们需要清楚,当TCP的Peer A ,Peer B 两端建立了连接之后,如果一端突然拔掉网线或拔掉电源时,怎么检测到拔掉网线或者拔掉电源、链路不通? 原因是在需要长连接的网络通信程序中,经常需要心跳检测机制,来实现检测对方是否在线或者维持网络连接的需要。

什么是 TCP 保活?

当你建立一个 TCP 连接时,你关联了一组定时器。其中一些计时器处理保活过程。当保活计时器达到零时,向对等方发送一个保活探测数据包,其中没有数据并且 ACK 标志打开。

由于 TCP/IP 规范,可以这样做,作为一种重复的 ACK,并且远程端点将没有参数,因为 TCP 是面向流的协议。另一方面,将收到来自远程主机的回复,没有数据和ACK 集。

如果收到对 keepalive 探测的回复,则可以断言连接仍在运行。事实上,TCP 允许处理流,而不是数据包,因此零长度数据包对用户程序没有危险。

此过程很有用,因为如果其他对等方失去连接(例如通过重新启动),即使没有流量,也会注意到连接已断开。如果对等方未回复 keepalive 探测,可以断言连接不能被视为有效,然后采取正确的操作。

为什么要使用 TCP keepalive?

1、检查死节点
2、 防止因网络不活动而断开连接

检查死节点

想一想 Peer A 和 Peer B 之间的简单 TCP 连接:初始的三次握手,从 A 到 B 的一个 SYN 段,从 B 到 A 的 SYN/ACK,以及从 A 到 B 的最终 ACK。

此时,我们处于稳定状态:连接已建立,现在我们通常会等待有人通过通道发送数据。

那么问题来了:从 B 上拔下电源,它会立即断电,而不会通过网络发送任何信息来通知 A 连接将断开。

从它的角度来看,A 已准备好接收数据,并且不知道 B 已经崩溃。现在恢复B的电源,等待系统重启。 A 和 B 现在又回来了,但是当 A 知道与 B 仍然处于活动状态的连接时,B 不知道。当 A 尝试通过死连接向 B 发送数据时,情况自行解决,B 回复 RST 数据包,导致 A 最终关闭连接。

    _____                                                     _____
   |     |                                                   |     |
   |  A  |                                                   |  B  |
   |_____|                                                   |_____|
      |--->--->--->-------------- SYN -------------->--->--->---|
      |---<---<---<------------ SYN/ACK ------------<---<---<---|
      |--->--->--->-------------- ACK -------------->--->--->---|
      |                                       system crash ---> X
      |                                     system restart ---> ^
      |--->--->--->-------------- PSH -------------->--->--->---|
      |---<---<---<-------------- RST --------------<---<---<---|

Keepalive 可以告诉您何时无法访问另一个对等点,而不会出现误报的风险。

防止因网络不活动而断开连接

keepalive 的另一个有用目标是防止不活动断开通道。当你在 NAT 代理或防火墙后面时,无缘无故断开连接是一个非常常见的问题。这种行为是由代理和防火墙中实现的连接跟踪过程引起的,它们跟踪通过它们的所有连接。

它们跟踪通过它们的所有连接。由于这些机器的物理限制,它们只能在内存中保留有限数量的连接。最常见和合乎逻辑的策略是保持最新的连接并首先丢弃旧的和不活动的连接。

    _____           _____                                     _____
   |     |         |     |                                   |     |
   |  A  |         | NAT |                                   |  B  |
   |_____|         |_____|                                   |_____|
      ^               ^                                         ^
      |--->--->--->---|----------- SYN ------------->--->--->---|
      |---<---<---<---|--------- SYN/ACK -----------<---<---<---|
      |--->--->--->---|----------- ACK ------------->--->--->---|
      |               |                                         |
      |               | <--- connection deleted from table      |
      |               |                                         |
      |--->- PSH ->---| <--- invalid connection                 |
      |               |                                         |

Linux下使用TCP keepalive

Linux 内置了对 keepalive 的支持。涉及 keepalive 的过程使用三个用户驱动的变量,可以使用 cat 查看参数值。

在这里插入图片描述
前两个参数以秒表示,最后一个是纯数字。 这意味着keepalive 例程在发送第一个keepalive 探测之前等待两个小时(7200 秒),然后每75 秒重新发送一次。 如果连续9次没有收到 ACK 响应,则连接被标记为断开。

修改这个值很简单,可以这样修改:

echo 7000 > /proc/sys/net/ipv4/tcp_keepalive_time
echo 40 > /proc/sys/net/ipv4/tcp_keepalive_intvl
echo 10 > /proc/sys/net/ipv4/tcp_keepalive_probes

还有另一种访问内核变量的方法,使用 sysctl 命令

setsockopt 、getsockopt 函数调用

在 Linux 操作系统中,我们可以通过代码启用一个 socket 的心跳检测,为特定套接字启用 keepalive 所需要做的就是在套接字本身上设置特定的套接字选项。函数原型如下:

int getsockopt(int sockfd, int level, int optname,
                      void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,
                      const void *optval, socklen_t optlen);

第一个参数是socket;第二个必须是 SOL_SOCKET,第三个必须是 SO_KEEPALIVE。第四个参数必须是布尔整数值,表示我们要启用该选项,而最后一个是之前传递的值的大小。

在编写应用程序时,还可以为 keepalive 设置其他三个套接字选项。它们都使用 SOL_TCP 级别而不是 SOL_SOCKET,并且它们仅针对当前套接字覆盖系统范围的变量。如果不先写入就读取,将返回当前系统范围的参数。

TCP_KEEPCNT:覆盖 tcp_keepalive_probes
TCP_KEEPIDLE:覆盖 tcp_keepalive_time
TCP_KEEPINTVL:覆盖 tcp_keepalive_intvl 		

TCP keepalive 代码实现

在写TCP keepalive 服务程序时,除了要处理SIGPIPE外,还要有客户端连接检测机制,用于及时发现崩溃的客户端连接。我们使用TCP的 keepalive 机制方式。

tcp_keepalive_client:

int main(int argc, char *argv[])
	kat_arg0 = basename(argv[0]);
	bzero(&cp, sizeof (cp));
	cp.cp_keepalive = 1;
	cp.cp_keepidle = -1




    
;
	cp.cp_keepcnt = -1;
	cp.cp_keepintvl = -1;
	while ((c = getopt(argc, argv, ":c:d:i:")) != -1) {
		switch (c) {
		case 'c':
			cp.cp_keepcnt = parse_positive_int_option(
			    optopt, optarg);
			break;
		case 'd':
			cp.cp_keepidle = parse_positive_int_option(
			    optopt, optarg);
			break;
		case 'i':
			cp.cp_keepintvl = parse_positive_int_option(
			    optopt, optarg);
			break;
		case ':':
			warnx("option requires an argument: -%c", optopt);
			usage();
			break;
		case '?':
			warnx("unrecognized option: -%c", optopt);
			usage();
			break;
	if (optind > argc - 1) {
		warnx("missing required arguments");
		usage();
	ipport = argv[optind++];
	if (parse_ip4port(ipport, &cp.cp_ip) == -1) {
		warnx("invalid IP/port: \"%s\"", ipport);
		usage();
	(void) fprintf(stderr, "going connect to: %s port %d\n",
	    inet_ntoa(cp.cp_ip.sin_addr), ntohs(cp.cp_ip.sin_port));
	(void) fprintf(stderr, "set SO_KEEPALIVE  = %d\n", cp.cp_keepalive);
	(void) fprintf(stderr, "set TCP_KEEPIDLE  = %d\n", cp.cp_keepidle);
	(void) fprintf(stderr, "set TCP_KEEPCNT   = %d\n", cp.cp_keepcnt);
	(void) fprintf(stderr, "set TCP_KEEPINTVL = %d\n", cp.cp_keepintvl);
	rv = connectandwait(&cp);
	return (rv == 0 ? EXIT_SUCCESS : EXIT_FAILURE);

tcp_keepalive_server:

int main(int argc, char *argv[] )
   /* 创建套接字 */
   if((listen_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
      perror("socket()");
      exit(EXIT_FAILURE);
   /* 检查 keepalive 选项的状态  */
   if(getsockopt(listen_sock, SOL_SOCKET, SO_KEEPALIVE, &optval, &optlen) < 0) {
      perror("getsockopt()");
      close(listen_sock);
      exit(EXIT_FAILURE);
   printf("SO_KEEPALIVE default is %s\n", (optval ? "ON" : "OFF"));
   /* 将选项设置为活动  */
   optval = 1;
   optlen = sizeof(optval);
   if(setsockopt(listen_sock, SOL_SOCKET, SO_KEEPALIVE, &optval, optlen) < 0) {
      perror("setsockopt()");
      close(listen_sock);
      exit(EXIT_FAILURE);
   printf("SO_KEEPALIVE set on socket\n");
   /* 再次检查状态  */
   if(getsockopt(listen_sock, IPPROTO_TCP, TCP_KEEPIDLE, &optval, &optlen) < 0) {
      perror("getsockopt()");
      close(listen_sock);
      exit(EXIT_FAILURE);
   printf("TCP_KEEPIDLE is %d\n", optval );
      /* 再次检查状态  */
   if(getsockopt(listen_sock, IPPROTO_TCP, TCP_KEEPCNT, &optval, &optlen) < 0) {
      perror("getsockopt()");
      close(listen_sock);
      exit(EXIT_FAILURE);
   printf("TCP_KEEPCNT is %d\n", optval);
      /* 再次检查状态  */
   if(getsockopt(listen_sock, IPPROTO_TCP, TCP_KEEPINTVL, &optval, &optlen) < 0) {
      perror("getsockopt()");
      close(listen_sock);
      exit(EXIT_FAILURE);
   printf("TCP_KEEPINTVL is %d\n", optval );
  /* 初始化套接字结构 */
   bzero((char *) &serv_addr, sizeof(serv_addr));
   int portno = atoi(argv[1]);
   serv_addr.sin_family = AF_INET;
   serv_addr.sin_addr.s_addr = INADDR_ANY;
   serv_addr.sin_port = htons(portno);
  ...

在这里插入图片描述
在这里插入图片描述
程序创建一个 TCP 套接字并将 SO_KEEPALIVE 套接字选项设置为 1。如果指定了“-c”、“-d”和“-i”选项中的任何一个,则设置 TCP_KEEPCNT、TCP_KEEPIDLE 和 TCP_KEEPINTVL 套接字选项 在相应选项参数的套接字上。

通过测试程序,我们可以使用tcpdump、或者tshark是命令行抓包工具,来分析KeepAlive。

tshark -nn -i lo port 5050
tcpdump -nn -i lo port 5050

在这里插入图片描述
tcpdump -nn -i lo port 5050
在这里插入图片描述
整个keepalive过程很简单,就是client给server发送一个包,server返回给用户一个包。注意包内没有数据,只有ACK标识 被打开。

ps -aux | grep tcp_keepalive
在这里插入图片描述

keepalive 是一个设备向另一个设备发送的消息,用于检查两者之间的链路是否正在运行,或防止链路中断。

欢迎关注微信公众号【程序猿编码】,需要TCP keepalive 完整源码的添加本人微信号(c17865354792)

参考:TCP/IP详解

在默认的情况下,TCP连接是没有保活的心跳的。这就是说,当一个TCP的socket,客户端与服务端谁也不发送数据,会一直保持着连接。这其中如果有一方异常掉线,另一端永远也不可能知道。这对于一些服务型的程序来说,将是灾难性的后果。   所以,必须对创建的socket,启用保活心跳,即Keepalive选项。 启用Keepalive   对于WIN32或者Linux平台来说,设置so
KeepAlive并不是TCP协议规范的一部分,但在几乎所有的TCP/IP协议栈(不管是Linux还是Windows)中,都实现KeepAlive功能 RFC1122#TCP Keep-Alives 如何设置它? 在设置之前我们先来看看KeepAlive都支持哪些设置项 KeepAlive默认情况下是关闭的,可以被上层应用开启和关闭 tcp_keepalive_time:...
  前两天在blink上发的思路,花了一天时间给实现了 。不得不说,好久没写Qt程序,手生了不少,要不应该可以更有效率的。 目录KeepAlive小项目一、功能二、详细介绍三、源码下载四、可执行文件下载 主要实现以下功能: 显示当前系统时间 给定一个定期提醒时间间隔,显示离提醒还有多久 显示程序运行以来经过的时间 可最小化到托盘,实现后台运行 当达到提醒时间间隔后,弹窗提醒 辅助功能: 可手动输入,调整时间间隔 可自动创建桌面快捷方式 二、详细介绍
心跳机制tcp keepalive的讨论及其应用---断网检测的C代码实现(Windows环境下) 之前很多网友都问过一个类似这样的问题: tcp连接ok后,网络如果断了, 怎么检测断网? 其实, 说白了, 也就是检测tcp死链接。 在本文中, 我们来详细讨论一下, 并尝试用C代码实现这个断网检测功能。 本文的讨论还是以Windows为例, 程序也是在Wi...
intfd=tcpSocket->socketDescriptor(); intkeepAlive=1;//开启keepalive属性,缺省值:0(关闭) intkeepIdle=5;//如果在5秒内没有任何数据交互,则进行探测,缺省值:7200(s) intkeepInterval=2;//探测时发探测包的时间间隔为2秒,缺省值:75(s) intkeepCount=2;//探测重试的次数,全部超时则认定连接失效,缺省值:9(...
<think>嗯,用户之前问过关于C++中的TcpSocket类,现在他们想要在Windows下实现一个包含连接、读取、写入、断开连接和检查keepalive功能的类。我需要确保回答符合他们的需求,同时考虑到他们可能的知识水平。 首先,用户可能已经了解C++的基础,但对网络编程可能不太熟悉。他们可能在开发一个需要网络通信的应用,比如客户端或服务器程序。用户的需求不仅仅是基本功能,还包括keepalive,这说明他们可能关心连接的稳定性和长时间维持。 接下来,我需要回忆Windows下Socket编程的要点。Windows的Socket API与POSIX有所不同,比如需要WSAStartup初始化和清理,使用SOCKET类型而不是int,以及错误处理使用WSAGetLastError()。这些细节在之前的回答中提到过,但可能需要更详细的说明,特别是针对Windows的实现。 然后,如何封装一个类。用户可能需要一个易于使用的类,隐藏底层细节,提供连接、读写等方法。需要包括构造函数、析构函数,以及连接、发送、接收、断开的方法。此外,keepalive功能的实现需要设置Socket选项,比如SO_KEEPALIVE,可能还需要调整TCP_KEEPIDLE等参数,但要注意Windows和Linux的不同,这里用户指定的是Windows,所以需要针对Windows的特定方法。 可能需要注意的问题包括错误处理、资源管理(比如使用RAII确保Socket关闭)、阻塞与非阻塞模式,以及数据粘包处理。不过用户没有特别提到非阻塞,所以可能默认使用阻塞模式,但最好在代码中注释说明。 另外,keepalive实现部分,用户可能需要知道如何在Windows下设置相关参数,比如通过WSAIoctl和SIO_KEEPALIVE_VALS。需要检查这些API的可用性,以及示例代码的正确性。 用户可能还会关心示例代码的完整性和可编译性,所以需要提供一个完整的类定义和示例用法,确保没有遗漏关键部分,比如WSAStartup和WSACleanup的调用,以及字节序转换(htons、inet_addr等)。 测试部分,用户可能需要知道如何测试连接、发送接收数据,以及keepalive是否生效。可能需要建议使用网络调试工具或编写简单的服务器和客户端进行测试。 最后,要确保代码符合现代C++实践,比如使用RAII管理资源,避免内存泄漏,使用异常或错误码处理错误,根据用户的偏好。在之前的回答中提到了使用RAII和智能指针,所以这里的类设计应该遵循这些原则。</think>以下是一个基于Windows平台原生API实现C++ TCP Socket类示例,包含连接、读写、断开和KeepAlive功能。代码遵循Winsock2 API规范,使用RAII管理资源: ```cpp #include <winsock2.h> #include <ws2tcpip.h> #include <system_error> #include <string> #pragma comment(lib, "ws2_32.lib") class TcpSocket { public: TcpSocket() : m_socket(INVALID_SOCKET) { WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { throw std::system_error(WSAGetLastError(), std::system_category(), "WSAStartup failed"); ~TcpSocket() { disconnect(); WSACleanup(); // 连接到服务器 void connect(const std::string& ip, uint16_t port) { if (m_socket != INVALID_SOCKET) disconnect(); m_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (m_socket == INVALID_SOCKET) { throw std::system_error(WSAGetLastError(), std::system_category(), "Socket creation failed"); sockaddr_in serverAddr{}; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(port); inet_pton(AF_INET, ip.c_str(), &serverAddr.sin_addr); if (::connect(m_socket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) { closesocket(m_socket); m_socket = INVALID_SOCKET; throw std::system_error(WSAGetLastError(), std::system_category(), "Connection failed"); // 发送数据 size_t send(const void* data, size_t length) { int result = ::send(m_socket, static_cast<const char*>(data), static_cast<int>(length), 0); if (result == SOCKET_ERROR) { throw std::system_error(WSAGetLastError(), std::system_category(), "Send failed"); return static_cast<size_t>(result); // 接收数据(阻塞模式) size_t receive(void* buffer, size_t bufferSize) { int result = recv(m_socket, static_cast<char*>(buffer), static_cast<int>(bufferSize), 0); if (result == SOCKET_ERROR) { throw std::system_error(WSAGetLastError(), std::system_category(), "Receive failed"); return static_cast<size_t>(result); // 设置KeepAlive void setKeepAlive(bool enable, DWORD idleTime = 5000, DWORD interval = 1000) { struct tcp_keepalive ka; ka.onoff = enable ? 1 : 0; ka.keepalivetime = idleTime; // 首次探测时间(ms) ka.keepaliveinterval = interval; // 探测间隔(ms) DWORD bytesReturned; if (WSAIoctl(m_socket, SIO_KEEPALIVE_VALS, &ka, sizeof(ka), nullptr, 0, &bytesReturned, nullptr, nullptr) == SOCKET_ERROR) { throw std::system_error(WSAGetLastError(), std::system_category(), "Set KeepAlive failed"); // 断开连接 void disconnect() { if (m_socket != INVALID_SOCKET) { shutdown(m_socket, SD_BOTH); closesocket(m_socket); m_socket = INVALID_SOCKET; // 检查连接状态 bool isConnected() const { if (m_socket == INVALID_SOCKET) return false; char dummy; int err = recv(m_socket, &dummy, 1, MSG_PEEK); if (err == SOCKET_ERROR) { return WSAGetLastError() != WSAENOTCONN; return true; private: SOCKET m_socket; ### 核心功能说明 1. **连接管理**: - 使用`connect()`方法建立TCP连接 - `disconnect()`方法实现优雅关闭(发送FIN包) - `isConnected()`通过`MSG_PEEK`模式检测连接状态 2. **数据传输**: - `send()`方法支持二进制数据发送 - `receive()`使用阻塞模式读取数据 - 支持发送/接收任意二进制数据(需自行处理粘包问题) 3. **KeepAlive机制**: - 通过`SIO_KEEPALIVE_VALS`控制参数: - `idleTime`:连接空闲多久后开始探测(默认5秒) - `interval`:探测包发送间隔(默认1秒) - 需要Windows XP SP1+支持 4. **异常处理**: - 使用`std::system_error`抛出异常 - 通过`WSAGetLastError()`获取错误码 ### 使用示例 ```cpp try { TcpSocket socket; socket.connect("127.0.0.1", 8080); socket.setKeepAlive(true); const char* msg = "Hello Server"; socket.send(msg, strlen(msg)); char buffer[1024]; size_t received = socket.receive(buffer, sizeof(buffer)); if (socket.isConnected()) { // 处理接收数据 } catch (const std::system_error& e) { std::cerr << "Error: " << e.what() << " (code: " << e.code() << ")\n"; ### 关键注意事项 1. **Winsock初始化**: - 构造函数调用`WSAStartup()` - 析构函数调用`WSACleanup()` - 确保每个`WSAStartup`都有对应的清理 2. **资源管理**: - 使用RAII确保Socket句柄正确关闭 - 禁止拷贝构造/赋值操作(可添加`= delete`) 3. **性能优化**: - 可添加`setNonBlocking()`方法实现非阻塞模式 - 可扩展支持IPv6(`AF_INET6`) 4. **跨平台差异**: - Windows的`SOCKET`类型实际为`HANDLE` - KeepAlive参数设置方式与Linux不同 5. **超时控制**: 可扩展设置收发超时: ```cpp DWORD timeout = 5000; // 5秒 setsockopt(m_socket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout)); 建议在实际项目中使用以下改进: - 添加缓冲区管理机制 - 实现异步I/O操作 - 支持DNS解析(使用`getaddrinfo()`) - 添加SSL/TLS支持 - 实现连接池管理 完整实现需要约200-300行代码,建议参考Windows SDK的`winsock2.h`和MSDN文档进行扩展。