面试 | 计算机网络

文章目录

计算机网络面试完全指南(Go 校招向)


一、网络模型

OSI 七层 vs TCP/IP 四层

复制代码
OSI 七层模型              TCP/IP 四层模型       代表协议
┌─────────────┐          ┌──────────────┐
│   应用层     │          │              │   HTTP/HTTPS/FTP/DNS
│   表示层     │    ──▶   │   应用层      │   SMTP/WebSocket/gRPC
│   会话层     │          │              │
├─────────────┤          ├──────────────┤
│   传输层     │    ──▶   │   传输层      │   TCP / UDP
├─────────────┤          ├──────────────┤
│   网络层     │    ──▶   │   网络层      │   IP / ICMP / ARP
├─────────────┤          ├──────────────┤
│   数据链路层  │          │              │   Ethernet / WiFi
│   物理层     │    ──▶   │   网络接口层  │   光纤 / 双绞线
└─────────────┘          └──────────────┘

每层的核心职责

职责 关键词
应用层 面向用户的协议,定义数据格式 HTTP、DNS、协议
传输层 端到端通信,区分进程(端口号) TCP/UDP、端口、可靠传输
网络层 主机到主机,跨网络路由 IP 地址、路由、TTL
数据链路层 同一局域网内节点到节点 MAC 地址、以太网帧
物理层 比特流的物理传输 光纤、电信号

面试答法:每层只关心本层的事,通过封装/解封装向上/向下交互。发送数据时每层加上本层头部(封装),接收时逐层去掉头部(解封装)。分层的好处是各层独立演化,比如 HTTP/2 替换 HTTP/1.1 不影响 TCP 层。


二、TCP 协议(面试重中之重)

TCP 报文头部

复制代码
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
├─────────────────────────┬─────────────────────────────────────┤
│       源端口(16位)      │           目标端口(16位)           │
├─────────────────────────┴─────────────────────────────────────┤
│                      序列号(Seq,32位)                        │
├───────────────────────────────────────────────────────────────┤
│                      确认号(Ack,32位)                        │
├──────────┬─────────────┬────┬────┬────┬────┬────┬────┬───────┤
│ 数据偏移  │   保留位     │URG │ACK │PSH │RST │SYN │FIN │ 窗口  │
├──────────┴─────────────┴────┴────┴────┴────┴────┴────┴───────┤
│        校验和           │           紧急指针                    │
└──────────────────────────────────────────────────────────────┘

重要标志位:
  SYN:建立连接请求
  ACK:确认(Ack号有效)
  FIN:请求断开连接
  RST:强制重置连接(连接异常时)
  PSH:提示接收方立即上交数据给应用层

重要字段:
  序列号(Seq):本次发送的第一个字节的编号
  确认号(Ack):期望收到对方下一个字节的编号(已收到 Ack-1)
  窗口大小:告诉对方自己的接收缓冲区还有多少空间(流量控制)

三次握手(建立连接)

复制代码
Client                              Server
  │                                   │
  │──── SYN(seq=x) ────────────────▶│  Server: SYN_RCVD
  │                                   │
  │◀─── SYN+ACK(seq=y, ack=x+1) ────│
  │                                   │
  │──── ACK(ack=y+1) ──────────────▶│  Server: ESTABLISHED
  │                                   │
ESTABLISHED                        ESTABLISHED

为什么是三次,不是两次或四次

  • 两次不够:服务端发出 SYN+ACK 后不知道客户端是否收到(单向确认),无法确认双方的发送和接收能力都正常;且历史失效的连接请求可能让服务端白白分配资源
  • 四次多余:服务端的 SYN 和 ACK 可以合并发送,没必要拆开
  • 三次刚好:双方都能确认自己的发和收是正常的

SYN Flood 攻击:攻击者发大量 SYN 包但不回 ACK,导致服务端半连接队列(SYN Queue)满,无法接受新连接。

  • 防御:SYN Cookie(不占用半连接队列资源,用加密 Cookie 验证合法性)

四次挥手(断开连接)

复制代码
Client(主动关闭)                    Server(被动关闭)
  │                                   │
  │──── FIN(seq=u) ────────────────▶│  Server: CLOSE_WAIT
  │                                   │(服务端可能还有数据要发)
  │◀─── ACK(ack=u+1) ───────────────│
  │                    (等待服务端数据发完)
  │◀─── FIN(seq=v) ─────────────────│  Server: LAST_ACK
  │                                   │
  │──── ACK(ack=v+1) ──────────────▶│  Server: CLOSED
  │                                   │
TIME_WAIT(等 2MSL)
  │
CLOSED

为什么四次(不能合并成三次)

