【问题标题】:Can another client app close TCP connection which a crashed client app opened with a server?另一个客户端应用程序可以关闭崩溃的客户端应用程序与服务器打开的 TCP 连接吗?
【发布时间】:2019-02-01 01:01:21
【问题描述】:

考虑以下顺序:

  1. 客户端应用程序(网络浏览器)打开多个 TCP 连接到不同的网络服务器;
  2. 然后以太网电缆断开;
  3. 然后关闭客户端应用程序;
  4. 以太网电缆断开连接数小时;
  5. 以太网电缆已重新连接;
  6. 我看到来自长期关闭的客户端应用程序连接到的一些服务器的“TCP keep-alive”数据包(每 60 秒,持续数小时)!

通常,当应用程序关闭时,应用程序会启动关闭每个打开的套接字,然后 TCP 层会尝试向每个远程端点发送一个 FIN 数据包。如果物理上可以发送 FIN 数据包,并且这种发送确实发生了,则本地端点从 ESTABLISHED 状态进入 FINWAIT_1 状态(并等待从远程端点接收 ACK 等)。但是,如果物理链接断开,则 TCP 本地端点无法发送该 FIN,并且服务器仍然假定 TCP 连接仍然存在(并且客户端对“关闭”函数的调用将无限期阻塞,直到物理假设套接字设置为阻塞模式,链接已重新建立,对吗?)。

在任何情况下,在所有传统联网应用程序(例如,网络浏览器)长时间关闭一段时间后重新连接以太网电缆时,我会以精确的 60 秒间隔从三个独立的网络服务器接收“TCP Keep-Alive”数据包几小时!

Wireshark 显示这些TCP Keep-Alive 数据包发送到的本地端口号,但TCPViewnetstat -abno 均不显示任何应用程序正在使用的本地端口号。使用 Process Explorer 查看每个正在运行的进程的“TCP/IP”属性也不会显示任何匹配的端口号。由于任何正在进行的子进程(例如插件应用程序),我认为端口不会因为僵尸“进程记录”(例如,Web 浏览器进程)而被保留,但我不确定我的观察是否使用 TCPView/netstat/Process Explorer 足以排除这种可能性。

鉴于远程 Web 服务器(例如 Akamai 服务器)的身份,我相信这些连接是通过“最近”使用 Web 浏览器建立的。但是,即使浏览器已关闭,物理链接已中断数小时,这些保持活动仍然来自这三个 Web 服务器。

如果连接出现在TCPView,我可以简单地选择它们并手动关闭它们。但是,客户端 TCP 端点似乎早已不复存在。

与此同时,我很困惑为什么服务器要重试这么多次才能得到对其保持活动数据包的回复。

TCP 保持连接行为通常由三个参数控制:\

(1) 等待下一次“爆发”或“探测”尝试的时间;

(2) 在一次“探测”尝试期间发送每个保持活动数据包之间的时间间隔;

(3) 在“突发”被视为失败之前“探测”尝试的最大次数(因此 TCP 连接被视为永久断开)。

对于我从三个不同服务器看到的 TCP 保持活动数据包,“探测”重试之间的时间间隔正好是 60 秒。但是,“探测”重试的最大次数似乎是无限的,这对于任何服务器来说似乎都是一个非常糟糕的选择!

虽然我很好奇这种无情的保持活动流是如何创建和维持的,但我更感兴趣的是如何使用客户端应用程序来强制服务器端端点关闭,因为有'不存在接收这些保持活动数据包的现有本地 TCP 端点。

我的粗略想法是创建一个应用程序,它创建一个 TCP 模式套接字,绑定(允许端口号重用)到传入的 keep-alives 指向的端口号,然后调用“open”,然后“关闭”,希望服务器端点使 TCP 状态转换以一种或另一种方式达到关闭状态!另一种方法可能是创建一个原始模式套接字,并接收 TCP keep-alive 数据包(这只是一个 ACK​​),然后形成并发送一个适当的 FIN 数据包(具有适当的序列号等,以获取长期终止的客户端应用程序显然已停止),然后在发送最终 ACK 之前接收 ACK 和 FIN。

