抓包看TCP协议
前言
在计算机的世界中,通信双方的交互一般要通过网络这个中间媒介。而这个中间体在为不同的通信实体之间进行信息传递之时,可能会采用不同的通信协议,为的就是更好,更清晰、更有效率的协调双方信息传递。 在众多的通信协议中,有一些协议在实际的应用中被使用的越来越多,比如说我们今天要说的TCP协议。
学计算机网络时,TCP协议作为一个大头部可难倒了不少的同学。一部分可能是因为其确实不简单(握手过程、挥手过程,序列号,ack、封包拆包巴拉巴拉的),另一个可能是因为我们在业务上层使用的大都是应用层协议(或者框架给我们封装好了的网络库), 不太能接触TCP的实现细节。但是如果想要理解这个协议,更好的为上层业务做支撑,从底层细节来窥探协议过程不得不说是一个好的方式。
这里就以网络抓包的方式来总结TCP协议的一些重点内容,记录在此,希望可以便人便己。
关于TCP协议,在我看来其主要包括两个主要方面:
- 一个是保证协议的正确性。即在不可靠的网络传输之上如何保证协议能够正常工作。这里面主要涉及ACK、校验和、重传、和序列号等机制。
- 二是保证通信(和网络)的效率和性能。主要是尽可能在提高发送数据速率以及防止网络拥塞之间达到一个平衡。这里面这里面主要有选择重传、滑动窗口、拥塞控制等重点内容。
这里主要谈谈保证协议正确的中的建立连接(三次握手)和断开连接的过程(四次挥手)。
一、三次握手
1.1 为什么需要握手?
握手的过程是一个协商的过程。可靠通信的双方总要确定对方是否存在,是否准备好接受数据,对吧(UDP那种不管对方的状态怎么样,直接甩过去一堆数据的不是可靠老实的TCP的风格)。同时,由于可能会遇到不可靠的快递员(下层的通信网络)弄丢包裹(发送的数据报文段)的情况,因此时不时的tcp得重新发送上丢失的报文。这时候维护一个序列号seq,很正常吧(否则,上帝也不知道该重传哪一个报文吧)。 因此,可靠的通信需要一个握手协商的过程,合作嘛,对方的底细或多或少总的了解些吧。
1.2 为什么需要三次握手?
一言以蔽之,为了防止C端连接的重复初始化。因为有可能C端的第一次握手会发送多次(比如说以前的第一次握手的报文在中间的时候丢失了或者阻塞了,那么在超时之后就会重复发送第一段握手)。所以S端也可能收到好多个建立连接的请求(第一次握手的报文),服务端没有充足的信息来判断哪个连接才是真正的。所以很无奈,S端只能全部ACK,把皮球提给客户端。
客户端由于保存了最新请求的序列号,它知道哪一个请求才是最新的,因此它会给最新的那个ACK,其他过期的直接RST掉。 这个过程也就是必须的第三次握手的过程。
先盗一张图。
还可以从序列号建立的角度来看三次握手。
因为TCP是全双工协议,通信的双方可以同时发送数据给对方。因此协商的过程是双向的,而且必须得到ACK确认。
不明白? 好,解释细点。
- 第一次握手是C端(客户端)发起请求,准备建立连接。同时提供C端的初始序列号Seq1。( hello, 我想问你几个问题,这是我的起始问题编号,你看看有没有问题?)
- 第二次握手是S端(服务端)对C端的请求进行回复ACK;同时,S端为了向C端发送数据,也会提供一个S段的初始序列号Seq2。(enen,我收到了你的Seq1,ok,已经记录下来了;我也有几个问题想问你,这是我的起始序列号。你确认一下?)
- 第三次握手是C端对S端协商参数的确认(enne,好,我这边也准备好了)。同时,C端也可以在这次握手的时候发送一部分请求的数据。
上面两个握手只保证了S端对C端的确认,保证了C端可以向S端发送数据。但是无法保证S端的协商参数还没有得到C端的确认(在不可靠的下层网络中,谁也不知道第二次握手会会不会丢失)。因此还必须有第三次过程。只有经历过第三次握手,服务端才能确认客户端准备好接受数据了。当然,由于前两次握手已经说明S端准备好接受数据了,因此C端可以在第三次握手的时候夹带些私货(真正需要发送的数据)。
二、四次挥手
2.1 为何需要挥手?
既然相见时协商建立了连接,那么分离时,贸然离去总不是一个好的选择,毕竟对方还一直在傻傻等待(而且还维持了一些资源)。
所以,在断开连接时,tcp协议中规定了要有断开连接,即挥手的过程。
2.2 为何需要四次挥手?
那么为何需要四次挥手才能把连接正确的断开呢。
因为,tcp是全双工的通信协议,任何时刻对方都有可能在发送数据。因此,在断开连接时要告知对方(并尽量得到对方的ack确认)。
通信双方都可以首先进行断开连接。这里以客户端断开连接为例进行介绍。
- 在tcp包中打上FIN标志,就是向对方申明,自己这端不会再发送数据了。然后进入到FIN_WAIT_1状态 。
- 服务端接收到客户端的请求之后,向对方发送一个ACK,然后进入到CLOSE_WAIT的状态。(客户端接受到这个ACK之后进入到FIN_WAIT_2状态)
需要注意的是这个时候服务端依旧可以发送数据,因为服务端还没表明自己已经没有数据需要发送了
- 当服务端把应用层的数据发送完毕之后,也会发送一个FIN标志的tcp包来表明自己已经把所有的数据发送完毕了。这个时候服务端进入LAST_ACK状态,准备接收客户端的ACK。
- 客户端接收到服务端发送的FIN请求之后,会回一个ACK包。然后进入到最后的TIME_WAIT状态,等待 2MSL时间,进入最终的CLOSE状态。
以上是四次挥手的过程,两个FIN,两个ACK, 匹配的相当对称完美。
这里有几个问题需要阐明一下,当客户端返回最后一个ACK之后,与它相关的四次握手都已经结束,为什么还要在等待一个2MSL的时间呢?
两个原因:
- 一个是为了优雅的关闭整个连接,即如果客户端的第四次ACK握手丢失了,服务端会重传第三次握手,客户端处于TIME_WAIT状态(还没有关闭连接)可以继续响应ACK。
- 让本次连接中的所有报文在网络中都消息(有可能有延时的报文)。比如说,如果有一个阻塞在哪儿的SYN报文一直没到,在挥手过程中姗姗来迟了,服务端接收到这个请求后还以为是个新的连接呢(服务端会放弃目前的挥手阶段,进入到新建连接的阶段,但是这个SYN请求是过期的,不会得到客户端的响应)?
更详细的分析可以参见【4】
三、抓个包看看
上面讲的是理论部分,下面我们用tcpdump和wireshark抓包看一下。关于tcpdump和wireshark软件教程网上有一大堆,这里就不赘述了。
上简单的实验代码:
客户端程序:
#!/usr/bin/env python3
import socket
HOST = '9.134.149.248' # 服务器的主机名或者 IP 地址
PORT = 9998 # 服务器使用的端口
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.sendall(b'Hello, world')
data = s.recv(1024)
print('Received', repr(data))
~
~
服务端程序
#!/usr/bin/env python3
import socket
HOST = '0.0.0.0'
PORT = 9998 # 监听的端口 (非系统级的端口:大于 1023)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
conn, addr = s.accept()
with conn:
print('Connected by', addr)
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
~
抓个包看看?
握手过程:
上面就是三次握手过程。
- 客户端(9.134.128.119:35254)发起请求,使能SYN标志位,其初始的序列号为Seq1=1303148347.
- 服务端(9.134.149.248:9998)响应ACK, ack=Seq1+1;并且其也使能SYN标志,初始序列号为Seq2=3323688393;
- 客户端响应ACK,ack=Seq1+1.
当然除了协商以上信息之外,我们还能看到协商的其他参数,如窗口大小等。
传输数据过程:
中间的几个包是数据传输过程,一来一回,典型的echo服务,不用多讲。
挥手过程:
上图所示是tcp断开连接时、挥手时的包,是不是有点奇怪?
哎,为啥就三个包?
按理来说,是四个挥手的包来着。两个FIN、两个ACK。 但是我们的服务端程序在收到客户端的FIN之后,自己也没有数据要发送而来,直接退出了。所以,第二次握手和第三次握手就直接合并一起发送了。这样从某种程度上所也提高了断开连接的效率。
所以说三次挥手也是有可能滴。这种方式刚好和三次握手相对称了。
不过上述过程需要一定的条件,需要开启TCP 延迟确认机制才可以。关于TCP 延迟确认机制这里就不说了,否则又是一篇文章。想了解的可以参见【3】
后记
tcp协议是网络中的一个大头,看起来简单的握手挥手很容易,但是里面有很多设计细节和要点,深究下去都有不少东西。通过实验抓包,可以把协议理解的更形象些,更具体些。
ps:写到最后才慢慢发现,有点小小的头重脚轻,抓包协议部分写的有点少了,勿喷勿怪。
参考
【1】为什么 TCP 建立连接需要三次握手
【2】为什么 TIME_WAIT 状态的 TCP 连接,收到 SYN 报文后,可以正常建立连接?
【3】美团二面:TCP 四次挥手,可以变成三次吗?
【4】被微信面麻了,问的太细节了。。。