本文以一次 wireshark 抓包经历,理解TCP的三次握手和四次挥手
(1)序号(sequence number):Seq序号,占32位(bit),4个字节(byte), 用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。
(2)确认号(acknowledgement number):Ack序号,4个字节(byte), 只有ACK标志位为1时,确认序号字段才有效,Ack=Seq+1。
(3)标志位(Flags):8位(bit), 1个字节(byte), 常见6个,即URG、ACK、PSH、RST、SYN、FIN等。具体含义如下:
如图.1所示:A 向 B 发送消息 SYN 基于TCP传输的可靠性,B会回复一个确认消息 ACK,作为收到消息SYN的回复;而对于B 也同样对 A 做一轮收发
, 此时 AB双向收发能力正常。
可以看到 AB 通信中间两步都是 B>>>A 那么这两个消息包可以合并成一个如图.2
很清晰可以看出来,这就是TCP建立连接时的三次握手
注意: 图中的x
为客户端初始化序列号;图中的y
为服务端初始化序列号
握手之前主动打开连接的客户端结束CLOSED阶段,被动打开的服务器端也结束CLOSED阶段,并进入LISTEN阶段。随后开始“三次握手”:
(1)首先客户端向服务器端发送一段TCP报文,详细:
- 标记位 SYN=1,表示“请求建立新连接”;
- 序号为Seq=X(sequence=X(随机int) 一般为1);
- 随后客户端进入SYN-SENT阶段。
(2)服务器端接收到来自客户端的TCP报文之后,结束LISTEN阶段。并返回一段TCP报文,详细:
- 标志位为SYN=1,ACK=1,表示“确认客户端的报文Seq序号有效,服务器能正常接收客户端发送的数据,并同意创建新连接”,序号为Seq=y;
- 确认号为Ack=x+1,表示收到客户端的序号Seq并将其值加1作为自己确认号Ack的值;
- 随后服务器端进入SYN-RCVD阶段。
(3)客户端接收到来自服务器端的确认收到数据的TCP报文之后,明确了从客户端到服务器的数据传输是正常的,结束SYN-SENT阶段。并返回最后一段TCP报文。详细:
- 标志位为ACK=1,表示“确认收到服务器端同意连接的信号”;
- 序号为Seq=x+1,表示收到服务器端的确认号Ack,并将其值作为自己的序号值;
- 确认号为Ack=y+1,表示收到服务器端序号Seq,并将其值加1作为自己的确认号Ack的值;
- 随后客户端进入ESTABLISHED阶段。
服务器收到来自客户端的“确认收到服务器数据”的TCP报文之后,明确了从服务器到客户端的数据传输是正常的。结束SYN-SENT阶段,进入ESTABLISHED阶段。
在客户端与服务器端传输的TCP报文中,双方的确认号Ack和序号Seq的值,都是在彼此Ack和Seq值的基础上进行计算的,这样做保证了TCP报文传输的连贯性。一旦出现某一方发出的TCP报文丢失,便无法继续"握手",以此确保了"三次握手"的顺利完成。
此后客户端和服务器端进行正常的数据传输。这就是“三次握手”的过程。
client第一个syn包丢失,没有收到server的ack,则client进行持续重传syn包。总尝试时间为75秒。参与文献《TCP/IP详解 卷1:协议》p178
server收到了client的syn,并发出了syn+ack包,syn+ack包丢失。
client方面,因为没收server的。将执行情况(1);
server方面,超时时间内没有收到client的ack包(或者数据包),会持续发送syn+ack包;
当Client端收到Server的SYN+ACK应答后,其状态变为ESTABLISHED,并发送ACK包给Server;
如果此时ACK在网络中丢失,那么Server端该TCP连接的状态为SYN_RECV,并且依次等待3秒、6秒、12秒后重新发送SYN+ACK包,以便Client重新发送ACK包,以便Client重新发送ACK包;
Server重发SYN+ACK包的次数,可以通过设置/proc/sys/net/ipv4/tcp_synack_retries修改,默认值为5;
如果重发指定次数后,仍然未收到ACK应答,那么一段时间后,Server自动关闭这个连接;
第三次握手最终loss后,Client 最终跟 Server的通信有两种说法,抓包做个解析:
1.Client认为这个连接已经建立,如果Client端向Server写数据,Server端将以RST包响应,方能感知到Server的错误。(存疑)
2.如果此时client向server发送数据包,server能正常接收数据。并认为连接已正常。
应用层编写socket代码时,三次握手发生在client的connect,所以为了避免长时间(75秒)无响应连接,应设置为非阻塞socket,同时用select检测设置合适的超时时间。
挥手之前主动释放连接的客户端结束ESTABLISHED阶段。随后开始“四次挥手”:
(1)首先客户端想要释放连接,向服务器端发送一段TCP报文,其中:
标记位为FIN,表示“请求释放连接“;
序号为Seq=U;
随后客户端进入FIN-WAIT-1阶段,即半关闭阶段。并且停止在客户端到服务器端方向上发送数据,但是客户端仍然能接收从服务器端传输过来的数据。
注意:这里不发送的是正常连接时传输的数据(非确认报文),而不是一切数据,所以客户端仍然能发送ACK确认报文。
(2)服务器端接收到从客户端发出的TCP报文之后,确认了客户端想要释放连接,随后服务器端结束ESTABLISHED阶段,进入CLOSE-WAIT阶段(半关闭状态)并返回一段TCP报文,其中:
标记位为ACK,表示“接收到客户端发送的释放连接的请求”;
序号为Seq=V;
确认号为Ack=U+1,表示是在收到客户端报文的基础上,将其序号Seq值加1作为本段报文确认号Ack的值;
随后服务器端开始准备释放服务器端到客户端方向上的连接。
客户端收到从服务器端发出的TCP报文之后,确认了服务器收到了客户端发出的释放连接请求,随后客户端结束FIN-WAIT-1阶段,进入FIN-WAIT-2阶段
前"两次挥手"既让服务器端知道了客户端想要释放连接,也让客户端知道了服务器端了解了自己想要释放连接的请求。于是,可以确认关闭客户端到服务器端方向上的连接了
(3)服务器端自从发出ACK确认报文之后,经过CLOSED-WAIT阶段,做好了释放服务器端到客户端方向上的连接准备,再次向客户端发出一段TCP报文,其中:
标记位为FIN,ACK,表示“已经准备好释放连接了”。注意:这里的ACK并不是确认收到服务器端报文的确认报文。
序号为Seq=W;
确认号为Ack=U+1;表示是在收到客户端报文的基础上,将其序号Seq值加1作为本段报文确认号Ack的值。
随后服务器端结束CLOSE-WAIT阶段,进入LAST-ACK阶段。并且停止在服务器端到客户端的方向上发送数据,但是服务器端仍然能够接收从客户端传输过来的数据。
(4)客户端收到从服务器端发出的TCP报文,确认了服务器端已做好释放连接的准备,结束FIN-WAIT-2阶段,进入TIME-WAIT阶段,并向服务器端发送一段报文,其中:
标记位为ACK,表示“接收到服务器准备好释放连接的信号”。
序号为Seq=U+1;表示是在收到了服务器端报文的基础上,将其确认号Ack值作为本段报文序号的值。
确认号为Ack=W+1;表示是在收到了服务器端报文的基础上,将其序号Seq值作为本段报文确认号的值。
随后客户端开始在TIME-WAIT阶段等待2MSL
为什么要客户端要等待2MSL呢?见后文。
服务器端收到从客户端发出的TCP报文之后结束LAST-ACK阶段,进入CLOSED阶段。由此正式确认关闭服务器端到客户端方向上的连接。
客户端等待完2MSL之后,结束TIME-WAIT阶段,进入CLOSED阶段,由此完成“四次挥手”。
后“两次挥手”既让客户端知道了服务器端准备好释放连接了,也让服务器端知道了客户端了解了自己准备好释放连接了。于是,可以确认关闭服务器端到客户端方向上的连接了,由此完成“四次挥手”。
与“三次挥手”一样,在客户端与服务器端传输的TCP报文中,双方的确认号Ack和序号Seq的值,都是在彼此Ack和Seq值的基础上进行计算的,这样做保证了TCP报文传输的连贯性,一旦出现某一方发出的TCP报文丢失,便无法继续"挥手",以此确保了"四次挥手"的顺利完成。
(1)、client发的FIN包丢了,对于client,因为没收对应的ACK包,应当一直重传(像普通包一样),直至到达上限次数,直接关闭连接;对于server,它应该无任何感知;
(2)、server回client的ACK包丢了,对于client,将执行(1),对于server将像丢普通的ack一样,再次收到FIN后,再发一个ACK包;
(3)、如果client收到ACK后,server直接跑路。client将永远停留在这个状态(半打开状态,就像client关闭了输出一样)。linux有tcp_fin_timeout这个参数,设置一个超时时间 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看,默认60s,可否修改看linux具体版本; windows 注册表有TcpTimedWaitDelay,win10默认值30s;
(4)、server发的FIN包丢了,对于server,像丢普通的包一样,重传。若此时client早已跑路且与其他人建立的连接,client应会不认识这个FIN包,直接回个RST包给server。
如若client没跑路,且没收到server的FIN包,如(3)描述;
(5)、client回复ACK后,按道理来说,可以跑路了,但防止回复的ACK包丢失(丢失后,server因为没收FIN的ACK,所以会再发一个FIN),将等待2MSL(最大报文存活时间)(RFC793定义了MSL为2分钟,Linux设置成了30s)为什么要这有TIME_WAIT?为什么不直接给转成CLOSED状态呢?主要有两个原因:1)TIME_WAIT确保有足够的时间让对端收到了ACK,如果被动关闭的那方没有收到Ack,就会触发被动端重发Fin,一来一去正好2个MSL,2)有足够的时间让这个连接不会跟后面的连接混在一起(你要知道,有些自做主张的路由器会缓存IP数据包,如果连接被重用了,那么这些延迟收到的包就有可能会跟新连接混在一起),这期间如若再收到server的FIN,则再回复ACK;
可以看到时三次握手第二步时,server收到client的SYN包并回复SYN+ACK包后,server会把这条连接放入“半连接队列”。这边有个问题,假设这个client是恶意的,client只发SYN包,收到SYN+ACK后不回复,那在server方面,会一直存有这条“半连接”,client数量达到一定程序,serve就炸了。这就是所谓的SYN FLOOD攻击;那怎么防止这种情况呢?有下面几种方法:(来自RFC 4987)
过滤
增加积压
减少SYN-RECEIVED定时
复用古老的半开通TCP
SYN缓存
SYN Cookie
混合方法
防火墙和代理
MSL是报文在网络中最长生存时间,这是一个工程值(经验值),不同的系统中可能不同。场景:1. A发出ACK后,等待一段时间T,确保如果B重传FIN自己一定能收到分析:1. ACK从A到B最多经过1MSL,超过这个时间B会重发FIN2. B重发的FIN最多经过1MSL到达A结论:如果B重发了FIN,且网络没有故障(重发的FIN被丢弃或错误转发),那么A一定能在2MSL之内收到该FIN,因此A只需要等待2MSL。
通信所要解决的首要问题就是,保持通信双方的信息对称,使通信双方处于同步状态。先来一个例子:罗密欧大学期间写信给中学同学朱丽叶,信的内容如下:小叶子,我喜欢你!这封信发出之后,罗密欧无法知道朱丽叶能否收到,只有收到小叶子的回信,才能知道自己的信已经到达对方。三天之后,小叶子回信了,信的内容如下:小欧,来信已阅,我也喜欢你…此时,小叶子眼中双方的状态是:互相爱慕!如果小欧收到回信,小欧眼中双方的状态也是:互相爱慕!如果小欧没有收到回信,小欧眼中双方的状态是:单相思!小叶子为了杜绝小欧模棱两可的状态,使他与自己达成“互相爱慕”的共识,需要做以下工作:1)先耐心地等小欧的第三封信2)如果若干天没有收到回信,需要把自己的第二封信再次发出如果收到了小欧的回信,那么双方的状态终于同步了:“互相爱慕”!即使2)发生了,N天之后也可以达成同步状态。之后,双方可以甜言蜜语地谈恋爱了。TCP四次挥手也遵循相似的套路。主动断开的一侧为A,被动断开的一侧为B。第一个消息:A发FIN第二个消息:B回复ACK第三个消息:B发出FIN此时此刻:B单方面认为自己与A达成了共识,即双方都同意关闭连接。此时,B能释放这个TCP连接占用的内存资源吗?不能,B一定要确保A收到自己的ACK、FIN。所以B需要静静地等待A的第四个消息的到来:第四个消息:A发出ACK,用于确认收到B的FIN当B接收到此消息,即认为双方达成了同步:双方都知道连接可以释放了,此时B可以安全地释放此TCP连接所占用的内存资源、端口号。所以被动关闭的B无需任何wait time,直接释放资源。但,A并不知道B是否接到自己的ACK,A是这么想的:1)如果B没有收到自己的ACK,会超时重传FiN那么A再次接到重传的FIN,会再次发送ACK2)如果B收到自己的ACK,也不会再发任何消息,包括ACK无论是1还是2,A都需要等待,要取这两种情况等待时间的最大值,以应对最坏的情况发生,这个最坏情况是:去向ACK消息最大存活时间(MSL) + 来向FIN消息的最大存活时间(MSL)。这恰恰就是2MSL( Maximum Segment Life)。等待2MSL时间,A就可以放心地释放TCP占用的资源、端口号,此时可以使用该端口号连接任何服务器。为何一定要等2MSL?如果不等,释放的端口可能会重连刚断开的服务器端口,这样依然存活在网络里的老的TCP报文可能与新TCP连接报文冲突,造成数据冲突,为避免此种情况,需要耐心等待网络老的TCP连接的活跃报文全部死翘翘,2MSL时间可以满足这个需求(尽管非常保守)!
link1
link2