作为前端的你了解多少tcp的内容

关于tcp你知道多少

经常在前端的面试群中发现有人会碰到面试官去询问tcp的握手和挥手问题,诸如你了解tcp吗,解释一下tcp的三次握手和四次挥手,我认为如果只是简单的了解这2个问题,真的那么有意义吗?所以,不防试着去多了解一点网络通信的内容,记得在上家公司的时候有个老哥说过,网络通信其实还是蛮重要的,毕竟我们现在无论是工作还是生活基本都处于互联网之中,尤其作为开发者基本上每天都在和http请求打交道,so 了解网络传输的原理还是有必要的,下面我们稍微深入的来看下网络传输的内容。

base

OSI(Open Systems Interconncection,开放系统互联)网络分层

从上到下分别是:

7.应用层(Application)
6.表示层(Presentation)
5.会话层(Session)
4.传输层(Transport)
3.网络层(Network) -- 路由器
2.数据链路层(Data Link) -- 交换机
1.物理层(Physical) -- 网卡、集线器(Hub)

基础内容不做过多的讲解,有需要的可以出门右转 ok 我们今天主要关注的是tcp层的内容,下面的内容,如果有兴趣建议大家按照步骤实际操作去看看,首先介绍一个工具wireshark,这个工具可以帮助我们抓到tcp以及更底层的包,下载到这里,打开后有个download,下载自己系统能用的就好,一路安装到全部完成,接下来我们开始一次实战抓包。

抓包

我这里利用了百度的首页做了一次抓包实验,首先要设置一个过滤

你可以选择http 和 tcp 或者 tcp only

然后到控制台中去ping 一下百度首页的ip

ok 看到ip是180.97.33.108

然后我们到wireshark中设置一下查询的ip

到chrome中打开百度的首页,然后就能看到tcp的传输信息了

内容有点多,我们通过这个内容来观察一下tcp的传输过程

tcp base

先看下tcp的头部报文结构 1.tcp协议层是不关心ip的,具体ip的定位是由ip层来决定的,但是tcp层需要确定端口号,所以他会携带source 和 destination的port信息,以便能找到对应的端口号;

2.sequence number 实际中使用的SEQ,也就是序号,这个序号起了很重要的作用,我们都知道tcp和udp最大的区别在于tcp是稳定并且有序的,其中seq就可以保证有序,当A向B发送一个数据包的时候,seq会叠加,每一个传输方在传送数据的时候都会带上这个信息,另一端能按照这个序号来排序收到信息的顺序,从未保证了信息的传递是有序的,也能通过它来确认有没有出现丢包的情况;另外要注意的是当有数据需要发送的时候,seq会随该序列号为原点,对自己将要发送的每个字节的数据进行编号,比如当前seq = 10,本次要发送的数据包大小是200字节,那么实际发送的时候会更新seq=210,以便保证传输的数据的顺序;

3.acknowledge number,实际中使用的ACK,是另一端对对方seq的一个回应,一般会把对方给的seq+1然后下一次发包的时候带上,这样的话对方就知道我们是收到前面的消息的;

4.windown代表的是滑动窗口,实际中用win来表示,win的大小很重要,win越大的传输越快,因为win的大小直接决定了某一端一次可以同时发送多少个数据包,而不用等待对方的应答ACK回来,但是win会随着每一个数据包的发送而变小(稍后解释);

5.reserved 是tcp传输很重要的角色,标志位,响应方会根据对方给的信号执行对应的操作,比如执行断开连接的时候一般都是使用FIN标志位;

基础内容不做过多介绍,不懂的可以移步这里先看下概念,后面我们会结合实际来介绍

三次握手(敲黑板)

观察前几次传输,TLS类型的我们可以先忽略,它是处理ssl加密的内容,有兴趣的可以自己去google,重点看前三次的tcp,第一次是我们自己的ip向百度的服务器ip发了第一个包,seq=0,起始的数据信号是0,win=65535代表我这边的窗口大小是65535,len=0代表我这边希望接受的包的大小长度是0,mss代表我这边本次传输能接收的最大包的内容是1460(其余的我们暂时不用关心)。下面我们模拟一下对话内容:

A:B,你好,我是A 请求建立连接,我的seq是0,我的win是65535,我希望本次回应我的内容长度len为0,我本次能接收的最大内容是1460,over; B:A,你也好,收到你的信息了,我是B,我本次的seq是0(注意,双方的序号是独立计算的,这里都从0开始的),我回应你的ack是1(A的seq+1,代表我收到你seq是0的消息了),我的窗口大小是8192,我希望你回应我本次消息的len也是0,我这边能接收的最大回应大小是1452,over; A:好的,我收到你的回应了,我现在给你发送的seq是1(上一次是0,这次是1),我回应你的ack是1(B的seq+1),我当前的窗口大小是25984,我希望的回应长度是0;我们建立好连接了,over;

到这里,完整的三次握手就结束了,后面就可以执行别的数据传输了,到这里,不知道有没有想过,为什么确定一次连接需要三次握手,不是1次,也不是2次,也不是4次,

首先,1次肯定不行,1次的话 一方无法确认另一方的情况,所以最少都是2次起步,

2次:

A:喂喂喂,我是A,你听的到吗? B:在在在,我能听到,我是B,你能听到我吗? A:(听到了,老子不想理你) B:喂喂喂?听不听到?我X,对面死了,我挂了。。

4次:

A:喂喂喂,我是A,你听的到吗? B:在在在,我能听到,我是B,你能听到我吗? A:听到了,你呢?你能听到吗? B:??你是智障?我不是说了我能听到吗,不想跟xx说话。。。

所以最合理的还是3次:

A:喂喂喂,我是A,你听的到吗? B:在在在,我能听到,我是B,你能听到我吗? A:听到了。我们今天去钓鱼吧。。balabala

so,就是这样,其实不是不能更多,但是可靠的同时,还要考虑性能和时间问题,所以,目前公认的握手次数还是三次比较合理。

四次挥手

我们知道tcp的连接是全双工的,A和B是可以互相通信的,不理解的话,可以想想打电话(类比,不要当真),打电话的场景就是单双工的,因为同一时间只能一个人说话,另一个人听,如果2个人一起说话,那谁都听不清楚了,没有意义,但是tcp是全双工的,就是A 正在给 B发信息的同时,B也在给A发信息,所以当断开的时候,必须要求双方都得知道,如果只有一方知道,肯定不行,因此,断开的时候,就需要下面这样:

A:B,不好意思,我这边需要关闭连接了,你准备一下?(发了一个fin信号给B,等待回应)

B:好的A,我收到你的关闭信号了,我还有数据没发好,你等我下(回应A,带回去ACK的最后一个信息,失败可以重发)

B:A老弟,我好了,我可以关闭了,给你最后说一下,等下你回应我的话,我就直接关了;

A:好的老哥,我回应你一下,你收到就关闭吧,不用理我(发完这条信息后,进入time_wait状态)

B:(收到ack信息,直接就关闭了),此过程不产生数据的交互,不算挥手次数

A:等待2MSL(最大报文段生存时间)后,B没东西给过来,我也关了;

到这里4次挥手就结束了,2个问题:

1.为什么握手需要三次,而挥手却需要四次?

握手的时候,A和B打个招呼,B可以直接把自己的SYN信息和对A的回应ACK信息一起带上,但是挥手的时候,A说我要断开了,B还没发完最后的数据,因此需要先回应一下A,我收到你的断开的请求了,但是你要等我把最后的内容给你,所以这里分开了2步: (1)回应A; (2)发送自己的最后一个数据

2.为什么A进入TIME_WAIT需要等待最大报文段生存的时间后,才能关闭?

原因是,担心网络不可靠而导致的丢包,最后一个回应B的ACK万一丢了怎么办,在这个时间内,A是可以重新发包的,但是超过了最大等待时间的话,就算收不到也没用了,所以就可以关闭了。

如何理解滑动窗口的作用?

从上面的内容,我们简单了解了三次握手和四次挥手的内容,然后也知道了一些报文字段的意义,但是网络本身是不稳定的,也就是说中间无法保证数据包一定会到对面,那么tcp是如何在尽可能少的时间内实现稳定和有序传输的?

我们知道SYN信息中会带上自己的seq,序号,这样可以保证另一方接受到后知道如何排序,但是如果发送必须都是同步的,想象,A 给 B发送的时候,需要给B 1,2,3,4,5个包,发了1后,死等1的ack回来,再给2,死等2的ack回来,在linux下每个tcp的timeout最大是2^5 - 1 = 63s(默认的retrytime是5次)的时间,因为当发了一个包出去后,在一定时间内没收到ACK回应,为了确认不能丢包的问题,会启动重试机制,重试5次,它们的延迟分别是:1 秒、3 秒、7 秒、15 秒、31 秒,其中31s是前5次重试的时间1+2+4+8+16=31s,最后的32s是等待最后一次重试也超时(等待的时间是2的N次方秒),所以一共就是63s,如果一个一个等,是不是有点太恐怖了,万一网络环境比较差,所以为了能在不丢包的情况下,尽量减少时间的损耗,引入了滑动窗口的概念,window

