TCP汇总
- 头部分析
- 三次握手
- 四次挥手
- 滑动窗口
- 拥塞控制
- 效率问题
- 两个重传
从外部看TCP
- 计算机网络5层协议:物理层,数据链路层,网络层,传输层,应用层。
- TCP属于传输层(数据包到达计算机后,通过传输层确认分派的端口),下接网络层的IP协议,上接的应用层有SMTP、TELNET、HTTP、FTP。
- TCP是可靠的传输:发送方发会确保接收方接收到发出的信息。
- 由于tcp所依赖的ip协议并不是可靠的协议(这里意思是发送方发出的包无法确定是否正常接收),因此可靠性是TCP内部实现的。
- TCP与UDP同时传输层协议,做一个简单的比较:
- 效率优先,少部分包丢失无伤大雅的可以选择UDP,像实时直播等;准确性优先,效率低一点也能接受的则可以选择TCP,像文件传输等。
TCP头
1. 源端口号,目的端口号
- 记录发送方和接收方的端口号。
2. 32位序列号seq:
- 可以用于标记包的编号,方便与接收方沟通该包是否收到。
- 序列号的生成算法:
- 开始创建连接握手的时候,初始化seq=0,此时没有携带有效数据,但有SYN标志,此时下一个包的seq会加一。
- 序列号由上一个包的序列号加上上一个包的有效长度获得。如果是链接握手的话,
- 序列号溢出问题:
- 序列号是无负数的整数,溢出会自动归零,通常影响不大。
- 若告诉传输可能会出现回绕的问题:TCP头时间戳选项与回绕序列号
3. 32位确认号ack:
- 用于告诉发送端下一次要发送的包的序列号应当是多少,发送方可以因此知道发送的包是否被收到。
- 确认号由最近一次收到的包的序列号加上该包的有效长度,若是收到SYN包则上一个包的seq+1;(跟序列号计算方法是一致的)
- 例子:A发送一个seq=50的包给B,长度是100;B此时的ack=50;
- B接收成功:B会向A发送ack=150,当然也有一个对应的seq。A发现B发过来的ack是150就知道已经接收成功了。下一次发消息,A的seq就会变成150.
- B接收失败:因为B根本没有接收到消息,因此ack仍然是50,没有变化。下一次A发送消息,该消息seq=150,B会发现不对劲,要求A发送seq=50的包,A就会知道seq=50的包没有成功发送。
4. 首部长度
- 记录当前tcp包头的长度。默认为32bit,因为tcp头可以有可选部分,32bit不是定死的长度,因此需要有这个字段来记录这个长度。
- tcp头固定长度有32bit,因此tcp至少有32bit那么长。
5. 保留字
Reserved for future use. Must be zero. (rfc793 16页)
必须全0,说是留给以后使用...
6. 控制字
- URG: 标识紧急指针是否有效。
- ACK: 标识确认。
- PSH: 用来提示接收端应用程序立刻将数据从tcp缓冲区读走
- RST: 标识要求重新建立连接。
- 例如A和B已创建了连接,A此时又发送SYN请求创建连接,则B会认为A出错,因此置RST为1,从而请求放弃当前连接,让A重新与B创建连接。
- SYN: 标识请求建立连接。一开始都会先发送SYN报文请求远端创建连接。
- 例子:A想请求跟B创建连接,A就会发一个SYN为1的报文给B。B确认了A的报文也会给A发一个SYN报文,详见三次握手。
- FIN: 标识关闭,即通知对端,本端即将停止发送有效数据。
- 若A与B连接中,A没有数据要发给B了,则A会发送一个FIN为1的报文给B,表示我这边不再发送有效数据,但仍可以接收B发过来的有效数据。
- 若双方都发送了FIN包,则表示当前连接可以中断。
7. 窗口大小
- 表示当前本方希望接受的数据量,用于告诉对方本方当前的窗口大小。
8. 校验和
- 校验和是啥:校验和是把TCP报文通过计算生成一个16bit长度的数据。
- 作用:用于判断报文数据是否被修改。
- 原理:接收方收到TCP报文后重新通过报文生成校验和,并与报文中的校验和比较,若一致则数据没有被修改。
- 进行校验和的数据:TCP虚拟头,TCP头部,数据体。
- 虚拟头:虚拟头包括源ip,目的ip,数据协议,tcp报文长度(该长度仅是TCP头和数据体的总长度,不包括TCP虚拟头的长度。)
- 虚拟头长度固定是96bit(4*32bit)
- 虚拟头:虚拟头包括源ip,目的ip,数据协议,tcp报文长度(该长度仅是TCP头和数据体的总长度,不包括TCP虚拟头的长度。)
9. 紧急指针
- 该指针是一个正偏移量值,表示当前数据体偏移多少位开始是紧急数据。
- 该指针偏移量需要标志URG设为1才能使用。
10. 选项
- 选项是可选的数据,不一定有。
- 选项字段的拓展阅读:网络-----TCP报头中的选项字段
11. 数据体
- 数据体放的是有效数据,这个有效数据是对于TCP来说的,即是上层传下来的数据。这里可能包含上层的协议头等信息,并非完全就是用户要发送的数据。
TCP机制
三次握手,四次挥手,窗口控制。
三次握手
- 初始态:服务器B正在监听态,等待被连接;客户机A准备向服务器发出请求。
- 第一次握手:A向B发送SYN报文(SYN标志位是1)。此时A进入SYN-SENT状态,等待B的回复。
- 第二次握手:B收到请求会回复A,发送SYN-ACK报文(SYN和ACK标志位都为1),进入SYN-RCVD(SYN-RECEVIED即SYN接收状态),等待A的再一次确认。
- 第三次握手:A收到B的报文,会发送ACK报文。此时A认为连接已创立。B收到A的ACK报文之后也认为连接已创立。
- 开始数据传输。
1. 为什么不能两次握手?
- 问题详述:A发送请求报文,B收到并回复,连接就创起来了,为什么要A再一次回复B呢?
- 假设只需要两次握手,如果A发的第一条消息a因种种原因没有及时到达B,那么A会重新发送一条请求连接消息b,若此新消息b能到达B,B会回复一条确认消息c而成功创立连接。若此时第一条消息a经过百转千回终于到达了B,此时分两种情况:
- A和B刚创建的连接已经交流完毕并断开了,此时B收到了A发送的那条迟到的请求信息a,以为A又要创建新的连接,就返回一条确认信息d,B开启了新的连接。此时A收到d觉得莫名奇妙,此时分两种假设:
- 若A一条返回RST消息,B收到后会关闭连接。
- 若A觉得他目前跟B完全没有交集,这条也不是B的请求连接消息,因此不理会B发的这条确认消息,这就会导致B以为连接已经创建了,而且消息可能丢失了A没收到,因此不断重发,消耗了大量的资源。重发机制下面会讲。
- 若A和B创立的连接且还在交流中,B突然收到迟到的消息a,此时分两种情况:
- B觉得莫名奇妙,以为A出了故障,因此会发出RST报文要求关闭当前连接,因此这次连接被提前中断了。
- B忽略掉A发的这条信息,不作任何回应,连接继续保持。
- A和B刚创建的连接已经交流完毕并断开了,此时B收到了A发送的那条迟到的请求信息a,以为A又要创建新的连接,就返回一条确认信息d,B开启了新的连接。此时A收到d觉得莫名奇妙,此时分两种假设:
- 在三次握手中,
- 如果断开了连接之后,B突然发送一条确认消息过来会被A忽略,B也因为没有收到最后一次确认消息(没有第三次握手)而没有打开新的连接。
- 如果还没有断开原来的连接,B突然收到了一条确认消息,具体怎么做暂时还没有查到...不过既然能够稳定的运行,应该是要么直接忽略;要么发现seq不符合B的预期,重新向A发送B预期的序列号。
- 结论:我觉得即使是两次握手还是可以正常执行的。不过因为B过早的开启了新的连接,虽然可以通过后续判断关掉这个新连接,但已经不可避免对B造成了资源的浪费。
2. 为什么不能四次握手?
回答:三次能解决的事干嘛要用四次?
四次挥手
- 连接中:A和B进行数据交流。
- 第一次挥手:A数据已经发送完毕,向B发送FIN报文,等待B的确认报文
- 第二次挥手:B接收到报文,向A发送ACK报文。
- 半关闭:A接收到ACK报文后进入半关闭状态.此时,A不再向B发送数据,只接收数据。
- 第三次挥手:B数据发送完毕,向A发送FIN报文。这里上面图片显示Server发的是FIN-ACK报文,但rfc793说A只需要接收到FIN标志即可认为对方希望关闭连接。
- 第四次挥手:A接收B的FIN报文,回复ACK报文。
- 等待时间:A会等两个MSL单位的时间。
1. 为什么A要等待2MSL的时间才关闭?
- MSL:MSL(最大分段生存期)指明TCP报文在Internet上最长生存时间,每个具体的TCP实现都必须选择一个确定的MSL值。RFC 1122建议是2分钟。
- 假如不等两个MSL的时间,A发完最后一条ACK消息立刻关闭服务器。若A最后一条ACK消息丢失,B会一直重传FIN报文,但由于A已经关闭连接,B的重传不会得到回应,需要重传到指定次数才会放弃,其中会消耗大量资源。
- 假如有两个MSL等待时间,若最后一次ACK报文丢失,B会重传FIN报文,A接收后会再一次发送ACK报文,使B能够及时关闭,避免B资源浪费。
- 总结:本质上A等待2MSL的时间是用来确保最后一条ACK消息能已经被B接收,也避免了B的资源浪费。
2. 为什么同样是资源浪费,我们选择了让A等待2MSL而不让B通过重传失败而关闭连接?
- 从效益上看:由于2MSL时间远小于重传失败到放弃的时间,因此2MSL的资源相对小。
- 从网络上看:重传会不断产生报文,造成网络压力,而等待2MSL只是监听是否还有消息到达,不会产生新报文。
- 从责任看:是因为A发送的报文丢失了才导致这个问题,应该由A来解决维护。
滑动窗口
MSS:
- MSS:最大报文段长度。在发送SYN的时候,双方会互相告知本方支持的最大报文长度,最终双方会选择小的值作为最大报文长度。
- 由于有MSS的限制,如果发送一个大小超过MSS的数据,会被分割成多个TCP包发送。
为什么会出现滑动窗口?
- 由于TCP是可靠传输,但IP协议并不可靠,因此TCP通过确认机制来确保消息是否发送成功,即回复方通过发送确认消息让发送方知道消息已送达。
- 但倘若发送方每发一条消息要等待确认,再发送新消息,效率会非常低下。由此人们想到了可以让发送方一直发送新消息,接收方一直接收消息,一边发送确认消息,从而提高效率,所谓的滑动窗口也就出现。
滑动窗口是啥?
- 由于确认机制的存在,发送方要用一个队列记录下发送出去的报文,从而等待这些报文被确认。
- 这时候还有一个问题,接收方处理报文的速度可能比发送方要慢,因此发送方不断的发送报文可能导致接收方的缓存爆仓从而报文被丢弃。这个时候发送方要控制发送的数量,假如获知对方的接收队列快满了或者已满了,则停止发送(怎么获知的后面会讲)。
- 合起来看,就是发送方有一个队列有序的记录着要发送的字节(队列每个节点的单位不是报文,而是字节,这与字段“窗口”的单位相匹配,用于动态调整窗口大小,详情下面有讲)。有两个指针——发送指针,待确认指针。
- 发送指针,该指针指向最后发送的字节,指针所指字节及前面的字节都是以被发送过的;tcp是按报文发送的,一个报文包含多个字节,因此发送方发送一个报文发送指针会向右移动多个格子(移动的数量等于报文搭载的数据的长度)。
- 待确认指针,该指针指向最早发出但还没有收到确认消息的字节,指针所指字节的前面(不包括该字节)都是都是确认已被接收的字节。
- 补充:待确认指针一定要在发送指针后面,只有发送了才能确认。把前后指针连接起来就像一个窗口,如下图所示。前后指针都会往同一个方向移动,看起来就像一个滑动窗口。
- 窗口大小不是固定的,从确认旧报文再发出新报文之间总有个时间差,所以一般都是窗口先缩小再扩大,也有可能接收方处理能力突然提升,发送指针允许不断右移,而待确认指针因还没有收到确认包而不动,则窗口一直向右扩展。
滑动窗口的动态调整
- TCP有个“窗口”字段,用于告诉对方目前本方可接收数据大小,单位是字节。发送方获知了接收方的窗口大小后,会向接收方发送报文,这些报文的总和等于或小于接收方的窗口大小,从而最大程度利用资源提升效率。下图是比较特殊tcp交流过程,发送方每发送一次报文都能立刻获得接收方的确认包,新的接收窗口大小也记录在确认包中(通常发送方会发送多个报文才收到一次确认包)。
- 图引用自相关链接3
- 上面wireshark抓包结果的简图。
- 与简图对应的发送队列对照图
- 简图解析:
- 三次握手
- 上半部分接收方可能遇到较复杂的任务导致无法处理接收到的报文,接收窗口按照规律(当前窗口大小 = 上一次窗口大小 - 上一条报文的长度)递减,直至0堵塞,发送方停止发送。
- 接收方窗口大小增大,主动发送一条更新消息给发送方,发送方继续发送报文。
- 下半部分接收窗口仍然一边接收一边解析,由于解析速度加快,窗口甚至在接收了报文后仍然进一步扩大,发送方的发送速度也随之增大。
- 四次挥手
拥塞控制
背景
若发送方要发送的数据非常多,而接收方的接收窗口非常大,理论上发送方可以短时间内发送大量的数据给接收方。但数据能否传递到接收方取决于中间的网络传输效果,大量的数据突然涌向网络可能导致网络来不及处理从而导致丢包的现象,从而发送方将重发大量的数据,再一次导致网络塞车。因此合理的发送速度也是非常的必要。
概念
- 拥塞窗口:一次能发往网络的字节数。
- 慢开始:发送方从一个字节开始向接收方发送数据,若数据没有丢失(收到确认包),则下一次发送的窗口大小扩大一倍,以此类推。
- 拥塞避免算法:发送方向接收方发送数据,若数据没有丢失,则下次发送的拥塞窗口扩大1。
- 重复ack:接收方建立这样的机制,如果一个报文丢失,则对后续的报文继续发送针对该包的重传请求。
- 快重传:发送方收到三次针对同一个报文的重发请求时,会立刻重发该报文。
- 快恢复:出现重复ack现象时,发送方把拥塞窗口缩小一半,继续用拥塞避免算法发送数据。
- ssthresh阈值:一个阈值,当窗口小于阈值的时候用慢开始方法,当窗口大于阈值的时候用拥塞避免算法。
流程讲解
一开始用慢开始发送数据,到达阈值后改为拥塞避免算法。若出现重复ack现象,则启用快恢复和快重传机制。若出现超时时(指定时间内没有收到ack),则把阈值缩小一半,用慢启动重发数据。
例子
A向B发送数据,假设A的数据足够多,B的接收窗口足够大。一般初始的ssthresh是16,拥塞窗口初始大小为1.
- (慢开始)A开始向B传递数据:发送1个字节=》收到确认包=》发送2个字节=》收到确认包=》发送4个字节=》收到确认包=》...
- (拥塞避免算法)A向B发送数据达到阈值ssthresh,开始变成拥塞避免算法:发送16个字节=》收到确认包=》发送17个字节=》收到确认包=》...
- (重复ack,快重传,快恢复)A向B发送数据出现丢包现象:发送29个字节=》收到确认包=》发送30个字节=》收到多次重传请求=》重发丢失的包=>调整阈值和窗口为15=》发送15个字节=》收到确认包=》发送16个字节=》...
- (超时,慢开始)A向B发送数据时,突然有多个用户发送大数据导致网络非常阻塞:发送20个字节=》没有收到任何ack回应=》阈值设为15=》发送1个字节=》...
问题1:为什么要不断的扩大拥塞窗口,直到出现丢包?尝试拓展到一个可观的窗口大小后就一直保持着不好吗,效果甚至比快恢复还要快。
问题2:为什么遇到丢包的现象时要把窗口缩小一半而不选择上一次交付成功的窗口大小?
- 最近成功策略本质上也是想尽可能的缩小浪费的面积,提高网络的利用率。
- 网络传输失败可能有两个原因
- 一个是到达了当前网络的传输极限,没法再加快了,例如最快传30个字节的数据,每次一到31就丢包,如上图所示,这种情况用最近一次传输成功的窗口大小是合适的,甚至比快恢复还要好。
- 一个是有可能突然很多用户同时开始传输数据,网络突然出现阻塞,此时可能可传输的数据大小远小于窗口,若此时采用最近成功传输的窗口大小(大概率是拥塞算法,即当前窗口大小减1)大概率也是传输失败,这样往前追溯会重试很多次,浪费很多时间,且大量的发充实包也会加剧网络堵塞,造成恶性循环。
- 综上所述,使用最近成功的窗口大小这个策略无法适应第二种情况。网络的状态每一刻都在变化,不可能性能一直保持着一条直线那么平滑,因此第二种情况应当也被考虑其中。虽然把窗口缩小到重新扩大这个过程似乎浪费了一部分网络资源,却能够增大重发成功概率。
- 例如上面这个模型,如果网络质量突然下降的很快,用最近成功探测可能需要探测很多次,才能探测成功,效率反而差的多。
拥塞控制与滑动窗口
滑动窗口主要是限制数据发送的总数量,避免数据发过去了,但接收不来的现象,因此滑动窗口强调的是发送多少数据给对方。拥塞控制主要是限制短时间内发出去的数据,避免的是数据发不过去的问题,因此拥塞控制强调的是发送多少数据给网络。合起来看,发送方会先了解到接收方的接收窗口大小,再通过拥塞算法把要发送的数据发到网络之中。
例子
- 这里假设接收方接收窗口是14,且只接收不处理,所以这里接收窗口在逐渐减小。
- 只看发送方和接收方的话,发送方完全可以一开始就发出14字节的报文,但是由于拥塞控制的作用,发送方实际发送如图所示,有一个逐步试探且越发越多的趋势,从而探索网络能交到接收方手中的极限。
效率问题
- 糊涂窗口综合症,
- 发送方优化:Nagle算法
- 接收方优化:ack延迟,ack
先要知道有一个东西叫糊涂窗口综合症
当发送端应用进程产生数据很慢、或接收端应用进程处理接收缓冲区数据很慢,或二者兼而有之;就会使应用进程间传送的报文段很小,特别是有效载荷很小。
糊涂窗口综合症既会发生在发送方,也会发生在接收端。由此衍生了两个方案:Nagel算法->发送方;延迟ack->接收方。
Nagle算法
- 一个极端的例子引出这个算法的思想。
- Nagel算法的原理
一个极端的例子
- 场景:如果一个请求每次只传输一个字节。
- 分析:一般tcp报文最小头部有20个字节,ip头最小也是20个字节,假如数据体只有1个字节,那么整个报文来看我们用了40个字节来确保1个字节传输成功,某种程度上这种效率非常低。
- 改进:通过分析很容易发现数据体越大,这个包的效率也越大。但是由于ip协议的限制(分包)和TCP本身MSS限制,数据体是不可以无穷大的,但可以尽可能的大。
- 猜想:这里每次都是发送1个字节,我们可不可以大胆的把多个报文合并再一起发送呢?这时候产生了一系列的问题:
- 合并多少个报文后发送呢?或者说到达什么的条件才发送呢?
- 把多个包合并在一起会不会产生不必要的问题?例如怎么把包分隔开?
Nagle算法详解
- Nagle算法通过对小报文的限制,减少网络上小报文的数量,进而可以改善网络阻塞的问题。
- 实现:假如有多个小数据要发送,Nagle会把多个数据合并,直到达到以下任意一个条件才会发送:
- 收到之前所有包的确认包;
- 当前数据大小达到MSS;
- 收到FIN报文;
- 上述条件都未满足,但发生了超时(一般为200ms),则立即发送。
- 好处:
- 能适应低俗网络,在网络质量差的时候,减少了发向网络的报文数量,降低拥塞的概率。
- 能适应高速网络,随着确认报文回复的加快发送速度也会随之加快。
- 坏处:
- 增加了tcp的逻辑复杂度。
- 降低了小包的传输的速率。
Nagle算法中第一个报文怎么处理?
第一个报文直接发出,不管数据体的大小是多少,这样可以更好的适应高速网络。如果要累计到MSS大小的报文可能会由于应用一直没有产生数据而迟迟不能发出,这不符合具体应用。
延迟ack
- 实现:接收端收到报文后不立刻发送ack报文,而是等待一段时间(开启一个定时),如果这段时间有数据发送就捎上这个ack的信息;若直到超时也没有数据发送,才发送ack报文。
- 好处:
- 减少单独的ack报文:ack确认是在tcp的头部,没有数据体,可以合并到带数据体的tcp报文中。
- 合并ack报文:在延迟的时候收到多个报文,只用确认最后一个报文即可。
Nagle遇到延迟ack
假设有两个小于mss的报文要发送,发送方发出第一个报文,接收方接受后不立即确认,如果接收方没有数据发送,需要等待一个超时才发送ack。而发送方因为angle算法需要等待到ack包或者等待本方超时,两者都会造成较大的延迟。
解决方案
- 关闭angle算法: 不推荐
- writev:writev是linux的一个高级IO的函数,大概就是一次可以从多个地方读取信息。不太懂这里为啥可以解决这个问题。这里放个链接:高级I/O之readv和writev函数
两个丢包
RTO超时
发送方在发出一个包后会维护一个计时器,在指定时间内收不到确认ack,则判断为丢包,将重发这个包。
重发策略
发送方第一次发送报文之后会等待RTO的时间,linux中最小是200ms;若超时重发,下一次RTO会设为原来的两倍长,即400ms,再下一次是800ms,以此类推。重发次数最多是15次(加上第一次正常发送一个报文对多发送16次),若仍然失败,发送方会认为接收方异常而关闭连接。
- 最终16次重传下来耗时大概15分钟
- 四次挥手为什么最后发送ack那方要保留2msl的时间,因为一旦最后一个ack丢失,另一方要花费15分钟的时间重传FIN报文并等待对应的确认包,代价巨大。
关于RTO
- RTO:全称Retransmission Timeout,重发超时,即表示一个发出报文多长时间内没收到确认报文即为超时的时间。
- 具体的值:上述为了方便描述用了200ms这个值,实际上RTO是动态计算的,有经典方法和标准方法两种,而200ms只是方法中的一个参数,同时也是RTO能达到的最小值。详细介绍传送门:TCP系列13—重传—3、协议中RTO计算和RTO定时器维护
重复ack
当接收方连续接收报文时发现缺失了报文,会重复发送相同的ack去请求缺失的报文。例如发送方发了A,B,C,D,接收方收了A,C,D,接收方在收C,D都会发送B的请求包给发送方。发送方接收到3个即重发B。
为什么次数定为3个,定为2不是更快重发吗?
接收方这里可能是丢了B包,也有可能只是B包迟到了而已。如果接收方过了一会收到了B,就不会再发送B的请求报文,发送方也就没必要再重发B包增加网络负担。 至于次数3应该是经验主义。详细介绍传送门:TCP 快速重传为什么是三次冗余 ACK,这个三次是怎么定下来的?
一个例子总结两个重传
发送方发出了A[1-10],B[11-20],C[21-30],D[31-40],若接收方接收到的顺序是A,C,D,从C开始,后面每收一个包都会发送ack(B)给发送方,请求发送方发送B。此时分两种情况: 1. 乱序:如果过了一会,接收方收到了B,这时候会发送ack(E[41-50]),发送发若有E的数据就可以直接发送。 2. 丢包分两种: 1. 发送方连续收到3次ack(B)包,则立刻重传ack(B)包。 2. 发送方超时时间内没收到B的确认包,则慢启动重传B及后面的包。
上面例子的特殊情况
- 描述:如上面例子:发送方发出A,B,C,D,丢失了B,但接收方只在C,D发送了ACK(B)报文,由于没有收到3个ack(B),发送方理论上不会重发B包,怎么处理?
- 由于没找到具体这种情况的做法,这里大概分析了一下可能的方法:
- 等待B包RTO超时,走慢启动路线。这个感觉不太符合慢启动策略的场景,应该不会用这种办法。
- 接收方在接收了D包发送了ack(B)后,等待一段时间再次发送ack(B)包,那样就凑齐了3个ack(B),可以快速重传啦。我觉得这个办法比较靠谱。