你好,我是胜辉。

在前面预习篇的两节课里,我们一起回顾和学习了网络分层模型与排查工具,也初步学习了一下抓包分析技术。相信现在的你,已经比刚开始的时候多了不少底气了。那么从今天开始,我们就要正式进入TCP这本大部头,而首先要攻破的,就是握手和挥手。

TCP的三次握手非常有名,我们工作中也时常能用到,所以这块知识的实用性是很强的。更不用说,技术面试里面,无论是什么岗位,似乎只要是技术岗,都可能会问到TCP握手。可见,它跟操作系统基础、编程基础等类似,同属于计算机技术的底座之一。

握手,说简单也简单,不就是三次握手嘛。说复杂也复杂,别看只是三次握手,中间还是有不少学问的,有些看似复杂的问题,也能用握手的技术来解决。不信你就跟我看这几个案例。

TCP连接都是用TCP协议沟通的吗?

看到这个小标题,可能你都觉得奇怪了:TCP连接不用TCP协议沟通还用什么呢?

确实,一般来说TCP连接是标准的TCP三次握手完成的:

  1. 客户端发送SYN;
  2. 服务端收到SYN后,回复SYN+ACK;
  3. 客户端收到SYN+ACK后,回复ACK。

这里面SYN会在两端各发送一次,表示“我准备好了,可以开始连接了”。ACK也是两端各发送了一次,表示“我知道你准备好了,我们开始通信吧”。

那既然是4个报文,为什么是三次发送呢?显然,服务端的SYN和ACK是合并在一起发送的,就节省了一次发送。这个在英文里叫Piggybacking,就是背着走,搭顺风车的意思。

如果服务端不想接受这次握手,它会怎么做呢?可能会出现这么几种情况:

  1. 不搭理这次连接,就当什么都没收到,什么都没发生。这种行为,也可以说是“装聋作哑”。
  2. 给予回复,明确拒绝。相当于有人伸手过来想握手,你一巴掌拍掉,真的是非常刚了。

第一种情况,因为服务端做了“静默丢包”,也就是虽然收到了SYN,但是它直接丢弃了,也不给客户端回复任何消息。这也导致了一个问题,就是客户端无法分清楚这个SYN到底是下面哪种情况:

  1. 在网络上丢失了,服务端收不到,自然不会有回复;
  2. 对端收到了但没回,就是刚才说的“静默丢包”;
  3. 对端收到了也回了,但这个回包在网络中丢了。

你看,就这么简单的一个SYN,还能引申出三种状况出来。感觉什么东西一沾上网络,就要变成麻烦事啊。所以,跟我们在第1讲里学过的一样:设计网络协议真的不简单。

那么,从客户端的角度,对于SYN包发出去之后迟迟没有回应的情况,它的策略是做重试,而且不止一次。那会重试几次呢?重试多久呢?这个问题,一下子还不太好回答。不过,有tcpdump帮忙,我们可以搞清楚重试的问题,也可以搞清楚“TCP连接是否都用TCP协议沟通”的问题。

动手实验

你可以借助Iptables和tcpdump做个实验,来验证这件事。你需要一台测试用的服务端,安装Ubuntu等Linux类系统,然后用你的笔记本作为客户端发起测试。这里我也放了一个视频,展示了这个实验过程,你可以结合着对照来看。

注意:在这个视频中,我是直接在tcpdump窗口里解读抓包结果的,而在下面我们是用Wireshark来解读,思路其实是一样的,只是操作方式略有不同,正好你可以都学习一下。

第一步,在服务端,执行下面的这条命令,让Iptables静默丢弃掉发往自己80端口的数据包:

Iptables -I INPUT -p tcp --dport 80 -j DROP

第二步,在客户端启动tcpdump抓包:

sudo tcpdump -i any -w telnet-80.pcap port 80

第三步,从客户端发起一次telnet:

telnet 服务端IP 80

这个时候,这个telnet会挂起:

大约一两分钟后才会失败退出,你随后就会明白背后发生了什么。

这时,你可以把客户端的tcpdump停掉了(按下Ctrl+C)。然后用Wireshark打开这个抓包文件,看看里面是什么:

telnet挂起的原因就在这里:握手请求一直没成功。客户端一共有7个SYN包发出,或者说,除了第一次SYN,后续还有6次重试。客户端当然也不是“傻子”,这么多次都失败,就放弃了连接尝试,把失败的消息传递给了用户空间程序,然后就是telnet退出。