最后一点——我知道会有翻白眼和嘲笑:这里的工作环境是在 Windows 7 上的 VirtualBox 中运行的 Windows XP SP3!因此,我更喜欢可以在 Windows XP SP3 中实现目标(关闭半开 TCP 连接)的代码或开源应用程序。当然,我可以重新启动快照,这可能会关闭连接——但我更感兴趣的是学习如何获取有关网络连接状态的更多信息,以及我可以做些什么来处理这种情况TCP 状态问题。

【问题讨论】:

  • 我相信默认情况下 TCP 甚至不发送任何保持活动数据包。应用程序必须首先启用此功能。你确定这些是保活数据包吗?
  • @usr 是的,Wireshark 清楚地为我收到的定期 ACK 数据包指示“TCP keep-alive”。而且,是的,它需要服务器显式设置套接字选项以启用 TCP 保持活动行为。

标签: sockets networking tcp


【解决方案1】:

我通过编写一个简单的程序(完整代码如下)成功地触发了每个明显的半开 TCP 连接的关闭,该程序将本地套接字绑定到服务器认为它已经连接的端口,尝试建立一个新连接,然后关闭连接。

(注意:如果连接成功,我会发出 HTTP GET 请求,只是因为在我的情况下,幻象 TCP 保持活动显然来自普通 HTTP 服务器,我想知道我可能会得到什么响应。我想可以删除“send”和“recv”调用,而不会影响代码实现所需结果的能力。)

在下面的代码中,src_port_num变量代表服务器发送“TCP keep-alive”数据包的客户端端口号(当前未使用),dst_ip_cstr strong> 是服务器的 IP 地址(例如,Akamai Web 服务器),dst_port_num 是端口号(在我的情况下,它恰好是端口 80 上的普通 HTTP 服务器)。

注意! 我分享这段代码并不意味着它的操作理论可以通过对 TCP 协议规范的理解来严格解释。我只是猜想,声明一个远程端点正在向其发送 TCP 保持活动数据包的废弃本地端口,并尝试与同一个远程端点建立新连接,会以一种或另一种方式促使远程端点关闭陈旧的半开连接——它恰好对我有用

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")

void main()
{
  // Local IP and port number
  char * src_ip_cstr  = "10.0.2.15";
  int    src_port_num = 4805;

  // Remote IP and port number
  char * dst_ip_cstr  = "23.215.100.98";
  int    dst_port_num = 80;

  int res = 0;
  WSADATA wsadata;
  res = WSAStartup( MAKEWORD(2,2), (&(wsadata)) );
  if (0 != res) { printf("WSAStartup() FAIL\n"); return; }

  printf( "\nSRC IP:%-16s Port:%d\nDST IP:%-16s Port:%d\n\n",
  src_ip_cstr, src_port_num, dst_ip_cstr, dst_port_num );

  sockaddr_in src;
  memset( (void*)&src, 0, sizeof(src) );
  src.sin_family           = AF_INET;
  src.sin_addr.S_un.S_addr = inet_addr( src_ip_cstr );
  src.sin_port             = htons( src_port_num );

  sockaddr_in dst;
  memset( (void*)&dst, 0, sizeof(dst) );
  dst.sin_family           = AF_INET;
  dst.sin_addr.S_un.S_addr = inet_addr( dst_ip_cstr );
  dst.sin_port             = htons( dst_port_num );

  int s = socket( PF_INET, SOCK_STREAM, IPPROTO_TCP );
  if ((-1) == s) { printf("socket() FAIL\n"); return; }

  int val = 1;
  res = setsockopt( s, SOL_SOCKET, SO_REUSEADDR, 
  (const char*)&val, sizeof(val) );
  if (0 != res) { printf("setsockopt() FAIL\n"); return; }

  res = bind( s, (sockaddr*)&src, sizeof(src) );
  if ((-1) == res) { printf("bind() FAIL\n"); return; }

  res = connect( s, (sockaddr*)&dst, sizeof(dst) );
  if ((-1) == res) { printf("connect() FAIL\n"); return; }

  char req[1024];
  sprintf( req, "GET / HTTP/1.1\r\nHost: %s\r\nAccept: text/html\r\n"
  "Accept-Language: en-us,en\r\nAccept-Charset: US-ASCII\r\n\r\n", 
  dst_ip_cstr );
  printf("REQUEST:\n================\n%s\n================\n\n", req );

  res = send( s, (char*)&req, strlen(req), 0 );
  if ((-1) == res) { printf("send() FAIL\n"); return; }

  const int REPLY_SIZE = 4096;
  char reply[REPLY_SIZE];
  memset( (void*)&reply, 0, REPLY_SIZE );
  res = recv( s, (char*)&reply, REPLY_SIZE, 0 );
  if ((-1) == res) { printf("recv() FAIL\n"); return; }
  printf("REPLY:\n================\n%s\n================\n\n", reply );

  res = shutdown( s, SD_BOTH );
  res = closesocket( s );

  res = WSACleanup();
}

