1. TCP和UDP区别是什么?

  • 连接:TCP 是面向连接的传输层协议,传输数据前先要建立连接;UDP 是不需要连接,即刻传输数据。

  • 服务对象:TCP 是一对一的两点服务,即一条连接只有两个端点。UDP 支持一对一、一对多、多对多的交互通信

  • 可靠性:TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。UDP 是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议

  • 拥塞控制、流量控制:TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。

  • 首部开销:TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。UDP 首部只有 8 个字节,并且是固定不变的,开销较小。

  • 传输方式:TCP 是流式传输,没有边界,但保证顺序和可靠。UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。

2. HTTP用的是哪个传输协议,为什么?

HTTP/1.1 底层基于 TCP 协议(传输控制协议),原因是可靠性需求。HTTP 作为应用层协议,需要确保数据完整、有序地传输(例如网页内容、图片等不能丢失或乱序)。TCP 提供了可靠传输(通过确认机制、重传机制、序号机制),恰好满足这一需求。

HTTP/2 仍基于 TCP 协议,但通过以下优化缓解了 HTTP/1.1 的问题:

  • 多路复用:在同一 TCP 连接中,多个请求 / 响应可并行传输(通过帧和流的机制),理论上解决了 “队头阻塞”。

  • 二进制帧:将数据分割为二进制帧传输,更高效且易于解析。

  • 头部压缩(HPACK):减少重复的请求头数据传输。

但 HTTP/2 仍受限于 TCP 的固有缺陷,比如TCP 层的队头阻塞:多路复用依赖同一 TCP 连接,若 TCP 层出现丢包,所有流(请求)都会被阻塞等待重传,导致 HTTP/2 的多路复用优势被削弱。

HTTP/3 彻底改用 QUIC 协议 作为传输层协议,而 QUIC 基于 UDP 实现,原因是为了解决 TCP 无法克服的问题:

  • 解决队头阻塞:QUIC 实现了 “流级别的重传”,某一流(请求)的丢包不会影响其他流,从根本上避免了 TCP 层的队头阻塞。

  • 更快的连接建立:QUIC 合并了 TCP 三次握手和 TLS 握手过程,只需 1-2 个 RTT 即可建立加密连接(TCP + TLS 需 3-4 个 RTT)。

  • 连接迁移:QUIC 使用 “连接 ID” 标识连接,而非 TCP 的 “IP + 端口”,因此当设备网络切换(如从 WiFi 到 4G)时,连接不会中断。

QUIC 基于 UDP 却能提供类似 TCP 的可靠性:通过序号、确认机制、重传机制等实现可靠传输,同时保留了 UDP 的低延迟优势。

3. HTTP状态码你知道哪些?

HTTP 状态码分为 5 大类

  • 1xx 类状态码属于提示信息,是协议处理中的一种中间状态,实际用到的比较少。

  • 2xx 类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。

  • 3xx 类状态码表示客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向。

  • 4xx 类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。

  • 5xx 类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。

其中常见的具体状态码有:

  • 200:请求成功;

  • 301:永久重定向;302:临时重定向;

  • 404:无法找到此页面;405:请求的方法类型不支持;

  • 500:服务器内部出错。

4. HTTP是长连接还是短连接?

HTTP 协议采用的是「请求-应答」的模式,也就是客户端发起了请求,服务端才会返回响应,一来一回这样子。

图片

由于 HTTP 是基于 TCP 传输协议实现的,客户端与服务端要进行 HTTP 通信前,需要先建立 TCP 连接,然后客户端发送 HTTP 请求,服务端收到后就返回响应,至此「请求-应答」的模式就完成了,随后就会释放 TCP 连接。

图片

如果每次请求都要经历这样的过程:建立 TCP -> 请求资源 -> 响应资源 -> 释放连接,那么此方式就是 HTTP 短连接,如下图:

图片

这样实在太累人了,一次连接只能请求一次资源。

能不能在第一个 HTTP 请求完后,先不断开 TCP 连接,让后续的 HTTP 请求继续使用此连接?

当然可以,HTTP 的 Keep-Alive 就是实现了这个功能,可以使用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,避免了连接建立和释放的开销,这个方法称为 HTTP 长连接。

图片

HTTP 长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。

所以,HTTP/1.1 版本之后,默认就是长连接的模式。

5. .Linux查看进程指令是哪个?

ps 命令

6. linux 命令 ps -aux第二列是什么,和ps -elf区别?

ps -aux第二列是 进程的pid号:

图片

下图是ps -elf的显示:

图片

两者的区别:

  • ps -aux 更侧重 用户、资源占用(CPU / 内存) 等信息,适合快速查看进程的资源消耗情况。

  • ps -elf 更侧重 进程关系(PPID)、优先级(PRI/NI)、系统级标志(F) 等信息,适合分析进程间的父子关系和系统调度相关属性。

7. mysql的索引如何实现的?

MySQL InnoDB 引擎是用了B+树作为了索引的数据结构。

