文章目录
- 1. HTTP 和 HTTPS 的差别
- 2. 加密方式
- 3. TCP 挥手过程(四次挥手)
- 4. TIME_WAIT 作用
- 5. HTTP/1.0 1.1 2.0 3.0
- 6. 偏特化(Partial Specialization)
- 7. 指针常量(Pointer Constant)
- 8. malloc 函数的原理
- 9. Linux 物理页大小
- 10. 物理页仅用 1K 时的管理
- 11. Linux 查看 CPU 状态/内存的命令
- 12. GDB 断点与堆栈命令
- 13. TCP 拥塞控制原理
- 14. 拥塞避免(Congestion Avoidance)
- 15. TIME_WAIT 状态的目的(复述)
- 16. 用户级内存池实现[给你一块内存,怎么管理起来]
- 17. Buffer 和 Cache 的区别
- 18. 字节序(Endianness)
- 19. 索引的目的
- 20. MySQL 索引原理(InnoDB vs MyISAM)
- 21. 多线程使用场景
- 22. 线程池参数设置
- 23. 线程安全的单例模式
- 24. Docker 与 Kubernetes(k8s)基础
- 25. Map 类型选择
- 26. HashTable 线程安全实现
- 27. 索引失效场景
- 28. 慢查询优化步骤
- 29. 优化后仍慢的解决方案
- 30. 行锁 vs 表锁
- 分库分表落地示例(补充)
- 31. 乐观锁与悲观锁的实现
- 32. Redis 使用场景
- 33. Redis 作为注册中心的优劣
- 34. Redis 分布式锁的时钟问题
- 35. 时钟问题解决方案
- 36. Redis 持久化方案
- 37. 进程通信(IPC)方式
- 38. RPC 与常用框架
- 39. 同步调用 vs 异步调用
- 40. Linux 常用命令
- 分库分表补充(应对面试官质疑)
- 1. **查询HTTPS是否数据安全的命令及注意事项**
- 2. **调用HTTPS服务的方法(C++后端)**
- 3. **消息队列使用经验**
- 4. **消息不丢失与顺序性保证**
- 5. **一致性哈希算法**
- 6. **Redis与MySQL一致性**
- 7. **本地缓存与Redis协调**
- 8. **技术敏感度保持**
- 9. **为何离开百度寻找新实习**
- **1. 补充项目测试结果:分析极限情况下的性能瓶颈**
- **2. Vector如何主动释放内存空间**
- **3. 稳定排序算法**
- **4. TCP三次握手的数据携带问题**
- **5. select 和 epoll 的适用场景**
- 模拟
- **1. C++ HTTP服务器线程模型与并发能力**
- **2. 服务崩溃原因分析**
- **3. 大批量请求下的服务端状态**
- **4. CGI机制详解**
- **FastCGI**
- **FastCGI 是什么?**
- **FastCGI 的核心特性**
- **FastCGI 工作原理**
- **FastCGI 的优势**
- **典型应用场景**
- **FastCGI 的局限性**
- **常见问题与解决方案**
- **总结**
- HTTP/2 与 gRPC
- **HTTP/2 与 gRPC 的核心对比**
- **HTTP/2 的核心特性**
- **gRPC 的核心特性**
- **与 FastCGI 的对比**
- **如何选择?**
- **示例:gRPC 服务端与客户端(Python)**
- **总结**
- **5. 进程创建与通信**
- **6. Cookie vs Session**
- **7. 线程池的作用与优势**
- **8. Vector内存释放**
- **9. 红黑树 vs 哈希表**
- **10. 右值引用解决的问题**
- **11. 稳定排序算法**
- **12. TCP三次握手的必要性**
- **13. TCP握手阶段的数据携带**
- **14. TCP数据安全性**
- **15. 网线拔插对TCP连接的影响**
- **1. 物理层 & 数据链路层的影响**
- **2. TCP 层的行为**
- **(1) 服务器仍然认为连接是“活跃的”**
- **(2) 服务器继续发送数据**
- **(3) TCP 超时重传机制生效**
- **3. 可能出现的异常**
- **(1) 客户端重新插回网线**
- **(2) 客户端 IP 地址变化**
- **(3) TCP Keepalive**
- **4. 结论**
- **短时间内拔插网线**
- **长时间断开**
- **影响总结**
- **附:TCP 连接断开的三种情况**
- **5. 代码实验(模拟拔网线)**
- **模拟拔网线**
- **模拟插回网线**
- **总结**
- **16. HTTP传输层协议**
- **HTTP 在传输层使用的协议**
- **为什么 HTTP 默认使用 TCP?**
- **HTTP 可以使用 UDP 吗?**
- **HTTP + UDP 的情况**
- **结论**
- **17. 优雅关闭Socket**
- **1. shutdown() + close()**
- **2. 使用 SO_LINGER 控制关闭行为**
- **3. 正常 TCP 四次挥手**
- **1. TCP 连接的正常关闭**
- **2. `close()` VS `shutdown()`**
- **(1)close(fd)**
- **(2)shutdown(fd, how)**
- **3. 优雅关闭的实现**
- **(1)客户端优雅关闭**
- **(2)服务器优雅关闭**
- **4. 总结**
- **18. 数据库事务四大特性**
- 1. **原子性(Atomicity)**
- 2. **一致性(Consistency)**
- 3. **隔离性(Isolation)**
- 4. **持久性(Durability)**
- **持久性保证不会丢失数据吗?**
- **如何尽量避免数据丢失?**
- **总结**
- **19. epoll vs select**
- **核心原因**
- **关键优势分析**
- **适用场景总结**
- **实际测试数据**
- **结论**
1. HTTP 和 HTTPS 的差别
- 安全性:明文传输:ssl+tls
- 端口号:80:443
- CA机构颁发证书
- 性能开销
2. 加密方式
- 非对称加密(如 RSA、ECC):
用于密钥交换。客户端用服务器公钥加密对称密钥,服务器用私钥解密。 - 对称加密(如 AES、ChaCha20):
建立安全连接后,使用协商的对称密钥高效加密数据。 - 完整性校验:
通过 HMAC 或 AEAD(如 AES-GCM)防止数据篡改。
3. TCP 挥手过程(四次挥手)
- 主动方发送 FIN:进入
FIN_WAIT_1
,表示不再发送数据。 - 被动方回复 ACK:进入
CLOSE_WAIT
,通知应用层处理剩余数据。 - 被动方发送 FIN:进入
LAST_ACK
,表示数据已发送完毕。 - 主动方回复 ACK:进入
TIME_WAIT
,等待 2MSL(确保被动方收到 ACK),最后关闭连接。
4. TIME_WAIT 作用
- 确保可靠终止:
若最后一次 ACK 丢失,被动方会重传 FIN,主动方需重新响应。 - 避免旧连接干扰:
等待 2MSL(报文最大生存时间),确保网络中旧连接的报文失效,防止与新连接冲突。
5. HTTP/1.0 1.1 2.0 3.0
- 1.0:短连接 非标准字段强制开启长连接
- 1.1:长连接+管道即一个TCP链接同时发送多个请求
- 2.0:二进制分帧提供多路复用解决对头阻塞+相同头部压缩+服务端推送缓存给客户端
- 3.0:二进制分帧提供多路复用解决对头阻塞+基于UDP 实现的 QUIC 协议+
- 0-RTT 连接:复用先前会话密钥,快速建立连接。
- 连接迁移:网络切换(如 Wi-Fi 到 4G)时无需重建连接。
- 内置加密:默认整合 TLS 1.3,安全性更高。
6. 偏特化(Partial Specialization)
-
概念:
对模板的部分参数特化,而非全部。例如:template <typename T, typename U> class A {}; // 通用模板 template <typename T> class A<T, int> {}; // 偏特化:第二个参数为 int
-
应用场景:
- 优化特定类型(如
vector<T*>
特化以减少内存占用)。 - 类型萃取(如
is_pointer<T>
判断是否为指针)。
- 优化特定类型(如
7. 指针常量(Pointer Constant)
-
定义:
指针本身是常量,不可修改指向的地址,但可修改指向的值。int a = 1, b = 2; int *const ptr = &a; // ptr 不能指向其他地址(如 ptr = &b 非法) *ptr = 3; // 允许修改 a 的值
8. malloc 函数的原理
- 底层机制:
- 小内存:通过
brk
调整堆顶,从堆区分配,维护空闲块链表(如 glibc 的 ptmalloc,使用 fast bins、small bins 等)。 - 大内存:直接通过
mmap
映射匿名内存页,避免碎片。
- 小内存:通过
- 碎片处理:
合并相邻空闲块(coalescing),分割块以满足请求大小。
9. Linux 物理页大小
- 默认大小:
多数架构(如 x86_64)物理页为 4KB,大页(HugePage)可配置为 2MB 或 1GB。 - 管理策略:
- 伙伴系统:管理连续物理页,解决外部碎片。
- slab 分配器:将页拆分为小对象(如 task_struct),减少内部碎片。
- 用户空间管理:如
malloc
将页划分为更小块(通过内存池或 slab 机制)。
10. 物理页仅用 1K 时的管理
- 内核层面:
伙伴系统和 slab 分配器管理物理页,但 不关心应用层如何使用内存。 - 用户空间管理:
malloc
将分配的页切割为更小单位(如 16B、32B),通过空闲链表管理。- 例如,4KB 页可能被分为 32 个 128B 块,未使用的块保留在链表中供后续分配。
- 内部碎片:
属于应用层内存管理的固有代价,需通过高效分配算法(如 jemalloc、tcmalloc)优化。
11. Linux 查看 CPU 状态/内存的命令
- CPU 状态:
top
/htop
:实时查看 CPU 使用率、进程状态。mpstat -P ALL
:查看每个 CPU 核心的利用率。vmstat 1
:监控系统整体状态(CPU、内存、I/O)。
- 内存状态:
free -h
:显示内存和交换空间总量及使用量。cat /proc/meminfo
:查看详细内存分配信息。
12. GDB 断点与堆栈命令
- 断点命令:
- 设置断点:
break <函数名>
或b <文件名:行号>
- 条件断点:
break <位置> if <条件>
(如b main.c:10 if x==5
) - 查看断点:
info breakpoints
- 删除断点:
delete <断点编号>
- 设置断点:
- 查看堆栈:
backtrace
(或bt
):显示当前调用堆栈。frame <编号>
:切换到指定堆栈帧,查看局部变量。
13. TCP 拥塞控制原理
- 核心目标:避免网络过载,动态调整发送速率。
- 关键机制:
- 慢启动(Slow Start):初始窗口指数增长,直到阈值(ssthresh)。
- 拥塞避免(Congestion Avoidance):窗口线性增长。
- 快速重传(Fast Retransmit):收到 3 个重复 ACK 时立即重传丢失报文。
- 快速恢复(Fast Recovery):重传后不重置窗口到初始值,继续拥塞避免阶段。
14. 拥塞避免(Congestion Avoidance)
- 定义:TCP 进入稳定传输阶段后,窗口按线性增长(每 RTT 增加 1 MSS),避免窗口膨胀导致网络拥塞。
- 触发条件:当窗口大小达到慢启动阈值(ssthresh)后,进入拥塞避免阶段。
15. TIME_WAIT 状态的目的(复述)
- 核心作用:
- 确保被动关闭方收到最后的 ACK,防止其重传 FIN。
- 等待 2MSL(报文最大生存时间),确保网络中旧连接的报文失效,避免与新连接混淆。
16. 用户级内存池实现[给你一块内存,怎么管理起来]
-
设计要点:
- 预分配大块内存:通过
malloc
或mmap
申请一大块内存作为池。 - 划分内存块:将池划分为固定大小块(如 4KB)或动态分块。
- 空闲链表管理:用链表记录空闲块,分配时取链表头部,释放时插回链表。
- 碎片优化:合并相邻空闲块,或按不同块大小分级管理(类似 slab)。
- 预分配大块内存:通过
-
示例伪代码:
struct MemoryBlock { void* start; size_t size; MemoryBlock* next; }; MemoryBlock* free_list; // 空闲块链表 void* my_alloc(size_t size) { // 从 free_list 中查找合适块,分割或直接分配 } void my_free(void* ptr) { // 将释放的块插回 free_list,尝试合并相邻块 }
17. Buffer 和 Cache 的区别
- Buffer:
- 目的:缓冲写入操作(如磁盘 I/O),合并多次小写为单次大写,减少系统调用。
- 场景:内核将磁盘写入暂存到缓冲区,定期刷盘。
- Cache:
- 目的:缓存读取数据(如文件内容),加速后续访问。
- 场景:文件被首次读取后,内容缓存在内存中供下次快速访问。
18. 字节序(Endianness)
- 定义:数据在内存中的存储顺序:
- 大端序(Big-Endian):高位字节在前(如网络字节序,
0x1234
存储为12 34
)。 - 小端序(Little-Endian):低位字节在前(如 x86 CPU,
0x1234
存储为34 12
)。
- 大端序(Big-Endian):高位字节在前(如网络字节序,
19. 索引的目的
- 核心作用:
- 加速数据检索,减少全表扫描的 I/O 开销。
- 强制唯一性约束(如唯一索引)。
- 代价:
- 占用额外存储空间。
- 增删改操作需维护索引,降低写入速度。
20. MySQL 索引原理(InnoDB vs MyISAM)
-
存储引擎对比:
特性 InnoDB(聚簇索引) MyISAM(非聚簇索引) 数据与索引存储 数据按主键顺序存储 数据与索引分开存储 主键查找 直接定位数据页 先查索引,再根据地址读数据 非主键索引 叶子节点存储主键值 叶子节点存储数据地址 -
B+ 树深度:
- 假设单节点存储 1000 条记录:
- 3 层 B+ 树可支持约 10 亿条数据(1000^3)。
- 实际深度通常为 3~4 层(与数据量和节点大小相关)。
- 假设单节点存储 1000 条记录:
21. 多线程使用场景
- 常见场景:
- 任务并行:分解计算密集型任务(如图像处理、数据批处理)。
- 异步 I/O:处理网络请求、文件读写,避免阻塞主线程。
- 生产者-消费者模型:通过队列解耦任务生产与消费(如日志系统)。
- 定时任务:定时触发线程执行清理、统计等操作。
22. 线程池参数设置
- 核心参数:
- 核心线程数(corePoolSize):常驻线程,根据任务类型设置:
- CPU 密集型:
核心数 + 1
(避免上下文切换过多)。 - IO 密集型:
2 * CPU 核心数
(充分利用等待时间)。
- CPU 密集型:
- 最大线程数(maxPoolSize):突发流量时扩容上限,需避免 OOM。
- 任务队列:
- 有界队列(如
ArrayBlockingQueue
):防止资源耗尽,需配合拒绝策略。 - 无界队列(如
LinkedBlockingQueue
):可能导致内存溢出。
- 有界队列(如
- 拒绝策略:如丢弃任务、调用者执行、抛出异常等。
- 核心线程数(corePoolSize):常驻线程,根据任务类型设置:
23. 线程安全的单例模式
-
实现方式:
-
双检锁(C++11 后安全):
class Singleton { public: static Singleton* getInstance() { if (instance == nullptr) { std::lock_guard<std::mutex> lock(mutex); if (instance == nullptr) { instance = new Singleton(); } } return instance; } private: static Singleton* instance; static std::mutex mutex; };
-
局部静态变量(C++11 起线程安全):
Singleton& Singleton::getInstance() { static Singleton instance; return instance; }
-
24. Docker 与 Kubernetes(k8s)基础
- Docker:容器化技术,打包应用及依赖到镜像,实现环境一致性。
- Kubernetes:容器编排系统,管理容器集群,核心功能:
- 自动扩缩容:根据负载调整 Pod 数量。
- 服务发现:通过 Service 暴露服务。
- 故障恢复:重启异常容器或节点迁移。
25. Map 类型选择
- 场景与实现:
- 有序 Map:
- C++
std::map
(红黑树),JavaTreeMap
。 - 适用范围:需按序遍历或范围查询。
- C++
- 哈希 Map:
- C++
std::unordered_map
,JavaHashMap
。 - 适用场景:快速单点查询,无需有序。
- C++
- 并发 Map:
- Java
ConcurrentHashMap
(分段锁),C++ TBBconcurrent_hash_map
。
- Java
- 有序 Map:
26. HashTable 线程安全实现
- 常见方案:
- 全局锁:简单但性能差(如
Collections.synchronizedMap
)。 - 分段锁(ConcurrentHashMap):将哈希表分片,每段独立加锁。
- 无锁 CAS:通过原子操作实现无锁访问(适用于高并发读场景)。
- 全局锁:简单但性能差(如
27. 索引失效场景
- 常见原因:
- 违反最左前缀原则:联合索引未从最左列开始查询。
- 隐式类型转换:如字符串字段用数字查询。
- 函数或表达式:
WHERE YEAR(date) = 2023
。 - OR 条件未全覆盖索引:部分条件无法使用索引。
28. 慢查询优化步骤
- EXPLAIN 分析:查看执行计划,确认是否走索引。
- 索引优化:添加缺失索引或优化索引顺序。
- 查询重写:避免
SELECT *
、拆分复杂查询。 - 数据归档:清理历史数据,减少扫描范围。
29. 优化后仍慢的解决方案
- 分库分表:
- 垂直分表:按业务拆分表(如用户表与订单表分离)。
- 水平分表:按哈希或范围分片(如订单按用户 ID 分 10 表)。
- 硬件升级:提升 CPU、内存、使用 SSD。
- 缓存层:引入 Redis 缓存热点数据。
30. 行锁 vs 表锁
- 行锁(如 InnoDB):
- 场景:高并发写(如订单支付),锁粒度细,冲突少。
- 实现:通过索引加锁,若未走索引则退化为表锁。
- 表锁(如 MyISAM):
- 场景:批量更新、全表扫描,锁粒度粗,并发度低。
分库分表落地示例(补充)
- 中间件选型:
- ShardingSphere:通过代理或 JDBC 驱动实现透明分片。
- 分片键设计:选择离散度高且查询频繁的字段(如用户 ID)。
- 数据迁移:双写过渡 + 历史数据同步工具。
注:若面试官质疑分库分表方案,可强调具体场景的权衡(如分片后跨片查询复杂度),并补充监控和弹性扩容策略。
31. 乐观锁与悲观锁的实现
-
悲观锁:
- 实现:直接加锁,确保独占访问。
- 数据库:
SELECT ... FOR UPDATE
(行级锁)。 - Java:
synchronized
关键字或ReentrantLock
。
- 数据库:
- 场景:写操作频繁,竞争激烈(如账户扣款)。
- 实现:直接加锁,确保独占访问。
-
乐观锁:
- 实现:通过版本号或 CAS(Compare And Swap)实现无锁化。
- 数据库:
UPDATE table SET val = new_val, version = version + 1 WHERE id = x AND version = old_version
。 - Java:
AtomicInteger
等原子类(基于 CPU 的 CAS 指令)。
- 数据库:
- 场景:读多写少,冲突概率低(如库存更新)。
- 实现:通过版本号或 CAS(Compare And Swap)实现无锁化。
32. Redis 使用场景
- 核心用途:
- 缓存:缓存热点数据(如用户信息),减轻数据库压力。
- 发布订阅:实现消息广播(如订单状态通知)。
- 排行榜:通过
ZSET
(有序集合)存储实时排名(如游戏积分榜)。 - 分布式锁:利用
SETNX
+ 过期时间实现互斥控制。 - 注册中心:临时存储服务节点地址(配合心跳机制)。
33. Redis 作为注册中心的优劣
- 优势:
- 高性能:读写速度快,适合高频服务发现。
- 简单易用:无需额外组件(对比 ZooKeeper/Etcd)。
- 劣势:
- 数据可靠性:持久化可能丢失数据(需权衡 AOF/RDB 配置)。
- 一致性弱:主从同步延迟可能导致短暂不一致。
- 功能局限:缺乏健康检查、Watcher 机制(需自行实现)。
34. Redis 分布式锁的时钟问题
- 问题描述:
- 客户端 A 获取锁后因 GC 或网络延迟,锁过期被释放,客户端 B 获取锁后,A 仍认为持有锁,导致数据冲突。
- 根因:各机器本地时钟不同步,锁过期时间计算偏差。
35. 时钟问题解决方案
- RedLock 算法:
- 向多个 Redis 节点申请锁,过半成功才算获取锁。
- 争议点:依赖“网络延迟和时钟漂移可控”假设,实际复杂场景可能失效。
- 租约机制:
- 锁设置较短过期时间,客户端启动后台线程定期续期(如 Redisson 的
WatchDog
)。
- 锁设置较短过期时间,客户端启动后台线程定期续期(如 Redisson 的
- 单调时间戳:
- 使用逻辑时钟(如 HLC)替代物理时钟,避免回拨问题。
36. Redis 持久化方案
- RDB(快照):
- 原理:定时 fork 子进程生成内存快照文件(
.rdb
)。 - 优点:恢复速度快,文件紧凑。
- 缺点:可能丢失最后一次快照后的数据。
- 原理:定时 fork 子进程生成内存快照文件(
- AOF(追加日志):
- 原理:记录所有写操作命令(
appendonly.aof
),支持每秒/每次同步。 - 优点:数据丢失少(配置为
appendfsync always
时)。 - 缺点:文件大,恢复慢。
- 原理:记录所有写操作命令(
- 混合模式(Redis 4.0+):
- RDB 全量 + AOF 增量,兼顾速度和可靠性。
37. 进程通信(IPC)方式
- 管道(Pipe):单向通信,父子进程间使用(如
ls | grep txt
)。 - 消息队列(如 RabbitMQ、POSIX mq):解耦生产者和消费者。
- 共享内存:通过
shmget
创建,高效但需同步机制(信号量)。 - 信号(Signal):异步通知(如
kill -9
)。 - 套接字(Socket):跨网络通信(TCP/UDP)。
38. RPC 与常用框架
- RPC(Remote Procedure Call):
- 像调用本地方法一样调用远程服务,核心步骤:
- 序列化:将参数转换为二进制(如 Protobuf)。
- 传输:通过 TCP/HTTP 发送请求。
- 反序列化:服务端解析并执行方法,返回结果。
- 像调用本地方法一样调用远程服务,核心步骤:
- 常见框架:
- gRPC:基于 HTTP/2 和 Protobuf,跨语言支持。
- Dubbo:阿里开源,集成服务治理(熔断、负载均衡)。
- Thrift:Facebook 开发,支持多种传输协议。
39. 同步调用 vs 异步调用
- 同步调用:
- 调用方阻塞等待结果返回(如
HttpClient.get()
)。 - 场景:需立即获取结果的简单逻辑。
- 调用方阻塞等待结果返回(如
- 异步调用:
- 调用方提交请求后继续执行,通过回调或 Future 获取结果(如
CompletableFuture
)。 - 场景:高并发、长耗时操作(如批量文件处理)。
- 调用方提交请求后继续执行,通过回调或 Future 获取结果(如
40. Linux 常用命令
- 系统监控:
top
/htop
:实时资源监控。vmstat
/iostat
:查看 CPU、内存、I/O 状态。
- 文件操作:
grep
/awk
/sed
:文本搜索与处理。find
:文件查找(如find / -name "*.log"
)。
- 网络调试:
netstat -tuln
:查看端口监听状态。tcpdump
:抓包分析(如tcpdump port 80
)。
- 进程管理:
ps aux
:查看所有进程。kill -9 <PID>
:强制终止进程。
分库分表补充(应对面试官质疑)
- 水平分片:
- 分片键选择:确保数据均匀分布(如用户 ID 哈希)。
- 路由策略:客户端或中间件(如 ShardingSphere)根据分片键路由。
- 垂直分片:
- 业务拆分:将字段按业务分离到不同表(如用户基础信息与扩展信息)。
- 挑战:
- 跨分片查询:通过汇总多个分片结果或冗余数据解决。
- 事务一致性:使用最终一致性或分布式事务(如 Seata)。
1. 查询HTTPS是否数据安全的命令及注意事项
命令:
curl -v https://example.com
:查看SSL握手细节,验证证书信息。openssl s_client -connect example.com:443
:检查证书链、协议版本、加密算法等。- 浏览器开发者工具(Security Tab):可视化检查证书有效性、加密协议。
- 工具如
nmap --script ssl-enum-ciphers
:扫描服务器支持的加密套件。
注意事项:
HTTPS本身通过加密和证书机制保障安全,但需注意:
- 证书伪造:需验证证书颁发机构(CA)是否受信,域名是否匹配。
- 协议/算法漏洞:如禁用TLS 1.0/1.1,避免弱加密算法(如RC4)。
- 中间人攻击:确保客户端不忽略证书错误(如curl加
--cacert
指定CA)。
2. 调用HTTPS服务的方法(C++后端)
-
使用HTTP库:如
libcurl
,需配置SSL/TLS参数:CURL *curl = curl_easy_init(); curl_easy_setopt(curl, CURLOPT_URL, "https://example.com"); curl_easy_setopt(curl, CURLOPT_CAINFO, "/path/to/cacert.pem"); // 指定CA证书 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); // 验证证书 curl_easy_perform(curl);
-
自定义证书验证:通过回调函数深度校验证书指纹或扩展属性。
-
双向认证:若服务端要求客户端证书,需设置
CURLOPT_SSLCERT
和CURLOPT_SSLKEY
。
3. 消息队列使用经验
- Kafka:高吞吐、持久化,适合日志、流处理。
- RabbitMQ:AMQP协议,事务/确认机制,适合业务解耦。
- RocketMQ:阿里系,顺序消息、事务消息支持较好。
4. 消息不丢失与顺序性保证
- 不丢失:
- 生产者:开启ACK确认(如Kafka的
acks=all
)。 - 消息队列:持久化消息到磁盘。
- 消费者:手动提交Offset,处理成功后确认。
- 生产者:开启ACK确认(如Kafka的
- 顺序性:
- Kafka:同一Partition内消息有序,需业务确保单分区消费。
- RocketMQ:通过MessageQueue锁定保证局部顺序。
5. 一致性哈希算法
- 原理:将节点和数据映射到哈希环,数据按顺时针找到最近节点。
- 优势:节点增减时仅影响相邻数据,避免全量重哈希。
- 虚拟节点:解决物理节点分布不均问题,提高负载均衡性。
6. Redis与MySQL一致性
- 策略:
- Cache-Aside:读时先查缓存,无则读DB并回填;写时更新DB后删除缓存。
- 双写:同步更新DB和缓存(需事务保证,可能引入复杂性)。
- 监听Binlog:通过Canal等工具订阅DB变更,异步更新缓存。
- 注意事项:设置合理的缓存过期时间,兜底防雪崩。
7. 本地缓存与Redis协调
- 读写策略:
- 读:本地缓存 → Redis → DB,逐级回填。
- 写:先更新DB,再失效Redis和本地缓存(如广播消息通知其他节点)。
- 一致性保障:
- 本地缓存设置短过期时间(如5秒)。
- 使用Pub/Sub或中间件(如ZooKeeper)同步失效事件。
8. 技术敏感度保持
- 日常实践:
- 关注Github Trending、Hacker News、技术博客(如掘金、InfoQ)。
- 参与开源项目,阅读源码(如Redis/Kafka)。
- 定期输出技术文章,复盘项目难点。
- 体系化学习:
- 极客时间/极客大学等课程系统性补足知识盲区。
- 参加行业会议(QCon、ArchSummit)了解前沿方案。
9. 为何离开百度寻找新实习
- 求知挑战:希望接触不同场景(如高并发/分布式),拓宽技术视野。
- 方向契合:当前岗位更偏向某领域(如云原生/中间件),与职业规划一致。
- 时间规划:原实习周期结束,寻求更深度参与的项目机会。
示例回答:
“在百度的实习中积累了XX经验,但目前希望深入探索XX领域(如分布式系统/高性能网络),而新机会的技术栈和业务方向更契合我的长期规划,因此决定尝试新的挑战。”
以上回答需结合个人实际经验调整,突出技术细节与思考深度。
1. 补充项目测试结果:分析极限情况下的性能瓶颈
-
常见性能瓶颈方向:
- CPU密集型:高并发下线程竞争锁、复杂计算逻辑(如JSON序列化)、频繁GC(Java场景)。
- 内存泄漏:未释放资源(如数据库连接、文件句柄)、缓存无限增长(未设TTL)。
- I/O瓶颈:磁盘读写慢(机械硬盘)、数据库慢查询(未优化索引)、网络带宽打满。
- 协议层限制:TCP连接数受限于操作系统(
ulimit
)、HTTP/1.1队头阻塞。
-
极限场景示例:
- 高并发写入:Kafka Partition数量不足,导致Producer线程阻塞。
- 缓存击穿:大量请求同时查询同一个失效的Redis Key,穿透到数据库。
- 分布式锁竞争:Redis锁粒度太粗,多个服务争抢同一资源。
-
优化方法:
- 水平扩展:增加服务节点、分库分表、Kafka Partition扩容。
- 异步化:耗时操作(如日志写入)改为异步队列处理。
- 资源复用:数据库连接池、线程池合理配置,避免频繁创建销毁。
2. Vector如何主动释放内存空间
-
问题本质:
vector::clear()
仅清空元素(size=0),不会释放底层内存(capacity不变)。 -
释放方法:
- C++98:
vector<T>().swap(v);
,通过空临时对象交换,释放原内存。 - C++11+:
v.shrink_to_fit();
请求减少capacity至匹配size(实现可能不严格保证)。
- C++98:
-
示例代码:
std::vector<int> v(1000); // capacity=1000 v.clear(); // size=0,capacity仍为1000 std::vector<int>().swap(v); // capacity变为0
-
适用场景:
在长期运行的服务中,若某个vector
曾扩容到很大但后续不再需要,主动释放避免内存浪费。
3. 稳定排序算法
- 定义:相等元素的相对顺序在排序前后保持不变。
- 稳定算法:
- 冒泡排序:相邻元素交换,相等时不交换。
- 插入排序:元素逐个插入有序区,相等时插入到后方。
- 归并排序:合并时保留左子数组元素的原始顺序。
- 基数排序:按位排序时依赖稳定的子排序算法。
- 非稳定算法:
- 快速排序:分区过程中可能打乱相等元素的顺序。
- 堆排序:建堆和调整堆时破坏稳定性。
- 选择排序:每次选择最小元素交换到前方,可能跨越相等元素。
4. TCP三次握手的数据携带问题
- RFC规定:
- 第一次握手(SYN):不允许携带数据,防止SYN Flood攻击(攻击者发送大量SYN+Data消耗服务端资源)。
- 第二次握手(SYN-ACK):不允许携带数据,原因同上。
- 第三次握手(ACK):允许携带数据,此时连接已建立(客户端进入ESTABLISHED状态)。
- 影响:
- 优点:减少一次RTT(应用层数据可在第三次握手时发送)。
- 风险:服务端在第三次握手前需分配资源,可能被攻击者利用(需结合SYN Cookie等防护机制)。
5. select 和 epoll 的适用场景
场景 | select | epoll |
---|---|---|
活跃连接数少 | 更简单,代码量少 | 无优势,反而需要维护epoll实例 |
活跃连接数多 | O(n)遍历所有fd,性能差 | O(1)事件通知,高效 |
文件描述符数量 | 受限于FD_SETSIZE(默认1024) | 支持数万级fd |
触发模式 | 仅水平触发(LT) | 支持水平触发(LT)和边缘触发(ET) |
- 选择建议:
- 短连接、低并发:select或poll更简单(如小型嵌入式系统)。
- 长连接、高并发:epoll(如Web服务器、实时消息推送)。
- 边缘触发(ET)注意事项:
- 需一次性读完数据,否则可能丢失事件。
- 非阻塞IO必须,避免阻塞其他fd的处理。
总结:以上问题需结合底层原理(如TCP协议栈、OS内核机制)和实际场景(如高并发服务优化)回答,突出对细节的理解和工程权衡能力。
模拟
1. C++ HTTP服务器线程模型与并发能力
- 线程模型:
通常采用Reactor模式,基于线程池 + epoll多路复用,而非一个线程处理一个请求。- 主线程负责监听连接(
epoll_wait
),将就绪的I/O事件分发给工作线程。 - 工作线程处理请求(解析HTTP、业务逻辑、响应)。
- 主线程负责监听连接(
- 并发测试:
- 测试环境:4核8G云服务器,Ubuntu 20.04,测试工具
wrk
。 - 结果:短连接场景下支持约5K QPS,长连接(Keep-Alive)可达10K+并发连接。
- 瓶颈:内存带宽、线程竞争锁、文件描述符限制(需调整
ulimit -n
)。
- 测试环境:4核8G云服务器,Ubuntu 20.04,测试工具
2. 服务崩溃原因分析
- 常见崩溃原因:
- 内存泄漏:未释放动态分配的内存(如
new
后未delete
)。 - 线程安全:多线程竞争资源(如全局变量未加锁)。
- 资源耗尽:文件描述符、线程数、连接数超过系统限制。
- 未捕获异常:如C++中未处理的
std::exception
。
- 内存泄漏:未释放动态分配的内存(如
- 调试方法:
- 使用
gdb
分析core dump文件,定位崩溃点。 - 日志记录关键路径(如请求处理前后)。
- 工具检测:Valgrind(内存泄漏)、AddressSanitizer(越界访问)。
- 使用
3. 大批量请求下的服务端状态
- 服务端状态:
- CPU打满:线程池全忙,请求排队。
- 内存增长:连接缓冲区、业务对象累积。
- 网络拥塞:TCP缓冲区满,丢包重传。
- 表现:
- 响应延迟增加,超时错误率上升。
- 可能触发OOM Killer终止进程。
4. CGI机制详解
- 原理:Web服务器将HTTP请求转发给外部程序(如Python脚本),通过环境变量和标准输入传递参数,标准输出返回响应。
- 流程:
- 服务器解析请求,设置环境变量(如
QUERY_STRING
)。 - 创建子进程执行CGI程序。
- CGI程序读取输入,处理结果写入
stdout
。 - 服务器捕获输出并返回客户端。
- 服务器解析请求,设置环境变量(如
- 缺点:每次请求需创建进程,性能差(已被FastCGI取代)。
FastCGI
FastCGI 是什么?
FastCGI(Fast Common Gateway Interface)是一种用于提升 Web 应用程序性能的协议。它通过优化 Web 服务器(如 Nginx、Apache)与外部应用程序(如 PHP、Python 脚本)之间的通信方式,解决了传统 CGI(Common Gateway Interface)的性能瓶颈。
FastCGI 的核心特性
-
持久化进程
- 传统 CGI 对每个请求都需启动新进程,而 FastCGI 使用持久化进程处理多个请求,减少进程频繁创建/销毁的开销。
- 优势:显著降低延迟,提高并发能力。
-
服务分离
- Web 服务器(如 Nginx)和应用程序(如 PHP-FPM)可运行在独立进程中,甚至部署在不同服务器上。
- 优势:提升扩展性,方便单独优化或横向扩展。
-
语言无关性
- 支持多种编程语言(PHP、Python、Ruby 等),只需应用程序实现 FastCGI 协议接口。
-
二进制通信协议
- 使用二进制协议传输数据(相比 CGI 的文本协议),传输效率更高。
-
负载均衡与容错
- 支持将请求分发给多个后端应用进程,避免单点故障。
FastCGI 工作原理
-
请求流程:
- 用户发起 HTTP 请求 → Web 服务器(如 Nginx)接收。
- Web 服务器通过 FastCGI 协议将请求转发给应用进程(如 PHP-FPM)。
- 应用进程处理请求并返回结果 → Web 服务器将结果返回给用户。
-
进程模型:
- FastCGI 进程常驻内存,通过复用处理多个请求(类似线程池)。
- 例如:PHP-FPM 通过进程池管理 PHP 解释器实例。
FastCGI 的优势
对比项 | FastCGI | 传统 CGI |
---|---|---|
性能 | 高(持久化进程) | 低(每次请求新建进程) |
资源占用 | 低(进程复用) | 高(频繁创建/销毁进程) |
扩展性 | 支持分布式部署和负载均衡 | 单进程,难以扩展 |
安全性 | 应用与 Web 服务器隔离,风险更低 | 耦合度高,安全性较低 |
典型应用场景
-
PHP 应用
-
通过 PHP-FPM(FastCGI Process Manager)处理动态请求,如 WordPress、Laravel。
-
配置示例(Nginx + PHP-FPM):
location ~ \.php$ {fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;include fastcgi_params;fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; }
-
-
Python/Ruby 应用
- 使用
flup
(Python)或fcgi
(Ruby)等库实现 FastCGI 接口。
- 使用
-
高并发网站
- 适合需要快速响应大量请求的场景(如电商、API 服务)。
FastCGI 的局限性
- 协议复杂性:需额外配置 Web 服务器与应用进程的通信(如 Socket 或 TCP)。
- 长连接管理:持久化进程需监控资源泄漏(如内存未释放)。
- 现代替代方案:部分场景已被更高效的协议取代(如 HTTP/2、gRPC)。
常见问题与解决方案
-
502 Bad Gateway 错误
- 原因:FastCGI 进程崩溃或通信超时。
- 解决:检查 PHP-FPM 是否运行,调整
fastcgi_read_timeout
参数。
-
性能瓶颈
- 优化方向:增加 PHP-FPM 进程数、调整进程池策略(
pm.max_children
)。
- 优化方向:增加 PHP-FPM 进程数、调整进程池策略(
-
安全性配置
- 禁止执行敏感操作:在
php.ini
中禁用危险函数(如exec
)。 - 使用 Unix Socket 替代 TCP,减少网络暴露。
- 禁止执行敏感操作:在
总结
FastCGI 通过持久化进程和高效通信协议,成为动态 Web 应用的主流解决方案。尽管面临现代协议竞争,其在 PHP 生态(如 PHP-FPM)中仍是性能优化的基石。合理配置 FastCGI 参数(如超时时间、进程数),可显著提升服务器吞吐量和稳定性。
HTTP/2 与 gRPC
以下是关于 HTTP/2 和 gRPC 的对比与分析,结合其特性、应用场景及与 FastCGI 的差异:
HTTP/2 与 gRPC 的核心对比
特性 | HTTP/2 | gRPC | FastCGI(参考对比) |
---|---|---|---|
协议类型 | 传输层协议(基于 TCP) | 应用层 RPC 框架(基于 HTTP/2) | 应用层通信协议 |
数据格式 | 文本(如 JSON/HTML)或二进制 | 二进制(Protocol Buffers) | 二进制(FastCGI 协议格式) |
主要目标 | 优化 Web 传输效率 | 高性能跨语言服务通信 | Web 服务器与后端应用通信 |
多路复用 | ✔️ 单连接并发多个请求/响应 | ✔️ 继承 HTTP/2 的多路复用 | ❌ 单请求/响应模型 |
头部压缩 | ✔️ HPACK 压缩头部 | ✔️ 继承 HTTP/2 的头部压缩 | ❌ 无压缩 |
流式通信 | ✔️ 支持服务端推送和流式传输 | ✔️ 支持双向流(Client/Server Streaming) | ❌ 仅单向请求-响应 |
语言支持 | 所有支持 HTTP 的语言 | 多语言(C++, Java, Python, Go 等) | 多语言(PHP, Python 等) |
典型应用场景 | 网页加载、API 交互 | 微服务、分布式系统、实时通信 | 动态网页处理(如 PHP-FPM) |
HTTP/2 的核心特性
-
多路复用(Multiplexing)
- 在单一 TCP 连接上并行传输多个请求/响应,解决 HTTP/1.1 的队头阻塞问题。
- 优势:减少延迟,提升页面加载速度。
-
头部压缩(HPACK)
- 压缩重复的 HTTP 头部,降低传输开销(尤其在 API 高频调用中效果显著)。
-
服务端推送(Server Push)
- 服务器可主动推送资源(如 CSS/JS)到客户端缓存,无需等待客户端请求。
-
二进制分帧(Binary Framing)
- 数据以二进制帧传输,解析效率高于 HTTP/1.1 的文本格式。
适用场景:
- 优化现代 Web 应用(如 SPA 单页应用)。
- 高频 API 请求(如移动端与后端交互)。
gRPC 的核心特性
-
基于 HTTP/2 和 Protocol Buffers
- 使用 HTTP/2 作为传输层,默认通过 Protocol Buffers(高效二进制序列化格式)编码数据。
- 优势:体积小、序列化速度快,跨语言兼容。
-
强类型接口定义(Protobuf IDL)
-
通过
.proto
文件定义服务接口和数据结构,自动生成客户端和服务端代码。 -
示例:
service UserService {rpc GetUser (UserRequest) returns (UserResponse); } message UserRequest { int32 id = 1; } message UserResponse { string name = 1; }
-
-
四种通信模式:
- Unary RPC:传统请求-响应模式。
- Server Streaming:服务端流式返回多个响应。
- Client Streaming:客户端流式发送多个请求。
- Bidirectional Streaming:双向流式通信(如聊天应用)。
-
跨语言支持
- 支持多种编程语言(需官方或社区 SDK),适合微服务架构中的多语言协作。
适用场景:
- 微服务间的高效通信(如 Kubernetes 内部服务调用)。
- 实时数据传输(如 IoT 设备、游戏服务器)。
与 FastCGI 的对比
-
定位差异:
- FastCGI:专注 Web 服务器与后端应用(如 PHP/Python)的通信,优化动态内容生成。
- HTTP/2/gRPC:面向更通用的网络通信,尤其是服务间(Service-to-Service)交互。
-
性能:
- gRPC > HTTP/2 > FastCGI(得益于二进制协议和多路复用)。
- FastCGI 的瓶颈在于进程管理和单请求模型。
-
使用复杂度:
- FastCGI:配置简单,适合传统 LAMP 架构。
- gRPC:需定义接口和生成代码,适合中大型分布式系统。
如何选择?
场景 | 推荐协议 | 理由 |
---|---|---|
动态网页渲染(PHP/Python) | FastCGI | 成熟、与 Nginx/Apache 集成度高 |
浏览器与服务器的 Web 交互 | HTTP/2 | 兼容性强,优化资源加载和 API 性能 |
微服务间通信(如 Go ↔ Java) | gRPC | 跨语言、高性能、支持流式通信 |
实时数据传输(如股票行情) | gRPC(Streaming) | 低延迟,支持双向流 |
示例:gRPC 服务端与客户端(Python)
-
定义
.proto
文件:syntax = "proto3"; service Greeter {rpc SayHello (HelloRequest) returns (HelloReply) {} } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
-
生成代码:
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. greeter.proto
-
服务端实现:
class Greeter(grpc.GreeterServicer):def SayHello(self, request, context):return HelloReply(message=f'Hello, {request.name}!') server = grpc.server(ThreadPoolExecutor()) add_GreeterServicer_to_server(Greeter(), server) server.add_insecure_port('[::]:50051') server.start()
-
客户端调用:
channel = grpc.insecure_channel('localhost:50051') stub = GreeterStub(channel) response = stub.SayHello(HelloRequest(name='World')) print(response.message) # 输出:Hello, World!
总结
- HTTP/2 是 Web 传输的未来标准,适用于优化浏览器与服务器的通信。
- gRPC 是云原生时代的 RPC 框架,适合高性能、跨语言的微服务架构。
- FastCGI 仍适用于传统 Web 应用(如 PHP),但逐渐被容器化和服务化架构取代。
根据场景需求选择协议:
- 追求极致性能 → gRPC。
- 兼容现有 Web 生态 → HTTP/2。
- 传统动态页面渲染 → FastCGI。
5. 进程创建与通信
- 创建子进程:
fork()
:复制父进程地址空间。exec()
系列:替换子进程代码段(如执行CGI程序)。
- 通信方式:
- 管道(pipe):单向通信,
fork()
前创建。 - 共享内存(shm):高效但需同步。
- 信号(signal):简单通知,不可传数据。
- Unix域套接字:本地进程间全双工通信。
- 管道(pipe):单向通信,
6. Cookie vs Session
特性 | Cookie | Session |
---|---|---|
存储位置 | 客户端(浏览器) | 服务端(内存/数据库) |
安全性 | 低(易被篡改) | 高(仅存Session ID在Cookie) |
生命周期 | 可设置过期时间 | 通常随会话结束失效 |
适用场景 | 简单状态跟踪(如语言偏好) | 敏感信息(如登录态) |
7. 线程池的作用与优势
- 解决的问题:
- 线程创建/销毁开销:避免频繁系统调用(Linux中线程是轻量级进程,但仍需分配栈、TCB等)。
- 资源竞争:控制并发线程数,防止耗尽内存或CPU。
- 响应延迟:复用已存在的线程,减少初始化时间。
- 适用场景:
- 高并发短任务(如HTTP请求处理)。
- 需要限制资源使用的场景(如数据库连接池)。
8. Vector内存释放
- clear()行为:
- 仅将
size
置为0,capacity
保持不变,内存未释放。
- 仅将
- 主动释放方法:
- C++98:
vector<T>().swap(v);
(交换空临时对象)。 - C++11+:
v.shrink_to_fit();
(请求缩减容量,实现可能忽略)。
- C++98:
9. 红黑树 vs 哈希表
场景 | 红黑树 | 哈希表 |
---|---|---|
有序性 | 支持有序遍历 | 无序 |
查询速度 | O(log n) | 平均O(1),最差O(n) |
内存开销 | 每个节点需额外存储颜色/指针 | 桶数组 + 链表/红黑树解决冲突 |
适用场景 | 需范围查询(如C++ std::map ) | 快速单点查询(如缓存) |
10. 右值引用解决的问题
- 核心问题:避免深拷贝,实现资源移动语义。
- 示例:
std::vector
的移动构造函数“窃取”临时对象的内部指针,避免复制元素。 - 应用:
std::move()
、完美转发(std::forward
)。
- 示例:
11. 稳定排序算法
- 稳定性定义:相等元素的相对顺序在排序前后不变。
- 稳定算法:
- 冒泡排序、插入排序、归并排序、基数排序。
- 非稳定算法:
- 快速排序、堆排序、选择排序。
12. TCP三次握手的必要性
- 三次握手目的:
- 双方确认彼此的序列号初始化(ISN),防止历史重复连接(如延迟的SYN包)。
- 两次握手不足:客户端无法确认服务端已收到ACK,服务端可能误建连接。
- 四次握手冗余:第三次握手已可携带数据,无需额外步骤。
13. TCP握手阶段的数据携带
- RFC规定:
- 第一次握手(SYN):不允许数据,防SYN Flood攻击。
- 第二次握手(SYN-ACK):不允许数据。
- 第三次握手(ACK):允许携带数据(客户端已进入ESTABLISHED状态)。
14. TCP数据安全性
- TCP自身不加密:数据明文传输,安全性依赖上层协议(如HTTPS的TLS加密)。
- 安全机制:
- 序列号/确认号:防止数据篡改(但可预测)。
- 校验和:检测数据错误,无法防恶意攻击。
15. 网线拔插对TCP连接的影响
假设 客户端和服务器已经建立了 TCP 连接,并且正在正常通信。在这个时候,如果 客户端的网线被拔掉,会发生以下情况:
1. 物理层 & 数据链路层的影响
- 网线拔掉后,客户端的网络接口立即失效,无法发送或接收任何数据。
- 客户端不会立刻通知 TCP 层,也不会主动发送 FIN(连接关闭) 或 RST(连接重置),因为它已经失去了网络连接。
- 服务器端并不会立刻察觉到客户端掉线,因为 TCP 连接依赖超时机制来判断连接的存活状态。
2. TCP 层的行为
(1) 服务器仍然认为连接是“活跃的”
服务器并不会立即关闭 TCP 连接。因为 TCP 是面向连接、可靠的协议,它依赖于超时机制来检测连接状态,而不是立即终止。
(2) 服务器继续发送数据
如果服务器有数据要发送,它会将数据放入 TCP 发送缓冲区 并尝试通过网络发送:
- 服务器会等待客户端的 ACK(确认包)。
- 由于客户端网线被拔掉,客户端无法接收这个数据,也无法返回 ACK。
- 服务器不会立刻放弃,而是会 重传 数据(TCP 可靠传输机制)。
(3) TCP 超时重传机制生效
TCP 采用超时重传(RTO, Retransmission Timeout)策略:
-
初次超时(第 1 次重传)
- 服务器等待一定时间(超时时间 RTO),如果未收到 ACK,则触发第一次重传。
-
指数退避(指数增长的超时机制)
-
TCP 采用
指数退避
机制(即每次超时,等待时间加倍),例如:
- 第一次重传:等待 1s
- 第二次重传:等待 2s
- 第三次重传:等待 4s
- ……
-
这个过程会持续,直到超过 最大重传次数(一般是 TCP 超时上限,如 2 分钟 ~ 10 分钟,视操作系统和具体 TCP 实现而定)。
-
-
达到最大重传次数后,服务器判定连接失败
- 服务器最终会发送 RST(Reset),强制关闭连接,并释放 TCP 资源。
3. 可能出现的异常
(1) 客户端重新插回网线
如果客户端在 TCP 连接完全超时之前重新插回网线,但 IP 地址未变:
- 服务器的 TCP 连接仍然存活,客户端可以继续发送 ACK。
- 服务器会继续传输数据,连接不会断开。
如果客户端在 TCP 连接超时后才插回网线:
- 服务器已经关闭连接(RST)。
- 客户端的 TCP 连接仍然认为连接存在(但是数据传输失败)。
- 任何新请求都可能收到 RST,必须重新进行 三次握手 建立新连接。
(2) 客户端 IP 地址变化
如果客户端拔掉网线后,重新插入网线时 IP 地址发生了变化:
- 服务器仍然尝试向旧 IP 地址发送数据,连接最终会超时并断开。
- 客户端即使恢复网络,也无法继续使用旧连接,必须重新建立 TCP 连接。
(3) TCP Keepalive
某些服务器可能启用了 TCP Keepalive
机制,它会定期发送探测包来检测连接是否仍然存活:
- 如果客户端未响应
keepalive
探测包(通常间隔 75s 或更长),服务器会判断连接已失效,主动关闭 TCP 连接。
4. 结论
短时间内拔插网线
如果客户端在 TCP 超时前 重新插上网线:
- 连接可能不会断开,数据传输可能继续。
- 但如果错过了 TCP 超时窗口,服务器会判定连接失败并断开。
长时间断开
- 服务器会尝试重传数据。
- 经过多次重传后,如果仍未收到响应,服务器会 发送 RST 并关闭连接。
- 客户端无法继续使用该 TCP 连接,必须重新建立连接。
影响总结
- TCP 不会立刻断开连接,而是通过超时重传来判断是否断开。
- 客户端拔网线不会主动关闭连接,服务器只能通过重传和 Keepalive 机制检测连接是否断开。
- 如果长时间未恢复,服务器最终会超时并关闭连接。
- 如果客户端 IP 发生变化,旧连接无法恢复,需要重新建立连接。
附:TCP 连接断开的三种情况
断开原因 | 发生情况 | 服务器行为 |
---|---|---|
正常关闭(FIN) | 客户端/服务器主动关闭 | 通过 四次挥手 断开连接 |
异常断开(RST) | 客户端进程崩溃、服务器异常退出 | TCP 立刻终止连接,发送 RST |
超时断开 | 客户端拔网线,长时间无响应 | 服务器多次重传数据,最终超时关闭 |
5. 代码实验(模拟拔网线)
你可以使用 iptables
模拟网线拔插的行为:
模拟拔网线
# 阻止客户端与服务器的 TCP 通信(相当于拔掉网线)
iptables -A INPUT -p tcp --sport 12345 -j DROP
iptables -A OUTPUT -p tcp --dport 12345 -j DROP
模拟插回网线
# 允许客户端与服务器的 TCP 通信(相当于插回网线)
iptables -D INPUT -p tcp --sport 12345 -j DROP
iptables -D OUTPUT -p tcp --dport 12345 -j DROP
总结
拔网线的核心影响:
- 不会立即导致 TCP 连接断开,服务器依靠超时机制(如 TCP 重传、Keepalive)来判断连接状态。
- 短时间内插回网线可能不会断开连接,但如果拔网线时间过长,服务器最终会关闭连接(RST)。
- IP 地址变化会导致连接不可恢复,必须重新建立连接。
- 应用层(如 WebSocket、RPC)通常会实现自动重连机制,以应对这种情况。
你可以在实际环境中测试 tcpdump
或 netstat
来观察 TCP 连接的状态变化 🚀。
16. HTTP传输层协议
HTTP 在传输层使用的协议
HTTP(HyperText Transfer Protocol,超文本传输协议) 在传输层通常使用 TCP(Transmission Control Protocol,传输控制协议)。
为什么 HTTP 默认使用 TCP?
- 可靠性(Reliable)
- TCP 提供 可靠的传输,确保数据包按顺序到达且不会丢失。
- 通过 ACK 确认机制,TCP 确保每个数据包都被成功接收,如果丢失则会重传。
- 流量控制(Flow Control)
- TCP 具有 滑动窗口机制,能够根据网络状况调整数据发送速率,避免网络拥塞。
- 错误检测与校正
- TCP 通过 校验和 检测数据是否被篡改,并能请求重传丢失的数据。
- 面向连接(Connection-Oriented)
- HTTP 通过 TCP 三次握手 先建立连接,再进行数据传输,确保通信双方已建立稳定的连接。
HTTP 可以使用 UDP 吗?
传统的 HTTP (HTTP/1.1, HTTP/2) 不使用 UDP,但 HTTP/3 使用了 UDP。
HTTP + UDP 的情况
-
HTTP/3 + QUIC
-
HTTP/3(下一代 HTTP 协议)不再基于 TCP,而是基于 UDP + QUIC(Quick UDP Internet Connections)。
-
QUIC 解决了 TCP 的一些问题,如 “队头阻塞”(Head-of-line blocking),并提供类似 TCP 的可靠传输。
-
特点
:
- 仍然提供 可靠传输(像 TCP 一样保证数据不丢失)。
- 连接建立更快(减少握手延迟)。
- 更适合移动网络,因为可以应对 IP 变化(如 WiFi 和 4G 切换时不断开连接)。
-
-
非标准场景
- 某些特殊应用 可能使用 HTTP-over-UDP,比如 CDN、视频直播、游戏数据 传输,但通常不用于标准 HTTP 页面加载。
结论
- HTTP 主要使用 TCP(HTTP/1.1, HTTP/2)。
- HTTP/3 采用 UDP + QUIC,提供更快的连接和更好的移动性支持。
- 传统 HTTP 不能直接用 UDP,因为 UDP 不可靠、无连接、不保证顺序,但 HTTP/3 通过 QUIC 解决了这些问题。
17. 优雅关闭Socket
优雅关闭 Socket 主要指在 TCP 连接 中,确保数据完整传输后再正确关闭连接。常用方法:
1. shutdown() + close()
cpp复制编辑// 关闭写入端,通知对方接收完数据后再关闭连接
shutdown(sock, SHUT_WR);// 继续读取对方发送的数据,直到 EOF
char buffer[1024];
while (recv(sock, buffer, sizeof(buffer), 0) > 0) {}// 彻底关闭
close(sock);
2. 使用 SO_LINGER 控制关闭行为
cpp复制编辑struct linger so_linger;
so_linger.l_onoff = 1; // 启用 linger
so_linger.l_linger = 5; // 等待 5 秒
setsockopt(sock, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger));
close(sock);
- l_onoff = 1, l_linger > 0:等待数据发送完成再关闭,否则丢弃数据。
- l_onoff = 0(默认):直接关闭,操作系统负责发送未传输的数据。
- l_onoff = 1, l_linger = 0:发送 RST 立刻关闭连接(强制断开)。
3. 正常 TCP 四次挥手
- 客户端 先
shutdown(sock, SHUT_WR)
,再recv()
直到对方FIN
,最后close()
- 服务器 读取完数据后
shutdown(SHUT_WR)
并close()
这样可以确保数据完整传输,避免 TIME_WAIT 影响系统资源。
在网络编程中,优雅关闭(Graceful Shutdown) 指的是在 确保数据完整传输 的情况下,正确地 关闭 TCP 连接,避免数据丢失或资源泄露。
1. TCP 连接的正常关闭
TCP 连接关闭时,通常需要进行 四次挥手(Four-Way Handshake):
- 客户端发送
FIN
(主动关闭) - 服务器返回
ACK
- 服务器发送
FIN
- 客户端返回
ACK
(完成关闭)
这确保了双方都已完成数据传输。
2. close()
VS shutdown()
在 C/C++ 网络编程中,Socket 关闭主要有两种方式:
close(fd)
: 直接关闭 Socket,可能导致未发送完的数据丢失。shutdown(fd, how)
: 分阶段关闭 连接,保证数据完整性。
(1)close(fd)
cpp复制编辑
close(socket_fd);
- 直接释放 Socket 资源,如果有未发送数据,操作系统可能会尝试发送但不保证完成。
- 适用于不关心数据完整性的情况。
(2)shutdown(fd, how)
cpp复制编辑
shutdown(socket_fd, SHUT_WR); // 只关闭写端,仍可接收数据
shutdown(fd, how)
允许更细粒度的控制:
how 选项 | 作用 |
---|---|
SHUT_RD | 关闭 读(不再接收数据) |
SHUT_WR | 关闭 写(发送 FIN ,通知对方 “我不再发送数据”) |
SHUT_RDWR | 关闭 读+写(相当于 close(fd) ) |
3. 优雅关闭的实现
(1)客户端优雅关闭
cpp复制编辑#include <iostream>
#include <sys/socket.h>
#include <unistd.h>void graceful_close(int sockfd) {// 先关闭写端,发送 FINshutdown(sockfd, SHUT_WR);char buffer[1024];// 继续读取服务器可能发送的剩余数据while (read(sockfd, buffer, sizeof(buffer)) > 0) {std::cout << "Received: " << buffer << std::endl;}// 关闭整个 socketclose(sockfd);
}
(2)服务器优雅关闭
cpp复制编辑void server_graceful_close(int sockfd) {// 关闭写端,通知客户端shutdown(sockfd, SHUT_WR);char buffer[1024];while (read(sockfd, buffer, sizeof(buffer)) > 0) {std::cout << "Client sent: " << buffer << std::endl;}close(sockfd);
}
4. 总结
-
直接
close(fd)
:可能导致数据丢失,不推荐直接用来关闭 TCP 连接。 -
使用
shutdown(fd, SHUT_WR)
:
- 先关闭写端,保证数据完整传输。
- 继续读取 对方可能发送的数据,确保四次挥手完整执行。
- 最后
close(fd)
释放资源。
推荐做法:
shutdown(fd, SHUT_WR)
通知对方不再发送数据(触发 FIN)。- 继续
read(fd, buffer, size)
读取剩余数据,等待对方关闭连接。 close(fd)
释放资源。
这样可以确保 TCP 连接完整关闭,数据不会丢失,达到 优雅关闭 的效果! 🚀
18. 数据库事务四大特性
- ACID实现:
- 原子性(A):Undo Log(回滚日志)。
- 隔离性(I):锁(行锁、表锁)、MVCC(多版本并发控制)。
- 持久性(D):Redo Log(先写日志,后刷盘)。
- 一致性(C):应用层约束(如外键、唯一索引)。
- 持久性风险:
- 日志未刷盘时宕机可能导致数据丢失(需配置
sync_binlog=1
等)。
- 日志未刷盘时宕机可能导致数据丢失(需配置
数据库的四大特性(ACID)是 原子性、一致性、隔离性 和 持久性,它们保证了数据库在各种异常情况下(如系统崩溃、电力故障等)能保持数据的一致性和可靠性。我们可以逐一分析数据库如何通过不同机制保证这四个特性。
1. 原子性(Atomicity)
原子性意味着数据库中的操作要么全部成功,要么全部失败,不能部分完成。即如果一个事务包含多个操作,数据库会确保所有操作要么全部执行,要么都不执行。
- 实现方法:
数据库使用 事务日志(日志文件) 来确保原子性。如果事务过程中发生故障,数据库会根据日志回滚到事务开始之前的状态。- 举个例子:假设转账操作需要从 A 账户扣款并加到 B 账户。如果在扣款后发生崩溃,数据库会在恢复过程中发现只有部分操作完成(扣款完成但未加款),然后会 回滚操作,使得扣款操作也回滚,从而保持事务的一致性。
2. 一致性(Consistency)
一致性确保数据库从一个一致的状态转换到另一个一致的状态。每个事务都必须遵守数据库的规则和约束(例如外键约束、唯一性约束等),使得数据始终保持合法和有效。
- 实现方法:
- 事务前后的数据一致性检查:在提交一个事务时,数据库会检查操作是否满足数据完整性约束。如果不满足,事务就会被回滚。
- 外键、唯一约束、触发器等机制也确保数据的约束性,从而确保数据库的一致性。
- 举个例子:在一个银行系统中,转账操作需要确保 A 账户扣款和 B 账户加款的操作始终成对发生。若一部分操作失败,数据库会通过回滚确保一致性。
3. 隔离性(Isolation)
隔离性确保一个事务的执行不受其他并发事务的干扰。事务执行的结果对于其他事务是不可见的,直到事务完成并提交。
- 实现方法:
- 数据库使用 锁机制(如行锁、表锁) 来保证事务隔离。例如,一个事务执行过程中,其他事务不能修改同一数据,直到当前事务完成。
- 隔离级别:数据库提供不同的隔离级别来控制并发事务的行为,常见的隔离级别有:
- Read Uncommitted:允许读取未提交的数据。
- Read Committed:只允许读取已提交的数据。
- Repeatable Read:在一个事务中读取的数据在整个事务执行期间保持一致。
- Serializable:最严格的隔离级别,确保事务之间完全隔离。
- 举个例子:如果两个事务都想更新同一条记录,通过锁机制保证只有一个事务能修改数据,另一个事务必须等前一个事务完成后再进行。
4. 持久性(Durability)
持久性确保一旦事务提交,对数据库的更改是永久性的,即使发生系统崩溃、断电等意外情况,数据也不会丢失。
- 实现方法:
- 事务日志(WAL,Write-Ahead Logging):数据库会先将修改记录写入日志文件,然后再修改数据库文件本身。这样,即使系统崩溃,只要日志中的记录没有丢失,可以通过日志恢复未完成的操作。
- 数据写入磁盘:在数据库提交一个事务后,所有的更改都会被 持久化到磁盘,并且系统会确保这些数据的写入是成功的。
- 定期备份:很多数据库系统会定期进行 全备份 和 增量备份,在发生严重故障时可以恢复数据。
- 举个例子:当你在银行系统中进行转账时,如果事务提交成功,数据库会确保即使发生断电或崩溃,转账的记录依然能够恢复并保持持久。
持久性保证不会丢失数据吗?
虽然持久性是数据库保证的一部分,但在极端情况下,持久性并不能 100% 保证不会丢失数据。以下是一些需要考虑的因素:
- 硬件故障:
- 即使数据库进行了事务日志写入并持久化数据,如果发生硬件损坏(如磁盘故障),而且没有合适的 RAID 配置或 冗余备份,数据可能会丢失。
- 日志丢失:
- 如果事务日志被损坏或丢失,而没有合适的备份机制,恢复过程可能无法恢复部分操作。
- 操作系统崩溃:
- 操作系统崩溃可能导致某些数据丢失,但现代数据库通常有 事务日志、快照技术 和 持久化机制,可以最大限度地减少这种风险。
- 未提交的数据:
- 在极端情况下(如系统突然崩溃),某些事务如果还没有提交,可能会丢失。
如何尽量避免数据丢失?
- 使用事务日志(WAL):数据库使用写前日志来保证数据一致性,即使在系统崩溃时也能恢复。
- 定期备份:定期进行全量备份和增量备份,尤其是对于关键的数据。
- 冗余存储(RAID):使用 RAID 等技术来确保硬盘故障时数据不会丢失。
- 容灾机制:使用主从复制、分布式存储等技术来防止单点故障导致数据丢失。
总结
- ACID 特性通过事务日志、锁机制、数据备份等技术来保证数据库的可靠性。
- 持久性确保已提交的事务数据不会丢失,但在极端情况下(如硬件故障、日志丢失等),也不能 100% 保证数据不丢失。
- 防止数据丢失的关键在于定期备份、使用冗余存储、确保事务日志的完整性以及设置高可用性和容灾机制。
19. epoll vs select
特性 | select | epoll |
---|---|---|
时间复杂度 | O(n)遍历所有fd | O(1)事件通知 |
最大fd数 | 受限于FD_SETSIZE(默认1024) | 支持数万级连接 |
触发模式 | 水平触发(LT) | 支持LT和边缘触发(ET) |
适用场景 | 低并发、跨平台 | 高并发Linux服务 |
- 活跃连接少时选择:
- select:简单且代码量少,适合连接数 < 1000的场景。
- epoll:仍需维护内核事件表,优势不明显。
在总连接数多且活跃连接数少的场景下,epoll 的性能明显优于 select。以下是详细分析:
核心原因
-
工作机制差异:
- select:采用轮询机制,每次调用需将所有被监听的 fd 从用户态复制到内核态,内核线性扫描所有 fd 判断状态变化(时间复杂度 O(n))。即使活跃连接极少,仍需遍历所有 fd,造成大量无效操作。
- epoll:采用事件驱动机制,通过
epoll_ctl
预先注册关心的 fd,内核通过回调直接通知就绪的 fd。调用epoll_wait
时仅返回已就绪的 fd 列表(时间复杂度接近 O(1)),避免无效遍历。
-
性能对比:
场景 select 性能 epoll 性能 总连接数多,活跃少 低(每次遍历所有 fd,冗余开销大) 极高(仅处理活跃 fd) 资源占用 高(频繁复制全量 fd 集合) 低(零拷贝,仅维护就绪列表) 扩展性 受限于 FD_SETSIZE
(默认 1024)支持百万级连接
关键优势分析
-
避免冗余遍历:
- 示例:10,000 个连接中仅有 10 个活跃。
select
:每次调用需复制 10,000 个 fd 到内核,遍历所有 fd 判断状态。epoll
:内核直接返回 10 个就绪 fd,无需遍历非活跃连接。
- 示例:10,000 个连接中仅有 10 个活跃。
-
零拷贝优化:
select
每次需将全量 fd 集合从用户态复制到内核态,而epoll
通过epoll_ctl
预先注册,epoll_wait
直接返回就绪 fd,减少数据拷贝开销。
-
高并发支持:
select
受限于FD_SETSIZE
(默认 1024),需修改内核参数以支持更多连接,而epoll
可轻松支持数十万并发。
适用场景总结
场景 | 推荐选择 | 理由 |
---|---|---|
高并发且活跃连接少 | epoll | 事件驱动机制避免无效遍历,性能显著优于轮询。 |
低并发(连接数 < 100) | select | 实现简单,少量连接时开销差异不明显。 |
跨平台兼容性需求 | select | epoll 仅限 Linux,若需支持 Windows 或多平台,select 是备选方案。 |
实际测试数据
- 10,000 并发连接,1% 活跃:
select
:CPU 占用率高(约 80%),延迟波动大。epoll
:CPU 占用率低(约 20%),延迟稳定。
- 吞吐量对比:
epoll
的 QPS(每秒查询数)可达select
的 5-10 倍。
结论
在总连接数多且活跃连接数少的场景下,epoll 是绝对更优的选择。它通过事件驱动机制和零拷贝优化,大幅降低了无效操作的开销,显著提升吞吐量并降低延迟。而 select
仅适用于低并发或跨平台兼容性需求,性能上无法与 epoll
竞争。