前言
本文是我学习TCP中收集的资源,主要参考来源于网络并加以丰富,这里列举几个参考比较多的连接,如果知识复述有误,以原作者为准。
三元大佬的:(建议收藏)TCP协议灵魂之问,巩固你的网路底层基础 - 掘金 (juejin.cn)
潜行前行: 网络篇:朋友面试之TCP/IP,回去等通知吧 - 掘金 (juejin.cn)
TCP和UDP的区别
- TCP是
面向连接的,可靠的,基于字节流,可控制、有状态的传输层协议 - UDP是面向无连接的传输层协议
面向连接:
指的是客户端和服务端的连接,在通信前TCP需要三次握手建立连接,而UDP没有相应对的建立连接的过程。
可靠性:
TCP花了非常 多的功夫保证连接的可靠性,具体体现在有状态和可控制
有状态:TCP会精准记录哪些数据发送了,哪些数据被对方接受了,哪些没有被接收到,而且保证数据的顺序性。
可控制:当意识到丢包了或者网络状态不佳,TCP会自我调节,可以控制自己的发送速度或者重发
相应的,UDP就是无状态,不可控
面向字节流:
UDP的数据传输是基于数据报的,数据包是多少就发多少。这是因为仅仅只是继承了IP层的特性。TCP为了维护状态,就把一个个IP包变成了字节流
UDP的经典应用:DNS(域名解析),TFTP(文件传输)
网络层
掩码:用来和IP二进制的按位与运算,区分主机在哪个局域网的。
如果按位与运算的结果与局域网不同,则会选择下一跳进行再匹配。
如果相同,则会把包丢给下一跳,gateway
IP协议是不可靠协议,只负责传输,没有确认到达的机制,这一个机制是由上层协议TCP或者UDP来实现的
ARP及RARP协议
ARP是根据IP地址获取MAC地址的一种协议
本来主机是完全不知道这个IP对应的是哪个主机的哪个接口,当主机要向一个主机发送一个IP包的时候,会先从ARP高速缓存中查询。
如果查不到,则会向网络发一个ARP协议广播包,这个广播包有主机自己的IP地址和待查询的IP地址。
收到这个广播包的主机们,就会查询自己的IP地址是否符合,如果符合,则准备好一个包含自己的MAC地址的ARP包发送给ARP广播的主机
ICMP协议
IP协议不是一个可靠协议,他不能保证数据能够准确无误地传达到接收端。
而ICMP(网络控制报文)就是负责这一部分的工作,ICMP是网络层的协议。
当传送IP数据包发生错误,比如主机不可达,路由不可达等等。ICMP会将错误信息封包,然后传送给主机。给主机一个处理异常的机会。
ICMP中比较出名的ping应用指令。
链路层
网络层拿着下一跳地址去链路层找到下一跳的MAC网卡地址
ARP协议
ip在通信的过程中,是一直不会变得,代表的是端点
mac地址是会变的,代表的是下一跳,是一个节点的概念
负责的工作:
- 封装成帧:把网络层的数据报文,追加头和尾,帧头包括源MAC地址和目的MAC地址
数据可靠性传
- 校验和
- 序列号
- 三次握手四次分手
- 确认应答机制(ACK)
- 超时重传
- 流量控制
- 拥塞控制
TCP报文
源端口、目标端口
如何表示一个唯一链接?
答案:是TCP连接的四元组,源IP + 端口 和 目标IP + 端口
为什么TCP报文没有IP的信息,因为在IP层已经处理了IP,TCP只需要记录端口号信息即可。
序列号
即Sequence number,长度4个字节,也就是32位的无符号整数。如果到达最大值就循环到0。
序列号起的作用:
- 在SYN报文中交换彼此的初始序列号
- 保证数据包按正确的顺序组装
- 用于ACK确认消息是否成功传递
时间戳Timestamp
Timestamp是TCP报文首部的一个可选项
TCP的时间戳主要解决两大问题
- 计算往返时延RTT(Round-Trip-Time)
- 防止序列号回绕问题
计算往返时延RTT
在没有引入时间戳的时候,计算RTT,就会遇到上面两种问题
第一种情况就是,客户端传输第一次数据,但是服务端并没有响应,因此客户端进行重传,重传后,服务器并且也回应了。
如果以第一次发包时间为开始时间,则会出现RTT过长,应该采用第二次发包时间为基准
第二种情况就是,客户端传输第一次数据,服务端回应了,但是由于网络阻塞的原因,ACK迟迟没有到达客户端,客户端进行重传后,ACK到达了。如果以第二个发包时间为开始时间的话,就会导致RTT过短,应该采用第一个的。
因此我们得出结论,实际上无论以第一个发包时间还是第二个发包时间,都是不准确的。
因此引入了时间戳来解决这个问题。
场景:
现在a向b发送一个报文s1,b向a回复一个包含ACK的报文s2
-
a向b发送的时候,timestamp中存放的内容就是a主机发送时刻的
ta1, -
b向a回复s2报文的时候,timestamp中存放的就是b主机时刻的tb,timestamp echo字段是从是
报文中解析出来的ta1
-
a收到b的s2报文之后,此时a主机的内核时刻是
ta2,而在s2的报文中timestamp echo选项中可以得到ta1,ta2 - ta2就是 RTT的值
防止序列号回绕问题
产生问题的前提背景是:
因为网络阻塞,导致某个序列号滞留,然后大量的消耗序列号之后,又轮到了那个滞留的序列号那里。
此时那个滞留的序列号的包回来了,怎么区分谁是谁的呢,这就产生了序列号回绕的问题,
有了timestamp之后,即使序列号相同,他们的时间戳也不可能相同,这样就能区分两个序列号
三次握手
数据传输,可以建立所谓的连接
接受queue 和 发送queue 监听的队列
三次握手完了之后,会开辟资源,内核交互
socket(套接字):四元组。能够 表示绝对唯一的连接
- 源IP + port
- 目的IP + port
两次握手可以吗?
不可以,原因是无法确定客户端的接受能力
如果是两次握手就建立连接,假设当前客户端发了一个SYN请求想要请求建立连接,但是由于网络阻塞的问题,导致迟迟没有到达服务端,这时候客户端选择重连,再发了一个SYN请求,两次握手建立好了连接。
完成了全部的传输后,客户端和服务端断开了连接。此时滞留的SYN请求到达了服务端,服务端只需要回应一个ACK,双方就可以再建立连接。因此导致浪费了资源。但是客户端是断开的。
四次握手可以吗?
当然可以,100次握手都可以,但是为了考虑资源的问题,3次握手就可以了。
三次握手过程中可以携带数据吗?
第三次握手的时候可以携带,前两次不可以携带。
第三次握手的时候吗,客户端已经处于ESTABLISHED的状态,并且已经确认过服务端的接受和发送能力,这个时候相对安全,可以携带数据。
如果前两次都可以携带数据,那么就会有的坏人对服务器进行攻击,在每个SYN请求,携带大量数据,那么服务端势必会消耗更多的时间和内存空间去处理这些数据。
四次挥手
分手 分的是连接(内存资源,释放资源)
多了一次操作是因为服务端会把一些数据最后一次传给客户端。
等待2MSL的意义
1个MSL就是2分钟,2个MSL就是4分钟
- 第一个MSL:确保四次挥手中,主动关闭的那一方的最后一个ACK最终能达到对端
- 第二个MSL:确保对端的ACK重传的FIN请求可以到达
- 确保被关闭的一方能接受到主动关闭一方的最后一个ACK
- 避免上次TCP连接的数据包影响到下一次的TCP连接
让客户端等待2MSL,是为了保证客户端发出的ACK能够到达客户端。
假设在一个很坏很坏的网络中,最后一个ACK在1个MSL时间没有到达服务端就消失了,服务端会进行重传的操作,如果没有等待2MSL那么客户端直接就关闭了,无法收到服务端的重传消息,导致服务端会一重传。
如果客户端关闭了连接之后,和别的服务端在同一个端口号建立了新的连接,那么旧的请求将会干扰下一次的TCP连接。
为什么需要四次分手而不是三次?
当客户端发送了FIN请求给服务端后,服务端此时可能还剩余其他报文需要发给客户端,因此先发送ACK表示已经收到客户端的FIN,等全部的报文都发送完毕了之后,在发送FIN
三次握手的话,就是把FIN和ACK的发送合并为一次挥手,这个长时间,会导致客户端以为丢包了,从而让客户端不断地重发FIN
半连接队列和SYN Flood攻击的关系
半连接队列:在三次握手前,服务端的状态从CLOSED变为LISTEN,同时在内部创建了两个队列,半连接队列和全连接队列,即SYN队列和ACCEPT队列。
半连接队列
当客户端发送SYN请求给服务端,服务端收到以后回复ACK和SYN,状态由LISTEN变为SYN_RCVD,此时这个连接就被推入了SYN队列(半连接队列)
一来一回就是半连接队列
全连接队列
当客户端回复ACK,服务端接受之后,三次握手完成了,它会被推入另外一个TCP维护的队列,也就是Accpet队列(全连接队列)
完成了三次握手,就是全连接队列
SYN Flood攻击原理
当服务端接受到SYN请求后,就会为该请求分配一个TCB,通常一个TCB至少需要280个字节,在某些操作系统中TCB甚至需要1300个字节。
因此SYN请求攻击的根本原理,就是让服务端创建TCB从而消耗资源
SYN Flood属于典型的Dos/DDos攻击,原理是,通过短时间伪造大量不存在的IP地址,并向服务端疯狂发送SYN
- 首先这些请求会塞满半连接队列,导致无法处理正常的请求
- 由于是不存在的IP,那么自然无法收到他们回应的ACK,服务端就会不断地重发,浪费资源。
预防手段
- 增加SYN连接,也就是增加半连接队列的容量
- 减少SYN+ACK重试次数,避免大量的超时重发
- 延缓TCB分配,具体技术:SYN Cookie的技术或者是SYN Cache
SYN Cookie
原理跟HTTP Cookies技术类似,服务端通过特定的办法把半开连接信息编码成Cookie,用作B给A的消息编号,随着SYN-ACK消息一同返回给连接发起方,这样在连接完全建立前不保存任何信息。
- 如果A是正常用户,则会向B发送最后一个握手信息,B收到后,验证Cookie的内容并建立连接
- 如果A是攻击者,则不会向B发送ACK消息,B也没损失,也就是说单纯的SYN攻击不会给B造成连接资源的消耗。
但是这种技术的缺点也是非常明显。B不保存连接的半开状态,意味着丧失了重发SYN-ACK消息的能力,一方面会降低正常用户的连接成功率。并且额外多出编码Cookie这一环节也会消耗一定的资源。
SYN Cache
这种即使在收到SYN数据报文的时候,不急着去分配TCB。
构造一个全局的Hash Table,用来缓存当前系统所有的半开连接信息,连接成功则从Cache中清除相关的信息。
Hash Table中每一个桶(buket)的容量大小也有限制,当桶满的时候,会把旧的数据清除。类似一个LRU机制。
当服务端收到SYN请求后,会将半开连接信息加入到HashTable。最关键就是里面的key,这个key要包含SYN的消息中的部分信息,比如IP、port等。并且还会再做一步加密的操作。
无论是正常的SYN请求还是攻击者的SYN请求都会加入到这个HashTable中。
TCP快速打开的原理(TFO)
TCP Fast Open 就是 TFO,用的是SYN Cookie的技术
流程:
首轮三次握手
首先客户端发送SYN给服务端,服务端收到。
但这次并不是回复SYN + ACK,而是通过计算得到一个SYN Cookie,将这个Cookie放到TCP报文的Fast Open选项中。
客户端拿到这个Cookie的值缓存下来,用来后面的握手。
后面的三次握手:
客户端将之前缓存的Cookie、SYN和HTTP请求(重点)发送给服务端,服务端验证了Cookie的合法性之后就返回正常的SYN + ACK。
可以看出,采用了TFO之后,在未三次握手成功也可以发送HTTP请求,这个是最显著的改变。
优势:
服务端在拿到客户端的Cookie等信息之后,进行合法校验并通过后,可以直接返回HTTP响应,充分利用了1个RTT(Round-Trip-Time,往返时延)的时间提前进行数据传输,积累起来是一个比较大的优势。
TCP超时重传的时间如何计算
经典方法-----SRTT(Smoothed round trip time,平滑往返时间)
每产生一次新的RTT,就根据一定的算法对SRTT进行更新。
局限性:
在RTT稳定的地方表现还可以,而在RTT变化较大的地方,就不行。
标准方法也叫jacobson/karels算法
在SRTT基础上,引入了个新的变量RTTVAR,记录了最新的RTT与当前SRTT之间的差值,让系统能更敏锐的感知RTT的变化,更为简单地说就是提高了RTT的权重。
TCP的流量控制
对于发送端和接收端而言,TCP需要把发送的数据放到发送缓冲区,将接受的数据放到接受缓存区
而流量控制要做的事情,就是通过接收缓存区的大小,控制发送端的发送。如果对方的接收缓存区满了,就不能继续发送了。
TCP滑动窗口
TCP滑动窗口分为两种:发送窗口和接收窗口
发送窗口
SND:Sent
WND:window.size
UNA:unackKnowledge
NXT:next
发送窗口 = 已发送但未确认 + 未发送且未确认
接收窗口
流量控制过程
如果发送方发的速度很快,那么就会产生数据堆积,或者是接收方来不及接受。所谓流量控制就是让发送方的发送速率可以动态地控制,让接收方来得及接受。
接收方会通过发送ACK请求来告诉发送方,接收窗口rwnd的大小是多少,从而控制发送的速率。
当接收窗口大小为0的时候,发送方接收到此消息,就会停止发送数据
过一段时候后,发送方会发送一个TCP Keep Alive的报文,也就是窗口大小探测报文,去问问接收方的窗口大小,是否可以继续发送数据
滑动窗口面向流的可靠性
- 最基本的传输可靠性来源于确认重传
- 发送窗口只有收到对端对于本段发送窗口内字节的ACK确认,才会移动发送窗口的左边界
- 接收窗口只有在前面所有的段都确认的情况下,才会移动左边界
TCP的拥塞控制
流量控制是发送端和接收端之间,是端与端之间的关系,并没有考虑整个网络环境,如果网络特别差,特别容易丢包,那么发送端就要注意。拥塞控制就是负责解决此类问题。
- 拥塞窗口(Congestion Window,
cwnd) - 慢启动阈值(Slow Start Threshold,ssthresh)
经典算法:慢启动、拥塞避免、快速重传和快速恢复
拥塞窗口
拥塞窗口(cwnd)是指目前自己还能传输的数据量大小
发送窗口的大小 = Min ( 拥塞窗口,接收窗口 )
拥塞控制,就是控制拥塞窗口(cwnd)的大小
慢启动
刚准备进入传输数据的时候,是不知道网路的堵塞情况,如果一下子传送的数据超出了负载,那么就会产生很严重的丢包从而发生雪崩式的网络灾难
所以拥塞控制采用一种保守的算法,用来慢慢适应整个网络,有一种预热的感觉。这就叫做慢启动
- 首先,三次握手,双方宣告自己的接收窗口(rwnd)大小
- 双方初始化自己的拥塞窗口(cwnd)
- 在开始传输的一段时间,发送端每接受到一个ACK,拥塞窗口大小加1,也就是说,每经过一个RTT,cwnd都会翻倍。(每发送一个报文,都会有一个ACK确认,因此就会翻倍)
当然他也不能无止境地翻倍下去,他有一个阈值,叫做慢启动阈值。当cwnd达到这个阈值之后,涨幅就会变得很慢。而这部分的工作就叫做拥塞避免
拥塞避免
原本是每接受到一个ACK,cwnd就会加1。
现在达到了阈值,cwnd只能加1 /cwnd
因此一个RTT下来后,cwnd只会增加1,并不会翻倍增加。
快速重传
RTO(Retransmission TimeOut):超时重传间隔
在TCP传输的过程中,如果发生了丢包,即接收端发现数据段不是按序到达的时候,接收端的处理是重复发送之前的ACK。
比如第5个包丢了,即使第6,7个包到达接收端,也是返回给发送端第四个包的ACK。
当发送端接收到3个重复的ACK,意识到丢包了,于是马上重传第5个包,不用等一个RTO的时间再重传。
这就是快速重传,解决是否需要重传的问题。那么TCP会重传第5个包还是第5个包之后的所有包呢?这就是选择性重传需要解决的
选择性重传
TCP只需要重传丢包的那一个包即可
在报文首部的可选项中,加上SACK这个属性,通过left edge 和right edge告诉发送端已经接收到了哪些区间的数据包。这个过程就叫做选择性重传,解决的是如何重传的问题
快速恢复
发送端收到接收端3个ack之后,发现丢包,察觉到现在网络并不是很通畅,自己就会进入快速恢复阶段
在这个阶段,发送端会做出以下改变:
- 拥塞阈值降低为cwnd的一半
- cwnd的大小变为拥塞阈值
- cwnd线性增加
Nagle算法
延迟发包
试想,发送端不断地发送很小很小的包。这种频繁的发送时存在问题的,不光时传输的时延消耗,发送和确认本身也是需要耗时的。
而避免小包的频繁发送,将多个小包打包成大包,就是Nagle算法要做的事情
Nagle算法的基本定义:
一个TCP连接中,最多只能拥有一个未被确认的小段。所谓小段,指的是小于MSS尺寸的数据块,
并且该数据块没有收到对方发送的ACK确认该数据已收到
Nagle规则:
- 如果包长度达到MSS,则允许直接发送
- 如果该包含有FIN,则允许直接发送
- 发生超时,也立即发送
MSS(Max Segment Size):最大段
延迟确认
延迟ACK
接收方在接受到发送方的数据后之后,在很短的时间里,又接收到了第二个包。
那么接收方不会一个一个去回复,而是回复最近的那个数据包。相当于一起回复了。
注意:
并不是所有情况都可以延迟确认
- 出现乱序的包
- 需要调整滑动窗口
- 大于MSS的包
一般情况下,Nagle算法和延迟确认算法是不会一起使用的。
Nagle算法是延迟发
延迟确认算法是延迟收
这一来一回的,就会造成很大的延迟,本末倒置了,并不是我们想要的。他们是相悖的,相当于拔河的两端。
TCP粘包和拆包
TCP中MSS是发送报文段最大的长度,但是程序需要发送的数据大小可能会比这个值小很多,也可能比这个值大很多。
因此为了解决小包的频繁传送,TCP会将若干个小包打包成一个大包进行传送。
也会将超出MSS的大包,拆分成多个小包进行传送
在IP协议层、链路层、物理层都存在拆包、粘包
TCP粘包和拆包的解决办法
- 发送端给每个数据包添加包首部,首部中应该告诉接受端数据包的长度,这样接收端在接受到数据包之后,通过读取该字段,即可知道数据包的实际长度(解决了拆包的问题)
- 固定每个数据包的长度。不足的补特殊符号,比如换行符或者是空格之类的。(解决了粘包的问题)
- 添加特殊字符进行分割。