TCP 是全双工的,关闭是单向的。客户端发 FIN 只表示"我不再发数据了",但服务端可能还有数据要发给客户端,所以服务端的 ACK 和 FIN 不能合并(中间有时间差)。

TIME_WAIT 为什么等 2MSL(最大报文存活时间)

  1. 确保最后一个 ACK 能到达服务端。若服务端没收到,会重发 FIN,客户端需要能接收并重发 ACK
  2. 让本次连接产生的所有报文在网络中消失,防止旧报文影响同四元组的新连接

TIME_WAIT 过多的影响与解决

bash 复制代码
# 大量 TIME_WAIT 会耗尽本地端口(默认 28232 个)
ss -s | grep TIME-WAIT

# 解决(/etc/sysctl.conf):
net.ipv4.tcp_tw_reuse = 1        # 允许复用 TIME_WAIT 的端口(客户端侧)
net.ipv4.tcp_fin_timeout = 30    # 缩短 FIN_WAIT2 等待时间

半连接队列与全连接队列(面试高频)

服务端 listen() 后,内核维护两个队列:

复制代码
客户端 SYN
    │
    ▼
半连接队列(SYN Queue)
  存放已收到 SYN、等待完成三次握手的连接
  服务端发出 SYN+ACK,等待客户端 ACK
    │
    │ 收到客户端 ACK,三次握手完成
    ▼
全连接队列(Accept Queue)
  存放已完成三次握手、等待 accept() 取走的连接
    │
    │ 应用层调用 accept()
    ▼
  连接交给应用程序处理

队列满了会怎样

  • 半连接队列满:丢弃新 SYN(或开启 SYN Cookie 绕过)
  • 全连接队列满:丢弃 ACK,客户端以为连接建立,服务端实际丢弃

相关内核参数

bash 复制代码
net.ipv4.tcp_max_syn_backlog = 1024   # 半连接队列大小
net.core.somaxconn = 128               # 全连接队列上限
# listen(fd, backlog) 中的 backlog 参数也影响全连接队列大小
# 实际大小 = min(backlog, somaxconn)

面试答法 :高并发服务需要同时调大 tcp_max_syn_backlogsomaxconn,并在代码的 listen() 调用中传入足够大的 backlog,否则连接会被静默丢弃,表现为客户端连接超时。


TCP Keep-Alive(连接保活)

HTTP Keep-Alive 是应用层的连接复用,TCP Keep-Alive 是传输层的连接探活机制,两者不同:

复制代码
TCP 连接建立后,双方长时间无数据交换
  → 中间路由器/防火墙可能悄悄关闭连接(NAT 超时)
  → 但两端不知道,以为连接还在

TCP Keep-Alive:连接空闲一段时间后,自动发探测包确认对端仍存活

相关参数:
  tcp_keepalive_time    = 7200s   # 空闲多久开始探测(默认 2 小时)
  tcp_keepalive_intvl   = 75s     # 探测间隔
  tcp_keepalive_probes  = 9       # 探测失败多少次认定连接断开

Go 中启用 TCP Keep-Alive

go 复制代码
// net.Dial 默认已开启 Keep-Alive,间隔 15s
conn, _ := net.Dial("tcp", "host:port")

// 手动控制
tcpConn := conn.(*net.TCPConn)
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(30 * time.Second)

可靠传输机制

确认与重传(ARQ)
复制代码
停止等待 ARQ:发一个包,等确认,再发下一个(效率低)

连续 ARQ(Go-Back-N):
  发送窗口内的包都发出去,等确认
  若包3丢失 → 重传包3及其后的所有包(Go-Back-N)

选择重传(Selective Repeat,TCP使用):
  发送窗口内的包都发出去
  若包3丢失 → 只重传包3,不影响已收到的包4/5/6

超时重传:发出数据包后启动定时器,超时未收到 ACK 则重传。

  • RTO(重传超时时间)动态计算:RTO = SRTT + 4 × RTTVAR(平滑往返时间 + 4倍时间波动)
  • 每次重传后 RTO 翻倍(指数退避),避免网络拥塞时雪上加霜

快速重传:收到 3 个重复 ACK 就立即重传,不等超时(更快)。

复制代码
发送: 1 2 3 4 5
收到: ACK1 ACK2 ACK2 ACK2 ACK2  ← 收到 3 个重复 ACK2,说明 3 丢了
→ 立即重传 3,不等 RTO 超时

流量控制(接收方控制发送方速度)

滑动窗口:接收方在 ACK 报文的窗口字段告知发送方自己的缓冲区剩余空间:

复制代码
接收方缓冲区: [已处理][ 已收到未处理 ][   空闲   ]
                                     ↑
                              rwnd = 空闲大小,放入 ACK 的 window 字段

发送方最多发送 min(cwnd, rwnd) 的数据(cwnd 是拥塞窗口)