这里有个信息很值得我们关注。第二列是数据包之间的时间间隔,也就是1秒,2秒,4.2秒,8.2秒,16.1秒,33秒,每个间隔是上一个的两倍左右。到第6次重试失败后,客户端就彻底放弃了。

显然,这里的翻倍时间,就是“指数退避”(Exponential backoff)原则的体现。这里的时间不是精确的整秒,因为指数退避原则本身就不建议在精确的整秒做重试,最好是有所浮动,这样可以让重试成功的机会变得更大一些。

这里实际上也是一个知识点了:TCP握手没响应的话,操作系统会做重试。在Linux中,这个设置是由内核参数net.ipv4.tcp_syn_retries控制的,默认值为6,也就是我们前面刚观察到的现象。以下就是我的Ubuntu 20.04测试机的配置:

$ sudo sysctl net.ipv4.tcp_syn_retries
net.ipv4.tcp_syn_retries = 6

还有另外好几个有关TCP重试的设置值,也都可以调整。更全面的内容呢,你可以直接man tcp,查看tcp的内核手册的信息。比如下面就是对于tcp_syn_retries的解释:

tcp_syn_retries (integer; default: 5; since Linux 2.2)
       The  maximum  number of times initial SYNs for an active TCP connection attempt will be retransmitted.  This value should not be higher than 255.  The default value is 5, which corresponds to approximately 180 seconds.

既然静默丢包会引起客户端空等待的问题,那我们直接拒绝,应该就能解决这个问题了吧?

正好,Iptables的规则动作有好几种,前面我们用DROP,那这次我们用REJECT,这应该能让客户端立刻退出了。执行下面的这条命令,让Iptables拒绝发到80端口的数据包:

Iptables -I INPUT -p tcp --dport 80 -j REJECT

跟前面的实验一样,我们在客户端发起telnet 服务端IP 80。果然,telnet立刻退出,显示:

$ telnet 47.94.129.219 80
Trying 47.94.129.219...
telnet: connect to address 47.94.129.219: Connection refused
telnet: Unable to connect to remote host

可见,连接请求确实被拒绝了。我在telnet同时也抓了包,我们来看一下抓包文件:

奇怪,抓包文件里并没有期望的TCP RST?是我们抓包命令没写对吗?下面是这条命令,你已经初步学过tcpdump抓包命令了,看看有没有什么问题?

sudo tcpdump -i any -w telnet-80-reject.pcap host 47.94.129.219 and port 80

命令语法没问题,要不然命令都无法执行。那过滤条件呢?指定了远端IP和端口,这是很常见的用法,应该也没什么问题。

但是,这里隐藏了一个假设的前提,也就是我们认为,这次握手的所有过程都是通过这个80端口进行的。但事实上呢?我们稍微改一下抓包条件,只保留远端IP,去掉端口的限制:

sudo tcpdump -i any -w telnet-80-reject.pcap host 47.94.129.219

然后再来看看,我们抓到的报文是怎样的:

很意外,居然对端回复了一个ICMP消息:Destination unreachable (Port unreachable)。这还不是最意外的,我们选中这个报文,进一步看它的详情,可能会更惊讶:

原来,这个ICMP消息不仅通过type=3表示,这是一个“端口不可达”的错误消息,而且在它的payload里面,还携带了完整的TCP握手包的信息,而这个握手包,可是客户端发过来的。

补充一下:如果我们回头再检查一下前面生成的Iptables规则,它是这样的:

-A INPUT -p tcp -m tcp --dport 80 -j REJECT --reject-with icmp-port-unreachable

原来,它自动补上了–reject-with icmp-port-unreachable,也就是说确实用ICMP消息做了回复。当然,你还可以把这个动作定义为–reject-with tcp-reset,那样的话就符合我们一开始的期望了。
 
事实上,无论是收到TCP RST还是ICMP port unreachable消息,客户端的connect()调用都是返回ECONNREFUSED,这就是telnet都报“connection refused”的深层次原因。

所以,这个握手失败的情况终于搞清楚了,它是这么发生的:

TCP握手拒绝这个事,竟然可以是ICMP报文来达成的。“握手过程用TCP协议做沟通”,看起来这么理所当然的事情居然也会反转,你是不是也有点自我怀疑了:是不是其他网络知识,也未必是我自己认为的那样呢?

