为什么TCP连接不可靠

原文链接:http://watter1985.iteye.com/blog/1924977

原文在此

这篇文章是关于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, );
connect(sock, &remote, sizeof(remote));
write(sock, buffer, );// returns 1000000
close(sock);

B:

 int sock = socket(AF_INET, SOCK_STREAM, );
bind(sock, &local, sizeof(local));
listen(sock, );
int client=accept(sock, &local, locallen);
write(client, "220 Welcome\r\n", );
int bytesRead=, res; for(;;) {
res = read(client, buffer, );
if(res < ) {
perror("read");
exit();
}
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, );
connect(sock, &remote, sizeof(remote));
write(sock, buffer, );// returns 1000000
shutdown(sock, SHUT_WR);
for(;;) {
res=read(sock, buffer, );
if(res < ) {
perror("reading");
exit();
}
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协议栈时会安全返回,并且如果在返回后修改文件也不会改变该调用的行为。

请注意该函数不会等待所有的数据被确认,它只会等待数据都发送出去。

上一篇:阿里云Centos+Django+Nginx+uWSGI


下一篇:微信小程序开发《三》:微信小程序请求不能使用session的原因及解决办法