当 rwnd = 0:发送方停止发送,启动零窗口探测定时器
            定期发 1 字节探测包,等接收方窗口恢复

拥塞控制(防止网络过载)

四个算法:

复制代码
cwnd(拥塞窗口)随时间变化:

cwnd
 │                 ssthresh
 │         ╱───────────────────────
 │        ╱
 │       ╱  ← 慢启动(指数增长)
 │      ╱─────────────────────────── ← 拥塞避免(线性增长)
 │     ╱
 │  ──╱
 └──────────────────────────────── 时间
    ↑               ↑
   开始            触发拥塞

慢启动(Slow Start)

  • 初始 cwnd = 1 MSS(最大报文段)
  • 每收到一个 ACK,cwnd += 1(指数增长:1→2→4→8...)
  • 到达 ssthresh(慢启动阈值)后转入拥塞避免

拥塞避免(Congestion Avoidance)

  • 每个 RTT,cwnd += 1 MSS(线性增长,更保守)

拥塞发生时

  • 超时触发(严重):ssthresh = cwnd/2,cwnd = 1,重新慢启动
  • 3个重复ACK(较轻,快速重传后):ssthresh = cwnd/2,cwnd = ssthresh,进入拥塞避免(TCP Reno)

TCP BBR(现代拥塞控制)

不依赖丢包信号,而是测量带宽和 RTT,主动探测网络最优发送速率,Google 提出,Linux 4.9+ 支持,延迟更低,吞吐更高。

面试答法 :拥塞控制和流量控制都是控制发送速率,但目的不同。流量控制防止发送方压垮接收方 缓冲区,由接收方通过 window 字段控制。拥塞控制防止发送方压垮网络,由发送方根据网络反馈(丢包/时延)自主调节 cwnd。实际发送量 = min(rwnd, cwnd)。


TCP 粘包与拆包

粘包 :多个小数据包被合并成一个大数据包发送(Nagle 算法)
拆包:一个大数据包被拆成多个小包

复制代码
应用层发送:  [Hello][World]
实际收到:   [HelloWorld](粘包)或 [Hel][loWorld](拆包)

解决方案:
  1. 固定长度消息
  2. 消息头包含长度字段(最常用):[4字节长度][消息体]
  3. 特殊分隔符(如 \r\n,适合文本协议)
  4. 使用有边界的协议(HTTP/gRPC 已处理好)

三、UDP 协议

UDP 报文头部

复制代码
┌────────────────────┬────────────────────┐
│      源端口(16位)  │     目标端口(16位) │
├────────────────────┼────────────────────┤
│      长度(16位)   │     校验和(16位)   │
└────────────────────┴────────────────────┘
│                数据                      │
└──────────────────────────────────────────┘

仅 8 字节头部,无连接建立、无确认、无重传、无流控、无拥塞控制。


TCP vs UDP

对比项 TCP UDP
连接 需要三次握手 无连接,直接发
可靠性 确认+重传,保证送达 不保证
顺序 保证有序 不保证
流量控制 有(滑动窗口)
拥塞控制
头部大小 20 字节起 8 字节
传输方式 字节流(无消息边界) 数据报(有消息边界)
速度 较慢
适用场景 HTTP、数据库、文件传输 DNS、视频直播、游戏、QUIC

UDP 适用场景分析

  • 实时性 > 可靠性:视频通话(丢一帧画面,无需重传,重传了也来不及播放)
  • 广播/多播:UDP 天然支持一对多
  • 简单请求响应:DNS 查询(一问一答,丢了重问就行)
  • QUIC 协议:基于 UDP 自己实现可靠传输,规避 TCP 的队头阻塞(HTTP/3 的基础)

四、HTTP 协议

HTTP/1.0 → HTTP/1.1 → HTTP/2 → HTTP/3

HTTP/1.0 问题
  • 短连接:每个请求新建 TCP 连接,响应后断开,三次握手开销大
  • 无 Host 头:一个 IP 只能部署一个网站
HTTP/1.1 改进
  • 持久连接(Keep-Alive):默认复用 TCP 连接,减少握手次数
  • 管道化(Pipelining):可以连续发多个请求,但响应必须按序返回
  • 队头阻塞(Head-of-Line Blocking):前一个响应没回来,后面的请求被阻塞
HTTP/2 改进
复制代码
HTTP/1.1:
  请求1 ──→ 响应1 ──→ 请求2 ──→ 响应2(串行,或管道化但有队头阻塞)

HTTP/2(多路复用):
  请求1 ──┐
  请求2 ──┼──→ 单 TCP 连接(二进制分帧,Stream 并发)──→ 响应1
  请求3 ──┘                                              响应2
                                                         响应3

