什么是三次握手
三次握手是指,在建立TCP连接时,客户端和服务端总共会发送三个数据包。只有三个数据包都发送成功后,TCP连接才会建立成功。否则,丢失任何一个包,都会导致连接建立失败。发送三个数据包的过程,被人形象的称为三次握手。
三次握手过程
如图,是我找到的三次握手示意图:

通常,在socket编程中,服务端会绑定端口,然后调用listen接口,挂起等待。而客户端会绑定服务端的IP和端口,然后调用connect接口,发起连接。连接成功后,双方便会相互发送消息。其实,这个过程就是TCP建立连接的过程,也就是三次握手过程。只是底层原理和过程全部封装在内核层面上,用户编程时无需感知和亲自操作。
那现在就详细说下这个建立连接的过程。首先第一次握手时,客户端会发送一个报文,其中标志位SYN=1,其余为0。表示这是一个请求建立连接的报文。同时客户端还会生成一个随机数x,当作序列号,即seq=x,也称之为初始序列号。后续的序号确认、超时重传、重复丢弃等等,都会基于这个初始序列号去做。所以这个初始序列号很关键。前面我们知道,序列号seq占4个字节,所以x的范围也就是[0,4294967295]。
服务端收到这个报文后,通过SYN=1便可以知道,这是一个请求建立连接的报文。如果此时,服务端已经准备好,同意建立连接。那么它便会回复一个确认报文,即ACK=1,ack=x+1。注意,普通报文中ACK为0,确认报文中ACK一定为1。那ack为啥是x+1呢?
首先我们要明白,大写的ACK是标志位,小写的ack是确认号。确认号就是用来回复对方的,告诉对方,我接收了多少数据,下次你应该从哪里发送(参考上一章节)。那为啥是x+1,而不是x+100或+1000呢?这里就要说下ack的计算逻辑了。如果发送方发送的序号是x,携带数据长度是n。如果接收方全部接收成功,则返回的ack就是x+n。这样我们只需要知道建立连接时,携带数据长度是多少就行了。答案是,三次握手中,三个包均没有携带任何数据,即数据长度为0。那为啥不是x+0呢?答案是,**TCP协议约定,SYN=1的报文段不能携带数据,但要消耗掉一个序号。FIN报文同样如此,即使不携带数据,也要消耗一个序号。**所以,确认号就是x+1,即ack=x+1。
tcp是全双工的,就是双方都能收发数据。那客户端到服务端的连接通了后,服务端肯定也要向客户端发起请求,建立连接。过程和客户端发起时一模一样,就是发送一个报文,其中SYN=1,序号seq=y。聪明的你,立马会想到,到这里,服务端已经发送了两个报文,一个是确认报文ACK=1,一个是请求建立连接报文SYN=1。这和上面只发送一条报文,显然是不一样的。为什么呢?答案是,TCP协议做了优化,将两条报文合并成一个报文了。就是服务端只发送一条报文:SYN=1,SCK=1,seq=y,ack=x+1,既表示确认报文,又表示请求建立连接报文。这在TCP中被称为"捎带应答"。这个全部过程也被称为二次握手。(这里,也说明为啥建立连接时候,最少需要3次握手,其实4次更标准,但效率低,没必要。)
客户端收到服务端的SYN=1报文后,立即回复一个确认报文:ACK=1,seq=x+1,ack=y+1。此时的确认号ack的计算逻辑和上面一样,所以是y+1。这个过程被称为第三次握手。
下面,我同时用抓包的报文数据来验证这个过程。图如下:

前三行,就是三次握手中的三个包。第一次握手,客户端->服务端:SYN=1,seq=0。第二次握手,服务端->客户端:SYN=1,ACK=1,seq=0,ack=1。第三次握手,客户端->服务端:ACK=1,seq=1,ack=1。注意,此处的两个初始序列号都是0,这里的0是相对值,只是展示方便。实际传输的都是真实值,一般比较大。后面讲解抓包的报文时,再详细说明。
三次握手结束后,连接正式建立成功,双方便可以相互发送数据了。
三次握手中同步哪些信息
上面示意图简化了建立连接的过程。从上面我们知道,建立TCP连接时候,双方会互相同步自己的初始序列号,后续的确认号都是基于这个而来。那除了这个外,还同步哪些信息呢?从上面抓包的报文中还可以看到,有win,MSS,SACK_PERM,以及时间戳等。
其中,win表示窗口大小,后面每个报文中也都会携带上,用来告知对方,我的剩余缓冲区空间还有多大,你下次还能发多少。这个不是只在建立连接时候才有,每次收发报文都会带上,故先忽略。
而MSS和SACK这两个,是明确只在建立连接时候才有,其余报文中不会携带。上一章节说过,MSS的出现是为了解决TCP包过大而导致被分片。而MSS又依赖网卡的MTU,MTU越大,单包携带的有效数据便越多。那如果双方机器性能不一样,怎么办呢?举例,如果客户端MTU=2000,服务端MTU=1500。TCP/IP包头都是按最小情况算(下同),在不交换信息情况下,客户端的MSS最大便可以达到2000-20-20=1960,而服务端的MSS最大是1500-20-20=1460。
如果此时客户端发送TCP包携带数据长度是1800字节(不超过1960,正常数据包),则到达网络层后,IP包大小为1800+20+20=1920字节。虽然1920<2000,在客户端网络上不会被分片,但到达服务端时,还是会因为超过了1500,在服务端上被分片。依旧会造成丢包等问题。
所以TCP建立连接时,需要协商一个双方都支持的最小MSS,也就是取客户端和服务端两个中最小的那个MSS。这也是为啥必须在建立连接时候同步MSS的原因。MSS一旦确定后,不在改变,所有后续包不会再携带这个信息。
同理,SACK也是这个原因,才会出现在首次建立连接中。只有双方都支持SACK才行。仔细点会发现,连接建立时候发送的是SACK_PERM,全称是SACK-Permitted。表示是否允许开启SACK这个选项功能,不允许就算了,允许后,后续报文头的选项中,可能会出现SACK的内容。就是批量发送报文中,哪些区间丢失了,在SACK中体现。后面只需要针对性重传这些丢失的区间就行,不用全部都传输,提高了效率。
所以,让我们总结下,三次握手时会同步哪些信息:初始序列号、MSS、SACK、时间戳等。
为什么不能两次握手
首先,从理论上分析,为啥不能两次握手就可以建立连接。
TCP传输是可靠的,是全双工的。双方都需要知道自己的发送和接收是否正常。如果是两次握手,即A发送信息给B,B接收到后返回消息给A。到这里,A是能够确认自己的发送和接收时没问题的,但B只能确认自己的接收没有问题,无法确认自己的发送是否正常。只有A给B回消息后,B收到了,才能确认自己的发送也没问题。所以,从理论上讲,两次握手做不到,只有三次握手才能确认双方是否都正常。