简介:这份PDF资料面向准备技术面试的开发者与计算机专业学生,系统梳理计算机网络核心考点,帮助读者在面试与工程实践中理清网络通信的底层逻辑。内容围绕网络模型、TCP/IP协议族及协议细节展开,涵盖OSI七层参考模型与TCP/IP四层模型的对比、TCP三次握手与四次挥手、状态机与TIME_WAIT、超时重传与快速重传、流量控制与拥塞控制,以及IPv4/IPv6、ICMP、ARP、IGMP等网络层协议,并配有TCP Header结构与协议栈报文格式举例。资源包共1个PDF文件,约2.08MB,目录按知识点分层组织,便于按模块检索与快速复习。目前已有969人学习,适合需要系统梳理网络知识、查漏补缺或冲刺面试的读者参考。
1. 计算机网络基础知识:从一次「连接超时」说起
线上服务报connect timeout,你 SSH 上去ping网关通、curl外部地址也通,唯独业务端口连不上。这时候如果只会重启服务,问题大概率还会复现。真正能定位问题的,是脑子里那张分层图:物理链路、IP 路由、TCP 握手、应用协议,每一层都有独立的排查手段。计算机网络基础知识不是考试背诵题,它是你面对bind: only one usage of each socket address、read udp: unknown error、iperf3打流异常这些真实报错时的诊断地图。
这篇笔记面向三类人:刚学完 OSI 七层模型但不知道怎么用到排障上的新手、写 C# UDP 编程或 Python socket 时被参数绕晕的开发者、以及需要给团队讲清楚 TCP/IP 四层模型各层职责的工程师。我会先把 OSI 与 TCP/IP 的对应关系立住,再落到三次握手、四次挥手、UDP 打流这些能动手复现的操作,最后把踩过的坑摊开讲。读完你应该能独立完成一次端到端的 TCP/UDP 发包收包测试,并看懂抓包结果里每一层在说什么。
2. OSI 七层与 TCP/IP 四层:模型怎么对应到真实协议栈
2.1 两张模型图不是二选一,而是粗细不同的刻度尺
很多人背 OSI 七层模型各层功能时觉得和实际对不上,原因是 OSI 是教学参考模型,TCP/IP 四层模型才是工程落地的那把尺子。它们的关系不是替代,而是粒度差异:OSI 把「会话」和「表示」单独拆出来,TCP/IP 把它们合并进应用层,因为实际协议栈里这两件事往往由应用自己处理。
自上而下看 TCP/IP 四层模型,每层核心工作可以这样记:
| 层级 | 核心工作 | 典型协议 | 数据单元 | 排障关注点 |
|---|---|---|---|---|
| 应用层 | 定义报文语义与交互流程 | HTTP、DNS、Modbus TCP、MQTT | 报文 Message | 端口、请求格式、超时设置 |
| 传输层 | 端到端可靠性或实时性 | TCP、UDP | 段 Segment / 数据报 Datagram | 握手、重传、窗口、丢包 |
| 网络层 | 寻址与路由转发 | IP、ICMP、ARP | 包 Packet | 路由表、MTU、分片 |
| 网络接口层 | 物理传输与帧封装 | Ethernet、Wi-Fi | 帧 Frame | 网卡、链路状态、CRC 校验 |
OSI 七层模型自上而下分别是应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。其中表示层管编码压缩加密,会话层管连接建立维持,这两层在 TCP/IP 里被应用层吸收。所以当你看到「OSI 七层模型各层功能」的资料时,重点记传输层和网络层的分工,其余作为理解辅助即可。
2.2 数据在 TCP/IP 模型中传输的完整过程
一次 HTTP 请求从浏览器发出到服务端收到,数据经历的封装过程是这样的:应用层生成 HTTP 报文,传输层加上 TCP 头(含源端口、目的端口、序号),网络层加上 IP 头(含源 IP、目的 IP),网络接口层加上以太网帧头帧尾(含 MAC 地址和 CRC 校验)。到达对端后逐层解封装,每层只关心自己那部分头部。
这个「封装—解封装」的过程解释了为什么排障要分层:ping通只证明网络层及以下正常,不代表 TCP 端口可达;telnet端口通只证明 TCP 握手成功,不代表应用层协议正确。常见做法是自下而上逐层验证,而不是一上来就怀疑业务代码。
2.3 用 tcpdump 观察分层封装的最小命令
理论讲完必须动手看一眼真实的分层结构。下面这条命令抓取本机与目标主机之间 80 端口的流量,-nn禁止域名和端口名解析,-v显示 IP 和 TCP 头部细节:
# 抓取与 192.168.1.100 之间 80 端口的 10 个包,显示详细头部 sudo tcpdump -i eth0 -nn -v host 192.168.1.100 and port 80 -c 10 # 输出中你会看到类似结构: # IP (tos 0x0, ttl 64, id 12345, offset 0, flags [DF], proto TCP (6), length 60) # 192.168.1.10.54321 > 192.168.1.100.80: Flags [S], seq 1000, win 64240逻辑说明:-i eth0指定网卡,多网卡机器必须写对,否则抓不到;host和port是 BPF 过滤表达式,组合使用能大幅减少噪音;Flags [S]表示这是一个 SYN 包,即三次握手的第一步。参数上,-c 10抓够 10 个包自动退出,避免刷屏;-w file.pcap可以把原始包存下来用 Wireshark 打开,适合事后分析。
看到Flags [S]后如果对端没有回[S.],说明 SYN 到达不了或对端没监听,问题在网络层或传输层,不在应用层。这就是分层模型的实际价值:它把「连不上」这个大问题切成四个可独立验证的小问题。
3. TCP 三次握手与四次挥手:连接建立和释放的每一步在做什么
3.1 三次握手为什么不是两次或四次
TCP 三次握手的目标是双方同步初始序号(ISN)并确认对方收发能力正常。第一次客户端发 SYN(seq=x),第二次服务端回 SYN+ACK(seq=y, ack=x+1),第三次客户端回 ACK(ack=y+1)。两次不够,因为服务端无法确认客户端能收到自己的包;四次多余,因为服务端的 SYN 和 ACK 可以合并成一个包。
用 Python 写一个最小客户端,配合 tcpdump 就能看到完整握手:
import socket # 创建 TCP socket,AF_INET 表示 IPv4,SOCK_STREAM 表示 TCP sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5) # 设置 5 秒超时,避免握手失败时无限等待 try: # connect 触发三次握手,成功返回说明握手完成 sock.connect(("192.168.1.100", 8080)) print("握手成功,本地端口:", sock.getsockname()[1]) sock.sendall(b"GET / HTTP/1.1\r\nHost: test\r\n\r\n") data = sock.recv(1024) print("收到:", data[:50]) except socket.timeout: print("握手超时,检查目标端口是否监听、防火墙是否放行") finally: sock.close() # close 触发四次挥手逻辑说明:connect()是阻塞调用,内部完成三次握手,返回即代表连接建立。settimeout(5)很关键,默认无超时会让程序在 SYN 无响应时卡死。参数上,AF_INET对应 IPv4,如果目标只有 IPv6 地址要改成AF_INET6;SOCK_STREAM是 TCP,换成SOCK_DGRAM就是 UDP。
3.2 四次挥手与 TIME_WAIT 的真实含义
连接释放需要四次挥手:主动关闭方发 FIN,对方回 ACK,对方数据发完再发 FIN,主动方回 ACK。之所以是四次,因为 TCP 是全双工,两个方向要各自关闭。主动关闭方最后进入 TIME_WAIT 状态,等待 2MSL(通常 60 秒)才真正释放。
TIME_WAIT 不是 bug,它保证最后一个 ACK 能到达对端,并让本次连接的迟到包在网络中消散。高并发短连接服务上大量 TIME_WAIT 会耗尽本地端口,常见做法是开启net.ipv4.tcp_tw_reuse让内核复用处于 TIME_WAIT 的端口,而不是简单调小tcp_fin_timeout。
# 查看当前 TIME_WAIT 连接数量 ss -tan state time-wait | wc -l # 查看监听队列溢出情况,Send-Q 持续大于 0 说明 accept 队列满 ss -lnt # 临时开启端口复用(需 root) sudo sysctl -w net.ipv4.tcp_tw_reuse=1参数说明:ss -tan中-t只看 TCP,-a显示所有状态,-n不解析服务名。state time-wait是过滤器。如果 TIME_WAIT 数量上万且持续增长,优先排查是不是客户端频繁短连接,而不是急着改内核参数。
3.3 用 iperf3 验证 TCP 吞吐与握手开销
iperf3是端到端 TCP/IP 发包收包测试的常用工具。服务端执行iperf3 -s,客户端执行:
# TCP 打流 10 秒,每 1 秒报告一次 iperf3 -c 192.168.1.100 -t 10 -i 1 # 单线程 vs 多线程对比,观察握手和窗口对吞吐的影响 iperf3 -c 192.168.1.100 -t 10 -P 4逻辑说明:-t 10持续 10 秒,-i 1每秒输出一次中间结果,-P 4开 4 条并行连接。如果单线程吞吐远低于多线程总和,说明瓶颈在单条 TCP 连接的窗口或 RTT,而不是带宽本身。这时候要查Retr列有没有重传,重传多说明链路丢包,需要从网络层找原因。
4. UDP 协议栈与打流:无连接场景下的分片和调试
4.1 UDP 为什么简单,以及简单带来的责任转移
UDP 协议栈只做两件事:加端口号、算校验和,然后交给 IP 层。它不握手、不重传、不排序、不控流。这意味着可靠性、顺序、去重全部由应用层自己负责。选 UDP 的典型场景是实时音视频、DNS 查询、Modbus UDP 这类「宁可丢一帧也不要卡顿」或「单次请求响应极短」的通信。
UDP 划分 IP 数据报片是必须理解的机制。当 UDP 数据报超过链路 MTU(以太网通常 1500 字节),IP 层会把它切成多个分片,每片独立传输,到达对端再重组。任何一片丢失,整个数据报就废了。所以 UDP 应用要主动控制单包大小,一般建议不超过 1400 字节,给 IP 和 UDP 头留出余量。
4.2 Python UDP 收发与丢包统计
下面这段代码发送 1000 个带序号的数据报,并在接收端统计丢包和乱序:
import socket # 发送端 sender = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for i in range(1000): # 每条消息带序号,方便接收端检测丢包和乱序 msg = f"seq:{i:04d}".encode() sender.sendto(msg, ("192.168.1.100", 9999)) sender.close() # 接收端(另开一个进程运行) recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) recv.bind(("0.0.0.0", 9999)) # 绑定所有网卡,端口 9999 recv.settimeout(3) received = [] while True: try: data, addr = recv.recvfrom(2048) # 缓冲区 2048 字节 received.append(int(data.decode().split(":")[1])) except socket.timeout: break # 统计 total = len(received) lost = 1000 - total print(f"收到 {total}, 丢失 {lost}, 丢包率 {lost/10:.1f}%")逻辑说明:sendto不保证送达,也不阻塞等待确认。接收端bind的地址用0.0.0.0表示监听所有网卡,如果只写127.0.0.1则收不到外部包。recvfrom的缓冲区参数要大于最大数据报,否则多余部分被截断且无提示。参数上,settimeout(3)用于在发送结束后自动退出接收循环,实际生产代码应该用序号连续性判断结束。
4.3 iperf3 UDP 打流与分片观察
# 服务端 iperf3 -s # 客户端 UDP 打流,带宽 10M,包长 1400 iperf3 -c 192.168.1.100 -u -b 10M -l 1400 -t 10 # 观察分片:把包长设成 3000,超过 MTU 会触发 IP 分片 iperf3 -c 192.168.1.100 -u -b 10M -l 3000 -t 10参数说明:-u切到 UDP 模式,-b 10M限制发送带宽,-l 1400指定每包负载长度。当-l 3000时,你会看到iperf3报告的丢包率明显上升,因为分片增加了丢失概率。用tcpdump -i eth0 -nn 'udp and port 5201'能直接看到frag标记的分片包。这就是「UDP 划分 IP 数据报片」在真实流量里的样子。
注意:UDP 打流时如果客户端报
read udp: unknown error (code=10054),在 Windows 上通常是对端端口未监听导致 ICMP 端口不可达,被系统转成了连接重置错误,不代表 UDP 本身有连接。
5. 排障避坑:那些让新手翻车的典型问题
5.1 端口占用报错却找不到进程
现象:启动服务报bind: only one usage of each socket address或error: listen tcp 127.0.0.1:11434: bind,但ps里看不到占用进程。
原因:端口被另一个用户或已退出的进程以 TIME_WAIT 状态占用,或者被系统保留端口范围覆盖。
解决:用ss -lntp | grep 端口号查监听进程,lsof -i :端口号查所有引用。如果是 TIME_WAIT,等 60 秒或开启tcp_tw_reuse。如果是保留端口,换端口或调整net.ipv4.ip_local_port_range。
5.2 ping 通但端口连不上
现象:ping目标主机有响应,telnet目标端口超时。
原因:ICMP 和 TCP 走的是不同协议路径,中间防火墙可能放行 ICMP 但拦截 TCP;或者目标服务只监听了127.0.0.1而非0.0.0.0。
解决:先在目标机ss -lnt确认监听地址,127.0.0.1:8080表示只接受本机连接。再在中间节点用traceroute -T -p 端口确认 TCP 包能到哪一跳。最后检查防火墙规则是否放行该端口。
5.3 UDP 收不到包但发送无报错
现象:UDP 客户端sendto返回成功,接收端始终收不到。
原因:UDP 无连接,sendto成功只代表包交给了本机协议栈,不代表到达对端。常见原因是接收端bind了错误地址、防火墙拦截 UDP、或包被 NAT 设备丢弃。
解决:两端同时用tcpdump抓包,确认包是否离开发送端网卡、是否到达接收端网卡。如果发送端有、接收端没有,问题在中间网络;如果接收端有但应用收不到,检查bind地址和缓冲区大小。
5.4 抓包看到大量重传但带宽没跑满
现象:iperf3TCP 测试中Retr列数值很高,吞吐远低于预期。
原因:链路丢包触发 TCP 重传和拥塞窗口收缩,实际有效带宽被重传吃掉。
解决:先用ping -f或mtr确认丢包发生在哪一跳。如果是无线链路,检查信号强度和干扰;如果是有线链路,检查网线、光模块和交换机端口 CRC 错误计数。不要一上来就调 TCP 参数,物理层问题调参没用。
5.5 服务端 accept 队列溢出导致连接被拒
现象:高并发时部分客户端连接超时,服务端ss -lnt显示Send-Q持续大于 0。
原因:accept队列(全连接队列)满了,内核丢弃新完成握手的连接。
解决:调大net.core.somaxconn和应用的 backlog 参数,同时检查应用是否 accept 太慢。ss -lnt的Send-Q就是当前队列上限,Recv-Q是当前排队数,后者持续接近前者说明要扩容或优化处理逻辑。
6. 进阶:用 CRC 校验和抓包把「玄学丢包」变成可观测数据
前面讲的都是连接层和传输层,但真实排障里最耗时的往往是「数据看起来发了但内容不对」。这时候要下沉到数据链路层的 CRC 校验和抓包分析。CRC 是帧尾的 4 字节校验值,网卡收到帧后重新计算,不匹配就丢弃并计数。这个计数在ethtool -S eth0里能看到,字段名通常是rx_crc_errors。
# 查看网卡统计,重点关注 CRC 和丢包计数 ethtool -S eth0 | grep -E "crc|drop|error" # 持续抓包并只保存异常帧,配合 Wireshark 分析 tcpdump -i eth0 -nn -w capture.pcap -c 10000 # 用 tshark 统计 TCP 重传和乱序 tshark -r capture.pcap -q -z io,stat,1,"COUNT(tcp.analysis.retransmission)tcp.analysis.retransmission"逻辑说明:ethtool -S输出的是网卡硬件计数器,rx_crc_errors持续增长说明物理链路有信号完整性问题,换网线或光模块比调任何软件参数都有效。tshark的io,stat能按秒统计重传次数,把「偶尔卡一下」变成可量化的曲线。
一个我反复用到的技巧:把tcpdump抓到的包按「握手失败」「重传」「分片」三类过滤,分别统计数量。如果重传集中在某个时间段,对照那个时间的业务日志和系统负载,往往能定位到是突发流量打满了队列还是对端处理慢。这套方法比盯着ping的延迟数字有用得多,因为延迟是结果,重传和 CRC 才是原因。
我自己的习惯是:任何一次线上网络问题,先抓 30 秒包存下来,再动手改配置。没有抓包就改参数,等于闭着眼睛调玄学,改好了不知道为什么好,改坏了没有后悔药。把每次抓包和对应的ethtool计数存成基线,下次出问题一对比就知道是链路劣化还是流量突增。希望帮到你。
本文还有配套的精品资源,点击获取