HTTP/2 核心特性

  • 二进制分帧:HTTP 报文被拆分为帧(Frame),帧有 StreamID,可以乱序传输
  • 多路复用:同一 TCP 连接并发多个请求/响应,彻底解决应用层队头阻塞
  • 头部压缩(HPACK):维护头部字典,只传差量,减少重复头部的开销
  • 服务器推送:服务端可以主动推送客户端可能需要的资源(如 CSS/JS)
  • 流优先级:可以设置 Stream 的优先级

HTTP/2 的问题:TCP 层的队头阻塞依然存在(丢了一个 TCP 包,所有 Stream 都要等待重传)

HTTP/3 改进
  • 基于 QUIC(Quick UDP Internet Connections) 协议
  • QUIC 跑在 UDP 上,自己实现可靠传输
  • Stream 级别的独立可靠:一个 Stream 的丢包不影响其他 Stream
  • 0-RTT / 1-RTT 握手:相比 TCP+TLS 的 3-RTT,大幅减少握手延迟
  • 连接迁移:换 IP(切换 WiFi/4G)连接不断(用 ConnectionID 标识连接而非四元组)

HTTP 请求方法

方法 语义 幂等 有 Body
GET 获取资源
POST 创建资源/提交数据
PUT 完整替换资源
PATCH 部分更新资源
DELETE 删除资源
HEAD 获取响应头(不含 Body)
OPTIONS 查询支持的方法(CORS 预检)

幂等:多次执行结果相同。PUT 幂等(多次 PUT 同一内容结果一样),POST 不幂等(多次 POST 可能创建多条记录)。


HTTP 状态码

复制代码
1xx:信息性(100 Continue,101 Switching Protocols→WebSocket升级)
2xx:成功
     200 OK
     201 Created(POST 创建成功)
     204 No Content(成功但无响应体,如 DELETE)
3xx:重定向
     301 Moved Permanently(永久重定向,浏览器缓存)
     302 Found(临时重定向)
     304 Not Modified(缓存有效,不返回内容)
4xx:客户端错误
     400 Bad Request(请求格式错误)
     401 Unauthorized(未认证)
     403 Forbidden(已认证但无权限)
     404 Not Found
     405 Method Not Allowed
     429 Too Many Requests(限流)
5xx:服务端错误
     500 Internal Server Error
     502 Bad Gateway(网关收到上游无效响应)
     503 Service Unavailable(服务不可用/过载)
     504 Gateway Timeout(上游超时)

HTTP 缓存机制

复制代码
强缓存(不请求服务器):
  Cache-Control: max-age=3600    ← 1小时内直接用缓存(优先级更高)
  Expires: Wed, 01 Jan 2025 ...  ← 绝对过期时间(受客户端时间影响)

协商缓存(请求服务器确认):
  ETag / If-None-Match           ← 资源唯一标识符(精确)
  Last-Modified / If-Modified-Since ← 资源最后修改时间(精度 1s)

  客户端请求: If-None-Match: "abc123"
  服务端响应:
    ├─ 内容未变 → 304 Not Modified(不返回 Body,节省带宽)
    └─ 内容已变 → 200 OK + 新内容 + 新 ETag

优先级:Cache-Control > Expires > ETag > Last-Modified

HTTPS 与 TLS 握手

复制代码
HTTP  = HTTP + TCP
HTTPS = HTTP + TLS + TCP

TLS 1.2 握手(4-RTT):
Client                                      Server
  │──── ClientHello(支持的加密套件,随机数) ─▶│
  │◀─── ServerHello(选定套件,随机数,证书) ───│
  │     验证证书(CA 签名链验证)               │
  │──── ClientKeyExchange(预主密钥,公钥加密)─▶│
  │──── ChangeCipherSpec ────────────────────▶│
  │◀─── ChangeCipherSpec ────────────────────│
  │ 双方用三个随机数生成会话密钥(对称加密)      │
  │─── 加密的 HTTP 请求 ────────────────────▶│

TLS 1.3 优化(1-RTT,更快):
  合并握手消息,减少往返次数
  移除不安全的加密算法(RSA 密钥交换)
  支持 0-RTT(Session 恢复时,第一个请求直接带数据)

证书验证流程

复制代码
服务端证书 → 中间CA证书 → 根CA证书(浏览器/OS内置信任)
验证链:
  1. 证书未过期
  2. 域名匹配(CN 或 SAN)
  3. 证书签名有效(用上级 CA 的公钥验证)
  4. 未被吊销(OCSP/CRL 查询)

对称加密 vs 非对称加密

对比 对称加密 非对称加密
密钥 同一密钥加解密 公钥加密,私钥解密
速度 快(AES) 慢(RSA,约慢 1000 倍)
用途 大量数据加密 密钥交换、数字签名
TLS 中的角色 传输阶段加密数据 握手阶段协商会话密钥

