文章目录

  • Ⅰ. 如何正确关闭TCP连接示例程序
  • Ⅱ. b 程序数据接收不完整的原因可能是 a 程序没有发送完所有的数据(tcp send buffer中的数据)就退出了
  • Ⅲ. 尝试让程序a中所有排队的消息(tcp send buffer)都成功发送,但是程序 b 仍然不能接收到完整的数据。
  • Ⅳ. 即使程序a发送完所有报文,b程序仍然接收不到完整的数据。猜想可能和调用close()时发送了RST报文有关
  • Ⅴ. 正确关闭连接的方式为使用 shutdown() ,发起FIN报文断开连接
  • Ⅵ. 使用nc命令模拟此过程
Ⅰ. 如何正确关闭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 1000000close(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");elsefprintf(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 andwill not try to send anything.--send-only (Only send data)If this option is passed, then Ncat will only send data andwill ignore anything received. This option also causes Ncatto close the network connection and terminate after EOF isreceived on standard input.--no-shutdown (Do not shutdown into half-duplex mode)If this option is passed, Ncat will not invoke shutdown on asocket aftering seeing EOF on stdin. This is provided forbackward-compatibility with OpenBSD netcat, which exhibitsthis 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 记录了未确认数据的数量,可以使用ioctlSIOCOUTQ选项查询,如果这个数字达到 0,我们至少可以确认所有的发送数据到达了远程操作系统,只是只能在 Linux 平台下实现。

如何正确关闭TCP连接相关推荐

  1. linux 使用bash命令关闭TCP连接

    在进程中关闭tcp连接比较简单,直接调用socket的API即可关闭,或不发送心跳机制. 有时候为了测试异常环境,在不断网的情况下,如何使用bash命令关闭TCP的连接呢? 下面谈两种使用bash命令 ...

  2. 客户端能不等四次挥手就强制关闭 TCP 连接吗?

    在<Go 网络编程和 TCP 抓包实操>Conn.Close() 方法发起了关闭 TCP 连接的请求,这是一种默认的关闭连接方式. 默认关闭需要四次挥手的确认过程,这是一种"商量 ...

  3. 关闭TCP连接的学问

    从TCP协议角度来看,一个已建立的TCP连接有两种关闭方式,一种是正常关闭,即四次挥手关闭连接:还有一种则是异常关闭,我们通常称之为连接重置(RESET). 首先说一下正常关闭时四次挥手的状态变迁,关 ...

  4. 主动关闭TCP连接的一方为什么要有TIME_WAIT状态

    TCP连接是全双工通信,主动方和被动方都需要自主关闭通信链路,TCP正常情况下连接断开会进行四次挥手(流程如上图所示): 1.由主动断开方发起FIN 2.被动方回复ACK 3.待被动方数据传输完成,被 ...

  5. 关闭tcp连接时有时发送FIN有时发送RST

    使用http 发送同一个包,sleep 20 秒后再读取数据,htpp设置1s的超时时间,超时后会关闭socket ,可是有时发送RST,有时发送FIN,如下图: 后来google 了一把,发现 tc ...

  6. LwIP C TCP/IP Stack 正确的TCP连接数据发送姿态

    注意,本文提供的代码来自本人搞起耍的 netstack,有一些类似 tun2socks LwIP 实现,目前不会考虑集成到产品上面作为可选 TCP/IP 网络栈,当然不会是基于 go-gvisor.g ...

  7. php curl 关闭tcp连接,BASH CURL:顺序运行时,请勿关闭请求之间的连接

    我正在尝试编写一个BASH命令,该命令使用CURL将GET请求发送到两个不同的网页,但使用相同的连接.对我来说,就像向登录页面发送GET请求以向服务器进行身份验证,然后第二个请求模仿自动重定向到主页, ...

  8. 17.如何正确使用TCP

    netcat与socket文件描述符,stdin标准输入,stdout标准输出打交道 如何正确关闭tcp连接?tcp是可靠协议,如果程序不正确,传输数据可能出错/不完整 为什么IO多轮复用要和非阻塞i ...

  9. Linux网络协议栈:关闭一个还有没发送数据完的TCP连接

    <监视和调整Linux网络协议栈:接收数据> <监控和调整Linux网络协议栈的图解指南:接收数据> <Linux网络 - 数据包的接收过程> <Linux网 ...

最新文章

  1. 莹石云存储卡不兼容_继入股无锡好达之后,华为再度入股国产滤波器厂商德清华莹...
  2. 话说文件系统——aufs源码分析(三)【转】
  3. 答读者问(6):有关IT培训和毕业之前的迷茫等问题
  4. HTML5 API详解(15):History 不刷新也可以实现网页跳转
  5. C# 反射 设置字段值无效的解决办法
  6. [转]关于flash中图片(jpg\png\gif)旋转后锯齿(模糊)问题
  7. Linux与Ubuntu上SQL Server 2019
  8. 代码很烂,所以离职。
  9. 5.携程架构实践 --- 框架中间件
  10. linux git 撤销删除文件,git 撤销回滚学习
  11. java adt教程_用Eclipse安装ADT插件搭建Android环境(图文)
  12. 风景怡人一个生态村子 -国稻种芯-百色:华润谋定希望小镇
  13. Spring Boot-6-VO、PO
  14. html 简繁转换 批量,[推荐]几行代码轻松搞定网页的【简繁转换】
  15. 手淘抓包、 x-sign的签名算法和api接口
  16. 指出下列程序运行的结果()
  17. CSS学习笔记 07、2D与3D转换
  18. 【小程序模板】功能模块+红色招聘信息资讯小程序网页模板+行业职位招聘小程序+招聘信息网页下载
  19. workers.properties配置详解
  20. 性能起飞!驱动人生带你了解AMD FSR3.0及显卡驱动

热门文章

  1. 活着——活着就是对生命最好的尊重
  2. 关于ebay平台接口(php)对接示例
  3. mysql索引执行顺序_mysql索引及sql执行顺序
  4. Linux定时清理日志
  5. Android基础之十八显示gif动画,动态图片
  6. 【AI测试】人工智能测试整体介绍——第四部分
  7. python批量读取图片处理并保存
  8. jQuery中的使用end()方法
  9. 找不到com.sun.beans.introspect.PropertyInfo的类文件问题
  10. 在seo中,怎么写原创文章。