前置知识 - C++

⾯向对象编程(OOP)

  1. 封装:通过public,private,protected 来实现类的访问权限。
  2. 继承:复⽤现有类功能的⽅式,通过派⽣新类来扩展原有类的功能,⽽⽆需重新编写原始代码。
  3. 多态:使⼀个类的不同实例在不同情况下表现出不同的⾏为特性,使得具有不同内部结构的
  4. 抽象:通过抽象类和接⼝定义对象的⾏为,忽略具体的实现细节。
    对象能够共享相同的外部接⼝,从⽽达到灵活和统⼀处理的⽬的。
    • 静态多态(编译时多态):通过函数重载(Overloading)和模板技术(Templates)实现。
    • 动态多态(运⾏时多态):通过虚函数(Virtual Functions)和继承关系实现。

静态全局变量(static)

  • 静态成员变量:可以通过类名直接访问,⽆需创建类的对象,从程序开始到结束。
  • 静态成员函数:不与任何对象实例关联,通过类名直接调⽤,可访问静态数据成员,不能直接访问⾮静态数据成员(需通过对象引⽤或指针)。

虚函数

  • 纯虚函数:在基类中声明但没有提供实现的虚函数,必须在所有⾮抽象派⽣类中实现。纯虚函数使⽤=0来标识。
  • 虚函数的⼯作基于运⾏时类型信息(RTTI)和虚函数表(VTable)。每个具有虚函数的类都会有⼀个隐藏的虚函数表,其中包含了指向虚函数地址的指针数组。当创建⼀个对象时,编译器会在对象内部添加⼀个指向其对应类的虚函数表的指针(vptr)。当通过基类指针调⽤虚函数时,实际上是通过这个vptr找到正确的虚函数表,然后执⾏对应的函数。
  • 虚函数表是针对类的。同⼀个类的所有对象共享同⼀个虚函数表。每个对象内部都保存⼀个指向该类虚函数表的指针 vptr 。虽然每个对象的 vptr 地址不同,但它们都指向同⼀个虚函数表。
  • 构造函数不可以是虚函数。原因是虚函数的调⽤依赖于虚函数表,⽽虚函数表的指针 vptr 需要在对象的构造函数中初始化。在构造函数执⾏之前, vptr 还未被初始化,因此⽆法使⽤虚函数机制。
  • 当使⽤基类指针指向派⽣类对象时,为了正确调⽤派⽣类的析构函数来释放资源,基类的析构函数需要定义为虚函数。如果析构函数不是虚函数,那么在删除派⽣类对象时,可能⽆法正确调⽤派⽣类的析构函数,导致资源泄漏。

抛出异常

  • 构造函数:从语法上讲,构造函数可以抛出异常。但从逻辑和⻛险控制的⻆度来说,应尽量避免在
    构造函数中抛出异常。如果构造函数抛出异常,可能会导致资源(如已分配的内存)⽆法正确释
    放,从⽽引起内存泄漏。
  • 析构函数:析构函数不应该抛出异常。如果析构函数中抛出异常,可能会导致程序中断或⽆法正确
    释放资源。特别是在异常处理过程中,如果另⼀个异常被抛出(⽽当前异常还未处理完毕),程序
    可能会直接终⽌。因此,析构函数应该捕获并处理其内部可能产⽣的任何异常,⽽不是抛出它们。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Example {
    public:
    Example() {
    try {
    // 构造函数中的代码
    } catch (...) {
    // 处理构造过程中的异常
    throw; // 重新抛出异常
    }
    }
    ~Example() {
    try {
    // 析构函数中的代码
    } catch (...) {
    // 处理析构过程中的异常
    // 不要重新抛出异常
    }
    }
    }

堆和栈的区别

  1. 栈(Stack):
    • 栈上存储的数据主要包括:局部变量(包括基本类型和内置数组等)、函数参数、返回地址以及编译器⾃动分配的临时变量。(简单说基本就是new之外的局部变量都在这⾥)
    • 存储特点 :
      • ⾃动分配和释放:栈空间由编译器⾃动管理,当作⽤域结束时,栈上的变量会⾃动销毁,⽆需程序员⼿动释放。
      • 空间有限且固定:栈的⼤⼩⼀般在程序启动时由系统预先设定,并且有限制。如果栈上分配的空间过⼤,可能导致栈溢出。
      • ⽆碎⽚区分:栈上存储的数据连续,不会产⽣内存碎⽚。
        1
        2
        3
        void someFunction() {
        int stackVariable = 10; // 这个变量存储在栈上
        }
  2. 堆(Heap):
    • 堆上存储的数据主要包括:通过 new 操作符动态分配的对象、数组或其他数据结构。
    • 存储特点:
      • 动态分配和释放:使⽤ new 申请内存后,需要通过 delete 来释放,否则会导致内存泄漏;同样, malloc 与 free 配合使⽤也是相同道理。
      • ⼤⼩灵活可变:堆内存空间可以根据需要动态调整,没有固定的上限,但受限于系统资源。
      • 可能产⽣内存碎⽚:由于频繁地分配和释放不同⼤⼩的内存块,可能会导致堆内存中存在⽆法利⽤的⼩块内存,即内存碎⽚。
        1
        2
        int* heapVariable = new int(20); // 这个对象存储在堆上
        delete heapVariable;

