📋 摘要

用过哪些 C++ 网络框架?都有什么优缺点?

这是一道高频「经验类」面试题,考察点不在「背框架名」,而在你是否真正用过、能否说清为什么选它。核心逻辑:先用一句话给框架定性(Reactor/Proactor、C/C++、跨平台与否),再讲优缺点,最后落到「你实际怎么用的」。

常见 C++ 网络框架对比:

框架语言/风格核心模型跨平台优点缺点
libeventC / 回调Reactor + bufferevent是(epoll/kqueue/select 等后端)老牌稳定、轻量、自带 HTTP/DNS/buffereventC 接口回调地狱、无内置线程模型、多线程需自己管理
libuvC / 回调事件循环 + 线程池是(Node.js 底层)跨平台极好、自带异步文件/线程池/DNSC 风格、手动内存管理、回调嵌套深
Boost.AsioC++ / 头文件Proactor(也可 Reactor)是同步/异步统一接口、协程支持、类型安全、跨平台编译慢、模板报错难读、学习曲线陡
muduoC++ / Reactorone loop per thread + 非阻塞 epoll否(仅 Linux)多线程模型清晰、源码可读、性能好仅 Linux、接口偏旧、以 TCP 为主
ℹ️ 选型参考
  • 需要极致跨平台 + 异步文件/线程池:libuv;只想用 C++ 且要统一异步接口:Boost.Asio。
  • 追求高性能、只跑 Linux、想要清晰的多线程模型:muduo。
  • 老项目维护、C 环境:libevent。

典型使用:muduo 的 EchoServer 只需继承 TcpServer 并实现回调,多线程由 setThreadNum 一句搞定:

C++
// muduo 风格 echo server(简化示意)
#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
using namespace muduo;
using namespace muduo::net;

void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp t) {
    conn->send(buf);  // 原样回显
}

int main() {
    EventLoop loop;
    InetAddress listenAddr(8888);
    TcpServer server(&loop, listenAddr, "echo");
    server.setMessageCallback(onMessage);
    server.setThreadNum(4);   // one loop per thread
    server.start();
    loop.loop();
    return 0;
}
💡 面试加分项

不要只报框架名,主动补一句你「在什么项目、用它解决了什么」——比如「用 Asio 写了个异步网关,因为要同时处理大量长连接,Proactor 模型在高并发下比线程池省资源」。


用过哪些 C++ 数据库框架?都有什么优缺点?

同样考察「真实经验 + 选型理由」。数据库框架分两类:底层 C API(直接操作连接/语句句柄)与 C++ 封装/抽象层(RAII、类型安全、跨数据库)。

常见数据库框架对比:

框架定位优点缺点
MySQL C API (libmysqlclient)MySQL 官方 C 接口功能最全、性能好、文档全C 风格手动管理 MYSQL/MYSQL_RES、易内存泄漏、SQL 字符串拼接易注入
libpqPostgreSQL 官方 C 接口功能全、支持异步/COPY、性能好C 风格、连接与结果管理繁琐、非类型安全
SQLite C API嵌入式数据库零配置、单文件、事务、轻量C 风格、手动管理 sqlite3_stmt 生命周期、并发写受限
SOCIC++ 数据库抽象层类型安全、RAII、后端插件化(MySQL/PG/SQLite/ODBC/Oracle)文档较少、相对小众、抽象有少量开销
ODBC跨数据库通用标准一套接口连任意数据库、厂商驱动多API 繁琐、偏底层、性能一般
ℹ️ 选型参考
  • 只连一种库、追求性能:直接用官方 C API(MySQL C API / libpq / SQLite)。
  • 要跨数据库、要 C++ 类型安全:SOCI 或 ODBC。
  • 嵌入式/本地缓存:SQLite。

SOCI 的 RAII 用法示例——session 自动管理连接,into 绑定变量,无需手动释放结果集:

C++
#include <soci/soci.h>
#include <soci/sqlite3/soci-sqlite3.h>
#include <iostream>
#include <string>

int main() {
    soci::session sql(soci::sqlite3, "test.db");
    sql << "CREATE TABLE IF NOT EXISTS users(id INT, name TEXT)";
    sql << "INSERT INTO users VALUES(1, 'alice')";

    std::string name;
    // into 自动绑定并取值,异常由 soci_error 统一捕获
    sql << "SELECT name FROM users WHERE id = :id", soci::use(1), soci::into(name);
    std::cout << name << std::endl;  // alice
    return 0;
}
⚠️ 警告

底层 C API 千万不要忘 mysql_free_result / PQclear / sqlite3_finalize,否则长连接下内存持续增长;这正是 C++ 封装层(RAII)存在的价值。


用过哪些 C++ 日志框架?都有什么优缺点?

日志框架对比的维度:性能(是否异步/零拷贝)、易用性(头文件 vs 链接库)、可配置性(格式/输出目标/滚动)、与项目集成成本。

框架定位优点缺点
spdlog现代 C++ 头文件库极快、异步日志、多 sink、编译期级别开关零开销、头文件即用滚动/归档需自行组合 sink、依赖 fmt
glogGoogle 生产级分级 + 崩溃栈、线程安全、条件日志、FLAGS 控制输出文件命名/格式固定、不易定制、需全局初始化、不建议作为库对外依赖
log4cxxApache(log4j 移植)配置极灵活(XML/properties)、Appender/Layout 丰富、可动态调整较重、初始化复杂、性能一般、依赖 APR
ℹ️ 选型参考
  • 新项目默认选 spdlog:头文件 + 异步 + 高性能,几乎零学习成本。
  • 需要崩溃时自动落栈、与 gflags 集成:glog。
  • 需要运行时改配置、多目标复杂路由:log4cxx。

spdlog 的典型用法:一行初始化,异步 + 多 sink(控制台 + 滚动文件):

C++
#include <spdlog/spdlog.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/async.h>

int main() {
    // 异步日志 + 滚动文件(5MB x 3 个)
    auto logger = spdlog::rotating_logger_mt<spdlog::async_factory>(
        "app", "logs/app.log", 1024 * 1024 * 5, 3);
    spdlog::set_default_logger(logger);
    spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] %v");

    spdlog::info("用户 {} 登录,IP={}", "alice", "10.0.0.1");
    spdlog::error("请求处理失败,code={}", 500);
    return 0;
}
💡 面试加分项

主动提到「异步日志 + 双缓冲/队列」或「编译期用宏屏蔽低级别日志、接近零开销」这类性能点,能体现你对日志在线上性能影响的敏感度。


用过哪些 C++ 单元测试框架?都有什么优缺点?

单元测试框架对比维度:断言表达能力、编译速度、集成度(fixture/参数化/mock)、生态与 CI。

框架定位优点缺点
GoogleTest + GMock工业标准功能最全:断言丰富、fixture、参数化、死亡测试、GMock、CI/CMake 集成好编译慢、体积大、宏较多
Catch2头文件、BDD 风格表达式分解(失败时打印 a、b 实际值)、SECTION 结构清晰、单/双文件头文件重、编译慢、高级功能弱于 gtest
doctest极轻量头文件编译最快、可作单头文件嵌入、支持子用例功能较少、参数化/模拟需自己搭、生态小
Boost.TestBoost 生态fixture/数据驱动丰富、浮点比较、自动注册依赖 Boost、编译慢、宏复杂、文档分散
ℹ️ 选型参考
  • 大型工程、需要 Mock:GoogleTest + GMock。
  • 快速上手、BDD 风格、失败信息友好:Catch2。
  • 编译速度敏感、想把测试塞进头文件发布:doctest。

Catch2 的 REQUIRE 失败时会打印两侧实际值,排查体验好:

C++
#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>

int add(int a, int b) { return a + b; }

TEST_CASE("加法正确", "[math]") {
    REQUIRE(add(1, 2) == 3);      // 失败时打印两侧实际值
    REQUIRE(add(0, 0) == 0);
}
💡 面试加分项

提到「把单测接入 CI、用覆盖率(gcov/lcov)驱动」,或「用 GMock 隔离外部依赖(DB/网络)」,能证明你在真实工程里实践过 TDD/CI。


介绍下 socket 的多路复用?epoll 有哪些优点?

IO 多路复用:让单个线程同时监听多个 socket,当其中某个(或某几个)就绪时再处理,避免「一个连接一个线程」的资源浪费。Linux 下主流机制从 select → poll → epoll 演进。

select / poll / epoll 对比:

机制fd 管理内核实现获取就绪事件触发模式跨平台
selectfd_set 位图,默认上限 1024每次调用整体拷贝到内核O(n) 遍历所有 fd仅水平触发是
pollpollfd 动态数组,无数量上限每次调用整体拷贝O(n) 遍历仅水平触发是
epollepoll_ctl 注册到内核红黑树 + 就绪链表O(1) 只取就绪 fd支持 ET/LT否(仅 Linux)

epoll 的核心优点:

  1. 无 fd 数量上限:select 受 FD_SETSIZE(1024) 限制,epoll 只受系统内存限制。
  2. O(1) 获取就绪事件:epoll_wait 直接从就绪链表取事件,不像 select/poll 需要 O(n) 遍历全部 fd。
  3. 事件注册而非每次全量拷贝:epoll_ctl 把 fd 加入内核红黑树(增删改 O(log n)),事件状态由内核维护,用户态无需每次重新传入整个 fd 集合。
  4. mmap 共享内存减少拷贝:内核与用户空间通过共享内存交互就绪事件,避免 select/poll 每次 copy_from_user。
  5. 支持 ET/LT 两种触发模式,ET 可减少重复通知次数。

ET 与 LT 的区别:

模式全称通知时机要求
LT(水平触发,默认)Level Triggered只要缓冲区还有未读数据,每次 epoll_wait 都通知无所谓,可阻塞 IO
ET(边缘触发)Edge Triggered仅在「不可读→可读」等状态变化瞬间通知一次必须非阻塞 IO,一次读到 EAGAIN

epoll 使用骨架:

C++
#include <sys/epoll.h>
#include <unistd.h>

int epfd = epoll_create1(0);          // 创建 epoll 实例
struct epoll_event ev{}, events[64];

ev.events = EPOLLIN | EPOLLET;        // ET 模式监听读事件
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);  // 注册到红黑树

for (;;) {
    int n = epoll_wait(epfd, events, 64, -1);     // 只返回就绪 fd
    for (int i = 0; i < n; ++i) {
        if (events[i].data.fd == listen_fd) {
            // accept 新连接(ET 下要循环 accept 直到 EAGAIN)
        } else {
            // 处理已就绪的读写事件
        }
    }
}
⚠️ 警告

ET 模式下必须把 fd 设为非阻塞,并且循环读写直到返回 EAGAIN,否则数据可能被「遗忘」在缓冲区里、再也不触发通知。

💡 面试加分项

