计算机网络 - 运输层
多路复用与多路分解
一个进程并不是直接从运输层拿数据的,而是通过套接字。一个进程可能有几个套接字,是一种抽象,它像是进程与运输层的门户。运输层将数据交付到一个中间套接字,进程再从这个套接字里面拿
将运输层报文段中的数据交付到正确的套接字,称为多路分解;从源主机中的不同套接字手机数据块,将每个数据块封装成报文段,再将之递到网络层,称为多路复用
套接字有唯一的标识,并且每一个报文段有用来指示要交付的套接字的字段,这些字段一般会包含端口号,不同的运输层协议的所用的字段会不一样
端口号是一个 16 位的数,0~1023 是周知端口号,保留给周知的应用层协议使用
UDP 的套接字是由目的 IP 和目的端口号的二元组标识的。当 IP 数据报到达目的 IP 地址的主机时,接收主机的运输层检查其端口号,然后交付给端口号对应的套接字
TCP 的套接字则是一个四元组(源/目的 IP,源/目的端口号),到达的报文段只有与这四个字段全部匹配,才会交付到对应套接字(换言之 TCP 套接字是由这四元组标识的)
UDP
它只是把数据传过去而已,没有保证,没有拥塞控制,没有连接建立,首部包含 4 个 16 比特的字段:源端口号,目的端口号,长度,检验和
检验和是将这 3 个字段全部视为数字,加起来,然后取反。这样,在接收方,如果传输无误,那么这 4 个数字加起来应该是 16 个 1
TCP
在讲 TCP 之前,这本书讲了很多实现可靠传输的方法,有 ACK/NAK,停等,序号,重传,定时器,窗口,回退,选择重传
一些事实
TCP 是面向连接的,在发送数据之前,两个进程需要先握手,做一些初始化共组
TCP 的连接是一条逻辑连接,好似直接一条线连接了起来,但是背后非常复杂
TCP 提供全双工服务,并总是点对点的
进程一旦将数据推向套接字,接下来就是由 TCP 控制了。它会先将其引导至发送缓存,再时不时从缓存里面取。同样,接收方也有接收缓存。当然,数据的大小会受限于 MSS(最大报文段长度),MSS 由 MTU(最大链路层帧长度)确定,如果超过则会分块
TCP 首部
16 比特的源/目的端口,32 比特序号,确认号,4 比特的首部长度指示 TCP 首部长度,后面后 8 位是一堆 flags(SYN, ACK...),16 比特窗口字段,指示接收方愿意接受的字节数量,用于拥塞控制,16 比特校验和与紧急数据指针(一般不用),后面是一些选项
以上就是 TCP 首部,在这之后便是载荷数据
序号与确认号
其中序号是按照字节编址的,假设为第一个报文段分配序号 0, MSS 为 1000 字节,那么第二个报文段的序号为 1000, 第三个为 2000, 以此类推
关于确认号,我们只考虑主机 A 向主机 B 传输的情况:主机 A 向 B 发送报文,TCP 提供可靠传输,那么 A 如何确保 B 收到呢?答案是 B 在收到某一序号的报文段后,向 A 发送一个 ACK(确认)报文。不过 ACK 上的确认号不是收到报文的序号,而是 B 希望下一个接收的报文的序号。当 ACK 到达 A 时,A 传输这个报文,这样便可以保证传输的字节是按序的
如果 B 收到的不是下一个序号的报文怎么办?接收方可能丢弃,也可能先缓存下来。最后重新发送包含它所期望序号的 ACK,产生冗余 ACK(因为重复发送了)。而发送方可以根据冗余 ACK 操作
GBN- 回退 N 步
但是每发送一个报文,就要等待 ACK,收到后才能发送下一个,这样做太慢了。我们可以采用流水线和累计确认的方式:在我们刚刚提的策略中,发送方的报文有 3 种状态:已发送并确认收到的,已发送未确认的,即将发送的。我们维护如下几个变量:基序号:指向最早的已发送但未确认的序号;下一个序号:指向下一个发送的序号(下一个序号的前一个报文是已发送但未确认的报文);窗口长度:已发送但未确认的序号长度与即将发送的序号的长度的总长度,不能超过窗口的大小
最开始窗口内全是即将发送的报文,发送方一一发送,不需要考虑 ACK。这样,窗口内的已发送未确认报文逐渐增加,直到这些未确认报文占满整个窗口,这时发送方就要停止发送。当我们接到了符合基序号的 ACK 时,表示最早的发送但未确认的报文到达了,这时候我们将窗口前移,这样就空出了一个即将发送报文的位置
接收方采用的是累计确认的策略,即发送 ACK 时,就必须保证这个 ACK 之前所有的序号全部收到了,否则发送冗余 ACK
如果发送方迟迟没有等到期望的 ACK 的到来,说明出现了超时。此时发送方会重传所有的已发送但未确认报文
可以看到,单个分组的差错会引起大量的重传,为了避免不必要的重传,我们可以采取选择重传(SR)协议,非常简单来说,区别在于超时之后,只重传超时的那个分组,但是遇到窗口内有序号相同的分组时会出问题
TCP 的报文是有寿命的,所以这种 SR 会小心地确认网络中没有这个序号
TCP 的意见是用一种类似 GBN 与 SR 混合体的选择确认
往返时间估计与超时
TCP 会测量报文的往返时间,为超时重传提供参考,当然不是每发一个就测一次,只是在某个时候测量而已
因为网络的波动,不会把测量的 RTT 作为估值的 RTT,而是用一下公式:
EstimateRTT = (1-a) x EstimateRTT + a x SampleRTT
一般取 a = 0.125,这种方式称为指数加权移动平均
我们还会计算 DevRTT,来计算 SampleRTT 偏离 EstimateRTT 的程度:
DevRTT = (1 - b) x DevRTT + b x (SampleRTT - EstimateRTT)
b 推荐为 0.25
超时重传间隔一般为:
TimeoutInterval = EstimateRTT + 4 x DevRTT
超时间隔加倍
TCP 在每次超时事件发生之后,会将超时判定间隔设为先前的两倍(另一种形式的拥塞控制)
快速重传
当发送方收到 3 个冗余 ACK 时(实际已经收到 4 个),立即重传。这样可以不用等到超时触发后再重传
流量控制
和拥塞控制不是一个东西。流量控制是使得发送方发送的速度与接收方“消化”的速度相匹配,防止接收缓存爆了;拥塞控制则是处理网络拥塞的问题
TCP 通过让发送方维护一个接收窗口来提供流量控制,这个窗口提示发送方,对面的接收方还有多少缓存。假设主机 A 通过 TCP 向主机 B 传输一个大文件,主机 B 的接收缓存大小为 RcvBuffer,并维护两个变量 LastByteRead, LastByteRcvd,代表 B 从缓存读取的最后一个字节的编号,与 B 接收到放在缓存中的最后一个字节编号。显然,窗口大小 rwnd 为:
rwnd = RcvBuffer - (LastByteRcvd - LastByteRead)
主机 B 通过把 rwnd 放在 TCP 首部的接收窗口字段中,来通知主机 A
主机 A 维护两个变量 LastByteSent, LastByteAcked,显然在整个生命周期内:
LastByteSent - LastByteAcked <= rwnd
实际中还有一个问题。当主机 B 通知 A rwnd=0,并且此时 B 没有什么东西要发给 A 了,在这个情况下 B 就不会再向 A 发送报文,也就是说哪怕 B 的缓存已经清空,但是 A 并不知道,并因为收到了 rwnd=0 而停止发送
所以 TCP 规范要求,当 B rwnd=0 时,A 继续发送一个只有一字节数据的报文段,待缓存清空,B 需要就这个报文发送 ACK,并附带上新的 rwnd
TCP 连接管理
三次握手
客户中的 TCP 会用以下方式与服务器 TCP 建立连接:
客户首先互相服务器发送一个 SYN 报文段,不包含应用层数据,并随机选择一个初始序号 client_isn,并将其放在 SYN 报文段的序号字段中
一旦 SYN 报文段到达服务器主机,服务器提取出此报文段,并为该 TCP 连接分配缓存和变量,并向客户发送允许连接的报文段(其实在完成三次握手之前就分配,会使得 TCP 易于遭受 SYN 洪泛攻击)。这个报文也不包含数据,其 SYN 比特被置 1,确认号设为 cilent_isn + 1,服务器亦为自己选择初始号 server_isn,将其放在序号字段。这个允许连接报文被称为 SYNACK 报文段
在收到 SYNACK 后,客户也为该连接分配缓存与变量,然后向服务器发送另一个 SYN 置 0,序号为 server_isn + 1 的报文段,这个报文段可以负载数据了
完成这 3 个步骤,连接已然建立,客户与服务器可以互相发送包含数据的报文段了,当然,SYN 置 0。在创建连接的过程中,两台主机间发送了 3 个分组,故被称为三次握手
在第三次握手的时候,服务器确认收到了在第二步联系的客户的报文,是有必要的
cilent_isn 与 server_isn 决定了它们以后发送报文的序号
SYN 洪泛攻击
客户发送大量的 TCP SYN,但不完成第三次握手。服务器会为这些半开的连接分配资源,最终消耗殆尽。在大多数主流的操作系统中,会使用 SYN cookie 来防御(只是叫这个名字,没有真正用到 cookie)
当服务器收到 SYN 时,并不会生成一个半开连接。服务器会生成一个 TCP 序列号,由 SYN 报文的源和目的 IP 与端口号,还有一个仅有服务器知道的秘密数的一个散列函数,被称为 cookie,放在 SYNACK 的序号字段中
如果客户合法,那么再第三个报文段再散列一次,如果结果等于第三次握手的报文段的确认号 + 1,再生成连接。如果非法,对服务器也不会有影响
终止连接
简单来说就是,客户 TCP发送一个 FIN,服务器对此发送 ACK 确认。然后服务器发送 FIN,客户发送 ACK 确认
拥塞控制
在这里讨论的是不易网络辅助的拥塞控制机制,其实路由器可以直接向主机发送 ECN(明确拥塞通告)
拥塞窗口
拥塞控制又搞了一个额外的变量,叫做拥塞窗口 cwnd,结合一下就要求:
LastByteSent = LastByteAcked <= min(cwnd, rwnd)
指导原则
一个丢失的报文段意味着拥塞,出现丢失(超时或四个确认)时应当降低发送速率
一个确认报文段指示网络成功交付,因此,可以增加发送速度
ACK 指示无拥塞,丢包意味着拥塞。TCP 增加传输速率以响应 ACK,出现丢包后再减小
TCP 拥塞控制算法
包含三个部分:慢启动,拥塞避免,快速回复(非必要)。下面的讨论会暂时忽略 rwnd
慢启动
在 TCP 开始时,cwnd 设置为一个 MSS,每当收到一个 ACK,cwnd 就增加一个 MSS。这样 TCP 的发送速率就会指数型增长(收到一个后 cwnd 变 2,接着连发两个,收到两个 ACK 变成 4...)
当出现超时时,TCP 将 cwnd 设为 1 并重新开始慢启动过程,并将一个 ssthresh 的值设为 cwnd / 2
当 cwnd 的值到达或超过 ssthresh 时,继续翻倍就得小心了。此时 TCP 会结束慢启动并进入拥塞避免模式
如果检测到 3 个冗余 ACK,则执行快速重传并进入快速恢复
拥塞避免
进入这个状态,此时 cwnd 大概是上次的一半,可能离拥塞不愿,所以只是在一个 RTT(报文的往返时间)内增加一个 MSS
如果出现超时,ssthresh = cwnd / 2,cwnd 设为 1,回到慢启动状态
如果检测到 3 个冗余 ACK,执行快速重传并进入快速恢复
快速重传
如果那 3 个冗余 ACK 对应的丢失报文段,在重传之后到达,并收到了返回的 ACK,则回到拥塞避免
若还是收到同样的冗余 ACK,cwnd 加上 1 个 MSS,执行重传,保持当前状态
出现超时......这 3 个状态对于超时一个待遇
TCP CUBIC
这是另一种探测 TCP 发送速率的方法:令 K 为 假定无丢包状态下到达出现丢包的未来的时间点,CUBIC 以当前时间 t 与 k 的距离的立方来增加拥塞窗口
公平性
有空再写