为什么TCP不可靠
原文在此
这篇文章是关于TCP网络编程的一个不起眼的小问题。几乎人人都并不太明白这个问题是怎么回事。曾经我以为我已经理解了,但在上周,我才发现我没有理解。
所以我决定在网络上搜索并咨询专家,希望他们留下他们智慧的轨迹从而一劳永逸,希望可以为这个主题画上休止符。
专家(H. Willstrand, Evgeniy Polyakov, Bill Fink, Ilpo Jarvinen, and Herbert Xu)做出了回应,这里我将他们总结到一起。
我甚至参考了很多Linux的TCP实现,这个问题并不是Linux特有的,可以产生在任何操作系统上。
问题是什么?
有时候,我们需要把未知大小的数据从一个地方传送到另一个地方。看起来TCP(可靠的传输控制协议)正是我们所需要的。以下是从 Linux 的手册页tcp(7)获取的联机帮助:
“TCP在ip(7)层(ipv4 和 ipv6)之上建立了一个连接两个套接字之间的,可靠的,面向流的,全双工连接。TCP保证数据按序到达并重传丢失的数据包。它生成并检查每一个数据包的校验和来捕获传输错误。“
然而,当我们天真地使用TCP发送需要传输的数据时,它经常不能按照我们的想法去做,有时最后的几千字节或几兆字节永远不会到达。
比如,我们在两个POSIX兼容操作系统上运行以下两个程序,程序A向程序B发送100万字节的数据(程序可以在这里找到):
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)
小于
1000000
的某个数字
C)
一条错误消息
D)
以上都有可能
可悲的是,正确的答案是“D”。但是,怎么可能出现这种情况?程序A已报告的所有数据已被正确送往!
发生了什么事?
通过TCP套接字发送数据不提供类型与向普通文件写入(你最好记得调用fsync())的”到达硬盘“(”it hit the disk“)的语义。
事实上,在TCP的世界里,write()成功的意思是内核已经接受了你的数据并将在内核高兴时尝试将其传输出去。甚至内核认为数据包已经发送了,数据也只是交给了网络适配器处理。网络适配器也许会在其高兴时,才真的将数据发送出去。
从这一点上来说,数据将遍历网络上的很多适配器和队列,直至数据到达远程主机。接收端的内核会发送应答,如果有进程正在尝试从socket中读取数据,数据才会到达应用程序,在对文件系统来说才真正的”到达硬盘“。
注意确认报文的发出只意味着内核已经收到数据,并不意味着应用收到了数据!
好吧,我知道了这些内容,但为什么没有在上面的例子中没有收到所有的数据?
当我们发起一个TCP / IP套接字的close()方法,根据具体情况,内核可能是这样做的:关闭socket以及与它关联的TCP/IP连接。
实际上是这样的:尽管有一些数据正等待发送,或已经发送但没有得到确认,内核仍然会关闭整个连接。这个问题已经导致了在邮件列表,新闻组和论坛上产生了大量的贴子。这些帖子很快就被SO_LINGER套接字选项解决了,似乎只有下面的这个问题了:
“启用时,在close(2)或shutdown(2)直到所有排队的消息都成功发送或超过逗留时间时才会返回。否则,调用立即返回关闭将在后台完成。当套接字由exit(2)关闭时,它
将总在后台逗留。”
所以,我们设置这个选项,重新运行我们的程序。它仍然不工作,并不是所有的数据都被接收。
怎么会呢?
事实证明,在这种情况下,RFC 1122中的第4.2.2.13告诉我们,close()调用时,如果有任何挂起的可读数据,可能会导致立即发送复位(rest)。
“主机可以实现”半双工“TCP关闭序列,使得调用close的应用程序,不能继续从连接读取数据。如果这样的主机在读取TCP中挂起的数据时调用 close,或者在调用close以后又有新数据大大是,TCP应该发送一个RST来表明数据已丢失。”
在我们的例子中,这样的数据被挂起:我们在程序B中发送“220 Welcome\r\n”,但从未在程序A中读取!如果该行尚未发送的程序B,它是最有可能的是,我们所有的数据已经正确到达。
所以,如果先读取数据,然后设置LINGER,这样就行了吗?
还不行。调用close()不会按照我们的想法去做:当所有的数据都被发送时关闭连接。
幸好有系统调shutdown()可供使用,这个系统调用做的正是这件事。然而,仅用这个系统调用是不够的。shutdown()方法返回时,仍然没有办法知道数据是否全部被B收到。
我们需要做的是调用shutdown()方法,这将导致发送一个FIN包到程序B。程序B将会关闭它的socket,然后在程序A中可以检测到对端的close:后续的read()将会返回0。
程序A现在变成了:
sock = socket(AF_INET, SOCK_STREAM, 0);
connect(sock, &remote, sizeof(remote));
write(sock, buffer, 1000000); // returns 1000000
shutdown(sock, SHUT_WR);
for(;;) {
res=read(sock, buffer, 4000);
if(res < 0) {
perror("reading");
exit(1);
}
if(!res)
break;
}
close(sock);
那么完美的解决方案是什么?
如果我们看看HTTP协议,数据通常与其长度信息一起发送,无论是在一个HTTP响应的开始,或是在发送信息的过程中(即所谓的“分块”模式)。
这样做是有原因的。因为只有这样,接收端才能确保所有的数据都已接收。
使用上述的shutdown()技术只告诉我们远端关闭了连接。实际上它并不保证所有的数据都被正确的接收了。
最好的建议是发送长度信息,并让远端程序主动确认所有的数据都已接收。
当然,这只在能够选择自己的协议时才能起作用。
还需要做些什么?
如果你需要通过”愚蠢的TCP / IP墙洞“来传递流式数据,我曾经做了很多次,也许无法按照圣人的建议携带长度信息并获取确认。
在这种情况下,接受接收端关闭socket来表明所有的数据都已接受可能不是一个好办法。
幸运的是,Linux能够追踪未确认的数据。未确认数据可以使用ioctl() SIOCOUTO 来获取。一旦发现这个数字为0,我们就可以确认数据至少到达了远端的操作系统。
与前面所述的 shutdown() 方法不同,SIOCOUTQ似乎是Linux特有的。欢迎其他操作系统的更新。
示例代码包含一个例子如何使用SIOCOUTQ的例子。
但是,怎么回事,它已经“正确工作”很多次了!
只要没有未读的挂起数据,星星和月亮配合的就很好。你使用某个特定的操作系统版本,你可能仍然忽略前面意想不到的错误。它通常可以工作,但是不要依赖他。
非阻塞套接字上的一些注意事项
很多通信方面的开发者,想要混合使用SO_LINGER和非阻塞套接字(O_NONBLOCK)。我想说的是:千万别这么做。请使用 shutdown() 然后读取 eof 来代替他。当然可以适当的使用 poll/epoll/select()。
关于Linux的sendfile()和splice()系统调用的一些话
值得注意的是,Linux的系统调用sendfile()和splice()非常适合做这件事。当这两个系统调用返回时,立即调用close()也不会出现问题,这两个系统调用会管理文件发送的内容。
实际上由于splice()(sendfile()是基于splice()的)给予零拷贝,能够确保数据包到达TCP协议栈时会安全返回,并且如果在返回后修改文件也不会该改变调用的行为。
请注意该函数不会等待所有的数据被确认,它只会等待数据都发送出去。
相关推荐
但这并不能直接关闭TCP连接,只能停止程序运行。 2. **命令提示符**:使用`netstat`命令可以查看当前的TCP连接状态。输入`netstat -ano`可以看到所有活动连接,包括PID(进程ID)。找到目标连接,然后使用`taskkill...
TCP(Transmission Control Protocol)是一种广泛使用的传输层协议,它在互联网通信中扮演着至关重要的角色。...正确地设置和查看TCP连接数,有助于确保系统能够有效地处理网络负载,为用户提供高效、稳定的网络服务。
TCP连接分为长连接和短连接,这两种连接方式各有其特点和适用场景。本Demo是用C++语言在VS2017环境下编写的,旨在帮助开发者理解TCP长连接和短连接的实现。 首先,我们要理解TCP连接的基本概念。TCP是一种面向连接...
这款工具可以帮助用户实时查看所有正在运行的进程与哪些远程主机建立了TCP连接,以及这些连接的状态、端口号等详细信息。在IT行业中,了解和掌握TCPView的使用对于网络故障排查、性能优化以及安全监控都有着重要的...
在使用 TCPCP 时,目的主机通过设置 TCP—I CI 套接字参数来创建新的连接端点,并将套接字参数 TCP—CP—FN 设置为 TCPCP—ACTI VATE 来激活这个连接。 TCPCP 还提供了许多外层 API 函数,应用程序可以通过这些函数...
1. TCP连接的原理:TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。在TCP连接建立之前,需要经过三次握手来确保双方都能正常通信。在连接过程中,每个TCP连接都有一个唯一的标识,即源IP地址、源端口号...
本主题将深入解析“简单的TCP连接”,适合初学者理解TCP连接的基本流程。 TCP是一种面向连接的、可靠的传输层协议,它确保了数据在网络中的正确传输。TCP连接的建立与关闭通常分为三个阶段:三次握手和四次挥手。 ...
本篇文章将详细讲解如何在Android中实现本地TCP连接,以及如何进行数据的发送与接收。 首先,我们需要了解TCP的基本原理。TCP是一种面向连接的、可靠的传输协议,它通过三次握手建立连接,确保数据能够按序、无丢失...
本篇将深入探讨C#中如何实现TCP连接,包括客户端和服务端的TCP收发信息。 首先,TCP是一种面向连接的、可靠的传输协议,它确保了数据的顺序传输和错误校验。在C#中,我们主要通过System.Net.Sockets命名空间中的...
在IT行业中,网络通信是至关重要的一个领域,TCP(传输控制协议)和UDP(用户数据报协议)是两种主要的传输层协议,它们为应用程序提供数据传输服务。本篇文章将详细探讨TCP和UDP连接,以及如何通过工具如TCPView来...
默认情况下,Windows对同时建立的TCP连接数量有所限制,这可能会影响到多任务并行处理,比如使用下载工具时的速度。本文将详细介绍如何修改Windows TCP/IP连接数以提高下载速度和网络性能。 首先,理解TCP/IP连接数...
TIME_WAIT 状态的存在是为了确保 TCP 连接的可靠终止和允许老的重复分节在网络中消逝。 了解 TCP 连接状态是非常重要的,因为它可以帮助我们更好地理解 TCP 协议的工作机理,并更好地解决网络连接问题。
本文将深入探讨“简单Tcp局域网连接”,主要关注C/S架构下的TCP连接实现。 C/S架构,即客户端/服务器架构,是一种常见的网络应用模式。在这种模式下,客户端应用程序发起请求,服务器端应用程序接收到请求后进行...
TCP连接迁移是一种技术,旨在提高网络服务器的可伸缩性和可靠性,特别是在服务器集群环境中。这种技术允许TCP连接在不被客户端感知的情况下,从一个节点迁移到另一个节点,以实现负载均衡。在Linux环境下,TCP连接...
本文将深入探讨如何利用Windows API来获取TCP连接信息,以帮助开发者更好地监控和管理系统的网络状态。 TCP是一种面向连接的、可靠的传输协议,它通过三次握手建立连接,并在数据传输过程中提供序列化、确认和错误...
当两台设备之间建立TCP连接后,Keep-Alive功能会在一段时间无数据交换时发送一个空的数据包(称为Keep-Alive probe)到对方,以确认连接是否仍然有效。如果在一定时间内未收到响应,TCP会重新发送probe,经过多次...
TCP(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,是互联网协议族的核心部分。在TCP文件传输中,确保数据的可靠性是关键,这包括了数据的顺序传输、错误检测与纠正以及...
本篇文章将详细探讨如何在Delphi编程环境中实现TCP连接的获取,并结合“tcp连接监控”这一主题,讲解相关的技术和实践方法。 首先,我们需要了解TCP连接的基本原理。TCP是一种面向连接的、可靠的、基于字节流的传输...
在IT领域,网络编程是不可或缺的一部分,而TCP(Transmission Control Protocol)作为一种面向连接的、可靠的传输层协议,广泛应用于各种互联网服务。多线程技术则是提高程序并发性能的有效手段,尤其在处理网络连接...