网络层只把分组发送到目的主机,但是真正通信的并不是主机而是主机中的进程。运输层提供了进程间的逻辑通信,运输层向高层用户屏蔽了下面网络层的核心细节,使应用程序看起来像是在两个运输层实体之间有一条端到端的逻辑通信信道。
正如以上所述,TCP/UDP协议主要是为通信的进程提供抽象的通信信道。
由于TCP协议应用广泛,因此是面试中必不可少的知识点,考察的形式大概有以下:
其中只有一小部分的问题是死记硬背能搞定的(如3次挥手、4次握手流程),但很大一部分是从实际场景中抽象出来的问题,光靠死记硬背往往不奏效。
因此需要我们主动去思考,了解协议流程背后的原理,这样才能达到举一反三的目的。
下面按照数据包发送流程对TCP/UDP协议进行一下梳理,希望大家看完后对以上的问题都能有自己的答案!
如果有想了解TCP/UDP协议在实际场景中应用的同学,我用wireshark对浏览器发起https://www.baidu.com访问进行了一次抓包分析,写在实战中的TCP/UDP协议(wireshark抓包),感兴趣的同学可以看一看
UDP协议是无连接,尽最大可能交付报文,没有拥塞控制,面向报文(不分割、不合并),支持一对一、一对多、多对一、多对多的交互通信。
由于UDP只提供无连接的通信,因此只需要在数据包中加入端口号、长度、检验和即可,UDP首部只有8个字节
面向连接,提供可靠交付,有流量控制、拥塞控制,面向字节流(从上层传下的数据进行分割,分割成合适运输的数据块)只能是一对一连接。
TCP协议提供可靠通信,因此TCP首部相较UDP变得复杂很多(20字节+):
这里涉及到具体的TCP协议实现,因此还需要介绍一些知识点:
在三次握手协议中,服务器维护一个未连接队列,该队列为每个客户端的SYN包(syn=j)开设一个条目,该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客户的确认包。这些条目所标识的连接在服务器处于Syn_RECV状态,当服务器收到客户的确认包时,删除该条目,服务器进入ESTABLISHED状态。
backlog参数:表示未连接队列的最大容纳数目。
服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。注意,每次重传等待的时间不一定相同。
是指半连接队列的条目存活的最长时间,也即服务器从收到SYN包到确认这个报文无效的最长时间,该时间值是所有重传请求包的最长等待时间总和。有时我们也称半连接存活时间为Timeout时间、SYN_RECV存活时间。
在明确了这些知识点后,我们可以看到,如果A一直向B发送SYN1包,在收到SYN1包后,B向A发送SYN2包,同时把A连接放到未连接队列中,尽管A一直向B发送SYN1包,由于IP、源端口、目的端口未变,B不会在半连接队列中重新加入A。
在B发现A没有返回确认报文,有两种可能,第一种是等待一段时间,重新向A发送SYN2包,直到达到最大重传次数,或者发现与A的连接在半连接队列中存活时间超过了SYN_RECV存活时间,这两种可能都会把A连接从半连接队列里删除,重置B的SYN_RECV状态
这也就是SYN攻击的原理,加入IP欺骗后,向服务器发送虚假的SYN1包,耗尽服务器的半连接队列资源,使得正常的SYN请求被丢弃,目标系统运行缓慢,严重者引起网络堵塞甚至系统瘫痪。
第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。
假设使用两次握手,A向B发送SYN1包,由于传输过程由IP层决定,无法保证报文包之间到达B的先后顺序,在一段时间后,A没有收到B的SYN2包,于是重新发送SYN1包(第二次),第二次SYN1包先到达,B与A的第二次SYN1包建立连接请求,然后A的第一次SYN1包到达B,B关闭第二次SYN1建立的连接,建立起与第一次SYN1的连接请求。
如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。
先了解一下长连接、短连接:
长连接: Client方与Server方先建立通讯连接,连接建立后不断开, 然后再进行报文发送和接收。
短连接: Client方与Server每进行一次报文收发交易时才进行通讯连接,交易完毕后立即断开连接。此种方式常用于一点对多点 通讯,比如多个Client连接一个Server.(如请求网页内容)
先说一下,在短连接的过程中,由于每次通信都会有连接的建立以及断开,因此一般不会有粘包的情况。
所谓粘包,是由于TCP协议是面向字节流的协议,没有消息的边界保护机制,因此在实际情况中,经常发生一个收到**多于(粘包)或者小于(拆包)**当前包字节数的情况,看起来像一个包后面"粘着"后面包的一点内容,或者一个包被拆开分成多份内容,所以被应用层的人形象的称为"粘包"、“拆包”。
举个形象的例子:
在很多情况下,我们需要发送多个图片,而且是连续发送的,那么这种情况就存在粘包的问题,比如图片1的尾部数据和图片2的头部数据粘在一起,发送到了接收端。这时候接收端如果直接readall,并保存为文件,那么很显然,结果就错了,图片1和图片2 都是错的。 --知乎
解决的办法也很简单,根据应用需求,在高层定义自己的协议,一般可以定义为 包头+包体;包头一般定长,并且至少包含了文件大小字段;包体就是发送文件的二进制数据;这样手动对包的内容进行了分割。
以上是对“粘包问题”较为直白的一个解释,此外,“粘包问题”还可能与Nagle算法有关
Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块提出。
它的基本定义是任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。
Nagle算法的规则(可参考tcp_output.c文件里tcp_nagle_check函数注释):
(1)如果包长度达到MSS,则允许发送;
(2)如果该包含有FIN,则允许发送;
(3)设置了TCP_NODELAY选项,则允许发送;
(4)未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;
(5)上述条件都未满足,但发生了超时(一般为200ms),则立即发送。
这里有个详细解释Nagle算法的博客(https://my.oschina.net/xinxingegeya/blog/485643)
首先,TCP协议是一种全双工协议,A可以向B发送数据,B也可以向A发送数据。
那么问题来了, 既然B已经把数据发完了,直接进行CLOSE就好,最后等待A发给B的ACK报文是否有必要?
明确一点,为了资源的可以重复利用,A、B的信道必须是一起关闭(不允许发生一方端口打开,另一方端口关闭的状态),B在向A发送FIN报文后,必须确保A收到FIN报文(换言之,如果报文在传输过程中丢了,B要负责重发FIN报文),否则就会发生B已关闭,A还处于FIN-WAIT(没关闭)的状态,
MSL(Maximum Segment Lifetime)指的是最大报文存活时间。
TCP 使用超时重传来实现可靠传输:保证发出的每个报文段都会受到确认,如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。
重传超时时间RTO(Retransmission TimeOut)的计算是超时的核心部分,TCP要求这个算法能大致估计出当前的网络状况,虽然这确实很困难。要求精确的原因有两个:
(1)RTO太长会造成网络利用率不高。
(2)RTO太短会造成多次重传,使得网络阻塞。
一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT
RTT初始为:
R T T = t 1 − t 0 RTT = t1 - t0 RTT=t1−t0
接下来根据每个传输成功的RTT,来动态调整RTTS,
R T T s = ( 1 − a ) ∗ R T T s + a ∗ R T T RTTs = (1-a)*RTTs + a*RTT RTTs=(1−a)∗RTTs+a∗RTT
a a a 的值介于0到1,若很接近0,则表示旧的RTTs值和新的RTTs值相比变化不大,也就是说,新的RTT样本不太影响RTTs;
若很接近1,则表明新的RTTs值,受当前采集的RTT样本影响较大,跟上次的RTTs差距大
RFC 2988:推荐的阿尔法值为1/8,也就是0.125 (这种方式得出的值更为平滑)
在每次发包成功的时候都会计算往返时间及其偏差,将这个“往返时间+偏差”,RTO就是比这个总和值要稍大一点的值。
R T O = R T T s + 4 ∗ R T T d RTO = RTTs +4*RTTd RTO=RTTs+4∗RTTd
R T T d 是 偏 差 RTTd是偏差 RTTd是偏差
RFC 2988建议这样计算 R T T d RTTd RTTd。当第一次测量时, R T T d RTTd RTTd值取为RTT样本值的一半。在以后的测量中,则使用下式计算加权平均 R T T d RTTd RTTd,
R T T d = ( 1 − β ) ∗ R T T d + β ∗ ∣ R T T s − R T T ∣ RTTd=(1 - \beta) * RTTd + \beta * | RTTs - RTT| RTTd=(1−β)∗RTTd+β∗∣RTTs−RTT∣
其中, β 推 荐 值 是 1 / 4 , 即 0.25 \beta推荐值是1/4,即0.25 β推荐值是1/4,即0.25
另外,若是进行了重发处理,则第二次、第三次的等待的超时时间会以2倍、4倍的指数函数增长,以此类推。
但是,数据包也不会倍无限次的反复的进行重发。当重发次数达到一定次数过之后,如果发送端还是接收不到对端主机的ACK包,那么发送端主机就会认为网络或者对端主机出现了异常,进行强制关闭连接,并且通知应用程序异常强制终止。
A向B发送SYN1包后下线,B收到SYN1包,并发送ACK1包,由于A已经下线,收不到也不会回复。
在Linux下,默认重传次数为5次,初始RTO设为1s, 然后1s过后收不到回复,开始重发,RTO开始每次都翻倍,5次的重传时间间隔为1s, 2s, 4s, 8s, 16s(分别表示1s后第一次重发,2s后第二次重发…),到16s后第五次重发,总共31s,第5次发出后,等待重传时间翻倍,RTO=32s,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s,TCP才会把断开这个连接,因此可以看到SYN攻击在消耗服务器资源方面是非常有效的。
窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。
发送方数据几个状态:
接收方数据的几个状态:
在滑动窗口控制中,因为网络的不确定性,接收方收到的包可能是乱序的,如上图中,假如B收到**{31, 34, 35}包,但由于中间{32, 33}包没有到,所以只确认31包收到**,{34, 35}包作为缓存,但不确认,等收到{32, 33}包再一起确认。
流量控制是为了控制发送方发送速率,保证接收方来得及接收。
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。
如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。
流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。
拥塞控制由4大核心组成,“慢启动”(Slow Start)、“拥塞避免”(Congestion voidance)、“快速重传 ”(Fast Retransmit)、“快速恢复”(Fast Recovery)
发送方维护一个拥塞窗口的状态变量(拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口),
如上图,发送的最初执行慢启动,令 cwnd=1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 …
注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能也就更高。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。
如果出现了超时,则令 ssthresh = cwnd/2,然后重新执行慢启动。
在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1 和 M2 ,此时收到 M4,应当发送对 M2 的确认。
在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M2,则 M3丢失,立即重传 M3。
在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令ssthresh = cwnd/2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免(与超时cwnd设为1不同,此时cwnd设为cwnd/2)。
慢启动和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。
UDP的优点是它的简单高效,而最大的缺点是它无法保证数据传输的可靠性。所以,为了最大限度的发挥UDP的高速率,可以对UDP的不可靠传输进行适当的改进,以使数据的丢失率降到最低。
因此我们可以自己设计类似TCP的机制:确认机制、重传机制和窗口发送机制
具体的实现可以参考这篇博客:https://blog.csdn.net/codes_first/article/details/78453713