目录

网络—TCP

TCP是什么?

TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。

TCP头部

./tcp首部格式.png

主要字段的作用:

  • Source Port和Destination Port:分别占用16位,表示源端口号和目的端口号;用于区别主机中的不同进程,而IP地址是用来区分不同的主机的,源端口号和目的端口号配合上IP首部中的源IP地址和目的IP地址就能唯一的确定一个TCP连接;
  • Sequence Number:用来标识从TCP发端向TCP收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节在数据流中的序号;主要用来解决网络报乱序的问题;
  • Acknowledgment Number:32位确认序列号包含发送确认的一端所期望收到的下一个序号,比如收到了12345这5个字节的数据,序列号是1,那确认序列号就是6,因为下次需要从第6个字节发了。因此,确认序号应当是上次已成功收到数据字节序号加1。不过,只有当标志位中的ACK标志(下面介绍)为1时该确认序列号的字段才有效。主要用来解决不丢包的问题;
  • Offset:首部长度,或者理解为数据部分距离整个tcp报文开始的偏移量,需要这个值是因为任选字段的长度是可变的。这个字段占4bit(最大能表示15,但这里1代表4个字节,即首部长度为4*15=60个字节),因此TCP最多有60字节的首部。然而,没有任选字段,正常的长度是20字节;
  • TCP Flags:TCP首部中有6个标志比特,它们中的多个可同时被设置为1,主要是用于操控TCP的状态机的,依次为URG,ACK,PSH,RST,SYN,FIN。每个标志位的意思如下:
  • URG:发送端的缓存窗口中的数据是顺序发送,如果想插队先发送这段数据,可设置该标志位1。配合紧急指针使用。
  • ACK:此标志表示应答域有效,就是说前面所说的TCP应答号将会包含在TCP数据包中;有两个取值:0和1,为1的时候表示应答域有效,反之为0;连接建立后ACK都要是1。
  • PUSH:这个标志位表示Push操作。所谓Push操作就是指在数据包到达接收端以后,立即传送给应用程序,而不是在缓冲区中排队,意思就是在接收端进行插队,可以和URG类比记忆。
  • RST:发生了异常,需要重新建立链接。
  • SYN:表示同步序号,用来建立连接。SYN标志位和ACK标志位搭配使用,当连接请求的时候,SYN=1,ACK=0;连接被响应的时候,SYN=1,ACK=1;这个标志的数据包经常被用来进行端口扫描。扫描者发送一个只有SYN的数据包,如果对方主机响应了一个数据包回来 ,就表明这台主机存在这个端口;但是由于这种扫描方式只是进行TCP三次握手的第一次握手,因此这种扫描的成功表示被扫描的机器不很安全,一台安全的主机将会强制要求一个连接严格的进行TCP的三次握手;
  • FIN: 表示发送端已经达到数据末尾,也就是说双方的数据传送完成,没有数据可以传送了,发送FIN标志位的TCP数据包后,连接将被断开。这个标志的数据包也经常被用于进行端口扫描。
  • 窗口:表示我本地的接收缓存窗口还能接收多少数据。
  • 可选项:最大报文段长度MSS等。

流量控制的两种方案。

停止等待协议和滑动窗口协议。 停止等待就是A发给B,B收到后回复确认,A收到后再发。A收到确认之前是要停止等待的,所以效率比较低,早期链路层就是这样的。 第二种方案是滑动窗口,连续发多个,如果有丢失的怎么办?两种方案1.选择重传方案(SR)tcp默认也是这样的。。2.回退N帧方案(GBN)。

./回退N帧.png

./选择重传.png

另外tcp还有个优化就是快重传,不用等到超时到期就能知道丢失了。

tcp怎么保证可靠传输?

校验、序号、确认、重传。

Tcp的确认默认是累计重传。即接收方回复ack号是n,说明n号之前的数据都已经接收成功了。

超时重传

./1538277874498.png

如图所示,接收方接收到数据后要给客户端响应,来告诉别人自己收到了数据。 如果压根就没发到服务端,肯定不会收到服务端的响应,客户端等,等到超时后重传上个丢失的数据。 超时时间是动态的。

确认丢失

客户端成功发M1到服务端,服务端响应确认,但是确认包丢失了,客户端一样还会等,重传M1,这时候服务端收到了重复的数据,就会舍弃第二次收到的包,并再发对M1的响应。

确认迟到

客户端在发送数据M1后,服务端发送响应,但是迟到了,客户端没有及时收到响应,超时后重发,服务端舍弃重复收到的数据并响应M1,此时客户端收到迟到的响应,并舍弃响应。

窗口

