4. 抓取第一个Socket通信包:编写Python TCP客户端/服务器代码、启动抓包、过滤tcp.port==xxxx、观察三次握手(SYN, SYN-ACK, ACK)

好,到了真正动手的时候了。前面几章我们聊了Wireshark的基本界面、过滤语法,也讲了TCP协议栈的大致模样。但说实话,光看不练是学不会网络分析的。这一章,我们亲手造一个TCP连接出来,然后用Wireshark把它“抓现行”。

我个人的习惯是,学任何协议都先写一个最小可用的Demo。这样你能完全控制通信的每一个细节,出了问题也知道从哪里查起。咱们今天就写一个最简单的Python TCP客户端和服务器,让它们握一次手,然后看看Wireshark里到底长什么样。

4.1 编写Python TCP服务器与客户端

先别急着打开Wireshark。咱们先把代码跑起来,确认能通,再开始抓包。这样能避免抓了一堆无关流量,自己都看花了眼。

4.1.1 服务器端代码

服务器的作用很简单:监听一个端口,等客户端来连。连上之后,打印一条消息,然后发回一个“Hello, Client”的响应。嗯,就这些。

# tcp_server.py
import socket

def main():
    # 1. 创建TCP socket
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 2. 绑定地址和端口
    server_address = ('127.0.0.1', 8888)
    server_socket.bind(server_address)
    
    # 3. 开始监听,最大等待队列长度为5
    server_socket.listen(5)
    print(f"[服务器] 正在监听 {server_address[0]}:{server_address[1]}")
    
    # 4. 接受客户端连接
    client_socket, client_address = server_socket.accept()
    print(f"[服务器] 收到来自 {client_address} 的连接")
    
    # 5. 接收数据
    data = client_socket.recv(1024)
    print(f"[服务器] 收到: {data.decode()}")
    
    # 6. 发送响应
    response = "Hello, Client"
    client_socket.send(response.encode())
    print(f"[服务器] 发送: {response}")
    
    # 7. 关闭连接
    client_socket.close()
    server_socket.close()

if __name__ == "__main__":
    main()

4.1.2 客户端代码

客户端更简单:连上去,发一条消息,收回复,然后关掉。注意,这里我故意让客户端先发数据,这样你能看到完整的请求-响应过程。

# tcp_client.py
import socket

def main():
    # 1. 创建TCP socket
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 2. 连接服务器
    server_address = ('127.0.0.1', 8888)
    print(f"[客户端] 正在连接 {server_address[0]}:{server_address[1]}")
    client_socket.connect(server_address)
    
    # 3. 发送数据
    message = "Hello, Server"
    client_socket.send(message.encode())
    print(f"[客户端] 发送: {message}")
    
    # 4. 接收响应
    data = client_socket.recv(1024)
    print(f"[客户端] 收到: {data.decode()}")
    
    # 5. 关闭连接
    client_socket.close()

if __name__ == "__main__":
    main()
小提示:端口号8888是我随便选的,只要不跟系统服务冲突就行。你在自己机器上跑的时候,如果8888被占了,换成其他端口比如9999、12345都可以。但记得,Wireshark过滤的时候也要跟着改。

4.2 启动抓包:三步走

代码写好了,怎么抓?我一般按这个顺序来,不容易漏包。

  1. 先开Wireshark,选对网卡。 我们用的是127.0.0.1(回环地址),所以选Loopback或者叫“Adapter for loopback traffic capture”。不同系统名字不一样,Windows上叫“Npcap Loopback Adapter”,macOS/Linux上直接选“lo0”或“any”。
  2. 设置过滤条件。 在过滤栏输入 tcp.port == 8888,然后点那个绿色小箭头应用过滤。这样Wireshark就只显示跟8888端口相关的包,其他乱七八糟的流量全被挡在外面。
  3. 开始抓包。 点那个鲨鱼鳍图标(或者双击网卡名字),Wireshark就开始收了。这时候先别急着跑代码,等个一两秒,确认抓包已经开始。
注意:我曾经犯过一个低级错误——先跑了客户端代码,再点开始抓包。结果三次握手的前两个包(SYN和SYN-ACK)全没抓到,只看到一个孤零零的ACK。所以顺序很重要:先开始抓包,再运行程序。

4.3 运行代码并观察三次握手

好,现在Wireshark已经在监听了。打开两个终端:一个跑服务器,一个跑客户端。

# 终端1:启动服务器
$ python tcp_server.py
[服务器] 正在监听 127.0.0.1:8888

# 终端2:启动客户端
$ python tcp_client.py
[客户端] 正在连接 127.0.0.1:8888
[客户端] 发送: Hello, Server
[客户端] 收到: Hello, Client

客户端跑完,立刻回到Wireshark,点那个红色方块停止抓包。这时候你应该能看到几行记录。如果过滤条件设对了,应该只有4-5个包,非常干净。

咱们把这几个包拆开看。下面这张图是我根据抓包结果画的,帮你理解每个包的角色。

