运输层
- 运输层在进程之间提供了逻辑通讯,而网络层是在主机之间提供了逻辑通讯。
- 运输层的分组叫做报文段。
- 有TCP和UDP两种协议。
- 它将主机间的数据交互扩展到了进程间的数据交互,这叫做运输层的多路复用与多路分解,即多个传输层连接复用一个网络层网卡链路,让它为多个应用工作。
- UDP套接字由一个二元组标识:(目标IP,目标端口),但是UDP报文是有源端口的,用作返回地址。
- TCP套接字由一个四元组标识:(源IP,源端口,目标IP,目标端口),所以两个有着相同目的地址但不同源地址的TCP报文到达,会被定向到两个不同的套接字(TCP创建请求报文除外)。
UDP
- UDP只实现了运输层最少的工作,除了多路复用/分解,就只有差错校验了。
- UDP使得开发者对数据的发送控制有更高自由性,但是也增加了开发者的工作量。
- 不需要建立连接,这使得它比TCP快。
- 不需要维护连接状态(接受和发送缓存等等),服务器资源开销更小。
- 分组首部开销小,TCP首部20字节,UDP只有8字节。
报文结构
| 字段名 | 长度 |
|---|---|
| 源端口号 | 16bit |
| 目的端口号 | 16bit |
| 报文长度 | 16bit |
| 校验和 | 16bit |
| 应用数据 | / |
差错检测
UDP会对报文段所有16bit的和进行反码求和,求和时遇到的溢出会被回卷,接收方会再次对报文进行求和计算,得到的结果如果和校验和不一致,则说明报文损坏。
可靠数据传输原理
可靠数据传输原理分为停等协议(也叫做比特交替协议)和流水线协议。
停等协议
由于性能原因,实际的运输层并没有使用该协议模型,它的研究价值大于实际运用价值,所以这里简单概括一下。
停等协议简单来说就是,发送方发送一个报文之后,一定要在收到接收方对该的确认报文之后,才会发送下一组报文,否则就一直等待,如果超时就会重发报文A。
流水线协议
为了解决停等协议性能差的问题,我们允许发送方发送多个分组而无需等待确认,因为这些报文看起来像是在一条流水线上面一起过去,所以就叫做流水线协议。
为了保证流水线协议的可靠性,必须有如下要求:
- 每个分组必须有一个唯一的序号
- 收发双方可能要缓存多个分组
序号的范围和缓存的大小取决于如何处理丢失、损坏及超时的分组,解决方案基本有两种:退回N步(Go-Back-N,GBN)和选择重传(Selective Repeat,SR)。
GBN
GBN协议允许发送方发送多个分组,而不需要等待确认(未确认的分组最大不超过N),这个范围可以看成一个长度为N的窗口,所以GBN也叫做滑动窗口协议。

图中的base为已确认分组序号中的最大序号,当接收方收到一个确认分组序号x,x>=base,则表示[base,x]内的所有报文全都接收成功,发送方将base=x。如果超时,发送方会重传base分组。
为什么可以知道序号小于x的分组也成功接收了呢?因为接收方采用了累计确认机制。
累计确认是指,接收方只会按序接收分组,假设接收方上一次接收到分组序号是x,那么现在只会接收序号为x+1的分组,然后返回接收分组,其他的文组直接丢弃(无论序号是大于x还是小于x)。
所以接收方收到了x分组的确认,说明x和x之前的分组已经全部正确交付了。
所以在GBN协议中,接收方需要有缓存(长度为N),用来缓存未确认的分组,而发送方不需要缓存。
SR
GBN协议中,如果窗口长度和带宽延时都很大时,单个分组的差错就会引起大量的分组重传(但其实很多分组根本没必要重传),就好像两个人在说话,说了100个字,其中第10个字没听清,我们就得把第10到1000个字全部重说一遍,这明显效率很慢。
SR协议顾名思义,就是让发送方发送那些它怀疑出错的分组。

接收方将缓存一个正确接收的分组(不管是不是按序),直到收到丢失分组中序号最小的分组(即图中的rcv_base),接收方就会将这一批分组按序交付给上层,然后窗口向前滑动到第一个丢失分组的位置。
发送方和接收方的策略类似,它会将收到确认的分组标记为已接收,如果收到的确认序号是未确认分组序号里的最小序号(即图中的send_base),则窗口向前滑动到第一个未确认分组处。
接收方如果接收到了序号在[rcv_base-N,rcv_base-1]内的分组,必须产生一个ACK,否则接收方将永远卡死!
因为考虑在极端情况下,发送方发送了N个报文,接收方也成功接收了N个报文并向前滑动了N步,但是由于网络原因,第一个报文的ACK失踪了。此时,发送方会重传第一个报文,即序号为rcv_base-N的报文,此时如果不产生ACK给发送方,则发送方将永远卡住不动!

