目录
引言
运输层位于应用层和网络层之间,是分层的网络体系结构的重要部分。该层为运行在不同主机上的应用进程提供直接的通信服务起着至关重要的作用。
7.1 概述和运输层服务
运输层协议为运行在不同主机上的应用进程之间提供了逻辑通信(logic communication)功能
从应用程序的角度看:
通过逻辑通信,运行不同进程的主机好像直接相连一样;实际上,这些主机也许位于地球的两侧,通过很多路由器及多种不同类型的链路相连。
应用进程使用运输层提供的逻辑通信功能彼此发送报文,而无需考虑承载这些报文的物理基础设施的细节。因为运输层协议是在端系统中而不是在路由器中实现的。
在发送端:
运输层将从发送应用程序进程接收到的报文转换成运输层分组,用因特网术语来讲该分组称为运输层报文段(segment)。实现的方法(可能)是将应用报文划分为较小的块,并为每块加上一个运输层首部以生成运输层报文段。
然后,在发送端系统中,运输层将这些报文段传递给网络层,网路层将其封装成网络层分组(即数据报)并向目的地发送。注意到下列事实是重要的:网络路由器仅作用于该数据报的网络层字段;即它们不检查封装在该数据报的运输层报文段的字段。
在接收端:
网络层从数据报中提取运输层报文段,并将该报文段向上交给运输层。
运输层则处理接收到的报文段,使该报文段中的数据为接收应用进程使用。
网络应用程序可以使用多种的运输层协议。例如,因特网有两种协议,即TCP和UDP。每种协议都能为调用的应用程序提供一组不同的运输层服务。
7.2 socket 的作用和存在意义
因为 TCP 是面向连接的协议,所以发送和接收数据的时候,都要指定收发双方的 IP 地址和端口号;
- IP 地址是对计算机终端在网络中位置的唯一标识,通过 IP 地址可以找到网络中的某台计算机
- 端口号 port 是对这台计算机中的某个应用服务进程进行唯一标定,让数据能够在计算机内部找到对应的应用进程
因为 TCP 的字节流需要向下交付到网络层,然后进行打包成 IP 数据报再发送,如果没有 socket,那么数据每次向下传输到网络层的时候都需要附带传一个 5元组,这样增大了层与层之间的数据压力,而且每次都如此很麻烦,有了 socket 就相当于把这个 5 元组通过 socket 打包成了一个值,每次向网络层传递这个值,网络层就明白了,原来是想往这个目标地址和端口号发送。
7.3 TCP连接
TCP被称为是面向连接的(connection-oriented)
这是因为在一个应用进程可以开始向另一个应用进程发送数据之前,这两个进程必须先相互“握手”,即它们必须相互发送某些预备报文段,以建立确保数据传输的参数。作为TCP连接建立的一部分,连接的双方都将初始化与TCP连接相关的许多TCP状态变量。
TCP的连接不是物理连接,TCP的连接的状态完全保留在两个端系统中(只在端系统中运行):
中间的网络元素(路由器和链路层交换机)不会维持TCP连接状态。事实上,中间路由器对TCP连接完全视而不见,它们看到的是数据报,而不是连接。
TCP连接提供的是全双工服务(full-duplex service):
如果一台主机上的进程A与另一台主机上的进程B存在一条TCP连接,那么应用层数据就可在从进程B流向进程A的同时,也从进程A流向进程B。
TCP连接也总是点对点(point-to-point)的:
即在单个发送方与单个接收方之间的连接。所谓“多播”,即在一次发送操作中,从一个发送方将数据传送给多个接收方,“多播”对TCP来说这是不可能的。
我们现在来看看TCP连接是怎样建立的:
该客户应用进程首先要通知客户运输层,它想与服务器上的一个进程建立一条连接。
客户首先发送一个特殊的TCP报文段。
服务器用另一个特殊的TCP报文段来响应。
最后,客户再用第三个特殊报文段作为响应。前两个报文段不承载“有效载荷”,也就是不包含应用层数据;而第三个报文段可以承载有效载荷。由于在这两台主机之间发送了3个报文段,所以这种连接建立过程常被称为三次握手(three-way handshake)。
一旦建立起一条 TCP连接,两个应用进程之间就可以相互发送数据了。我们考虑一下从客户进程向服务器进程发送数据的情况:
客户进程通过套接字(该进程之门)传递数据流。数据一旦通过该门,它就由客户中运行的TCP控制了。
TCP将这些数据引导到该连接的发送缓存(send buffer)里,发送缓存是在三次握手初期设置的缓存之一。
接下来TCP就会不时从发送缓存里取出一块数据。TCP可从缓存中取出并放入报文段中的数据数量受限于最大报文段长度(Maximum Segment Size,MSS)。MSS通常根据最初确定的由本地发送主机发送的最大链路层帧长度(即所谓的最大传输单元(Maximum Transmission Unit,MTU))来设置。设置该MSS要保证一个TCP报文段(当封装在一个IP数据报中)加上TCP/IP首部长度(通常40字节)将适合单个链路层帧。以太网和PPP链路层协议都具有1500字节的MTU,因此MSS的典型值为1460字节。注意到MSS是指在报文段里应用层数据的最大长度,而不是指包括TCP首部的TCP报文段的最大长度。
TCP为每块客户数据配上一个TCP首部,从而形成多个TCP报文段(TCP segment)。这些报文段被下传给网络层,网络层将其分别封装在网络层IP数据报中。然后这些IP数据报被发送到网络中。当TCP在另一端接收到一个报文段后,该报文段的数据就被放入该TCP连接的接收缓存中。应用程序从此缓存中读取数据流。TCP连接的每一端都有各自的发送缓存和接收缓存。
总结,TCP连接的组成包括:
一台主机上的缓存、变量和与进程连接的套接字,以及另一台主机上的另一组缓存、变量和与进程连接的套接字。
我们现在来看看TCP连接是怎样断开的:
四次挥手是TCP 连接断开的时候采用的方式
- 通信的双方都可以发起连接断开的请求
- 这个过程中使用的标志位是 FIN
- A 首先发送给 B 一个断开的请求 FIN=1
- B 接收请求并回复 ACK 表示 B 收到了 A 的断开请求
- A 关闭对 B 的数据传送;但是这个时候 B 依然可以发消息给 A,A 也可以发内容给 B,但是不能发送数据了(发送的内容可以包含 TCP head 的其他部分)
- 这时候如果 B 也想断开连接了,那么 B 也发送一个 FIN=1 给 A,A 接收之后回复一个 ACK,那么这个时候 A,B 之间的连接彻底中断(这个 connection 断掉)
- 当 A 或者 B 处在单方面断开的情况我们称之为 half-closed 即半关闭状态,在上文的描述中,当 A 主动发送 FIN 并收到 B 的 ACK 之后,依然可以接收和回复 B 的状态就处于 half-closed 状态。
- 再重申一遍,当一方发送 FIN 的时候并不代表他对接收方的 connection 断掉了,而仅仅是关闭数据发送,但是依然有方式回复对方的(数据之外)的请求
总结四次挥手:
使用到的标志位有:
- ACK 确认收到了对方的终止连接请求
- FIN 表示主动想要结束连接的请求
- 当一方的 FIN 请求得到 ACK 时,整个连接处于 half-closed 的状态
但其实四次挥手依然存在问题:
当最后一次 ACK 出现丢失的问题,那么这个问题其实没有什么好的办法解决
TCP连接突然终止:
在本图的例子中,A 已经成功地接收到 B 的 ACK 消息,然后 A 端的程序崩溃,这个时候 A 会向对方发送 RST 来表示让对方释放此连接的资源
RST 是一个单方向的任务,也就是说当 B 收到了 A 的 RST 之后不会通过 ACK 来确认这个 RST
因此 RST 的传输是不可靠的,我们一般不会使用 RST。
7.3.1 TCP 报文段结构
TCP 报文段由首部字段和一个数据字段组成。数据字段包含一块应用数据。
TCP首部:
包括源端口号和目的端口号,它被用于多路复用/分解来自或送到上层应用的数据。
也包括检验和字段(check sumfield)。
32比特的序号字段(sequence number field)和32比特的确认号字段(acknowl- edgment number field)。这些字段被TCP发送方和接收方用来实现可靠数据传输服务。
16 比特的接收窗口字段(receive window field),该字段用于流量控制。该字段用于指示接收方愿意接受的字节数量。
4比特的首部长度字段(header length field), 该字段指示了以32比特的字为单位的TCP首部长度。由于TCP选项字段的原因,TCP首部的长度是可变的。(通常,选项字段为空,所以TCP首部的典型长度就是20字节。)
可选与变长的选项字段(optionsfield),该字段用于发送方与接收方协商最大报文段长度(MSS)时,或在高速网络环境下用作窗口调节因子时使用。首部字段中还定义了一个时间戳选项。
6比特的标志字段(flag field)。
- ACK比特用于指示确认字段中的值是有效的,即该报文段包括一个对已被成功接收报文段的确认。RST、SYN和FIN比特用于连接建立和拆除。
- 当PSH比特被设置的时候,就指示接收方应立即将数据交给上层。
- URG比特用来指示报文段里存在着被发送端的上层实体置为“紧急”的数据。紧急数据的最后一个字节由16比特的紧急数据指针字段指出。当紧急数据存在并给出指向紧急数据尾的指针的时候,TCP必须通知接收端的上层实体。
7.3.2 可靠数据传输
不可靠数据传输:
因特网的网络层服务(IP 服务)是不可靠的。IP不保证数据报的交付,不保证数据报的按序交付,也不保证数据报中数据的完整性。对于IP服务,数据报能够溢出路由器缓存而永远不能到达目的地,数据报也可能是乱序到达,而且数据报中的比特可能损坏(由0变为1或者相反)。由于运输层报文段是被IP数据报携带着在网络中传输的,所以运输层的报文段也会遇到这些问题。
可靠数据传输:
TCP在IP不可靠的尽力而为服务之上创建了一种可靠数据传输服务(reliable data transfer service)。TCP的可靠数据传输服务确保一个进程从其接收缓存中读出的数据流是无损坏、无间隔、非冗余和按序的数据流;即该字节流与连接的另一方端系统发送出的字节流是完全相同。
一个TCP发送方的高度简化的描述:
该发送方只用超时来恢复报文段的丢失;
TCP发送方有3个与发送和重传有关的主要事件:
- 从上层应用程序接收数据;
- 定时器超时;
- 收到ACK。
一旦第一个主要事件发生,TCP从应用程序接收数据,将数据封装在一个报文段中,并把该报文段交给IP。注意到每一个报文段都包含一个序号,这个序号就是该报文段第一个数据字节的字节流编号。还要注意到如果定时器还没有为某些其他报文段而运行,则当报文段被传给IP时,TCP就启动该定时器。
第二个主要事件是超时。TCP通过重传引起超时的报文段来响应超时事件。然后TCP重启定时器。
第三个主要事件是一个来自接收方的确认报文段(ACK)的到达(更确切地说是一个包含了有效ACK字段值的报文段)。当该事件发生时,TCP将ACK的值y与它的变量SendBase进行比较。TCP状态变量SendBase是最早未被确认的字节的序号。(因此SendBase-1是指接收方已正确按序接收到的数据的最后一个字节的序号。)如前面指出的那样,TCP采用累积确认,所以y确认了字节编号在y之前的所有字节都已经收到。如果y > SendBase,则该ACK是在确认一个或多个先前未被确认的报文段。因此发送方更新它的SendBase变量;如果当前有未被确认的报文段,TCP还要重新启动定时器。
一个超时重传的简易算法:
发送方:
- 对于待发的字节设置一个定时器 timer。
- 当 timer 的时间耗尽如果还是没有收到 ACK,那么这个消息就会被重传并且重新设置定时器。
- 重复这个过程直到收到 ACK 为止。
接收方:
- 接收到一个 packet p。
- 检查这个 p 是否能够通过检验和(checksum)的检测;如果通过检测,那么证明这个 packet 的完整性有保证,就会给 S 端发送一个 ACK 表明自己收到了。
- 如果当前的字节没有被 app 取走使用,就将字节交付给上层的 app。
我们定义一个 RTT 为从 S 端发出一个消息经过网络达到 R 端的时间 + R 端发出一个 ACK 这个消息到达 S 端的时间。
过短的超时时间:
如果 Timeout 的时间小于 RTT 就会导致冗余的重发,因为即使是没有丢包的情况下,RTT 的时间也是信息在网络中跑一个来回的最短时间了,因此这样的设定不合适。
过长的超时时间:
如果 Timeout 的时间远大于 RTT,那么即使真的丢数据了,S 也会在很久之后才意识到要重传这个 packet ,会使得整个系统的效率变得很低。
动态调整的超时时间:
- 现在我们知道网络环境是很复杂的,如果贸然设置一个较大或者较小的网络超时时间是不符合常理的,因此我们想让这个超时时间的设定根据实际情况自己做出调整。
- 设置每个被发送的 TCP segment 都有一个重传timer(retransmission timer RTO):最开始使用默认值来初始化这个 RTO,然后根据网络的实际情况做出调整。
7.3.2.1 一些有趣的情况
我们刚刚描述了一个关于TCP如何提供可靠数据传输的高度简化的版本。但即使这种高度简化的版本,仍然存在着许多微妙之处。为了较好地感受该协议的工作过程,我们来看几种简单情况。
上图描述了第一种情况,主机A向主机B发送一个报文段。假设该报文段的序号是92,而且包含8字节数据。在发出该报文段之后,主机A等待一个来自主机B的确认号为100的报文段。虽然A发出的报文段在主机B上被收到,但从主机B 发往主机A的确认报文丢失了。
在这种情况下,超时事件就会发生,主机A会重传相同的报文段。当然,当主机B收到该重传的报文段时,它将通过序号发现该报文段包含了早已收到的数据。因此,主机B中的TCP将丢弃该重传的报文段中的这些字节。
在第二种情况中,如上图所示,主机A连续发回了两个报文段。第一个报文段序号是92,包含8字节数据;第二个报文段序号是100,包含20字节数据。假设两个报文段都完好无损地到达主机B,并且主机B为每一个报文段分别发送一个确认。第一个确认报文的确认号是100,第二个确认报文的确认号是120。现在假设在超时之前这两个报文段中没有一个确认报文到达主机A。当超时事件发生时,主机A重传序号92的第一个报文段,并重启定时器。只要第二个报文段的ACK在新的超时发生以前到达,则第二个报文段将不会被重传。
在第三种也是最后一种情况中(上图所示),假设主机A与在第二种情况中完全一样,发送两个报文段。第一个报文段的确认报文在网络丢失,但在超时事件发生之前主机A收到一个确认号为120的确认报文。主机A因而知道主机B已经收到了序号为119及之前的所有字节;所以主机A不会重传这两个报文段中的任何一个。
7.3.2.2 超时间隔加倍
我们现在讨论一下在大多数TCP实现中所做的一些修改。首先关注的是在定时器时限过期后超时间隔的长度。在这种修改中,每当超时事件发生时,如前所述,TCP重传具有最小序号的还未被确认的报文段。只是每次TCP重传时都会将下一次的超时间隔设为先前值的两倍,而不是用从EstimatedRTT和DevRTT推算出的值。
例如,假设当定时器第一次过期时,TimeoutInterval是0.75秒。TCP就会重传该报文段,并把新的过期时间设置为1.5秒。如果1.5秒后定时器又过期了,则TCP将再次重传该报文段,并把过期时间设置为3.0秒。
然而,每当定时器在另两个事件(即收到上层应用的数据和收到ACK)中的任意一个启动时,TimeoutInterval由最近的EstimatedRTT值与DevRTT值推算得到。这种修改提供了一个形式受限的拥塞控制。定时器过期很可能是由网络拥塞引起的,即太多的分组到达源与目的地之间路径上的一台(或多台)路由器的队列中,造成分组丢失或长时间的排队时延。在拥塞的时候,如果源持续重传分组,会使拥塞更加严重。相反, TCP使用更文雅的方式,每个发送方的重传都是经过越来越长的时间间隔后进行的。
7.3.2.3 快速重传
超时触发重传存在的问题之一是超时周期可能相对较长。当一个报文段丢失时,这种长超时周期迫使发送方延迟重传丢失的分组,因而增加了端到端时延。幸运的是,发送方通常可在超时事件发生之前通过注意所谓冗余ACK来较好地检测到丢包情况。冗余ACK(duplicate ACK)就是再次确认某个报文段的ACK,而发送方先前已经收到对该报文段的确认。要理解发送方对冗余ACK的响应,我们必须首先看一下接收方为什么会发送冗余ACK。下表总结了TCP接收方的ACK生成策略:
当TCP接收方收到一个具有这样序号的报文段时,即其序号大于下一个所期望的、按序的报文段,它检测到了数据流中的一个间隔,这就是说有报文段丢失。这个间隔可能是由于在网络中报文段丢失或重新排序造成的。因为TCP不使用否定确认,所以接收方不能向发送方发回一个显式的否定确认。相反,它只是对已经接收到的最后一个按序字节数据进行重复确认(即产生一个冗余ACK)即可。
因为发送方经常一个接一个地发送大量的报文段,如果一个 报文段丢失,就很可能引起许多一个接一个的冗余ACK。如果TCP发送方接收到对相同数据的3个冗余ACK,它把这当作一种指示,说明跟在这个已被确认过3次的报文段之后的报文段已经丢失。一旦收到3个冗余ACK,TCP就执行快速重传(fast retransmit),即在该报文段的定时器过期之前重传丢失的报文段。对于采用快速重传的TCP,可用下列代码片段代替ACK收到事件:
7.3.2.4 是回退N步还是选择重传
考虑下面这个问题来结束有关TCP差错恢复机制的学习:TCP是一个GBN协议还是一个SR协议?前面讲过,TCP确认是累积式的,正确接收但失序的报文段是不会被接收方逐个确认的。因此,TCP发送方仅需维持已发送过但未被确认的字节的最小序号(SendBase)和下一个要发送的字节的序号(NextSeqNum)。
在这种意义下,TCP看起来更像一个GBN风格的协议。但是TCP和GBN协议之间有着一些显著的区别。许多TCP实现会将正确接收但失序的报文段缓存起来。 另外考虑一下,当发送方发送的一组报文段,并且所有的报文段都按序无差错地到达接收方时会发生的情况。进一步假设对分组
的确认报文丢失,但是其余
个确认报文在分别超时以前到达发送端,这时又会发生的情况。在该例中,GBN不仅会重传分组
,还会重传所有后继的分组
。在另一方面,TCP 将重传至多一个报文段,即报文段
。此外,如果对报文段n+1的确认报文在报文段
超时之前到达,TCP甚至不会重传报文段
。对TCP提出的一种修改意见是所谓的选择确认(selective acknowledgment),它允许TCP接收方有选择地确认失序报文段,而不是累积地确认最后一个正确接收的有序报文段。当将该机制与选择重传机制结合起来使用时(即跳过重传那些已被接收方选择性地确认过的报文段),TCP看起来就很像我们通常的SR协议。因此,TCP的差错恢复机制也许最好被分类为CBN协议与SR协议的混合体。
7.4 滑动窗口机制
在建立 TCP 连接的双方,即 TCP 的发送端和接收端分别都使用滑动窗口:
- 滑动窗口是一个 buffer (缓存)。
- 对于发送端 S 来说,滑动窗口内部的那些内容是已经发出去但是尚未得到接收方 R 确认(ACK) 的内容。
- 对于接收端来说,滑动窗口内部的内容是没有被上层的 app 取走的数据。
因为 TCP 是为上层的应用层提供数据支持(服务)的,因此什么时候取走 TCP 接收端缓存中的数据完全取决于上层的 app;因此在接收端,如果数据没有被上层的 app 取走,滑动窗口就不会向后移动。
数据发送和接收的过程应该如下:
- 发送方 S:滑动窗口的尺寸 size = 50byte,因为滑动窗口内可以包含 5 个数据段,每个数据段的长度假设是 10 个 byte。
- 接收方 R:滑动窗口的尺寸 size = 50 byte。
- 根据上面所说的,一开始 S 会将格子标号 1- 41 的内容全部发送给 R,然后等待 R 返回的确认 ACK。
- 假设没有任何丢包的情况发生,R 应该返回 ACK=51 表示前 50 byte 的数据都收到了,希望下一次 S 从 51 byte 及其以后的数据开始发送;除了这个 ACK=51 的回执之外,由于 R 方的 buffer(滑动窗口)已经满了,暂时不需要 S 往这边发新的数据了,因此 S 还会收到 R 关于接收方滑动窗口剩余空间大小的描述;这个信息写在 advertised window 里面。
- 通过这种方式,其实是 R 方决定了 S 方是否能继续发数据,发多少数据;
假设当前的 R 方的滑动窗口中的内容被上层的 app 取走了 20 byte,也就是说 buffer 中多了 20 byte 的空间,那么这个时候,R 会向 S 发送一个 TCP head,里面的 ACK 依然是 51, 但是这个时候的 advertised window 的内容变成了 20 byte,也就是告诉 S 可以继续发送信息了,依然还是从第 51 byte 开始发送,发送长度是 20 byte。
因此 S 的滑动窗口会向后滑动到包含 51, 61 这两个格子;但是这就产生了一个问题:根据上面提到的:滑动窗口内部的字节都是等待确认的字节,这个时候的滑动窗口到底是下面两种情况中的哪种呢?
其实之前的那种阐述不准确,只是为了帮助理解而粗略的对滑动窗口进行了简化,其实滑动窗口有两个主要的部分组成:
- 第一部分就是那些发送出去但是没有收到 ACK 的字节。
- 第二部分就是那些在发送窗口范围内(允许发送)但是尚未发送的字节。
- 在这个例子中,由于 R 的剩余窗口大小仅为 20 byte,因此 51, 61 作为第一个部分发送出去,但是 71,81, 91 这三个格子的 byte需要等到 R 有剩余的窗口空间的时候才会被发送出去。
总结滑动窗口:
发送方 S:
- last byte written 是上层应用向下写入的最后一个字节的位置。
- last byte acked 是 S 方最新的被确认的字节。
- last byte can send 是 S 方最多可以发送的字节,相当于上文中的标号为 61 的格子。
- EffectiveWindow:发送端还需要发送的窗口大小。
接收方 R:
- Last Byte Read:上层应用已经读完的最后一个字节的位置。
- Next Byte Needed:接收到发送端的 连续序列号 的最大值的位置。
- LastByteRcved:已经收到的最后一个字节的位置。
- AdvertisedWindow:接收端还能接收的窗口大小。
这个时候就有下面这个问题:
为什么接收方会出现这种空白的情况。也就是 next byte needed 和 last byte received 不是同一个位置:
- 这是因为 S 在发送的过程中发送的 byte 到达的顺序取决于网络,可能后发的 byte 先到达,也可能先发的信息后到达。
- 而这个空白的部分如果长时间没有被补齐,就会导致快速重传。