运输层协议为运行在不同的主机上的应用进程之间提供了逻辑通信(logic communication), 它是在端系统中被实现的, 发送端的运输层把报文转换为报文段(segment)(为了避免混淆, 把数据报这个名字给网络层), 并将报文段传输给网络层, 网络层将其封装为网络层分组(数据报)并向目的的发送, 注意网络层是不会检查报文段的. 接收端的网络层从数据报中提取运输层报文段, 并将该报文段上交给运输层, 运输层处理报文段, 把其中的数据为接受应用进程所使用.
概述
运输层与网络层关系
网络层提供了主机之间的逻辑通信, 而运输层提供了主机之间不同进程的逻辑通信, 运输层协议往往受制于网络层协议, 但是即使网络层协议不可靠(有分组丢失, 篡改, 冗余). 运输层也能为应用程序提供可靠的运输服务.
因特网运输层概述
主要就是TCP和UDP两种协议. 需要注意的是, IP是不可靠服务, 只能保证尽力交付, 而TCP/UDP最基本的任务是将两个端系统间的IP交付任务扩展为运行在端系统上的两个进程之间的交付任务. 这个扩展交付的名字叫运输层的多路复用(transport-layer multiplexing)和多路分解(demultiplexing). 多路分解指将运输层中报文段的数据交付给正确的socket, 多路复用指源主机从不同的socket中收集数据块, 为数据块添加首部信息, 生成报文段, 将其传输到网络层.
多路复用和多路分解
运输层检查报文段中的字段, 标识出接受socket, 进而将报文段定向到嵌套字, 并将运输层报文段中的数据交付到正确的socket的工作叫多路分解, 源主机从不同socket中收集数据块, 为每个数据块封装首部信息(为了后面的分解)并生成报文段, 把报文段传递给网络层就是多路复用. 多路复用的要求是socket有唯一的标识符/每个报文段有特殊字段指示其要交付的socket.
下图是报文段的大致结构. 其中, 端口号是16bit的数, 其中0~1023范围内是周知端口号(weel-known port number), 此端口号受限制, 保留给HTTP(80), FTP(21)等周知的应用层协议使用. 主机上的每个socket都能分配一个端口号, 当报文段到达主机时, 运输层检查报文段中的端口号并将其定向到socket, 报文段中的数据就通过socket进入其所连接的进程.
- 一个UDP socket包含了
(目的IP, 目的端口号)
这样的二元组, 证明了socket解析的时候不会受源的影响, 这里区分首部字段包含的内容和socket内容. - TCP相较于UDP的区别就是socket是由
(源IP, 源端口号, 目的IP, 目的端口号)
这样的四元组标识的, 所以主机收到数据报时, 会通过这四个值来把报文段分解到对应的socket.
虽然上面都是用进程举例的, 但实际上对于现在的高性能Web服务器, 不同的socket对应的是同一个进程下的不同线程.
UDP
DNS就是使用UDP的例子, 当主机的DNS程序想查询时, 它会构造DNS擦汗讯报文给UDP, UDP为其加上首部字段, 形成数据报并给网络层, 网络层再将其封装到IP数据报中. UDP的区别于TCP的特点如下:
- 应用层控制更加精细, 不用担心阻塞控制, 不过分关心数据丢失
- 无需连接建立, 没有三次握手的时延, 这也是为什么DNS协议用UDP
- 无连接状态, 不用追踪序号确认号等参数, 一般相较TCP能支持更多活跃用户
- 分组首部开销小, UDP只需要8个byte, TCP需要20个byte
UDP报文段结构
伪首部包含了IP首部, 不属于UDP, 只是为了提醒检验和的计算要包括它, 所以也画在了这里.
长度字段表示UDP报文段的字节数(首部加数据), 检验和是为了接收端检查报文段是否有错误.
UDP检验和
发送方先计算一次放在校验和字段中, 接收方把所有16bit的和加在一起(包括伪首部/首部/数据), 并取反码, 如果遇到了最高位进位, 就需要回卷. 若没有差错则在接受方做同样的计算的和就应该为1111111111111111
, 相当于源码+反码(多出来的检验和). 因为传输过程中的链路和路由器不能保证没有差错, 所以UDP就必须在端到端的基础上在运输层提供差错检测, 这就是端到端原则(end-end principle). 注意UDP可以检测差错但不能恢复. 并且UDP的校验和是可选的, 相对的TCP的校验和是必选的.
可靠数据传输原理
todo
TCP
TCP依赖上一节的许多基本原理, 包括差错检测, 重传, 累积确认, 定时器和用于序号和确认号的首部字段.
TCP连接
TCP是面向连接的, 提供全双工服务(full-duplex service), 是点对点的. 注意TCP和电路交换中端到端的TDM和FDM电路是两回事, TCP的连接状态完全保留在两个端系统中, 不在中间的网路元素中(路由器和链路层交换机)中运行. 而TCP连接就是经典的三次握手.
一旦连接成功, 客户进程就可以通过socket传递数据流, TCP拿到了数据流就把他们引导到发送缓存(send buffer)中, TCP就能在合适的时间以报文段的形式发送这些数据. TCP可从缓存中取出并放入报文段中的数据数量受限于最大报文长度(Maximun Segment Size). MSS最开始通常根据最**最大链路层帧长度(Maximum Transmission Unit)**来设置. 使MSS保证TCP报文段封装在IP数据报(封装指加了TCP/IP的首部, 一般是各20个byte)之后适合单个链路层帧. 以太网的MTU是1500 byte, 所以MSS一般是1460 byte.
MSS指的是封装在报文段中对应的应用层数据的最大长度, 这里比较容易混淆
TCP为每块用户数据加上首部, 形成TCP报文段. TCP报文段被下传到网络层, 网络层将其分别封装在IP数据报中, 通过网络发送. 而在接收端, 收到了这些报文段数据之后, 就会放进TCP接收缓存. 如下图, TCP连接只包含了这两台主机的缓存, 变量和socket, 而网络中的路由器/交换机/中继器没有为TCP连接分配任何缓存和变量.
TCP报文段结构
由首部字段和数据字段组成. 而MSS限制的其实就是数据字段的最大长度, 大文件会被切分成多个MSS块, 而交互式应用的传递长度往往会小于1个MSS. TCP班文段首部一般是20个字节.
- 源端口号和目的端口号可以用于多路复用和多路分解
- 32bit的**序号字段(sequence number field)和确认号字段(acknowledge number field)**来保证可靠数据传输服务.
- 16bit**接收窗口字段(window size)**用于流量控制, 使接收端告诉发送端还有多少缓冲区接收数据.
- 4bit**首部长度字段(header length field)**指示32bit的首部长度, 因为首部的长度是可变的, 一般就是20字节.
- 可选与变长的**选项字段(options field)**用于发送方和接收方协商MSS.
- 6bit**标志字段(flag field)**包含ACK比特(指示确认字段中的值是有效的), RST, SYN和FIN比特用于连接的建立和拆除, PSH比特被设置就表明接收方因立即把数据交给上层, URG比特表示报文段中存在着被发送端上层实体设置为紧急的数据, 紧急数据的最后一个byte由16bit的紧急数据指针字段指出. 实际中PSH, URG和紧急数据指针没有使用(书里面的确是这么写的).
序号和确认号
TCP把数据看成无结构的, 有序的字节流. 而序号就建立在字节流上, 而非传送的报文段序列上. 报文段的序号就是该报文段首字节的字节流编号. 如下图500000byte的文件, MSS是1000byte, 那么其中0, 1000, …就是相应TCP报文段首部的序号字段.
主机B填进报文段的确认号是期望从主机A收到的下一字节的序号, 比如B收到了A发送的0~535的个字节, B就会等待A的数据流中536及以后的所有字节, 所以确认号字段就是536. 但是如果B收到了0~535和900~1000的字节流, 确认号依然是536, 因为B只会确认流中第一个丢失字节为止的字节, 这就是TCP的累计确认(cumulative acknowledgment). 而RFC其实没有规定中间丢失的报文段怎么处理, 这一部分是留给开发的事. 要么丢弃失序的报文段, 要么等待缺少的字节, 因为后者对网络带宽更有效, 在实践中常被采用.
下图是Telnet的例子, B收到了Seq=42表示已经成功收到了42及以前的byte, 所以ACK=43作为回复, 表示还要43byte以后的数据. 返回’C’是Telnet的回显, 保证用户发送的字符被服务器接收.第三个报文段唯一目的是确认A已从B收到数据, 该报文段数据字段空.
往返时间的估计与超时
超时重传机制的这个超时时间是必须大于一个往返时间RTT的, 估计这个RTT就显得尤为重要. 一个报文段的样本RTT就是从报文段发出(交给IP)到收到确认的时间. 大多数TCP会在某一个时刻测量一个SampleRTT(不能测量重传的报文段), 加权平均的公式为$EstimatedRTT = (1-\alpha)\cdot EstimatedRTT + \alpha\cdot SampleRTT$, 在RFC6298中给$\alpha$的参考值是0.125.
测量RTT的变化在RFC6298中的公式为$DevRTT = (1-\beta)\cdot DevRTT + \beta\cdot |SampleRTT-EstimatedRTT|$, $\beta$推荐值是0.25.
那么, 超时重传的间隔就是$TimeInterval=EstimantedRTT + 4\cdot DevRTT$, 初始值为1, 同时考虑了时间及其波动. (有点像自动控制的pid).
可靠数据传输
例子
3-34是理想的超时重传. 3-35中的seq=100
的报文段没有重传, 因为重传seq=92
时定时器已经重置, 而在新的重置时间内ACK=120
已经到了主机A.
3-36中因为ACK=120
已经应答了, 所以seq=100
实际上已经被主机B收到了119及之前的字节, 那么就不需要重传seq=92
了.
超时间隔加倍
每次TCP重传都将会让下一次的TimeInterval加倍, 防止网络阻塞导致源持续重传, 让阻塞加重.
快速重传
超时重传带来的问题是, 报文段丢失时, 如果超市周期比较长, 发送方在延迟长时间之后再重传丢失的分组, 增加端到端时延. 所以可以让发送端在收到3个冗余ACK后, 在定时器重启前就重传丢失的报文段. 现在的语音和图像流量用TCP一部分原因也是因为可以重传.
回退N步还是选择重传
TCP有选择确认机制, 让TCP接收方有选择地确认失序报文段, 而不是累计地确认最后一个正确接收的有序报文段, 从而忽略已经成功接受的报文段.
流量控制
TCP为应用程序提供了**流量控制服务(flow-control service)来消除发送方使接收方缓存溢出的可能性. 流量控制目的是匹配发送方的发送速率与接受方app的读取速率. TCP靠两端的发送方接收窗口(receive window)**来提供流量控制, 其指示了接收方还有多少可用的缓存空间. 在传输文件的例子中, 我们仅研究接收窗口. A通过TCP向B发送大文件, B为该文件分配大小为RcvBuffer的接收缓存. 定义LastByteRead
是B上的app从缓存读出的数据流的最后一个字节的编号, LastByteRcvd
是已放入B接收缓存的最后一个字节的编号. 接收窗口rwnd = RcvBuffer - [LastByteRcvd - LastByteRead]
, 也就是剩下的可以缓存的空间.
而A主要监控LastByteSent - LastByteAcked(已发送但是未确认), 使其小于rwnd. 但是如果B缓存已满, 发了rwnd=0给A, 并且后面B没有数据发给A, 那么就会导致A阻塞, 因为B之后清空了缓存之后A也无法知道, 为了解决这个问题, 即使rwnd为0, A依然会向B发送1个字节的报文段, b收到后清空缓存并ack, 告诉A rwnd值, 解决了0 window的问题. 事实上这就通过滑动窗口(TCP头的那个窗口), 让接收端控制发送端.
TCP连接管理
三!次!握!手!
假设C是客户端, S是服务器.
- C向S发送TCP报文段, 该报文段不包含应用层数据. 报文段首部
Syn = 1
,Seq=client_isn
是随机的, 放在序号字段中. 然后报文段会被封装到IP数据报中, 发送给S. 这就是SYN报文段. - S取出报文段, 为该TCP连接分配TCP缓存和变量, 并向C发送允许连接报文段. S的Syn字段被设置为1, 首部确认号为
client_isn+1
,server_isn
由S自己设置. 这就是SYNACK报文段. - C为该链接分配缓存和变量. 返回给服务器端一个报文段. 这里的Syn被设置为0. 并且这个阶段可以在报文段负载中携带C到S的数据.
四!次!挥!手!
- C向S发送报文段, 其中FIN为1. C进入FIN_WAIT_1状态.
- S返回ACK表示收到了该报文段. C进入FIN_WAIT_2状态.
- S关闭后(把剩下的数据发完)向C发送终止报文段, 其中FIN为1.
- C向S返回ACK, 进入TIME_WAIT状态. 如果ACK丢失, C会重传最后的确认报文.
阻塞控制原理
这里主要讲异步传递方式(ATM), 和**可用比特率(ABR)**来讨论阻塞控制.
原因与代价
用3个例子来了解阻塞.
-
两个发送方和一台有无穷大缓存的路由器. 其中共享输出链路的容量是R. 下图是主机A的性能和端到端的平均时延, 当$\lambda_{in}$超过R/2时, 他的吞吐量只能到R/2, 因为A, B共享链路. 可以看到当发送速率接近R/2的时候, 时延的微分也会逐渐增加. **这里的代价就是当分组到达速率接近链路容量时, 分组经历巨大排队时延**.
-
两个发送方和一台有有限缓存的路由器. 因为缓存有限, 所以路由器会丢包, 所以这个例子引入重传, $\lambda_{in}^{'}$表示供给载荷. a中是最理想的假设: A可以知道路由器缓存是否空闲, 缓存空闲才发送分组. b中是确认丢包才重传, **这里的代价是发送方必须重传来补偿因为缓存溢出而丢失的分组**. c中的情况是, 发送方提前发生了超时, 而初始数据分组和重传分组其实可以到接收方. **这里的代价是不必言的重让路由器用链路带宽转发不必要的分组副本**.
-
4个发送方和多个有缓存的路由器组层的多跳路径. 这里的代价是一个分组在一条路径被丢弃, 上游路由器从转发到丢弃次分组而使用的传输容量被浪费.
阻塞控制方法
todo
TCP阻塞控制
TCP能根据网络的阻塞程度, 通过**阻塞窗口(cwnd)**来限制发送流量的速率(实际上是cwnd和rwnd的最小值, 但是在后面的例子中, 我们假设TCP接收缓存足够大, 所以可以不考虑rwnd). 之前的流量控制只是知道了接收端和发送端的状态, 而阻塞控制相当于是通过整个网络的状态来调整TCP segment的发送.
慢启动
TCP连接开始时会使cwnd从1个MSS开始, 每收到一个确认就再对每个确认增加1个MSS. 结束这种增长有3种情况.
- 如果发生了超时指示带来的丢包/阻塞, 那么cwnd又会从1开始重新慢启动.
- cwnd达到了慢启动阈值ssthresh(cwnd/2), TCP就转移到阻塞避免状态.
- 检测到了3次冗余的ACK, TCP开始快速重传并进入快速恢复状态.
阻塞避免
每个RTT只将cwnd增加一个MSS, 也就是每个到达的ACK增加MSS/cwnd byte. 改变这种状态有2种情况.
- 超时指示的丢包, 将ssthresh设置为cwnd/2. 这个时候认为网络阻塞了.
- 冗余的ACK, ssthresh设置为cwnd/2, cwnd减小为之前的一半(已经收到的3个MSS也会加上), 进入快速恢复状态. 这个时候认为网络实际上没有阻塞.
快速恢复
对收到的每个冗余的ACK, cwnd增加一个MSS. 当丢失报文段的ACK到达后, TCP就在降低cwnd之后进入阻塞避免状态. 如果出现超时, 那么cwnd又会从1开始重新慢启动, 并且设置ssthresh设置为cwnd/2. 对于冗余的ACK, 注意Tahoe和Reno的区别, 较早的Tahoe无论超时/3个冗余都会重新慢启动. 而Reno的cwnd变成了阈值加3个MSS.
实验
UDP
看抓包的详细信息, 首部字段为源端口号, 目标端口号, 报文长度, 校验和组成. 每个字段都对应4个16进制位也就是16bit, 2byte. Ipv4的ip数据报长度就是20byte的ip头+8byte的UDP头+UDP数据.
我们可以推出, 因为长度最长是2byte所对应的数字, 也就是2的16次方, 65536个byte, 那么去掉8byte的头, 数据区最大只能是65528byte. 考虑到IP头其实也有20byte, 实际上数据区必须小于65528byte. 端口号最大也只能到65535.
从网络层来看, UDP的协议号是17(16进制 11).
TCP
我直接使用了作者的抓包记录. NO.1是第一次握手, 用SYN=1
请求建立连接, 序列号是seq=232129012
.
NO.2是第二次握手, 序列号也刚好为0. ack=232129013
就是客户端的seq加1.
下图的PSH代表有数据传输了, 在TCP segment data中可以看到POST命令. 此时的序列号为swq=232129013
. 可以看出最后一次握手后, 服务端没有向客户端发送TCP报文段.
参考
- 计算机网络-自顶向下方法(第6版)
- Computer Networking A Top-Down Approach(7th edition)
- 作业
- 习题答案
- wireshark实验
- UDP协议校验和计算
- TCP 的那些事儿(上)
- TCP 的那些事儿(下)
comments powered by Disqus