由于窗口由16位bit所定义,所以接收端TCP,窗口能最大提供65535个字节的缓冲,其实这个滑动窗口主要就是做限流和缓冲用的,每一个tcp传输中的win提供的是对方的窗口大小,当A向B发数据的时候,超过B的win长度的数据会被丢掉,同时窗口还可以提高发送数据的效率,通过类似于并发的行为,如下图:

可以看到A向B连续发了3条数据,但是回应B的ACK没有变,也就是说都是回应同一个B的同一个响应,但是A自己的seq更新了3次,先是1,然后是69最后是1521,说明这三个包是连续发出去的,实际上只要当前数据包的大小不超过对方的window大小,就可以连续发的,接着看:

这四个是B响应给A的数据包,可以看到都是在响应A给的数据包,注意看因为A连续发了好几条,B可能一下反应不过来,所以B会把这些信息放到缓冲区,但是放的数据越多,那么自己的缓冲区就越小,就是通过B自己的win来体现,我们可以看到B的win再连续变小,说明它还没处理好,到第4条信息的时候,我们看到B给了一个信息叫WINDOW UPDATE 然后发现B的win变大了,这就意味着它已经处理好A之前连续发的几个数据包了,然后就会重新更新自己的win的大小,要注意的是当tcp一端的win接近或者等于0的时候,传输将会停止,直到window update更新说buffer已经清空了,传输才会继续。

丢包?

看下面这个图

明显能看到ACK是连续变大的,在一次连接中,不可能存在说先回复ACK=3922然后再回复ACK=3913,这样的话另一端在收到3922的时候就认为之前的全部接收到了,实际上3913还没收到,要注意SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包。那么,考虑以下情况,假如A给的分别是1,2,3,4到5 5个包,B这边收到1,Ack一个2(代表收到1了),然后2丢了,3、4和5收到了,能直接ACK = 6吗?当然不行,这样的话tcp就是不稳定的了,考虑超时重传的2种方案:

1.timeout后只重新传2; 2.timeout后重新给2、3、4、5;

2种方案有好有坏,第一种比较慢,第二种浪费带宽,所以tcp引入了一种快速超时重试机制(Fast Retransmit算法),不以时间计算,而以数据做驱动重新传送,如果包没有连续到达,比如1到了,2没到,3,4,5也到了,这个时候,B始终返回ACK=2,代表只确认1,然后A就知道2没到,重新发2,但是B一旦收到2会直接ACK=6给A,这个的意思就是说2拿到后,345也收到了,直接给6就ok,如下图:

上面说的只是一种特别简单的方案,目前,linux2.4之后,采用了一种更先进的方式,有想了解的可以走这里

攻击

典型的场景是DDOS攻击,也可以说是tcp的SYN Flood攻击,又叫洪水攻击; 根据上面的分析,我们知道tcp的握手环节是比较耗时的,当client端发起连接请求的时候,server端会回应,然后等待client的最终确认信息,默认情况下的linux会等待1到63s这样(如果有特殊的设置,这个时间可以到1-2min这样),默认最长是63s之后才会断开,之前这段时间内属于半连接的状态,服务器不会丢弃掉这些连接,而是会等,试想如果有一个人突然想你的server瞬间之内发送了几千万个连接请求,但是对服务端的响应不做理睬,这样很容易就导致我们正常的tcp连接进不去,从而出现服务拒绝的情况,而他只需要一个简简单单的脚本去给你丢包就可以了,这种情况就会导致服务器对正常的客户端表现为宕机。。此种攻击的成本比较低,但是防护却特别麻烦,因为你必须要保证正常的不能因为访问次数的提高而出现拒绝。

另外一个没有这个情况严重的攻击是ACK Flood攻击,有兴趣的可以自行去查看。

TCP是个巨复杂的协议,我们今天通过一次抓包了解了三次握手,四次挥手,滑动窗口。超时重传机制和典型的tcp攻击方式,希望能对各位看官对tcp的认识有一定的帮助;

声名:以上内容都属于个人理解和总结,如有问题,欢迎指出~

猜你喜欢

转载自juejin.im/post/5c078058f265da611c26c235