如何正确关闭TCP连接

  • Post author:
  • Post category:其他




Ⅰ. 如何正确关闭TCP连接示例程序

参考自:

https://blog.netherlabs.nl/articles/2009/01/18/the-ultimate-so_linger-page-or-why-is-my-tcp-not-reliable


翻译:

https://www.iteye.com/blog/watter1985-1924977

假设我们在两个POSIX兼容操作系统上运行以下两个程序,目的是从程序A发送100万字节来编程B(这里可以在

此处

找到程序):

A:

  sock = socket(AF_INET, SOCK_STREAM, 0);  
  connect(sock, &remote, sizeof(remote));
  write(sock, buffer, 1000000);             // returns 1000000
  close(sock);

B:

  int sock = socket(AF_INET, SOCK_STREAM, 0);
    bind(sock, &local, sizeof(local));
    listen(sock, 128);
    int client=accept(sock, &local, locallen);
    write(client, "220 Welcome\r\n", 13);

    int bytesRead=0, res;
    for(;;) {
        res = read(client, buffer, 4096);
        if(res < 0)  {
            perror("read");
            exit(1);
        }
        if(!res)
            break;
        bytesRead += res;
    }
    printf("%d\n", bytesRead);

测验问题——将程序B打印完成什么?

A) 1000000
B) something less than 1000000
C) it will exit reporting an error
D) could be any of the above

正确的答案是’d’。

下面通过演示上述程序,分析答案选D的原因。




Ⅱ. b 程序数据接收不完整的原因可能是 a 程序没有发送完所有的数据(tcp send buffer中的数据)就退出了

代码下载:

http://ds9a.nl/tcp-programs.tar.gz


注:这里提供的代码 program-a.c 是最终版本,下面博客将通过示例程序展示各阶段 program-a.c 代码。 program-b.c 代码无需改变。

代码:program-a.c

/* Run as 'program-a 1.2.3.4' to connect to 1.2.3.4 port 9876 */
#include <sys/types.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/ioctl.h>

#ifdef __linux__
#include <linux/sockios.h>
#endif

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

int main(int argc, char** argv)
{
	int sock=socket(AF_INET, SOCK_STREAM, 0);
	
	struct sockaddr_in remote;
	remote.sin_family=AF_INET;
	inet_pton(AF_INET, argc > 1 ? argv[1] : "127.0.0.1", &remote.sin_addr);
	remote.sin_port = ntohs(9876);
	
	if(connect(sock, (struct sockaddr*)&remote, sizeof(remote)) < 0) {
		perror("Connecting to remote");
		exit(1);
	}
	
	char buffer[4000];
	int n, res;
	/* 发送数据 */
	for(n=0; n < 250 ; ++n) {	// 每次发送 4000, 发送250 次,共1,000,000字节
		res=write(sock, buffer, sizeof(buffer));
		if(res != sizeof(buffer)) {
			if(res < 0)
				perror("writing");
			else
				fprintf(stderr,"Partial write\n");
			exit(1);
		}
	}
	close(sock);
	return 0;
}

运行程序:程序B显示,每次收到的数据都小于 1000000 。

在这里插入图片描述

使用tcpdump对发送端(程序A)抓包

tcpdump tcp and port 9876

:可以看到最后一次发送时 seq=757561。

猜想:可能是程序A并没有将数据发送完就调用了close(),从未发送

RST报文

,强行断开了连接,使得b程序没有接收到全部的数据

在这里插入图片描述



Ⅲ. 尝试让程序a中所有排队的消息(tcp send buffer)都成功发送,但是程序 b 仍然不能接收到完整的数据。

由抓包结果可知是由于发送端(程序A)没有将数据发完造成的,因此考虑是否是通过让发送端程序将数据发送完整。

这里有两种方法:① 我们使用 ioctl 函数获取a程序的发送缓冲区,查看是否有剩余数据未发送完,如果没有缓冲区还有数据,则一直阻塞直至数据发送完。② 设置SO_LINGER后,如果send buffer中还有数据,系统会试着先把send buffer中的数据发送出去,然后close才返回。

在program-a.c程序中添加函数 depleteSendBuffer() 。将其在 program-a.c 函数 close() 前一行调用。

// 阻塞,直至tcp buffer中的数据空了
void depleteSendBuffer(int fd) 
{
	int lastOutstanding=-1;
	for(;;) {
		int outstanding;
		ioctl(fd, SIOCOUTQ, &outstanding);
		if(outstanding != lastOutstanding) 
			printf("Outstanding: %d\n", outstanding);
		lastOutstanding = outstanding;
		if(!outstanding)
			break;
		usleep(1000);
	}
}

