TCP报文结构图
首部固定部分各字段意义如下:
1 - 源端口和目的端口:各占 2 个字节,分别写入源端口和目的端口。IP 地址 + 端口号就可以确定一个进程地址
2 - 序号/序列号(Sequense Number,SN):在一个 TCP 连接中传送的字节流中的每一个字节都按顺序编号。该字段表示本报文段所发送的数据的第一个字节的序号。初始序号称为 Init Sequense Number, ISN(序号/序列号这个字段很重要,大家留个印象,下文会详细讲解)
例如,一报文段的序号是 101,共有 100 字节的数据。这就表明:本报文段的数据的第一个字节的序号是 101,最后一个字节的序号是 200。显然,下一个报文段的数据序号应当从 201 开始,即下一个报文段的序号字段值应为 201。
3 - 确认号 ack:期望收到对方下一个报文段的第一个数据字节的序号。若确认号为 N,则表明:到序号 N-1 为止的所有数据都已正确收到。
4 - 数据偏移(首部长度):它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远。这个字段实际上是指出TCP报文段的首部长度。
5 - 保留:占 6 位,应置为 0,保留为今后使用。
大家看上图,保留位的右边还有 6 个控制位(重要),这是TCP 用来说明该报文段性质的:
- 紧急位 URG:当 URG = 1时,表明此报文段中有紧急数据,是高优先级的数据,应尽快发送,不用在缓存中排队。该控制位需配合紧急指针使用(紧急指针指出本报文段中紧急数据的字节数)
举个例子:我们需要取消一个已经发送了很长程序的运行,因此用户从键盘发出中断命令。如果不使用紧急数据,那么这个指令将存储在接收 TCP的缓存末尾,只有在所有的数据被处理完毕后这两个字符才被交付接收方的应用进程,这样做就无法实现立即中断。 - 确认 ACK:仅当 ACK = 1 时确认号字段才有效,当 ACK = 0 时确认号无效。TCP规定,在连接建立后所有传送的报文段都必须把 ACK 置为 1。
- 推送 PSH:当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应。在这种情况下,TCP就可以使用推送(push)操作。这时,发送方 TCP 把 PSH 置为 1,并立即创建一个报文段发送出去。接收方 TCP 收到 PSH =1 的报文段,就尽快地交付接收应用进程。而不用等到整个缓存都填满了后再向上交付。
- 复位 RST:当 RST = 1 时,表明 TCP连接中出现了严重错误(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立传输连接。
- 同步 SYN:SYN = 1 表示这是一个连接请求或连接接受报文。
当 SYN = 1 而 ACK = 0 时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使 SYN = 1 且 ACK = 1。 - 终止 FIN:用来释放一个连接。当 FIN = 1时,表明此报文段的发送发的数据已发送完毕,并要求释放运输连接。
三次握手( three-way handshake)
所谓的握手即一次发包到接收的过程,可能从客户端发送到服务端,也可能从服务端发送到客户端。
进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号(Init Sequense Number, ISN) 为后面的可靠性传输做准备
过程图
三次握手的流程图:
回顾一下图中字符的含义:
- SYN:同步位,表示连接请求
- seq:序列号
- ACK:确认位,1有效 0无效
- ack:确认号,对方发送序号+1
刚开始客户端处于 Closed 的状态,而服务端处于 Listen 状态:
CLOSED :没有任何连接状态
LISTEN :侦听来自远方 TCP 端口的连接请求
解析
1)第一次握手: 客户端向服务端发送一个 SYN 报文(SYN = 1),并指明客户端的初始化序列号 ISN(x),即图中的 seq = x,表示本报文段所发送的数据的第一个字节的序号。此时客户端处于 SYN_Send 状态。
SYN-SENT :在发送连接请求后等待匹配的连接请求
**2)第二次握手:**服务器收到客户端的 SYN 报文之后,会发送 SYN 报文作为应答(SYN = 1),并且指定自己的初始化序列号 ISN(y),即图中的 seq = y。同时会把客户端的 ISN + 1 作为确认号 ack 的值,表示已经收到了客户端发来的的 SYN 报文,希望收到的下一个数据的第一个字节的序号是 x + 1,此时服务器处于 SYN_REVD 的状态。
SYN-RECEIVED:在收到和发送一个连接请求后等待对连接请求的确认
**3)第三次握手:**客户端收到服务器端响应的 SYN 报文之后,会发送一个 ACK 报文,也是一样把服务器的 ISN + 1 作为 ack 的值,表示已经收到了服务端发来的的 SYN 报文,希望收到的下一个数据的第一个字节的序号是 y + 1,并指明此时客户端的序列号 seq = x + 1(初始为 seq = x,所以第二个报文段要 +1),此时客户端处于 Establised 状态。
服务器收到 ACK 报文之后,也处于 Establised 状态,至此,双方建立起了 TCP 连接。
ESTABLISHED:代表一个打开的连接,数据可以传送给用户
为什么要三次握手
三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。
只有经过三次握手才能确认双发的收发功能都正常,缺一不可:
- 第一次握手(客户端发送 SYN 报文给服务器,服务器接收该报文):
客户端什么都不能确认;
服务器确认了对方发送正常,自己接收正常
- 第二次握手(服务器响应 SYN 报文给客户端,客户端接收该报文):
客户端确认了:自己发送、接收正常,对方发送、接收正常;
服务器确认了:对方发送正常,自己接收正常
- 第三次握手(客户端发送 ACK 报文给服务器):
客户端确认了:自己发送、接收正常,对方发送、接收正常;
服务器确认了:自己发送、接收正常,对方发送、接收正常
ISN (Initial Sequence Number) 是固定的吗
三次握手的其中一个重要功能是客户端和服务端交换 ISN(Initial Sequence Number),以便让对方知道接下来接收数据的时候如何按序列号组装数据。
当一端为建立连接而发送它的 SYN 时,它会为连接选择一个初始序号。ISN 随时间而变化,因此每个连接都将具有不同的 ISN。如果 ISN 是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。
三次握手过程中可以携带数据吗
第三次握手的时候,是可以携带数据的。但是,第一次、第二次握手绝对不可以携带数据
假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据,然后疯狂重复发 SYN 报文的话(因为攻击者根本就不用管服务器的接收、发送能力是否正常,它就是要攻击你),这会让服务器花费很多时间、内存空间来接收这些报文。
简单的记忆就是,请求连接/接收 即 SYN = 1 的时候不能携带数据
而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以当然能正常发送/携带数据了。
半连接队列
服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把这种状态下的请求连接放在一个队列里,我们把这种队列称之为半连接队列。
当然还有一个全连接队列,完成三次握手后建立起的连接就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。
SYN 洪泛攻击
SYN 攻击就是 Client 在短时间内伪造大量不存在的 IP 地址,并向 Server 不断地发送 SYN 包,Server 则回复确认包,并等待 Client 确认,由于源地址不存在,因此 Server 需要不断重发直至超时,这些伪造的 SYN 包将长时间占用半连接队列,导致正常的 SYN 请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。
使用SYN Cookie进行有效地防御
所谓的SYN Cookie防御系统,与前面接收到SYN 报文就分配缓存不同,此时暂不分配资源;同时利用SYN 报文的源和目的地IP和端口,以及服务器存储的一个秘密数,使用它们进行散列,得到server_isn,然后附着在SYNACK 报文中发送给客户端,接下来就是对ACK 报文进行判断,如果其返回的ack字段正好等于server_isn + 1,说明这是一个合法的ACK,那么服务器才会为其生成一个具有套接字的全开的连接。
SYN Cookie 防御
当然这种方案也有一定缺点,最明显的就是服务器不保存连接的半开状态,就丧失了重发SYN-ACK消息的能力,这一方面会降低正常用户的连接成功率,另一方面会导致某些情况下正常通信的双方会对连接是否成功打开产生误解,如客户端发给服务端的第三次握手消息(ACK)半路遗失,客户端认为连接成功了,服务端认为没收到ACK,连接没成功,这种情况就需要上层应用采取策略特别处理了。
如果第三次握手丢失了,客户端服务端会如何处理
服务器发送完 SYN-ACK 包,如果未收到客户端响应的确认包,也即第三次握手丢失。那么服务器就会进行首次重传,若等待一段时间仍未收到客户确认包,就进行第二次重传。如果重传次数超过系统规定的最大重传次数,则系统将该连接信息从半连接队列中删除。
注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s,2s,4s,8s…
2.两个TCP建立请求相互之间同时发起时会发生什么?建立几个连接?
首先理解题意,我们知道三次握手正是建立TCP连接的过程,我们假设 A 是客户端,B 是服务端:
rfc793-同时启动
这里先给大家讲一下上图中一些符号的含义:
- 右箭头 (–>) :从 A 发送到 B 的 TCP 报文段,且 B 接收到了;
- 左箭头 (<–) :从 B 发送到 A 的 TCP 报文段,且 A 接收到了;
- 省略号 (…) :TCP 报文段仍在网络中(delayed);
- 丢失 (“XXX”) :TCP 报文段丢失或者被拒绝。
- 注释会放在括号中;
- TCP 状态代表了处于中间的报文段到达之后的状态(AFTER);
- 报文段的内容只显示了序列号(SEQ)、控制符(CTL)和 ACK,其余内容被省略。
接下来我们详细来看看上图中,两个请求同时相互发起时,两个TCP均会经历如下状态的转换:
同步请求的状态转换
上述的状态转换图可以跟前面的三次握手的转换图进行对比理解,同时发起的两个请求最终只会建立一个连接。
3.客户端正在和服务端建立 TCP 连接,然而当服务器变 SYN-RCVD 后,此时一个旧的 SYN 报文 又到达了,服务器会如何处理?
其实这道题更加深挖了TCP 建立连接的过程,我们可以在rfc793中了解到详细信息。
从上图可以看到,第三行就是旧的SYN 连接到达服务器时,第四行是服务器照常返回,第五行是客户端给服务端发送RST 报文,将服务端重置为LISTEN。
我们需要从上图了解到的一点是,服务端在SYN_RECEIVED状态下,接收到旧的SYN 报文时是不能作出判断的,而是照常返回,当客户端接收到该报文后发现异常,才会发送RST 报文,重置连接。
确实说明了TCP B不能检测这个旧的SYN 报文是否正确,所以正常返回。而客户端收到会进行检测,发现是旧的报文,就会返回RST 报文。
4.第三次握手失败了怎么办?
这个问题在网上找到的答案质量参差不齐,翻阅了rfc793,仔细研究后,最终整理出以下答案:
ACK报文丢失导致第三次握手失败
当客户端收到服务端的SYNACK应答后,其状态变为ESTABLISHED,并会发送ACK包给服务端,准备发送数据了。如果此时ACK在网络中丢失(如上图所示),过了超时计时器后,那么服务端会重新发送SYNACK包,重传次数根据/proc/sys/net/ipv4/tcp_synack_retries来指定,默认是5次。如果重传指定次数到了后,仍然未收到ACK应答,那么一段时间后,Server自动关闭这个连接。
问题就在这里,客户端已经认为连接建立,而服务端则可能处在SYN-RCVD或者CLOSED,接下来我们需要考虑这两种情况下服务端的应答:
- 服务端处于CLOSED,当接收到连接已经关闭的请求时,服务端会返回RST 报文,客户端接收到后就会关闭连接,如果需要的话则会重连,那么那就是另一个三次握手了。
- 服务端处于SYN-RCVD,此时如果接收到正常的ACK 报文,那么很好,连接恢复,继续传输数据;如果接收到写入数据等请求呢?注意了,此时写入数据等请求也是带着ACK 报文的,实际上也能恢复连接,使服务器恢复到ESTABLISHED状态,继续传输数据。
这个结论也可以在STACKFLOW上找到验证:
What if a TCP handshake segment is lost?
上图圈住的部分:
总的来说,如果一个ACK 报文丢失了,但它的下一个数据包没有丢失,那么连接正常,否则,连接会被重置。
5.知道SYN攻击吗?如何防范?
所谓SYN 洪泛攻击,就是利用SYNACK 报文的时候,服务器会为客户端请求分配缓存,那么黑客(攻击者),就可以使用一批虚假的ip向服务器大量地发建立TCP 连接的请求,服务器为这些虚假ip分配了缓存后,处在SYN_RCVD状态,存放在半连接队列中;另外,服务器发送的请求又不可能得到回复(ip都是假的,能回复就有鬼了),只能不断地重发请求,直到达到设定的时间/次数后,才会关闭。
服务器不断为这些半开连接分配资源(但从未使用),导致服务器的连接资源被消耗殆尽,不过所幸,我们可以
四次挥手
和握手类似,每次挥手也代表一次报文的发出和接收。
过程描述
建立一个 TCP 连接需要三次握手,而终止一个 TCP 连接要经过四次挥手(也有将四次挥手叫做四次握手的)。这是由于 TCP 的半关闭(half-close)特性造成的,TCP 提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
TCP 连接的释放需要发送四个包(执行四个步骤),因此称为四次挥手(Four-way handshake),客户端或服务端均可主动发起挥手动作。
回顾一下上图中符号的意思:
- FIN :连接终止位
- seq:发送的第一个字节的序号
- ACK:确认报文段
- ack:确认号。希望收到的下一个数据的第一个字节的序号
刚开始双方都处于ESTABLISHED 状态,假设是客户端先发起关闭请求。四次挥手的过程如下:
1)第一次挥手:客户端发送一个 FIN 报文(请求连接终止:FIN = 1),报文中会指定一个序列号 seq = u。并停止再发送数据,主动关闭 TCP 连接。此时客户端处于 FIN_WAIT1 状态,等待服务端的确认。
FIN-WAIT-1 - 等待远程TCP的连接中断请求,或先前的连接中断请求的确认;
2)第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。
CLOSE-WAIT - 等待从本地用户发来的连接中断请求;
**此时的 TCP 处于半关闭状态,客户端到服务端的连接释放。**客户端收到服务端的确认后,进入FIN_WAIT2(终止等待 2)状态,等待服务端发出的连接释放报文段。
FIN-WAIT-2 - 从远程TCP等待连接中断请求;
3)第三次挥手:如果服务端也想断开连接了(没有要向客户端发出的数据),和客户端的第一次挥手一样,发送 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态,等待客户端的确认。
LAST-ACK - 等待原来发向远程TCP的连接中断请求的确认;
4)第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答(ack = w+1),且把服务端的序列值 +1 作为自己 ACK 报文的序号值(seq=u+1),此时客户端处于 TIME_WAIT (时间等待)状态。
TIME-WAIT - 等待足够的时间以确保远程TCP接收到连接中断请求的确认;
注意 !!!这个时候由服务端到客户端的 TCP 连接并未释放掉,需要经过时间等待计时器设置的时间 2MSL(一个报文的来回时间) 后才会进入 CLOSED 状态(这样做的目的是确保服务端收到自己的 ACK 报文。如果服务端在规定时间内没有收到客户端发来的 ACK 报文的话,服务端会重新发送 FIN 报文给客户端,客户端再次收到 FIN 报文之后,就知道之前的 ACK 报文丢失了,然后再次发送 ACK 报文给服务端)。服务端收到 ACK 报文之后,就关闭连接了,处于 CLOSED 状态。
为什么要四次挥手
由于 TCP 的**半关闭(half-close)**特性,TCP 提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。
通俗的来说,两次握手就可以释放一端到另一端的 TCP 连接,完全释放连接一共需要四次握手。
举个例子:A 和 B 打电话,通话即将结束后,A 说 “我没啥要说的了”,B 回答 “我知道了”,于是 A 向 B 的连接释放了。但是 B 可能还会有要说的话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道了”,于是 B 向 A 的连接释放了,这样整个通话就结束了。
2.为什么 TIME_WAIT 状态需要经过 2MSL 才能转换到 CLOSE 状态?
- 第一,为了保证客户端发送的最后一个ACK 报文能够到达服务器。我们必须假设网络是不可靠的,ACK 报文可能丢失。如果服务端发出FIN 报文后没有收到ACK 报文,就会重发FIN 报文,此时处于TIME-WAIT状态的客户端就会重发ACK 报文。当然,客户端也不能无限久的等待这个可能存在的FIN 报文,因为如果服务端正常接收到了ACK 报文后是不会再发FIN 报文的。因此,客户端需要设置一个计时器,那么等待多久最合适呢?所谓的MSL(Maximum Segment Lifetime)指一个报文在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL时间后,客户端都没有再次收到FIN 报文,那么客户端推断ACK 报文已经被服务器成功接收,所以结束TCP 连接。
- 第二,防止已失效的连接请求报文段出现在新的连接中。客户端在发送完最后一个ACK 报文后,再经过时间2MSL,就可以使由于网络不通畅产生的滞留报文段失效。这样下一个新的连接中就不会出现旧的连接请求报文。