为什么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 为提供可靠性传输,实行“顺序控制”或“重发控制”机制。此外还具备“流控制(流量控制)”、“拥塞控制”、提高网络利用率等众多功能。 TCP有以下特点: TCP充分地实现了数据传输时各种控制功能,可以进行丢...
3. 通过多台电脑建立一台电脑的TCP连接,可以分析流量控制的实质。 实验步骤 1.在IP地址为 192.168.0.250的主机A的命令行窗口里输入telnet 218.65.113.46 会发现连接不上,因为23号端口没有打开。打开IP地址为218.65...
4) 服务器启动后,开启TCP:2021端口,UDP:2020端口,其中,TCP连接负责与用户交互,UDP负责传送文件; 5) 客户端启动后,连接指定服务器的TCP 2021端口,成功后,服务器端回复信息:“客户端IP地址:客户端端口号>...
TCP,全称Transfer Control Protocol,中文名为传输控制协议,它工作在OSI的传输层,提供面向连接的可靠传输服务。 IP层并不保证数据报一定被正确地递交到接收方,也不指示数据报的发送速度有多快。正是TCP负责既要...
TCP协议是在TCP/IP协议模型中的运输层中很重要的一个协议、负责处理主机端口和端口直接的数据传输。...TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双向通信的数据。(通过发送和接收窗口)
TCP和UDP协议是TCP/IP协议的核心。...而UDP则不为IP提供可靠性、流控或差错恢复功能。一般来说,TCP对应的是可靠性要求高的应用,而UDP对应的则是可靠性要求低、传输经济的应用。 好用的TCP UDP调试助手NetAssist.exe
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义
TCP:Transmission Control Protocol 传输控制协议TCP是一种面向连接(连接导向)的、可靠的、基于字节流的运输层(Transport layer)通信协议,由IETF的RFC 793说明(specified)。在简化的计算机网络OSI模型中,它...
本文详细分析了TCP三次握手、四次挥手wireshark抓包过程。。传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、...TCP假设它可以从较低级别的协议获得简单的,可能不可靠的数据报服务。
TCP协议是面向连接的协议,负责把信息划分成IP协议所能够处理的,也要能把接收到的包拼成一个完整的信息,实现网络数据在运输层的可靠传输。TCP链路的建立和数据通信通过三次握手和四次挥手完成。为实现TCP数据包...
TCP三次握手,TCP三次握手是TCP连接建立过程的可靠性保证
传输控制协议(Transport Control Protocol)是一种面向连接的,可靠的传输层协议。面向连接是指一次正常的TCP传输需要通过在TCP客户端和TCP服务端建立特定的虚电路连接来完成,该过程通常被称为“三次握手”。可靠性...
tcp和udp的区别 TCP(Transmission Control Protocol)和UDP(User Datagram Protocol)是两种在网络通信中常用的传输层协议,它们有... - UDP的传输效率相对较高,因为它没有提供可靠性保证,不需要维护连接状态或
TCP是提供面向连接的,可靠的字节流服务。面向连接是TCP在正式进行通讯之前首先通过一些握手机制确认双方通讯意向并建立一条认为可以传输的概念通道。字节流服务是TCP传输的最小单位为字节,认为字节是有意义的单位。...
其中最重要的区别就是一个面向连接另外一个不是,这个区别就导致了他们是否能够保证稳定传输,显然不面向连接的 UDP 是没办法保证可靠传输的,他只能靠底层的网络层和链路层来保证。我们都知道网络层采用的是不...
TCP协议工作在OSI的传输层,是一种可靠的面向连接的数据流协议,TCP之所以可靠,是因为它保证了传送数据包的顺序。顺序是用一个序列号来保证的。响应包内也包括一个序列号,表示接收方准备好这个序列号的包。在TCP...
一旦通信双方建立了TCP连接,连接中的任何一方都能向对方发送数据和接收对方发送来的数据。发送数据时,程序员可以通过程序不断将数据流陆续写入TCP的发送缓冲区中,然后TCP自动从发送缓冲区中取出一定数量的数据,...
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层...不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换
传输控制协议(TCP,Transmission Control Protocol)是一...TCP假设它可以从较低级别的协议获得简单的,可能不可靠的数据报服务。 原则上,TCP应该能够在从硬线连接到分组交换或电路交换网络的各种通信系统之上操作。