面试答法:HTTPS 先用非对称加密(RSA/ECDHE)安全地交换会话密钥,然后用对称加密(AES)加密实际数据。这样兼顾了安全性(非对称保证密钥交换安全)和性能(对称加密速度快)。


五、DNS

DNS 解析过程

复制代码
浏览器输入 www.example.com

1. 浏览器 DNS 缓存 → 命中则直接用
2. 操作系统 DNS 缓存(/etc/hosts)→ 命中则直接用
3. 本地 DNS 服务器(运营商/公司 DNS,如 8.8.8.8)
         │
         │(本地 DNS 缓存未命中,开始递归查询)
         ▼
4. 根域名服务器(.)→ 返回 .com 顶级域名服务器地址
         ▼
5. .com 顶级域名服务器 → 返回 example.com 权威域名服务器地址
         ▼
6. example.com 权威域名服务器 → 返回 www.example.com 的 IP
         ▼
7. 本地 DNS 服务器缓存结果,返回给客户端

DNS 记录类型

类型 说明 示例
A 域名 → IPv4 地址 www → 1.2.3.4
AAAA 域名 → IPv6 地址 www → ::1
CNAME 域名 → 另一个域名 www → lb.example.com
MX 邮件服务器 example.commail.example.com
TXT 文本记录(域名验证/SPF) ---
NS 该域名的权威 DNS 服务器 ---

DNS 使用 UDP 的原因:DNS 查询是简单的一问一答,报文小(通常 < 512 字节),UDP 无需握手,延迟更低。报文超大时(如 DNSSEC)降级使用 TCP。


六、WebSocket

与 HTTP 的区别

复制代码
HTTP:客户端发请求,服务端响应,单向,连接后关闭(或 Keep-Alive 复用但仍是请求响应模式)
WebSocket:全双工,服务端可以主动推送,长连接

升级过程(HTTP Upgrade):
Client:
  GET /chat HTTP/1.1
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

Server:
  HTTP/1.1 101 Switching Protocols    ← 101 表示协议升级成功
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

之后:TCP 连接不断,WebSocket 帧双向传输

适用场景:IM 聊天、实时通知、在线游戏、股票行情、协同编辑

WebSocket vs SSE vs 轮询

方案 方向 适用场景
短轮询 单向(客户端拉) 简单,延迟高,浪费资源
长轮询 单向(客户端拉) 延迟较低,每次响应后重连
SSE 单向(服务端推) 消息推送、日志流
WebSocket 双向 需要双向通信的场景

七、代理与负载均衡

正向代理 vs 反向代理(面试高频)

复制代码
正向代理(Forward Proxy):代理客户端
  客户端 → [正向代理] → 服务端
  服务端不知道真实客户端是谁

  场景:VPN、访问被限制的网站、企业内网上网

反向代理(Reverse Proxy):代理服务端
  客户端 → [反向代理] → 多个后端服务
  客户端不知道真实服务端是谁

  场景:Nginx 反向代理、API 网关、CDN、负载均衡

一句话记住

  • 正向代理:客户端知道代理的存在,代理帮客户端访问外部
  • 反向代理:客户端不知道后端的存在,代理帮后端承接请求

负载均衡算法

算法 原理 优点 缺点 适用场景
轮询(Round Robin) 依次分配 简单均匀 不考虑服务器差异 服务器性能相近
加权轮询 按权重比例分配 可区分性能差异 不考虑实时负载 服务器性能不同
最少连接 分配给当前连接数最少的 动态感知负载 需维护连接数状态 长连接场景
IP Hash 对客户端 IP 取模 同一客户端打到同一服务器 负载不均衡 有状态服务(Session)
一致性哈希 哈希环,节点增减影响范围小 扩缩容影响小 实现复杂 分布式缓存
随机 随机选一个 简单 可能不均 无状态服务

一致性哈希详解(面试常问):

复制代码
普通 Hash:hash(key) % N
  问题:N 变化时(增删节点),几乎所有 key 都要重新映射

一致性哈希:
  1. 将 [0, 2^32) 排列成哈希环
  2. 节点映射到环上某位置(多个虚拟节点分散分布)
  3. key 顺时针找到第一个节点

  节点增删时,只影响该节点到前一个节点之间的 key
  影响范围:1/N(N 为节点数)

  虚拟节点:解决节点分布不均的问题(每个真实节点映射多个虚拟节点)

八、IP 与路由

IP 地址与子网

复制代码
IPv4:32 位,点分十进制,如 192.168.1.100
IPv6:128 位,冒号十六进制,如 fe80::1