前置知识-STL

编译时连接库

1
g++ main.cpp -o main -l<library_name>

链接

链接的本质是将程序中的符号引⽤(如函数调⽤、变量访问等)与它们的定义关联起来。C++ 程序通
常分为多个源⽂件或模块,每个模块可能会调⽤其他模块中的函数或使⽤其他模块中的变量。链接过
程确保每个符号都有正确的定义和实现,并将它们整合成⼀个最终的可执⾏⽂件或动态库。

  • 静态链接:在编译时将库的代码嵌⼊到可执⾏⽂件中,⽣成的可执⾏⽂件不需要依赖外部的库⽂件。
  • 动态链接:在运⾏时加载外部的库⽂件(如 .dll 或 .so ⽂件),可执⾏⽂件只包含库的引⽤,库的实际代码存储在外部的动态库中。

链接的原理:
C++ 的编译过程通常分为⼏个步骤,其中链接是⾮常重要的⼀个环节。整个流程可以分为以下⼏个步骤:

  1. 预处理(Preprocessing):对源代码进⾏宏展开、头⽂件包含等预处理操作。
  2. 编译(Compilation):将预处理后的 C++ 源代码转换为⽬标代码(机器码),⽣成.o 或.obj ⽂件(⽬标⽂件)。
  3. 链接(Linking):将所有⽬标⽂件与库⽂件进⾏链接,⽣成最终的可执⾏⽂件。

静态链接与动态链接的区别

泛型编程

1
2
3
4
5
6
7
8
9
10
11
12
13
// 泛型函数模板,允许不同类型的参数 T 和 U
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
int main() {
// 使⽤模板函数处理不同类型的数据
std::cout << "整数相加: " << add(3, 5) << std::endl; // 输出 8
std::cout << "浮点数相加: " << add(2.5, 4.5) << std::endl; // 输出 7
std::cout << "字符相加: " << add('A', 2) << std::endl; // 输出 'C'(ASCII 运算)
std::cout << "整数和浮点数相加: " << add(3, 4.5) << std::endl; // 输出 7.5
return 0;
}

前置知识-C++11/14/17 新特性(auto lambda shared_ptr)

C++ 编译过程

  1. 预处理(Preprocessing)
    • 宏定义展开:使⽤ #define 定义的宏会被替换为其内容。
    • 处理头⽂件: #include 指令会将指定头⽂件的内容插⼊到源代码中。
    • 删除注释:预处理器会移除所有注释内容,
  2. 词法分析(Lexical Analysis)
    • 预处理后的⽂本被编译器分解成⼀系列符号或标记(tokens)。这些tokens包括关键字、标识符、常量、运算符和分隔符等。
    • ⽰例:对于语句 int a = 5; ,会分解为以下tokens: int 、 a 、 = 、 5 和 ; 。
  3. 语法分析(Syntax Analysis 或 Parsing)
    • 编译器根据C++的语法规则,将tokens组合成结构化的数据结构,即抽象语法树(AST)。
  4. 语义分析(Semantic Analysis)
    • 在构建AST的同时,编译器进⾏语义检查,以确保所有变量和函数的声明与使⽤符合语义规则。
    • 包括类型检查、作⽤域解析和其他静态语义规则的验证。例如,如果尝试将字符串赋值给整型变量,编译器将在这⼀阶段报告错误。
  5. 中间代码⽣成(Intermediate Code Generation)
    • 编译器⽣成⼀种与特定机器⽆关的中间表⽰形式,如字节码或三地址码。这种形式便于后续的优化和跨平台移植。
  6. 优化(Optimization)
    • 编译器对中间代码进⾏各种优化,以提⾼运⾏效率和减少资源消耗。
    • 优化⽰例:循环优化,死代码删除,常量传播。
  7. ⽬标代码⽣成(Code Generation)
    • 编译器将优化后的中间代码转换为特定计算机架构的⽬标代码(Object Code),⽬标代码通常为汇编语⾔或直接是⼆进制格式,能被CPU执⾏
  8. 链接(Linking)
    • 最后,链接器将多个⽬标代码⽂件及必要的库⽂件合并,形成最终的可执⾏⽂件