这个知识点,其实是几年前我在处理一个客户的TCP连接问题时遇到的。剧情么,前面已经给你“演”过一遍了。当时我也深感TCP的水太深,快没过脖子了,甚至有点喘不过气来……从此以后,我再也不敢小看任何知识点,同时也领教了tcpdump和Wireshark在网络分析方面的威力。有了这两个大杀器的帮助,我的网络水平提高很快。这个经验我也分享给你,相信你也一定能从中受益。

Windows服务器加域报RPC service unavailable?

虽然tcpdump + Wireshark的组合威力强大,但用起来总是会稍微花点时间。有没有不用抓包分析,也能做排查TCP连接问题的方法呢?这样也好快一点啊。接下来这个例子,就是这样的。

我们eBay也有不少Windows服务器,这些机器都由Active Directory(简称AD)管理。有一次,我们有一台Windows服务器加入AD失败,相关同事已经排查了好久,一直没找到原因。操作过程就是最普通的加域动作:

然后,一开始显示加域成功,但是过一两分钟后,又会来个“回马枪”,冒出来一个The RPC server is unavailable的报错:

在Windows的体系里面,这个报错大体意思是连不上RPC服务器。同事检查过RPC服务端并没有问题,然后其他Windows客户端加域呢,也都正常,唯独这台就不行。

单独一台机器加不了域,本身也不是特别大的麻烦,但是同事还是想找一下根因,于是就让我帮忙。很幸运,当时我只用了大概十分钟就找到了原因(这里我有点不谦虚了,我对你扔过来的鸡蛋和番茄表示接受)。

这倒不是我对Windows多么精通,主要是正确的排查思路帮助了我。给你分享一下我当时的思路:

  1. 既然报错是RPC unavailable,那可能意味着有一个RPC服务没有得到响应。
  2. 没有得到服务端的响应,那多半是跟网络有关系,特别是跟端口的连通性有关系。
  3. 要知道,RPC使用的是动态端口,每次连接都可能连接到不同的服务端口。所以,我也没办法预先知道是具体哪几个端口,如果我知道的话,直接找防火墙团队去把那几个服务端口打开就好了,但这个做不到。这一点也是同事卡了许久的原因之一,他也不知道如何找到这些“动态会变的RPC端口”。
  4. 要找到实时在用的动态RPC端口,最方便的方法就是运行netstat命令。无论连接是处在什么状态,比如是在传输数据的ESTABLISHED状态、新近关闭端口的TIME_WAIT状态,都可以用netstat命令看到。
  5. 我运行了netstat,在当时的命令输出中,我注意到有一个 SYN_SENT状态的连接,它要连的就是服务端的一个高端口。

那么,这个SYN_SENT状态究竟说明了什么呢?

SYN_SENT是TCP的11个状态之一。要理解SYN_SENT的含义,我们首先要把整个TCP状态机的机制搞清楚。关于TCP状态机,目前流传比较广的是下面这张图。我没有考证过这张图的出处,不过在Stevens的《UNIX网络编程:套接字联网API》里就有这张图,很有可能最早就是来自于Stevens:

这张图浓缩了TCP状态转换的所有知识点,确实值得反复研读。不过,我鸡蛋里挑个骨头:这张图也有个小小的问题,就是对于初学者来说,它并不容易理解。

比如,多年前我自己在学习TCP的时候,就一直没有彻底看懂这张图。好笑的是,我经常假装自己看懂了,还拿这张图跟别人侃侃而谈,而对方还被我唬住了呢。所以你也要学会了:当大家都不是很懂的时候,你对自己的话越相信,你就越有说服力哦。

好了,当然是跟你开个玩笑,做学问还是要严谨。那么,这张图的难点在哪呢?我觉得主要是视角不固定,一会是发送方,一会是接收方,对初学者来说很容易混淆。实际上,在Stevens的这本书中,还有另外一张图,我认为更加清晰明了,也是我想推荐给你的:

在上面这张图里,无论是客户端或者服务端,我们从上往下看,它要经历的各个TCP状态,都展示得十分清楚。我把这个过程解读如下:

后续的过程,不用我继续解读,你也会看得很清楚了:分别沿着左边和右边的垂直线从上往下看,就经历了客户端和服务端的TCP生命周期里的各种状态,这个过程中,视角保持一致。你觉得是否比前面那张转换图,更加容易理解呢?

