自顶向下方法 -- 第三章 运输层
[TOC]
概述与运输层服务

运输层为不同主机上的进程提供逻辑通信功能,是在端系统而不是路由器中实现的
运输层从应用程序中接收到报文转换为运输层分组(报文段segment),一般是将应用报文划分为较小的块,为每个块加上运输层的协议信息,然后将这些报文段传递给网络层,网络层将其封装为网络层分组(数据报)向目的地发送,注意网络路由器仅作用与该数据报的网络层字段不检查运输层报文字段,接收端从网络层数据报中提取运输层报文并上交给运输层,运输层处理收到的报文后再交给应用进程使用
运输层和网络层的关系
这里书中举了一个例子:两个远房亲戚家族通信,家族A让小刘统一负责收发信件,家族B让小张负责收发信件,于是两个家族里面的人都只合负责收发信件的小刘小张打交道就行,邮政服务将信件从一个家族送往另一个家族,而不是具体的一个人到另一个人
- 应用层报文 = 信封里的内容
- 进程 = 家族里的亲戚
- 主机(端系统) = 家族
- 运输层协议 = 小刘和小张
- 网络层协议 = 邮政服务(包括邮车)
在这里小刘和小张都是在各自家里工作的,他们不参与到邮件的分拣和传输,类似的运输层协议值工作在端系统中
运输层中最常用的两个协议,UDP和TCP
多路复用与多路分解
设备上运行多个服务需要进行网络通信,建立了多个socket连接,这个时候需要将对应的数据通过对应的socket进行传输,运输层报文中有几个字段用来标识对应的套接字
- 发送端主机从不同的套接字中收集数据块,并为他们封装上标识信息,然后将报文段传递到网络层,这个过程叫做多路复用
- 对应的目标主机接收到报文段之后,通过首部标识将他们分发到对应的套接字,这个过程叫做多路分解
看一下具体的工作流程:
- 套接字有唯一标识符
- 每个报文段有特殊字段来指示需要交付的套接字,这个特殊字段是源端口号和目的端口号,端口号是一个16bit的数,也就是0-65535,0-1023范围的端口号为周知端口号,是受限制的,比如HTTP使用80,HTTPs使用443

UDP的多路复用与分解
UDP的套接字是一个二元组(目的ip,目的端口号),所以当不同来源ip和不同来源端口号的两个拥有相同二元组信息时,这两个报文段将通过相同的套接字定向到相同的目的进程
TCP的多路复用与分解
TCP套接字是一个四元组(源IP,源端口,目的IP,目的端口),需要这四个信息完全匹配时才会被放到同一个套接字进程中,也就是不同的源IP和端口的报文将被定向到两个不同的套接字
web服务器与TCP
例如一台端口80上运行Apache的服务器,当客户端发来报文段时(由于源ip和端口不同)会创建对应的HTTP进程,每个进程有自己的套接字,通过这些套接字可以接收和响应HTTP请求;实际上目前高性能的web服务器一般使用一个进程多线程的方式(线程对应套接字)
如果客户与服务器建使用持续HTTP则整个生命周期会使用同一个套接字,如果使用非持续的HTTP,则每次请求/响应后都要重新创建套接字
无连接运输:UDP
最简单的运输层协议是什么,即UDP,由于应用层和网络层之间实在无法直接通信,网络层把报文送达后目的主机不知道该把报文给哪个应用进程,所以最少要求运输层要实现多路复用与分解,而UDP就是这样的协议(还有少量的差错检测)
DNS通常使用UDP的应用层协议,当DNS应用程序想要进行一次查询时,它构建一个DNS查询报文交给UDP,UDP封装后丢给网络层,然后就开始等待目的主机的响应,如果没有响应要么会向另一个服务器发一次查询要么就通知应用程序不能获得响应
UDP优势:
- 希望对应用层有更精细的控制,前面说道TCP有一个拥塞控制机制,会系统的调度报文的发送,而UDP简单粗暴(这个行为比较自私,大家都得不到好,大佬们已经提出了面向所有数据源的自适应拥塞控制,这个机制也导致UDP的优势减小)
- 无需连接建立,TCP三次握手带来建立连接的时延,UDP速度更快,这个应该是DNS采用UDP的主要原因
- 无连接状态,TCP需要维护连接状态,带来更多的资源开销
- 分组首部开销,TCP首部占用20字节,UDP占用8字节
- 开发者可以通过自定义检查机制来基于UDP完成可靠数据传输,只是实现起来比较繁琐,也不可能达到TCP那样的稳定性