智能指针

  • shared_ptr:是⼀种共享拥有权的智能指针,多个 shared_ptr 可以指向相同的对象。它的原理包括:
    • 内部引⽤计数: shared_ptr 内部维护⼀个引⽤计数,记录有多少个 shared_ptr 共享同⼀对象。
    • 拥有权:当⼀个 shared_ptr 指向⼀个对象时,引⽤计数增加。当 shared_ptr 超出作⽤域或被显式重置时,引⽤计数减少。当引⽤计数为零时,对象被⾃动释放。这种机制确保了对象的⽣命周期与 shared_ptr 的⽣命周期⼀致,避免了内存泄漏和悬挂指针的问题
  • std::unique_ptr 是⼀种独占拥有权的智能指针,只能有⼀个 unique_ptr 指向对象。其原理包括:
    • 禁⽌复制构造和赋值: unique_ptr 不允许复制构造和赋值,因此它只能有⼀个所有权。
    • 移动语义:通过移动语义, unique_ptr 可以将所有权从⼀个指针转移到另⼀个,使得资源的管理更⾼效。这种机制确保了对象的独占拥有权,避免了资源的重复释放和多个指针同时指向⼀个对象的问题。
  • std::weak_ptr :配合 shared_ptr 使⽤,作为观察者,不影响引⽤计数。解决了循环引⽤的问题

新特性:

  • C++11
    1. 范围for循环(Range-based for loop):允许⽅便地遍历容器。
    2. 右值引⽤(Rvalue References)和移动语义(Move Semantics):允许资源的转移⽽⾮拷⻉,提⾼性能。
    3. 初始化列表(Initializer lists):⽤于容器和对象的统⼀初始化⽅式。
    4. 线程⽀持库(Threading Library):⽀持多线程编程。
  • C++14
    1. 返回类型推导(Return type deduction):函数返回类型可以⽤ auto 关键字⾃动推导。
    2. 泛型Lambda表达式(Generic lambdas):Lambda表达式可以使⽤ auto 在参数中实现参数类型推导。
  • C++17
    1. 并⾏算法(Parallel algorithms):标准库算法的并⾏版本,⽤于提⾼性能。

前置知识-Linux基础知识

操作系统

操作系统是⼀个控制和管理计算机硬件与软件资源的软件系统

Linux特性

  1. ⼀切皆⽂件(Everything is a file): 在Linux中,⽆论是硬件设备、⽬录、常规⽂件还是⽹络套接字等资源,都被抽象为“⽂件”,并可通过统⼀的系统调⽤来操作。这意味着你可以对它们进⾏读写操作,就像对待普通⽂件⼀样。例如,硬件设备可以通过特殊的设备⽂件来访问和控制。
  2. 强⼤的命令⾏⼯具: Linux提供了丰富的命令⾏⼯具,如 bash shell 、 grep 、 sed 、awk 、 find 等,这些⼯具可以⾼效地处理⽂本、查找信息和管理系统。
  3. 模块化设计: Linux内核采⽤模块化设计,允许动态加载和卸载驱动程序、⽂件系统以及其他内核模块,使得系统可以根据需要灵活扩展功能。
  4. 多⽤⼾与多任务:⽀持多个⽤⼾同时操作,能够⾼效管理多个任务

Linux⽂件操作与管理

  1. ⽂件描述符:⽂件描述符是⼀个抽象指标(⼀个⾮负整数),⽤于表⽰对⽂件或其他I/O资源的访问。
  2. ⽂件描述符表:每个进程都有⼀张独⽴的⽂件描述符表,确保每个描述符对应着⼀个独⽴的I/O资源。
    • 包括文件描述符(fd)和文件指针
  3. 打开⽂件表i-node 表。这两张表存储了每个打开⽂件的打开⽂件句柄(open file handle)。⼀个打开⽂件句柄存储了与⼀个打开⽂件相关的全部信息。
  4. 打开⽂件表(Open File Table):记录了进程如何使⽤特定的打开⽂件,如当前读写位置、访问模式等。这样可以快速定位到进程对⽂件的具体操作状态,提⾼系统处理I/O请求的效率。
  5. i-node 表:存储的是⽂件本⾝的元数据信息,包括但不限于⼤⼩、权限、时间戳以及指向实际数据块的指针。这些信息对于管理⽂件系统中的所有⽂件是通⽤且持久的,不依赖于任何特定进程。

Linux⽂件操作相关函数

open

  • 功能:⽤于打开⽂件或设备,返回⼀个⽂件描述符。
  • 原型:
    1
    int open(const char *pathname, int flags, mode_t mode);
  • 参数:
    • pathname :要打开的⽂件路径。
    • flags :打开⽂件的模式,如只读( O_RDONLY )、只写( O_WRONLY )、读写( O_RDWR )等。
    • mode :设置新⽂件的权限,仅在创建新⽂件时使⽤。

read

  • 功能:从⽂件描述符指向的⽂件中读取数据。
  • 原型:
    1
    ssize_t read(int fd, void *buf, size_t count);
  • 参数:
    • fd :⽂件描述符。
    • buf :数据读取后存放的缓冲区地址。
    • count :要读取的字节数。
    • 返回值:⼀个 ssize_t 类型的整数,它表⽰成功读取的字节数或者出现错误时的返回值。
  • 错误:如果读取过程中出现错误,返回-1,并设置全局变量 errno 来指⽰出现的具体错误类型。如:
    • EINTR :读取操作被信号中断。
    • EFAULT : buf 指针⽆效,即指向的内存不可访问。
    • EIO :发⽣硬件I/O错误。
    • EISDIR :试图从⽬录⽂件中读取数据

