CP、UDP都是属于运输层的协议,提供端到端的进程之间的逻辑通信,而IP协议(网络层)是提供主机间的逻辑通信,应用层规定应用进程在通信时所遵循的协议。
一、UDP主要特点:传输的是用户数据报协议。
1.UDP是无连接的,即发送数据之前不需要建立连接。
2.UDP 使用尽最大努力交付,即不保证可靠交付,同时也不使用拥塞控制。
3.UDP是面向报文的。UDP没有拥塞控制,很适合多媒体通信的要求。
4.UDP支持一对一、一对多、多对一和多对多的交互通信。
5.UDP的首部开销小,只有 8个字节。
发送方 UDP对应用程序交下来的报文,在添加首部后就向下交付 IP层。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。应用层交给 UDP多长的报文,UDP就照样发送,即一次发送一个报文。接收方 UDP对 IP 层交上来的 UDP 用户数据报,在去除首部后就原封不动地交付上层的应用进程,一次交付一个完整的报文。
应用程序必须选择合适大小的报文。
二、TCP的主要特点:
1.TCP 是面向连接的运输层协议。
2.每一条 TCP 连接只能有两个端点(endpoint),每一条 TCP 连接只能是点对点的(一对一)。
3.TCP 提供可靠交付的服务。
4.TCP 提供全双工通信。
5,.TCP是面向字节流。
6.首部最低20个字节。
TCP可靠传输的工作原理:停止等待协议(确认重传机制)
1.无差错:
1>:无差错: A发送分组M1,发送就暂停发送,等待B的确认,B收到M1就向A发送确认,A收到对M1的确认后再发送下一个分组
2>:出现差错:
A只要超过一段时间仍然没有收到确认,就认为刚才发送的分组丢失了,就重传前面发过的分组,叫超时重传。由重传计时器实现。
而且:(1)A每次发送分组必须暂时保留分组副本;(2)分组和确认分组必须进行编号‘(3)超时计时器的重传时间应当比数据在分组的平均往返时间更多一些。
3>:如果B收到重复的分组M1,不想上一层交付;而且,向A发送确认。
2.超时重传:
停止等待协议的优点是简单,但是信道利用率太低了。解决方法是采用连续ARQ协议,发送方维持发送窗口,每次连续发送几个分组,接收方采用累积确认,对按序到达的最后一个分组发送确认。缺点是不能向发送方反映出接收方已经正确收到的所有分组信息,例如丢失中间的分组。
TCP可靠传输的实现:
TCP 连接的每一端都必须设有两个窗口——一个发送窗口和一个接收窗口。TCP 的可靠传输机制用字节的序号进行控制。TCP 所有的确认都是基于序号而不是基于报文段。
发送过的数据未收到确认之前必须保留,以便超时重传时使用。发送窗口不动(没收到确认)和前移(收到新的确认)
发送缓存用来暂时存放: 发送应用程序传送给发送方 TCP 准备发送的数据;TCP 已发送出但尚未收到确认的数据。
接收缓存用来暂时存放:按序到达的、但尚未被接收应用程序读取的数据; 不按序到达的数据。
必须强调三点:
1> A 的发送窗口并不总是和 B 的接收窗口一样大(因为有一定的时间滞后)。
2> TCP 标准没有规定对不按序到达的数据应如何处理。通常是先临时存放在接收窗口中,等到字节流中所缺少的字节收到后,再按序交付上层的应用进程。
3> TCP 要求接收方必须有累积确认的功能,这样可以减小传输开销
TCP流量控制:
流量控制(flow control)就是让发送方的发送速率不要太快,既要让接收方来得及接收,也不要使网络发生拥塞。利用滑动窗口机制可以很方便地在 TCP 连接上实现流量控制。
TCP 为每一个连接设有一个持续计时器。只要 TCP 连接的一方收到对方的零窗口通知,就启动持续计时器,发送一个零窗口探测报文段。
TCP的拥塞控制:
在某段时间,若对网络中某资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏——产生拥塞(congestion)。出现资源拥塞的条件:对资源需求的总和 > 可用资源。
开环控制方法就是在设计网络时事先将有关发生拥塞的因素考虑周到,力求网络在工作时不产生拥塞。
闭环控制是基于反馈环路的概念。属于闭环控制的有以下几种措施:监测网络系统以便检测到拥塞在何时、何处发生。将拥塞发生的信息传送到可采取行动的地方。调整网络系统的运行以解决出现的问题。
拥塞控制方法:
1.慢开始:在主机刚刚开始发送报文段时可先将拥塞窗口 cwnd 设置为一个最大报文段 MSS 的数值。在每收到一个对新的报文段的确认后,将拥塞窗口增加至多一个 MSS 的数值。用这样的方法逐步增大发送端的拥塞窗口 cwnd,可以使分组注入到网络的速率更加合理。每经过一个传输轮回,拥塞窗口(发送端)就加倍。
2.拥塞避免:让拥塞窗口缓慢增大,每经过一个往返时间就加1,而不是加倍,按线性规律缓慢增长。拥塞窗口大于慢开始门限,就执行拥塞避免算法。“乘法减小”:指不论在慢开始还是拥塞避免阶段,只要出现超时重传就把慢开始门限值减半。"加分增大“:指执行拥塞避免算法后,使拥塞窗口缓慢增大,以防止网络过早出现拥塞。合起来叫AIMD算法。
3.快重传算法:发送方只要一连收到三个重复确认就应当重传对方尚未收到的报文。而不必等到该分组的重传计时器到期。
4.快恢复算法:(1)当发送端收到连续三个重复的确认时,就执行“乘法减小”算法,把慢开始门限 ssthresh 减半。但接下去不执行慢开始算法。(2)由于发送方现在认为网络很可能没有发生拥塞,因此现在不执行慢开始算法,即拥塞窗口 cwnd 现在不设置为 1,而是设置为慢开始门限 ssthresh 减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大.
TCP的运输建立:
采用客户服务器方式,主动发起建立的是客户,被动等待连接建立的应用进程是服务器。
1.A 的 TCP 向 B 发出连接请求报文段,其首部中的同步位 SYN = 1,并选择序号 seq = x,表明传送数据时的第一个数据字节的序号是 x。
2.B 的 TCP 收到连接请求报文段后,如同意,则发回确认。 B 在确认报文段中应使 SYN = 1,使 ACK = 1,其确认号ack = x + 1,自己选择的序号 seq = y。
3.A 收到此报文段后向 B 给出确认,其 ACK = 1, 确认号 ack = y + 1。A 的 TCP 通知上层应用进程,连接已经建立。
4.B 的 TCP 收到主机 A 的确认后,也通知其上层应用进程:TCP 连接已经建立
TCP的连接释放
1.数据传输结束后,通信的双方都可释放连接。现在 A 的应用进程先向其 TCP 发出连接释放报文段,并停止再发送数据,主动关闭 TCP 连接。A 把连接释放报文段首部的 FIN = 1,其序号seq = u,等待 B 的确认。
2.B 发出确认,确认号 ack = u + 1,而这个报文段自己的序号 seq = v。TCP 服务器进程通知高层应用进程。从 A 到 B 这个方向的连接就释放了,TCP 连接处于半关闭状态。B 若发送数据,A 仍要接收。
3.若 B 已经没有要向 A 发送的数据, 其应用进程就通知 TCP 释放连接, B到 A 这个方向的连接也释放了。
4.A 收到连接释放报文段后,必须发出确认,在确认报文段中 ACK = 1,确认号 ack = w + 1,自己的序号 seq = u + 1
5.TCP 连接必须经过时间 2MSL 后才真正释放掉。因为:为了保证 A 发送的最后一个 ACK 报文段能够到达 B;防止“已失效的连接请求报文段”出现在本连接中。A 在发送完最后一个 ACK 报文段后,再经过时间 2MSL(时间等待计时器),就可以使本连接持续的时间内所产生的所有报文段,都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。
有机状态图:
图中有三种不同的箭头。粗实线箭头表示对客户进程的正常变迁。粗虚线箭头表示对服务器进程的正常变迁。另一种细线箭头表示异常变迁。
下面两图大家再熟悉不过了,TCP的三次握手和四次挥手见下面左边的”TCP建立连接”、”TCP数据传送”、”TCP断开连接”时序图和右边的”TCP协议状态机”
TCP三次握手、四次挥手时序图
TCP协议状态机
要弄清TCP建立连接需要几次交互才行,我们需要弄清建立连接进行初始化的目标是什么。TCP进行握手初始化一个连接的目标是:分配资源、初始化序列号(通知Peer对端我的初始序列号是多少),知道初始化连接的目标,那么要达成这个目标的过程就简单了,握手过程可以简化为下面的四次交互:
(1) Client端首先发送一个SYN包告诉Server端我的初始序列号是X;
(2) Server端收到SYN包后回复给Client一个ACK确认包,告诉Client说我收到了;
(3) 接着Server端也需要告诉Client端自己的初始序列号,于是Server也发送一个SYN包告诉Client我的初始序列号是Y;
(4) Client收到后,回复Server一个ACK确认包说我知道了。
整个过程4次交互即可完成初始化,但是,细心的同学会发现两个问题:[1] Server发送SYN包是作为发起连接的SYN包,还是作为响应发起者的SYN包?怎么区分?比较容易引起混淆 ;[2] Server的ACK确认包和接下来的SYN包可以合成一个SYN ACK包一起发送的,没必要分别单独发送,这样省了一次交互同时也解决了问题[1]。 这样TCP建立一个连接,三次握手在进行最少次交互的情况下完成了Peer两端的资源分配和初始化序列号的交换。
大部分情况下建立连接需要三次握手,也不一定都是三次,有可能出现四次握手来建立连接的。如下图,当Peer两端同时发起SYN来建立连接时,就出现了四次握手来建立连接(对于有些TCP/IP的实现,可能不支持这种同时打开的情况)。
在三次握手过程中,细心的同学可能会有以下疑问:
TCP进行断开连接的目标是:回收资源、终止数据传输。由于TCP是全双工的,需要Peer两端分别各自拆除自己通向Peer对端方向的通信信道。这样需要四次挥手来分别拆除通信信道,就比较清晰明了了。
(1) Client发送一个FIN包来告诉Server我已经没数据需要发给Server了;
(2) Server收到后回复一个ACK确认包说我知道了;
(3) 然后Server在自己也没数据发送给Client后,Server也发送一个FIN包给Client告诉Client我也已经没数据发给Client了;
(4) Client收到后,就会回复一个ACK确认包说我知道了。
到此,四次挥手,这个TCP连接就可以完全拆除了。在四次挥手的过程中,细心的同学可能会有以下疑问:
呢(超时设置是 2*MSL,RFC793定义了MSL为2分钟,Linux设置成了30s),在
TIME_WAIT的时候又不能释放资源,白白让资源占用那么长时间,能否省去
TIME_WAIT`,为什么?如果初始化序列号(缩写为ISN:Inital Sequence Number)可以固定,我们来看看会出现什么问题。假设ISN固定是1,Client和Server建立好一条TCP连接后,Client连续给Server发了10个包,这10个包不知怎么被链路上的路由器缓存了(路由器会毫无先兆地缓存或者丢弃任何的数据包),这个时候碰巧Client挂掉了,然后Client用同样的端口号重新连上Server,Client又连续给Server发了几个包,假设这个时候Client的序列号变成了5。接着,之前被路由器缓存的10个数据包全部被路由到Server端了,Server给Client回复确认号10,这个时候,Client整个都不好了,这是什么情况?我的序列号才到5,你怎么给我的确认号是10了,整个都乱了。
RFC793中,建议ISN和一个假的时钟绑在一起,这个时钟会在每4微秒对ISN做加一操作,直到超过2^32,又从0开始,这需要4小时才会产生ISN的回绕问题,这几乎可以保证每个新连接的ISN不会和旧连接的ISN产生冲突。这种递增方式的ISN,很容易让攻击者猜测到TCP连接的ISN,现在的实现大多是在一个基准值的基础上随机进行的。
Client发送SYN包给Server后挂了,Server回给Client的SYN-ACK一直没收到Client的ACK确认,此时这个连接既没建立起来,也不能算失败。这就需要一个超时时间让Server将这个连接断开,否则这个连接就会一直占用Server的SYN连接队列中的一个位置,大量这样的连接就会将Server的SYN连接队列耗尽,让正常的连接无法得到处理。目前,Linux下默认会进行5次重发SYN-ACK包,重试的间隔时间从1s开始,下次的重试间隔时间是前一次的双倍,5次的重试时间间隔为1s,2s,4s,8s,16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s,TCP才会断开这个连接。由于,SYN超时需要63秒,那么就给攻击者一个攻击服务器的机会,攻击者在短时间内发送大量的SYN包给Server(俗称SYN flood攻击),用于耗尽Server的SYN队列。对于应对SYN过多的问题,Linux提供了几个TCP参数:tcp_syncookies
、tcp_synack_retries
、tcp_max_syn_backlog
、tcp_abort_on_overflow
来调整应对。
由上面的”TCP协议状态机 “图可以看出,TCP的Peer端在收到对端的FIN包前发出了FIN包,那么该Peer的状态就变成了FIN_WAIT1
,Peer在FIN_WAIT1
状态下收到对端Peer对自己FIN包的ACK包的话,那么Peer状态就变成FIN_WAIT2
,Peer在FIN_WAIT2
下收到对端Peer的FIN包,在确认已经收到了对端Peer全部的Data数据包后,就响应一个ACK给对端Peer,然后自己进入TIME_WAIT
状态;但是如果Peer在FIN_WAIT1
状态下首先收到对端Peer的FIN包的话,那么该Peer在确认已经收到了对端Peer全部的Data数据包后,就响应一个ACK给对端Peer,然后自己进入CLOSEING状态,Peer在CLOSEING状态下收到自己FIN包的ACK包的话,那么就进入TIME WAIT
状态。于是,TCP的Peer两端同时发起FIN包进行断开连接,那么两端Peer可能出现完全一样的状态转移FIN_WAIT1---->CLOSEING----->TIME_WAIT
,Client和Server也就会最后同时进入TIME_WAIT
状态。同时关闭连接的状态转移如下图所示:
答案是可能的。TCP是全双工通信,Cliet在自己已经不会再有新的数据要发送给Server后,可以发送FIN信号告知Server,这边已经终止Client到对端Server的数据传输。但是,这个时候对端Server可以继续往Client这边发送数据包。于是,两端数据传输的终止在时序上独立并且可能会相隔比较长的时间,这个时候就必须最少需要2+2=4次挥手来完全终止这个连接。但是,如果Server在收到Client的FIN包后,再也没数据需要发送给Client了,那么对Client的ACK包和Server自己的FIN包就可以合并成一个包发送过去,这样四次挥手就可以变成三次了(似乎Linux协议栈就是这样实现的)。
TIME_WAIT
状态要说明TIME_WAIT
的问题,需要解答以下几个问题:
相信大家都知道,TCP主动关闭连接的那一方会最后进入TIME_WAIT
。那么怎么界定主动关闭方?是否主动关闭是由FIN包的先后决定的,就是在自己没收到对端Peer的FIN包之前自己发出了FIN包,那么自己就是主动关闭连接的那一方。对于疑症四中描述的情况,那么Peer两边都是主动关闭的一方,两边都会进入TIME_WAIT
。为什么是主动关闭的一方进行TIME_WAIT
呢,被动关闭的进入TIME_WAIT
可以吗?我们来看看TCP四次挥手可以简单分为下面三个过程
过程一:主动关闭方发送FIN;
过程二:被动关闭方收到主动关闭方的FIN后发送该FIN的ACK,被动关闭方发送FIN;
过程三:主动关闭方收到被动关闭方的FIN后发送该FIN的ACK,被动关闭方等待自己FIN的ACK
问题就在过程三中,据TCP协议规范,不对ACK进行ACK,如果主动关闭方不进入TIME_WAIT
,那么主动关闭方在发送完ACK就走了的话,如果最后发送的ACK在路由过程中丢掉了,最后没能到被动关闭方,这个时候被动关闭方没收到自己FIN的ACK就不能关闭连接,接着被动关闭方会超时重发FIN包,但是这个时候已经没有对端会给该FIN回ACK,被动关闭方就无法正常关闭连接了,所以主动关闭方需要进入TIME_WAIT
以便能够重发丢掉的被动关闭方FIN的ACK。
TIME_WAIT
主要是用来解决以下几个问题:
(1) 上面解释为什么主动关闭方需要进入TIME_WAIT
状态中提到的:主动关闭方需要进入TIME_WAIT
以便能够重发丢掉的被动关闭方FIN包的ACK。如果主动关闭方不进入TIME_WAIT
,那么在主动关闭方对被动关闭方FIN包的ACK丢失了的时候,被动关闭方由于没收到自己FIN的ACK,会进行重传FIN包,这个FIN包到主动关闭方后,由于这个连接已经不存在于主动关闭方了,这个时候主动关闭方无法识别这个FIN包,协议栈会认为对方疯了,都还没建立连接你给我来个FIN包?于是回复一个RST包给被动关闭方,被动关闭方就会收到一个错误(我们见的比较多的:connect reset by peer。这里顺便说下Broken pipe,在收到RST包的时候,还往这个连接写数据,就会收到Broken pipe错误了),原本应该正常关闭的连接,给我来个错误,很难让人接受。
(2) 防止已经断开的连接1中在链路中残留的FIN包终止掉新的连接2[重用了连接1的所有5元素(源IP,目的IP,TCP,源端口,目的端口)],这个概率比较低,因为涉及到一个匹配问题,迟到的FIN分段的序列号必须落在连接2一方的期望序列号范围之内,虽然概率低,但是确实可能发生,因为初始序列号都是随机产生的,并且这个序列号是32位的,会回绕。
(3) 防止链路上已经关闭的连接的残余数据包(a lost duplicate packet or a wandering duplicate packet)干扰正常的数据包,造成数据流不正常。这个问题和(2)类似。
TIME_WAIT
带来的问题主要是源于:一个连接进入TIME_WAIT
状态后需要等待2*MSL(一般是1到4分钟)那么长的时间才能断开连接释放连接占用的资源,会造成以下问题:
(1) 作为服务器,短时间内关闭了大量的Client连接,就会造成服务器上出现大量的TIME_WAIT连接,占据大量的tuple,严重消耗着服务器的资源;
(2) 作为客户端,短时间内大量的短连接,会大量消耗Client机器的端口,毕竟端口只有65535个,端口被耗尽了,后续就无法再发起新的连接了。
(由于上面两个问题,作为客户端需要连本机的一个服务的时候,首选UNIX域套接字而不是TCP)
TIME_WAIT
很令人头疼,很多问题是由TIME_WAIT
造成的,但TIME_WAIT
又不是多余的,所以不能简单将TIME_WAIT
去掉,那么如何来解决或缓解TIME_WAIT
问题?可以进行TIME_WAIT
的快速回收和重用来缓解TIME_WAIT
的问题。是否有一些清掉TIME_WAIT
的技巧?
(1) TIME_WAIT快速回收
Linux下开启TIME_WAIT
快速回收需要同时打开tcp_tw_recycle
和tcp_timestamps
(默认打开)两选项。Linux下快速回收的时间为3.5*RTO(Retransmission Timeout),而一个RTO时间为200ms至120s。开启快速回收TIME_WAIT
,可能会带来问题一中说的三点危险,为了避免这些危险,要求同时满足以下三种情况的新连接被拒绝掉。
[1] 来自同一个对端Peer的TCP包携带了时间戳
[2] 之前同一台peer机器(仅仅识别IP地址,因为连接被快速释放了,没了端口信息)的某个TCP数据在MSL秒之内到过本Server
[3] Peer机器新连接的时间戳小于Peer机器上次TCP到来时的时间戳,且差值大于重放窗口戳(TCP_PAWS_WINDOW
)
初看起来正常的数据包同时满足上面3条几乎不可能,因为机器的时间戳不可能倒流的,出现上述的3点均满足时,一定是老的重复数据包又回来了,丢弃老的SYN包是正常的。到此,似乎启用快速回收就能很大程度缓解TIME_WAIT
带来的问题。但是,这里忽略了一个东西就是NAT——在一个NAT后面的所有Peer机器在Server看来都是一个机器,NAT后面的那么多Peer机器的系统时间戳很可能不一致,有些快,有些慢。这样,在Server关闭了与系统时间戳快的Client的连接后,在这个连接进入快速回收的时候,同一NAT后面的系统时间戳慢的Client向Server发起连接,这就很有可能同时满足上面的三种情况,造成该连接被Server拒绝掉。所以,在是否开启tcp_tw_recycle
需要慎重考虑。
(2) TIME_WAIT重用
Linux上比较完美地实现了TIME_WAIT
重用问题。只要满足下面两点中的一点,一个TW状态的四元组(即一个socket连接)可以重新被新到来的SYN连接使用
[1] 新连接SYN告知的初始序列号比TIME_WAIT
老连接的末序列号大
[2] 如果开启了tcp_timestamps
,并且新到来的连接的时间戳比老连接的时间戳大
要同时开启tcp_tw_reuse
选项和tcp_timestamps
选项才可以开启TIME_WAIT
重用,还有一个条件是:重用TIME_WAIT
的条件是收到最后一个包后超过1s。细心的同学可能发现TIME_WAIT
重用对Server端来说并没解决大量TIME_WAIT
造成的资源消耗的问题,因为不管TIME_WAIT
连接是否被重用,它依旧占用着系统资源。即便如此,TIME_WAIT
重用还是有些用处的,它解决了整机范围拒绝接入的问题,虽然一般一个单独的Client是不可能在MSL内用同一个端口连接同一个服务的,但是如果Client做了bind端口那就是同一个端口了。时间戳重用TIME_WAIT
连接机制的前提是IP地址唯一性,得出新请求发起自同一台机器,但是如果是NAT环境下就不能这样保证了,于是在NAT环境下,TIME_WAIT
重用还是有风险的。
有些同学可能会混淆tcp_tw_reuse
和SO_REUSEADDR
选项,认为是相关的东西,其实它们是两个完全不同的东西,可以说半毛钱关系都没。tcp_tw_reuse
是内核选项,而SO_REUSEADDR
用户态的选项,使用SO_REUSEADDR
是告诉内核,如果端口忙,但TCP状态位于TIME_WAIT
,可以重用端口。如果端口忙,而TCP状态位于其它状态,重用端口时依旧得到一个错误信息,指明Address already in use。如果你的服务程序停止后想立即重启,而新套接字依旧使用同一端口,此时SO_REUSEADDR
选项非常有用。但是,使用这个选项就会有(问题二)中说的三点危险,虽然发生的概率不大。
可以用下面两种方式控制服务器的TIME_WAIT
数量:
(1) 修改tcp_max_tw_buckets
tcp_max_tw_buckets
控制并发的TIME_WAIT
数量,默认值是180000。如果超过默认值,内核会把多的TIME_WAIT
连接清掉,然后在日志里打一个警告。官网文档说这个选项只是为了阻止一些简单的DoS攻击,平常不要人为降低它。
(2) 利用RST包从外部清掉TIME_WAIT链接
根据TCP规范,收到任何发送到未侦听端口、已经关闭的连接的数据包、连接处于任何非同步状态(LISTEN, SYS-SENT,SYN-RECEIVED)并且收到的包的ACK在窗口外,或者安全层不匹配,都要回执以RST响应(而收到滑动窗口外的序列号的数据包,都要丢弃这个数据包,并回复一个ACK包),内核收到RST将会产生一个错误并终止该连接。我们可以利用RST包来终止掉处于TIME_WAIT
状态的连接,其实这就是所谓的RST攻击了。为了描述方便:假设Client和Server有个连接Connect1,Server主动关闭连接并进入了TIME_WAIT
状态,我们来描述一下怎么从外部使得Server处于TIME_WAIT
状态的连接Connect1提前终止掉。要实现这个RST攻击,首先我们要知道Client在Connect1中的端口port1(一般这个端口是随机的,比较难猜到,这也是RST攻击较难的一个点),利用IP_TRANSPARENT
这个socket选项,它可以bind不属于本地的地址,因此可以从任意机器绑定Client地址以及端口port1,然后向Server发起一个连接,Server收到了窗口外的包于是响应一个ACK,这个ACK包会路由到Client处,这个时候可能99%的Client已经释放连接Connect1了,这个时候Client收到这个ACK包,会发送一个RST包,Server收到RST包然后就释放连接Connect1提前终止TIME_WAIT
状态。提前终止TIME_WAIT
状态是可能会带来(问题二)中说的三点危害,具体的危害情况可以看下RFC1337。RFC1337中建议,不要用RST过早的结束TIME_WAIT
状态。
至此,上面的疑症都解析完毕,然而细心的同学会有下面的疑问:
按照TCP协议,确认机制是累积的,也就是确认号X确认指示的是所有X之前但不包括X的数据已经收到了。确认号(ACK)本身就是不含数据的分段,因此大量的确认号消耗了大量的带宽,虽然大多数情况下,ACK还是可以和数据一起捎带传输,但是如果没有捎带传输,那么就只能单独回来一个ACK,如果这样的分段太多,网络的利用率就会下降。为缓解这个问题,RFC建议了一种延迟的ACK,也就是说,ACK在收到数据后并不马上回复,而是延迟一段可以接受的时间,延迟一段时间的目的是看能不能和接收方要发给发送方的数据一起回去,因为TCP协议头中总是包含确认号的,如果能的话,就将数据一起捎带回去,这样网络利用率就提高了。延迟ACK就算没有数据捎带,那么如果收到了按序的两个包,那么只要对第二包做确认即可,这样也能省去一个ACK消耗。由于TCP协议不对ACK进行ACK,RFC建议最多等待2个包的积累确认,这样能够及时通知对端Peer我这边的接收情况。Linux实现中,有延迟ACK和快速ACK,并根据当前的包的收发情况来在这两种ACK中切换。一般情况下,ACK并不会对网络性能有太大的影响,延迟ACK能减少发送的分段从而节省带宽,而快速ACK能及时通知发送方丢包,避免滑动窗口停等,提升吞吐率。关于ACK分段,有个细节需要说明一下,ACK的确认号,是确认按序收到的最后一个字节序,对于乱序到来的TCP分段,接收端会回复相同的ACK分段,只确认按序到达的最后一个TCP分段。TCP连接的延迟确认时间一般初始化为最小值40ms,随后根据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整。
TCP交互过程中,如果发送的包一直没收到ACK确认,是要一直等下去吗?显然不能一直等(如果发送的包在路由过程中丢失了,对端都没收到又如何给你发送确认呢?),这样协议将不可用,既然不能一直等下去,那么该等多久?等太长时间的话,数据包都丢了很久了才重发,没有效率,性能差;等太短时间的话,可能ACK还在路上快到了,这时候却重传了,造成浪费,同时过多的重传会造成网络拥塞,进一步加剧数据的丢失。也是,我们不能去猜测一个重传超时时间,应该是通过一个算法去计算,并且这个超时时间应该是随着网络状况在变化的。为了使我们的重传机制更高效,如果我们能够比较准确知道在当前网络状况下,一个数据包从发出去到回来的时间RTT——Round Trip Time,那么根据这个RTT我们就可以方便设置TimeOut——RTO(Retransmission TimeOut)了。
为了计算这个RTO,RFC793中定义了一个经典算法,算法如下:
[1] 首先采样计算RTT值
[2] 然后计算平滑的RTT,称为Smoothed Round Trip Time (SRTT),SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT)
[3] RTO = min[UBOUND,max[LBOUND,(BETA*SRTT)]]
其中:UBOUND是RTO值的上限,例如:可以定义为1分钟;LBOUND是RTO值的下限,例如,可以定义为1秒。ALPHA is a smoothing factor (e.g., .8 to .9), and BETA is a delay variance factor (e.g., 1.3 to 2.0). 然而这个算法有个缺点就是:在算RTT样本的时候,是用第一次发数据的时间和ACK回来的时间做RTT样本值,还是用重传的时间和ACK回来的时间做RTT样本值?不管怎么选择,总会造成会要么把RTT算过长了,要么把RTT算过短了。如下图:(a)就计算过长了,而(b)就是计算过短了。
针对上面经典算法的缺陷,提出Karn/Partridge Algorithm对经典算法进行了改进(算法大特点是——忽略重传,不把重传的RTT做采样),但是这个算法有问题:如果在某一时间,网络闪动,突然变慢了,产生了比较大的延时,这个延时导致要重转所有的包(因为之前的RTO很小),于是,因为重转不算,所以,RTO就不会被更新,这是一个灾难。于是,为解决上面两个算法的问题,又有人推出来一个新的算法,这个算法叫Jacobson / Karels Algorithm(参看RFC6289),这个算法的核心是:除了考虑每两次测量值的偏差之外,其变化率也应该考虑在内,如果变化率过大,则通过以变化率为自变量的函数为主计算RTT(如果陡然增大,则取值为比较大的正数,如果陡然减小,则取值为比较小的负数,然后和平均值加权求和),反之如果变化率很小,则取测量平均值。
公式如下:(其中的DevRTT是Deviation RTT的意思)
SRTT = SRTT + α (RTT – SRTT) —— 计算平滑RTT
DevRTT = (1-β)DevRTT + β(|RTT-SRTT|) ——计算平滑RTT和真实的差距(加权移动平均)
RTO= µ * SRTT + ∂ *DevRTT —— 神一样的公式
(其中:在Linux下,α = 0.125,β = 0.25, μ = 1,∂ = 4 ——这就是算法中的“调得一手好参数”,nobody knows why, it just works…)最后的这个算法被用在今天的TCP协议中并工作非常好。
知道超时怎么计算后,很自然就想到定时器的设计问题。一个简单直观的方案就是为TCP中的每一个数据包维护一个定时器,在这个定时器到期前没收到确认,则进行重传。这种在设计理论上是很合理的,但是实现上,这种方案将会有非常多的定时器,会带来巨大内存开销和调度开销。既然不能每个包一个定时器,那么多少个包一个定时器比较好?这似乎比较难确定。可以换个思路,不要以包量来确定定时器,以连接来确定定时器是否会比较合理?目前,采取每一个TCP连接单一超时定时器的设计则成了一个默认的选择,并且RFC2988给出了每连接单一定时器的设计建议算法规则:
[1] 每一次一个包含数据的包被发送(包括重发),如果还没开启重传定时器,则开启它,使得它在RTO秒之后超时(按照当前的RTO值)。
[2] 当接收到一个ACK确认一个新的数据, 如果所有发出数据都被确认了,关闭重传定时器。
[3] 当接收到一个ACK确认一个新的数据,还有数据在传输,也就是还有没被确认的数据,重新启动重传定时器,使得它在RTO秒之后超时(按照当前的RTO值)。
[4] 当重传定时器超时后,依次做下列3件事情:
[4.1] 重传最早的尚未被TCP接收方ACK的数据包;
[4.2] 重新设置RTO为RTO*2(“还原定时器”),但是新RTO不应该超过RTO的上限(RTO有个上限值,这个上限值最少为60s);
[4.3] 重启重传定时器。
二. 流控制(Flow Control)
(1)滑动窗口协议的特征定义
TCP协议中使用
维持发送方/接收方缓存区
此缓存区主要用于解决网络传输的不可靠问题,在上一点已经介绍过网络传输的不可靠问题,如丢包、重复包、出错等,在TCP协议中发送方、接收方各自维护自己的缓存区,互相商定包的重传机制,由此解决不可靠问题。
(2)提出问题
如果没有滑动窗口协议,如何保证接收方能够收到正确有序的包?
如上图所示,发送方发送包1,接收方确认包1,发送包2,确认包2,这样即可解决不可靠性问题。但同时此过程的问题十分明显:吞吐量低,必须要等接收方确认完后才能发送下一个包。试考虑,若能连发几个包,接收方可以同时确认,这样效率岂不更高?
(3)简单改进
在此问题上,出现了以下改进:发送方可以同时发多个包,接收方一起确认。
(4)深度改进——滑动窗口实现
由此又衍生出一个问题,同时发包的数量多少才会是最优方案呢?例如发送方同时发送包1、2,在获得接收方确认包1消息后,能否不等包2确认信息,直接发送包3呢?
这样很自然地思考到了“滑动窗口实现”。以下有16个数据包,发送方希望按照顺序发送,在接收方收到每个包后都逐一给予确认:
初始:(窗口为4到7)
1、2、3包已发送并且获取发送方Ack确认;
4、5、6、7包已发送但尚未获取发送方Ack确认;
8、9、10包待发送;
而11、12、13、14、15、16包未发送甚至都没有装入内存;
正常:(窗口为5到9)
1、2、3、4包已发送并且获取发送方Ack确认;
5、6、7、8、9包已发送但尚未获取发送方Ack确认;
10、11包待发送;
而12、13、14、15、16包未发送甚至都没有装入内存;
丢Ack:(窗口为5到11)
5、6、7、8、9包未收到Ack(丢Ack),在等待过程又发送了10、11包,此时窗口已满,无法读进包12,只能等待Ack。如果真的是丢包,始终无法收到Ack,此时超时重传机制会从包5开始重新发送。(注意,这里的Ack是按照顺序发送的!)
重发: (窗口为9到15)
5、6、7、8包获取发送方Ack确认;
9、10、11、12、13包已发送但尚未获取发送方Ack确认;
13、14包待发送;
而16包未发送甚至都没有装入内存;
(5)总结
运用工程学的思想来考虑滑动窗口机制较为容易,为了增加线路的吞吐量,改进原版方案,令发送方同时发送包;为了衡量同时发送的数量达到吞吐量最优解,从而引进滑动窗口机制;为了解决丢包等不可靠性问题导致发送方无法收到接收方的Ack,又引进了重发机制。以上一系列过程使得整个传输过程更加可靠。
(1)出现的问题
发送端根据自己的实际情况发送数据,但是接收端在处理别的事(可能正处于高负荷的状态无法接收任何数据),而且此数据包并无重要意义,这样导致此包丢失又会触发重发机制,令网络流量无端浪费。
(2)解决方法
为了防止此现象发生,TCP提供一种机制可以让发送端根据接收端的实际接收能力控制发送的数据量。这就是“控制流”。
(3)具体操作
接收端想发送端通知自己可以接收数据的大小,发送端发送时不会超过这个限度的数据,该大小限制被称为窗口大小。
TCP首部中专门有一个字段用来通知窗口大小。接收端将自己可接收的缓冲区大小放入字段中并通知发送端。此值越大代表网络的吞吐量越高。
当缓存区一旦面临数据溢出时,窗口大小的值也会随之被设置成一个更小的值发送给发送端,从而控制数据发送量。也就是说,发送端会根据接收端的指示,对发送数据量进行控制。这就形成了一个完整的TCP流控制。
查看下图示例:
如上图所示,当接收端收到从3001号开始的数据段后,缓冲区已满,需要暂时停止接收数据。之后在收到发送窗口更新通知后通信才得以继续进行。如果此窗口更新通知在传送途中丢失,可能导致无法继续通信。为避免此类问题产生,发送端主机会时不时发送一个叫做“窗口探测”的数据段,此数据段仅含一个字节以获取最新的窗口大小信息。
可是过了一会儿,接收端如何通知发送端窗口可用了呢?
解决这个问题,TCP使用了Zero Window Probe技术,缩写为ZWP。即发送端在窗口变成0后,会发ZWP的包给接收端,让接收端来ack他的Window尺寸,一般这个值会设置成3次,第次大约30-60秒(不同的实现可能会不一样)。如果3次过后还是0的话,有的TCP实现就会发RST把链接断了。
注意:只要有等待的地方都可能出现DDoS攻击,Zero Window也不例外,一些攻击者会在和HTTP建好链发完GET请求后,就把Window设置为0,然后服务端就只能等待进行ZWP,于是攻击者会并发大量的这样的请求,把服务器端的资源耗尽。
三. 拥塞控制(Congestion Control)
有了TCP的窗口控制,收发端之间不再以一个数据段为单位发送确认应答,也能够连续发送大量数据包。但是刚开始通信就发送大量数据会引发其它问题。计算机网络处于一个共享的环境,因此有可能因为其他主机之间的通信使得网络拥堵,此时突然发送一个大量数据,可能导致整个网络瘫痪。
拥塞控制主要是四个算法:
慢启动
拥塞避免
拥塞发生
快速恢复
根据以上机制,可有效减少通信开始时连续发包导致的网络拥堵,还可以避免网络拥塞的情况。
慢启动的算法如下:(cwnd全称Congestion Window)
连接建好的开始先初始化cwnd = 1,表明可以传一个MSS大小的数据。
每当收到一个ACK,cwnd++; 呈线性上升
每当过了一个RTT,cwnd = cwnd*2; 呈指数让升
还有一个ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”(后面会说这个算法)
所以,我们可以看到,如果网速很快的话,ACK也会返回得快,RTT也会短,那么,这个慢启动就一点也不慢。下图说明了这个过程
收到一个ACK时,cwnd = cwnd + 1/cwnd
当每过一个RTT时,cwnd = cwnd + 1
这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。很明显,是一个线性上升的算法。
a)等到RTO 超时,重传数据包。TCP认为这种情况太糟糕,反应也很强烈:
sshthresh = cwnd /2
cwnd 重置为 1
进入慢启动过程
1
2
3
b) Fast Retransmit算法,也就是在收到3个duplicate ACK时就开启重传,而不用等到RTO超时,TCP Tahoe的实现和RTO超时一样。TCP Reno的实现是:
cwnd = cwnd /2
sshthresh = cwnd
进入快速恢复算法——Fast Recovery
1
2
3
上面我们可以看到RTO超时后,sshthresh会变成cwnd的一半,这意味着,如果cwnd<=sshthresh时出现的丢包,那么TCP的sshthresh就会减了一半,然后等cwnd又很快地以指数级增涨爬到这个地方时,就会成慢慢的线性增涨。我们可以看到,TCP是怎么通过这种强烈地震荡快速而小心得找到网站流量的平衡点的。
cwnd = cwnd /2
sshthresh = cwnd
1
2
真正的Fast Recovery算法如下:
cwnd = sshthresh + 3 * MSS (3的意思是确认有3个数据包被收到了)
重传Duplicated ACKs指定的数据包
如果再收到 duplicated Acks,那么cwnd = cwnd +1
如果收到了新的Ack,那么,cwnd = sshthresh ,然后就进入了拥塞避免的算法了。
仔细思考一下你会发现上面这个算法也有问题:它依赖于3个重复的Acks。
注意:3个重复的Acks并不代表只丢了一个数据包,很有可能不止一个。但此算法只会重传一个,而剩下的那些包只能等到RTO超时,从而导致一种可怕的现象:超时一个窗口就减半一下,多个超时会超成TCP的传输速度呈级数下降,而且也不会触发Fast Recovery算法了。
(还有一些其他算法,在此只着重选取几个重要的介绍,其余的读者可自行了解学习)
四. TCP和UDP的区别
面向报文传输,应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文。因此,应用程序必须选择合适大小的报文。
若报文太长,则IP层需要分片,降低效率。
若报文太短,浪费资源。
UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。即应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文。
因此它有以下特点:
传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快的把它扔到网络上
由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等,因此一台服务机可同时向多个客户机传输相同的消息
在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制
在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段
UDP信息包的标题很短,只有8个字节,相对于TCP的20个字节信息包的额外开销很小
吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、源端和终端主机性能的限制
UDP是面向报文的。发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界,因此,应用程序需要选择合适的报文大小。
优点
传输速率快:传输数据前,不需要像TCP一样建立连接;传输数据时,没有确认、窗口、重传、拥塞控制等机制。
较安全:由于没有了TCP的一些机制,被攻击者利用的漏洞就少了
缺点
不可靠,不稳定:由于没有了TCP的机制,在数据传输时如果网络不好,很可能丢包
2. 区别
其实在真正理解了TCP相关特征属性后,两者的区别显而易见就出来了,简单的总结如下:
TCP UDP
面向字节流 面向报文
一对一 可以一对一,一对多
面向有链接的通信服务 面向无连接的通信服务
速度快 速度慢
提供可靠的通信传输 不可靠,会丢包
保证数据包顺序 不保证
有流量控制,拥塞控制 没有
数据无边界 数据有边界
报头至少20字节 报头8字节
3. 常见问题
(1)为什么UDP比TCP快
因为TCP中连接需要三次握手,断开连接需要四次握手,传输过程中还有拥塞控制,控制流量等机制。
(2)为什么TCP比UDP可靠
TCP是面向有连接的,建立连接之后才发送数据;而UDP则不管对方存不存在都会发送数据。
TCP有确认机制,接收端每收到一个正确包都会回应给发送端。超时或者数据包不完整的话发送端会重传。UDP没有。因此可能丢包。
(3)什么时候使用TCP
当对网络通讯质量有要求的时候,比如:整个数据要准确无误的传递给对方,这往往用于一些要求可靠的应用,比如HTTP、HTTPS、FTP等传输文件的协议,POP、SMTP等邮件传输的协议。
例如日常生活中使用TCP协议的应用如下: 浏览器,用的HTTP FlashFXP,用的FTP Outlook,用的POP、SMTP Putty,用的Telnet、SSH QQ文件传输
(4)什么时候应该使用UDP
当对网络通讯质量要求不高的时候,要求网络通讯速度能尽量的快,这时就可以使用UDP。
比如日常生活中使用UDP协议的应用如下: QQ语音 QQ视频 TFTP
(5)TCP无边界,UDP有边界
TCP无边界:客户端分多次发送数据给服务器,若服务器的缓冲区够大,那么服务器端会在客户端发送完之后一次性接收过来,所以是无边界的。
UDP有边界:客户端每发送一次,服务器端就会接收一次,也就是说发送多少次就会接收多少次,因此是有边界的