追问 epoll 内部结构时,能答出「红黑树管注册、就绪链表管派发、mmap 减拷贝」这三点,是区分「背过」和「真懂」的关键。


Reactor 和 Proactor 模式的区别?epoll 属于哪种?

Reactor 与 Proactor 是两种经典的事件驱动 IO 模型,核心区别在于**「谁来完成真正的 IO 读写」以及「事件通知的是就绪还是完成」**。

  • Reactor(反应器):事件循环监听 IO 就绪事件,就绪后由用户代码同步调用 read/write 完成实际读写。本质是同步非阻塞 IO + 事件回调(分发)。
  • Proactor(前摄器):用户先发起异步 IO 请求(注册读/写 + 用户缓冲区),内核/运行时异步完成实际读写,完成后通过回调/事件通知用户「数据已就绪」。本质是异步 IO + 完成事件回调。

对比:

维度ReactorProactor
IO 触发方式同步非阻塞(IO 就绪事件)异步 IO(IO 完成事件)
谁执行读写用户代码主动 read/write内核/运行时完成,用户只取结果
通知内容「可读 / 可写」「读 / 写已完成」
典型实现epoll/kqueue/select(muduo、Netty)Boost.Asio、io_uring、IOCP(Windows)
编程复杂度事件回调 + 自己管理缓冲区回调/协程,代码更直观但需支持异步 IO

Reactor 骨架:epoll 通知「可读」后,用户代码自己同步 read。

C++
#include <sys/epoll.h>
#include <unistd.h>

// Reactor:epoll 监听就绪事件,用户代码同步 read
void handle_read(int fd) {
    char buf[1024];
    ssize_t n = read(fd, buf, sizeof(buf));  // 此刻 fd 已就绪,同步读取
    if (n > 0) { /* 处理 buf */ }
}
ℹ️ 信息

一句话区分:Reactor 是「可以读了,你去读」;Proactor 是「已经读好了,你用吧」。前者关注「就绪」、用户同步完成;后者关注「完成」、内核异步代劳。

💡 面试加分项

epoll 属于 Reactor(同步非阻塞 + 就绪事件回调);Boost.Asio(Linux 下默认用 epoll 模拟)与 io_uring / IOCP 属于 Proactor(真正的异步 IO)。能主动说出「io_uring 让 Linux 也有了原生 Proactor」,能明显拉开与只背结论的差距。


零拷贝技术(mmap、sendfile、splice)的原理?

传统「读文件 → 发 socket」要走 4 次用户态/内核态切换 + 4 次数据拷贝(磁盘 → 内核缓冲区 → 用户缓冲区 → socket 缓冲区 → 网卡)。零拷贝的目标是减少内核态 ↔ 用户态之间的 CPU 数据搬运,让数据尽量在内核内流转或共享,降低 CPU 与上下文切换开销。

三种常见手段对比:

手段原理减少的拷贝适用场景
mmap + write把文件映射到进程地址空间,内核缓冲区与用户空间共享同一物理页,省去「内核 → 用户」拷贝1 次大文件读写、随机访问
sendfile内核内直接把数据从「文件页缓存」搬到「socket 缓冲区」,全程不进用户态2 次(少「内核→用户」+「用户→内核」)静态文件 / HTTP 文件下载
splice在内核内用管道连接两个 fd,数据在两者间「拼接」搬运,无需经过用户态与 sendfile 相当两个 fd 间转发(如代理)

对比维度:拷贝次数、是否经过用户态、是否需要用户缓冲区。

C++
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

// sendfile:内核内完成「文件 → socket」的传输,不经过用户态
int in_fd = open("file.bin", O_RDONLY);
struct stat st;
fstat(in_fd, &st);
off_t offset = 0;
ssize_t sent = sendfile(sock_fd, in_fd, &offset, st.st_size);  // 直接发送
⚠️ 警告

sendfile 的经典限制:源必须是可 mmap 的普通文件(不能是普通 socket,旧内核),且一次只能发给一个目标 fd——这正是它后来被 splice/tee 补充、以及部分场景仍选 mmap+write 的原因。

💡 面试加分项

追问「nginx/kafka 怎么发文件」时答 sendfile;再补一句「sendfile 之后内核还支持 SG-DMA(Scatter-Gather DMA),进一步省掉 socket 缓冲区那次拷贝」直接拉满。mmap 虽省一次拷贝,但带来**缺页中断 + TLB 抖动 + 写保护(COW)**等代价,小文件反而不如直接 read/write。


TCP 粘包/拆包如何解决?如何设计应用层协议?

TCP 是字节流协议:数据像水流一样连续到达,没有消息边界。发送端多次 send 可能被合并发送(粘包),一条消息也可能被拆成多段到达(拆包),接收端无法仅靠 TCP 自身还原出「一条消息」的边界。

粘包/拆包的根因:TCP 面向字节流 + 内核缓冲区 + Nagle 算法合并小包 + MTU 分片。

三种经典解法:

方案做法优点缺点
定长消息每条消息固定 N 字节,不足补零实现最简单浪费带宽、长度不灵活
分隔符消息末尾加特殊分隔符(如 \r\n)灵活、可读(HTTP/Redis)需转义、需扫描
长度字段(头+体)消息头固定长度存放「body 长度」,再读 body高效、无转义、工业主流需处理半包/粘包缓冲

应用层协议设计:消息头(固定长度字段) + 消息体(变长)。

C++
#include <cstdint>
#include <cstring>
#include <vector>
#include <arpa/inet.h>

struct Header { uint32_t body_len; };  // 网络字节序

// 编码:包头(4 字节长度)+ 包体
std::vector<char> encode(const std::string& body) {
    Header h;
    h.body_len = htonl(body.size());
    std::vector<char> out(sizeof(h) + body.size());
    memcpy(out.data(), &h, sizeof(h));
    memcpy(out.data() + sizeof(h), body.data(), body.size());
    return out;
}

// 解码:接收端维护累积缓冲区,先读满 4 字节头,再按长度读满 body
// while (buf.size() >= 4) {
//     uint32_t len = ntohl(*(uint32_t*)buf.data());
//     if (buf.size() < 4 + len) break;   // 半包,等待下次数据到达
//     取出 [4, 4 + len) 为一条完整消息;buf 左移继续解析
// }
ℹ️ 信息

面试常追问「HTTP/Redis 怎么解决粘包」:HTTP 用 \r\n\r\n 分隔头部 + Content-Length 定 body;Redis 用行分隔(RESP 协议)。核心都在「接收端维护一个累积缓冲区,按协议规则切出完整消息」。

⚠️ 警告

无论哪种方案,接收端都必须维护接收缓冲区 + 处理半包(数据不够时先缓存、等后续到达再继续解析)——这是拆包无法靠 TCP 解决、只能由应用层逻辑兜底的部分。



到底什么是 TCP 连接?

TCP 连接不是一条物理的、独占的信道,而是一个逻辑/虚拟概念:它由「四元组 + 两端各自维护的状态」共同构成。数据在网络里仍然是逐个 IP 包逐跳转发,路由器并不知道「这是一条 TCP 连接」。所谓「连接建立」,本质是让通信双方在各自内核里创建并同步一套状态(序列号、确认号、窗口、拥塞状态、状态机),并对四元组达成一致。

Text
TCP 连接的构成
─────────────────────────────────────────────────────────
  客户端内核                          服务端内核
  ┌─────────────────┐               ┌─────────────────┐
  │ 源IP:源端口      │               │ 目的IP:目的端口   │
  │ 目的IP:目的端口  │               │ 源IP:源端口      │
  │ seq / ack       │◄── 状态同步 ──►│ seq / ack       │
  │ 窗口 / 拥塞窗口  │   (三次握手)   │ 窗口 / 拥塞窗口  │
  │ 状态机(11 种)   │               │ 状态机(11 种)    │
  └─────────────────┘               └─────────────────┘
       逻辑连接 = 四元组 + 两端状态,无物理独占信道
─────────────────────────────────────────────────────────

连接的三个层面:

层面内容
标识四元组(源IP、源端口、目的IP、目的端口)唯一确定一条连接
状态两端各自维护序列号、确认号、接收/发送窗口、拥塞状态、TCP 状态机
物理/网络底层仍是逐包路由的 IP 报文,无任何独占链路
💡 面试加分项

强调「连接是端到端的状态,不是物理链路」——这是理解三次握手、四次挥手、TIME_WAIT 这些状态机问题的基础。一句「TCP 连接是两端协议栈共同维护的一段状态 + 一个四元组」就能让答案站得住脚。


TCP 是用来解决什么问题?

IP 层只提供尽力而为(best-effort)的数据报投递:可能丢包、乱序、重复、损坏,也不区分「这条包属于哪条流」。TCP 在 IP 之上补齐了可靠、有序、面向字节流的端到端传输能力,把不可靠的网络变成对应用层友好的可靠通道。

IP 的「不可靠」与 TCP 的「解法」一一对应:

IP 层的问题TCP 的对应机制
丢包超时重传 / 快速重传
乱序序列号 + 接收端排序
重复序列号去重
数据损坏校验和
发送方过快压垮接收方滑动窗口(流量控制)
发送方过快压垮网络拥塞控制(慢启动/拥塞避免/快重传/快恢复)
无连接、无流的概念三次握手建立连接、字节流语义
ℹ️ 信息

一句话总结:TCP 把「不可靠的 IP 分组传输」变成「可靠的字节流传输」。它解决的核心问题就是「如何在一个会丢、会乱、会重的网络上,让两个进程像读写文件一样可靠地收发字节流」。

💡 面试加分项

主动补一句「TCP 面向字节流、没有消息边界,所以才有粘包/拆包问题」,并交叉链接到 03-Resources/cpp/C++ 网络编程,能把这道基础题顺势引到你准备好的知识点上。


一台服务器,最多能建立多少 TCP 连接?

这个问题的关键在「理论上限由四元组决定,实际瓶颈是内存和文件描述符」。很多人第一反应是「端口号只有 65535」,这是典型误区——连接由四元组区分,服务端监听端口虽固定,但对端组合空间巨大。

Text
一条连接唯一由四元组标识:
  { 源IP, 源端口, 目的IP, 目的端口 }

服务端监听 80 端口时:
  目的IP、目的端口固定
  源IP 有 2^32 种,源端口 2^16 种
  → 理论组合 ≈ 2^48,远超 65535

真实约束:

约束说明相关参数/命令
内存每条连接占几 KB~几十 KB(socket 结构、sk_buff、收发缓冲)这是最硬的约束
文件描述符每连接一个 fd,受进程/系统上限限制ulimit -n、/proc/sys/fs/file-max
内核参数半连接/全连接队列、端口范围、缓冲区大小net.ipv4.ip_local_port_range、net.core.somaxconn、net.ipv4.tcp_rmem/wmem
CPU大量连接的定时器、事件处理百万连接需 epoll + 多队列
💡 面试加分项