write

  • 功能:向⽂件描述符指向的⽂件写⼊数据。
  • 原型:
    1
    ssize_t write(int fd, const void *buf, size_t count);
  • 参数:
    • fd :⽂件描述符。
    • buf :要写⼊⽂件的数据的缓冲区地址。
    • count :要写⼊的字节数。

lseek

  • 功能:重新定位⽂件描述符的⽂件偏移量。
  • 原型:
    1
    off_t lseek(int fd, off_t offset, int whence);
  • 参数:
    • fd :⽂件描述符。
    • offset :相对偏移量。
    • whence :偏移量的起始位置,如⽂件开头( SEEK_SET )、当前位置( SEEK_CUR )、⽂件末尾( SEEK_END)。

stat函数

  • 功能:获取⽂件的状态信息。
  • 原型:
    1
    int stat(const char *pathname, struct stat *statbuf);
  • 参数:
    • pathname :⽂件路径。
    • statbuf : stat 结构体,存储获取到的⽂件信息。

⽬录操作函数

  • 功能:提供了⼀系列操作⽬录的函数,如:
    • opendir :打开⼀个⽬录流。
    • readdir :读取⽬录流中的下⼀个⽬录项。
    • closedir :关闭⽬录流。

dup函数和dup2函数

  • dup函数:⽤于复制⽂件描述符。
  • dup :创建⼀个新的⽂件描述符,复制指定的⽂件描述符。
  • dup2函数:与 dup 类似,但可以指定新的⽂件描述符值。

fcntl函数

  • 功能:改变已打开的⽂件的性质。
  • 原型:
    1
    int fcntl(int fd, int cmd, ... /* arg */ );
  • 用途:包括改变⽂件描述符的标志、对⽂件加锁等。

GCC编译

1
2
gcc program.c -o program
g++ program.c -o program

编译选项
只需记住:优化级别越⾼,编译过程越慢,编译出来的程序越快

1
gcc -o my_program my_program.c -lm -L/path/to/libm

Makefile

1
2
3
4
5
6
7
8
9
10
all: program

program: program.o
gcc program.o -o program

program.o: program.c
gcc -c program.c

clean:
rm -f program program.o
  • 伪⽬标: all 和 clean 是伪⽬标,它们不代表⽂件,⽽是规则的名字。
  • 依赖关系: program 依赖于 program.o , program.o 依赖于 program.c 。
  • 规则:每个规则后的⾏定义了如何⽣成⽬标⽂件,例如⽤ gcc 来编译 .c ⽂件或链接 .o ⽂件。
  • 清理: clean 规则⽤于删除编译过程中产⽣的⽂件。
  • 规则:Makefile由⼀系列的规则构成。每个规则的格式通常为:
    1
    2
    ⽬标: 依赖
    命令

GDB调试

1
2
3
4
5
gdb program
(gdb) break main
(gdb) run
(gdb) print variable
(gdb) continue

GDB调试⼯具的基本使⽤⽅法:

  1. 启动GDB:通常使⽤命令 gdb executable 来启动GDB,其中executable是你的程序⽂件。
  2. 设置断点:在源代码中某⼀⾏设置断点,可以使⽤命令 break filename:linenumber 或者 break function_name。
    1
    (gdb) break main.cpp:10
  3. 运⾏程序:使⽤ run [args] 命令运⾏程序,可以传递命令⾏参数给程序。
    1
    (gdb) run arg1 arg2
  4. 查看变量:在程序暂停时,可以通过 print variable 查看变量的当前值。
    1
    (gdb) print myVariable
  5. 单步执⾏:
  • next (n):执⾏下⼀⾏代码,如果下⼀⾏是函数调⽤,则整个函数体将被执⾏。
  • step (s):单步执⾏,如果遇到函数调⽤,将进⼊该函数内部。
  1. 继续执⾏:使⽤ continue (c)命令继续执⾏程序,直到遇到下⼀个断点或者程序结束。
  2. 查看堆栈信息:使⽤ backtrace (bt)命令查看调⽤堆栈,了解函数调⽤层级及各层的局部变量情况。

虚拟地址空间

每个进程在Linux中拥有独⽴的虚拟地址空间,是对物理内存的抽象。这提供了内存保护和地址隔离的功能。

前置知识-⽹络编程基础

IP 和端⼝

IP (ipv4 && ipv6)

  • 定义:IP 地址(Internet Protocol Address)是指分配给⽹络设备的唯⼀地址,⽤于标识⽹络中的每⼀台计算机或设备。
  • 作用:在⽹络通信中,IP 地址⽤于定位和识别设备,以确保数据能够准确地从源地址发送到⽬的地址
    端⼝号
  • 定义:端⼝号是⽤于标识计算机上特定进程或⽹络服务的数字,范围从 0 到 65535。
  • 作⽤:通过端⼝号,操作系统能够将收到的数据包准确地交给对应的应⽤程序。

⽹络模型