此时接收方重发0号报文,但是接收方分不清该报文是重发的报文,还是后面的报文。
所以从这里也能得到,窗口长度N<=序号空间/2。因为如果窗口长度大于序号空间的一半,那么可能把两个不一样的分组当成同一个分组(因为这两个分组的序号是一样的)。
TCP
- 是一个面向连接的协议
- 通过三次握手建立连接,即相互发送一些预备报文段,以确保传输的参数。
- 连接双方都要维护许多TCP状态变量。
- 全双工通讯,双方即是发送方又是接收方,每一方都有自己的发送缓存和接收缓存。
报文结构
| 字段名 | 长度 | 说明 |
|---|---|---|
| 源端口 | 16bit | / |
| 目标端口 | 16bit | / |
| 序号 | 32bit | 序号字段和确认号字段被用来实现可靠传输(相当于前面讨论的分组序号) |
| 确认号 | 32bit | 序号字段和确认号字段被用来实现可靠传输(相当于前面讨论的分组序号) |
| 首部长度 | 4bit | 用来存放以32bit为单位的TCP首部的长度,通常是20字节,即5 |
| 未使用空间 | 6bit | / |
| ACK | 1bit | 用来标记确认号中的值是否有效 |
| RST | 1bit | 重置报文标记,表示请求的端口号没有开启TCP连接 |
| SYN | 1bit | 用于连接的建立 |
| FIN | 1bit | 用于连接的关闭 |
| 校验和 | 16bit | 实现差错校验 |
| 接收窗口 | 16bit | 接收窗口字段用于流量控制,表示我接收缓存的可用大小 |
| 可选 | 不固定 | 用于双方协商MSS和窗口调节(通常为空) |
| 应用数据 | 不固定 | 实际发送的数据 |
- 在实践中并没有用到PSH、URG和紧急数据指针
序号和确认号
TCP把数据看成一个有序的字节流,因此序号是该报文段首字节的字节流编号(不是按照报文段编号),连接双方的初始序号都是随机选择的,这是为了减少把老连接的报文当作新连接报文的可能性(恰巧这两个连接的端口号都一样)。这两个序号的初始值都是在三次握手里定下来的。
TCP可靠传输
TCP接收方是采用累计确认的,和GBN协议一样,但是和SR一样,许多TCP的实现会将失序的报文缓存起来。
TCP发送方则是只需要维护未被确认字节的最小序号,和下一个要发送的字节序号,这和GBN一样。
所以,TCP的可靠传输是GBN和SR的混合体。
流量控制
当TCP接收到正确的报文之后,不会马上交给上层(上层可能在忙),所以就需要一个接收缓存来保存数据,上层会从缓存中拿数据。但是如果应用的读取速度对于发送来说太慢了,接收缓存就会很容易溢出,这个时候就需要流量控制,来消除缓存溢出的可能性。
TCP也可能因为网络的拥塞而限制发送方的速度,这个叫做拥塞控制,和流量控制是两个概念。
流量控制是一个速度匹配服务,即发送方的发送速度要和接收方的读取速度匹配,它的原理是通过维护接收窗口字段,它表示接收方接收缓存的可用大小。
我们定义如下变量:
RcvBuffer:接收缓存大小
LastByteRead:被上层从缓存中,读取数据的最后一个字节的编号
LastByteRcvd:被放入缓存的数据的最后最后一个字节的编号
然后我们就可用得到,接受缓存已被使用的空间大小为:LastByteRcvd - LastByteRead
所以缓存的空闲空间就是:rwnd = RcvBuffer - [ LastByteRcvd - LastByteRead ]
接收窗口字段的值就是rwnd,即缓存的空闲空间大小。
现在得到了接收窗口的大小,下面开始讲如何进行流量控制:
发送方维护下面两个变量:
LastByteSent:被发送数据流的最后一个字节的编号
LastByteAcked:被确认字节流的最后一个字节的编号
所以在发送方这边,已发送还未被确认的数据量的大小就是:LastByteAcked - LastByteSent 。
因此,接收方只需要全程保证,已发送还未被确认的数据大小不超过接收窗口的大小就行了,即:
LastByteAcked - LastByteSent <= rwnd
这个方案还有一个小问题,如果rwnd=0,且接收方没有发送任何数据被发送方(即发送方感知不到rwnd的变化,即使接收方的缓存已经被清空了),那么发送方将被阻塞永远不会发送给数据。为了解决这个问题,在发送方收到窗口为0的情况下,会继续发送只有一个自己数据的报文,然后接收方会返回确认报文,从而感知到rwnd的变化。
连接管理
三次握手
客户端先发送一个SYN(SYN为1)报文,不包含任何应用数据,这个报文已经带上了随机的选择了一个初始序号。
服务器收到SYN报文,开始为TCP连接分配资源(各种缓存和变量),然后返回一个SYNACK(SYN和ACK为1)报文,表示服务器同意了客户端的连接请求。这个报文也带上了一个随机的初始序号,以及一个确认号(为客户端的初始序号+1)。
客户端收到了SYNACK之后,开始为连接分配资源,然后就开始给服务器发送正常的应用数据了(SYN为0了)。
发送方从缓存取出的数据最大不超过最大报文长度(Maximum Segment Size,MSS),MSS要保证一个TCP报文是一个合适的链路层帧。
想想上面的过程,其实两次发送就已经可以建立连接了,第三次其实是发送数据,为什么说是三次握手?
真实的TCP连接建立,客户端和服务器的连接资源的分配其实都在第三次握手(上面说服务器的资源分配是在第二次),因为这样可以有效的避免SYN泛洪攻击。即一个程序疯狂向服务器发送SYN报文而不完成第三次握手步骤,此时如果服务器在第二步分配资源,那么服务器的资源会迅速被消耗完,所以我们把双方的资源分配都放到了第三步。
具体步骤是,服务器在接收到SYN报文后,不会为该报文生成一个半开连接,服务器会使用SYN报文的源IP、源端口、目标IP、目标端口和一个秘密数(只有服务器知道这个数字)通过一个散列算法生成SYNACK报文的初始序号。
如果客户端合法,则将会发送第三步的ACK报文,此时**服务器就会根据ACK报文的四元组和秘密数进行进行计算,如果计算结果+1等于ACK报文的确认号,就认为该ACK是合法的,**服务器会生成一个具有套接字的全开连接。
四次挥手
- 客户端发起FIN报文(说明客户端的报文都发送完了),进入FIN-WAIT-1状态,即半关闭阶段,并停止发送(ACK报文除外),但是可以接收。
- 服务器接收FIN,并返回ACK报文,进入CLOSE-WAIT状态,客户端收到ACK报文,进入FIN-WAIT-2状态。
- 服务器发送FIN报文(说明服务器的报文都发送完了),进入LAST-ACK状态(半关闭状态)。
- 客户端接收FIN,并返回ACK报文,进入TIME-WAIT状态。
- 服务器收到ACK,关闭连接,客户端在等待一定时间之后,也关闭连接。如果ACK丢失,服务器会再次发送FIN报文,要求客户端重传ACK,然后客户端重新计时。
TIME-WAIT 状态存在有两个理由:
- 1.允许老的重复报文分组在网络中消逝。
- 2.保证TCP全双工连接的正确关闭。
四次挥手则是为了保证等数据完整的被接收完再关闭连接。既然提到需要保证数据完整的传输完,那就需要保证双方都达到关闭连接的条件才能断开。


拥塞控制
分组丢失一般是网络拥塞导致路由器缓存溢出引起的,我们把分组重传作为网络拥塞的征兆来对待。因为有太多的源想高速发送数据,导致网络拥塞,我们需要一些机制,在网络拥塞时遏制发送方。
如果TCP发送方感知到从它到目的地路径没有什么拥塞,则加速发送,否则就降低发送速度。这里有三个问题:
- TCP如何限制发送速率?
- TCP如何感知路径拥塞?
- TCP采用什么算法来控制发送速率?
TCP发送方维护一个变量:cwnd(拥塞窗口),发送方中,未被确认的数据量不会超过cwnd和rwnd的最小值,即:
LastByteSend - LastByteAcked <= min( cwnd, rwnd )
我们通过调节cwnd的值来控制发送速率,如果太大,会导致网络拥塞,太小又不能充分利用网络。
- 当丢失报文时(超时或者收到三个冗余ACK),减少长度
- 当收到ACK报文时,增加长度
