著名的三次握手将在这里登场。
1. 建立连接
先来看看图1,客户端和服务器是如何建立起连接的。
图1 三次握手
图2,是我们实际抓取的数据:
图2 请看红色框框中的三次握手
(1) 客户端发送一个 SYN 段(SYN 标志位置位),以及初始序号 ISN,在图 2 中,这个序号的值 seq = 2379453243. 在这个过程中,客户端是通过 connect 函数发起连接请求的,此时 connect 函数阻塞,等待服务器发回 ACK 应答。
(2) 服务器端接收到 SYN 段后(被动打开,只有设置了被动 socket 才有效),知道有新的连接请求到来,于是初始化一个序号 ISN,在上面的例子中这个值是 seq = 4269857. 此时服务器创建一个 TCP 段,将 SYN 和 ACK 标志置位,让 seq = 4269857, ack = 2379453244,然后将这个 TCP 段发送给客户端。这个步骤完全有内核完成,并将此连接加入未完成连接队列。
(3) 客户端再次收到服务器发送来的 TCP 段后,检查到带有 SYN 和 ACK 标志,于是客户端一方连接已经建立成功,此时 connect 函数返回。客户端创建一个 TCP 段,将 ACK 标志置位,同时将 ack 的值设置为 4269858,发送给服务器。
(4) 服务器收到客户端的 ACK 后,将此连接移到已完成连接队列。
在上面的描述中提到了未完成队列和已完成队列,这两个队列合在一起称为未决连接队列。下面具体解释一下。
2. 未决连接队列
在内核中,内核为任何一个给定的监听套接字(被动 socket)维护了一个未决连接队列,该队列分为两个部分,分别是未完成连接队列和已完成连接队列。
未完成连接队列(incomplete connection queue),服务器只要收到了 SYN 段,就将该连接加入未完成连接队列。
已完成连接队列(complete connection queue),服务器收到了客户端对 SYN 段的确认,将未完成连接队列中的连接移到已完成连接队列。
图3 未决连接队列
这个两个队列元素个数之和不超过 backlog,这是函数 listen 的参数。
3. 为什么要三次握手
三次握手是有效的
先解释建立连接的一个基础条件:假设 A 和 B 两端要建立连接,A 给 B 发送 SYN,直到 A 确定下来 B 已经收到。这种方法前面已经用发短信的例子介绍过了,即 ACK + 超时重传的机制。
同样的,B 也可以利用 ACK + 超时重传机制给 A 发送 SYN 段。
这样双方都可以保证对方收到了自己的 SYN 段。写成步骤就是下面这样:
(1) A —— SYN ——> B
(2) B —— ACK ——> A
(3) B —— SYN ——> A
(4) A —— ACK ——> B
聪明的同学一眼就可以看出来步骤 2 和 3 可以合并成一个。因此,三次握手是有效的,正确的。
为什么要三次握手
从两方面解释:
(1) 防止某个在网络中滞留的 SYN 段突然又被传送到服务器端
假设某上在网络中滞留的 SYN 突然被传送到服务器端,服务器收到此 SYN,必然认为是有客户发起了请求,于是回复 SYN + ACK. 这对应于前面讲的步骤 2 和 3,接下来服务器就需要等待对端回复的 ACK,对应于步骤 4.
实际上,根本就不存在这样的一个客户端,因为 SYN 段只是一个滞留数据段,客户端早已不存在,因此服务器永远等不到对端的 ACK 回复,经过一次次数的超时重传,服务器最终会放弃这个连接。
显然,这种滞留的 SYN 会极大的浪费服务器资源。SYN 泛洪攻击服务器就是利用的这个原理。具体同学们可以 Baidu or Google.
(2) 三次握手的目的
有些同学会有疑问,即使没有三次握手,就不能利用 ACK + 超时重传发送数据了吗,也就是说使用 UDP 协议 + ACK 能不能完成 TCP 的功能?
不妨这样看,你按顺序发了两个 UDP 数据报,假设为数据报 1 和 2,而对端先收到数据报 2,其次才收到数据报 1. 这时候,对端凭什么才能知道哪个数据报应该在前,哪个在后呢?在你不解释的情况下,对端就认为数据报 2 排在前面,数据报 1 排在后面。
为了解决这个问题,你不得不在你的 UDP 报文里加入编号,对端收到了这个报文,就可以自己根据报文编号排序了。好了,这个问题也解决了,还有一个问题,如果才能告诉对方,哪个 udp 报文是第一个报文?
假设你连续发送了 1 、2、3、4、5 号报文,对方收到了3、4、5,还有 1 和 2 对方暂时还没收到,你说,对端能不能把 3、4、5 交付给应用程序,虽然 3、4、5 的顺序没错?显然不行,你还得告诉对方哪个报文是第一个报文,是不是?然后你又加了一个标记在报文 1 里,当对方收到报文 1 了,就知道 1 号报文是第一个报文了,于是就按照顺序从 1 号报文开始,把数据交付给上层应用程序了。
上面的过程,也就是确定初始报文序号的过程,可以单独提炼出来。为了这样就简化了程序的设计,不妨把确定初始报文序号的过程在一开始就确定下来,只要没确定下来,就不发数据。对应到三次握手,本质上就是确定 ISN,即初始序号。
因此,三次握手的目的,即建立 TCP 连接的目的,是为了确认双方的初始序号 —— ISN.
4. 总结
掌握三次握手的过程
知道未决连接队列