看懂了这张图,你应该就明白了:SYN_SENT这个状态,意味着当时这个连接请求(SYN包),已经从这台Windows服务器发出,试图跟远端的AD域控制器进行连接。但由于对端迟迟没有回应SYN+ACK报文,那么客户端这个连接的状态,就只能“停留”在SYN_SENT状态,无法转化为ESTABLISHED状态。

等到达了SYN timeout时间后,Windows操作系统会放弃这次连接,而这个SYN_SENT状态的连接也会消失不见。所以,前面提到的“实时”两字,也是很关键的。如果不是在问题发生时运行netstat,哪怕是过了几分钟再去运行netstat,错过了这个SYN_SENT,我也不能发现这个失败的TCP连接企图,也就无法定位到真正的原因了。

然后我们拿着这个端口去找防火墙团队,对方检查了配置,发现这个端口确实是禁止的。在开通后,问题就解决了。

所以说,真的不要小看任何知识点和小工具,你掌握以后,完全可以起到关键性的作用(对了,排查防火墙也时常是我们工作的痛点,我在第5和第6讲会专门讲解这方面的排查技巧,敬请期待)。

这里还有一个技术点我想给你展开一下。我们在前面已经讨论过了SYN重试的问题,显然,这次Windows的SYN_SENT的背后,我们相信,应该也是有数次的SYN重试的情况。同时,因为我观察到,这个SYN_SENT停留了大约有十几二十秒,所以我判断应该也有指数退避的存在,所以这个状态才保留了那么长时间。

也就是说,无论是Linux还是Windows,都实现了类似的TCP握手方面的容错手段。还是那句话:设计网络不容易。理解了设计者的初心,很多问题就不会那么模糊了,可能你一下子就能看清。

发送的数据还能超过接收窗口?

最后一个案例表面上并不直接跟握手相关,但背地里就……不剧透了,看剧情。

前段时间,有个朋友找到我咨询一个问题。他们最近处理了一个Redis相关的技术问题,让他们既开心又“闹心”。开心的是整体分析是正确的,问题也得以解决;“闹心”的是,唯独有个技术点好像无法自圆其说,所以想让我看看到底是怎么回事。

这个问题是:Redis服务告诉客户端它的接收窗口是190字节,但是客户端居然会发送308字节,大大超出了接收窗口。下图是他们用Wireshark打开抓包文件后的界面:

我一开始也懵了:难道TCP的深水又到我脖子这儿了?在我多年的抓包分析经历中,数据超过接收窗口的情况,好像还没有遇到过,这次算是TCP准备再次让我“开开眼”吗?

不过我很快又稳定了下来,因为我想到了一个朋友他们没有注意到的细节。在说到TCP窗口的时候,一般都会提到一个很重要的概念:Window Scale。这是因为,TCP最初是七八十年代的产物,1981年9月定稿的RFC793才第一次正式确定了TCP的标准。当时的网络带宽还处于“石器时代”,机器的带宽只有现在的百分之一,那么TCP接收窗口自然也没必要很大,2个字节长度代表的65535字节的窗口足矣。

但是后来网络带宽越来越大,65535字节的窗口慢慢就不够用了,于是设计者们又想出了一个巧妙的办法。原先的Window字段还是保持不变,在TCP扩展部分也就是TCP Options里面,增加一个Window Scale的字段,它表示原始Window值的左移位数,最高可以左移14位。

如果你还没有完全忘记计算机课的基本知识,那么应该明白这是一个非常大的提升了(扩大了2的14次方,即16384倍)。16384乘以65535,这个数字就是1G字节,也就是说,一个启用了Window Scale特性的TCP连接,最大的接收窗口可以达到1GB。可以说,这个数字至今都是够用的。

说了这么多,我们用Wireshark来看看它究竟长啥样。选中一个SYN报文,在Wireshark窗口中部找到TCP的部分,展开Options就能看到了:

我们逐一理解下。

小小提醒:SYN包里的Window是不会被Scale放大的,只有握手后的报文才会。

当然,TCP的窗口也是TCP知识体系里一块挺大的分支领域,我会在当前这个“实战一”模块的传输效率部分,也就是第9~11讲里,详细讲解这方面的知识,帮你把这块的东西真正搞透。

回到握手。既然Window Scale这么有用,那每个TCP报文应该都是带上这个信息的吧,因为它在TCP头部里面嘛,而每个TCP报文都有头部的,不是吗?

