一.TCP基础知识
1.1什么是TCP
TCP 是?向连接的、可靠的、基于字节流的传输层通信协议。 面向连接:意味着一对一的连接,不可像udp一对多发送。 可靠:?论的?络链路中出现了怎样的链路变化,TCP 都可以保证?个报??定能够到达接收端; 字节流:TCP是一种流协议(stream protocol)。这就意味着数据是以字节流的形式传递给接收者的,没有固有的”报文”或”报文边界”的概念。所以?论我们消息有多?都可以进?传输。它只需要保证其,有序,不重复。
1.2为什么需要TCP
IP 层是「不可靠」的,它不保证?络包的交付、不保证?络包的按序交付、也不保证?络包中的数据的完整性。 如果需要保障?络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。 因为 TCP 是?个?作在传输层的可靠数据传输的服务,它能确保接收端接收的?络包是?损坏、?间隔、?冗余和按序的。
1.3TCP头部格式:
序列号(32位):TCP用序列号来解决网络传输中可能会出现的乱序问题,在建?连接时由计算机?成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送?次数据,就「累加」?次该「数据字节数」的??。?来解决?络包乱序问题。
确认应答号(32位):指下?次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。?来解决不丢包的问题。
控制位 :不同控制位只有一位,其只能为0、1
ACK:当其为1,确认应答字段变为有效字段,由于TCP需要确认应答,可以想象除了建立连接的第一个包以外,所有包的ACK都为1. SYN:: 当其为1,表示其想建立连接,设置序列号初始值以便开始建立连接。 **FIN:**当其为1,表示发送FIN的一方没有数据发送了,希望断开连接。由此可见TCP断开连接必定需要4次挥手,由于TCP是全双工的,其一次FIN一次ack只能断开一方,所以双方都断开则需要4次。 RST: :当其为1,代表连接异常,必须强制断开连接。
1.4TCP连接
1.4.1什么是TCP连接? 我们先看什么叫做连接,简单来说就是,?于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括**Socket(IP+端口号)、序列号和窗???(用来做流量控制)**称为连接。
1.4.2如何确定一个TCP连接? tcp四元组:源地址,目的地址,源端口,目标端口 源地址和?的地址的字段(32位)是在 IP 头部中,作?是通过 IP 协议发送报?给对?主机。 源端?和?的端?的字段(16位)是在 TCP 头部中,作?是告诉 TCP 协议应该把报?发给哪个进程。
1.4.3一个ip服务器监听某端口,最多可以连接多少TCP连接? 服务器通常固定在某个本地端?上监听,等待客户端的连接请求。 最大连接数=客户端IP数*客户端端口数 对于ipv4,最大理论连接数为2^32 *2 ^16,即2 ^48 当然,服务端最?并发 TCP 连接数远不能达到理论上限。
二.TCP与UDP
2.1UDP
UDP 不提供复杂的控制机制,利? IP 提供?向「?连接」的通信服务。 头部只有 8 个字节( 64 位),UDP 的头部格式如下:
2.2TCP与UDP的区别
1.连接 TCP面向连接,传输数据之前需要先建立连接,而UDP不需要连接 2.服务对象 TCP因为面向一对一的连接,所以只能提供两个端点的服务。 而UDP可以支持一对一和一对多以及多对多的交互。 3.可靠性 TCP保证可靠交付,不错不丢不重, 而UDP尽最大努力交付,不保证可靠性 4.拥塞控制以及流量控制 TCP具有拥塞控制以及流量控制机制,以此来保证数据传输的安全性 而UDP没有,即使网络拥堵,其也不会改变发送速率。 5.首部开销 TCP在没有额外添加选项的时候,头部为20字节(4B5行) UDP则只有8B,(4B2行) 6.传输方式 TCP是字节流式传输,无边界 UDP是按数据包发送,有边界 7.最大分片问题 TCP如果数据大于MSS,其在传输层就自行分片,且如果丢失了某个分片TCP会重传这个分片。 而UDP如果数据大于MTU,其在下一层IP层分片,如果丢失了某个分片,一般来说就算了,但也有实现可靠传输的UDP,其需要重传所有数据包。 8.应用场景 因为TCP的可靠性,FTP文件传输,HTTP/HTTPS都依赖于TCP。 因为UDP的高效性,其经常应用于包总量少的通信,如DNS,SNMP,视频音频多媒体,广播。
9.我们发现TCP和UDP包头部各有一个记录长度的,但是TCP记录的是“首部长度”而UDP记录的是“数据包长度”? 因为TCP可添加额外选项,其在不加的时候头部长度为20字节,但是加了就不一定了。而UDP头部是固定的8个字节不会变化。
数据包长度的意义在哪?其实没有什么意义,因为TCP的数据包长度可以用IP包的总长度减去IP和tcp头部的长度算出去来。 而到了UDP这里,为了保证计算机硬件设计处理的方便,首部长度是4字节的整数倍,所以UDP补充了这一部分。
三.TCP连接的建立与断开
3.1三次握手
一开始两端都CLOSED,然后服务器端主动监听某个端口之后,服务器端就处于LISEN状态。 第一次握手 客户端随机初始化一个序列号,置于字段中,同时把SYN标志置位1,发送SYN报文(不包含应用层数据),表示其想建立连接。之后客户端处于SYN_SENT状态。 第二次握手 服务器收到客户端的SYN报文之后,其也随机初始化一个序列号,SYN置1,然后在确认应答号字段填入刚才听到的客户端发来的序号+1,ACK置1,发送报文(不包含应用层数据)。之后服务器端就处于SYN_RCVD状态。
第三次握手 客户端此时收到服务器端发送的报文,还要ACK一次,同上ack的序列号+1并且ACK置为1即可。 这次发送的报文,可以携带应用层数据了,之后客户端处于ESTABLISHED状态。 当服务器端收到ACK之后也进入ESTABLISHED状态。?旦完成三次握?,双?都处于 ESTABLISHED 状态,此时连接就已建?完成,客户端和服务端就可以相互发送数据了。
从上?的过程可以发现第三次握?是可以携带数据的,前两次握?是不可以携带数据的 ?旦完成三次握?,双?都处于 ESTABLISHED 状态,此时连接就已建?完成,客户端和服务端就可以相互发送数据了。
为什么一定要三次握手建立连接?
我们回顾以下1.4的内容? 什么是连接:?于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗???称为连接。 所以重点一定在三次握手才可以初始化socket,序列号和窗口大小并建立tcp连接。 共有三个原因:
1.主要原因:只有三次握手才能——阻止历史连接的初始化(即两次不能)
比如有一个客户端,在网络情况不是很好产生了拥塞的情况下,连续多次发送的发送了SYN建立连接的报文。
- 客户端发送seq=90,网络拥塞,服务器没收到
- 客户端接着发送seq=100,网络拥塞服务器又没收到。
- 过了一会儿,网络畅通了,这个时候客户端本来想着等第二个报文的ACK,没想到旧的SYN(seq=90)这个时候到了服务器,服务器自然就回复一个SYN(随机)+ACK(seq=91)。此时客户端发现自己期望收到的ACK是101而不是91,便可以发起RST,表示连接异常,强行终止了这次连接。(所以客户端在第二次握手结束的时候判断出了这是一个历史连接)
-等再过一会儿,新的SYN到了服务器端,其开始按照客户端需要正常进行握手建立tcp连接。
如果是两次握手: 客户端在刚才的第二次握手就不会发现异常,其就建立了历史连接,而三次握手就不会。
即客户端:
- 如果收到历史seq(比其当前期望的ACK小)的ack,就能发现这是历史连接,发送RST来终止
2.三次握手就能——同步双方初始序列号(即不需要四次) 四次握?其实也能够可靠的同步双?的初始化序号,但由于第?步和第三步可以优化成?步,所以就成了「三次握 ?」。 ?两次握?只保证了??的初始序列号能被对?成功接收,没办法保证双?的初始序列号都能被确认接收。
3.三次握手能避免资源的浪费 如果只有两次握手:如果客户端的 SYN 阻塞了,重复发送相同的SYN 建立连接报?,那么服务器在收到请求后就会建?多个冗余的?效链接,造成不必要的资源浪费。
总结:
- 通过三次握手,能阻止历史连接的初始化,能同步双方初始化序列号,能减少不必要的连接数量。
- 两次握手:会造成历史连接的初始化,和多个冗余的无效连接,且无法同步双方初始序列号。
- 四次握手:三次握手理论上已经够了,不需要多余的四次。
初始序列号的产生
起始序列号ISN的产生是基于时钟的,然后再讲过hash算法,得到一个初始序列号。 为什么不使用相同的初始序列号 1.为了避免历史连接 2.为了安全考虑,防止黑客伪造序列号
IP层会按找MTU分片,为什么TCP层还需要MSS
- MTU:网络包的最大长度,以太网中为1500字节。包括IP头,TCP头,和数据部分。
- MSS:去除两个头部的数据部分,就是一个网络包所能容纳的最大TCP数据长度。
当IP层的数据超过一个MTU的大小,IP层就需要将其分片,保证每一个分片大小都小于MTU,由目标主机的IP层进行组装上传给目标主机的传输层。 但是,因为IP层是不保证可靠性的,IP层本身没有超时重传的机制,所以一旦一个IP分片丢失,所有IP报文都需要重传。 所以我们由传输层的TCP来负责超时重传,一旦发现某片TCP报文丢失,其不会ACK这个报文,那么发送方在超时之后就会发现对方没有收到,从而重传整个TCP报文(包括头部和数据),因此我们发现IP层本身不保证可靠的话,其分片是非常没有效率的。
所以,我们为了最佳的传输性能,在建立TCP连接的时候双方需要协商MSS的值,每当TCP报文大于MSS就分片,TCP的分片自然不会大于IP层的MTU,自然IP层不需要分片。 下图就是tcp在三次握手期间协商MSS的过程。
SYN攻击
TCP连接需要三次握手,假设攻击者短时间伪造不同IP发送大量的SYN报文,以占用服务器资源,攻击者在服务器SYN_RCVD并发送ACK之后却不回复第三次握手的应答,久而久之服务器的SYN接收队列就会占满,从而无法为别的用户提供服务。 正常服务器端的两个队列:
出问题之后解决方法:
1.修改Linux 内核参数,控制等待数据包队列大小,SYN_RCVD连接数目,队列满时直接丢弃。
- 网卡接收数据包速度大于内核处理速度时,会将数据包放入一个队列中,我们可以控制该队列的最大值。
- 可以修改SYN_RCVD连接的最大个数。
- 当超出处理能力时,所有新的SYN直接回复RST丢弃。
2.启用tcp_syncookies 我们发现受到SYN攻击时可能会导致,SYN队列满但是ACCEPT队列可能为空,这时我们启动syncookies,使后续SYN包不进入SYN队列,而是计算cookies,返回客户端一个序列号为(SYN+ACK)的包,此时如果客户端回复的ACK合法,证明其是合法客户端,直接将其加入Accept队列。 之后就可以顺利进行。
3.2 四次挥手
TCP连接断开需要经历四次挥手:
- 客户端没有数据需要发送了,打算关闭连接,此时发送FIN=1的报文,随后其进入FIN_WAIT_1状态
- 服务器端收到后,就应答ACK,然后自己进入CLOSED_WAIT状态。客户端收到ACK之后,进入第二阶段的等待FIN_WAIT_2状态。
- 服务器处理完数据之后,自己也没有数据需要发送了,向客户端发送FIN报文,随后自己进入LAST_ACK状态。
- 客户端收到FIN报文回复ACK,随后进入TIME_WAIT,等待2MSL就CLOSE. 服务器端收到ACK就可以CLOSE.
至此,每个方向的关闭都需要一个FIN和一个ACK,所以总共4次挥手。 注意主动关闭连接的一方才有TIME_WAIT状态(可以理解成主动提出分手的一方会等对方挽回一会儿)
为什么要四次挥手
- 关闭连接时,客户端向服务器端发送FIN,表示自己没数据发了,但是自己还能收。
- 服务器端收到FIN时,先应答一下ACK,表示自己知道你要走了,但服务器端可能自己还有数据需要处理和发送,等自己忙完了,才发送FIN给客户端表示自己也没有数据要发送了。
服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN ?般都会分开发 送,从??三次握?导致多了?次。
为什么主动结束的一方要等待TIME_WAIT=2MSL的时间
MSL(Maximum Segment Lifetime)报文最大生存时间。任何报文在网络中最大存在MSL的时间,超过这个时间的报文都将被丢弃。
主动发起关闭连接的一方,才有TIME_WAIT状态,TIME_WAIT的作用是什么
- 防止旧连接数据包:假设主动关闭连接一方将第四个ACK发出去之后直接CLOSE,这个时候就结束通讯。在小于2MSL的时间里,相同四元组(源地址,目的地址,源端口,目的端口)可能被重新建立,那么如果在上次四次握手之前网络中有延迟的包,会被新连接误接收。所以TCP让其等2MSL,这段时间足以让两个方向上所有延迟的包都被丢弃,使新连接不会误接收旧连接的包。
- 保证连接正确关闭(确保被动关闭的一方能收到第四个ACK),具体见下文。
客户端收到服务器端的FIN,为什么要等2MSL? 因为可能存在被动关闭的一方发送了FIN却没有收到第四个ACK,这个时候它重传FIN,那么最后这个包到达客户端还得被处理了再传回来,一来一回需要等待2MSL的时间。
linu系统中2MSL=60s。
四.重传机制(序列号与确认应答)
TCP 实现可靠传输的?式之?,是通过序列号与确认应答。
4.1超时重传
设定?个定时器,当超过指定的时间后,没有收到对?的 ACK确认应答报?,就会重发该数据,也就是我们常说的超时重传。 所以要么自己的数据包对面没有收到,要么对面回应的消息自己没有收到。
超时时间如何设置 RTT(Round-Trip Time 往返时延):是数据包的往返时间 RTO(Retransmission Timeout 超时重传时间)
- 如果RTO太小:会造成很多不必要的重传,比如对方其实已经收到了,正在处理,你就催催催,不必要的重传会导致网络负荷增大。
- 如果RTO太大:会使网络空隙时间增大,降低了网络传输的效率。
- RTO应该略大于数据包往返时间RTT。但由于RTT根据网络情况时常变化,所以RTO也随之动态变化。
超时重传的数据再次超时怎么办? 每次遇到超时重传,会将下一次的RTO加倍,如果两次超时,说明网络环境差
4.2快速重传
快速重传以数据驱动重传。 如果接收方某一次没有收到预定Seq的包却收到了大于其的包,这说明中间有个包丢了,它会一直回复丢了的那个包的ACK,当发送方收到三个序号同样的ACK就直接触发重传,这个过程是比超时重传快的。 由于有窗口机制,接收方是可以正常接收后面几个包的,所以在收到丢掉的数据包之后可以正常ACK6。 新的问题:发送方知道自己要快速重传了,但是由于接收方一直ACK2,它并不知道345接收方有没有收到,那么是只重传2这个包,还是2345都重传呢? 于是有了选择性重传机制SACK。
4.3SACK(Selective Acknowledgment 选择性确认)
这种方式在TCP头部【选项】中需要加入SACK,这样可以将自己接收的缓冲区发送给对方。这样发送方就知道其收到哪些信息了,他就可以只重传没有收到的消息。即发送方在快速重传之前查一下SACK信息,以确定自己需要重传哪些内容
4.4D-SACK(Duplicate SACK)
该功能主要使用sack告诉发送方哪些消息重复发送了 还有一种情况,假设发送方某个包被网络延迟了,三次触发快速重传后,接收方收到包后又收到了之前延迟的相同的包。这个时候发送方就可以知道是网络延迟的问题。
所以D-SACK的作用有:
- 可以让发送方知道往返两个包到底谁丢了。是发出去的包丢了还是接收方回应的ACK丢了。
- 可以让发送方知道是不是网络延迟了。
五.滑动窗口
刚才我们说过有缓冲区存在,滑动窗口就是这么一个机制。避免发送方每个包都要等着被ACK一下。 窗口大小代表着:无需等待接收方确认,发送发可以连续发送数据的数目最大值 所以滑动窗口下的ACK,代表着该序号之前的所有数据包接收方都已经收到,这个模式称之为累计应答
双方通信的窗口大小如何确定? 按接收方窗口大小确定。发送方的数据大小不能超过接收方窗口大小。
发送方窗口 发送方一旦按照对方接收窗口确认窗口大小后,窗口内的所有数据包都可以直接发送,每当收到ACK,就用ACK-当前窗口最小序号=num,代表现在num个数被收到了,窗口就可以右移num个位置。 发送方主要靠三个指针来确认己方窗口的四种状态(1:已发送,已ACK;2已发送,未ACK;3:未发送,但是可发送 ;4:未发送,但超出窗口大小的):
- SND.WND:表示发送窗口大小。
- SND.UNA:指向已发送但未ACK的第一个序号
- SND.NXT:指向未发送但可发送的第一个序号。
接收方窗口: 状态: #1 + #2 是已成功接收并确认的数据(等待应?进程读取); #3 是未收到数据但可以接收的数据; #4 未收到数据并不可以接收的数据;
接收方只需要两个指针就可:
- RCV.WND :表示接收窗?的??,它会通告给发送?。
- RCV.NXT :是?个指针,它指向期望从发送?发送来的下?个数据字节的序列号,也就是 #3 的第?个字节。
- 指向 #4 的第?个字节是个相对指针,它需要 RCV.NXT 指针加上 RCV.WND ??的偏移?,就可以指向#4 的第?个字节了。
接收窗口和发送窗口是不是完全相等
不是完全相等,是约等于,因为接收方可以按自己的接收能力扩大或缩小接收窗口大小,通过TCP报文的Windows字段告诉发送方,这个过程是有延迟的。
六.流量控制
流量控制主要是为了发送方根据接收方的实际接收能力控制发送的数据量。
在实际情况下,发送窗口和接收窗口中存放的存放的,都放在操作系统内存缓冲区中,会被操作系统调整。 主要是接收方虽然收到了数据,但是应用层没有把收到的数据读走这样接收方不敢贸然继续接收接收窗口以外的数据,只能不断缩小接收窗口,并回复的时候告诉发送方自己的接收窗口缩小了。 这种情况可能导致接收窗口和发送窗口都逐渐缩小到0; 更极端的情况,如果发送方还没有收到接收方缩小接收窗口(因为接收方应用层缩小了缓存空间)的通知就已经把超出现在接收窗口大小的数据发出去了,接收的数据包大于接收窗口大小,此时接收方直接丢包
窗口关闭现象
假设接收方利用流量控制,不断缩小自己的接收窗口,直到0,这个时候发送方也收到了窗口大小。此时双方的窗口都关闭了 过了一段时间,接收方处理能力上来了,把接收窗口增大了,发消息告诉发送方却丢包了。那么双方进入了持续等待状态,形成死锁 如何解决潜在死锁现象: TCP为每个连接设有一个持续定时器,只要TCP某一方收到对方的0窗口通知,就启动计时器。计时器超时的时候,就会发送窗口探测报文 以用来询问对方现在的接收窗口到底是多少。如果三次之后接收窗口还是0,就RST中断连接。
糊涂窗口综合征
假设有一个大巴车队,每辆车刚上一两个人就发车了,显然不经济。 糊涂窗口综合征就是:如果接收方每次刚腾出一小块儿几个字节的空间,就告诉发送方,发送方又发送了几个字节,这样的发送体量相对于TCP+IP头部总共40字节的成本来说,显然开销太大。
造成上述原因有两个:
- 接收方可以通告发送方小窗口
- 而发送方也可以发送小数据
所以解决办法就是:
- 不让接收方通告小窗口
- 不让发送方发送小数据
1.如何不让接收方通告小窗口 接收方策略: 如果当前窗口大小<min(MSS,缓存空间的一半)时,直接关闭窗口通知发送方窗口为0,阻止数据再发过来。 当大于这个min值的时候,就把窗口打开让发送方发
2.如何不让发送方发送小数据: 发送方策略: Nagle算法:延时处理,必需满足以下某一种条件才能发数据:
- 窗口或数据大小>=MSS
- 收到之前数据回复的ACK包(因为如果接收方已经不会通告小窗口了,所以ACK时接收方必定准备好了)
只要没有满足上述两个条件之一,发送方就一直囤积数据。
七.拥塞控制
拥塞控制与流量控制的关系
流量控制是接收方对发送方发送窗口的调整,避免发送方的数据填满接收方的缓存。但对于发送方来说,他并不清楚为什么接收方要做这样的调整 因为计算机网络处在一个共享的环境之中,所以,发送方发送窗口不一定是因为接收方的调整,还有可能是其他主机之间的通信使得网络发生了拥堵,这个时候发送方如果继续发送大量数据,则会进一步加重网络的负担。形成恶性循环。
所以,发送方的发送速率不仅仅要看接收方的接收能力,还要看整个网络的接收能力。
拥塞窗口与发送窗口
拥塞窗口cwnd是发送方维护的一个状态变量,它会根据网络的拥塞程度动态变化。
前面提过的发送窗口与接收窗口是一个约等于的关系。而加入了拥塞窗口之后,发送窗口的值发送了变化 发送窗口=min(拥塞窗口,接收窗口)
拥塞窗口主要网络中没有出现拥堵,就增大,反之则减小。 那么如何判断网络是否出现了拥塞呢,出现超时重传报文,就认为网络出现了拥塞
拥塞控制四大算法:
1.慢启动
当TCP刚建立一个连接的时候,它并不清楚发送多少数据比较合适,所以有个慢启动的过程,一点点的试探自己能发多少比较合适。 (即避免上来就发送大量的数据)
发送方每收到一个ACK,拥塞窗口cwnd就增加一。拥塞窗口线性增长,实际每次发包数量是一个指数型增长 因为指数爆炸性增长,我们得设置一个上限,避免其一直慢启动指数型增长: 慢启动门限ssthresh(slow start threshold):
- 拥塞窗口cwnd<慢启动门限ssthresh,使用慢启动算法。
- cwnd>=ssthresh,使用拥塞避免算法
一般来说ssthreash的大小是65535字节。
2.拥塞避免
拥塞避免算法:每收到一个ACK,拥塞窗口只增加1/cwnd 由于发生拥塞避免的时候,拥塞窗口大于慢启动门限,当收到cwnd个ACK确认时,cwnd增加一。 拥塞避免算法使得数据变成了线性增长
3.拥塞发生
一旦发生了重传,即拥塞发生 重传分为两种:
3.1超时重传
超时重传发生时,门限和拥塞窗口都发生变化:
- 门限ssthresh变为拥塞窗口cwnd的一半
- 而后拥塞窗口重置为1
随后就是正常的慢启动了。
3.2快速重传
当连续收到三个同样的ACK,这时代表接收方发现丢了中间某一个包,发送方就可以快速的重传这个包。 此时有三个反应:
- 将拥塞窗口减半。cwnd/=2;
- 将门限和拥塞窗口相等。ssthresh=cwnd。
- 开始快速恢复算法
3.2.1快速恢复
当发生快速重传的时候,你发送方还能收到三个一样的ACK,说明网络情况没有超时重传那么糟糕。 按快速重传,拥塞窗口减半了,慢启动门限等同于拥塞窗口了。
此时快速恢复:
- 拥塞窗口cwnd=ssthresh+3;拥塞窗口在刚才减半之后先加3,代表已经收到了三个ACK.(如还有重复ACK,cwnd再加一);
- 重传数据。
- 如果收到新数据的ACK说明网络已经恢复,此时门限值恢复重传之前的值,并开始拥塞避免。
拥塞算法示意图
我们描述上述过程:
- 慢启动
- 发生超时重传,门限变为拥塞窗口的一半,拥塞窗口重置为1.
- 慢启动
- 达到慢启动门限,开始拥塞避免,线性增长。
- 发生快速重传,门限减半,拥塞窗口减半之后加3.快速恢复,门限恢复。拥塞避免。
|