1. TCP是一种流协议(stream protocol)

这就意味着数据是以字节流的形式传递给接收者的,没有固有的”报文”或”报文边界”的概念。从这方面来说,读取TCP数据就像从串行端口读取数据一样–无法预先得知在一次指定的读调用中会返回多少字节。

为了说明这一点,我们假设在主机A和主机B的应用程序之间有一条TCP连接,主机A上的应用程序向主机B发送一条报文。进一步假设主机A有两条报文要发送,并两次调用send来发送,每条报文调用一次。很自然就会想到从主机A向主机B发送的两条报文是作为两个独立实体,在各自的分组中发送的,如图2-25所示。

但不幸的是,实际的数据传输过程很可能不会遵循这个模型。主机A上的应用程序会调用send,我们假设这条写操作的数据被封装在一个分组中传送给B。实际上,send通常只是将数据复制到主机A的TCP/IP栈中,就返回了。由TCP来决定(如果有的话)需要立即发送多少数据。做这种决定的过程很复杂,取决于很多因素,比如发送窗口(当时主机B能够接收的数据量),拥塞窗口(对网络拥塞的估计),路径上的最大传输单元(沿着主机A和B之间的网络路径一次可以传输的最大数据量),以及连接的输出队列中有多少数据。更多与此有关的内容请参见技巧15。图2-26只显示了主机A的TCP封装数据时可能使用的诸多方法中的4种。在图2-26中,M11和M12表示M1的第一和第二部分,M21和M22与之类似。如图2-26所示,TCP不一定会将一条报文的全部内容都放在一个分组中传送出去。

现在,我们从主机B应用程序的角度来看这种情形。总的来说,主机B应用程序任意一次调用recv时,都不会对TCP发送给它的数据量做任何假设。比如,当主机B应用程序读取第一条报文时,可能会出现下列4种结果。

实际上,可能的结果不止4种,但我们忽略了出错和EOF之类的结果。我们还假设应用程序读取了所有可读的数据。

  1. 没有数据可读,应用程序阻塞,或者recv返回一条指示说明没有数据可读。到底会发生什么情况取决于套接字是否标识为阻塞,以及主机B的操作系统为系统调用recv指定了什么样的语义。
  2. 应用程序获取了报文M1中的部分而不是全部数据。比如,发送端TCP像图2-26D那样对数据进行分组就会发生这种情况。
  3. 应用程序获取了报文M1中所有的数据,除此之外没有任何其他内容。如果像图2-26A那样对数据分组就会发生这种情况。
  4. 应用程序获取了报文M1的所有数据,以及报文M2的部分或全部数据。如果像图2-26B或图2-26C那样对数据进行分组就会发生这种情况。

注意,这里还有一个定时问题。如果主机B的应用程序在主机A发送了第二条报文之后一段时间内都没有读取第一条报文,那么这两条报文都会成为可读的。这就和图2-26B所示情况相同了。这些描述说明,通常,在任意指定时刻,可读的数据量都是不确定的。
需要再次说明的是,TCP是一个流协议(stream protocol),尽管数据是以IP分组的形式传输的,但分组中的数据量与send调用中传送给TCP多少数据并没有直接关系。而且,接收程序也没有什么可靠的方法可以判断数据是如何分组的,因为在两次recv调用之间可能会有多个分组到来。

即使接收端应用程序的响应非常及时,也可能会发生这种情况。例如,一个分组丢失了(参见技巧12,在当今的因特网中,这是非常常见的情况),而且后继分组都安全到达,TCP会将后继分组中的数据保存起来,直到重传第一个分组并正确收到为止。此时,所有数据对应用程序都是可用的。

TCP会记录它发送了多少字节,以及确认的字节,但它不会记录这些字节是如何分组的。实际上,有些实现在重传丢失分组的时候传送的数据可能比原来的多一些或少一些。这就足以支撑下面再次重复说明的内容了。

对TCP应用程序来说,就没有”分组”这种概念。如果应用程序的设计与TCP对数据的分组方式有所关联,就应该考虑重新设计这个应用程序了。

既然任意一次指定的读操作中返回的数据量都是不可预测的,就必须在应用程序中做好应对这种情况的准备。通常这不是什么问题。比如说,我们可能在用fgets这样标准的I/O库程序读取数据。在这种情况下,fgets会将字节流划分成行。图3-6显示了一个这样的例子。在其他情况下的确需要关注报文边界问题,而这些情况下边界都是由应用程序级维护的。
最简单的情况就是定长报文。在这种情况下,只需要读取报文中固定数量的字节就可以了。根据前面的讨论,读操作返回的字节数可能小于sizeof(msg)(图2-26D),所以只进行

recv(s, msg, sizeof(msg), 0);

这样的简单调用是不够的。图2-27显示了处理这种情况的标准方法。

函数readn的用法与read非常相似,但在读到len字节,并从对等实体收到EOF,或出现错误之前,它是不会返回的。我们将其定义如下。

#include "etcp.h"
int readn( SOCKET s, char *buf, size_t len );