单机几十万到百万级连接是可行的,但必须调优:放宽 fd 上限、调大缓冲区、用 epoll、开多队列网卡(RSS)、必要时多进程/多 IP 分摊。能答出「端口数不是上限、内存才是」就已经超越了大多数候选人。


TCP 和 UDP 有什么区别?

TCP 与 UDP 是传输层的一对「可靠 vs 轻量」设计,几乎每个维度都相反。核心一句:TCP 面向连接、可靠、字节流;UDP 无连接、不可靠、数据报。

维度TCPUDP
连接面向连接(三次握手建立)无连接,直接发
可靠性可靠(确认 + 重传 + 序号)不可靠,尽力而为
数据形态字节流,无消息边界数据报,保留报文边界
顺序保证有序不保证
传输方式一对一、全双工一对一/一对多/多对多(组播广播)
流量/拥塞控制有无
头部开销20~60 字节固定 8 字节
典型场景文件传输、HTTP、SSH、数据库DNS、音视频通话、直播、游戏同步、IoT 上报
Text
        TCP(字节流)                 UDP(数据报)
  发送 3 次 send                    发送 3 次 sendto
  ─────────────►  合并/拆分          ─────────────►  独立成包
  接收端看到连续字节,无边界          接收端 3 次 recvfrom,各得一个完整包
  → 有粘包/拆包问题                  → 天然有边界,无粘包问题
ℹ️ 信息

字节流 vs 数据报是面试最爱追问点:TCP 没有消息边界所以有粘包/拆包问题(详见 03-Resources/cpp/C++ 网络编程);UDP 每个 sendto 对应一个完整数据报,接收端一次拿一个,天然有边界但可能丢、可能乱。


为什么要 TCP,IP 层实现控制不行么?

可以但不应该,这是经典「端到端原则(end-to-end argument)」的直接结论:可靠传输这类功能必须由通信的两端(传输层)自己保证,而不应下沉到中间的 IP 层去实现。

Text
端到端原则:
  可靠性应由「端系统」保证,网络层保持「尽力而为、无状态」

  若 IP 层强制可靠:
  每个路由器都要为每条流维护状态、缓存重传
  → 路由器要存储海量连接状态,转发性能崩、扩展性崩
  → 且应用需求千差万别(实时性 vs 可靠性),一刀切不可行

  正确分层:
  应用层 ──► 想要什么语义自己选
  传输层 ──► TCP(可靠) / UDP(轻量),端到端实现
  网络层 ──► IP:简单、无状态、尽力转发

理由要点:

  1. 扩展性:让每台路由器为每个连接维护重传/状态,核心网络会被状态撑爆,违背「简单无状态、快速转发」的设计初衷。
  2. 端到端原则:真正的可靠性由发送端确认才算数,中间某一段可靠并不能保证整条路径可靠。
  3. 应用多样性:视频通话宁可丢包也不愿等待重传(选 UDP),文件下载必须一字不差(选 TCP)——把选择权留在传输层/应用层,而不是在网络层一刀切。
💡 面试加分项

能点出「端到端原则(Saltzer/Reed/Clark 的 end-to-end argument)」这个出处,是这道题最亮的加分点,直接拉开与「背答案」的差距。


说说 TCP 的三次握手?

三次握手的目的是双方确认彼此的收发能力都正常,并同步初始序列号 ISN。三次之后,客户端确认自己能发能收、服务端能发能收,双方各自进入 ESTABLISHED。

Text
       客户端                              服务端
   CLOSED                                LISTEN
     │ ① SYN, seq=x                        │
     │────────────────────────────────────►│ 收到后进入 SYN_RECV
     │                                     │
     │ ② SYN+ACK, seq=y, ack=x+1          │
     │◄────────────────────────────────────│
     │                                     │
     │ ③ ACK, seq=x+1, ack=y+1            │
     │────────────────────────────────────►│
   ESTABLISHED                           ESTABLISHED

为什么是三次(不是两次、不是四次):

  1. 防历史/失效连接:两次握手时,若客户端第一次发送的 SYN 因网络延迟迟到,服务端误以为新连接、建立后空等,浪费资源;三次握手让客户端对「是否真的要建立」做最终确认。
  2. 确认双向能力:第一次证明「客户端→服务端」可达、客户端能发;第二次证明「服务端→客户端」可达、服务端能收发;第三次证明客户端能收。三缺一都无法确认双向通信能力。
  3. 不是四次:服务端的 SYN 与 ACK 可以合并成同一个报文(SYN+ACK),所以三次就够。
💡 面试加分项

「为什么是三次」几乎必被追问,核心答「防止失效的重复 SYN 建立无效连接 + 确认双方收发能力」两点。能再补一句「第三次握手可以携带数据(快速打开 TFO 的思想)」属于进阶加分。


TCP 初始序列号 ISN 怎么取值的?

ISN 不能固定为 0 或可预测值,而必须随机化,现代实现用一个「基于时间的计数器 + 随机偏移(密钥 hash)」生成。

Text
ISN 生成思路(Linux):
  ISN = 单调递增的时间分量(每 4μs +1,RFC793)
        + 随机偏移(基于源/目的 IP、端口、密钥的 hash)
  → 结果既随时间单调、又难以被外部预测

为什么要随机化:

  1. 防序列号预测攻击:攻击者若能预测 ISN,就能伪造合法连接注入数据或劫持会话(历史上 TCP 劫持的经典手法)。
  2. 防新旧连接串包:同一四元组复用后,若 ISN 从固定值开始,上一次连接的迟到报文可能被新连接误认;随机 ISN 让旧报文序号大概率落在窗口之外,被丢弃。
  3. 防半开连接误认:配合时间戳(timestamp)进一步防止「旧包被新连接接收」。
💡 面试加分项

能说出「ISN 随时间递增 + 随机 hash,防止预测攻击和旧包串扰」即可;再补一句「Linux 还带 timestamp 选项辅助防回绕(PAWS)」直接证明你读过实现。


TCP 三次握手时,发送 SYN 之后就宕机了会怎么?

分两种情况看,核心是「对端通过超时重传兜底,最终放弃」。

Text
客户端发 SYN 后宕机:
  客户端                    服务端
    │ ① SYN                   │
    │──────────►              │ 进入 SYN_RECV
    │ ② SYN+ACK              │
    │◄──────────              │ 未收到 ACK,按指数退避重传 SYN+ACK
    │  (宕机,无 ACK)          │ 1s → 2s → 4s → 8s → 16s → 32s
    │                         │ 重传 tcp_synack_retries 次(默认5)后放弃
    │                         │ → 回收连接,回到 LISTEN
  • 服务端收不到第三次 ACK,会把连接放入半连接队列(SYN queue),按 1s、2s、4s、8s、16s、32s 指数退避重传 SYN+ACK,默认 tcp_synack_retries = 5 次后放弃并回收(总耗时约 63s)。
  • 若宕机后重启:重启后的客户端收到迟到的 SYN+ACK,因本地无此连接状态,会回一个 RST,服务端收到 RST 立即释放连接。
  • 若服务端(应答方)在发 SYN+ACK 后宕机,同理客户端会重传 ACK 直到超时。
⚠️ 警告

大量「SYN 发出后无 ACK」的半连接堆积正是 SYN Flood 攻击的利用点(见下题),内核用 net.ipv4.tcp_synack_retries 和半连接队列长度 tcp_max_syn_backlog 来控制。


什么是 SYN Flood 攻击?

SYN Flood 是经典 DoS 攻击:攻击者伪造大量源 IP 不可达的 SYN 报文,服务端对每个 SYN 回 SYN+ACK 并进入 SYN_RECV,但这些半连接永远等不到第三次 ACK,最终把半连接队列(SYN queue)打满,导致正常连接无法建立。

Text
攻击者                          服务端
 伪造源IP的 SYN ─────────────►  每收到一个 SYN 就:
                                1) 分配半连接条目
                                2) 回 SYN+ACK(发往伪造的源IP,无人回应)
                                3) 进入 SYN_RECV,等待 ACK
  大量伪造 SYN ─────────────►   半连接队列被打满
                                正常用户的 SYN 被丢弃 → 服务不可用

防御手段:

手段原理
SYN Cookie(tcp_syncookies=1)半连接队列满时不真正分配资源,而是把连接信息编码进 SYN+ACK 的 seq(Cookie),收到合法 ACK 再重建连接,几乎零资源消耗
增大 backlognet.core.somaxconn + tcp_max_syn_backlog 扩大队列
减少重传次数调小 tcp_synack_retries,让无效半连接更快释放
限流/清洗防火墙、负载均衡对来源限速,或用 CDN/高防清洗
💡 面试加分项

SYN Cookie 是必答点,能说清「不提前分配资源、把状态编码进 Cookie、靠合法 ACK 才真正建连」的机制即为亮点。能再提「半连接队列 vs 全连接队列(accept queue)是两个不同的队列」,说明你真懂 Linux 实现。


TCP 协议是如何保证可靠传输的?

TCP 的「可靠」不是单一机制,而是一整套序号 + 确认 + 重传 + 校验 + 流量/拥塞控制的组合拳:

机制作用
序列号 seq给每个字节编号,保证有序、去重
确认 ack接收方告知「已按序收到」
校验和 checksum检测传输过程中的比特错误
超时重传 RTO定时器超时未确认则重传
快速重传收到 3 个重复 ACK 立即重传,不等超时
滑动窗口(流量控制)用接收窗口 rwnd 防止发送方压垮接收方
拥塞控制慢启动/拥塞避免/快重传/快恢复,防止压垮网络
Text
可靠传输核心闭环:
  发送方                   接收方
  发 seq=100~199           收到后回 ack=200(表示 200 之前已按序收到)
     │── 数据 ──►          若有乱序,暂存并按 seq 排序
     │◄── ack=200          重复/损坏/失序按规则丢弃或缓存
  超时或 3 个重复 ACK → 重传

几个要点:

  1. 重传:超时重传的 RTO 是动态计算的(RTT 加权平均 + 偏差),比固定超时更贴合网络。
  2. 快速重传:接收方收到乱序包会重复发送「期望的下一个 ACK」,发送方连续收到 3 个相同 ACK 就认为该包丢了,立即重传而不等 RTO。
  3. 流量控制:接收方通过窗口字段告诉发送方「我还能收多少」,窗口为 0 时发送方停发。
  4. 拥塞控制:区别于流量控制(针对接收方),它针对整条网络的拥塞,通过慢启动、拥塞避免动态调整发送速率。
💡 面试加分项

被追问时能把「超时重传 vs 快速重传(3 个重复 ACK)」和「流量控制(rwnd,对端)vs 拥塞控制(cwnd,网络)」两对概念分清,基本就拿到了这道题的高分。


说说 TCP 的四次挥手?