子网掩码:区分网络部分和主机部分
  192.168.1.100 / 24
  网络地址:192.168.1.0(主机位全0)
  广播地址:192.168.1.255(主机位全1)
  可用主机:192.168.1.1 ~ 192.168.1.254(254个)

私有 IP 地址(不路由到公网):
  10.0.0.0/8
  172.16.0.0/12
  192.168.0.0/16

ARP(地址解析协议)

复制代码
问题:知道 IP 地址,不知道 MAC 地址(局域网通信需要 MAC)

流程:
  主机A(192.168.1.1)想发数据给主机B(192.168.1.2)
  ↓
  广播 ARP 请求:"谁是 192.168.1.2?告诉我你的 MAC"
  ↓
  主机B回复(单播):"我是 192.168.1.2,MAC 是 xx:xx:xx:xx:xx:xx"
  ↓
  主机A缓存 IP→MAC 映射(ARP 缓存),直接发送

NAT(网络地址转换)

复制代码
家庭网络(私有 IP)         路由器(NAT)            互联网(公有 IP)
192.168.1.2:54321 ──────▶ 替换源地址为公有IP ──────▶ 1.2.3.4:54321
                           维护 NAT 映射表
192.168.1.2:54321 ◀────── 恢复目标地址 ◀───────────  1.2.3.4:54321

NAT 表项:内部IP:端口 ↔ 外部IP:端口

九、CORS 跨域(后端必懂)

什么是跨域

浏览器的同源策略(Same-Origin Policy):协议 + 域名 + 端口三者全同才算同源,不同源的 JS 脚本不能发送 AJAX 请求。

复制代码
http://a.com:80  访问  http://a.com:80   ✅ 同源
http://a.com:80  访问  https://a.com:80  ❌ 跨域(协议不同)
http://a.com:80  访问  http://b.com:80   ❌ 跨域(域名不同)
http://a.com:80  访问  http://a.com:8080 ❌ 跨域(端口不同)

简单请求 vs 预检请求

简单请求(直接发送,无预检):

  • 方法:GET / POST / HEAD
  • 头部:只含标准头(Content-Type 为 form / text / json 之一)

预检请求(Preflight) :不满足简单请求条件时,浏览器先发 OPTIONS 询问服务端是否允许:

复制代码
浏览器:
  OPTIONS /api/user HTTP/1.1
  Origin: http://frontend.com
  Access-Control-Request-Method: DELETE
  Access-Control-Request-Headers: Authorization

服务端响应(允许跨域):
  HTTP/1.1 204 No Content
  Access-Control-Allow-Origin: http://frontend.com
  Access-Control-Allow-Methods: GET, POST, DELETE
  Access-Control-Allow-Headers: Authorization
  Access-Control-Max-Age: 86400    ← 预检结果缓存 1 天

之后浏览器才发真正的 DELETE 请求

Go 中处理 CORS(Gin 示例)

go 复制代码
// 方式1:手动中间件
func CORSMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("Access-Control-Allow-Origin", "*")   // 或指定域名
        c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
        c.Header("Access-Control-Allow-Headers", "Content-Type,Authorization")

        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)   // 预检请求直接返回
            return
        }
        c.Next()
    }
}

// 方式2:使用 github.com/gin-contrib/cors 库
router.Use(cors.New(cors.Config{
    AllowOrigins: []string{"http://frontend.com"},
    AllowMethods: []string{"GET", "POST", "DELETE"},
    AllowHeaders: []string{"Authorization"},
    MaxAge:       12 * time.Hour,
}))

面试答法 :CORS 是浏览器的安全机制,由浏览器检查响应头决定是否放行,服务端只需在响应中加正确的 Access-Control-Allow-* 头。预检请求是浏览器自动发的 OPTIONS,后端要正确处理 204 响应,否则跨域请求永远失败。


十、常见网络攻击与防御

DDoS(分布式拒绝服务)

复制代码
SYN Flood:大量伪造 SYN 包,耗尽半连接队列
  防御:SYN Cookie、增大半连接队列、限制 SYN 包速率

UDP Flood:大量 UDP 包消耗带宽
  防御:流量清洗、限速

HTTP Flood(CC攻击):大量合法 HTTP 请求消耗服务器资源
  防御:验证码、IP 限速、WAF

SQL 注入

复制代码
漏洞:SELECT * FROM users WHERE name = '$input'
攻击:input = "' OR '1'='1" → 查出所有用户

防御:
  参数化查询(Prepared Statement):? 占位符
  ORM 框架自动处理
  输入白名单验证

XSS(跨站脚本)

复制代码
存储型 XSS:攻击者在数据库存入恶意脚本,其他用户访问时执行
反射型 XSS:恶意脚本在 URL 参数中,服务端直接拼入响应