readn使用的逻辑与从串行端口,或者从其他基于流的、在任意指定时间内可读取数据量都未知的源端,读取指定数量的字节所使用的逻辑一样,这不足为奇。实际上,在所有这些情况下都可以,也经常使用readn(用int代替SOCKET,用read代替recv)。

如果recv调用被信号中断,第11行和第12行的if语句

if ( error == EINTR)        /* interrupted? */   continue;               /* restart the read*/

会重启recv调用。有些系统会自动重启被中断的系统调用,这种系统就不需要这两行程序了。从另一个角度来看,这两行代码也不会带来什么问题,因此为了实现最大限度的可移植性,最好还是把它们放在那里。

对必须支持可变长报文的应用程序来说,有两种可用的方法。第一种,可以用记录结束标记来分隔记录。如前所述,使用fgets这样标准的I/O程序将报文分成单个行时,就会发生这种情况。使用标准I/O程序时,很自然地会将新行作为记录结束标记使用。但使用这种方法通常会有一些问题。首先,除非在报文主体中从未用到记录结束标记,否则发送程序就要在报文中扫描这些标记,对其进行转义,或者编码,以免将其误认作记录结束标记。比如,如果将记录分隔字符RS作为记录结束标记使用,发送端就要搜索报文主体,找到所有RS字符,并对其进行转义,比如在前面加上一个\。这就意味着要转移数据以便为转义字符腾出位置。当然,还要对出现的所有转义字符进行转义。因此,如果用\作转义字符的话,就要将报文主体中出现的所有\都改成\。

在接收端,必须再次对整条报文进行扫描,这次要移除转义字符,并搜索(未转义的)记录结束标记。使用记录结束标记要对整条报文扫描两次,所以最好只在那些有”自然”记录结束标记的情况下使用,比如用换行符分隔文本行记录的时候。

另外一种处理可变记录的方法是在每条报文前面加上一个首部,这个首部(至少)包含下面的报文长度,如图2-28所示。(比如http协议的Content-Length)

读取记录长度

6-8 将记录长度读入reclen中,如果readn返回的长度不等于interger类型的大小,readvrec就返回0(EOF),如果出错就返回 1。

9 将记录长度从网络字节序转换为主机字节序。更多相关内容请参见技巧28。

查看是否装得下记录

10-27 查看调用程序的缓冲区大小,验证它能否装下整条记录。如果缓冲区中没有足够的空间,就依次将长度为len的片段读入缓冲区,并将记录丢弃。丢弃记录之后,将errno设置为EMSGSIZE,readvrec返回 1。

读取记录

29-32 最后,读取记录本身。根据readn返回的是错误、不足计数还是成功返回,readvrec会向调用程序返回 1、0或者reclen。

readvrec是个很有用的函数,会在其他一些技巧中用到,所以将其定义记录如下。

#include "etcp. h"
int readvrec( SOCKET s, char *buf, size_t len );
// 返回:读取的字节数,或者在出错时返回-1

图2-30显示了一个用readvrec从TCP连接中读取变长记录,并将其写入stdout的简单服务器代码。


10-17 初始化服务器,并接受一个连接。

20-24 调用readvrec读取下一个变长记录。如果出错,打印一条诊断信息,并读取下一条记录。如果readvrec返回一个EOF,就打印一条提示消息,服务器退出。

26 将记录写入stdout。

图2-31显示了对应的客户端程序,这个程序从其标准输入读取报文,附加上报文长度,然后将其发送给服务器。

定义分组结构

6-10 定义packet结构,调用send时用它来装载报文及其长度。数据类型u_int32_t是一个无符号的32比特整数。

连接、读取并逐行发送

12 客户端通过调用tcp_client连接到服务器。

13-21 调用fgets从stdin中读取一行数据,并将其放入报文分组的buf字段中。行的长度由对strlen的调用决定,将这个值转换成网络字节序后,放入报文分组的reclen字段中。最后,调用send向服务器发送分组。

发送这些由两个或多个部分组成的报文的另一种方法请参见技巧24。

在sparc上启动服务器vrs,然后在bsd上启动客户端vrc,来测试这些程序。将程序的运行并排显示出来,就可以看到客户端的输入,以及相应的服务器输出了。第4行还对错误消息进行了换行显示。

服务器缓冲区有10字节,所以发送11字节1, …, 0, 时,readvrec会返回一条错误。

小结

初级网络程序员最常犯的错误之一就是无法理解TCP传送的是一个没有记录边界概念的字节流。这一点很重要,可以总结为TCP中没有用户可见的”分组”概念,它只是传送了一个字节流,我们无法准确地预测在一个特定的读操作中会返回多少字节。

2. TCP粘包和拆包

  • TCP传输数据时,会根据底层的TCP缓存区实际情况进行数据包划分:
  • 1.业务上定义的完整数据(比方说一个完整的json串),可能会被TCP拆分成多个数据包进行发送(拆包)。
  • 2.业务上特殊含义的独立数据,也有可能因为大小或者缓冲区原因,被TCP封装成一个大数据包发送(粘包)。