TCP 是全双工,两个方向各自独立关闭,所以挥手需要「双方各发一次 FIN + 各回一次 ACK」,共四次。主动关闭方(FIN-WAIT)与被动关闭方(CLOSE-WAIT)状态如下:

Text
   主动方                                    被动方
 ESTABLISHED                              ESTABLISHED
    │ ① FIN, seq=u(我不再发数据)            │
    │──────────────────────────────────────►│ 进入 CLOSE_WAIT
    │ ② ACK, ack=u+1                        │(可能还有数据要发)
    │◄──────────────────────────────────────│
    │ 进入 FIN_WAIT_2,等待对方 FIN          │
    │ ③ FIN, seq=w, ack=u+1                 │(数据发完,也关闭发送)
    │◄──────────────────────────────────────│
    │ ④ ACK, ack=w+1                        │
    │──────────────────────────────────────►│
  TIME_WAIT(等 2MSL)                     LAST_ACK → CLOSED

为什么是四次(不是三次):

  • FIN 只关闭「自己→对方」这一个方向。被动方收到 FIN 后,可能还有数据没发完,所以先回 ACK 表示「知道了」,等自己的数据发完再发自己的 FIN。
  • 服务端的「ACK ②」和「FIN ③」因为中间隔着未发完的数据,通常无法合并,于是变成四次。
ℹ️ 信息

极端情况下也有「三次挥手」:若被动方收到 FIN 时恰好没有数据要发,它可以把 ACK 和 FIN 合并成一个报文(FIN+ACK),此时挥手就变成 3 个报文(FIN → FIN+ACK → ACK),但标准流程仍是四次。


为什么 TCP 挥手需要有 TIME_WAIT 状态?

TIME_WAIT 出现在主动关闭方,持续 2MSL(MSL 是报文最大生存时间,Linux 默认 30s,所以 TIME_WAIT 默认 60s)。它存在两个不可替代的原因:

Text
原因一:保证最后一个 ACK 可靠送达
  主动方发 ④ACK 后直接关,若 ACK 丢失,被动方会重传 FIN
  → 主动方必须保留状态,能再次回应 ACK
  → 否则被动方会一直 LAST_ACK 重传、无法正常关闭

原因二:让旧连接的迟到报文在网络中消亡
  2MSL = 一个报文去 + 一个报文回的最大时间
  → 保证本连接所有残留在网络里的报文都已消失
  → 防止「旧连接的迟到报文」被复用同四元组的新连接误收

两个原因的对应关系:

原因解决的问题
保证最后 ACK 送达可靠关闭连接(被动方能收到最终 ACK)
等旧报文消亡防止旧连接报文串到新连接(串包/串数据)

TIME_WAIT 的代价与处理:

代价处理方式
占用端口/内存资源服务端大量主动关闭时出现大量 TIME_WAIT
端口复用SO_REUSEADDR 允许复用
内核调优tcp_tw_reuse(客户端可复用);⚠️ tcp_tw_recycle 已因 NAT 环境下时间戳问题被废弃,不要用
💡 面试加分项

两个原因缺一不可,都要答到:「可靠关闭(防 ACK 丢失)+ 防止旧包串新连接(2MSL 让旧报文自然消亡)」。再补一句「2MSL = 60s(Linux,MSL=30s)」「tcp_tw_recycle 已废弃」会显得非常专业。


除了四次挥手,还有什么方法断开连接?

除了标准的四次挥手(FIN 正常关闭),TCP 还有一条「快速通道」——RST(Reset)异常终止,此外还有 keepalive 探测、半关闭等边界手段。

Text
两种关闭方式对比:
  FIN(正常关闭)              RST(异常关闭)
  ────────────────            ────────────────
  四次挥手,优雅关闭           单报文,立即断开
  保证数据可靠交付完            丢弃发送/接收缓冲中未处理数据
  主动方进入 TIME_WAIT        主动方直接 CLOSED,不进入 TIME_WAIT
  对端收到后进入 CLOSE_WAIT    对端收到后直接复位

RST 的典型触发场景:

场景说明
收到不存在的连接的报文对端发来数据但本地无此四元组状态 → 回 RST
连接过程中发生错误收到非法报文、序列号越界等 → 发 RST 强制终止
主动「不想等」应用设置 SO_LINGER(linger=0) 时,close 会直接发 RST
半开连接被探测到keepalive 探测失败也可能触发 RST/关闭

其他边界手段:

  • 半关闭(half-close):shutdown(SHUT_WR) 只关闭「自己→对方」的发送方向,仍可继续接收对方数据——这正是四次挥手中间态的来源。
  • keepalive 保活:长时间无数据时周期探测对端是否存活,检测到对端已消失则回收连接。
💡 面试加分项

能主动答出「RST 是异常终止、单报文、不进入 TIME_WAIT、会丢缓冲数据」,并和「FIN 是优雅关闭、四次挥手、进入 TIME_WAIT」形成对比,就把这道「开放式追问」稳稳接住了。再补 SO_LINGER 触发 RST 属于加分。



TCP 超时重传机制是为了解决什么问题?

TCP 提供的是可靠传输,其前提是「发送的每个字节都能被对端正确收到」。但底层 IP 网络是尽力而为(best-effort)的:数据包可能丢失、损坏、乱序、重复。超时重传(RTO,Retransmission Timeout)解决的核心问题就是「如何发现并补救丢包」——当发送方在某个时限内没收到对端的确认(ACK),就认为数据段可能丢了,于是重发。

它的工作逻辑:

  1. 发送方每发出一个数据段,就启动一个重传定时器。
  2. 若在定时器到期前收到该段的 ACK,取消定时器,认为传输成功。
  3. 若定时器到期仍未收到 ACK,判定该段丢失,重新发送,并把定时器时间翻倍(指数退避)。

关键点在于超时时间(RTO)如何设定:设太短会误判、频繁重发浪费带宽;设太长则丢包后迟迟不重发、延迟高。所以 RTO 由 RTT 动态估算而来:

Text
RTT 测量 -> 平滑往返时间 SRTT
SRTT = (1-α)·SRTT + α·RTT           (α 常取 1/8)
RTTVAR = (1-β)·RTTVAR + β·|SRTT - RTT|   (β 常取 1/4)
RTO = SRTT + 4·RTTVAR                 (保证留有余量)
ℹ️ 两个重要细节
  • Karn 算法:重传的数据段不参与 RTT 采样——因为无法区分收到的 ACK 是对「原始包」还是「重发包」的确认,混入采样会污染 RTT 估计。
  • 指数退避:连续重传时每次把 RTO 翻倍(如 1s → 2s → 4s …),上限通常 60s;连续超时多次仍未收到 ACK,说明对端可能已不可达,TCP 最终放弃并关闭连接。
💡 面试加分项

能说清「RTO 不是固定值,而是由 SRTT + RTTVAR 动态算出的」,并补充 Karn 算法 + 指数退避,是「真懂超时重传」的分水岭。


TCP 中何时会出现 RST(reset)报文?

RST(Reset) 是 TCP 首部标志位之一,作用是异常终止连接,表示「这个连接我不认识/不想要了,直接重置,不需要四次挥手」。它出现的典型场景:

场景触发原因举例
请求不存在的端口对端主机可达,但目标端口无进程监听connect 到未监听的端口,内核回 RST
异常终止连接一方想立刻断开,不等四次挥手SO_LINGER 设为 0,close 时发 RST
半开连接(half-open)一方崩溃/重启,另一方继续发数据,对方已不认识该连接收到对「未知四元组」的数据包时回 RST
SYN 请求到已建立连接的端口收到与当前连接不匹配的 SYN服务器端口已有连接,又收到异常 SYN
超时放弃后多次重传仍无响应,主动发 RST 关闭连接存活超时
NAT/防火墙策略中间设备拒绝该连接防火墙拦截后主动发 RST

RST 与 FIN 的区别:FIN 是优雅关闭(走完整四次挥手、双方确认后才断);RST 是暴力重置(立即断开,不确认,接收方直接丢弃发送/接收缓冲区的未处理数据)。

Text
正常关闭:  A --FIN--> B --ACK--> A --FIN--> B --ACK--> A   (四次挥手)
异常重置:  A --RST--> B   (B 收到后立即关闭,无需 ACK)
⚠️ 警告

收到 RST 后,本端会立即丢弃连接上所有未读数据并关闭 socket,且不会向对端回 ACK。因此 RST 意味着「数据可能丢」,只应在确实不需要可靠交付时使用。

💡 面试加分项

能主动区分 RST(异常重置,无 ACK)与 FIN(优雅关闭,有四次挥手),并说出「SO_LINGER=0 会发 RST」这种工程细节,会让回答更扎实。


TCP 有超时重传为什么还需要快速重传机制?

因为超时重传太慢。RTO 通常被限制为最小 200ms(RFC 6298 建议下限 1s,实际实现多为 200ms),且一旦丢包会触发拥塞控制把 cwnd 打回慢启动,代价巨大。在高带宽、低延迟网络里,「等一个 RTO 再重传」会带来明显卡顿。

快速重传(Fast Retransmit) 利用 TCP 的累计确认(cumulative ACK)特性:接收方每收到一个乱序数据段,就立即重发上一个按序到达段的 ACK,形成重复 ACK(duplicate ACK)。发送方收到 3 个重复 ACK,就判断「该 ACK 后面的那个段丢了」,不等超时,立即重传。

Text
发送方发 [1][2][3][4][5],其中 [2] 丢失:

接收方收到 [1]  -> ACK 2          (期望 2)
接收方收到 [3]  -> 重复 ACK 2     (乱序,重发对 [1] 的确认)
接收方收到 [4]  -> 重复 ACK 2     (第 2 个重复 ACK)
接收方收到 [5]  -> 重复 ACK 2     (第 3 个重复 ACK)

发送方收到 3 个重复 ACK 2 -> 立刻重传 [2],无需等待 RTO

两者对比:

维度超时重传(RTO)快速重传(Fast Retransmit)
触发条件定时器超时收到 3 个重复 ACK
反应速度慢(≥200ms,且指数退避)快(几乎实时)
对拥塞窗口直接降到 1 MSS 重进慢启动配合快速恢复,cwnd 只减半
适用场景整个窗口丢光、无 ACK 可依后续数据仍能到达、能产生重复 ACK
ℹ️ 为什么是「3 个」重复 ACK

1 个可能是乱序重排,2 个也可能是网络抖动,3 个重复 ACK 通常意味着对端后续数据仍在正常到达、唯独中间某个段丢了——这时快速重传最可靠,也避免对乱序误判。

💡 面试加分项

强调「快速重传解决的是速度问题,超时重传是兜底:当窗口整个丢了、连重复 ACK 都产生不了时,仍要靠超时重传恢复」,说明两者互补而非替代。