你要这样想就错了。事实上,Window Scale只出现在TCP握手里面。你再想想就明白了:这个是“控制面”的信息,说一次让双方明白就够了,每次都说,不光显得“话痨”,也很浪费带宽啊。一般传输过程中的报文,完全不需要再浪费这3个字节来传送一个已经同步过的信息。所以,握手之后的TCP报文里面,是不带Window Scale的。

比如,我们来看一个传输中的报文,比如客户端发送的TLS Client Hello报文:

可见,原始窗口502字节,放大128倍后就是64256字节了。

说到这里,想必你已经明白了:我朋友这次的疑惑,其实就是缺少TCP握手包造成的。要知道,Wireshark也一样要依赖握手包,才能了解到这次连接用的Window Scale值,然后才好在原始Window值的基础上,对Window值进行右移(放大),得出真正的窗口值。于是,因为这次他们的抓包没有抓取到握手报文,所以Wireshark里看到的窗口,就是190字节,而不是190字节的某个倍数了!

当时通信的另一端当然知道这个信息,所以它发送308字节一点都不意外,因为这个值根本就没超出接收窗口。

那么,是不是没有抓取到握手包的话,Wireshark里读取到的Window就一定不对呢?大部分时候是这样的。不过,还有一部分老系统的TCP栈并没有启用Window Scale,那么抓包文件中有没有握手包都没关系,只要看基本Window就好了。

说到这里,你对TCP握手的印象,是不是又有改变呢?它简单,也丰富;它靠谱,也调皮。你只有真的读懂它,才不会被它牵着鼻子走。而读懂它的方法是什么呢?

就是多读些TCP理论,就是多做些抓包分析,就是多处理些案例,更是多走走,多看看。只要有心,你总有机会可以学会,可以成长。

小结

作为这个模块的第一课,这次我们围绕TCP握手展开了几个有趣的案例,并从中梳理了以下知识点:

  1. 客户端发起的连接请求可能因为各种原因没有回复,这时客户端会做重试。一般在Linux里,重试次数默认是6次,内核参数是net.ipv4.tcp_syn_retries。重试间隔遵循了指数退避原则
  2. 服务端拒绝TCP握手,除了用TCP RST,另外一种方式是通过ICMP Destination unreachable(Port unreachable)消息。从客户端应用程序看,这两种回复都属于“对端拒绝”,所以应用表面看不出区别,但我们在抓包的时候要注意,如果单纯抓取服务端口的报文,就会漏过这个ICMP消息,可能对排查不利。
  3. 对于连通性相关的问题,除了用tcpdump+Wireshark这个黄金组合,我们还可以在理解TCP握手原理的基础上,使用小工具(比如netstat)来排查。特别是对于RPC服务场景,在问题发生时及时执行netstat -ant,找到SYN_SENT状态的连接,这个很可能是突破口。
  4. 我们也学习了如何在Wireshark中查看Window Scale。握手包中的Window Scale信息十分重要,这会帮助我们知道正确的接收窗口。在分析抓包文件时,要注意是否连接的握手包被抓取到,没有握手包,这个Window值一般就不准。

可以说,应用都靠连接,连接都靠握手。掌握好了握手,你的TCP就算入门了。学完这节课之后,你有没有觉得,今天的你比昨天的你,要强一些了呢?加油!后面更多的知识在等你来发现。

思考题

最后,还是按照惯例,还是给你留几道思考题:

  1. 在Linux中,还有一个内核参数也是关于握手的,net.ipv4.tcp_synack_retries。你知道这个参数是用来做什么的吗?
  2. 如果握手双方,一方支持Window Scale,一方不支持,那么在这个连接里,Window Scale最终会被启用吗?你可以参考RFC1323,给出你的解答。

欢迎在留言区分享你的答案,如果觉得有收获,也欢迎你把今天的内容分享给更多的朋友。

扩展知识:聊聊几个常见误区

很多时候,我们的成长不仅是由于学到了正确的知识,更是由于纠正了“错误的认知”。下面列几个常见误区,你看看自己有没有“中招”。

UDP也有握手?

有些同学会有这个误解,可能是跟nc这个命令有关。我们来看一个TCP端口22的测试:

victor@victorebpf:~$ nc -v -w 2 47.94.129.219 22
Connection to 47.94.129.219 22 port [tcp/ssh] succeeded!

