其实搞懂tcp/ip协议,总体来说就是回答六个问题:
1.Tcp/ip协议是在哪一层生效的?
2.Tcp协议与IP协议的报文是什么样子?
3.Tcp如何实现三次握手、四次挥手的?为啥要这样做?每一步都在干嘛?可以优化吗?
4.Tcp是如何实现滑动窗口的?
5.Tcp是如何实现拥塞控制的?
6.Tcp的优点与实现原理是如何一一对应的?
第一章:网络分层模型 [1]
OSI模型:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层
tcp/ip模型:应用层、传输层、网络连接层、主机到我网络层
首先我们要明白:Tcp协议使用的就是传输层、Ip协议是在网络层
结合图可以看出,正常情况下,路由器只会解析IP协议,并不关心Tcp协议。为啥不关心?下面可以从报文入手看看原因。
第二章:IP报文结构 [2]
先看 IPv4的报文结构
(图中固定头部20个字节,每行4字节,32个bit位)
各个字段明细:
- 版本号:4bit。表明IP协议的版本号。一般为0100(IPv4),0110(IPv6)
- IP包头长度:4bit。用于描述IP包头长度,因为IP包头长度是可变的。这里所指示的长度,是以4个字节为一个单位。例如,一个IP包头的长度最长为“1111”,即15*4=60个字节。IP包头最小长度为20字节。
- 服务类型:长度8比特。
- IP包总长:16bit。 以字节为单位计算的IP包的长度 (包括头部和数据),所以IP包最大长度65535字节。
- 标识符:16bit。该字段和Flags和Fragment Offest字段联合使用,对较大的上层数据包进行分段(fragment)操作。路由器将一个包拆分后,所有拆分开的小包被标记相同的值,以便目的端设备能够区分哪个包属于被拆分开的包的一部分。
- 标记:3bit。第一位是保留位不使用。第二位是DF(Don't Fragment)位,DF位设为1时表明路由器不能对该数据包分包。如果一个数据包无法在不分段的情况下发送,则路由器会丢弃该数据包并返回一个错误信息。第三位是MF(More Fragments)位,当路由器对一个上层数据包分段,则路由器会在除了最后一个分段的IP包的包头中将MF位设为1。
- 片偏移:13bit。表示该IP包在该组分片包中位置,接收端靠此来组装还原IP包。
- 生存时间:8bit。当IP包在网络上传送时,每经过一个路由器,TTL就自动减一。值为0时,则丢弃报文。防止报文进入环路
- 协议:8bit。标识IP头后面的报文协议类型 以下是比较常用的协议号:
** 1 ICMP
** 2 IGMP
** 6 TCP
** 17 UDP
** 88 IGRP
** 89 OSPF - 头校验和:16bit。用来做IP头部的正确性检测,但不包含数据部分。由于路由器会改变TTL,所以路由器会为每个通过的数据包重新计算这个值。
- 源和目的地址:这两个地段都是32比特。标识了这个IP包的起源和目标地址。要注意除非使用NAT,否则整个传输的过程中,这两个地址不会改变。
IPv6的报文结构
- 版本:协议的版本,对于 IPv6 是 0110
- 流标号:“流”指互联网络上从特定源点到特定终点的一系列数据报,所有属于同一个流的数据报都具有同样的流标号。
- 有效载荷长度:表示 IPv6 数据报除基本首部以外的字节数。
- 下一个首部:当没有扩展首部时,下一个首部字段指出基本首部后面的数据应该移交给哪个高层协议。当出现扩展首部时,下一个首部字段的值表示后面第一个扩展首部的类型。
- 数据报图中经过的路由器不处理扩展首部。
- IPv6 采用冒号十六进制记法,如 68E6:8C64:FFFF:FFFF:0:1180:960A:FFFF,冒号十六进制记法允许零压缩,即一连串连续的零可以为一对冒号所取代,任一地址中只能够使用一次零压缩。
第三章:TCP的报文结构 [3]
TCP连接的特点:面向连接,提供可靠的服务,有流量控制,拥塞控制,无重复、无丢失、无差错,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),只能是点对点,首部 20 字节,全双工
TCP协议报文结构.png- 序号 :用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。
- 确认号 :期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。
- 数据偏移 :指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。
- 确认 ACK :当 ACK=1 时确认号字段有效,否则无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。
- 同步 SYN :在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1,ACK =1。
- 终止 FIN :用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放运输连接。
- 窗口 :窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。
- URG(紧急位):设置为1时,首部中的紧急指针有效;为0时,紧急指针没有意义。
- PSH(推位):当设置为1时,要求把数据尽快的交给应用层,不做处理
- RST:重置连接。
要点:
(1)序号:Seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。
(2)确认序号:Ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,Ack=Seq+1。
(3)标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:
(A)URG:紧急指针(urgent pointer)有效。
(B)ACK:确认序号有效。
(C)PSH:接收方应该尽快将这个报文交给应用层。
(D)RST:重置连接。
(E)SYN:发起一个新连接。
(F)FIN:释放一个连接。
那么TCP协议与IP协议的关系,则如下图所示:
第四章:TCP三次握手(每一次握手都是单向发起,单侧处理) [4]
第一次握手:客户端➡️服务端 [服务端知道客户端发消息没问题]
server猜测客户端要开始tcp请求,但也有可能是乱发的
报文:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。\
第二次握手:服务端➡️客户端 [客户端知道服务端收消息和发消息都没有问题]
客户端知道服务器是可以支持tcp的,并且告诉自己就是要连接tcp
报文:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
第三次握手:客户端➡️服务端 [服务端知道客户端收消息没问题]
server知道客户端是支持tcp的,且是要建立连接的
报文:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
第五章:滑动窗口(下面以一个半双工模型为例子) [5]
第一小节:如果没有滑动窗口,如何保证传输顺序?
最原始的方法就是,“一一确认”:发送方发送一个包1,这时候接收方确认包1。发送包2,确认包2。就这样一直下去,知道把数据完全发送完毕,这样就结束了。那么就解决了丢包,出错,乱序等一些情况。
同时也存在一些问题。问题:吞吐量非常的低。我们发完包1,一定要等确认包1.我们才能发送第二个包。整个过程,浪费了四个时间段。
第二小节:逐步优化减少耗时
这个就是我们把两个包一起发送,然后一起确认。可以看出我们改进的方案比之前的好很多,所花的时间只是两个时间段。
那如果数据不止两个包,我们如何去分配每一次发送包的数量呢?接下来,把这种思路优化一下,看看更优的滑动窗口模型是如何实现的。
第三小节:滑动窗口模型
在图中:
- 1、2、3包是灰色的,表示已经发送完毕,并且已经收到Ack。这些包就已经是过去式。
- 4、5、6、7号包是黄色的,表示已经发送了。但是并没有收到对方的Ack,所以也不知道接收方有没有收到。
- 8、9、10号包是绿色的。是我们还没有发送的。这些绿色也就是我们接下来马上要发送的包。 可以看出我们的窗口正好是11格。
- 后面的11-16还没有被读进内存。要等4号-10号包有接下来的动作后,我们的包才会继续往下发送。
正常情况:
可以看到4号包对方已经被接收到,所以被涂成了灰色。“窗口”就往右移一格,这里只要保证“窗口”是7格的。 我们就把11号包读进了我们的缓存。进入了“待发送”的状态。8、9号包已经变成了黄色,表示已经发送出去了。接下来的操作就是一样的了,确认包后,窗口往后移继续将未发送的包读进缓存,把“待发送“状态的包变为”已发送“。
整个过程窗口一直向后移动,确实“滑动窗口”的名字很形象。
丢包情况:
假设我们5号包发送出去了,但是迟迟未得到对方确认,我们的窗口就会停滞不前,变成下面这种情况:
(注意:对于接收方来说,这个Ack是要按顺序的。必须把5的ack发送过去,才能发6-11的Ack。这样就保证了滑动窗口的一个顺序。)
下面就会用到超时重传机制:
我们发现在规定的时间内收不到5号包的确认,会重新去补发一下。等收到5号ack确认,我们就可以继续将窗口向后滑动
一些小细节:
影响超时重传机制协议效率的一个关键参数是重传超时时间(RTO,Retransmission TimeOut)。RTO的值被设置过大过小都会对协议造成不利影响。
(1)RTO设长了,重发就慢,没有效率,性能差。
(2)RTO设短了,重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。
连接往返时间(RTT,Round Trip Time),指发送端从发送TCP包开始到接收它的立即响应所消耗的时间。
第六章:TCP的拥塞控制 [6]
tcp拥塞控制基于滑动窗口,它主要用到4个核心算法:慢开始(slow start)、拥塞避免(Congestion Avoidance)、快速重传(fast retransmit)、快速回复(fast recovery)
慢开始与拥塞避免:
拥塞窗口(cwnd,congestion window),其大小取决于网络的拥塞程度,并且动态地在变化。
慢开始算法的思路就是,不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。
为了防止cwnd增长过大引起网络拥塞,还需设置一个慢开始门限ssthresh状态变量。ssthresh的用法如下:
- 当cwnd < ssthresh时,使用慢开始算法。
- 当cwnd > ssthresh时,改用拥塞避免算法。
- 当cwnd = ssthresh时,慢开始与拥塞避免算法任意。
拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送发的拥塞窗口cwnd加1,而不是加倍。
无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞,就把慢开始门限设置为出现拥塞时的发送窗口大小的一半。然后把拥塞窗口设置为1,执行慢开始算法。如下图:
乘法减小:是指不论在慢开始阶段还是拥塞避免阶段,只要出现超时,就把慢开始门限减半,即设置为当前的拥塞窗口的一半(于此同时,执行慢开始算法)。当网络出现频繁拥塞时,ssthresh值就下降的很快,以大大将小注入到网络中的分组数。
加法增大:是指执行拥塞避免算法后是拥塞窗口缓慢增大,以防止网络过早出现拥塞。
快重传和快恢复
快速重传:
要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方),而不要等到自己发送数据时捎带确认。
快重传算法规定,发送方只要一连收到3个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计数器时间到期。
快速恢复:
当发送方连续收到三个重复确认,就执行“乘法减小”算法,把慢开始门限ssthresh减半。这是为了预防网络发生拥塞。请注意:接下去不执行慢开始算法。
由于发送方现在认为网络很可能没有发生拥塞,因此与慢开始不同之处是现在不执行慢开始算法(即拥塞窗口cwnd现在不设置为1),而是把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。
第七章:TCP四次挥手(每一次挥手都是,单向发起,双向处理)[7]
实际上是两次通知,
第一次:服务端告诉客户端说完了,客户端应答 【不应答,服务端还以为客户端还在听】
第二次:客户端告诉服务端说完了,服务端应答 【不应答,客户端以为服务器还没收到,还在听】
其中第二次和第三次是可以合并的(如果传输是同时完成的话)
第八章:TCP的特点 [8]
1、TCP是面向连接的运输层协议。 (从分层模型和报文结构可知)
2、每一条TCP连接只能有两个端点,每一条 TCP 连接只能是点对点的。(因为必须建立三次握手)
3、TCP提供可靠交付的服务。通过 TCP 连接传送的数据,无差错、不丢失、不重复,并且按序到达。(序号校验、顺序传输、重传机制)
4、TCP提供全双工通信。TCP 允许通信双方的应用进程在任何时候都能发送数据。TCP 连接的两端都设有发送缓存和接受缓存,用来临时存放双向通信的数据。
扩展:rst攻击
A和服务器B之间建立了TCP连接,此时C伪造了一个TCP包发给B,使B异常的断开了与A之间的TCP连接,就是RST攻击了。实际上从上面RST标志位的功能已经可以看出这种攻击如何达到效果了。
那么伪造什么样的TCP包可以达成目的呢?我们至顶向下的看。
假定C伪装成A发过去的包,这个包如果是RST包的话,毫无疑问,B将会丢弃与A的缓冲区上所有数据,强制关掉连接。
如果发过去的包是SYN包,那么,B会表示A已经发疯了(与OS的实现有关),正常连接时又来建新连接,B主动向A发个RST包,并在自己这端强制关掉连接。
这两种方式都能够达到复位攻击的效果。似乎挺恐怖,然而关键是,如何能伪造成A发给B的包呢?这里有两个关键因素,源端口和序列号。
一个TCP连接都是四元组,由源IP、源端口、目标IP、目标端口唯一确定一个连接。所以,如果C要伪造A发给B的包,要在上面提到的IP头和TCP头,把源IP、源端口、目标IP、目标端口都填对。这里B作为服务器,IP和端口是公开的,A是我们要下手的目标,IP当然知道,但A的源端口就不清楚了,因为这可能是A随机生成的。当然,如果能够对常见的OS如windows和linux找出生成source port规律的话,还是可以搞定的。
序列号问题是与滑动窗口对应的,伪造的TCP包里需要填序列号,如果序列号的值不在A之前向B发送时B的滑动窗口内,B是会主动丢弃的。所以我们要找到能落到当时的AB间滑动窗口的序列号。这个可以暴力解决,因为一个sequence长度是32位,取值范围0-4294967296,如果窗口大小像上图中我抓到的windows下的65535的话,只需要相除,就知道最多只需要发65537(4294967296/65535=65537)个包就能有一个序列号落到滑动窗口内。RST包是很小的,IP头+TCP头也才40字节,算算我们的带宽就知道这实在只需要几秒钟就能搞定。
那么,序列号不是问题,源端口会麻烦点,如果各个操作系统不能完全随机的生成源端口,或者黑客们能通过其他方式获取到source port,RST攻击易如反掌,后果很严重。
参考链接:
https://blog.csdn.net/weixin_42100064/article/details/102739531
https://www.zhihu.com/question/51074319
https://baijiahao.baidu.com/s?id=1654225744653405133&wfr=spider&for=pc
https://juejin.cn/post/6844903809995505671
https://www.cnblogs.com/postw/p/9678454.html【】
网友评论