OSI 七层模型

  1. 物理层(Physical Layer)
  • 功能:传输⽐特流(0 和 1),定义物理设备标准,如电压、电缆类型、传输速率。
  • ⽰例:⽹线、集线器、光纤。
  1. 数据链路层(Data Link Layer)
  • 功能:将⽐特流组织成数据帧,进⾏物理地址(MAC 地址)寻址,提供错误检测。
  • ⽰例:⽹卡驱动、交换机。
  1. ⽹络层(Network Layer)
  • 功能:负责逻辑地址(IP 地址)寻址和路由选择,实现⽹络间的数据传输。
  • ⽰例:IP 协议、路由器。
  1. 传输层(Transport Layer)
  • 功能:提供端到端的可靠或不可靠传输服务,数据传输的错误检测和恢复。
  • ⽰例:TCP、UDP 协议。
  1. 会话层(Session Layer)
  • 功能:管理通信会话,建⽴、维护和终⽌会话。
  • ⽰例:RPC、SQL 会话。
  1. 表⽰层(Presentation Layer)
  • 功能:数据的格式化、加密、解密、压缩。
  • ⽰例:JPEG、MPEG、SSL/TLS。
  1. 应⽤层(Application Layer)
  • 功能:为应⽤程序提供⽹络服务。
  • ⽰例:HTTP、FTP、SMTP。

TCP/IP 四层模型

TCP/IP 模型是互联⽹中实际使⽤的模型,更加简化实⽤,将通信过程分为四个层次:

  1. 链路层(Link Layer)
  • 对应 OSI 的物理层和数据链路层。
  • 功能:处理硬件设备和数据帧传输。
  • ⽰例:以太⽹、Wi-Fi。
  1. ⽹络层(Internet Layer)
  • 对应 OSI 的⽹络层。
  • 功能:处理 IP 地址寻址和路由。
  • ⽰例:IP 协议、ICMP。
  1. 传输层(Transport Layer)
  • 对应 OSI 的传输层。
  • 功能:提供端到端的数据传输服务。
  • ⽰例:TCP、UDP 协议。
  1. 应⽤层(Application Layer)
  • 对应 OSI 的会话层、表⽰层、应⽤层。
  • 功能:为应⽤程序提供⽹络服务。
  • ⽰例:HTTP、FTP、DNS。

两种模型的⽐较

  • OSI 模型更注重理论,提供了⼀个全⾯的⽹络通信框架。
  • TCP/IP 模型更加实际,直接对应互联⽹的实际协议和应⽤。

协议

TCP(Transmission Control Protocol)

  • 特点:
    • ⾯向连接:通信前需要建⽴连接(三次握⼿)。
    • 可靠传输:提供数据确认、重传机制,保证数据不丢失、不重复。
    • 字节流传输:数据以字节流的形式传输,没有明确的消息边界。
  • 应⽤场景:⽂件传输(FTP)、邮件(SMTP)、⽹⻚浏览(HTTP/HTTPS)。

UDP(User Datagram Protocol)

  • 特点:
    • ⽆连接:不需要建⽴连接,直接发送数据。
    • 不可靠传输:不保证数据到达,不提供重传机制。
    • 数据报传输:以独⽴的数据报形式传输,有明确的消息边界。
  • 应⽤场景:视频直播、在线游戏、语⾳通话、DNS 查询。

字节序

⼤端字节序(Big Endian)

  • 定义:⾼位字节存储在内存的低地址处,低位字节存储在⾼地址处。
  • 形象⽐喻:数据的⾼位在左边,像阅读书本⼀样从左到右。

⼩端字节序(Little Endian)

  • 定义:低位字节存储在内存的低地址处,⾼位字节存储在⾼地址处。
  • 形象⽐喻:数据的低位在左边,与⼤端相反。

⽹络字节序

  • 定义:为了在不同字节序的主机之间正确传输数据,⽹络通信统⼀采⽤⼤端字节序。
  • 转换函数:
  • htonl :将 32 位主机字节序转换为⽹络字节序。
  • htons :将 16 位主机字节序转换为⽹络字节序。
  • ntohl :将 32 位⽹络字节序转换为主机字节序。
  • ntohs :将 16 位⽹络字节序转换为主机字节序。

为什么需要统⼀字节序

  • 原因:不同计算机架构可能采⽤不同的字节序,如果不统⼀,会导致数据解析错误。
  • 解决⽅案:在⽹络通信中,发送⽅将数据转换为⽹络字节序,接收⽅再转换回主机字节序。

IP 操作函数

inet_pton 函数

  • 作⽤:将点分⼗进制的 IP 地址字符串转换为⽹络字节序的数值形式。
  • 原型:
    1
    int inet_pton(int af, const char *src, void *dst);
  • 参数:
    • af :地址族, AF_INET 表⽰ IPv4, AF_INET6 表⽰ IPv6。
    • src :点分⼗进制的 IP 地址字符串。
    • dst :指向存储结果的内存地址。
  • 返回值:
    • 成功:返回 1。
    • 失败:返回 0(⽆效地址)或 -1(系统错误)。