核心观察:三次握手不是“客户端发一个SYN,服务器回一个SYN-ACK,客户端再回一个ACK”这么简单。注意看序列号(Sequence Number)和确认号(Acknowledgment Number)的变化,这才是精髓。
TCP三次握手流程(端口8888) 客户端 (127.0.0.1) 服务器 (127.0.0.1) CLOSED → SYN_SENT LISTEN ① SYN (Seq=0) ② SYN-ACK (Seq=0, Ack=1) ③ ACK (Seq=1, Ack=1) SYN_SENT → ESTABLISHED LISTEN → ESTABLISHED 数据交换(Hello, Server / Hello, Client)

4.4 逐包分析:Wireshark里看到了什么

现在咱们对照着Wireshark的抓包结果,一包一包地看。我假设你抓到的包跟我下面描述的一致。

包序号 源IP:端口 目标IP:端口 Flags(标志位) Seq(序列号) Ack(确认号) 含义
1 127.0.0.1:49152 127.0.0.1:8888 SYN 0 0 客户端请求建立连接
2 127.0.0.1:8888 127.0.0.1:49152 SYN, ACK 0 1 服务器同意连接,并确认收到SYN
3 127.0.0.1:49152 127.0.0.1:8888 ACK 1 1 客户端确认收到SYN-ACK,连接建立
4 127.0.0.1:49152 127.0.0.1:8888 PSH, ACK 1 1 客户端发送数据 "Hello, Server"
5 127.0.0.1:8888 127.0.0.1:49152 PSH, ACK 1 14 服务器发送数据 "Hello, Client"

这里有几个细节,我当年学的时候踩过坑,跟你分享一下。

4.4.1 为什么客户端的端口是49152?

你可能会问:我代码里没写客户端端口啊,怎么冒出来一个49152?这是操作系统自动分配的临时端口(Ephemeral Port)。TCP连接需要一对IP和端口:服务器是固定的8888,客户端是系统随机挑一个没被占用的端口。49152是动态端口的起始范围,你每次跑可能不一样,这很正常。

4.4.2 序列号为什么从0开始?

Wireshark默认显示的是“相对序列号”(Relative Sequence Number),把三次握手的初始序列号显示为0。实际上,TCP握手时双方会各自生成一个随机的初始序列号(ISN),但Wireshark为了让你看得清爽,帮你做了归一化。你可以在Wireshark的Preferences里关掉这个功能,看到真正的原始序列号。不过我个人建议新手阶段就开着,省得被大数字搞晕。

4.4.3 确认号为什么是1?

看第2个包:服务器发SYN-ACK,Ack=1。这个1怎么来的?因为客户端发的SYN包虽然Seq=0,但它占用了1个序列号空间。服务器说“我收到了你的0号包,下次请从1号开始发”。同理,第3个包客户端回ACK,Ack=1,意思是“我收到了你的0号包(服务器的SYN),下次请从1号开始”。

记住这个规律:确认号 = 收到的序列号 + 该包占用的字节数。SYN和FIN标志位各占1个序列号空间,纯ACK不占。这个规则在后续分析重传、丢包时特别有用。

4.5 在Wireshark中验证三次握手

光看表格还不够,你得学会在Wireshark的界面里找到这些信息。我建议你按以下步骤操作:

  1. 选中第一个SYN包,在中间的面板里展开 Transmission Control Protocol 这一行。
  2. 看 Flags 字段:0x002 (SYN)。这就是SYN标志位被置1的证据。
  3. 看 Sequence Number (raw):这里显示的是原始序列号,通常是个很大的随机数。但Wireshark在Summary列里帮你算好了相对值。
  4. 再看第二个包,Flags字段变成0x012 (SYN, ACK)。注意,ACK标志位也亮了。
  5. 第三个包,Flags字段是0x010 (ACK)。只有ACK,没有SYN了。

嗯,到这里,三次握手就完整了。从第四个包开始,PSH标志位出现,表示开始推送实际数据了。

避坑指南:我曾经在分析一个线上问题时,看到三次握手之后紧跟着一个RST包,连接直接被重置了。后来发现是客户端发的数据格式服务器不认,服务器直接摔门了。所以,三次握手成功不代表应用层通信就正常——它只说明TCP连接建起来了,数据能不能被正确解析,那是另一回事。

4.6 本章小结

这一章我们亲手造了一个TCP连接,从代码到抓包到分析,走了一个完整的闭环。你学会了:

  • 用Python写一个最小化的TCP客户端和服务器
  • 在Wireshark里设置端口过滤,精准捕获目标流量
  • 识别三次握手的三个包:SYN、SYN-ACK、ACK
  • 理解序列号和确认号在握手过程中的变化规律

说实话,三次握手是TCP协议里最基础也最重要的概念。后面讲连接状态、超时重传、流量控制,全都建立在这个基础之上。你现在能亲手抓到并看懂这三个包,后面的路就好走多了。