这里单独说一下SNMP也是使用的UDP,网络管理应用程序一般要在网络处于重压的时候运行,这个时候很难建立TCP的稳定传输
UDP报文段结构

- 4个字段每个字段都是两个字节,所以UDP的首部占用8字节
- 长度字段记录UDP报文段中的字节数(包括首部)
- 检验和:接收方使用检验和来检查报文段是否出现差错
UDP检验和
- 检验和通过补码反码的方式来对数据进行校验,具体过程没看懂,先不纠结
- UDP为什么要提供检验和,主要是不能保证传输过程中的协议都有差错检测,所以自己进行了实现,但是UDP对于差错的处理很糟糕,要么丢弃受损报文段,要么给应用程序提出警告
可靠数据传输原理
在不可靠的底层系统上构建可靠数据传输协议是一个困难的任务,这里将展开探索具体的实现逻辑
构造可靠数据传输协议
可靠信道的可靠数据传输:rdt1.0

从最简单的模型来,假设底层信道的可靠传输时,这个时候没什么要做的,就是把应用层传过来的数据封装一下然后丢给下层,书上引入十个名词来解释给我看吐了
经具有比特差错信道的可靠数据传输:rdt2.0
实际上我们都知道底层传输是可靠的,和人类的行为类似,两个人通过bb机交流的时候,如果听到了会说收到收到,没听到就让人重复,数据报文传输也使用类似的方式叫做自动重传请求(ARQ)协议
- 差错检测:为了确定收到的内容是否和发送内容一致,需要差错检测和纠错技术(后面的章节会详细讲),数据报文中需要一些额外的比特来记录这个技术需要的信息
- 接收方反馈:接收方验证完差错之后给发送方反馈“肯定确认”ACK和“否定确认”NAK
- 重传:发送方收到接收方的NAK时重新传输该分组数据
- 停等:发送方发出分组后要等待接收方的反馈信号,这个过程中不会发送新的数据分组
现在我们要考虑另一个问题,如果接收方反馈数据(ACK和NAK)出现了受损该如何处理
- 从人类角度出发,发送方收到了一个看不懂的反馈数据时,让接收方再说一次收到没有,问题是这个“再说一次你收到没有”这个数据出现受损,进入一个死循环,这种方式不可取
- 第二种方式,增加足够的检验和比特,让发送方不仅可以检测差错还可以恢复差错,如果底层是只有可能产生差错不会丢失分组的信道,这个方法可以解决问题
- 第三种是,发送方收到含糊的反馈时,直接重新发送当前分组,引入冗余分组,冗余分组的根本困难在于接收方不知道收到的分组是新的还是一次重传
解决这个问题的办法是为数据分组加上编号,这样接收方通过对比编号就知道是否为冗余分组,发送方会一直发送冗余分组直到收到正确的ACK;对于非停等协议情况,就需要接收方明确的标识反馈对应的分组,这样发送方就能确认各个分组的传输情况
既有比特差错也有丢包的情况:rdt3.0
“从发送方的观点来看,重传是一种万能灵药。发送方不知道是一个数据分组丢失,还 是一个ACK丢失,或者只是该分组或ACK过度延时。在所有这些情况下,动作是同样 的:重传。”
所以解决丢包的方法也是重传,发送方在一个时间阈值下没有收到接收方的ACK则重传数据
比特交替协议:分组需要在0和1之间交替(1个比特即可表达出来)
流水线可靠传输协议
停等的方式效率很低,带宽利用率只有万分之2.7(大多数时间都在网络层传输数据),所以采取流水线的方式,即允许发送方发送多个分组而不用等待确认
- 比特交替不行了,需要扩大编号存储空间
- 发送方需要缓存那些未被确认的分组,接收方也许也需要缓存已正确接收的分组(取决于差错恢复策略)
- 解决流水线差错恢复的两种方法:回退N步和选择重传
回退N步(Go Back N GBN)