B+Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放的。每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每一个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表。

主键索引的 B+Tree 如图所示:

图片

比如,我们执行了下面这条查询语句:

select * from product where id= 5;

这条语句使用了主键索引查询 id 号为 5 的商品。查询过程是这样的,B+Tree 会自顶向下逐层进行查找:

  • 将 5 与根节点的索引数据 (1,10,20) 比较,5 在 1 和 10 之间,所以根据 B+Tree的搜索逻辑,找到第二层的索引数据 (1,4,7);

  • 在第二层的索引数据 (1,4,7)中进行查找,因为 5 在 4 和 7 之间,所以找到第三层的索引数据(4,5,6);

  • 在叶子节点的索引数据(4,5,6)中进行查找,然后我们找到了索引值为 5 的行数据。

数据库的索引和数据都是存储在硬盘的,我们可以把读取一个节点当作一次磁盘 I/O 操作。那么上面的整个查询过程一共经历了 3 个节点,也就是进行了 3 次 I/O 操作。

B+Tree 存储千万级的数据只需要 3-4 层高度就可以满足,这意味着从千万级的表查询目标数据最多需要 3-4 次磁盘 I/O,所以B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4次。

8. 为什么用B+不用B树?如果把内容存在内存上,用B树会不会快一点?

数据库索引通常存储在磁盘上,而 B+ 树的结构设计对磁盘 IO 更友好:

  • B+ 树的 非叶子节点只存储索引键,不存储实际数据(行记录),因此相同大小的节点可以容纳更多索引项,树的高度更低(通常 3-4 层),查询时需要访问的磁盘页数更少(IO 次数更少)。

  • B+ 树的 所有数据都存储在叶子节点,且叶子节点之间通过双向链表连接,这使得范围查询(如 WHERE id BETWEEN 100 AND 200)可以直接通过叶子节点的链表顺序遍历,效率远高于 B 树(B 树需要回溯父节点)。

另外,更好地支持顺序访问,数据库经常需要全表扫描或大范围查询,B+ 树的叶子节点链表结构天然支持高效的顺序遍历,而 B 树由于数据分散在各个节点,顺序访问效率低。

理论上,在 纯内存场景 下,B 树可能在某些场景下表现更好,但优势并不明显,甚至可能不如 B+ 树:

  • 随机查询的微小优势:B 树的非叶子节点可能直接存储数据,若查询命中非叶子节点,可减少一次内存访问(无需到叶子节点)。但实际中,索引树的高度通常很低(内存中 2-3 层),这种差异对性能影响极小。

  • 范围查询的劣势依然存在,即使在内存中,B+ 树的叶子节点链表结构对范围查询的支持仍优于 B 树。B 树进行范围查询时,仍需频繁回溯父节点,逻辑更复杂,效率更低。

9. C++ 中 struct和Class区别是什么?

在 C++ 中,struct 和 class 是两种用于定义自定义数据类型的关键字,它们的核心功能相似(都能包含成员变量和成员函数),但存在一些关键区别,主要体现在默认访问权限和默认继承方式上:

  • 默认访问权限不同:struct 默认访问权限为 public,class 默认访问权限为 private。比如:

struct A {
    int x;  // 默认 public,外部可直接访问
    void f() {}  // 默认 public,外部可调用
};

class B {
    int y;  // 默认 private,外部无法直接访问
    void g() {}  // 默认 private,外部无法直接调用
};

int main() {
    A a;
    a.x = 10;  // 合法(struct 成员默认 public)
    a.f();     // 合法

    B b;
    b.y = 20;  // 编译错误(class 成员默认 private)
    b.g();     // 编译错误
    return 0;
}
  • 默认继承方式不同:当使用继承时,两者的默认继承权限也不同,struct 默认继承方式为 public 继承,class 默认继承方式为 private 继承。比如:

struct Base {
    int x;
};

// struct 默认 public 继承:Base 的 public 成员在 Derived1 中仍为 public
struct Derived1 : Base {
    // 可直接访问 Base::x,外部也可通过 Derived1 对象访问 x
};

// class 默认 private 继承:Base 的 public 成员在 Derived2 中变为 private
class Derived2 : Base {
    // 可直接访问 Base::x,但外部无法通过 Derived2 对象访问 x
};

int main() {
    Derived1 d1;
    d1.x = 10;  // 合法(public 继承)

    Derived2 d2;
    d2.x = 20;  // 编译错误(private 继承)
    return 0;
}

10. 简单介绍一下C++ STL有哪些容器?

C++ STL(标准模板库)提供了多种容器,用于存储和管理数据,这些容器可分为序列式容器、关联式容器和容器适配器三大类,各自适用于不同场景:

图片

