“计算机网络第四次实验”这个题目,几乎每年期中前后都会被反复搜一遍。原因很简单:前三次实验多半是认识网线、配置 IP、看看 ping 和 tracert 的输出,属于“看得见摸得着”的层次;到第四次,课程往往突然跳到socket 编程 + 抓包验证,要求学生自己写一个能跑通的客户端/服务端,然后在抓包工具里把三次握手、数据传输、四次挥手一条一条指出来。跨度大、代码和协议细节同时压上来,多数人第一次做的时候是懵的。
这篇东西就是把我自己带实验、帮同学看报错、以及后来反复重构这套代码时踩过的东西整理出来。它不针对某一本教材的某一版实验指导书(各校差异很大),而是把那类“写一个 TCP/UDP 通信程序,并抓包分析”的实验拆成可复现的步骤:从题型判断、环境准备、协议选型、粘包处理、代码骨架,一直到抓包怎么看、报错怎么排、报告怎么写才不被扣分。刚接触 socket 的同学可以从第 2 节开始照着做,已经写完但跑不通的,直接跳到第 6 节的排查表更快。
1. 先搞清楚“第四次实验”到底在考什么
1.1 三类典型题型与快速判断方法
不同学校的第四次实验差别很大,但基本落在三个筐里。拿到任务书,先别急着开编辑器,花三分钟判断它属于哪一类,后面的路会顺很多。
| 题型 | 典型任务描述关键词 | 核心考察点 | 工作量 |
|---|---|---|---|
| 套接字编程型 | “实现一个客户端/服务端”“支持多客户端”“回显服务器” | socket API 生命周期、阻塞与非阻塞、并发模型 | 中到大 |
| 协议分析型 | “抓取并分析三次握手”“统计 HTTP 报文”“对比 TCP 与 UDP” | 报文结构、字段含义、时序理解 | 中 |
| 网络配置型 | “配置静态路由”“划分子网”“实现 VLAN 互通” | 地址规划、路由表、转发路径 | 小到中 |
判断依据其实就一句话:任务书里有没有出现“编写程序”。出现了,就是编程型,抓包只是配套的验证手段;没出现但要求交截图和字段说明,那就是分析型;如果给了一张拓扑图加一堆网段地址,那基本是配置型。我见过不少同学把分析型实验当成编程型来做,硬写了一个客户端去“制造流量”,结果报告里该分析的握手字段一个没写,分数自然不好看。
还有一种混合型:要求先写程序跑通通信,再用抓包证明“我这段代码确实走的是 TCP”。这种是最常见的形态,也是本文默认的主线。它的评分点通常被拆成三块:程序能跑(40%)、抓包截图能对应上代码行为(30%)、分析文字说清楚为什么(30%)。很多人前两项满分,最后一项扣光,非常可惜。
1.2 为什么大多数课程把 socket 编程放在第四次
这个安排不是随意的。前三次课建立的是“网络是一个黑盒,我给地址它送货”的直觉,第四次要把黑盒打开,让你亲手扮演一次“发货方”。而 socket 恰好是这套抽象里最薄的一层膜:它把传输层的 TCP/UDP 能力包装成几个函数,你调用connect,协议栈就替你去发 SYN;你调用send,它就替你分段、编号、等确认。
理解了这层对应关系,实验就不只是“调 API”。你会第一次意识到,代码里一行connect()背后是网络上真实往返的一个 RTT,而close()之后那个看似已经结束的连接,可能还要在 TIME_WAIT 状态待上 60 秒。这种“代码行为—网络行为”的映射能力,才是第四次实验真正想训练的东西,也是后面做课程设计、做后端开发时最吃香的部分。
顺带说一句,如果你以后要面后端岗位,这套东西的问法几乎是固定的:粘包怎么处理、TIME_WAIT 多了怎么办、为什么握手是三次而不是两次。第四次实验其实是一次免费的面试预演,认真做一遍比背八股强得多。
2. 实验前的环境与工具准备
2.1 环境清单与版本选择
这部分看起来琐碎,但至少三成的“跑不通”都出在环境上。我的建议是把环境压到最简:一门带标准库的语言 + 一个抓包工具 + 一个能看端口占用的命令行工具,不要引入任何第三方网络框架。
- 语言运行时:优先选标准库自带 socket 支持的语言。我习惯用 Python,因为
socket模块就是把 BSD socket 一对一映射过来,函数名和教材上的socket/bind/listen/accept/connect/send/recv完全对得上,写报告时可以直接引用教材术语。用 Java 也可以,但ServerSocket的封装层厚一点,讲“backlog 是什么”时容易讲不清。 - 抓包工具:图形化那一款足够,装的时候记得勾选抓包驱动,否则网卡列表是空的。命令行版本更轻,适合在服务器上跑,但初学者看时序图还是图形化直观。
- 端口排查工具:Linux 下用
ss -lntp,Windows 下用netstat -ano | findstr :端口号。这两个命令能在十秒内告诉你“端口到底被谁占了”,比反复重启程序高效得多。 - 编辑器:随便,但务必把缩进设成 4 空格,Python 混用 Tab 和空格会在某个莫名其妙的行报
IndentationError,排查起来很浪费时间。
注意:服务端和客户端如果在同一台机器上跑,地址要写
127.0.0.1;如果要让同宿舍/同实验室的另一台机器连进来,服务端必须绑定0.0.0.0,客户端填服务端的实际内网地址。绑定127.0.0.1是“只有本机能连”,这是新手最常见的坑之一。
2.2 抓包位置与过滤器的选择逻辑
抓包前先想清楚一件事:你要抓的是哪段流量。同一台机器上跑客户端和服务端,流量走的是回环接口,抓包工具默认监听的是物理网卡,所以你会看到一片空白——这跟代码没有任何关系。正确做法是选中回环接口,或者干脆用两台机器/一台虚拟机来跑。
过滤器建议分两层。第一层是抓包过滤器,在开始抓之前设,直接决定哪些包会被记录,写得太宽会抓出几万个包,文件大到打不开;第二层是显示过滤器,在抓完之后设,只影响展示。初学者的错误是把两者搞混,在显示过滤器里写了host 127.0.0.1 and port 9000这样的组合却抱怨抓不到东西,其实是抓包过滤器把流量丢了。
我的习惯是:抓包过滤器留空(全抓),靠显示过滤器收窄。因为实验往往需要多轮尝试,万一第一轮过滤条件写错,全抓的话还能救回来,只抓特定条件的话就得重跑一遍。
3. 整体设计思路拆解:从需求到代码骨架
3.1 协议选型:TCP 还是 UDP,别凭感觉选
任务书如果写了“回显服务器”“文件传输”“聊天”,那就默认 TCP;如果写了“测速”“广播”“实时性优先”“允许丢包”,那多半是 UDP。但真正的考点不在选哪个,而在你能不能说出为什么。
TCP 提供的是字节流:它保证顺序、保证到达、自动重传,但对应用层来说,它是一根没有边界的水管,你灌进去 100 字节,对面可能分三次读到 30、30、40,也可能一次读到 100。UDP 提供的是数据报:每次sendto就是一个完整报文,对面recvfrom一次就是一次,边界天然保留,但可能丢、可能乱序。
用一个生活类比:TCP 像打电话,你说的每句话都会按顺序传到对方耳朵里,但对方是连着听的,不会给你标点;UDP 像寄明信片,每张都是独立的,收到几张算几张,可能路上丢了一张。
实验里最典型的现象是:TCP 版本写着写着发现“我发了两条消息,对面只收到一条”,于是怀疑代码有 bug——其实不是,是两次send被合并成了一个报文段。这个问题在 UDP 上永远不会出现,反过来 UDP 上出现的“丢包”在 TCP 上也不会出现。选型选对了,后面一半的困惑自动消失。
3.2 服务端模型:迭代、多线程还是 IO 多路复用
三种模型对应三种复杂度,实验里选哪个要看任务书是否要求“支持多客户端”。
迭代式最简单:accept一个连接,处理完,close,再accept下一个。缺点是同一时刻只能服务一个客户端,如果第一个客户端连上后不说话,整个服务端就卡死在那儿。这在实验演示时很容易翻车——你开两个终端测试,第二个连上去毫无反应,其实是第一个还占着。
多线程是最常见的作业答案:主线程accept,每来一个连接就threading.Thread(target=handle, args=(conn, addr)).start()。优点是不用改代码结构,缺点是线程数没有上限,实验环境无所谓,生产环境不行。
IO 多路复用(select/poll/epoll)单线程处理所有连接,效率高但代码绕。实验里除非明确要求,不建议上,因为你会在set的增删上花掉两小时,而评分点根本不在这儿。
我的建议:先用迭代式把功能跑通,确认协议逻辑没问题,再加三行代码改成多线程。这样出问题时你能确定是“并发引入的”还是“协议本身就有 bug”,排查范围小得多。
3.3 报文边界设计:粘包与半包到底怎么解
这是第四次实验里最容易丢分、也最容易在答辩时被追问的点。TCP 是字节流,没有消息边界,所以你必须自己在应用层画一条线。三种常见做法:
- 定长报文:每条消息固定 128 字节,不够的补空格。实现最简单,但浪费带宽,且限制消息长度。
- 分隔符:用
\n或\r\n结尾,读到分隔符算一条。HTTP 的头部就是这么干的。缺点是如果消息内容本身可能包含分隔符,需要转义。 - 长度前缀:先发 4 字节表示“后面还有多少字节”,再发正文。这是最通用、最不易出错的方式,文件传输、RPC 框架基本都用它。
我一般推荐长度前缀,因为它的读取逻辑是确定的:先循环读满 4 字节,解析出长度 N,再循环读满 N 字节。整个过程不需要猜,也不怕内容里有特殊字符。
这里有个关键细节:recv不保证一次读满。很多人写成length = int.from_bytes(conn.recv(4), "big"),测试时在本机跑得好好的,一换到跨机器就崩,因为那次recv只返回了 2 字节。必须写一个recv_exact(n)循环读取,不足就继续读,读到b""说明对端关了。
4. 核心代码实现与逐行要点
4.1 服务端骨架:从 bind 到 accept 的每一步在做什么
下面这段是带长度前缀的 TCP 回显服务端,我把它当作标准模板,改改就能应付大部分实验要求。
import socket import threading HOST = "0.0.0.0" PORT = 9000 BACKLOG = 5 def recv_exact(conn, n): buf = b"" while len(buf) < n: chunk = conn.recv(n - len(buf)) if not chunk: raise ConnectionError("对端已关闭") buf += chunk return buf def handle(conn, addr): with conn: conn.settimeout(10) print(f"[+] 来自 {addr} 的连接") try: while True: head = recv_exact(conn, 4) length = int.from_bytes(head, "big") if length == 0 or length > 1 << 20: break payload = recv_exact(conn, length) print(f"[>] 收到 {length} 字节: {payload[:32]!r}") resp = payload.upper() conn.sendall(len(resp).to_bytes(4, "big") + resp) except (ConnectionError, socket.timeout): pass print(f"[-] {addr} 断开") def main(): srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((HOST, PORT)) srv.listen(BACKLOG) print(f"监听 {HOST}:{PORT}") while True: conn, addr = srv.accept() threading.Thread(target=handle, args=(conn, addr), daemon=True).start() if __name__ == "__main__": main()逐段说几个非显然的点。
socket(AF_INET, SOCK_STREAM)里的SOCK_STREAM就是“我要 TCP”,换成SOCK_DGRAM就是 UDP。这两个常量的名字比1/2好记,报告里写全名也显得清楚。
SO_REUSEADDR这一行是保命的。没有它,你改完代码重启服务端,很可能报OSError: [Errno 98] Address already in use。原因是你上次跑的时候服务端先关了连接,本地进入了 TIME_WAIT 状态,端口还被内核占着。SO_REUSEADDR允许复用处于 TIME_WAIT 的端口,本质上是告诉内核“我知道有旧连接残留,别拦我”。这个选项必须在bind之前调用,顺序错了没效果。
listen(BACKLOG)的 backlog 不是“最大连接数”,而是已完成三次握手但还没被 accept 取走的连接队列长度。写 5 意味着如果有 6 个客户端同时连上来,第 6 个可能被拒绝或重传。实验里写 5 到 128 都行,写 1 反而容易出玄学问题。
accept()返回的是一个新的 socket,它和监听 socket 是两个不同的描述符。监听 socket 继续监听,新 socket 负责这条具体连接。初学者常把两者搞混,比如在handle里误用了srv.recv,结果报错。
conn.settimeout(10)是防止某条连接卡死占着线程。但要注意,超时抛出的是socket.timeout,它继承自OSError,捕获时要写对,否则线程会带着异常退出,日志里一片红。
4.2 客户端骨架与“发完就关”的陷阱
import socket def recv_exact(s, n): buf = b"" while len(buf) < n: chunk = s.recv(n - len(buf)) if not chunk: raise ConnectionError("对端已关闭") buf += chunk return buf with socket.create_connection(("127.0.0.1", 9000), timeout=5) as s: for text in ["hello", "network lab", "x" * 3000]: data = text.encode() s.sendall(len(data).to_bytes(4, "big") + data) head = recv_exact(s, 4) body = recv_exact(s, int.from_bytes(head, "big")) print(body.decode()[:40])create_connection是socket()+connect()的语法糖,还自带超时,比手写两行清爽。sendall和send的区别必须记牢:send返回实际发出的字节数,可能小于你要发的长度,你得自己循环;sendall内部帮你循环到发完为止。实验里除了刻意演示“部分发送”,一律用sendall。
最容易踩的坑在with块结束时。with会自动close(),而close()在接收缓冲区还有未读数据的情况下,内核会发一个 RST 而不是正常的 FIN。表现就是服务端抓包看到连接被“暴力掐断”,或者客户端明明收到了响应,服务端日志却报ConnectionResetError。这个现象的根源是:TCP 的优雅关闭需要双方都读完对方的数据,你单方面关掉还留着垃圾没读,内核只能用 RST 通知对面。
规避办法有两个:一是确保协议是严格的一问一答,客户端读完响应再关;二是显式地先shutdown(socket.SHUT_WR)发 FIN,再循环读到b"",最后close()。第二种更规范,答辩时说出来很加分。
4.3 UDP 版本的关键差异
UDP 版本代码短很多,但语义完全不同,别把 TCP 的习惯直接搬过来。
import socket srv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) srv.bind(("0.0.0.0", 9001)) while True: data, addr = srv.recvfrom(2048) print(f"[>] {addr} 发来 {len(data)} 字节") srv.sendto(data.upper(), addr)没有listen,没有accept,一个 socket 直接收发所有客户端的数据,天然支持并发。recvfrom返回的addr必须保存下来,回复时sendto要带上它,否则你不知道回给谁。
这里有个参数值得算一下:recvfrom(2048)里的 2048 是单次能接收的最大字节数。如果对端发来 3000 字节的报文,你只会收到前 2048 字节,剩下的直接被丢弃,不会像 TCP 那样留在缓冲区等你下次读。原因是 UDP 报文在接收缓冲区里是一个完整单元,你给的缓冲区装不下,内核就截断了。
所以 UDP 实验里发送端要控制单包大小。以太网 MTU 是 1500 字节,减去 IP 头 20 字节、UDP 头 8 字节,有效载荷上限是 1472 字节。超过这个值,IP 层会分片,任何一片丢失整个报文就废了。实验里如果想演示“大包会分片”,可以故意发 2000 字节,然后在抓包里看Fragmented IP protocol的提示,这是个很好的报告素材。
4.4 几个参数的计算与取舍
写报告时,凡是出现数字的地方,最好都能说出怎么来的。
| 参数 | 常用取值 | 计算/依据 |
|---|---|---|
| 单条消息最大长度 | 1 MB | 超过就断开,防止恶意长度导致内存爆掉 |
| 长度前缀字节数 | 4 字节 | 最大可表示 4 GB,2 字节只能到 64 KB,不够用 |
recv缓冲区 | 4096 字节 | 与常见内存页大小对齐,且远小于内核默认接收缓冲区 |
| UDP 单包上限 | 1472 字节 | 1500 (MTU) − 20 (IP 头) − 8 (UDP 头) |
| TCP MSS | 1460 字节 | 1500 − 20 − 20 (TCP 头),协商时由双方取小 |
| TIME_WAIT 时长 | 60 秒 | 2 × MSL,Linux 下 MSL 默认 30 秒 |
MSS 这个值特别值得在报告里提一句:它是三次握手时就协商好的,双方在 SYN 和 SYN-ACK 里各带一个 MSS 选项,最终取较小的那个。所以你在代码里sendall一个 3000 字节的缓冲区,实际在网络上会被切成 3 个报文段(1460 + 1460 + 80)。抓包时如果看到这个切分,把截图和分析写上,基本就是这一节的满分答案。
5. 抓包验证:把三次握手“看见”
5.1 过滤表达式与抓包时机的把握
代码跑通只是第一步,实验真正的分数在看不懂抓包上。先给几条我常用的显示过滤器,照着改端口号即可。
| 目的 | 过滤器写法 | 说明 |
|---|---|---|
| 只看这条连接的流量 | tcp.port == 9000 | 端口换成你代码里的 |
| 只看握手包 | tcp.flags.syn == 1 | SYN 和 SYN-ACK 都会命中 |
| 只看挥手包 | tcp.flags.fin == 1 or tcp.flags.reset == 1 | FIN 与 RST 一起看 |
| 只看有数据的包 | tcp.len > 0 | 握手包 len 为 0,会被过滤掉 |
| 只看重传 | tcp.analysis.retransmission | 排查“为什么这么慢”时用 |
| 追踪整条流 | 右键 → 追踪 TCP 流 | 把双向数据按连接拼起来看 |
抓包时机很关键。先开抓包,再跑程序,这个顺序反了就抓不到握手。因为三次握手发生在connect()那一瞬间,等你看到程序打印出“连接成功”再去点开始,握手早就结束了。同理,客户端close()之后要立刻停抓包,否则 TIME_WAIT 期间不会有新包,你会以为抓漏了。
5.2 三个报文的关键字段怎么读
正常握手的三个包,我用一次实测的数据说明(序号是相对的,工具默认会把第一个 SYN 的序号显示为 0,方便阅读)。
第一个包:客户端 → 服务端,标志位SYN,相对序号Seq=0,Ack不显示。选项里能看到Maximum segment size: 1460、SACK permitted、Window scale: 7。这个包的作用是“我想连你,我的初始序号是 X,我最大能收 1460 字节的段”。
第二个包:服务端 → 客户端,标志位SYN, ACK,Seq=0(这是服务端自己的初始序号),Ack=1(等于对方的 Seq + 1)。注意Ack=1的含义不是“我收到了 1 个字节”,而是“我期待你下一个字节的序号是 1”,因为 SYN 本身占一个序号。
第三个包:客户端 → 服务端,标志位ACK,Seq=1,Ack=1。到这里双方都确认了对方的收信能力,连接建立。
一个特别容易被追问的点:为什么是三次而不是两次。两次的话,服务端发完 SYN-ACK 就认为连上了,但如果客户端根本没收到这个包(或者这是个网络里滞留的旧 SYN),服务端就会白白维持一个连接,资源被占着。第三次 ACK 的作用是让服务端确认“客户端确实收到了我的 SYN-ACK,而且它现在真的想连”。报告里能写到这一层,说明你不是抄的。
挥手是四个包(有时合并成三个),因为 TCP 是全双工,两个方向要各自关闭。主动关闭方发 FIN,被动方回 ACK,此时是半关闭状态:主动方不能再发数据,但仍可以接收。等被动方也把数据发完,它再发一个 FIN,主动方回 ACK,连接彻底关闭。主动关闭的那一方会进入 TIME_WAIT,等 2MSL 才释放端口。
5.3 抓包里那些“不正常”的现象怎么解读
实际抓包很少像教材那样干净。以下几种现象出现频率极高,看懂它们比背理论有用。
RST 包:红色背景,标志位只有 RST。含义是“这条连接我单方面不认了”。常见成因有三个:连了一个没监听的端口;程序close()时接收缓冲区还有未读数据;服务端进程被强杀。
Retransmission(重传):同一个序号的数据被发了两次。轻则是本机抓包时的偶发现象(网卡卸载功能导致,可以忽略),重则说明真的丢包了。判断方法看时间间隔:几百毫秒内连续重传是快速重传,秒级递增(1s、2s、4s)是超时重传。
Zero Window:某一方通告的窗口大小为 0,意思是“我缓冲区满了,你先别发”。接着你通常会看到一个Window Update把窗口恢复。这个现象在自己写“发 100 MB 文件”的实验里必现,是解释“为什么传输会卡一下”的最佳素材,也是讲流量控制的活教材。
Connection reset by peer 但抓包看不到 RST:多半是中间有防火墙或安全软件在拦,不是代码问题。换个端口(比如从 80 换到 9000)往往就好了。
6. 常见问题与排查实录
6.1 一张能救命的速查表
下面这些报错,我在帮人看代码时几乎每周都会遇到一次。按现象查,比从头读代码快十倍。
| 现象/报错 | 最可能的原因 | 处理办法 |
|---|---|---|
ConnectionRefusedError | 服务端没启动、端口写错、被防火墙拦 | 先用ss -lntp确认端口在监听,再核对两端端口号 |
Address already in use | 上次进程没退干净,端口在 TIME_WAIT | 加SO_REUSEADDR;或换端口;或ss -lntp找到进程 kill 掉 |
ConnectionResetError | 对端close()时还有未读数据 | 收发严格配对;或先shutdown(SHUT_WR)再读到空 |
| 客户端连上但收不到回复 | 服务端绑了127.0.0.1,跨机连不上 | 改绑0.0.0.0 |
| 两条消息被合成一条 | TCP 字节流特性,正常现象 | 加长度前缀或分隔符,不要依赖send次数 |
recv卡住不返回 | 对端没发数据,或长度前缀和实际长度不一致 | 打印每次 recv 的字节数,核对协议两端是否对称 |
| 大文件传到一半卡死 | 接收端读得慢,窗口降到 0 | 接收端及时读,或分块发送 |
| 抓包列表一片空白 | 网卡选错,回环流量走了 loopback | 换回环接口,或换两台机器测 |
| 中文乱码 | 两端编码不一致 | 统一utf-8,并在长度前缀里按字节数算,不是字符数 |
| 程序占用 CPU 100% | 循环里recv返回空没退出,死循环 | 判断if not chunk: break |
6.2 几条只有踩过才知道的经验
第一条,先在本机跑通,再跨机。本机127.0.0.1不经过物理网卡,能排除掉防火墙、路由、IP 配错这三类干扰。本机通了再换地址,出问题就只剩网络环境一个变量。
第二条,日志打全。很多人只在成功时打印,出问题两眼一抹黑。我的习惯是在accept、recv返回、send完成、close这四个位置都打一行日志,带上字节数和地址。数据量不大,但排查粘包问题时,这四行日志能直接告诉你“对面是一次发了 12 字节还是分两次发了 5 和 7”。
第三条,不要用端口号当变量到处改。实验里我见过一个同学把端口写死在六个地方,改一次漏一处,调了四十分钟。统一提到模块顶部的常量,两端都引用它。
第四条,中文按字节算长度。一个汉字在 UTF-8 下是 3 字节,如果你用len(text)算长度前缀,中文就会错位。必须用len(text.encode("utf-8")),发送时也发encode之后的字节串。这个 bug 只在中英混排时暴露,测试用例一定要带中文。
第五条,善用抓包的“追踪流”功能。它会把一条连接的收发数据按顺序拼成一个可读文本,粘包问题在这里暴露得一清二楚:你会看到两条逻辑消息粘在一个 TCP 段里,然后你就明白为什么必须自己加边界了。截图贴进报告,比写三百字解释更有说服力。
7. 实验报告怎么写才不丢分
7.1 一份能拿高分的结构模板
代码对了报告拉胯,是第四次实验最常见的失分方式。我的建议是严格按“动机—设计—证据—反思”四段式来写,每一段都对应一个评分点。
实验目的别照抄任务书,用自己的话写一句“我想验证什么”。比如“验证 TCP 的字节流特性会导致消息边界丢失,因此需要在应用层设计长度前缀”。这句话一出来,老师就知道你不是照搬的。
设计部分放一张表,列出你选的协议、并发模型、报文格式、参数取值,每个参数附一句为什么这么选。这比大段文字好读,也显得思路清晰。
证据部分是截图 + 字段标注。每张截图下面写三行:这张图对应代码的哪一行、关键字段的值是多少、这个值说明了什么。只贴图不写分析,基本拿不到分。
反思部分写你遇到的问题和解法。哪怕问题很小,比如“端口被占用”,只要写清楚现象、排查过程、根因,就是加分项,因为它证明你真的动手了。
7.2 截图和数据记录的几个细节
截图要带时间列和序号列,否则老师没法判断包的先后顺序。关键字段用箭头或框标出来,别指望老师自己在密密麻麻的十六进制里找Seq=1。
如果要求记录握手参数,做一张这样的表更清楚:
| 报文 | 方向 | 标志位 | 相对 Seq | 相对 Ack | 窗口 | 备注 |
|---|---|---|---|---|---|---|
| 1 | C→S | SYN | 0 | — | 64240 | 携带 MSS=1460 |
| 2 | S→C | SYN,ACK | 0 | 1 | 65160 | 窗口缩放 7 |
| 3 | C→S | ACK | 1 | 1 | 64240 | 连接建立 |
窗口值别照抄我的,一定要填你自己抓到的实际数值,不同系统默认值差很多。老师一眼就能看出你是不是抄的。
最后提一句,如果实验允许附加内容,可以把“如果改成 UDP 会怎样”写成一个对比小节:把同样的回显功能用 UDP 实现一遍,抓包对比报文数量(UDP 没有握手,2 个包就完成了 TCP 需要 3 个包加 4 个包才做完的事),再补一句“代价是没有确认和重传,丢包就真的丢了”。我自己做这组对比的时候才真正意识到,TCP 那些看起来啰嗦的机制,换来的正是应用层不用自己操心可靠性。这个体会写进报告,比任何理论复述都值钱。