客户端每次发送后,服务端对其响应,没有收到响应时会等待并重发。这样效率是低下的,由此引入“窗口”来提高效率。 简单来说,在窗口范围内不用先等上次的响应,继续发下面的内容。服务端没有收到数据时会告诉客户端重发。客户端连续三次收到重发标示就会重发失败的数据。

./1538288992405.png

发送窗口=min{接收窗口rwnd,拥塞窗口cwnd) 接收窗口:接收方根据接受缓存设置的值,并告知发送方,反映接收方容量。 拥塞窗口:发送方根据自己估算的网络拥塞程度而设置的窗口值,反映网络当前容量。 发送端有发送缓存窗口,接收端有接收缓存窗口。

流量控制

如果发送方把数据发送得过快,接收方可能会来不及接收,这就会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。

在通信过程中,接收方根据自己的接收缓存大小、动态地调整发送窗口大小,即接收窗口rwnd(receive window接收方设置确认报文段的窗口字段来将rwnd通知给发送方),发送方的发送窗口取决于接收窗口rwnd和拥塞窗口cwnd的最小值)。

设A向B发送数据。在连接建立时,B告诉了A:“我的接收窗口是 rwnd = 400 ”(这里的 rwnd 表示 receiver window) 。因此,发送方的发送窗口不能超过接收方给出的接收窗口的数值。请注意,TCP的窗口单位是字节,不是报文段。

TCP为每一个连接设有一个持续计时器(persistence timer)。只要TCP连接的一方收到对方的零窗口通知,就启动持续计时器。若持续计时器设置的时间到期,就发送一个零窗口控测报文段(携1字节的数据),那么收到这个报文段的一方就重新设置持续计时器

拥塞控制

慢开始和拥塞避免

上面讲到,窗口可以提高传输效率,但是刚开始窗口就比较大,就很可能造成堵塞。所以提出一个慢启动的概念,即刚开始还是先发送1个,慢慢增加这就是慢开始,2个、4个、8个、这样指数型增长,指数增长速度极快,到达一个点后,开始慢速增长,就是为了拥塞避免,这时候换成线性增长,一次增加一个。我们把这个转换的转折点叫做门限ssthresh。发送方维持一个叫做拥塞窗口 cwnd (congestion window)的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。

./pic3.png

我们图中可以看到,门限变为拥塞窗口的一半,再从1开始慢慢增加。

问题来了,如果网络一直不拥塞就可以一直增大吗?不会,发送的窗口大小还要受接收窗口rwnd大小的限制,所以 客户端发送窗口的上限 = min(rwnd,cwnd)

快重传

又一个问题来了,怎么及时知道网络拥塞了? 快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认。这样做可以让发送方及早知道有报文段没有到达接收方。每当比期望序号大的失序报文到达时,就发送ack号为期望号。 发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段。 不难看出,快重传并非取消重传计时器,而是在某些情况下可更早地重传丢失的报文段。

快恢复

当发送端收到连续三个重复的确认时,就执行“乘法减小”算法,把慢开始门限 ssthresh 减半。但接下去不执行慢开始算法。 换句话说,拥塞后并不是从1开始慢慢增加,而是从拥塞窗口的一半也就是ssthresh处开始慢慢增加。

./pic4.png

流量控制和拥塞控制有啥区别?

流量控制是发送端控制的,根据接收端的情况选择发送速度,原理是通过滑动窗口的大小改变来实现。而拥塞控制是根据的传输线路的拥挤程度来选择发送速度。好比是两个车站间传送货物,一个是根据接收车站的接收效率决定,一个是根据路上的车辆拥挤决定。

三次握手

先举个形象的例子,两个人AB进行电话沟通,为了保证双向是通的,常进行以下对话:

A:你能听到我说话吗?

B:可以,你能听到吗?

A:我也可以。

这样双方就知道无论是“去”还是“来”都是正常的,就可以聊天了。

第一次握手中seq=i的i值是随机的。

./三次握手.png

四次挥手

Tcp断开连接时需要4次挥手。 还是先举个例子,两个人进行电话沟通时突然有一个想挂断,一般会有如下对话:

A:我不想说了(第一次挥手。A发送 FIN,表示不A不再说什么事情)

B:好的 (第二次挥手。B发送ACK表示知道了,但是他可能有话没说话,会继续向A说)

B:对了,还有一个事情我要说完…(B没说完话的话继续说)

B:说完了,我也不想说了(第三次挥手。B发送FIN表示他也说完了)

A:好的(第四次挥手。A发送ACK表示知道了,自此完全断开)

在释放连接时,由于TCP是全双工的,因此最后要由两端分别进行关闭,这个流程如下:

