TCP 流协议和消息分帧的理解

来源:互联网 发布:手机淘宝5.10.3旧版本 编辑:程序博客网 时间:2024/06/08 13:51

TCP提供一种面向连接的、可靠的字节流服务。面向连接意味着两个使用TCP的应用(通常是一个客户和一个服务器)在彼此交换数据包之前必须先建立一个TCP连接。

应用数据被分割成TCP认为最适合发送的数据块。这和UDP完全不同,应用程序产生的数据长度将保持不变。由TCP传递给IP的信息单位称为报文段或段(segment)。

两个应用程序通过TCP连接交换8bit字节构成的字节流。TCP不在字节流中插入记录标识符。我们将这称为字节流服务(bytestreamservice)。如果一方的应用程序先传10字节,又传20字节,再传50字节,连接的另一方将无法了解发方每次发送了多少字节。只要自己的接收缓存没有塞满,TCP 接收方将有多少就收多少。一端将字节流放到TCP连接上,同样的字节流将出现在TCP连接的另一端。
另外,TCP对字节流的内容不作任何解释。TCP不知道传输的数据字节流是二进制数据,还是ASCⅡ字符、EBCDIC字符或者其他类型数据。对字节流的解释由TCP连接双方的应用层解释。
这种对字节流的处理方式与Unix操作系统对文件的处理方式很相似。Unix的内核对一个应用读或写的内容不作任何解释,而是交给应用程序处理。对Unix的内核来说,它无法区分一个二进制文件与一个文本文件。

以下节选 : 《TCP/IP高效编程:改善网络程序的44个技巧》

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

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

 图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不一定会将一条报文的全部内容都放在一个分组中传送出去。 图2-26 封装两条报文可能采用的4种方式

现在,我们从主机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); 

处理这种情况的标准方法,完成数据的完整接收:

int readn(SOCKET fd, char *buf, size_t len){     int cnt = len;     int recvlen;     recvlen = recv(fd, buf, cnt, 0)     if(recvlen < 0)     {
        /* EINTR: function was interrupted by a signal that was caught, before any data was available */            if(errno == EINTR)                  continue;             return -1;      }      /* EOF */      if(recvlen == 0)         return len - cnt;           buf += recvlen;      cnt -=  recvlen;   }   return len;}     

简单理解:

TCP是流协议,不区分流边界,如果不分帧,那么可能会发生你send了无数次,但是对方只接收一次的情况

client 发送字节流时,TCP会保证server 按顺序接收到全部的字节流,其他诸如数据包的大小等,TCP协议对我们来说是透明的,我们可以全部不考虑。

    通俗点说,我们发送数据只需要调用send函数,我们只需要关注send函数的返回值,从而知道了发送了多少个字节,在服务端,我们调用recv函数,我们只需要关注recv函数的返回值,从而知道接收了多少个字节,其他情况通通不管。

   在TCP通信过程中,我们不需要关心(也没法关心,但可以设置)数据包的大小,个数,我们只需要在客户端建立一个缓冲区不断发送,在服务端建立一个缓冲区不断接收就够了,当然,我们还可以定义一个包头,来实现诸如发送文件这样更强大的功能。

这就是TCP通信的本质,不会应平台的不同而改变




0 0