搞笑/可耻/引人入胜的披露

正如我在最初的问题中提到的,我在运行 Windows XP SP3(主机操作系统为 Windows 7)的 VirtualBox 中使用 Wireshark 观察到这些“TCP keep-alive”数据包。

当我今天早上醒来,喝杯咖啡,用新鲜的眼睛再次观察这个现象时,即使在 24 小时后,“TCP keep-alive”数据包仍然每 60 秒出现一次,我有了一个有趣的发现:这些数据包继续从三个不同的 IP 地址到达,精确的间隔为 60 秒(但三个 IP 是交错的),即使我断开了以太网电缆与 Internet 的连接!我的头脑被炸毁了!

因此,虽然这三个 IP 地址确实对应于我的网络浏览器很久以前连接到的真实网络服务器,但 TCP 保持活动数据包显然来自某个本地软件组件。

这一发现虽然令人震惊,但并没有改变我对这种情况的看法:从我的客户端软件的角度来看,我想要关闭“服务器端”半开的 TCP 连接。

在 VirtualBox 中,选择 “设备”->“网络”->“连接网络适配器” 可以打开或关闭虚拟网络适配器,就像连接或断开虚拟以太网电缆一样。切换到断开连接状态会导致虚拟 TCP 保持活动数据包停止到达 Wireshark。随后切换到连接状态导致 TCP 保持活动数据包继续到达 Wireshark。

无论如何,我有时需要运行两次以上的代码才能成功关闭半开连接。第一次运行代码时,Wireshark 会显示一个带有注释“[TCP ACKed unseen segment]”的数据包,这正是我希望制造的那种 TCP 气体照明混乱,哈哈!因为新的客户端端点对远程端点来说是意料之外的,所以对“连接”的调用在失败之前可能会挂起 30 秒。对于几个僵尸/幻象半开连接,只运行一次程序就足以导致 RST 数据包。

我需要反复修改程序以更改本地端口号、远程 IP 和远程端口号的组合,以匹配我在 Wireshark 中观察到的每个虚拟 TCP 保持活动数据包。 (我把实现用户友好的命令行参数留给亲爱的读者(就是你!)。)经过几轮修改和运行程序,所有僵尸保持活动数据包都停止了。有人可能会说“数据包的沉默”。

结语

[穿着燕尾服,手里拿着马提尼酒杯,在游艇甲板上渴望地凝视着大海,与黑客同行]“我从来不知道那些僵尸数据包是从哪里来的... 是 'VirtualBox Host-Only Network' 虚拟以太网适配器吗?只有 Oracle 知道!”

【讨论】:

    【解决方案2】:

    关闭远程套接字不需要做任何事情,它已经内置在 TCP 协议中。如果系统接收到的 TCP 数据包没有创建新的连接(即设置了 SYN)并且不属于任何已建立的连接,它将回复一个 RST 数据包。这样,对等方将知道端点不再存在并放弃连接。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-05-30
      • 2011-03-21
      相关资源
      最近更新 更多