上面的图片颜色有点不对劲,从左到右,分别是已经发送且确认、已发送未确认、待发送、不可用,需要注意的是这个是指分组序号,这个序号是存储在分组首部的一个数据,TCP中序号字段长度为32比特,也就是总共可用的序号为[0, 2^32-1],而上面的比特交替由于只有一个比特[0, 1]
N被叫做窗口长度,有个问题是:为什么要限制这个发送未被确认和待发送的长度为N不让它占满所有序号呢,后面会解释,这里还不知道
- 上层调用的时候要判断窗口是否已满,未满则放进去发送,满了就要告诉上层让他过一会再试,或者有一个同步机制让上层仅在窗口不满时才调用send方法
- GBN累计确认:收到序号n的ACK时表明包括n在内的之前的分组都已经收到了,后面会解释原理
- 超时事件:这里是为N个分组共享定时器,一个分组被确认这个定时器都会被重置,当然如果没有等待确认的分组时这个定时器被停止,如果超时则所有待确认的分组都会被重发
GBN的接收方逻辑被设置得很简单:如果正确接收了一个序号为n的分组,检查上次交付给上层的分组序号为n-1,则发送n的ACK并把n中的数据取出来交付给上层,所有其他情况丢弃该分组
也就是说接收方会丢弃所有失序的分组,哪怕这个分组是正确接收,这里就是为了简单的设计,如果对这个n进行缓存等待n-1分组被正确的接收,这个设计的复杂度就高了,先前说了发送方是为N个分组统一设置的定时器,所以整个是批量进行重发的
选择重传
回退N步的代价太高了,一个分组丢失,所有后面的分组都被重传
太累了,下次再看这个的实现逻辑!(晚上要吃火锅,爽到)
面向链接的运输:TCP
已经了解了可靠数据传输的基本原理,接下来看看TCP如何做的
TCP连接

最大报文长度(Maximum Segment Size, MSS):由于TCP的报文长度是受限于网络层IP协议报文长度的,再往下的以太网协议长度为1500,TCP和IP的首部一般都是20,所以这个MSS一般是1460
报文段结构

- 两个端口号和UDP一样
- 序号和确认号是发送方和接收方用来进行数据确认的,上面说过了
- 16比特的接收窗口字段,用于流量控制,后面会讲
- 4比特的首部长度字段,TCP首部有一些可选字段导致不是定长的,这个字段就是用来记录首部长度的,比较奇怪的是4比特最多能表示16,而TCP首部的最大长度为32,通常长度是20
- 可选与变长的选项字段:用于发送方和接收方协商最大报文的长度(MSS)
- 6比特的标志字段
- ACK
- RST、SYN和FIN比特用于连接的建立和拆除
- CWR和ECE服务于拥塞控制
- PSH用于标记将数据推送给应用层(实践中未使用)
- URG指示报文段里存在着被发送端上层实体置为紧急的数据(实践中未使用)
序号和确认号
- 序号:加入发送的数据流有500000字节,MSS为1000字节,那么第一个报文段分配序列号0,第二个分配序列号1000,是以数据流字节的编号作为序号
- 确认号:
- 简单的模型下接收方收到0-1000的数据后,给发送方发送一个报文其中确认号为1001,表示需要发送方从1001开始发送数据
- 实际上发送方和接收方是相互传输数据的,假设A收到了B发送的0-555的字节,A在等待B数据流中556及之后的字节,A往B发送报文(A向B传输数据,不单是ACK)的时候就会在确认号中写556
- 另外A收到了0-555,又收到了888-1000,但是由于中间的字节缺失,A到B的下一个报文的确认号仍然是556,这叫累积确认
- 上面的例子中,888-1000的字节该怎么处理,TCP没有进行规定,可以直接丢弃也可以等待中间缺失的报文,这个交给编程人员去进行实现,一般不会丢弃这样效率更高
- 另外为了避免端口号上之前建立过连接,起始序号一般是随机产生的,避免网络中存在之前的报文段被新的连接使用
Telnet:序号和确认号的一个学习案例