调用:运行新的 a 程序(参考下图),在输出提示中,我们可知此时send buffer中已经没有数据了。
在这里插入图片描述

但是,在 b 程序中数据任然没有收到完整的 1M 数据(多次测试,有一定的几率可以收到完整的数据)。

在这里插入图片描述

使用tcpdump抓包可以发现,这次显然数据已经发送完整了,从seq=1000001可以看出累计发送了 1M 数据(下面ack=1000001也表示数据成功到达了对端)。然后程序 b 的输出显示,b 程序并未接收到 1M 数据,而抓包结果的最后一条记录,可能就是解释这个原因的答案。

在这里插入图片描述


猜想可能的原因是:最后一个RST报文发送给 b 程序时,b程序立刻关闭连接,丢弃了缓冲区中还未读取完的数据,因此 b 程序的打印结果显示的数据并没有达到 1M



Ⅳ. 即使程序a发送完所有报文,b程序仍然接收不到完整的数据。猜想可能和调用close()时发送了RST报文有关

再次修改程序,在 program-a.c 程序中,

在close() 之前让程序休眠 5 秒钟

,这样 b 程序就来的及将所有的数据读取完之后,a 程序再关闭连接。哪怕 a 发送RST报文,此时 b 端已经读取完所有的数据了。

在这里插入图片描述

执行程序,同时,在 a 程序睡眠的 5 秒钟内,我们使用 netstat 命令,看一下当前tcp连接的情况:

在这里插入图片描述

可以看大,在 a 程序的tcp recv buffer中还有9个字节的数据没有接收,这是导致 a 程序产生RST报文的原因。

4.2.2.13 Closing a Connection

4.2.2.13 Closing a Connection


RFC 1122文档 4.2.2.13 节 Closing a Connection 讲到,有两种关闭连接的方式。一种是通过FIN报文,经过四次挥手将所有数据处理完后正常关闭 。另一种是通过RST报文,丢弃所有数据,立刻关闭。

A host MAY implement a “half-duplex” TCP close sequence, so that an application that has called CLOSE cannot continue to read data from the connection. If such a host issues a CLOSE call while received data is still pending in TCP, or if new data is received after CLOSE is called, its TCP SHOULD send a RST to show that data was lost.

主机可以实现”半双工“TCP关闭序列,使得调用close的应用程序,不能继续从连接读取数据。如果这样的主机在读取TCP中挂起的数据时调用 close,或者在调用close以后又有新数据到达时,TCP应该发送一个RST来表明数据已丢失。

而造成 b 程序收到数据不完整的原因是,b 程序向 a 程序发送了一段”220 Welcome\r\n”的数据,而 a 程序并没有去读取这段数据就调用了 close() 。

close()调用时,如果有任何挂起的可读数据,会导致立即发送复位(reset)

通过让 a 程序睡眠等待 b 程序接收完所有的数据再关闭显示是不显示的。那么接下来的我们应该考虑如何让 a 程序正常的发起FIN报文关闭,而不是通过RST的方式造成数据丢失。

FIN报文在四次挥手的过程中,会进行协调,保证双方数据都发送完之后才会进行关闭,因此我们只要让程序通过FIN的四次挥手过程关闭连接,那么就可以保证 b 程序一定可以将数据接收完整。



Ⅴ. 正确关闭连接的方式为使用 shutdown() ,发起FIN报文断开连接
  1. 发送方不再发送数据后,使用

    shutdown(sock,SHUT_WR)

    关闭本端套接字的输出流。

    shutdown() 会向对方发送 FIN 包。FIN 包通过四次挥手过程断开连接,可以有效的等待数据发送完成再断开连接。
  2. 调用 read() 函数,read()/resv() 将会返回0,代表对方也不再发送数据(对方可能也调用了shutdown()函数)。此时连接已断开。

    (这里read()返回0应考虑客户端存在Bug或恶意的不返回0的情况,使得本端永远不满足read()=0的情况。因此这里因考虑有超时机制,在shutdown之后若干秒内如果没有满足read()=0,则强制断开连接并有相应的错误处理)
  3. 调用 close() 函数关闭套接字。

需要注意的是,调用

close()/closesocket() 关闭套接字

时,或调用

shutdown() 关闭输出流

时,都会向对方发送 FIN 包。FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来了。

