赞
踩
今天开始,我们整理一些关于TCP协议的知识。这块的内容写起来是非常费劲的,因为本身TCP协议就不是一个简单的协议,它能获得如今的地位,和其复杂且出色的表现是分不开的。
众所周知,TCP是一款面向连接的协议,那首先,我们需要知道这个“面向连接”指的是什么?
简单的理解,“面向连接”首先得有“连接”。TCP协议在传输数据的时候,并不像UDP那样随心所欲,而是需要在发送数据之前,先建立一条点到点的连接。这就是“面向连接”和“无连接”的区别。(UDP就被称为是一种“无连接”的协议。)
这句话中,**点到点(point to point)**是我们第一个需要关注的点。在TCP的通讯中,永远只有通讯双方,而不存在第三方。它不像UDP协议,可以一个主机同时向多个主机发送消息,一对多对于TCP而言,是不可以接受的。
再一个重点就是这个连接了。当然,这个连接并不是指物理链路上的连接,而是一种逻辑上的连接。TCP为保证双方可以正常的完成可靠性通讯,所以,需要提前同步一些初始参数(这里的参数很多是和后面我们要接触的TCP具体机制有关,此处不需要具体关注),这个参数同步的过程就是建立连接的过程。其共同状态仅保留在两个通信端系统的TCP程序中,而不会在中间节点进行维护(中间节点只转发数据的路由器,交换机等网络设备)。所以,TCP协议也被称为“端到端”的协议。
不同的TCP连接是如何区分的呢?
区分不同的TCP连接主要靠四个参数 — 源IP地址,源端口,目标IP地址,目标端口。所以,这四个参数被称为是TCP连接的“四元组”。四元组可以唯一的标识一条TCP连接。
在具体的看连接建立的过程之前,我们先来看下TCP的报文结构。因为报文中的一些参数,其作用和我们建立连接是强相关的。
简答的解释一下里面定义的变量。
这里关于序列号的使用方法给一个小例子:
假设,TCP的通信双方A和B。A想要通过TCP连接向B发送一个数据流。假设一共有5000个字节。而TCP协议在发送的时候,将这50000个字节的数据拆分为了多个数据段,每一个数据段长度为1000个字节。假定,数据流的首字节编号为0,那第一个报文段的序号取值应该是从0到999的。则第二个报文段分配的序号应该就是1000。其实每一个报文段使用的序号就是这个报文段中第一个字节所分配的序列号,以此类推。
这里关于确认序列号也给出一个小例子:
假设,通信双方还是A和B。现在,A给B发送了一个报文段,这个报文段的序列号假设为0,这个报文段所包含的数据部分的字节数假设为1000字节。(就是上个例子中第一个报文段的数据被发送了过来)。B收到该数据段之后,按照要求,需要回复一个确认报文,这个确认报文中的确认序列号应该指示的是B想要的下一个字节的序号,则因该就是1000。(第二个数据段第一个字节的序号)。当然,同时它也代表B已经收到了1000之前所有的字节。则A收到B的确认报文之后,下一个数段就会去携带序号从1000开始的第二段数据。
我们一般将TCP建立连接的过程称为“三次握手”,这个过程中完成的其实就是上面提到的同步参数的任务,只不过,一般这个过程都是由三个数据包的交互来完成的,所以,我们习惯性的称之为“三次握手”。
TCP的连接过程是由参与TCP通讯的双方中的其中一方发起的,我们一般认定发起方为客户端,而另一方为服务器。这个TCP连接一旦建成,则将是一个双向的会话,即客户端可以向服务器发送信息,服务器也可以向客户端发送信息。
值得注意的是,这个TCP报文段是允许携带数据的。因为在客户端接收到服务器发送的AYN+ACK报文段之后,客户端指向服务器的会话就已经建立完成了。服务器已经做好了充足的准备来接收客户端发送的数据了。只是,服务器指向客户端的会话必须得等到这个数据包被服务器接收处理后才能建成。所以,整个TCP连接的建立是需要这完整的三个步骤的,也就是“三次握手”。
为什么每次初始的序列号都使用随机值而不使用固定值?
其实这样设计主要时为了防止历史报文被相同四元组的TCP连接接收。如果一个已经失效的连接被重用了,但是该旧连接的历史报文还残留在网络中,如果序列号相同,那么就无法分辨出该报文是不是历史报文,如果历史报文被新的连接接收了,则会产生数据错乱。所以,每次建立连接前重新初始化一个序列号主要是为了通信双方能够根据序号将不属于本连接的报文段丢弃。
正常的三次握手建立过程,我们刚才已经说完了。我们观察这张图发现,两边还写了一些close,listen之类的标识。其实,TCP连接在建立的过程中,也是可以分为不同的阶段的。不同阶段,我们客户端或者服务器的状态都不尽相同,所以,我们使用不同的状态来进行标识。
我们先从客户端的角度来看下状态的变化。
我们再从服务器的角度看下状态的变化。
我们上面讨论的都是假定客户端和服务器都已经准备好的情况下,但如果连接情况没有那么顺利该怎么办呢?
例如,假如一台主机接收到一个TCP SYN报文段,里面的目的端口号是80端口。但是,该主机80端口并不接受连接,可能是因为它并没有运行web服务器。
在这种情况下,主机就会给发送源发一个TCP报文段,将其中的RST标记位置1。用来中断这次连接。(一般发送到一个无效的TCP连接时,都会使用RST报文段来终止)。
在上面的描述过程中,提到了一种攻击手段,叫做SYN泛洪攻击,是Dos攻击中的一种。这是一种很经典的攻击手段,虽然已经有了有效的应对之法,但是其原理还是可以略做了解的。
其实,其原理还是非常简单的。我们前面说到了,客户端向服务器发送完SYN请求之后,服务器会为这次连接分配缓存空间,用来去接收后续TCP数据流中的数据。之后再发送SYN+ACK的报文段,并等待来自客户的ACK确认。这时的状态可以被称为是一种半连接状态。当然,如果客户端不发送ACK来完成第三次握手,服务器也会在超时(通常是1分钟)之后终止半连接并回收资源(服务器发送RST报文来终结连接)。
攻击者就是利用这一点,疯狂发送SYN请求报文段,而不去发送最终的ACK应答。这就导致服务器不得不对这些纷至沓来的半连接分配资源。最终,导致服务器的资源被消耗殆尽,达到攻击服务器的效果。
当然,现在其实已经有了一种比较有效的防御手段,我们称为SYN Cookie。
用这种机制之后,服务器将不再收到SYN请求报文后立即给这个TCP连接分配缓存空间了。而是会及将这个SYN报文中的源目IP地址以及端口号和一个随机数一起使用HASH算法生成一个摘要值。我们将这个摘要值称为是SYN Cookie。然后,服务器会使用这个SYN Cookie作为服务器的初始序列号server_isn来发送SYN+ACK报文(这个初始值本身就可以是一个随机值),等待客户端回复ACK。
如果客户端是合法的,则会正常回复ACK报文,并且其中会包含一个确认序列号。这个确认序列号应该是server_isn + 1。服务器将使用这个ACK报文中的源目IP和端口以及之前的随机数运行HASH重新计算一个摘要值。如果这个摘要值+1和客户端返回的确认序列号相同,则认为该连接合法,就会为该连接分配缓存空间。
如果客户不合法,自然不会返回ACK报文,则初始的SYN对服务器而言也并没有产生什么危害,因为服务器没有为他分配任何资源。
前面我们把整个TCP的连接过程搞清楚了,我们已经觉得TCP连接的建立就是需要三次过程才合理的。但我们还是应该避免这样的思维定式,我们还是应该多想一步,为什么就不能是两次或者四次呢?
其实不是四次,这个问题好理解,能三次为啥四次呢?四次握手无非就是服务器用来确认的ACK报文和用来请求的SYN报文分开来发送,但能合在一起并不会造成任何额外的问题,而且还可以节约资源。三次握手其实就是四次握手的一个简化。
这个问题就需要好好的研究一下了,因为如果能两次的话,那肯定是两次更加节约资源,又何必三次呢?
两次握手是一个什么样的概念呢?客户端发送请求建立,服务器收到请求后,给连接分配缓存空间,服务器端进入到ESTABLISHED状态;之后,回复SYN + ACK报文段。客户端收到后为该TCP连接分配缓存,客户端进入到ESTABLISHED状态,代表整个TCP的建立完成。
两次握手的连接过程表面上看,似乎也比较合理,但仔细去琢磨就会发现,这样的偷懒可能会带来不必要的麻烦。
这其中最主要的一个问题就是两次握手无法在发送数据之前识别出是历史连接,而造成资源浪费。三次握手可以防止旧的重复连接造成混乱。这个是在RFC文档中指出的。
这里说的这个旧的历史连接是啥意思呢?我们先给大家一个三次握手中的场景,帮大家理解下这种情况。
假设,客户端发送一个SYN请求报文段,其中的初始序列号Client_isn = 90。之后,因为设备故障,客户端宕机了。祸不单行的是,这个SYN请求报文段,也被阻塞在了这网络世界当中,导致服务器一时间并没有收到这个请求。很快,客户端重新启动了,则将重新发送一个SYN请求报文段,其中的初始序列号Client_isn = 100。(注意,两次都是随机产生的初始序列号。这种并不是客户端因为没有收到服务器的ACK而重传的报文,因为重传的报文中的序列号应该和之前的相同。)
重新发送之后,旧的序号为90的SYN请求报文段先到达了服务器,则服务器会应答一个SYN + ACK报文段,这个报文段中的确认序列号的值为90 + 1。客户端在收到这个报文段之后,发现其中的确认序列号并不是自己所期望的。(因为自己发送的请求报文中的序号为100,期望收到的确认序列号应该是100 + 1)。则此时,客户端将判定这是一个旧的历史连接,则将发送一个RST报文段来关闭这个连接。
之后,新的SYN报文段终于抵达了服务器,服务器收到之后,将应答新的SYN + ACK报文段,这次报文段中的确认序列号应该是100 + 1。客户端收到后,则将应答最终的ACK报文,完成TCP连接的建立。
三次握手在这样的场景中,就表现非常出色,并不会因为旧的历史连接而造成资源的浪费。
但是,如果现在使用的是两次握手呢?我们再来分析一下。
在两次握手中,服务端在收到 SYN 报文后,就进入 ESTABLISHED 状态,意味着这时可以给对方发送数据,但是客户端此时还没有进入 ESTABLISHED 状态,假设这次是历史连接,客户端判断到此次连接为历史连接,那么就会回 RST 报文来断开连接,而服务端在第一次握手的时候就进入 ESTABLISHED 状态,所以它可以发送数据的,但是它并不知道这个是历史连接,它只有在收到 RST 报文后,才会断开连接。
可以看到,如果采用两次握手建立 TCP 连接的场景下,服务端在向客户端发送数据前,并没有阻止掉历史连接,导致服务端建立了一个历史连接,又白白发送了数据,妥妥地浪费了服务端的资源。
因此,**要解决这种现象,最好就是在服务端发送数据前,也就是建立连接之前,要阻止掉历史连接,这样就不会造成资源浪费,而要实现这个功能,就需要三次握手。**这个就是使用三次握手而不是两次握手的一个最主要的原因。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。