telnet是基于TCP的远程登录应用层协议,但是由于是明文传输,现在大家都用ssh,telnet一般用来测试目标主机上的某个端口是否可用
简单说一下流程:TCP连接建立之后,A向B发送了一个数据字符串C,这个时候给它分配了序号为42,确认号79(这里还没有接收过数据,这个确认号只是告诉B如果要发送数据序号从79开始),然后B接收到字符串C并且向A发送回显字符串C,序号为79(A指定的),确认号为43(告诉A如果要发送数据从43开始),A再次向B发送报文,这次没有数据,序号仍然为43,确认号为80(告诉B 79及之前的数据收到了),这是一个单纯的确认报文
上面第二次通信,也就是B发送数据给A的时候,将确认号43放到了数据报文中而不是用一个单独的确认报文,这叫做捎带
往返时间与超时
TCP使用超时/重传的机制来处理报文丢失的问题
估计往返时
超时时间该如何指定呢,由于网络信道每时每刻都在变化,很难得到一个通用的值,大多数TCP采用的方法是,在某个时间点上取一次RTT,之后再随机的取获取RTT,然后通过函数来动态更新往返时间,具体公式有些复杂不必纠结
超时间隔
获取到一个趋近实际的往返时间之后,超时时间又该如何设置呢,肯定要大于往返时间,大太多会使得重发的效率低,大太少又会导致频繁的重发,这里也是带入往返时间进行函数动态评估,另外如果发生超时这这个超时的间隔会翻倍
可靠数据传输
- 超时时间加倍:通过这个加倍的规则,可以粗略的实现拥塞控制,也就是认为网络堵塞的时候将重传频率降低,避免整个系统越发恶化
- 快速重传:超时重传的问题之一,如果超时周期比较长,那一个报文丢失后重传的间隔也长,有一个机制来解决这个问题,冗余ACK,比如有abcd四个报文,其中a已经确认收到,b丢失,而c和d都收到,这个时候接收方不会发送一个对b的否定,而是重复发送对a的ACK,当接收方收到三个相同的ACK时,说明它之后的数据很可能是丢失了,不会再等到定时器过期(超市间隔),直接重传被认为丢失的报文段‘
回退N还是选择重传
TCP发送方仅需维持已发送但未被确认的字节的最小序号(SendBase)和下一个要发送的字节序号(NextSeqNum),看起来很想一个GBN,实际还是有些区别,TCP允许接收方有选择的确认失序报文段,而不是单纯的累计确认,和选择重传结合起来(跳过已被确认的报文段),TCP的差错恢复机制是GBN和SR的混合体
流量控制
TCP连接两端的主机都会设置一个接收缓存(正确有序的字节),应用层服务会从缓存里面读取数据,但是如果传输速度太快,但是应用层服务读取比较慢(服务可能在处理别的工作),可能会导致缓存溢出,TCP为解决这个问题提供了流量控制服务
注意流量控制服务是为了防止传输速度过快缓存溢出,而之前提到的拥塞控制是为了保持整个底层信道的通畅,虽然都是主动的限速操作,但是意义完全不同
实现的逻辑:有一个前置设定是接受方会丢弃所有无序的报文,这个时候发送方会跟踪接收方缓存中的两个变量,第一个是LastByteRcvd最后确认接受到的字节,第二个LastByteRead应用最后一次读取的字节,然后保证发送窗口中发送未确认的报文大小小于LastByteRcvd-LastByteRead
TCP连接管理
三次握手的详解
- 第一步是客户端发送一个SYN被标记为1的报文,叫做SYN报文,并随机初始化序号(为避免SYN泛洪攻击,这个随机的方式也有说道)
- 第二步服务器收到SYN报文,会为该TCP分配缓存和变量,并向客户端发送允许连接的报文,该报文中SYN为1,确认号为客户端SYN序号加一(不是序号本身),服务端初始化一个序号,这个报文叫做SYNACK
- 第三步客户端收到SYNACK,同样为TCP分配缓存和变量,再向服务器发送一个对SYNACK的确认报文(这个是为了向服务器表达自己的TCP资源准备好了),这个时候SYN为0,同样确认号为服务器初始化的序号加一
后面会讲为什么是三次而不是两次,之后的通信中SYN都是0
四次挥手
累了,以后再看
拥塞控制原理
上面我们讨论可靠传输的实现原理,主要原因就是底层的传输是不可靠的,而之前也讲过,网络拥塞导致的路由器缓存溢出是丢包最常见的原因,这里会对拥塞做进一步的探索
拥塞控制的方法
拥塞控制需要先了解底层网络的拥塞情况,两种情景下对拥塞情况的探知方式
- 端到端:底层不为运输层提供拥塞信息的支持
- 超时
- 3次冗余确认
- 往返时延的变化
- 网络辅助:
- 网络路由器直接向发送发反馈信息,表示自己拥塞了
- 路由器给分组数据中加入拥塞的标识,接收方收到带有标识的分组后会告诉发送方网络拥塞
TCP拥塞控制
TCP让发送方根据所感知到的网络拥塞程度来限制其发送流量的速度,核心问题:
- 如何限制流量发送速度
- 如何感知传输路径上的拥塞情况
- 出现拥塞时速率调整的算法