inet_ntop 函数

  • 作⽤:将⽹络字节序的 IP 地址数值转换为点分⼗进制的字符串形式。
  • 原型:
    1
    const char *inet_ntop(int af, const void *src, char *dst, socklen_tsize);
  • 参数:
    • af :地址族, AF_INET 或 AF_INET6 。
    • src :指向⽹络字节序的 IP 地址数值。
    • dst :⽤于存储结果字符串的缓冲区。
    • size :缓冲区⼤⼩,IPv4 通常为 INET_ADDRSTRLEN (16)。
  • 返回值:
    • 成功:返回指向结果字符串的指针。
    • 失败:返回 NULL 。

⽰例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <arpa/inet.h>
int main() {

// 点分⼗进制 IP 地址字符串
const char* ip_str = "192.168.1.1";
struct in_addr addr;

// 将字符串转换为⽹络字节序的数值
if (inet_pton(AF_INET, ip_str, &addr) == 1) {
std::cout << "转换后的⽹络字节序数值: " << addr.s_addr << std::endl;
} else {
std::cerr << "⽆法将 IP 地址转换为数值形式" << std::endl;
}

return 1;
}
// ⽤于存储转换后的字符串
char ip_buffer[INET_ADDRSTRLEN];

// 将⽹络字节序的数值转换回字符串
if (inet_ntop(AF_INET, &addr, ip_buffer, INET_ADDRSTRLEN) != nullptr) {
std::cout << "转换回的点分⼗进制 IP 地址: " << ip_buffer << std::endl;
} else {
std::cerr << "⽆法将数值形式转换回 IP 地址字符串" << std::endl;
return 1;
}

return 0;
}
1
2
转换后的⽹络字节序数值: 16885952
转换回的点分⼗进制 IP 地址: 192.168.1.1

Socket 基础

什么是 Socket

  • 定义:Socket(套接字)是⽹络通信的端点,⽤于描述 IP 地址和端⼝,是应⽤程序与⽹络之间的接⼝。
  • 作⽤:通过 Socket,应⽤程序可以向⽹络中发送和接收数据,实现进程间的通信。

Socket 的类型

  • 流式套接字(SOCK_STREAM):⽤于⾯向连接的 TCP 通信,提供可靠的数据传输。
  • 数据报套接字(SOCK_DGRAM):⽤于⽆连接的 UDP 通信,不保证数据可靠性。

基本流程

  1. 创建套接字:调⽤ socket() 函数,指定地址族、套接字类型和协议。
  2. 绑定地址(服务器端):调⽤ bind() 函数,将套接字绑定到特定的 IP 地址和端⼝号。
  3. 监听连接(服务器端):调⽤ listen() 函数,等待客⼾端的连接请求。
  4. 接受连接(服务器端):调⽤ accept() 函数,建⽴与客⼾端的连接。
  5. 连接服务器(客⼾端):调⽤ connect() 函数,向服务器发起连接请求。
  6. 数据传输:使⽤ send() 、 recv() 或 write() 、 read() 进⾏数据发送和接收。
  7. 关闭套接字:调⽤ close() 函数,关闭连接。

Sockaddr 数据结构

1
2
3
4
struct sockaddr {
sa_family_t sa_family; // 地址族,如 AF_INET、AF_INET6
char sa_data[14]; // 地址数据,具体内容与地址族相关
};

sockaddr_in

1
2
3
4
5
6
struct sockaddr_in {
sa_family_t sin_family; // 地址族,必须设置为 AF_INET
in_port_t sin_port; // 16 位端⼝号,需要使⽤ `htons()` 转换为⽹络字节序
struct in_addr sin_addr; // 32 位 IP 地址,使⽤ `inet_pton()` 设置
char sin_zero[8];// 填充字段,必须设置为 0
};
  • sin_family :地址族,IPv4 使⽤ AF_INET 。
  • sin_port :端⼝号,注意转换字节序。
  • sin_addr :IP 地址,可以使⽤ INADDR_ANY 绑定所有本地地址。
    sockaddr_in6
    1
    2
    3
    4
    5
    6
    7
    struct sockaddr_in6 {
    sa_family_t sin6_family; // 地址族,AF_INET6
    in_port_t sin6_port; // 端⼝号,⽹络字节序
    uint32_t sin6_flowinfo; // 流信息,通常为 0
    struct in6_addr sin6_addr; // IPv6 地址
    uint32_t sin6_scope_id; // 作⽤域 ID,通常为 0
    };

使⽤

  • 在调⽤ Socket 函数时,需要将具体的地址结构转换为通⽤的 sockaddr 结构。
    1
    2
    3
    struct sockaddr_in addr;
    // ... 初始化 addr ...
    bind(sock_fd, (struct sockaddr*)&addr, sizeof(addr));

TCP 通信

服务器端

  1. 创建套接字: socket()
    • 指定地址族 AF_INET 、套接字类型 SOCK_STREAM 、协议 0 。
  2. 绑定地址: bind()
    • 将套接字绑定到指定的 IP 地址和端⼝号。
  3. 监听连接: listen()
    • 开始监听客⼾端的连接请求,指定最⼤连接数。
  4. 接受连接: accept()
    • 阻塞等待客⼾端的连接请求,返回⼀个新的套接字⽤于通信。

