原文链接: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协议栈时会安全返回,并且如果在返回后修改文件也不会改变该调用的行为。

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

为什么TCP连接不可靠的更多相关文章

  1. 简述TCP连接的建立与释放(三次握手、四次挥手)

    在介绍TCP连接的建立与释放之前,先回顾一下相关知识. TCP是面向连接的运输层协议,它提供可靠交付的.全双工的.面向字节流的点对点服务.HTTP协议便是基于TCP协议实现的.(虽然作为应用层协议,H ...

  2. TCP连接与关闭

    1.建立连接协议(三次握手) (1)客户端发送一个带SYN标志的TCP报文到服务器.这是三次握手过程中的报文1. (2) 服务器端回应客户端的,这是三次握手中的第2个报文,这个报文同时带ACK标志和S ...

  3. TCP连接的状态详解以及故障排查

    我们通过了解 TCP各个状态 ,可以排除和定位网络或系统故障时大有帮助. 一.TCP状态 LISTENING :侦听来自远方的TCP端口的连接请求 . 首先服务端需要打开一个 socket 进行监听, ...

  4. TCP连接状态详解及TIME_WAIT过多的解决方法

    上图对排除和定位网络或系统故障时大有帮助,但是怎样牢牢地将这张图刻在脑中呢?那么你就一定要对这张图的每一个状态,及转换的过程有深刻地认识,不能只停留在一知半解之中.下面对这张图的11种状态详细解释一下 ...

  5. TCP连接的建立与释放(三次握手与四次挥手)

    TCP连接的建立与释放(三次握手与四次挥手) TCP是面向连接的运输层协议,它提供可靠交付的.全双工的.面向字节流的点对点服务.HTTP协议便是基于TCP协议实现的.(虽然作为应用层协议,HTTP协议 ...

  6. OSI七层协议与TCP连接

    概述 为了追求效率,我们写代码,不可能去关注底层知识,但往往到出了问题,或者性能调优.我们就会速手无策,仔细为自己查缺补漏,总结知识点. 网络协议 互联网的本质就是一系列的网络协议,让不同计算机能够互 ...

  7. TCP建立连接的三次握手和TCP连接断开的四次挥手

    1. TCP建立连接的3次握手 2. TCP断开连接的四次挥手 [注意]中断连接端可以是Client端,也可以是Server端. 图3—Client端主动发起关闭连接请求 1. 假设Client端主动 ...

  8. TCP连接 断开

     参考:http://blog.csdn.net/cyberhero/article/details/5827181 1.建立连接协议 (三次握手)      (1)客户端发送一个带SYN标志的TCP ...

  9. tcp连接是基于socket通信的吗

    https://zhidao.baidu.com/question/1305788160020716299.html ------ 网络七层协议 五层模型 TCP连接 HTTP连接 socket套接字 ...

随机推荐

  1. 关于jQuery源码分析

    http://www.w3ctech.com/topic/256 jQuery源码剖析(一)——概览&工具方法

  2. Database,Uva1592

    Peter studies the theory of relational databases. Table in the relational database consists of value ...

  3. redis 不能持久化问题 MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk.

    转载自:http://www.cnblogs.com/anny-1980/p/4582674.html kombu.exceptions.OperationalError: MISCONF Redis ...

  4. RMAN的实战篇--备份脚本

    案列一. 目标: 1.每天夜间1 点执行:2.数据库全备,同时备份控制文件及归档日志文件,备份文件保存至: /backup\目录下,并在完成归档日志文件备份后,自动删除已备份的归档日志:3.备份保留7 ...

  5. 解决JSP路径问题的方法(jsp文件开头path, basePath作用)

    原文:http://blog.csdn.net/mingxunzh/article/details/4627185 在JSP中的如果使用 "相对路径"  则有可能会出现问题. 因为 ...

  6. Android 四大组件之一(Activity)

    Activty的生命周期的也就是它所在进程的生命周期. 一个Activity的启动顺序: onCreate()——>onStart()——>onResume() 当另一个Activity启 ...

  7. HTML 水平线<hr/>标签

    定义和用法: <hr/>标签在HTML页面中创建一条水平线. 水平分隔线(horizontal rule)可以在视觉上将文档分隔成各个部分. HTML 与 XHTML 之间的差异 在 HT ...

  8. Javascript中对象的Obeject.defineProperty()方法-------------(ES5/个人理解)

    在讲到Obeject.defineProperty()方法之前先得说明一下ECMAScript中有两种属性:数据属性和访问器属性. 两种属性存在的意义:描述对象属性(key)的一些特性,因为这些属性是 ...

  9. 16 款最流行的 JavaScript 框架

    本文列举了16个当前最流行的JavaScript框架.在这个列表中,既包括jQuery和Mootools,也有Zepo移动JavaScript框架. 里面一定有你正在用的或想尝试用的JavaScrip ...

  10. iOS开发UI篇—多控制器和导航控制器简单介绍

    iOS开发UI篇—多控制器和导航控制器简单介绍 一.多控制器 一个iOS的app很少只由一个控制器组成,除非这个app极其简单.当app中有多个控制器的时候,我们就需要对这些控制器进行管理 有多个vi ...