Transport Layer
传输层的重要功能是为运行在不同的主机上的进程提供通信服务,事实上这是基于网络层为不同的主机提供通信服务的基础之上实现的。
同时,某些传输层协议,例如TCP,则致力于解决在易于丢失和损坏数据的传输介质之上实现可靠的数据传输,以及如何控制传输速度以防止网络拥塞或者从中恢复。
1. Introduction and Transport-Layer Services
在发送端,传输层从应用层获取数据并将它们分块,为每个数据块添加一个头部形成一个Segment。之后将Segment递交给网络层,网络层对它进行封装并将其发送到接收端,在中间的传输过程中并不会碰Segment中的内容。最后接收端的传输层获取Segment并将其中的数据递交给应用层。
传输层是基于网络层实现的,因此传输层能提供的服务往往会受到网络层能提供的功能的限制。比如,网络层不能保证传输数据的带宽和延迟,那么传输层同样不能保证传输数据的带宽和延迟。但是传输层也能提供网络层所不具备的功能,例如可靠的数据传输以及对传输数据进行加密。其实,对于无法实现的功能是网络本身的问题引起的,和分层无关,而传输层实现网络层不具备的功能,也只是因为这些功能在传输层实现更合适而已。
拥塞控制不是为调用TCP的应用提供的服务而是一个有益于全局的服务。拥塞控制防止了单个连接用巨大的流量淹没其流经的链路和路由器,从而能让所有连接都共享拥塞的链路,这是通过控制发送端的发送速度实现的。但是UDP并没有该限制,使用UDP的应用可以用任何速度发送流量并且可以持续任意长的时间。
2. Multiplexing and Demultiplexing
端口是传输层在Demultiplexing Segment时作为依据的标示,端口号从0~65535,其中0~1023是众所周知的端口。一般服务器端需要自己绑定端口而客户端则系统会默认分配一个端口。
对于UDP,一个Socket由(目标IP,目标端口)这样的二元组进行标示,只要两个Segment的目标IP和目标端口相等,即使源IP和源端口不相同,也会被发往目标IP所在主机的同一目标进程。
对于TCP,一个Socket由(源IP,源端口,目标IP,目标端口)这样的四元组进行标示。因此对于两个源IP或源端口不同的TCP Segment,它们会被发往不同的Socket(除了最开始携带建立连接请求的TCP Segment以外)。
TCP Socket创建的过程如下所示:
1)当Server首先会创建一个“Welcoming Socket”,例如在端口12000等待来自Client的建立连接请求
2)Client创建Socket并且向Server发送一个建立连接请求,而建立连接请求无非是一个Header中的Connection-Establishment Bit设置为true的TCP Segment
3)Server端的操作系统接收到2)中创建的建立连接请求,根据目的端口,将请求转发至Server,Server根据四元组创建一个Socket,之后所有包含该四元组的Segment都会发往该Socket
3. Connectionless Transport: UDP
UDP是最为简单的传输层协议,除了Multiplexing/Demultiplexing以及一些Error checking之外,它并没有在IP之上添加任何其他功能,值得注意的是UDP在发送Segment之前,UDP的接收方和发送方并没有握手的过程,因此UDP是Connectionless。既然TCP能提供可靠的数据传输服务,为什么还会有应用使用UDP?因为有的应用更适合UDP,原因如下:
1)可以在应用层面对发送什么数据以及何时发送有更好的控制,在传输层使用UDP并且在应用里实现其他必要的功能
2)没有连接建立的过程,因为建立连接会引入很大的延时,这也是DNS使用UDP而不是TCP的原因
3)UDP不需要维护连接状态,因此同样的Server,UDP能够支持更多的Client
4)更小的头部Overhead,TCP的头部是20字节,UDP的头部是8字节
多媒体应用对于TCP的拥塞控制机制并不适应,因此一般更倾向于使用UDP。但是随着丢包率变低以及一些组织因为安全原因屏蔽UDP流量时,对于多媒体应用TCP将变得更有吸引力。
基于UDP的多媒体应用是一直存在争议的,因为没有拥塞控制的UDP协议可能导致非常高的丢包率并且对于有拥塞控制的TCP是不公平的。
UDP的头部仅包含四个字段:源端口,目标端口,长度(包括头部和负载部分)以及校验码。
虽然有的二层协议(包括最流行的Ethernet)会提供校验,但是IP协议可能运行于任何二层协议之上,因此UDP依旧需要提供校验。
4. Principles of Reliable Data Transfer
可靠的数据传输事实上是指将数据无损地,不丢失地,按序地从发送发传输到接收方,为了达到这一目的,事实上不仅要传输有效负载,还应该传输一系列的控制数据。
当传输信道会损坏数据时
此时我们的传输协议需要三种能力:
1. Error Detection:通过校验码检验数据在传输过程中是否发生错误
2. Receiver Feedback:接收方需要给发送方反馈是否正确地收到信息,对于当前情况,只需返回接收成功(ACK)或者接收不成功(NAK)
3. Retransmission:对于接收方发现错误的数据,发送方要能够重传
但是我们忽略了接收方放回的ACK和NAK也存在损坏的可能,对于这一点,我们可以引入校验码并从中恢复出正确的信息,另外我们也可以在接收到NAK或者损坏的反馈信息时都进行重传。但是如果接收方返回的是ACK,但是损坏了,而发送方又进行重传了,则接收方会收到重复的信息。此时我们就需要为发送数据进行编号,从而防止重复。
事实上,我们完全不需要NAK,我们只需要在ACK之后增加一个编号即可,若接收方接收到符合要求的数据,则ACK对应数据的序号,否则返回上一次正确接收到的数据的序号即可。
当传输信道会损坏或者丢失数据时
通过超时重传即可解决该问题,但是超时的时间难以确定,一般该时间至少为Round-Trip Delay,不过这实际上也是一个在动态变化的数字,发送者通常需要自己对超时时间进行预估,虽然在预估的时间到达以后还是不能保证发送的数据或者对应的ACK已经丢失了。
Pipelined Reliable Data Transfer Protocols
如果基于发送数据并等待ACK之后再发送的Stop-And-Wait方式,则数据的传输效率会很低,事实上完全可以并行发送多个数据从而提升效率。但此时数据的Range of Sequence Numbers需要增加,同时发送者和接收者需要对数据进行缓存,特别是接收者的缓存会和具体的策略有关。
Go-Back-N(GBN)
GBN其实就是一个滑动窗口协议,窗口的大小为N,而流量控制和拥塞控制是限制窗口大小的原因,对于发送方来说有以下事件需要处理:
1. 上层需要发送数据:如果窗口未满,则直接发送数据,否则缓存数据或者利用同步机制在窗口有空闲时通知上层
2. 接收到ACK:GBN采用累计确认机制,当收到数据N的ACK时,则表示N之前的数据都已被确认
3. 超时:GBN使用一个统一的时钟,若发生超时,则重新发送所有已发送而未ACK的数据,若收到了一个ACK,则重启时钟,若没有已发送而未ACK的数据,则时钟停止
接收方若收到序号为N的数据,如果之前的数据都已被接收,则将数据传输至上层并返回ACK,若N之前有数据未收到,则直接丢弃序号为N的数据,因为N之前的数据如果丢失了,根据GBN,丢失数据之后的数据都会被重传。但是这样做的缺点是,重传的数据也可能丢失从而再次需要重传。总之,这种已经收到正确传输的数据但是又丢弃的行为是一种浪费。
Selective Repeat(SR)
在GBN中,如果N和带宽都很大,那么将有很多数据存在于信道中,但是其设计机制决定了可能因为一个数据的错误导致大量的重传,从而可能产生性能问题。
为了避免GBN的性能问题,SR会对正确但提前收到的数据进行缓存并返回相应的ACK,发送方根据接收到的ACK对相应的数据进行确认,当确认的数据能够连接在一起时就对窗口进行滑动。在SR中同样具有超时机制,但是时钟是和每个数据匹配的,一个时钟超时只会重传对应的数据。
由于数据编号的范围是有限的,当滑动窗口的大小大于数据编号的范围时,接收方就会无法区分收到的数据是新的数据还是重传的数据。例如数据编号的范围为0,1,2,3,窗口大小为3,开始发送0,1,2三个数据,且接收方全都收到并分别作出应答,若发送方成功接收ACK,则会继续发送3,0,1,若发送方未接收到ACK,则重传0,1,2,由此接收方完全无法判断,之后发来的数据0和数据1未新发的数据还是重传的数据。
滑动窗口大小问题
若窗口大小小于数据编号的二分之一,则在最极端的情况下,接收方期望接收的数据范围和重传的数据范围也永远不会重合。
窗口大小问题对于GBN同样存在,但是它只要求窗口大小不和数据编号的范围相等即可。如果两者相等且接收方的ACK全部丢失,发送方会不断重传,但是接收方仍然能够全部按序接收。
5. Connection-Oriented Transport: TCP
TCP是一个面向连接的协议,所谓的面向连接就是指两个位于不同主机的进程在通信之前先要进行“握手” ,而所谓的握手是指在传输数据之前先进行一系列的交互用于建立确保传输正常进行的参数。作为建立连接的一部分,连接的两端都会初始化一些TCP状态参数。
TCP连接的每一端都包含发送缓存和接收缓存以及一系列的参数,TCP从缓存中获取数据,构建一个TCP Segment,每个Segment中应用数据的大小不能超过Maximum Segment Size(MSS),MSS是由MTU决定的,以太网的MTU为1500,减去IP和TCP的头部各20,最终MSS为1460。
TCP Segment Structure
TCP的头部由如下几部分构成:
1. 16位的源端口和目的端口
2. 32位的Sequence Number和Acknowledgment Number,作为字节流的编号,用于可靠的数据传输
3. 4位的头部长度,TCP的头部理论上有可扩展的部分,但是一般都不用,正常情况下都是20字节
4. Flag字段:ACK位设置,表明Acknowledgment Number字段是有效的,说明有相应的Segment被确认
RST,SYN,FIN用于连接的建立和关闭
CWR和ECE主要用于显式的拥塞控制,PSH表明接收方需要立刻将数据交给上层,URG表示发送方的应用层将数据标记为"Urgent",紧急数据的起始位置由16位的Urgent Data Pointer字段决定。不过一般PSH,URG以及Urgent Data Pointer都不会用到。
Sequence Numbers and Acknowledgment Numbers
TCP对字节流中的每个字节而非每个Segment进行编号,其中Sequence Number是传输的字节块中第一个字节的编号,而Acknowledgment Number则是发送方期望从接收方获取的下一个字节的编号。例如,B从A获取到了字节0~574的Segment,则其中的Sequence Number为0,而A发往B的ACK中的Acknowledgment Number则为575.
TCP采用累计确认的方式,例如A已经收到从B发来的0~535以及900~1000,因此A还在等待来自B的536~899,所以A发往B的下一个Segment的Acknowledgment Number为536。同时对于Out-Of-Order的Segment,TCP的RFC并没有明确定义处理方式,一般为了节省带宽,将它缓存起来是更好的选择。
事实上,连接双方对于初始的Sequence Number都是随机选择的,从而避免之前完全相同的连接(同样的IP和端口)在网络中遗留的Segment对现有连接的影响。
Round-Trip Time Estimation and Timeout
TCP并不会对每个Segment的RTT进行测量,它一般每个RTT就只会测量一个Segment,而且对于重传的Segment也会不会测量。一般对于RTT的预测公式为:
EstimatedRTT = (1 - a) * EstimatedRTT + a * SampleRTT
上述公式表明,TCP更关注当前的SampleRTT,而非以往的Sample。
DevRTT用来测量RTT的波动情况,其生成公式如下:
DevRTT = (1 - b) * DevRTT + b * | SampleRTT - EstimatedRTT |
因此,重传的超时时间为:
TimeoutInterval = EstimatedRTT + 4 * DevRTT
TimeoutInterval初始化为1s,在第一次超时发生时会进行翻倍从而防止过早的超时,因为下一个Segment可能很快就会被ACK。当接收到第一个Segment的时候,EstimatedRTT就会更新,TimeoutInterval就会根据上述公式计算。
Reliable Data Transfer
为每一个传输但还未ACK的Segment配置一个时钟理论上是可行的,但是实际上会造成极大的Overhead,因此TCP仅会使用单个的时钟 ,一般该时钟和Oldest Unacknowledged Segment相绑定。
Fast Retransmit
TCP的接收方收到失序的Segment会将它缓存并且ACK第一个按序未被接收的Sequence Number,如果接收者收到三个及以上的Oldest Unacknowledged Sequence Number的ACK,即使还未超时,也要重新发送Oldest Unacknowledged Sequence Number。事实上,当TCP的接收者在收到一个按序的Segment之后,并不会立即发送相应的ACK,而是会等待500msec以检测能够进行累计确认。
Flow Control(假设接收者直接丢弃失序的Segment)
TCP连接的两端都有一个Receive Buffer用于缓存接收到的数据,但是由于应用不一定会及时处理这些数据,因此我们需要有一种机制来匹配Receive Buffer的空闲情况和发送者的发送速率。
Flow Control会让TCP的发送方维护一个Receive Window,该窗口表示了接收方可用缓存的大小,一般该值都是通过接收者发送给发送者的Segment中的Receive Window字段决定的。
可能出现的一种情况是,接收方的缓存已满,此时发送方获取的Receive WIndow大小为0,发送者将永远不再向接收者发送数据,但是接收方迟早会清空缓存,而接收者之后并不会通知发送者Receive Window已经不为0了,此时双方的通信将永远陷入停滞。因此TCP要求当发送者即使在Receive Window为0的情况下,也要持续向接收者发送Segment,其中包含一个字节,从而在接收者的缓存不为0时得以通知发送者。
UDP中并没有这种机制,它之后将数据简单地放到缓存中,应用程序会一次性读取缓存中的数据,如果读取不及时,则缓存就会溢出,就会有Segment被丢弃。
TCP Connection Management
TCP建立连接的过程分为以下三步:
1. Client向Server发送一个Segment,其中Flag中的SYN设置为1,随机选取一个Client Sequence Number,我们将这个Segment称为SYN Segment
---> Client从"CLOSED"状态转换为"SYN_SENT"状态
2. Server获取SYN Segment,为连接分配缓存以及初始化一系列的参数(这是造成SYN Flooding攻击的原因),再发送给Client一个Segment,SYN和ACK设置为true,随机选择一个Server Sequence Number,ACK Number设置为Client Sequence Number加一,我们将这个Segment称为SYN-ACK Segment
---> Server开始监听时,状态从"CLOSED"转换为"LISTEN",在步骤2之后,则切换为"SYN_RCVD"
3. Client收到SYN-ACK Segment,同样为连接分配缓存以及初始化一系列的参数,之后Client发送给Server一个Segment,ACK设置为true,ACK Number为Server Sequence Number + 1,SYN设置为false,可以携带数据。由此完成三次握手,连接建立
---> Client从"SYN_SENT"状态转换为"ESTABLISHED"状态,Server从"SYN_RCVD"切换为"ESTABLISHED"
最后,连接双方都可以结束连接,假设Client发起断开连接则:
1. Client向Server发送一个特殊的Segment,它的FIN设置为true
---> Client状态从"ESTABLISHED"切换为"FIN_WAIT_1"
2. Server收到该Segment回复一个ACK Segment
---> Server从"ESTABLISHED"切换为"CLOSE_WAIT",Client状态从"FIN_WAIT_1"切换为"FIN_WAIT_2"
3. Server也会向Client发送FIN Segment
---> Server从"CLOSE_WAIT"切换为"LAST_ACK"
4. Client收到Segment并ACK,连接结束
---> Client状态从"FIN_WAIT_2"切换为"TIME_WAIT"状态,该状态因实现而已,会持续30s,1min或者2min,从而允许在最后一个ACK丢失之后进行重传,Server从"LAST_ACK"切换为"CLOSED"
如果Server并没有在相应的端口进行监听,则Server在接收到Client的SYN Segment之后会直接返回一个特殊的Segment,它的RST位设置为true。对于UDP的话,则会返回一个特殊的ICMP Datagram
SYN Cookie可以用于解决SYN Flood Attack,当Server收到SYN Segment时不会立即进行分配内存等初始化操作,而是基于源,目的IP和端口以及Server独有的Secret创建初始的Server Sequence Number,若之后获取到的对应的ACK的ACK Number能够通过验证(验证方法即用同样的方法基于源,目的IP和端口生成一个Number,判断该Number是否为ACK Number - 1),则Server再分配缓存以及进行相应的初始化工作。