115 lines
13 KiB
Markdown
115 lines
13 KiB
Markdown
|
面向连接的运输层协议:应用程序在使用TCP协议之前,必须先建立TCP连接,数据传输完毕后,必须释放已建立的TCP连接
|
|||
|
点对点:TCP连接只能有两个端点,一对一
|
|||
|
可靠:通过TCP连接传输的数据,无差错、不丢失、不重复、按需到达
|
|||
|
全双工通信:连接的两端都设有发送缓存和接收缓存,用来临时存放双向通信的数据
|
|||
|
面向字节流:数据通过自己序列发送,发送和接收的字节流顺序必须完全一致
|
|||
|
TCP报文段的首部格式
|
|||
|
|
|||
|
|
|||
|
源端口和目的端口:各占2个字节
|
|||
|
序号:占4字节,范围是0~2的32次方减1,TCP在传输过字节流的每一个字节都按顺序编号,编号循环使用
|
|||
|
确认号:占4字节,期望收到对方下一个报文的数据字节的序号
|
|||
|
数据偏移:占4位,标记报文段的数据起始处距离报文段的起始处的距离
|
|||
|
保留:占6位
|
|||
|
紧急URG:当URG为1时,表明紧急指针字段有效,它告诉接收方此报文段有紧急数据,必须优先接收,此时该报文不再需需要按照原定的顺序排队,直接插入到本报文数据的最前面,优先处理(比如Control + C命令)
|
|||
|
确认ACK:TCP规定,在连接建立后,所有传送的报文段都必须把ACK置为1
|
|||
|
推送PUSH:当输出某些指令后立即需要收到对方响应,在这种情况下,TCP就可以使用push操作,发送方把PUSH置为1,并立即创建一个报文发送出去,接收方收到PUSH为1的报文段时,就立即将缓冲区的报文交付给应用进程,而不再等到整个缓冲区都填满后再向上交付
|
|||
|
复位RST:当RST为1时,表明TCP连接中出现严重差错,必须释放连接重新建立连接,可用于拒绝非法报文或非法连接
|
|||
|
同步SYN :在建立连接时用来同步序列号,当SYN=1而ACK=0时,表明建立连接请求,若对方同意建立连接,则应该在响应的报文段中使用SYN=1和ACK=1
|
|||
|
终止FIN:用来释放连接,当FIN=1时,表示此报文发送方的数据已经发送完毕,请求释放TCP连接
|
|||
|
窗口:占2个字节,窗口值作为接收方让发送方设置其发送窗口的依据,可靠传输和流量控制都依赖窗口实现(后面详细说)
|
|||
|
检验和:计算检验和时要在TCP报文段前面加上伟首部,用来检验传输数据是否有错
|
|||
|
紧急指针:占2个字节,在URG=1时才有意义,标记紧急报文中的紧急数据的字节数,紧急数据结束后就是普通数据。
|
|||
|
选项:规定每个报文段中的数据字段的最大长度MSS,一般在建立连接时,双方将自己能够支持的MSS写入选项中,以后就按照这个数据传输数据。
|
|||
|
数据
|
|||
|
报文段首部格式一定要记住,至少要大概理解后,再带着疑问去看下边,否则看下边有可能会一脸懵逼。
|
|||
|
|
|||
|
TCP可靠传输的实现
|
|||
|
|
|||
|
|
|||
|
TCP的可靠传输通过滑动窗口实现,滑动窗口以字节为单位。凡是已发送过的数据,在未收到确认之前都必须暂时保留,以便在超时重传时使用,窗口越大,发送方就可以在收到对方确认之前连续发送更多的数据,获得更高的传输速率。滑动窗口可以分为四部分:
|
|||
|
|
|||
|
已发送并搜到确认
|
|||
|
已发送未收到确认
|
|||
|
允许发送但尚未发送
|
|||
|
不允许发送
|
|||
|
窗口依赖三个指针,P1,P2和P3,小于1的是已经发送并收到确认的字节,1和2之间的是已发送但未收到确认的字节,2和3之间是允许发送但尚未发送但字节,大于3为不允许发送的字节,1和3之间的字节数即为接收方规定的滑动窗口大小(流量控制)。接收方B只能对按需收到的最高序号给出确认,当指针2和指针3重合时,发送方必须停止发送数据,等待接收方的确认,为了保证可靠传输,A必须等到B的确认才能继续发送数据,为了保证发送方不会一直等待,到达依赖超时计时器(动态计算超时重传时间)规定的时间后,就重传这部分数据,直到收到接收方B的确认。
|
|||
|
|
|||
|
超时重传时间的选择采用一个自适应算法,它会记录一个报文段发出的时间,以及收到响应的确认的时间,作为往返时间RTT,计算为加权平均往返时间RTTs,RTTd为RTT的偏差的加权平均值,通过这几个值计算得到超时重传时间RTO。计算的规则是:报文段每重传一次,就把超时重传时间RTO增大一些,当不再发生报文段的重传时,根据下面公式计算RTO。
|
|||
|
|
|||
|
RTO = RTTs + 4 * RTTd
|
|||
|
|
|||
|
TCP的流量控制
|
|||
|
流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收,利用滑动窗口实现。发送方的发送窗口不能超过接收方给出的接收窗口大小。当接收方接收能力下降时,会缩小自己的接收窗口,发送方通过报文头部的串口字段告知发送方。
|
|||
|
|
|||
|
还以上面发送方为A,接收方为B作为例子,当B的接收缓存满之后,将自己的接收窗口置为0,通过报文的窗口字段告知发送方A,过了一段时间后,B的应用程序将接收缓冲区的数据处理了一部分,接收窗口又有了空间,假设为300,此时B向A发送报文告知对方接收窗口为300,如果该报文在传输中丢失了,A不会一直傻傻地等待,在A收到B的0窗口报文后,就会开启该连接设定的持续计时器,当超过计时器规定的时间后,A会向B发送一个0窗口探测报文段,B在收到这个探测报文后,会重新发送300的窗口报文段。此时有人会问,B的接收窗口大小为0了,怎么还能接收0窗口探测报文段?这里需要记住:TCP规定,即使接收窗口为0,也必须可以接收0窗口探测报文段、ACK=1的报文段和URG=1的报文段
|
|||
|
|
|||
|
TCP的拥塞控制
|
|||
|
|
|||
|
|
|||
|
TCP进行拥塞控制的算法有4个:慢开始、拥塞避免、快重传、快恢复。
|
|||
|
|
|||
|
慢开始:将拥塞窗口大小设置为1个发送方的最大报文段MSS(上面说过哈,报文首部的选项字段),每经过一个传输轮次,拥塞窗口就加倍,直到达到慢开始门限后开启拥塞避免。
|
|||
|
拥塞避免:当达到慢开始门限后,每收到一次接收方的确认ACK,就将拥塞窗口大小+1,直到网络出现超时后,将慢开始门限调整为原来的一半,执行慢开始算法。
|
|||
|
快重传:快重传算法可以让发送方尽早知道发生了个别报文段的丢失。由于接收方必须对发送方每个报文回复确认ACK,假设发送方以此发送M1、M2、M3、M4、M5、M6报文,接收方收到1和2后都及时给出了确认,由于字节都是连续都,假设接收方没有收到3,但收到了4,此时接收方会在收到4、5、6报文后都发送对2的确认,当发送方连续收到3个重复确认时,就知道接收方没有收到报文段3,此时会立即重传报文3。
|
|||
|
快恢复:当发生快重传时,发送方知道现在只是丢失了个别字段,所以不启动慢开始,而是执行快恢复算法,将拥塞窗口调整为慢开始门限的一半,执行拥塞避免算法。
|
|||
|
下面通过一道题来理解:
|
|||
|
|
|||
|
问题:设tcp的拥塞窗口的慢开始门限值初始为8,当拥塞窗口上升到12时网络发生超时,那么第13次传输时的拥塞窗口大小为多少?
|
|||
|
|
|||
|
解答:
|
|||
|
|
|||
|
慢开始:0->1->2->4->8(传输4次)
|
|||
|
到达慢开始门限8,进入拥塞避免: 8->9->10->11->12(传输4次)
|
|||
|
增长到12发生超时,慢开始门限调整到6,慢启动:0->1->2->4->6(传输4次)
|
|||
|
6已经达到慢开始门限,进入拥塞避免:6->7(第十三次传输)
|
|||
|
所以最终答案为7
|
|||
|
TCP连接的三次握手
|
|||
|
|
|||
|
|
|||
|
最开始的 Client 和 Server 都是处于 Closed,由于服务器端不知道要跟谁建立连接,所以其只能被动打开,然后监听端口,此时 Server 处于 Listen 状态;
|
|||
|
|
|||
|
而 Client 会主动打开,然后构建好 TCB 「SYN= 1,seq = x」,发送给服务器端,此时 Client 会将状态置为 SYN_SEND 「同步已发送」;
|
|||
|
|
|||
|
服务器端收到客户端发来的同步请求后,会将状态置为 SYN_RECV「同步已接收」,同时会构建好 TCB「SYN = 1,seq = y,ACK = 1,ack = x + 1」发送给客户端;
|
|||
|
|
|||
|
客户端接收到了服务器端发来的传输控制块之后,会将自己的状态改为 ESTABLISHED「建立连接」,然后发送确认报文(ACK= 1,seq = x + 1,ack = y + 1);
|
|||
|
|
|||
|
服务器端在收到了客户端发来的报文之后,也将状态置为 ESTABLISHED「建立连接」,至此,三次握手结束,当然在这里,可以带 tcp 报文段信息过来了,因为此时客户端已经可以保证是可靠的传输了,所以在这一端可以发送报文段了。
|
|||
|
|
|||
|
题目1:为什么A最后还要再发送一次确认?
|
|||
|
|
|||
|
解答:为了防止因为网络原因迟来的A连接请求到达B,B单方面建立连接浪费B的资源。假如A发送了连接请求,但未收到确认,于是A又重传了一次连接请求,第二个连接请求正常连接并传输数据。此时A的第一个连接没有丢失,只是延迟了,又到达了B,B误以为是A又发送的一次连接,假如此时B向A发出确认后,A会丢弃B的确认,假如没有A最后一次的确认,B就会在发送确认后建立连接,白白浪费B的资源。
|
|||
|
|
|||
|
题目2:为何不直接在第一次握手就带上报文段消息,非要第三次才可以带?
|
|||
|
|
|||
|
解答:因为 TCP 是要保证数据的不丢失且可靠,如果在第一次就带上报文段消息,此次建立连接很有可能就会失败,那么就不能保证数据的不丢失了,在不可靠的机制上进行这种操作,换来的代价太大,每次发送报文段的资源也会增大,得不偿失;而第三次握手的时候,客户端已经知道服务器端准备好了,所以只要告诉服务器端自己准备好了就ok了,所以此时带上报文段信息没有任何问题。
|
|||
|
|
|||
|
TCP断开的四次挥手
|
|||
|
|
|||
|
|
|||
|
最开始客户端和服务器端都是 ESTABLISHED 的状态,然后客户端会主动关闭,而服务器端则是被动关闭。(主动关闭的我们认为是客户端)
|
|||
|
|
|||
|
客户端发送一个 FIN 报文段,seq = 结束的报文段序号 + 1「假设为 u」,告诉服务器端,客户端需要关闭了,此时将客户端的状态变为 FIN-WAIT-1,等待服务器端的反馈;
|
|||
|
|
|||
|
服务器在接收到了客户端发来的 FIN 包之后,会发一条 ack报文反馈给客户端,其中报文中包括 ACK = 1,seq = v,ack = u+1,告诉客户端收到了客户端要关闭的消息了,同时服务器端会通知应用进程需要关闭连接了,并将自己的状态置为 CLOSE-WAIT;
|
|||
|
|
|||
|
由于服务器端可能还有一些数据没处理完,所以需要一段时间的等待,当处理完了之后,会再发一条报文,其中 FIN = 1,ACK = 1,seq = w,ack = u+1,告知客户端,服务器端现在可以关闭了,并将服务器端的状态由 CLOSE-WAIT 变为 LAST-ACK;
|
|||
|
|
|||
|
客户端在收到了服务器端发来的消息之后,会发一条ack报文「ACK = 1,seq = u+1,ack = w+1」回去,告知服务器端,客户端已经知道了你准备好关闭了,此时会将客户端的状态由 FIN-WAIT-2 置为 TIME-WAIT,在两个最长报文段传输时间过后,会自动将客户端的状态由 TIME-WAIT 置为 CLOSED。
|
|||
|
|
|||
|
服务器端收到消息之后,就将状态由 LAST-ACK 置为了 CLOSED,自此,四次挥手全部结束。
|
|||
|
|
|||
|
题目1:在B向A发送FIN断开连接的报文后,A发送确认后做了什么?是马上断开吗?为什么要等待2个时间周期?
|
|||
|
|
|||
|
解答:图里很明了,并不是马上断开,而是等待了2个时间周期。原因有2个:
|
|||
|
|
|||
|
为了保证A发送的最后一个ACK报文能到达B,在这两个时间周期内如果B收不到A的确认,会超时重传这个FIN+ACK的报文,A就会收到重传的断开确认报文,此时A会重传确认,重新启动2MSL计时器。假如A不等待2MSL,就不会收到B的重传报文,也就不会再发送一次确认报文,B就无法按照正常步骤进入CLOSE状态。
|
|||
|
A在发送完最后一个ACK报文后,经过2MSL,就可以使本次连接内产生的所有报文都从网络中消失,在下一个新连接中就不会出现旧的连接报文段。
|
|||
|
题目2:主动关闭方和被动关闭方,谁先进入CLOSE状态?
|
|||
|
|
|||
|
解答:看图也可以发现,是被动关闭方先进入CLOSE状态,原因上一题的答案就可以解释明白。
|
|||
|
|
|||
|
题目3:为何不能三次挥手呢?
|
|||
|
|
|||
|
解答:首先如果去掉最后一次挥手,那么服务器端就不知道自己要关闭的报文有没有传输成功,可能半路上就失败了,但是此时客户端不知道,导致客户端一直在等待服务器关闭,但是此时服务器端直接就关闭了。
|