假设Client端发起中断连接请求,也就是发送FIN报文。Server端接到FIN报文后,意思是说"我Client端没有数据要发给你了",但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。所以你先发送ACK,“告诉Client端,你的请求我收到了,但是我还没准备好,请继续你等我的消息”。这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文。当Server端确定数据已发送完成,则向Client端发送FIN报文,“告诉Client端,好了,我这边数据发完了,准备好关闭连接了”。Client端收到FIN报文后,“就知道可以关闭连接了,但是他还是不相信网络,怕Server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。“,Server端收到ACK后,“就知道可以断开连接了”。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,我Client端也可以关闭连接了。Ok,TCP连接就这样关闭了!

关闭连接有主动关闭和被动关闭一说,这里为了简化理解,我们以客户端作为主动关闭方,服务器为被动关闭方。

./四次挥手.png

三次握手,四次挥手完整流程:

./1537948069294.png

整个过程Client端所经历的状态如下:

./pic1.gif

而Server端所经历的过程如下:

./pic2.gif

【注意】 在TIME_WAIT状态中,如果TCP client端最后一次发送的ACK丢失了,它将重新发送。TIME_WAIT状态中所需要的时间是依赖于实现方法的。典型的值为30秒、1分钟和2分钟。等待之后连接正式关闭,并且所有的资源(包括端口号)都被释放。

我们不仅疑问,为啥断开的时候会比握手的时候多一次? 第二次握手时,SYN+ACK其实可以放一起,但是第二次挥手时ACK 和 ACK却不能放一起,因为B也许有话没说完

粘包、拆包产生的原因

粘包、拆包问题的产生原因笔者归纳为以下3种:

  1. socket缓冲区与滑动窗口
  2. MSS/MTU限制
  3. Nagle算法

socket缓冲区与滑动窗口

每个TCP socket在内核中都有一个发送缓冲区(SO_SNDBUF )和一个接收缓冲区(SO_RCVBUF),TCP的全双工的工作模式以及TCP的滑动窗口便是依赖于这两个独立的buffer的填充状态。

SO_SNDBUF:

进程发送的数据的时候假设调用了一个send方法,最简单情况(也是一般情况),将数据拷贝进入socket的内核发送缓冲区之中,然后send便会在上层返回。换句话说,send返回之时,数据不一定会发送到对端去(和write写文件有点类似),send仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中。

SO_RCVBUF:

把接受到的数据缓存入内核,应用进程一直没有调用read进行读取的话,此数据会一直缓存在相应socket的接收缓冲区内。再啰嗦一点,不管进程是否读取socket,对端发来的数据都会经由内核接收并且缓存到socket的内核接收缓冲区之中。read所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,仅此而已。

滑动窗口:

TCP连接在三次握手的时候,会将自己的窗口大小(window size)发送给对方,其实就是SO_RCVBUF指定的值。之后在发送数据的时,发送方必须要先确认接收方的窗口没有被填充满,如果没有填满,则可以发送。

每次发送数据后,发送方将自己维护的对方的window size减小,表示对方的SO_RCVBUF可用空间变小。

当接收方处理开始处理SO_RCVBUF 中的数据时,会将数据从socket 在内核中的接受缓冲区读出,此时接收方的SO_RCVBUF可用空间变大,即window size变大,接受方会以ack消息的方式将自己最新的window size返回给发送方,此时发送方将自己的维护的接受的方的window size设置为ack消息返回的window size。

此外,发送方可以连续的给接受方发送消息,只要保证对方的SO_RCVBUF空间可以缓存数据即可,即window size>0。当接收方的SO_RCVBUF被填充满时,此时window size=0,发送方不能再继续发送数据,要等待接收方ack消息,以获得最新可用的window size。

MSS/MTU分片

MTU (Maxitum Transmission Unit,最大传输单元)是链路层对一次可以发送的最大数据的限制。MSS(Maxitum Segment Size,最大分段大小)是TCP报文中data部分的最大长度,是传输层对一次可以发送的最大数据的限制。

SYN Flood洪泛攻击

原理:攻击者首先伪造地址对 服务器发起SYN请求,服务器回应(SYN+ACK)包,而真实的IP会认为,我没有发送请求,不作回应。服务 器没有收到回应,这样的话,服务器不知 道(SYN+ACK)是否发送成功,默认情况下会重试5次(tcp_syn_retries)。这样的话,对于服务器的内存,带宽都有很大的消耗。攻击者 如果处于公网,可以伪造IP的话,对于服务器就很难根据IP来判断攻击者,给防护带来很大的困难。