防御:
  输出时对 HTML 实体编码(< → &lt;)
  Content-Security-Policy 头(限制脚本来源)
  HttpOnly Cookie(JS 无法读取 Cookie)

CSRF(跨站请求伪造)

复制代码
攻击:用户已登录 bank.com,访问恶意页面,恶意页面自动发请求到 bank.com(携带用户 Cookie)

防御:
  CSRF Token(随机令牌,恶意站点无法获取)
  SameSite Cookie(限制跨站发送 Cookie)
  验证 Referer / Origin 头

九、网络编程(Go 视角)

TCP 服务器基础

go 复制代码
// 服务端
listener, _ := net.Listen("tcp", ":8080")
for {
    conn, _ := listener.Accept()     // 阻塞等待新连接
    go handleConn(conn)              // 每个连接一个 goroutine
}

func handleConn(conn net.Conn) {
    defer conn.Close()
    buf := make([]byte, 4096)
    for {
        n, err := conn.Read(buf)     // 读取数据
        if err != nil { return }
        conn.Write(buf[:n])          // 回写(Echo)
    }
}

// 客户端
conn, _ := net.Dial("tcp", "localhost:8080")
defer conn.Close()
conn.Write([]byte("Hello"))

设置超时

go 复制代码
// 连接超时
conn, err := net.DialTimeout("tcp", "host:port", 5*time.Second)

// 读写超时(每次读写前设置)
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))

// 同时设置读写超时
conn.SetDeadline(time.Now().Add(10 * time.Second))

HTTP 客户端最佳实践

go 复制代码
// 共享 Transport,复用连接池,不要每次请求创建新 Client
var httpClient = &http.Client{
    Timeout: 10 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:     90 * time.Second,
        // 不设置这些参数会导致:
        // 1. 连接不复用,频繁三次握手
        // 2. 连接泄漏,fd 耗尽
    },
}

十、面试高频问题速答

Q:TCP 和 UDP 怎么选?

需要可靠传输(数据不能丢、需要顺序)选 TCP,如 HTTP、数据库;对实时性要求高、少量丢包可接受选 UDP,如视频通话、游戏。现代趋势是基于 UDP 自实现可靠传输(QUIC),兼顾可靠性和性能。

Q:TCP 三次握手,为什么不是两次?

两次握手只能让一方确认双方通信正常,另一方无法确认自己的报文被对方收到。第三次 ACK 让服务端确认客户端能正常接收,双方都验证了发送和接收能力。另外,两次握手无法应对历史失效的连接请求,可能导致服务端浪费资源建立无效连接。

Q:HTTP 和 HTTPS 的区别?

HTTP 是明文传输,存在被窃听、篡改、冒充的风险。HTTPS 在 HTTP 和 TCP 之间加了 TLS 层,提供加密(防窃听)、完整性校验(防篡改)和身份认证(防冒充)。握手阶段用非对称加密协商会话密钥,数据传输阶段用对称加密,兼顾安全和性能。HTTPS 有握手延迟和计算开销,但 TLS 1.3 和 Session 复用已大幅降低影响。

Q:HTTP/1.1 的队头阻塞是什么?HTTP/2 如何解决?

HTTP/1.1 管道化虽然可以连续发请求,但响应必须按顺序返回,如果第一个响应慢,后面所有响应都要等。HTTP/2 用二进制分帧和 Stream 实现多路复用,每个请求是独立的 Stream,响应可以乱序,彻底消除了应用层队头阻塞。但 TCP 层的队头阻塞依然存在,HTTP/3 用 QUIC 彻底解决。

Q:HTTPS 加密过程?

TLS 握手分两阶段:① 密钥协商------客户端发 ClientHello,服务端返回证书,双方通过 ECDHE 算法协商出会话密钥(整个过程用非对称加密保护);② 数据传输------用协商好的会话密钥对称加密所有 HTTP 数据。客户端同时验证证书的合法性(CA 签名链、域名匹配、有效期)。

Q:DNS 解析过程?

依次查询:浏览器缓存→OS 缓存(/etc/hosts)→本地 DNS 服务器缓存;缓存都未命中时,本地 DNS 服务器从根域名服务器开始递归查询:根→顶级域(.com)→权威域名服务器,最终得到 IP 并缓存返回。全程使用 UDP(报文小,一问一答)。

Q:浏览器输入 URL 到页面显示,经历了什么?

复制代码
1. URL 解析(协议、域名、路径)
2. DNS 解析(域名→IP)
3. TCP 三次握手(建立连接)
4. TLS 握手(HTTPS 时)
5. HTTP 请求发送
6. 服务端处理,返回 HTTP 响应
7. 浏览器解析 HTML,构建 DOM 树
8. 下载 CSS/JS/图片等资源(可能并行)
9. 构建 CSSOM,合并成渲染树
10. Layout(计算位置大小)→ Paint(绘制)→ Composite(合成)
11. TCP 四次挥手(或 Keep-Alive 保持连接)

