提问者:小点点

另一个客户端应用程序是否可以关闭崩溃的客户端应用程序与服务器一起打开的传输控制协议?


考虑以下顺序:

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

通常,当应用程序关闭时,应用程序将启动每个打开的套接字的关闭,然后TCP层将尝试向每个远程endpoint发送FIN数据包。如果物理上可以发送FIN数据包,并且这种发送确实发生了,那么本地endpoint将从ESTABLISHED状态变为FINWAIT_1状态(并等待从远程endpoint接收确认字符等)。但是,如果物理链路断开,那么TCP本地endpoint不能发送FIN,服务器仍然假设传输控制协议仍然存在(并且客户端调用关闭函数将无限期阻塞,直到物理链路重新建立,假设套接字设置为阻塞模式,对吗?)。

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

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

考虑到远程网络服务器(例如Akamai服务器)的身份,我认为连接是通过“最近”使用网络浏览器建立的。但是,这些保持生命的东西一直来自这三个网络服务器,尽管浏览器已经关闭,物理链接已经中断了几个小时。

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

TCP保活行为通常由三个参数控制:\

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

(2)单次“探测”尝试期间发送每个保活包的时间间隔;

(3)在突发之前的最大探测尝试次数被认为是失败的(并且传输控制协议因此被认为是永久损坏的)。

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

尽管我对这种持续不断的保持生命流是如何创建和维持的感到好奇,但我更感兴趣的是如何使用客户端应用程序来强制服务器端endpoint关闭,因为没有现有的本地TCPendpoint接收这些保持生命的数据包。

我的粗略想法是创建一个应用程序,该应用程序创建一个TCP模式套接字,绑定(允许端口号重用)到传入的保持生命指向的端口号,然后调用“打开”,然后调用“关闭”,希望服务器endpoint以某种方式进行TCP状态转换以达到关闭状态!另一种方法可能是创建一个原始模式套接字,并接收TCP的保活数据包(这只是一个确认字符),然后形成并发送一个适当的FIN数据包(带有适当的序列号等),从长期终止的客户端应用程序明显中断的地方开始),然后在发送最终确认字符之前接收一个确认字符和FIN。

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


共2个答案

匿名用户

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

(注意:如果连接成功,我会发出一个HTTPGET请求,只是因为在我的例子中,幻象TCP保持生命显然来自普通HTTP服务器,我想知道我可能会得到什么响应。我认为“发送”和“recv”调用可以被删除,而不会影响代码实现所需结果的能力。)

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

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

#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的VirtualBox中观察到这些带有Wireshark的“TCP保持活动”数据包,其中主机OSWindows 7。

当我今天早上醒来,用一杯咖啡和新鲜的眼睛再次审视这一现象时,即使在24小时后,“TCP保持活力”数据包仍然每60秒出现一次,我有了一个滑稽的发现:这些数据包继续从三个不同的IP地址到达,精确地以60秒的间隔到达(但三个IP交错),即使我从互联网上断开以太网电缆!我的头脑被炸飞了!

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

这一发现尽管令人震惊,但并没有改变我对这种情况的思考:从我的客户端软件角度来看,有“服务器端”半开的TCP连接,我想挑起它们关闭。

在VirtualBox中,选择“设备”-

无论如何,我有时需要运行两次以上的代码才能成功关闭半开连接。当第一次运行代码时,Wireshark会显示一个带有注释“[TCPACKed未看到的段]”的数据包,这正是我希望制造的那种TCP煤气灯混乱,哈哈!因为新的客户端endpoint是远程endpoint意想不到的,所以“连接”的调用在失败前挂起了大约30秒。对于几个僵尸/幻影半开连接,运行一次程序就足以导致RST数据包。

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

尾声

[穿着燕尾服,手里拿着马提尼酒杯,在一群黑客的陪伴下,从游艇甲板上渴望地凝视着大海]“我从来没有弄清楚那些僵尸包是从哪里来的……是‘虚拟盒子主机专用网络’虚拟以太网适配器吗?只有甲骨文知道!”

匿名用户

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