最近几天读了《计算机网络自顶向下》和《TCP/IP详解卷1:协议》,整理出一些之前靠博客学不到的东西,在此分享给大家。被讲烂的三次握手四次挥手也会写,但会重点标注出看书学到的新知识。
TCP报文段结构
按照《自顶向下》中图3-29画的图
- 源端口号和目的端口号用来多路复用/分解
- 32bit的序号字段和32bit的确认号字段用来实现可靠数据传输
- 16bit接收窗口字段用与流量控制
- 4bit的首部长度字段表明了这个TCP首部的长度。因为有选项字段的原因,所以TCP首部的长度是可变的。但通常情况下选项字段为空,所以通常情况下的TCP首部长度为20字节
- 选项字段用于发送方与接收方协商最大报文段长度MMS,或者用于在高速网络环境下,作窗口调节因子
- CWR——拥塞窗口减、ECE——ECN回显、URG——紧急、ACK——确认、PSH——推送、RST——重置连接、SYN——用于初始化一个连接的同步序列号、FIN——该报文段的发送方已经结束向对方发送数据
TCP的连接与释放
三次握手
第一次握手:client客户端发送一个TCP连接请求,即将SYN置1,并且进入同步已发送状态,ISN(c)是客户端初始序列号(这个序列号如何选择后面会说)
第二次握手:server服务端本来是处于监听的状态,在收到TCP连接请求后,如果同意建立连接,那么发回一个TCP连接请求确认报文段,然后自己进入同步已接收状态。其中SYN和ACK位都置1,ISN(s)为服务端的初始序列号,ACK确认的序号是刚才客户端的初始序列号+1,因为ACK代表的是“下一次想要收到的序号“
第三次握手:client客户端发送一个普通的确认报文段,即ACK置1的报文段,Seq=ISN(c)+1,因为这是客户端发的第二条,比上一条+1,ACK=ISN(s)+1代表想要收到的服务器下一条报文序号。发完以后自己进入ESTABLISHED连接已建立状态。服务器收到以后也进入ESTABLISHED
最后一次握手的意义是:如果仅有两次握手就建立连接,那迟到的第二次握手也许会使服务器打开很多无效的连接
三次握手的目的不仅在于让通信双方了解一个连接正在建立,还在于利用数据包的选项来承载特殊的信息,交换初始序列号(Initial Sequence Number,ISN)
四次挥手
这里的图是截网课的,因为他画的很清晰
首先客户端和服务器都处于连接已建立状态
第一次挥手:客户端发送TCP连接释放报文段,FIN=1,ACK=1,seq=u即之前发送的序号+1,ack=v之前收到的序号+1,TCP规定FIN=1的报文段不带数据也要消耗1。进入了终止等待1状态
第二次挥手:服务器发送给客户端一个普通的TCP确认报文段,ACK=1,seq=v,ack=u+1。进入关闭等待状态
然后从服务器到客户端的也需要关闭,继续挥手
第三次挥手:服务器给客户端发送一个TCP连接释放报文段,FIN=1,ACK=1,seq=w,ack=u+1。然后进入最后确认状态
第四次挥手:客户端发给服务器普通的TCP确认报文段,ACK=1,seq=u+1,ack=w+1。然后进入时间等待状态
TIME_WAIT的作用:防止第三次挥手走丢了服务器不断重发第三次挥手无法关闭,浪费资源
有关seq和ack
初始序列号
seq是每个报文段都有的序号,在数据交换过程中不断累加,那么第一个报文段的序号是0或者1吗? 实际上,在发送用于建立连接的SYN之前,通信双方会各自选择一个初始序列号。初始序列号会随着时间改变,因此每个连接都有不同的初始序列号。
可以设想一下,如果序列号每次都从相同的值开始,那么假如两个连接用了相同的收发IP和端口号,后一次的连接就可能把前一次连接中延迟到来的TCP报文段当作是有效的报文段(理应当作失效的无用的废弃报文段)
Linux中,采用基于时钟的方案,并且针对每一个连接为时钟设置随机的偏移量。随机偏移量是在连接标识即四元组(双方的IP和端口号)的基础上利用加密散列函数得到的。
ack的值
ack的取值是,“我刚接收到的报文段的seq+1”,表示“我想要接收到的下一条的seq”
SYN洪泛攻击
详细的自己看书吧
TCP的超时与重传
TCP在发送数据时,会设置一个计时器,若至计时器超时仍未收到数据确认信息,则会引发相应的超时或者基于计时器的重传操作,计时器超时称为重传超时RTO。另一种方式的重传称为快速重传,通常发生在没有延时的情况下。若TCP累积确认无法返回新的ACK,或者当ACK包含的选择确认信息SACK表明出现失序报文段时,快速重传会推断出现丢包。
- RTT——Round-Trip Time 往返时间
- RTO——Retransmission Timeout 重传超时时间 RTO是根据RTT进行估计的,有很多种方法,这里不详细说明(也没仔细看这块)
基于计时器的重传
在连接设定的RTO内,如果TCP没有接收到被计时报文段的ACK,将会触发超时重传,当发生这种事情时候,TCP通过降低当前数据发送速率来对此进行响应 有两种方法降低当前发送速率:
- 基于拥塞控制机制减小发送窗口大小
- 每当一个重传报文段被再次重传时,增大RTO的退避因子。RTO暂时地乘上n来形成新的超时退避值 RTO= n*RTO 一般是加倍
快速重传
一句话:一旦收到3个冗余ACK,TCP就执行快速重传
为什么会出现冗余ACK
当接收方收到一个比期望的seq大的报文段,说明中间出现了缺失,说明可能有报文段走丢了或者发生了重排序。因为TCP没有设计发送“我没接到”这样的否定信息,所以TCP只能再次发送“我接收到之前的报文段了”这样的信息, 举个例子:
我问“你吃饭了吗?”,你回答“我吃了”,我再问“你吃了什么?”,你还是说“我吃了”,你重复说三次“我吃了”三次答非所问,那就说明你没有接收到后面的问题。
因为造成这种三个冗余ACK情况的原因是丢包,通常是因为网络拥塞,所以TCP在启动快重传的同时也会触发拥塞控制机制(后面讲)
TCP流量控制与窗口管理
首先别把流量控制和拥塞控制弄混了 流量控制是因为发送方和接收方各自的缓存大小有限,所以进行流量控制,以免发的太多,接收缓存溢出 拥塞控制是因为IP网络能力有限,发送速率太快会堵
两端的滑动窗口
TCP以字节为单位维护其窗口结构
就画两张图表示一下就很明白了
发送窗口如果变成零窗口,则会以一定间隔发送窗口探测
糊涂窗口综合症
自己看书吧
TCP拥塞控制
cwnd(congestion window) 拥塞窗口是反映网络传输能力。发送端实际可用窗口W就是接收端通知窗口awnd和拥塞窗口cwnd中的较小者: W = min(cwnd,awnd)
这里再次借用一张图
慢启动
慢启动的目的是,使TCP在用拥塞避免探寻更多可用带宽之前得到cwnd值,以及帮助TCP建立ACK时钟。通常,TCP在建立新连接时执行慢启动,直到有丢包时,执行拥塞避免算法进入稳定状态。
初始窗口一般取一个或者两个SMSS(发送方最大段大小),然后不断乘2,进行增长
拥塞避免
cwnd依次+1来进行增长,直到丢包了,将ssthresh = ssthresh/2,cwnd = 1然后重新开始慢开始算法
慢启动和拥塞避免的选择
正常来说,TCP连接总是处于慢启动或者拥塞避免过程,不会出现同时进行的情况。 所以决定采用哪种算法的关键因素是什么呢? 是慢启动阈值ssthresh
当cwnd < ssthresh,采用慢启动,当cwnd > ssthresh采用拥塞避免,相等时候随意
ssthresh慢启动阈值
ssthresh并不是固定的,而是随时间改变的,ssthresh的主要目的是,在没有丢包发生的情况下,记住上一次“最好的”操作窗口估计值,即记录TCP最优窗口的估计值下界
当发生重传时,超时重传和快重传都算,ssthresh = max(在外数据值/2,2*SMSS)
ssthresh的初始值可以随意设定,但最终会因为慢开始和拥塞避免算法配合找到合适的值,比如设置成2,然后在10的时候发生丢包,那么ssthresh就会变成5,ssthresh相当于增大了。
快恢复
刚才我们说当cwnd > ssthresh时候,执行慢开始算法:将ssthresh = ssthresh/2,cwnd = 1 但可以用快恢复取代慢开始算法:ssthresh = cwnd = ssthresh/2,也有将cwnd设置成ssthresh/2+3
就是不一刀把cwnd砍到1,节省一些时间
展望
接下来会研究一下其他层的协议,HTTP和HTTPS,然后是《深入理解计算机系统》,然后是《InnoDB技术内幕》,《Redis设计与实现》