同一时间的tcpdump抓包,显示这个TCP经历了成功的握手和挥手:

$ sudo tcpdump -i any host 47.94.129.219
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked v1), capture size 262144 bytes
11:58:10.749372 IP victorebpf.51072 > 47.94.129.219.ssh: Flags [S], seq 966857509, win 64240, options [mss 1460,sackOK,TS val 1565897461 ecr 0,nop,wscale 7], length 0
11:58:10.781734 IP 47.94.129.219.ssh > victorebpf.51072: Flags [S.], seq 3170176001, ack 966857510, win 65535, options [mss 1460], length 0
11:58:10.781880 IP victorebpf.51072 > 47.94.129.219.ssh: Flags [.], ack 1, win 64240, length 0
11:58:10.782344 IP victorebpf.51072 > 47.94.129.219.ssh: Flags [F.], seq 1, ack 1, win 64240, length 0
11:58:10.782586 IP 47.94.129.219.ssh > victorebpf.51072: Flags [.], ack 2, win 65535, length 0
11:58:10.821202 IP 47.94.129.219.ssh > victorebpf.51072: Flags [P.], seq 1:42, ack 2, win 65535, length 41
11:58:10.821292 IP victorebpf.51072 > 47.94.129.219.ssh: Flags [R], seq 966857511, win 0, length 0

如果我们用nc测试 UDP 22端口,看看会发生什么。注意,UDP 22是没有服务在监听的。但是nc一样告诉我们succeeded!这似乎在告诉我们,这个UDP 22端口确实是在监听的:

$ nc -v -w 2 47.94.129.219 22
Connection to 47.94.129.219 22 port [tcp/ssh] succeeded!
victor@victorebpf:~$ nc -v -w 2 47.94.129.219 -u 22
Connection to 47.94.129.219 22 port [udp/*] succeeded!

同一时间的抓包,显示客户端发送了4个UDP报文,但服务端没有任何回复:

11:59:05.605556 IP victorebpf.54145 > 47.94.129.219.22: UDP, length 1
11:59:05.605995 IP victorebpf.54145 > 47.94.129.219.22: UDP, length 1
11:59:06.606436 IP victorebpf.54145 > 47.94.129.219.22: UDP, length 1
11:59:07.607134 IP victorebpf.54145 > 47.94.129.219.22: UDP, length 1

从表象上看,nc告诉我们:这个跟UDP 22端口的“连接”是成功的,这是nc的Bug吗?可能并不算是。原因就在于,UDP本身不是面向连接的,所以没有一个确定的UDP协议层面的“答复”。这种答复,需要由调用UDP的应用程序自己去实现。

那为什么在这里,nc还是要告诉我们成功呢?可能只是因为对端没有回复ICMP port unreachable。nc的逻辑是:

所以,当你下次用nc探测UDP端口,不通的结果是可信的,而能通(succeeded)的结果并不准确,只能作为参考。

一台机器最多65535个TCP连接?

这也是很常见的误区了。我还是小白的时候,也曾经深信不疑。当时读到一篇讨论服务器可以承受多少TCP连接(就是C10k问题)的文章时,还觉得奇怪,不是端口范围只有0~65535吗?为什么还会有几十万上百万连接呢?

这就是没有意识到,连接是四元组(咱们在第一节课讲到过),并不是单纯的源端口或者目的端口。那么多个数相乘,这个乘积当然可以远远超过65535了。先不谈论海量级网站的场景,就算我们维护一台Web服务器,假如当前有10万台客户端连着你,平均每个客户端跟你有6个连接(这很常见),那么就是60万个连接了,是不是也早就超过6万了?

当然,在限定场景下,一个客户端(假设只有一个出口IP)和一个服务端(假设也只有一个IP和一个服务端口),那么确实只能最多发起6万多个连接。但你自己也已经明白,这跟前面的误解,已经是两回事了。

不能同时发起握手?

如果两端同时发送了SYN给对方,也就是双方都收到了一个SYN,那么接下来,它们会进入什么状态呢?你可能觉得这应该不行。

其实,通信双方还真的可以同时向对方发送SYN,也能建立起连接。你可以参考这节课里我提到的TCP状态转换图。在Richard Stevens的《TCP/IP详解(第一卷)》里,也提到了这个知识点,参考下图:

当然,这种情况是很罕见的,你可以参考一下,也丰富一下你对TCP握手的理解。