linux内核参数调优主要有下面三个:

  • tcp_max_syn_backlog 从字面上就可以推断出是什么意思。在内核里有个队列用来存放还没有确认ACK的客户端请求,当等待的请求数大于tcp_max_syn_backlog时,后面的会被丢弃。 所以,适当增大这个值,可以在压力大的时候提高握手的成功率。手册里推荐大于1024。
  • tcp_synack_retries 这个是三次握手中,服务器回应ACK给客户端里,重试的次数。默认是5。显然攻击者是不会完成整个三次握手的,因此服务器在发出的ACK包在没有回应的情况下,会重试发送。当发送者是伪造IP时,服务器的ACK回应自然是无效的。 为了防止服务器做这种无用功,可以把tcp_synack_retries设置为0或者1。因为对于正常的客户端,如果它接收不到服务器回应的ACK包,它会再次发送SYN包,客户端还是能正常连接的,只是可能在某些情况下建立连接的速度变慢了一点。
  • tcp_syncookies Linux中SYN cookie是非常巧妙地利用了TCP规范来绕过了TCP连接建立过程的验证过程,从而让服务器的负载可以大大降低。 在三次握手中,当服务器回应(SYN + ACK)包后,客户端要回应一个n + 1的ACK到服务器。其中n是服务器自己指定的。当启用tcp_syncookies时,linux内核生成一个特定的n值,而不并把客户的连接放到半连接的队列里(即没有存储任何关于这个连接的信息)。当客户端提交第三次握手的ACK包时,linux内核取出n值,进行校验,如果通过,则认为这个是一个合法的连接。

面试问题

问题1. 为什么是四次挥手

发送FIN的一方就是主动关闭(客户端),而另一方则为被动关闭(服务器)。当一方发送了FIN,则表示在这一方不再会有数据的发送。其中当被动关闭方受到对方的FIN时,此时往往可能还有数据需要发送过去,因此无法立即发送FIN(也就是无法将FIN与ACK合并发送), 而是在等待自己的数据发送完毕后再单独发送FIN,因此整个过程需要四次交互。

问题2. 什么是半关闭

客户端在收到第一个FIN的ACK响应后,会进入FINWAIT2 状态时,此时服务器处于 CLOSEWAIT状态,这种状态就称之为半关闭。从半关闭到全关闭,需要等待第二次FIN的确认才算结束。此时,客户端要等到服务器的FIN才能进入TIMEWAIT, 如果对方迟迟不发送FIN呢,则会等待一段时间后超时,这个可以通过内核参数tcpfin_timeout控制,默认是60s。

问题3. RST 是什么,为什么会出现

RST 是一个特殊的标记,用来表示当前应该立即终止连接。以下这些情况都会产生RST:

向一个未被监听的端口发送数据 对方已经调用 close 关闭连接 存在一些数据未处理(接收缓冲区),请求关闭连接时,会发送RST强制关闭 某些请求发生了超时

题4.为什么需要TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

答:TIME_WAIT是主动提出结束的一方的状态,对方发送FIN说“我也说完了”,这边回应ACK,但是这个ACK可能会丢失,那么对方就会以为你没有收到,他会重发。如果不经历2MSL直接进入CLOSE状态,新的连接建立后,你会莫名收到一个FIN。

四次挥手中,A 发 FIN, B 响应 ACK,B 再发 FIN,A 响应 ACK 实现连接的关闭。而如果 A 响应的 ACK 包丢失,B 会以为 A 没有收到自己的关闭请求,然后会重试向 A 再发 FIN 包。

如果没有 TIME_WAIT 状态,A 不再保存这个连接的信息,收到一个不存在的连接的包,A 会响应 RST 包,导致 B 端异常响应。

此时, TIME_WAIT 是为了保证全双工的 TCP 连接正常终止。

题5.为什么TIME_WAIT状态需要经过2MSL才能返回到CLOSE状态?一个MSL行不行?

答:一个不行。

最大分段寿命(MSL, Maximum Segment Lifetime),它表示一个 TCP 分段可以存在于互联网系统中的最大时间,由 TCP 的实现,超出这个寿命的分片都会被丢弃。

A主动提出结束。第四次挥手,A向B回复 ack。B如果没有收到会重发FIN,所以A要等待足够的时间来判断B会不会发FIN。

A并不知道B是否接到自己的ACK,A是这么想的: 1)如果B没有收到自己的ACK,会超时重传FiN那么A再次接到重传的FIN,会再次发送ACK 2)如果B收到自己的ACK,也不会再发任何消息,包括ACK 无论是1还是2,A都需要等待,要取这两种情况等待时间的最大值,以应对最坏的情况发生,这个最坏情况是:去向ACK消息最大存活时间(MSL) + 来向FIN消息的最大存活时间(MSL)。这恰恰就是2MSL( Maximum Segment Life)。

参考文章: https://blog.csdn.net/Neo233/article/details/72866230 https://blog.csdn.net/hacker00011000/article/details/52319111 https://www.zhihu.com/question/67013338/answer/248375813