客⼾端

  1. 创建套接字: socket()
    • 与服务器端相同。
  2. 连接服务器: connect()
    • 指定服务器的 IP 地址和端⼝号,发起连接请求。
  3. 数据传输
    • 发送数据: send() 或 write()
    • 接收数据: recv() 或 read()
  4. 关闭连接
    • 关闭套接字: close()

TCP 三次握⼿

建⽴ TCP 连接的过程,确保双⽅都准备好进⾏通信。
第⼀次握⼿

  • 客⼾端:发送 SYN 包(同步序列号),请求建⽴连接。
  • 包含信息:客⼾端的初始序列号(ISN)。
    第⼆次握⼿
  • 服务器:收到 SYN 包,发送 SYN-ACK 包,表⽰同意连接。
  • 包含信息:服务器的 ISN,确认序列号(客⼾端 ISN + 1)。
    第三次握⼿
  • 客⼾端:收到 SYN-ACK 包,发送 ACK 包,确认连接建⽴。
  • 包含信息:确认序列号(服务器 ISN + 1)。
    连接建⽴
  • 状态:双⽅进⼊ ESTABLISHED 状态,开始数据传输。

TCP 滑动窗⼝

流量控制机制,⽤于控制发送⽅的发送速率,确保接收⽅有⾜够的缓冲区处理数据。
流量控制

  • ⽬的:防⽌发送⽅发送过快,接收⽅处理不过来,导致数据丢失。
    滑动窗⼝机制
  • 发送窗⼝:发送⽅维护,表⽰允许发送但未确认的数据量。
  • 接收窗⼝:接收⽅维护,表⽰可接收数据的缓冲区⼤⼩。
  • 动态调整:根据⽹络状况和接收⽅反馈,调整窗⼝⼤⼩。
    ⼯作原理
  1. 发送数据:发送⽅根据窗⼝⼤⼩发送数据,不必等待每个数据的确认。
  2. 接收确认:接收⽅接收数据后,发送 ACK 确认,并告知可⽤窗⼝⼤⼩。
  3. 窗⼝滑动:收到 ACK 后,发送⽅窗⼝向前滑动,继续发送新的数据。
    优点
  • 提⾼⽹络吞吐量,充分利⽤带宽。
  • 避免⽹络拥塞,提⾼传输效率。

TCP 四次挥⼿

终⽌ TCP 连接的过程,确保双⽅都同意关闭连接。
第⼀次挥⼿

  • 主动关闭⽅(通常是客⼾端):发送 FIN 包,表⽰不再发送数据。
  • 状态变为:FIN_WAIT_1。
    第⼆次挥⼿
  • 被动关闭⽅(通常是服务器):收到 FIN 包,发送 ACK 包,确认收到关闭请求。
  • 状态变为:CLOSE_WAIT。
    第三次挥⼿
  • 被动关闭⽅:处理完剩余数据后,发送 FIN 包,表⽰同意关闭连接。
  • 状态变为:LAST_ACK。
    第四次挥⼿
  • 主动关闭⽅:收到 FIN 包,发送 ACK 包,确认连接关闭。
  • 状态变为:TIME_WAIT,等待⼀段时间后进⼊ CLOSED。

为什么需要四次挥⼿

  • 原因:TCP 是全双⼯通信,发送和接收独⽴进⾏,双⽅需要分别关闭发送和接收通道。
  • 确保双⽅都已完成数据传输,避免数据丢失。

TCP 通信并发处理

实现⾼并发的⽹络服务器,需要有效地管理多个客⼾端连接。
M学⻓的考研Top帮
多进程模型

  • 原理:为每个客⼾端连接创建⼀个新的进程。
  • 优点:进程隔离性强,安全性⾼。
  • 缺点:系统开销⼤,创建进程代价⾼。
    多线程模型
  • 原理:为每个客⼾端连接创建⼀个新的线程。
  • 优点:⽐多进程开销⼩,共享内存空间。
  • 缺点:需要注意线程同步,可能出现竞争条件。
    I/O 多路复⽤
  • 原理:使⽤ select 、 poll 、 epoll 等系统调⽤,⼀个进程同时管理多个⽹络连接。
  • 优点:⾼效处理⼤量并发连接,资源占⽤少。
  • 缺点:编程复杂度较⾼,需要管理事件和状态。

半关闭

半关闭:TCP 连接的⼀⽅完成数据发送后,可以关闭发送⽅向,但仍然可以接收数据。
函数: shutdown(socket, how)
• how 参数:
◦ SHUT_RD :关闭接收通道。
◦ SHUT_WR :关闭发送通道。
◦ SHUT_RDWR :同时关闭发送和接收通道。
应⽤场景

  • ⻓连接中:⼀⽅发送完数据后,通知对⽅⾃⼰不再发送数据,但仍需接收对⽅的数据。
  • 节省资源:及时关闭不必要的通道,减少资源占⽤。