3. 参考

《TCP/IP高效编程 改善网络程序的44个技巧》–技巧6:记住TCP是一种流协议

记住,TCP是一种流协议相关推荐

  1. Linux 网络编程详解四(流协议与粘包)

    TCP/IP协议是一种流协议,流协议是字节流,只有开始和结束,包与包之间没有边界,所以容易产生粘包,但是不会丢包. UDP/IP协议是数据报,有边界,不存在粘包,但是可能丢包. 产生粘包问题的原因 1 ...

  2. Mocha NTA基于单采集器实现的多种流协议分析

    业内主流的Flow协议技术         网络业界基于流(Flow)的分析技术主要有NetFlow.sFlow.cFlow和NetStreem四种.NetFlow是Cisco公司的独有技术,它既是一 ...

  3. 局域网中最常用的三种网络协议简述

    目录 一.NETBEUI 二.IPX/SPX 三.TCP/IP v搜索公众号:zhulin1028.后台回复: [java经典源码][java手册][java全栈][java全栈][快速开发框架] [ ...

  4. 基于TCP流协议的数据包通讯

    Fanxiushu   2016-02-04,引用或转载请注明原始作者. TCP通讯是流协议,它不像UDP那样基于包为边界的通讯方式, TCP流式协议,举个简单例子,一端用send 分别发送 100, ...

  5. 重学TCP协议(8) TCP的11种状态

    TCP的11种状态 为了逻辑更加清晰,假设主动打开连接和关闭连接皆为客户端,被动打开连接和关闭连接皆为服务端 客户端独有的:(1)SYN_SENT (2)FIN_WAIT1 (3)FIN_WAIT2 ...

  6. wireshark的使用教程--用实践的方式帮助我们理解TCP/IP中的各个协议是如何工作的

     wireshark的使用教程 --用实践的方式帮助我们理解TCP/IP中的各个协议是如何工作的 wireshark是一款抓包软件,比较易用,在平常可以利用它抓包,分析协议或者监控网络,是一个比较好的 ...

  7. 网络:TCP/IP各层的协议

    教程书上虽然介绍了这些协议,但是就单个协议分别介绍的,很容易忘了这个协议属于那一层,以及属于底层哪个协议. 上图TCP/IP各层的协议很多,一张图就可以记住了. 应用层协议 0.  运行 在TCP\U ...

  8. 计算机网络体系结构(OSI七层、TCP/IP四层、五层协议)

    1.概述 2. 五层协议 2.1 应用层 为特定应用程序提供数据传输服务,例如 HTTP.DNS 等.数据单位为报文. 2.2 运输层 提供的是进程间的通用数据传输服务.由于应用层协议很多,定义通用的 ...

  9. OSI 七层模型和TCP/IP模型及对应协议

    OSI 七层模型和TCP/IP模型及对应协议图: 完成中继功能的节点通常称为中继系统.在OSI七层模型中,处于不同层的中继系统具有不同的名称. 一个设备工作在哪一层,关键看它工作时利用哪一层的数据头部 ...

最新文章

  1. Asterisk安装
  2. 面试官:你都工作3年了,连选择排序法都不会,我怎么能选择你
  3. 《Python数据分析》-Ch01 Python 程序库入门
  4. linux 下开源常见监控软件
  5. 自己使用的Android框架
  6. java 飞信接口_java 飞信接口
  7. C语言二分法在一个有序数组查找数的算法(附完整源码)
  8. Python中List的复制(直接复制、浅拷贝、深拷贝)
  9. Netty--Reactor模式
  10. 【QGIS入门实战精品教程】2.1:初识QGIS软件
  11. java获取参数编码_java获取接口数据编码问题
  12. Wpf之无法获取鼠标点击事件
  13. 华为ICT大赛省赛网络赛道
  14. 各软件官网下载地址合集
  15. python藏头诗_Python简单实现表白藏头诗-Go语言中文社区
  16. 如何用CMD查看电脑详细配置
  17. 【Redis踩坑日记】Redis由于目标计算机积极拒绝,无法连接
  18. 什么是Autorun病毒?它的运作原理是什么?如何手工清除?
  19. Node.js内置模块 events的基本使用
  20. BZOJ5011 [Jx2017]颜色(洛谷P4065)

热门文章

  1. JavaScript基本数据类型讲解
  2. 第二篇:稳定性之如何有条不紊地应对风险?
  3. 一文了解 Kubernetes 中的服务发现
  4. curl -s http://192.168.232.191/openapi/v2 | jq 不显示JSON格式的文档说明
  5. 10虚拟机的删除和迁移
  6. market1501正则表达式提取行人id和相机id
  7. datetimepicker中文不生效_搜索引擎技术(二十)- elasticsearch - 中文分词器
  8. Java 算法 换零钞
  9. python list清理列表中的空元素或特定元素
  10. 日志显示TypeError: Failed to fetch报错与TypeError: NetworkError when attempting to fetch resource报错