这道题我面试过不少Python候选人,自己也作为面试官问过不下几十次。先说结论:TCP和UDP都在传输层,对应OSI七层模型中的第4层,对应TCP/IP四层模型中的应用层与网络层之间。这个位置关系看着简单,但真正拉开差距的,是后面一连串追问:为什么需要这个层?TCP可靠在哪?UDP快在哪?Python里写socket时两种协议的行为有什么不同?如果你只是背了“TCP可靠、UDP不可靠”八个字,那这道题大概率只能拿个及格分。
这篇复习整理不打算写成教科书。我尽量用做项目时踩过的坑、抓包时看到的现象、写Python网络服务时的代码细节来拆解这道面试题。无论你在准备Python后端岗位、运维开发岗,还是平时写脚本要和网络设备、硬件设备打交道,把这题吃透都不会亏。
1. 先弄清楚TCP与UDP在网络协议里的位置
1.1 传输层到底管什么
很多人一开始不理解:IP层不是已经能让两台机器通信了吗?为什么上面还要有一个传输层?面试官如果追问到这里,你要能答出来:IP地址解决的是“哪台主机”,而传输层解决的是“这台主机上的哪个进程”。
每台机器上有大量程序需要收发网络数据,一个网页请求和一个视频通话请求可能同时在路上跑。IP包头里没有“端口号”这个概念,它只管把数据从一个IP搬到另一个IP。传输层的TCP和UDP引入了源端口和目的端口,相当于给数据加了“门牌号”。对端收到数据后,根据目的端口把数据交给对应的进程,比如80端口归Web服务、53端口归DNS、6379归Redis。这才是传输层存在的根本意义。
如果你觉得抽象,可以这样理解:IP层就像快递干线运输,只负责把包裹从上海仓运到北京仓;传输层是北京仓里的分拣员,必须把包裹送到具体办公室、具体工位的人手上。端口就是工位编号。
1.2 两种模型下的层次说法
面试的时候,最稳妥的回答是两层都讲:
- 在OSI七层模型里,TCP和UDP位于第4层传输层。上面是会话层、表示层、应用层,下面是网络层、数据链路层、物理层。
- 在TCP/IP四层模型里,只有应用层、传输层、网络层、网络接口层四层,TCP和UDP同样位于传输层,直接承接应用层数据,并向下交给IP层处理。
为什么很多教材会强调“TCP/IP协议族”?因为整个协议族就是围绕TCP和IP这两个核心协议命名的,但协议族里还有很多其他协议,比如UDP、ICMP、IGMP等。面试时如果顺带提到“TCP/IP是协议族,不只是TCP和IP两个协议”,会显得你层次感更强。
1.3 与前后层的边界划分
要避免把传输层和网络层混为一谈。网络层(IP层)负责主机到主机的寻址和路由选择,数据单位是IP数据报;传输层负责进程到进程的通信,TCP的数据单位叫报文段(segment),UDP的数据单位叫用户数据报(datagram)。面试时常说的TCP报文段、UDP数据报,就是这一层的数据形态。
也有面试官会问“TCP报文和IP报文什么关系”。可以这样答:TCP报文段会被封装进IP数据报的payload,IP层再把IP数据报交给数据链路层封装成帧。数据一层层套下来,很像俄罗斯套娃。这个封装关系也解释了为什么TCP头部里有校验和、序号、ACK等字段,它们都是为端到端传输服务的,而IP层只关心源IP、目的IP、TTL这些路由相关信息。
提示:如果面试官问“TCP在四层模型里是第几层”,别直接说“第3层”。这里最容易踩坑。OSI里是第4层,TCP/IP四层模型里它是第3层,但标准表述仍然是“传输层”。先报层名,再解释模型,基本不会错。
2. TCP与UDP的核心区别,从六个维度掰开讲
2.1 连接性:连接状态机 vs 发完就走
TCP是面向连接的协议。通信之前要建立连接,这就是三次握手;通信结束后要释放连接,也就是四次挥手。在连接的生命周期里,两端都要维护状态:SYN_SENT、ESTABLISHED、FIN_WAIT_1、TIME_WAIT等等。这套状态机保证了双方在逻辑上有“一条明确的通道”。
UDP是无连接的。它没有连接建立过程,发送方直接封装目标地址和端口,把报文扔给IP层就完事了。接收方也不需要维护什么连接状态,谁给我发数据,我就从报文里读出源地址和源端口,按需处理。这种设计让UDP天然没有握手延迟,但代价是“无状态”。服务端无法通过“连接”维度判断客户端是否在线,你也无法用UDP优雅地感知对端是否关闭。
面试里可以补一句:UDP的connect和TCP的connect完全不同。Python里对UDP socket调用connect,只是给socket绑定了一个默认目的地址,不产生任何握手报文。后续sendto可以不传地址,但recvfrom也只能接收该对端发来的数据了。
2.2 可靠性:挂号信 vs 明信片
TCP提供可靠的字节流传输。它会给每个字节一个序号,接收方收到数据后回ACK确认,发送方在超时时间内没收到ACK就重传。数据即使被IP层拆散、走不同路由乱序到达,TCP也会在接收端重新排序,确保应用层拿到的数据顺序和发送时一致。
UDP是尽力而为(best-effort)的。它只管把数据报发出去,不确认、不重传、不保证顺序。网络丢包了,UDP层不会管,应用层收到的报文可能是乱序的,也可能是重复的。理解“尽力而为”这四个字是关键:UDP并不保证一定丢包,它只是不负责恢复丢包。
生活类比很简单:TCP是发挂号信,寄件人跟踪签收单号,没送达就重发,收件人按编号整理。UDP是发普通明信片,丢不丢邮局不理,顺序乱了也不负责整理。
2.3 字节流与数据报:粘包问题的源头
这个区别做Python网络编程的人感触最深。TCP是流式协议,没有消息边界。发送方执行两次send,接收方可能一次recv就全收走;发送方执行一次send,接收方也可能分多次recv才收完。因为TCP把数据看成一串连续字节,应用层必须自己约定消息边界,这就引出了经典面试题“粘包怎么解决”。
UDP是报文协议,sendto封装成一个报文发出去,recvfrom每次读到一个完整报文。报文与报文之间天然有边界,不存在粘包问题。但报文有最大长度限制。在IPv4下,UDP报文理论上限是65535字节,再减去IP头20字节和UDP头8字节,payload最大约65507字节。实际网络中超过MTU(典型1500字节)就会触发IP分片,分片报文在传输中更容易丢,甚至有些防火墙直接丢分片包,所以大型UDP包要考虑应用层分片。
如果你被问到“TCP是否会发生粘包”,标准答案不是“会”这么简单,而是要从字节流特性说起:粘包是因为接收方没有正确识别消息边界,本质上不是TCP“制造”了粘包,而是应用层协议没有做边界划界。
2.4 头部开销与延迟:不只是8字节和20字节的差别
UDP头部固定8字节:源端口、目的端口、长度、校验和各占2字节。TCP头部最少20字节,包含序列号、确认号、标志位、窗口大小、校验和等,如果有选项字段,还会更长。头部大小差异虽然只有12字节,但TCP还有连接管理和响应机制带来的隐形成本。
TCP建立连接至少需要一个RTT(三次握手中的前两次),每次发送数据都要等ACK,拥塞时要降低发送速率。在弱网环境或者远距离传输下,TCP的“保底性能”可能变得很低。UDP没有ACK、没有重传、没有拥塞窗口,想发就发,单位时间内可以打满网卡带宽,所以常用于实时音视频、游戏同步这类不能等重传的场景。
要注意一个反直觉点:UDP不一定“更快”。在无拥塞、少丢包的局域网内,TCP用现代拥塞控制算法和批量确认机制,吞吐可能远超一个设计粗糙的UDP程序。只能说UDP的“延迟下限”更低,它不向你索要额外时间成本,但你要自己承担丢包和乱序的后果。
2.5 流量控制与拥塞控制
面试官如果往里挖,经常会问这两个机制的区别。流量控制是接收方主导的,它通过TCP头里的窗口字段告诉发送方“我的接收缓存还剩多少,你最多能发多少”,防止发送太快把接收方缓冲区打爆。拥塞控制是发送方感知网络状态后自我约束的机制,通过慢启动、拥塞避免、快重传、快恢复这些算法,探测网络能承受多快的发送速率,防止把路由器队列堵死。
UDP完全没有这两套机制。它不管接收方是否消化得了,也不管网络是否已经拥堵。这也是为什么有人说UDP可能导致网络拥堵崩溃。实际工程中,很多自研可靠UDP协议、QUIC协议,都会在应用层实现类似拥塞控制的功能。面试中主动说“UDP的拥塞控制需要应用层自己做,QUIC就是把这类工作放到用户态”,是非常大的加分项。
2.6 场景与选型快速对照表
| 维度 | TCP | UDP |
|---|---|---|
| 连接性 | 面向连接,三次握手 | 无连接,直接发报 |
| 可靠性 | 可靠,超时重传、ACK | 不可靠,尽力而为 |
| 有序性 | 保证字节顺序 | 可能乱序 |
| 传输边界 | 字节流,无边界 | 数据报,有边界 |
| 头部大小 | 最少20字节 | 固定8字节 |
| 流量控制 | 有 | 无 |
| 拥塞控制 | 有 | 无,应用层自理 |
| 广播/组播 | 不支持 | 支持 |
| 典型应用 | HTTP、SSH、MySQL、文件传输 | DNS、NTP、音视频、游戏、组播 |
这张表不只可以用于答题,也可以作为实际项目选型时的对照清单。我建议你把它存下来,工作中想快速判断协议选型时直接查。
3. 用Python把两种协议跑起来,感受区别
3.1 TCP服务端与客户端的最小骨架
Python的socket模块几乎是标准答案里绕不开的东西。TCP服务端骨架是:socket → bind → listen → accept → 收发数据 → close。
import socket server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(("0.0.0.0", 8888)) server.listen(5) conn, addr = server.accept() print("客户端已连接:", addr) data = conn.recv(1024) print("收到:", data.decode()) conn.sendall(b"hello from tcp server") conn.close() server.close()TCP客户端骨架是:socket → connect → 收发数据 → close。
import socket client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(("127.0.0.1", 8888)) client.sendall(b"ping") resp = client.recv(1024) print("服务端响应:", resp.decode()) client.close()这里最直观的是,TCP客户端必须connect,connect就是触发三次握手的过程。如果服务端根本没有listen,connect会直接抛ConnectionRefusedError,这就是你在排查TCP服务时最常见的“端口没通”现象。
3.2 UDP服务端与客户端的最小骨架
同样的事情,用UDP写出来短一截。UDP服务端不需要listen和accept,bind之后就能直接收发。
import socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind(("0.0.0.0", 9999)) while True: data, addr = s.recvfrom(1024) print(f"收到来自 {addr} 的数据: {data.decode()}") s.sendto(b"hello from udp server", addr)UDP客户端更简单,连connect都不调,sendto里带上目标地址就能发。
import socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.sendto(b"ping", ("127.0.0.1", 9999)) data, addr = s.recvfrom(1024) print("服务端响应:", data.decode()) s.close()对比两个服务端代码,你会立刻抓住重点:TCP多出listen和accept,UDP不需要。因为TCP必须先建立连接才能收发数据,accept就是“接受一次连接请求”;UDP的报文自带来源地址,recvfrom直接能拿到对端ID。
3.3 代码层面的六个不同点
把这些观察归纳一下,正好是面试回答的好素材:
- TCP服务端要listen,UDP不用。listen的本质是进入被动监听状态,等待握手请求进入内核连接队列。
- TCP客户端要connect才能发数据,UDP不需要。connect对应SYN/SYN-ACK/ACK握手过程。
- TCP收发用send和recv,UDP用sendto和recvfrom。因为UDP需要显式携带/获取对端地址。
- TCP有recv不到完整数据的问题,UDP一次recvfrom拿一个完整报文。
- TCP有粘包风险,UDP没有。代码上TCP要设计消息边界,UDP不需要。
- TCP服务端默认只能处理一个客户端(串行accept),UDP服务端天生能轮询处理多个客户端。但要注意,UDP“无连接”不代表没并发压力,处理大量客户端仍需多线程或异步。
3.4 实测:UDP会丢包,TCP不会
我经常建议候选人自己写脚本做一个丢包实验。在UDP服务端开一个1024字节的缓冲区,客户端不断发带序号的数据报,服务端打印序号,你会发现收到的序号不是连续的。
# 发送端 import socket import time s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for i in range(1000): msg = f"packet-{i:04d}".encode() s.sendto(msg, ("127.0.0.1", 9999)) time.sleep(0.001)# 接收端 import socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind(("0.0.0.0", 9999)) s.settimeout(3) received = [] try: while True: data, addr = s.recvfrom(1024) received.append(data.decode()) except socket.timeout: pass missed = [i for i in range(1000) if f"packet-{i:04d}" not in received] print(f"共收到多少个报文: {len(received)}") print(f"丢失个数: {len(missed)}")本地回环上丢包很少,因为数据不走物理网卡,不经过路由器队列,缓冲区几乎不会满。但你把发送间隔改小、把缓冲区改小,或者换到两台真机之间通过弱网发,丢包率会立刻显现。这个实验的价值在于:UDP的不可靠不是理论口号,是立刻能复现的。
如果换成TCP,把同样的循环改成sendall,接收端一定能按序收到全部数据。这就是“TCP可靠”的最直接工程诠释。
3.5 写Python socket常见的坑
- TCP发送用sendall,别用send。send只发一次,可能没发完;sendall会循环发完。它的性能略低一点,但正确性优先。
- UDP没有sendall。因为报文是原子的,你想发一个完整报文就只能用sendto一次发全,拆开了含义就变了。
- Python 3里收发的数据都是bytes,不是str。先encode再发,收到先decode再看,否则报错。
- UDP sendto发超长报文时,在IP层会被分片,对端可能要收多个分片再重组。测试时最好控制在1472字节以内(1500 MTU减去20字节IP头、8字节UDP头),超过这个数性能会明显下降。
- UDP socket也可以调用connect,它只是绑定默认对端,不发握手包。但一旦connect了,就不要再想“无连接发送”,某些系统上语义会变化。
4. 面试官会顺着追问的题:三次握手、四次挥手、粘包
4.1 三次握手:为什么必须是三次
TCP三次握手的过程是:客户端发SYN,服务端回SYN+ACK,客户端再回ACK。很多人能画出来,但面试官会追问“为什么不能两次”。两个核心点:
第一,双向确认收发能力。第一次,客户端证明自己有发送能力;第二次,服务端既确认收到,也向客户端证明自己能发;第三次,客户端再确认自己收到服务端的SYN。如果只有两次,客户端无法确认服务端是否收到了自己的ACK能力,服务端也无法确认客户端是否有接收能力被“校准”。
第二,防止历史连接请求误建连接。假设客户端第一次发了一个SYN,因为网络拥堵迟迟没到服务端,客户端超时重发SYN并成功建立了连接,传输结束后旧的SYN才到达。如果只有两次握手,服务端遇到旧SYN还会创建一条新连接,不但浪费资源,还可能把旧连接的数据和新连接混在一起。有了第三次握手,客户端能识别这是过期请求,主动发RST拒绝,服务端才会撤销半连接。网上常说的“防失效连接请求”,指的就是这个场景。
4.2 四次挥手:TIME_WAIT是怎么回事
四次挥手的过程是:主动关闭方发FIN,对端回ACK,对端也发FIN,主动方再回ACK。因为TCP是全双工的,每个方向的通道都需要单独关闭,所以需要四个步骤。前两次关闭主动方到对端的数据通道,后两次关闭对端到主动方的通道。
这里的高频追问是“为什么主动关闭方要进入TIME_WAIT并等2MSL”。两个原因:一是要保证最后一个ACK能到达对端。如果这个ACK丢了,对端会超时重发FIN,主动方必须还在监听并重新回ACK。二是要让网络中残留的旧数据包自然消亡。如果一条连接关闭后立刻用同样的IP和端口建立新连接,旧连接在网络中迟到的包可能串到新连接里。等2MSL(最大报文段生存时间的两倍),就能确保旧连接里的所有报文都已在网络中消失。Linux上TIME_WAIT默认约60秒,这也是你端口频繁重启时报“Address already in use”的原因之一。解决方法是设置SO_REUSEADDR,但要了解这只是“重用地址”的常见手段,不是万能药。
4.3 粘包与拆包:TCP字节流的必然结果
粘包是Python面试里的经典题。TCP没有消息边界,业务上的两个消息在发送方和接收方的视角里都是“一段连续字节”。当多个send的数据在内核缓冲区里被合并,一次recv拿到多条消息,就是粘包;当一条大消息占多个TCP报文,接收方一次recv只拿到部分,就是拆包。
解决办法无非是四类:
- 固定长度:每个消息定长,不够就补位。简单,但浪费带宽,不适合变长数据。
- 长度前缀:先发4字节长度,再发消息体。这是最通用、最推荐的做法,RPC框架基本都是这种风格。
- 分隔符:比如用换行符分割。适合文本协议,比如HTTP头部的行分隔,但二进制数据里要小心冲突。
- 应用层协议:直接使用HTTP/2、gRPC、protobuf等成熟协议,边界问题由协议库处理。
Python里用长度前缀的简易实现如下:
import struct def pack_msg(payload: bytes) -> bytes: return struct.pack("!I", len(payload)) + payload def read_msg(conn): header = conn.recv(4) if len(header) < 4: return None (length,) = struct.unpack("!I", header) data = b"" while len(data) < length: chunk = conn.recv(length - len(data)) if not chunk: break data += chunk return data这里用“!I”表示网络字节序的无符号4字节整数,避免大小端问题。实际代码里还要考虑TCP recv可能收不齐4字节头的情况,这里只展示核心思想。
顺带要记住:UDP没有粘包,但UDP有丢包、乱序、重复和上限问题。面试官如果问“UDP如何解决可靠传输”,标准思路是:应用层实现序列号、ACK确认、超时重传,或者直接使用RUDP、QUIC这类在UDP之上构建可靠传输的方案。能答到这里,这道题的深度分基本到手。
4.4 其他值得准备的小点
面试官还喜欢问TCP的流量控制和拥塞控制的区别。流量控制解决“接收方吃不消”的问题,靠滑动窗口实现;拥塞控制解决“网络撑不住”的问题,靠慢启动、拥塞避免、快重传、快恢复实现。两者同是“控制发送速度”,但出发点完全不同。
还有MTU与MSS:MTU是IP层最大传输单元,典型1500字节。TCP为了避免IP分片,会在握手时协商MSS,通常等于MTU减去IP头和TCP头,也就是1460字节。UDP不协商MSS,超长数据直接交给IP分片,分片越多,丢片概率越大。这也是为什么UDP传输大数据时要在应用层自己切包的另一个原因。
5. 工程选型与排障经验
5.1 什么时候坚决用TCP
只要数据丢掉一个字节都不行,就选TCP。典型场景包括HTTP网页交互、文件上传下载、数据库连接、消息队列、SSH远程登录、邮件SMTP/POP3等。还有工业场景里的Modbus TCP通信,很多PLC通过W5500这类以太网控制器走Modbus TCP协议,底层就是TCP连接,因为控制指令必须可靠到达,重复执行或漏执行都可能出事故。
另外,如果你要传输的数据长度不定、消息密度不大、实时性要求不高,直接走TCP最简单。因为TCP帮你处理了重传、排序、流控,应用层只需要关心业务逻辑。工作里我见过不少团队硬要用UDP“追求性能”,结果花了大量精力实现可靠传输,最后性能还拼不过一个优化过的TCP长连接,属于典型的过度设计。
5.2 什么时候放心用UDP
实时性比可靠性重要的场景,UDP是首选。在线视频、语音通话、多人实时对战,这类应用可以容忍偶尔丢几十毫秒的声音或画面,但不能容忍重传带来的延迟。DNS查询也默认使用UDP,因为请求小、交互多,一次请求失败客户端重发一次成本很低。NTP时间同步同样用UDP,时间敏感且协议简单。HTTP/3和WebRTC的底层也是UDP,它们把可靠传输做进了上层的QUIC,既拿到UDP的低延迟,又补齐了可靠性。
物联网场景也经常碰到UDP。我自己调试过ESP01S这类WiFi模块向手机App发送消息,小数据、低功耗、实时性优先,用UDP就很方便。工业现场还有西门子S7-1200做UDP组播、用IGMP做组播组的场景,一个PLC数据同时发给多个上位机,TCP无法完成广播和组播,只能靠UDP。
5.3 广播与组播:这是TCP绝对做不到的
TCP是点对点连接,不存在一对多的数据分发能力。UDP天然支持四种传输形式:单播、广播、组播、任播。广播是把数据发到同网段所有主机,组播是把数据发到加入特定组播组的一组主机。面试如果问你TCP为什么不能广播,本质上是因为TCP的建立连接需要点对点握手,一个服务端无法同时为成千上万个客户端维护连接并主动推送数据。而UDP的sendto目标地址可以直接填广播地址或组播地址,底层网络设备会帮忙复制分发。
Python中使用组播需要在socket上设置IP_ADD_MEMBERSHIP选项,加入组播组:
import socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(("", 8888)) group = "239.0.0.1" mreq = socket.inet_aton(group) + socket.inet_aton("0.0.0.0") s.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) while True: data, addr = s.recvfrom(1024) print(addr, data)这个代码在拉流调试和工控场景里很实用。但要注意,组播在跨三层路由器时需要路由器开启IGMP、PIM等协议,不是应用层能单独解决的。
5.4 常见UDP报错:10054、丢包与缓冲区溢出
看到热搜词里有“read udp: unknown error (code=10054)”,这种情况我用Windows做本地UDP调试时遇到过。Windows上UDP socket收到ICMP Port Unreachable(端口不可达)错误后,recvfrom会返回WSAECONNRESET,也就是错误码10054。常见原因是:你给一个本机不存在的端口发UDP包,系统反馈了一个ICMP错误,如果这个UDP socket做了connect绑定默认对端,内核就直接把错误抛给应用层。
处理方法:一是接受ICMP并让应用层忽略该错误;二是服务端程序没有启动或端口配置错误,优先检查目标端口是否监听;三是如果不想理会这种错误,可以用SocketException的ErrorCode判断后continue。很多生产框架在Windows下跑UDP服务都会打补丁处理10054,不是罕见问题。
UDP另一个典型问题就是丢包率居高不下。排障时先区分是内核丢还是应用层丢。Linux下用netstat -su可以看接收缓冲区溢出次数;用ss -unp可以看socket接收队列大小。如果recvfrom处理不过来,可以调大socket缓冲区:
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4 * 1024 * 1024)但单纯调大缓冲区只是缓解,根因是应用处理速度跟不上网卡到达速率。更实际的手段是开多线程读socket、改用DPDK或者写专门的流量采集进程。
5.5 实测工具:tcpdump、wireshark、iperf3
面试聊到抓包和压测,能说出具体工具和命令会非常加分。Linux下用tcpdump抓TCP握手包:
tcpdump -i eth0 tcp port 8080 -w tcp.pcap然后在Wireshark里过滤tcp.flags.syn == 1,可以清楚看到SYN、SYN-ACK、ACK的交互。排查TCP重传可以用tcp.analysis.retransmission过滤器,一眼看出哪些段丢包了。
UDP打流测试我常用iperf3。服务端:
iperf3 -u -s客户端:
iperf3 -u -c 192.168.1.100 -b 100M -t 30-i 参数可以打印带宽、抖动、丢包率,实测丢包率超过1%就要认真排查了。很多所谓“UDP太快所以网络崩了”的案子,本质是没做流控,应用层发送速率远大于网络承受能力。
我个人在面试别人时,不太喜欢候选人只说“TCP可靠UDP不可靠”这半句。我更想听到他从端口寻址、字节流边界、连接状态机这些底层角度去展开,再落到Python代码里sendall和sendto的差异。这道题练扎实了,不只是应付面试,后面排查线上网络问题、写网络服务的时候,思路会顺很多。最后再分享一个我自己的习惯:别只背图,用wireshark抓一次本机的三次握手,把序号、ACK号、窗口字段一行行看过去,很多直觉都是抓包抓出来的。