端⼝复⽤

定义:允许多个套接字绑定到同⼀ IP 地址和端⼝号的机制。
实现⽅式
SO_REUSEADDR :

  • 作⽤:允许在套接字关闭后⽴即重⽤地址。
  • 使⽤场景:服务器重启时,端⼝尚未释放,设置该选项可以⽴即绑定。

SO_REUSEPORT :

  • 作⽤:允许多个进程或线程绑定到同⼀ IP 和端⼝,实现负载均衡。
  • 使⽤条件:需要内核和库的⽀持(Linux 内核 3.9 及以上)。
    1
    2
    int opt = 1;
    setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
  • 参数说明:
    • sock_fd :套接字描述符。
    • SOL_SOCKET :套接字层级。
    • SO_REUSEADDR :选项名称。
    • &opt :选项值, 1 表⽰启⽤。

前置知识 - DOCKER

解决问题之“在我的电脑上能运⾏”

Docker的核⼼概念和⼯作原理

  • 容器(Container):轻量级、可执⾏的软件包,封装软件代码及其所有依赖,保证应⽤在任何环
    境中都能⼀致地运⾏。
  • 镜像(Image):容器的静态模版,包含创建容器所需的⽂件系统和应⽤程序。
  • 层叠的⽂件系统:镜像采⽤层叠的⽅式存储,每个层代表镜像的⼀部分。容器启动时,Docker叠加这些层并添加⼀个可写层。
  • Docker镜像不是⼀个单⼀的、巨⼤的⽂件,⽽是由⼀系列只读的层(或称为层叠的⽂件系统层)组成。每当你执⾏⼀个 RUN 命令来安装软件包、修改配置⽂件或添加⽂件时,Docker都会创建⼀个新的层,并记录下这些更改。每个新层仅包含相对于上⼀层的差异,因此形成了⼀个⾼效的增量式存储结构。
  • 隔离性:容器与主机和其他容器隔离,拥有⾃⼰的⽂件系统、⽹络配置和进程空间。
  • 轻量级:容器共享主机操作系统内核,运⾏在⾃⼰的隔离空间中,⽐虚拟机更轻量级。
  • 可移植性:容器包含应⽤程序及其所有依赖,可以在任何⽀持Docker的主机上运⾏。

Dockerfile 语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# FROM ubuntu:latest
FROM ubuntu:latest

# 替换为清华⼤学的 Ubuntu 镜像源
RUN sed -i
's/http:\/\/ports.ubuntu.com/http:\/\/mirrors.tuna.tsinghua.edu.cn/g'
/etc/apt/sources.list

# 更新软件包并安装所需的库
RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential python3 python3-
pip libsqlite3-dev curl && \
rm -rf /var/lib/apt/lists/*

# 复制代码到容器中
COPY . /usr/src/myapp

# 设置⼯作⽬录
WORKDIR /usr/src/myapp

# 暴露端⼝
EXPOSE 80 8080 8081

Dockerfile 中的⼀些常⽤指令

Dockerfile 是⼀个⽂本⽂件,包含了⼀系列指令,⽤于构建 Docker 镜像。每条指令对应⼀个镜像层。

  • FROM:指定基础镜像,是所有 Dockerfile 必须的指令。
    1
    FROM ubuntu:20.04
  • RUN:在镜像内执⾏命令,常⽤于安装软件或依赖。
    1
    RUN apt-get update && apt-get install -y python3
  • COPY:将⽂件或⽬录从上下⽂⽬录复制到镜像中。
    1
    COPY . /app
  • WORKDIR:设置⼯作⽬录。
    1
    WORKDIR /app
  • CMD:指定容器启动时要运⾏的命令。
    1
    CMD ["python3", "app.py"]
  • EXPOSE:声明容器监听的端⼝。
    1
    EXPOSE 8080
  • ENV:设置环境变量。
    1
    ENV DEBUG=true

Docker 常⻅命令

  • docker build: 使⽤Dockerfile构建镜像。
    1
    docker build -t my-image-name .
    其中 . 表⽰当前⽬录作为构建上下⽂, -t ⽤来指定标签名。
  • docker run: 创建并启动⼀个新的容器。
    1
    docker run -d --name container-name -p host-port:container-port my-image￾name
    -d 表⽰后台运⾏, –name 为容器命名, -p 进⾏端⼝映射。
  • docker start/stop/restart: 控制容器的⽣命周期。
    1
    2
    3
    docker start container-name
    docker stop container-name
    docker restart container-name
  • docker ps: 列出正在运⾏的容器。
    1
    docker ps
    若要查看所有容器(包括未运⾏的),可使⽤ docker ps -a 。
  • docker exec: 在运⾏中的容器内执⾏命令。
    1
    docker exec -it container-name bash
  • docker logs: 查看容器的⽇志输出。
    1
    docker logs container-name
  • docker rm: 删除容器。
    1
    docker rm container-name
  • docker rmi: 删除镜像。
    1
    docker rmi my-image-name