序列式容器:按元素插入顺序存储,元素位置与插入顺序相关,不自动排序。

  • vector:动态数组,内存连续,支持随机访问([] 或 at()),优点尾插 / 尾删效率高(O (1)),适合频繁访问元素的场景。缺点是中间插入 / 删除效率低(O (n)),扩容时可能重新分配内存。

  • deque:双端队列,内存分段连续,支持首尾高效操作。优点头插 / 头删、尾插 / 尾删效率均为 O (1),可随机访问,适合场景需要在两端频繁操作的场景(如实现队列、栈)。

  • list:双向链表,元素通过指针连接,内存不连续。优点任意位置插入 / 删除效率高(O (1),只需修改指针)。缺点不支持随机访问(访问元素需遍历,O (n)),内存开销较大。

  • array:固定大小数组(C++11 新增),编译时确定大小,内存连续,比原生数组更安全(支持边界检查),但大小不可变。

关联式容器:元素按键(key) 排序存储,支持快速查找(通常 O (log n)),分为有序和无序两类。

有序关联容器(基于红黑树实现)

  • set:存储唯一键值,元素自动按键升序排序,键即值(key=value)。适合场景需要去重且有序的数据集合(如存储不重复的 ID 并排序)。

  • multiset:与 set 类似,但允许键值重复,其他特性相同。

  • map:存储键值对(key-value),键唯一且自动排序,通过键快速查找值。适合场景键值映射场景(如字典、配置表)。

  • multimap:与 map 类似,但允许键重复(一个键可对应多个值)。

无序关联容器(C++11 新增,基于哈希表实现)

  • unordered_set / unordered_multiset:**功能同 set / multiset,但元素无序,通过哈希表实现,查找、插入、删除平均效率更高(O (1))。适合场景对顺序无要求,但需要快速增删查的场景。

  • unordered_map / unordered_multimap:**功能同 map / multimap,无序,基于哈希表,平均操作效率 O (1)。

容器适配器:基于其他容器实现,封装特定接口,提供受限功能。

  • stack:栈,遵循 “后进先出(LIFO)”,仅支持在顶部插入 / 删除 / 访问元素,默认基于 deque 实现,也可指定 vector 或 list 作为底层容器。

  • queue:队列,遵循 “先进先出(FIFO)”,仅支持在尾部插入、头部删除。默认基于 deque 实现,也可指定 list 作为底层容器。

  • priority_queue:优先队列,元素按优先级自动排序(默认最大元素在顶部),插入 / 删除效率 O (log n)。默认基于 vector 实现,底层用堆结构维护优先级。

11. C++  的map是线程安全的么?

不是线程安全的。如果多个线程同时对 std::map 进行操作(尤其是写操作,如插入、删除、修改元素),可能会导致未定义行为(例如数据损坏、迭代器失效、程序崩溃等)。

具体来说,线程不安全主要体现在:

  1. 并发写操作:多个线程同时执行插入(insert)、删除(erase)或修改(如 operator[] 赋值)时,会破坏 std::map 内部的红黑树结构(有序关联容器的底层实现),导致数据错乱。

  2. 读写并发:即使一个线程读、另一个线程写,也可能出现问题。例如,读线程正在遍历 map 时,写线程修改了结构,可能导致读线程的迭代器失效,触发不可预知的错误。

  3. 无内置同步机制:std::map 没有提供任何锁(如互斥量)或原子操作来保证多线程安全,所有同步逻辑需要开发者手动实现。

如果要多线程安全使用map,需通过外部同步机制保证线程安全,常见方式:

  • 加锁保护:使用 std::mutex 或 std::lock_guard 等同步工具,确保同一时间只有一个线程能访问或修改 map。例如:

#include <map>
#include <mutex>

std::map<int, int> my_map;
std::mutex mtx;  // 互斥锁

// 线程安全的插入操作
void safe_insert(int key, int value) {
    std::lock_guard<std::mutex> lock(mtx);  // 自动加锁/解锁
    my_map[key] = value;
}

// 线程安全的查找操作
int safe_find(int key) {
    std::lock_guard<std::mutex> lock(mtx);
    auto it = my_map.find(key);
    return (it != my_map.end()) ? it->second : -1;
}
  • 使用线程安全的替代容器:某些库(如 C++17 后的 concurrent_unordered_map 非标准扩展,或第三方库如 Intel TBB)提供了线程安全的哈希表 / 映射容器,可减少手动加锁的开销。

12. 队列和栈有什么区别?

主要区别在于元素的插入和删除方式以及元素的访问顺序。

插入和删除方式:

  • 队列:队列采用先进先出(FIFO)的方式,即新元素插入队尾,删除操作发生在队首。

  • 栈:栈采用后进先出(LIFO)的方式,即新元素插入栈顶,删除操作也发生在栈顶。

元素的访问顺序:

  • 队列:队列的元素按照插入的顺序进行访问,先插入的元素先被访问到。

  • 栈:栈的元素按照插入的顺序进行访问,但是最后插入的元素先被访问到。

队列适用于需要按照插入顺序进行处理的场景,例如任务调度;

图片

而栈适用于需要维护最近操作状态的场景,例如函数调用。

更多推荐