.

默认情况下,close()/closesocket() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而

shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包

。也就意味着,调用 close()/closesocket() 将丢失输出缓冲区中的数据的风险,而调用 shutdown() 不会



Ⅵ. 使用nc命令模拟此过程

使用nc命令可以在服务器和客户端之间发送数据,例如:

在这里插入图片描述

我们可以利用

nc -l -p 3123

监听一个端口,然后另一个机器上使用

nc server_ip port

进行连接。

在这里插入图片描述

示例1:使用 nc -l 模拟b程序(图左),然后使用原本的 a 程序发送数据。发现确实是由于 b 程序额外发送给a的数据导致的b端接收数据不完整。

在这里插入图片描述

示例2:使用原b程序接收数据,然后使用 nc 模拟 a 程序发送数据。发现确实是由于 b 程序额外发送给a的数据导致的b端接收数据不完整。

在这里插入图片描述

注:最后发现,在服务器上安装的nc版本支持–send-only参数,而本地的linux机器上的nc版本却不支持这个参数,因此在服务器上本地传输实验了以下,发现同时使用 –send-only 和 –no-shutdown 可以模拟 a程序 只发送不接受的特性,从而只要 b 端有数据发送,那么b端读取的数据就不完整。

       --recv-only (Only receive data)
           If this option is passed, Ncat will only receive data and
           will not try to send anything.

       --send-only (Only send data)
           If this option is passed, then Ncat will only send data and
           will ignore anything received. This option also causes Ncat
           to close the network connection and terminate after EOF is
           received on standard input.

       --no-shutdown (Do not shutdown into half-duplex mode)
           If this option is passed, Ncat will not invoke shutdown on a
           socket aftering seeing EOF on stdin. This is provided for
           backward-compatibility with OpenBSD netcat, which exhibits
           this behavior when executed with its '-d' option.


在这里插入图片描述


引用: https://ysw1912.github.io/post/network/how_to_close_tcp_connection_correctly/


原因



send()

成功返回只意味着内核接收了数据,并准备在某些时候发送它们。内核接收数据后,还要把数据包发送到网卡,并在网络中各个网卡遍历,最终到达远程主机。远程主机的内核确认到数据,拥有该 socket 的进程从中读取数据,此时数据才真正到达应用程序,用文件系统的话来说,是 “hit the disk”。

当调用

close()

关闭 socket fd 时,整个 TCP 连接也关闭了,即使一些数据还在内核的发送缓冲区里,或者已经发送但未被确认。

发送方如果 send() 后立即 close()

,就可能出现数据其实还未发送的情况。设置 socket 选项

SO_LINGER



尝试将残留在发送缓冲区的数据发送给对方

,看似解决了这种问题,但有时依然会出现数据发送不全的问题。

原因在于,发送方执行 close() 时,如果它的

接收缓冲区中仍有数据没有读取

,或者调用 close() 后

有新的数据到达

,这时它会发送一个

RST

告知对方数据丢失,没有正常使用

FIN

断开连接,因此设置

SO_LINGER

没有效果。


解决


那么如果发送方先读取了自己接受缓冲区的数据,再 close(),问题会得到解决吗?并不会。这时需要借助

shutdown()

,shutdown() 会确实发送一个

FIN

给对方,说明对方也即将关闭 socket,此时可以

通过 recv() 返回 0 (收到 EOF)检测到接受端的关闭

正确的关闭逻辑如下,建议用这种方式代替

SO_LINGER

发送方:send() → shutdown(WR) → recv() == 0(由接收方 close 导致) → close()

接收方:recv() == 0(由发送方 shutdown 导致) → more to send? → close()

值得注意,如果遇到恶意或错误 client,永远不 close(),则服务器 recv() 不会返回 0(阻塞且 errno == EAGAIN),因此需要加一个超时控制,若 shutdown(WR) 若干秒后 recv() 未返回 0,则直接 close() 强制关闭连接。

即使如此,

shutdown() 也不能保证接收方接受到所有数据

,这只是发送方能做到的最大努力。最好的办法还是像 HTTP 协议那样,附有消息的长度信息,这就需要有能力

自己设计协议

还有一种方法,Linux 记录了未确认数据的数量,可以使用

ioctl



SIOCOUTQ

选项查询,如果这个数字达到 0,我们

至少可以确认所有的发送数据到达了远程操作系统

,只是只能在 Linux 平台下实现。



版权声明:本文为weixin_43919932原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。