Q:什么是 CDN,原理是什么?

CDN(内容分发网络)在全球部署缓存节点,用户请求被调度到最近的节点,减少延迟。原理:DNS 解析时,权威 DNS 返回 CDN 的 CNAME,CDN 的 GSLB(全局负载均衡)根据用户 IP 返回最近节点的 IP。适合静态资源(图片、JS、CSS、视频)加速。

Q:Cookie、Session、Token(JWT)的区别?

Cookie 是浏览器存储机制,随请求自动发送,大小限制 4KB。Session 是服务端存储用户状态,客户端只持有 SessionID,服务端有状态,横向扩展需要共享 Session(Redis)。JWT 是服务端无状态方案,Token 自包含用户信息并签名,服务端只需验证签名,适合微服务,代价是无法主动让 Token 失效(需要黑名单)。

Q:JWT 的结构和验证原理?

复制代码
JWT = Header.Payload.Signature(Base64URL 编码,点分隔)

Header:  {"alg": "HS256", "typ": "JWT"}
Payload: {"sub": "user123", "exp": 1234567890, "role": "admin"}
         ↑ 不加密!只是 Base64,任何人都能解码看到内容
Signature: HMAC-SHA256(base64(header) + "." + base64(payload), secret)

验证流程:
  1. 用同一 secret 重新计算签名,与 Token 中的签名比较
  2. 签名一致 → Token 未被篡改
  3. 检查 exp 字段是否过期

注意:Payload 内容任何人可见,不要存密码等敏感信息
     只能验证"未被篡改",不能验证"是否被注销"

Q:Time_Wait 和 Close_Wait 哪个多说明什么问题?

TIME_WAIT 多:说明本机是主动关闭连接的一侧(客户端或主动断开的服务端),连接关闭太频繁(短连接场景),可以开启 tcp_tw_reuse 或使用长连接复用。CLOSE_WAIT 多:说明本机的代码有问题,收到 FIN 后没有调用 close() 关闭连接,连接长期处于半关闭状态,通常是连接/资源泄漏,需要检查代码。


十一、一张图记住核心

复制代码
TCP 可靠性保障:
  ┌──────────────────────────────────────────────────────┐
  │ 序列号+确认号 → 知道哪些收到了                         │
  │ 超时重传+快速重传 → 丢包了重发                         │
  │ 滑动窗口(流量控制)→ 不压垮接收方                     │
  │ 拥塞控制(慢启动+拥塞避免)→ 不压垮网络               │
  └──────────────────────────────────────────────────────┘

HTTP 演进:
  HTTP/1.0(短连接)→ HTTP/1.1(Keep-Alive,管道化但有队头阻塞)
  → HTTP/2(多路复用,二进制分帧,头部压缩)
  → HTTP/3(QUIC+UDP,Stream 级别可靠,0-RTT,连接迁移)

HTTPS = HTTP + TLS(非对称握手 + 对称加密传输 + 证书验证)

面试核心考点:
  TCP 三次握手/四次挥手(原因)
  拥塞控制 vs 流量控制(目的、机制)
  HTTP/1.1 vs HTTP/2 vs HTTP/3
  HTTPS 握手流程
  DNS 解析过程
  浏览器输入 URL 全流程

总结 :计算机网络面试核心是 TCP 可靠传输机制 (握手挥手、拥塞控制、流量控制)、HTTP 协议演进 (1.1→2→3 解决了什么问题)、HTTPS 加密流程DNS 解析浏览器 URL 全流程。这五条主线吃透,计算机网络面试稳了。

相关推荐
言午说数据1 小时前
数仓入门篇-数仓分层
大数据·面试
我叫黑大帅2 小时前
Go 标准库 net/http 包都能干嘛?
后端·面试·go
逆境不可逃2 小时前
LeetCode 热题 100 之 152. 乘积最大子数组 416. 分割等和子集 32. 最长有效括号 62. 不同路径
算法·leetcode·职场和发展
湛生2 小时前
pikachu通关笔记
笔记·计算机网络·安全·web安全·网络安全·bp
三小河2 小时前
从零实现ollama本地大模型可视化+内网穿透
前端·javascript·面试
狂师2 小时前
别再怕 AI 裁员!真相只有一句:会用 AI,就不会被淘汰
人工智能·面试·程序员
ErizJ2 小时前
面试 | Linux
linux·面试
iPadiPhone2 小时前
Spring Boot 核心注解全维度解析与面试复盘
java·spring boot·后端·spring·面试
ErizJ2 小时前
面试 | Kafka
面试·kafka