TCP 的 SACK 的引入是为了解决什么问题?

SACK(Selective Acknowledgment,选择性确认) 解决的是**「重传粒度太粗」的问题。传统 TCP 只有累计确认**:ACK = n 表示「n 之前的字节都收到了」,但无法告诉发送方 n 之后具体收到了哪些。

带来的问题:在快速重传场景下,发送方只知道「某个段丢了」,却不知道丢的是哪几个段。它可能:

  1. 重复重传后面其实已经收到的段(浪费带宽);
  2. 只重传一个段,但实际丢了多个段,还得再等一轮重复 ACK,恢复慢。

SACK 通过在 TCP 选项里携带已收到的「块(block)」的左右边界,让接收方明确告诉发送方「我收到了 [L1,R1)、[L2,R2) 这些区间,只缺中间的洞」:

Text
没有 SACK:发送方收到 3 个重复 ACK,只知道「有个段丢了」,
           可能把整个窗口的段都重传一遍。

有 SACK:  接收方 ACK 里附带「已收到 [1001,1500)、[2001,2500)」,
           发送方精准知道缺 [1501,2000],只重传这一个洞。
Text
TCP SACK 选项(示意):
+--------+--------+----------------+
| Kind=5 | Length | Left Edge ...  |
+--------+--------+----------------+
|   Right Edge ...  (可多个块)   |
+----------------------------------+
ℹ️ 配套:DSACK

DSACK(Duplicate SACK) 是 SACK 的扩展,接收方把自己重复收到的段也报给发送方,用于判断是「数据真丢了」还是「ACK 丢了/乱序」,避免不必要的重传。

💡 面试加分项

能说清「SACK 是让重传变精准:从『重传整个窗口』到『只重传丢失的洞』」,并能关联到上一题快速重传的「3 个重复 ACK」配合使用,逻辑链完整。注意 SACK 需双方在握手时协商 TCP_SACK 选项才生效。


TCP 滑动窗口的作用是什么?

滑动窗口(Sliding Window) 是 TCP 实现流量控制(flow control)的核心机制,作用是让发送方根据接收方的处理能力来调节发送速率,避免「发送过快,接收方缓冲区被撑爆、只能丢包」。

核心思想:接收方在 ACK 里携带窗口大小(rwnd, receive window),告诉发送方「我还能接收多少字节」;发送方未确认的数据量不能超过这个窗口。窗口随数据发送而前移(滑动),随 ACK 到达而右移。

Text
发送方视角(字节流):
|<-- 已确认 -->|<------ 已发送未确认 ------>|<-- 可继续发送 -->|<-- 不可发送 -->|
               ^ 窗口左沿                   ^ 窗口右沿
               |<---------- 发送窗口 --------->|  (受 rwnd 与 cwnd 共同约束)
  • 窗口左沿:随 ACK 到达向右移动。
  • 窗口右沿:随对端通告的 rwnd 大小伸缩。
  • 发送窗口 = min(rwnd, cwnd):rwnd 由接收方决定(流量控制),cwnd 由网络拥塞决定(拥塞控制)。

典型现象:

情况结果
接收方缓冲区快满,rwnd 变小甚至为 0发送方减速或暂停(零窗口)
接收方处理快,rwnd 变大发送方可加快
窗口太小带宽利用率低(小窗口导致频繁 ACK/停顿)
Text
rwnd = 4096,已发送未确认 2048 -> 还能发 2048
接收方处理后通告 rwnd = 8192 -> 窗口右移,可继续发更多
ℹ️ 与粘包/字节流的关联

滑动窗口管的是「发送节奏」,而 TCP 本身是字节流、无消息边界,窗口滑动不会替你切分消息——这就回到了 03-Resources/cpp/C++ 网络编程,接收端仍需自行处理消息边界。

⚠️ 零窗口问题

当 rwnd = 0 时发送方停止发送,但要靠**零窗口探测(zero window probe)**定时发探测包,否则接收方窗口恢复的通知若丢失,双方会永久僵住(死锁)。

💡 面试加分项

能说出「发送窗口 = min(rwnd, cwnd)」,把**流量控制(rwnd)和拥塞控制(cwnd)**两个概念区分开,是这道题的核心得分点。


说说 TCP 拥塞控制的步骤?

拥塞控制(Congestion Control) 解决的是「网络本身拥塞」的问题(区别于滑动窗口的接收方流量控制):当网络中间路由器缓存溢出、开始丢包时,TCP 需要主动降低发送速率,避免雪崩。它由发送方维护一个拥塞窗口 cwnd,控制未确认数据量。

经典算法(Reno)分四个阶段,配套两个核心变量:cwnd(拥塞窗口)与 ssthresh(慢启动阈值):

1. 慢启动(Slow Start)

  • 初始 cwnd = 1 MSS,每收到一个 ACK,cwnd 翻倍(指数增长)。
  • 直到 cwnd >= ssthresh,转入拥塞避免。

2. 拥塞避免(Congestion Avoidance)

  • cwnd 每过一个 RTT 只线性 +1 MSS(加法增大),温和探测网络容量。
  • 若发生超时:ssthresh = cwnd/2,cwnd = 1 MSS,重新慢启动。

3. 快速重传(Fast Retransmit)

  • 收到 3 个重复 ACK,立即重传丢失段,不等超时。

4. 快速恢复(Fast Recovery)

  • 收到 3 个重复 ACK 后:ssthresh = cwnd/2,cwnd = ssthresh + 3 MSS(而非降到 1)。
  • 之后每收到一个重复 ACK,cwnd += 1;收到新数据的 ACK,cwnd = ssthresh,回到拥塞避免。
Text
cwnd
  ^
  |         *   *   *   *    <- 拥塞避免:线性增长
  |      *
  |    *
  |  *                        <- 慢启动:指数增长
  | *
  +--------------------------> 时间
       ssthresh 阈值处切换
ℹ️ 为什么「超时」比「3 个重复 ACK」更严重

超时意味着「整个窗口可能都丢了、网络几乎不可用」,所以要直接回慢启动;3 个重复 ACK 说明「只是丢了几个段,后续数据还在流动」,只需减半 + 快速恢复。这就是 Reno 对两种丢包信号区别对待的原因。

💡 面试加分项

能说出「慢启动指数、拥塞避免加法增大(AIMD)、超时回慢启动、重复 ACK 走快速恢复」四条主线,并提到 NewReno/BIC/CUBIC 等现代算法(CUBIC 是 Linux 默认),即可拿满分。


ARP 和 RARP 分别是什么?有什么区别?

两者都是地址解析协议,工作在网络接口层(链路层),解决「逻辑地址 ↔ 物理地址」的映射问题,但方向相反。

  • ARP(Address Resolution Protocol):由 IP 地址查 MAC 地址。主机发送数据前,需要用目标 IP 找到下一跳的 MAC 地址,才能组装以太网帧。
  • RARP(Reverse ARP):由 MAC 地址查 IP 地址。早期无盘工作站开机时只知道自己的 MAC,用它向服务器查询自己的 IP。

工作流程对比:

Text
ARP:  A 要发给 IP=192.168.1.2,但不知其 MAC
      -> 广播 ARP 请求「谁是 192.168.1.2?告诉我你的 MAC」
      -> 目标机单播应答「我是,我的 MAC 是 xx:xx:xx:xx:xx:xx」
      -> A 把 IP-MAC 映射写入 ARP 缓存

RARP: 无盘机只知道自己的 MAC
      -> 广播 RARP 请求「我是 xx:xx:xx:xx:xx:xx,我的 IP 是多少?」
      -> RARP 服务器应答「你的 IP 是 192.168.1.5」
维度ARPRARP
映射方向IP → MACMAC → IP
谁发起任意需要通信的主机无盘工作站/无 IP 设备
广播类型广播请求、单播应答广播请求、单播应答
现状仍在广泛使用已被 DHCP/BOOTP 取代
缓存有 ARP 缓存表无(基本废弃)
⚠️ ARP 的安全风险

ARP 无认证机制,容易被ARP 欺骗(ARP spoofing):攻击者伪造 ARP 应答,把某 IP 映射到自己的 MAC,实现中间人攻击或断网。

💡 面试加分项

主动指出「RARP 已被 DHCP/BOOTP 取代(DHCP 还能下发更多配置),现在几乎没有实际部署」,并顺带提 ARP 缓存 + ARP 欺骗,体现知识的新鲜度与安全意识。


TCP/IP 四层模型是什么?

TCP/IP 四层模型是实际互联网采用的协议分层模型(相比 OSI 更贴近真实实现),由下到上分四层,每层职责清晰、只依赖下层、对上层透明:

层名称主要职责典型协议
4应用层为应用程序提供网络服务,定义数据格式HTTP、HTTPS、DNS、FTP、SMTP、SSH
3传输层端到端通信,进程间数据传输、可靠/不可靠传输TCP、UDP
2网络层(网际层)寻址与路由,把数据包从源送到目的主机IP、ICMP、IGMP、ARP
1网络接口层(链路层)物理传输,帧封装、物理地址(MAC)、差错检测以太网、WiFi、PPP
Text
+------------------+
|     应用层        |  HTTP / DNS / FTP ...
+------------------+
|     传输层        |  TCP / UDP(端口,端到端)
+------------------+
|     网络层        |  IP(IP 地址,路由转发)
+------------------+
|   网络接口层      |  以太网 / WiFi(MAC 地址,帧)
+------------------+

数据封装(发送时自上而下层层加头,接收时自下而上层层拆头):

Text
应用层数据
   -> + TCP 头   = 段(Segment)
   -> + IP 头    = 包(Packet)
   -> + 帧头/尾  = 帧(Frame)
ℹ️ 与 OSI 七层的对应

TCP/IP 的应用层 ≈ OSI 的「应用层 + 表示层 + 会话层」;传输层 ↔ 传输层;网络层 ↔ 网络层;网络接口层 ≈ 「数据链路层 + 物理层」。四层模型是「合并版」,更简洁实用。

💡 面试加分项

能一边说分层、一边说出「各层对应的地址标识:应用层用端口、传输层用端口、网络层用 IP、链路层用 MAC」,说明你理解了分层的本质是「不同层级用不同地址寻址」。


OSI 七层模型是什么?

OSI(Open Systems Interconnection)七层模型是 ISO 提出的理论参考模型,把网络通信划分为七个层次,比 TCP/IP 四层更细、更偏教学/标准化。自上而下:

层名称职责典型协议/设备数据单位
7应用层提供用户接口与应用服务HTTP、DNS、SMTP、FTP数据(Data)
6表示层数据格式转换、加密、压缩TLS/SSL、JPEG、ASCII数据
5会话层建立/管理/终止会话RPC、NetBIOS数据
4传输层端到端可靠传输、流量/拥塞控制TCP、UDP段(Segment)
3网络层逻辑寻址与路由IP、ICMP、路由器包(Packet)
2数据链路层帧封装、物理寻址(MAC)、差错检测以太网、交换机、ARP帧(Frame)
1物理层比特流传输、电气/物理特性网线、集线器、光纤比特(Bit)
Text
7 应用层      <-- 人机交互
6 表示层      <-- 加解密、编码
5 会话层      <-- 会话管理
4 传输层      <-- 端到端(TCP/UDP)
3 网络层      <-- IP 路由
2 数据链路层  <-- MAC 帧
1 物理层      <-- 比特/信号

记忆口诀:「物 数 网 传 会 表 应」(从下往上:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层)。

ℹ️ OSI 七层 vs TCP/IP 四层

OSI 是先定标准再实现,划分过细、实现复杂,实际落地失败但教学价值高;TCP/IP 是先有实现再归纳,更贴近真实互联网。面试中两者常被放在一起问「区别/对应关系」。

💡 面试加分项

能答出「OSI 是理论模型、七层划分过细;TCP/IP 是事实标准、四层合并了 5/6/7 层」,并举例「TLS 在 OSI 里算表示层、在 TCP/IP 里归应用层」,说明你真的理解了两者的映射关系。


Cookie、Session、Token 之间有什么区别?

三者都是 Web 应用中**维持「状态/身份」**的手段,解决的是「HTTP 无状态」带来的「如何记住用户」的问题,但机制和存放位置不同。

维度CookieSessionToken
本质浏览器本地存储的一小段键值对服务端保存的用户会话数据客户端持有的凭证(一串编码/签名数据)
存放位置客户端(浏览器)服务端(内存/DB/Redis)客户端(一般 localStorage/内存)
谁生成服务端 set-cookie 下发,浏览器自动回传服务端创建并维护服务端签发,客户端自持
携带方式每次请求自动带上通过 Cookie 里的 session_id 关联手动放 Header(如 Authorization)
状态性无状态(纯数据)有状态(服务端要存)无状态(JWT 可自校验)
典型用途记住偏好、保存 session_id登录态、购物车无状态 API 鉴权、SSO

三者的关系与典型流程:

Text
Cookie + Session 模式(传统,有状态):
  登录 -> 服务端生成 session_id -> 存服务端 -> 通过 Set-Cookie 下发
  之后每次请求 -> 浏览器自动带 Cookie(session_id) -> 服务端查 Session

Token 模式(无状态,如 JWT):
  登录 -> 服务端签发 Token(含用户信息+签名)-> 返回客户端
  之后每次请求 -> 客户端手动在 Header 带 Token -> 服务端验签即可
ℹ️ 一句话区分
  • Cookie 是「浏览器里的一小块存储」,本身只是载体,不一定是身份凭证。
  • Session 是「服务端的用户数据」,靠 Cookie 里的 session_id 关联,有状态。
  • Token 是「客户端自持的凭证」,服务端验签即可、无需存会话(JWT 为代表)。
⚠️ 警告

Cookie 存在 CSRF 风险(浏览器自动携带);Session 存服务端会带来扩展性问题(多机共享需 Redis 等);Token 一旦泄露则难以主动作废(除非加黑名单)。三者不是互相替代,而是「载体 + 数据 + 凭证」的分工。

💡 面试加分项

别只背定义,主动画清「Session 是数据、Cookie 是存放 session_id 的载体、Token 是自持凭证」的分工关系,并点出「分布式场景下 Session 要共享存储、Token 天然无状态更好扩展」,即能拉开差距。


JWT Token 能说说吗?

JWT(JSON Web Token) 是一种无状态、自包含的身份凭证标准(RFC 7519),由三部分组成,用 . 分隔:

Text
Header.Payload.Signature

Header    = base64url({ "alg": "HS256", "typ": "JWT" })
Payload   = base64url({ "sub": "10086", "name": "alice", "exp": 1720000000 })
Signature = HMACSHA256( base64url(Header) + "." + base64url(Payload), secret )
  • Header(头):声明签名算法(HS256/RS256 等)与类型。
  • Payload(载荷):存放 claims(声明),如 sub(主体)、exp(过期时间)、iat(签发时间)、自定义字段。⚠️ 只是 base64 编码,不是加密,任何人可解码,不能放敏感信息。
  • Signature(签名):用密钥对「头+载荷」签名,用于防篡改——改了 Payload 验签就会失败。

完整结构示例:

Text
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMDA4NiJ9.签名部分
  ^ Header          ^ Payload        ^ Signature
ℹ️ 为什么 JWT 适合分布式/无状态

服务端签发的 JWT 自带用户信息与有效期,服务端无需保存会话,验签即可恢复身份——因此多实例、微服务、跨域 API 都能直接验证,天然支持水平扩展。

C++
// JWT 使用思路(伪代码,仅示意流程)
std::string token = jwt::create()
    .set_issuer("myapp")
    .set_subject("10086")
    .set_expires_at(std::chrono::system_clock::now() + std::chrono::hours(2))
    .sign(jwt::algorithm::hs256{"secret"});   // 签发

auto decoded = jwt::decode(token);            // 校验并解析
auto verifier = jwt::verify().allow_algorithm(jwt::algorithm::hs256{"secret"});
verifier.verify(decoded);                     // 验签,失败则拒绝
⚠️ JWT 的三个坑
  1. Payload 不加密,别放密码等敏感字段(要加密得用 JWE)。
  2. 无法主动失效:Token 签发后到 exp 前一直有效,登出/改密需靠黑名单或短有效期缓解。
  3. 密钥/算法安全:别用 alg: none,HS256 密钥要够长,否则可被伪造。
💡 面试加分项

能答出「JWT = Header.Payload.Signature 三段 base64url,签名只保证完整性/防篡改、不保证机密性」,并说出「无状态、自包含、适合分布式,但难主动吊销」,就是高分回答。


简单谈谈你对 DNS 的理解?

DNS(Domain Name System) 是互联网的域名解析系统,作用是把人类易记的域名(如 www.baidu.com)转换成机器可路由的 IP 地址(如 110.242.68.4)。它本质上是一个分层、分布式、带缓存的数据库。

分层结构(域名从右到左逐级解析):

Text
.                       根域名服务器(Root,全球 13 组)
└── com                 顶级域 TLD(.com/.org/.cn ...)
    └── baidu.com       二级域(权威服务器)
        └── www         主机名

解析流程(递归 + 迭代结合):

Text
客户端 -> 本地 DNS 服务器(递归查询,本地帮你查到底)
本地 DNS -> 根服务器(问 .com 在哪)          -> 返回 .com 服务器
         -> .com 服务器(问 baidu.com 在哪)  -> 返回 baidu.com 权威服务器
         -> baidu.com 权威服务器(www 的 IP?)-> 返回最终 IP
         -> 缓存结果并返回给客户端

常见记录类型:

记录含义
A域名 → IPv4 地址
AAAA域名 → IPv6 地址
CNAME别名(指向另一个域名)
MX邮件服务器
NS该域名的权威 DNS 服务器
TXT文本记录(SPF/DKIM 验证、域名验证)
ℹ️ 递归 vs 迭代查询
  • 递归查询:客户端只问一次,本地 DNS 负责查到底再返回(客户端省事)。
  • 迭代查询:本地 DNS 一层层问根、TLD、权威服务器(本地 DNS 承担查询过程)。 现实是「客户端 → 本地 DNS 用递归,本地 DNS → 各级服务器用迭代」。
💡 面试加分项

主动提到「本地缓存 + TTL 加速解析、CDN 用 DNS 做就近调度(GSLB)、以及 DNS 污染/劫持、dig/nslookup 排查」,能把这道「理解题」答出工程深度。


简单谈谈你对 CDN 的理解?

CDN(Content Delivery Network,内容分发网络) 是在互联网上部署大量边缘节点(缓存服务器),把内容提前缓存到离用户最近的节点,用户就近获取,从而降低延迟、减轻源站压力、抗攻击。

核心原理:

  1. 内容缓存:静态资源(图片、JS、CSS、视频、文件)被分发/缓存到各地边缘节点。
  2. 就近调度:用户请求被 GSLB(全局负载均衡)按「地理位置、运营商、节点负载」调度到最近的边缘节点。
  3. 回源:边缘节点没有命中缓存时,才向源站(origin)拉取,并缓存下来。
Text
                 +-----------+
  用户A(北京) -->| 边缘节点  |\
                 +-----+-----+ \
                       |        \   未命中则回源
  用户B(上海) -->+-----+-----+   +---------+
                 | 边缘节点  |---| 源站(源)|
                 +-----------+   +---------+
   就近访问 -> 命中缓存直接返回,不绕远路到源站

CDN 解决了什么:

问题CDN 的解法
用户离源站远、延迟高边缘节点就近响应,减少物理距离
源站流量压力大边缘命中缓存,流量在边缘消化
单点故障/热点多节点冗余 + 负载分担
DDoS/突发流量边缘吸收攻击与突发,保护源站
ℹ️ CDN 与 DNS 的关系

CDN 调度常依赖 DNS:用户解析域名时,DNS 根据用户 IP 返回「最近边缘节点的 IP」(智能 DNS/GSLB),从而把用户导流到就近节点。常见技术还有 HTTP 302 重定向、Anycast。

💡 面试加分项

能答出「CDN 的价值 = 就近缓存 + 智能调度 + 回源 + 抗 DDoS」,并说出「缓存命中率、刷新/预热、TTL、回源」这些关键概念,说明你懂的不只是名词。


从网络角度来看,用户从输入网址到网页显示,背后发生了什么?

这是一道串联型面试题,考察你是否能把 DNS、TCP、TLS、HTTP、浏览器渲染串成一条完整链路。以输入 https://www.example.com/page 为例,按时间顺序:

Text
1. URL 解析/浏览器缓存 -> 判断协议(https)、域名、路径、是否命中浏览器/系统缓存
2. DNS 解析            -> 域名 -> IP(本地缓存 -> 递归 -> 根/TLD/权威)
3. TCP 三次握手        -> SYN -> SYN+ACK -> ACK,建立可靠连接
4. TLS 握手(HTTPS)   -> 协商加密套件、校验证书、交换密钥
5. 发送 HTTP 请求      -> GET /page HTTP/1.1 + 各种 Header
6. 服务器处理并响应     -> HTTP 响应(状态码 + 响应头 + 正文)
7. 浏览器接收并渲染     -> 解析 HTML -> 构建 DOM/CSSOM -> 布局 -> 绘制
8. 连接复用/关闭        -> keep-alive 复用,或四次挥手关闭

各阶段要点:

阶段关键点
URL 解析拆分协议/域名/端口/路径;先查浏览器缓存、系统 hosts、本地 DNS 缓存
DNS 解析见 03-Resources/cpp/C++ 网络编程,递归 + 缓存 + TTL
TCP 握手SYN/SYN+ACK/ACK 三次握手,确认双方收发能力
TLS 握手证书链校验、密钥交换(ECDHE)、对称加密会话
HTTP 请求方法、路径、Host、Cookie、User-Agent 等 Header
服务器响应状态码(200/301/404…)、响应头、正文(可能是 CDN 边缘返回)
渲染HTML 解析 + 并行加载子资源(CSS/JS/图片,可能走 CDN)
ℹ️ 补充:细节追问点
  • HTTPS 会多 TLS 握手(约 1~2 个 RTT),比 HTTP 慢,但可用 HTTP/2 多路复用、TLS 1.3(1-RTT)优化。
  • 一个页面往往发起多个连接/请求:HTML 里引用的 CSS/JS/图片会继续发起新的 DNS/TCP/HTTP 流程(除非 keep-alive 复用)。
💡 面试加分项

答这道题要有顺序、有层次,并主动点出「HTTPS 多了 TLS、静态资源走 CDN、keep-alive 复用连接、DNS 有缓存」等工程细节,体现你把零散知识点串成体系的能力。


常见的登录鉴权方式有哪些?各自的优缺点是?

登录鉴权的本质是「确认你是谁(认证)+ 决定你能干什么(授权)」。常见方式可归纳为四类:

方式原理优点缺点
Session + Cookie服务端存会话,客户端带 session_id简单成熟、可主动失效、状态可控有状态,分布式要共享存储;有 CSRF 风险
Token(JWT)服务端签发自包含凭证,客户端自持无状态、易扩展、跨域友好难主动吊销;Payload 不加密;体积随声明增大
OAuth 2.0 / OIDC第三方授权,发授权码/令牌免密接入第三方、权限粒度可控流程复杂、实现易踩坑
SSO(单点登录)一次登录,多系统共享身份(CAS/OIDC)用户体验好、集中管理中心化依赖、单点故障风险

补充几种常见变体:

  • HTTP Basic Auth:Authorization: Basic base64(user:pass),极简但明文(需配合 HTTPS),无会话管理。
  • API Key / App Secret:服务间调用或开放平台用的静态密钥,简单但易泄露、难轮换。
  • 生物识别/OTP 二次认证(MFA):作为第一道认证之上的额外保护。
Text
Session 模式:   登录 -> 服务端存 Session -> 下发 session_id(Cookie)
                 请求 -> 带 Cookie -> 服务端查 Session -> 判身份

JWT 模式:       登录 -> 服务端签 JWT -> 客户端自持
                 请求 -> Header 带 Token -> 服务端验签 -> 判身份

OAuth 模式:     用户 -> 同意授权 -> 第三方发 code -> 换 token -> 访问资源
⚠️ 安全要点

无论哪种方式都应全站 HTTPS、Cookie 设 HttpOnly/Secure/SameSite、Token 存内存或安全存储、口令加盐哈希存储,并注意防 CSRF、XSS、重放攻击。

💡 面试加分项

不要只罗列名词,要能说「选型逻辑」:单体老系统用 Session、微服务/移动端/跨域 API 用 JWT、对接第三方用 OAuth、企业内部多系统用 SSO。再补一句「认证与授权要分离(OIDC 在 OAuth2 上补了身份层)」,基本就是优秀回答。



HTTP 1.0 和 2.0 有什么区别?

HTTP/1.0(1996,RFC 1945)是早期版本,默认短连接:一个请求完成即关闭 TCP 连接,复用需手动发 Connection: keep-alive。HTTP/2.0(2015,RFC 7540,源自 Google SPDY)把 HTTP 从「文本、串行」升级为「二进制分帧、多路复用」,在应用层大幅提升并发与传输效率。

核心区别对比:

维度HTTP/1.0HTTP/2.0
连接模型短连接,一次请求一条 TCP(复用需手动 keep-alive)单条 TCP 连接多路复用多个流
传输格式文本(明文、按行)二进制分帧(帧 Frame + 流 Stream)
头部压缩无,每次重复携带完整头部HPACK 静态表 + 哈夫曼编码压缩
并发/队头阻塞串行,一个请求阻塞后续应用层多路复用,多个请求并行
服务器推送不支持支持 Server Push
流控制/优先级无有(Stream 优先级、WINDOW_UPDATE)
ℹ️ 信息

中间还有一个 HTTP/1.1(1997):引入了长连接 keep-alive(默认开启)、管道化(Pipelining)、Host 头等,但管道化仍受**队头阻塞(Head-of-Line Blocking)**困扰——一个慢响应会堵住整条连接。HTTP/2 的多路复用正是为解决这一问题而生。

HTTP/2 的二进制分帧与多路复用示意:

Text
HTTP/1.0(串行,一条请求一个连接)
连接1: [请求A] [响应A] ── 关闭
连接2: [请求B] [响应B] ── 关闭
        ↑ 每次都要 TCP 三次握手,资源开销大

HTTP/2(单连接多路复用)
连接:  Stream1 ─ [HEADERS][DATA]──响应A
       Stream2 ─ [HEADERS][DATA]──响应B   ← 多流交织在同一 TCP 连接
       Stream3 ─ [HEADERS][DATA]──响应C
       帧可交错,靠 Stream ID 区分归属
💡 面试加分项

追问「HTTP/2 解决了什么」时,扣住**「二进制分帧 → 多路复用 → 解决队头阻塞 + 头部压缩」这条因果链;再补一句「HTTP/2 只解决应用层**队头阻塞,TCP 层的丢包重传队头阻塞要到 HTTP/3 才解决」,能体现对协议演进的完整理解。

⚠️ 警告

别把 HTTP/1.0 和 HTTP/1.1 混为一谈:1.0 默认短连接、1.1 默认长连接;「多路复用、二进制帧」是 2.0 的招牌特性。

相关:多路复用仍建立在 TCP 字节流之上,帧的切分依赖应用层协议设计,见 03-Resources/cpp/C++ 网络编程。


HTTP 2.0 和 3.0 有什么区别?

HTTP/2 仍然跑在 TCP 上,HTTP/3(2022,RFC 9114)把传输层换成了基于 UDP 的 QUIC,从根上解决了 TCP 带来的队头阻塞与握手慢两个痛点。

核心区别对比:

维度HTTP/2.0HTTP/3.0
传输层TCP(可靠字节流)UDP + QUIC(自带可靠、有序、拥塞控制)
加密TLS(可协商,多握手)TLS 1.3 内建于 QUIC,强制加密
连接建立TCP 三次握手 + TLS 握手(多 RTT)0-RTT / 1-RTT 快速握手
队头阻塞应用层多路复用,但 TCP 丢包会阻塞整条连接流独立,一个流丢包不影响其他流
连接迁移IP/端口变化需重新握手用 Connection ID 标识连接,切网不断连

QUIC 快速握手示意:

Text
HTTP/2(TCP + TLS,需要多轮握手)
客户端          服务端
  │── TCP SYN ──→│
  │←─ SYN+ACK ──│    ① TCP 三次握手(1 RTT)
  │── ACK ─────→│
  │── ClientHello →│  ② TLS 握手(1~2 RTT)
  │←─ ServerHello ─│
  合计约 2~3 RTT 才能发出第一个 HTTP 请求

HTTP/3(QUIC,握手与 TLS 合并,支持 0-RTT)
客户端          服务端
  │─ Initial(CHLO+密钥) →│  ① 1-RTT 即建立连接并加密
  │─ 0-RTT 数据直接发出 →│  ② 复用会话时首个请求可 0-RTT 发送
ℹ️ 信息

QUIC 的三大卖点:① 建连快(0-RTT)② 无 TCP 队头阻塞(流独立)③ 连接迁移(Connection ID)。代价是 UDP 上的可靠传输、拥塞控制、TLS 全部要 QUIC 自己实现,内核态 → 用户态(如 Google quiche、Cloudflare quiche / mvfst)带来额外开销,且部分老旧网络/中间设备对 UDP 不友好。

💡 面试加分项

点破「HTTP/3 解决的是 TCP 的队头阻塞,而 HTTP/2 只解决应用层队头阻塞」这一句,是这题最能拉开差距的回答;再补「QUIC 让 HTTP 的语义不变、只换了传输层,上层 API 几乎无感」。

相关:TCP 字节流与 UDP 报文模型的差异(也是粘包产生的根因),见 03-Resources/cpp/C++ 网络编程。


常见的 HTTP 状态码有哪些?

HTTP 状态码由三位数字组成,第一位代表类别,共五类:

Text
1xx  信息响应:请求已收到,继续处理
2xx  成功:请求被成功接收、理解、处理
3xx  重定向:需要进一步操作才能完成请求
4xx  客户端错误:请求有语法错误或无法完成(错在客户端)
5xx  服务端错误:服务器处理请求失败(错在服务端)

常见状态码速查:

状态码含义一句话记忆
200 OK请求成功默认成功
201 Created已创建(POST 新建资源)新建成功
204 No Content成功但无响应体成功、无 body
206 Partial Content部分内容(断点续传/分片)Range 请求
301 Moved Permanently永久重定向永久搬家
302 Found临时重定向临时借用
304 Not Modified未修改(走缓存)用缓存
400 Bad Request请求语法错误请求有毛病
401 Unauthorized未认证(需登录)没登录
403 Forbidden已认证但无权限没权限
404 Not Found资源不存在找不到
405 Method Not Allowed方法不允许方法不对
429 Too Many Requests请求过于频繁(限流)限流
500 Internal Server Error服务器内部错误服务器崩了
502 Bad Gateway网关错误(上游无效响应)网关拿不到好响应
503 Service Unavailable服务不可用(过载/维护)服务挂了
504 Gateway Timeout网关超时(上游没及时响应)上游超时
💡 面试加分项

高频追问是**「301 和 302 区别」「502 和 504 区别」:301 是永久重定向(浏览器会缓存新地址),302 是临时**;502 是上游返回了无效响应,504 是上游超时未响应。再补「304 与缓存协商(ETag/Last-Modified)」能体现对缓存机制的理解。

⚠️ 警告

别把 401 和 403 搞混:401 是「没登录/没认证」,403 是「登录了但没权限」——面试常拿这俩考基本功。


HTTP 请求包含哪些内容,请求头和请求体有哪些类型?

一个 HTTP 请求报文由四部分组成:请求行 → 请求头 → 空行 → 请求体。请求行包含「方法 + 目标路径 + 协议版本」,空行(\r\n\r\n)是头部与体的分界。

Text
POST /api/user HTTP/1.1        ← 请求行:方法 路径 版本
Host: www.example.com           ← 请求头(若干行 key: value)
Content-Type: application/json
Content-Length: 27

{"name": "alice", "age": 20}   ← 请求体(可有可无)

请求头按作用可分为几类:

类型说明常见字段
通用头请求/响应都可用Cache-Control、Connection、Date
请求头描述请求/客户端Host、User-Agent、Accept、Accept-Encoding、Authorization、Cookie、Referer
实体头描述 body 本身Content-Type、Content-Length、Content-Encoding、Last-Modified
自定义/扩展头应用自定义X-Request-Id、X-Forwarded-For 等

请求体类型由 Content-Type 决定,常见几种:

Content-Type用途特点
application/x-www-form-urlencoded普通表单提交key=value&key2=value2,URL 编码
multipart/form-data文件上传 + 表单混合用 boundary 分隔多段,可传二进制
application/jsonJSON 数据现代 API 主流,结构化强
text/plain纯文本简单原始文本
application/octet-stream二进制流任意二进制文件
💡 面试加分项

关键区分 Content-Length(定长 body)与 Transfer-Encoding: chunked(分块传输):前者靠长度字段切 body,后者靠每块的十六进制长度 + 结束块 0\r\n\r\n 切分——这正是「服务端如何知道 body 读多少」的答案。

⚠️ 警告

请求体的长度**不能只靠「读到连接关闭」**判断:HTTP/1.1 长连接下必须依赖 Content-Length 或 chunked,否则会与下一条请求粘连。

相关:Content-Length 切 body 就是应用层协议解决 TCP 粘包/半包的具体实践,见 03-Resources/cpp/C++ 网络编程;服务端读请求的时机由 IO 就绪事件驱动,见 03-Resources/cpp/C++ 网络编程。


HTTP 和 HTTPS 有什么区别?

HTTPS = HTTP over TLS/SSL:在 HTTP 与 TCP 之间插入一层 TLS 加密,对报文做**机密性(加密)、完整性(防篡改)、身份认证(防冒充)**三件事。HTTP 则是明文传输。

核心区别对比:

维度HTTPHTTPS
加密明文,可被嗅探/篡改TLS 加密(对称 + 非对称 + 哈希)
端口80443
证书无需需 CA 签发证书(身份认证)
连接层HTTP 直接跑在 TCP 上HTTP → TLS → TCP
性能无加解密开销握手 + 加解密有额外 CPU/时延开销
安全性无防窃听、防篡改、防中间人

TLS 握手(ECDHE 简化)时序:

Text
客户端                         服务端
  │──── ClientHello ────────────→│   ① 客户端给出支持的加密套件/随机数
  │←── ServerHello + 证书 ──────│   ② 服务端选套件、发证书与随机数
  │←── ServerKeyExchange/Finished│
  │── 校验证书 + 生成预主密钥 ───→│   ③ 双方算出会话密钥(对称)
  │──── Finished(加密生效)────→│
  │⇄═════ 之后的 HTTP 报文全部对称加密传输 ═════⇄
ℹ️ 信息

HTTPS 用混合加密:非对称加密(RSA/ECDHE)只用来安全协商出会话密钥,之后的海量数据用对称加密(AES),兼顾安全与性能;再配合 HMAC/哈希保证完整性。证书链由 CA 逐级签发,防止中间人伪造。

💡 面试加分项

能答出「对称加密保性能 + 非对称加密保安全 + 证书保身份」这套组合拳,并说出「HTTPS 只保证传输安全,不保证服务器本身可信、也不能防端点被攻破」,就比只背「HTTPS 就是加密版 HTTP」高一个层次。

⚠️ 警告

HTTPS ≠ 绝对安全:它防的是传输途中的窃听/篡改,防不了应用层漏洞(SQL 注入、越权)与客户端被恶意软件控制。

相关:HTTPS 仍基于 TCP 的可靠字节流,连接与粘包机制见 03-Resources/cpp/C++ 网络编程。


HTTP 中 GET 和 POST 的区别是什么?

两者都是 HTTP 请求方法,语义上:GET 用于获取资源(只读、幂等、可缓存),POST 用于提交/创建数据(通常改变服务端状态、非幂等)。这是最本质的区别,其余差异多由此派生。

核心区别对比:

维度GETPOST
语义获取资源提交/创建数据
参数位置URL 查询串 ?a=1&b=2请求体(body)
幂等性幂等(多次调用结果相同)非幂等(多次提交可能重复创建)
缓存可被缓存、可收藏为书签一般不缓存
安全性(可见性)参数暴露在 URL(会被浏览器历史/日志记录)参数在 body,相对不易泄漏
长度限制URL 长度受限(浏览器/服务器)body 长度基本无上限
编码仅 URL 编码支持多种 Content-Type
Text
GET /api/user?id=123&name=alice HTTP/1.1     ← 参数在 URL,明文可见
Host: www.example.com

POST /api/user HTTP/1.1                       ← 参数在 body
Host: www.example.com
Content-Type: application/json
Content-Length: 27

{"id": 123, "name": "alice"}
💡 面试加分项

这题的「高分答法」是先讲语义与幂等(GET 幂等可缓存、POST 非幂等改状态),再讲参数位置与可见性(GET 在 URL、POST 在 body),最后补「RESTful 规范建议:GET 不应有副作用,创建/更新用 POST/PUT」——体现的是设计思维而非死记。

⚠️ 警告

「GET 不安全、POST 安全」是常见误区:两者本质都不安全,POST 只是参数不进 URL、不易被日志/历史直接看到;真要安全必须上 HTTPS,否则 body 一样明文传输。

相关:POST 的 body 靠 Content-Length/chunked 定界,见 03-Resources/cpp/C++ 网络编程;body 切分底层是 TCP 粘包问题,见 03-Resources/cpp/C++ 网络编程。


WebSocket 与 HTTP 有什么区别?

WebSocket(RFC 6455)是在 HTTP 之上通过 Upgrade 握手升级得到的全双工、长连接协议,用于服务端主动推送与双向实时通信。HTTP 则是请求-响应、一问一答的无状态协议(服务端不能主动推)。

核心区别对比:

维度HTTPWebSocket
通信方向单向请求-响应(服务端不能主动发)全双工,双方随时互发
连接生命周期短连接(1.1 可复用但每次请求-响应)一次握手后长连接保持
消息格式请求行/头/体(文本)二进制帧(opcode + payload)
每次通信开销每次携带完整头部帧头极小(2~14 字节),无重复头
应用场景静态页面、普通 API聊天、游戏、行情推送、实时协作
状态无状态有连接状态

WebSocket 握手(本质是一次 HTTP Upgrade):

Text
客户端                                   服务端
  │── GET /chat HTTP/1.1 ──────────────→│
  │   Host: example.com                  │
  │   Upgrade: websocket                 │  ① 客户端申请升级
  │   Connection: Upgrade                │
  │   Sec-WebSocket-Key: xxx(base64)     │
  │←── HTTP/1.1 101 Switching Protocols ─│  ② 服务端同意升级
  │   Sec-WebSocket-Accept: yyy          │
  │⇄══════ 之后切换为 WebSocket 帧双向通信 ══════⇄
ℹ️ 信息

与「HTTP 长轮询 / 轮询」对比:长轮询靠客户端反复请求模拟推送,每次都有 HTTP 头部开销、且存在响应时延;WebSocket 升级后帧头极小、双向即时,是真正为实时设计的协议。

💡 面试加分项

主动提「心跳保活(Ping/Pong 帧)」和「断开重连 + 消息补发」这两个工程点,能证明你不只会背协议,还处理过 WebSocket 长连接的可靠性问题。

相关:WebSocket 长连接的服务端通常用 epoll/Reactor 高效承载海量连接,见 03-Resources/cpp/C++ 网络编程 与 03-Resources/cpp/C++ 网络编程;帧边界同样是应用层协议设计,见 03-Resources/cpp/C++ 网络编程。


服务端是如何解析 HTTP 请求的数据?

服务端解析 HTTP 请求的本质是:在 TCP 字节流上,用「累积缓冲区 + 状态机」按 HTTP 文法把一段连续字节切成「请求行 / 头部 / body」并交给业务。因为 TCP 是字节流、无消息边界,必须处理半包(数据没到齐)与粘包(多条请求连在一起)。

整体流程:

Text
accept 新连接 → 注册到 epoll 监听可读
       ↓ epoll 通知可读
read 数据追加进「接收缓冲区」
       ↓
在缓冲区中查找 "\r\n\r\n" 定界头部
       ↓(未找到 → 半包,继续等下一次 read)
解析请求行:方法、路径、版本
解析请求头:逐个 "Key: Value" 行
       ↓
按 Content-Length 或 Transfer-Encoding: chunked 确定 body 长度
       ↓(body 没读满 → 继续缓存等待)
取出完整 body → 交给业务处理(Router/Handler)
       ↓
按 Keep-Alive 决定是否继续在同一连接解析下一条请求

简化 C++ 解析骨架(状态机思路):

C++
#include <string>
#include <string_view>

// 简化示意:从累积缓冲区中切出一条完整 HTTP 请求
// 返回 true 表示已切出一条完整请求并填入 req;否则数据未到齐(半包)
bool parse_request(std::string& buf, std::string& req) {
    auto header_end = buf.find("\r\n\r\n");      // 头部与 body 的分界
    if (header_end == std::string::npos) {
        return false;                             // 头部还没收全,等下一次 read
    }
    // 解析头部中的 Content-Length
    size_t content_length = 0;
    std::string_view headers(buf.data(), header_end);
    if (auto pos = headers.find("Content-Length: "); pos != std::string_view::npos) {
        content_length = std::stoul(std::string(
            headers.substr(pos + 16)));           // 取出长度值
    }
    size_t total = header_end + 4 + content_length;
    if (buf.size() < total) {
        return false;                             // body 还没到齐(半包)
    }
    req = buf.substr(0, total);                   // 切出完整请求
    buf.erase(0, total);                          // 剩余字节留给下一条(粘包)
    return true;
}
ℹ️ 信息

生产级解析用状态机而非简单的 find:按「读请求行 → 读头部 → 读 body」逐步推进,任何一步数据不足就挂起状态、等下次可读继续,从而正确处理半包/粘包。成熟实现可参考 http-parser(Node.js 用)或 llhttp 的自动机。

⚠️ 警告

解析器还要防两类攻击:① 超大头部/超长 body 耗尽内存(限制 max_header_size、max_body_size);② 慢速攻击(Slowloris)——恶意客户端一小段一小段地发、占住连接不释放,需设置读超时。

💡 面试加分项

把「epoll 通知可读 → 累积缓冲区 → 状态机切包 → 处理半包粘包」这条链路串起来讲,并主动提到「用 \r\n\r\n 定界头部 + Content-Length/chunked 定 body」,基本就覆盖了这题的全部考察点。

相关:这套「累积缓冲区 + 按协议切包」正是解决 TCP 粘包/拆包的通用方法,见 03-Resources/cpp/C++ 网络编程;读事件由 epoll 驱动,见 03-Resources/cpp/C++ 网络编程。