myserver-helloword
前置知识 - C++
⾯向对象编程(OOP)
- 封装:通过public,private,protected 来实现类的访问权限。
- 继承:复⽤现有类功能的⽅式,通过派⽣新类来扩展原有类的功能,⽽⽆需重新编写原始代码。
- 多态:使⼀个类的不同实例在不同情况下表现出不同的⾏为特性,使得具有不同内部结构的
- 抽象:通过抽象类和接⼝定义对象的⾏为,忽略具体的实现细节。
对象能够共享相同的外部接⼝,从⽽达到灵活和统⼀处理的⽬的。- 静态多态(编译时多态):通过函数重载(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
19class Example {
public:
Example() {
try {
// 构造函数中的代码
} catch (...) {
// 处理构造过程中的异常
throw; // 重新抛出异常
}
}
~Example() {
try {
// 析构函数中的代码
} catch (...) {
// 处理析构过程中的异常
// 不要重新抛出异常
}
}
}
堆和栈的区别
- 栈(Stack):
- 栈上存储的数据主要包括:局部变量(包括基本类型和内置数组等)、函数参数、返回地址以及编译器⾃动分配的临时变量。(简单说基本就是new之外的局部变量都在这⾥)
- 存储特点 :
- ⾃动分配和释放:栈空间由编译器⾃动管理,当作⽤域结束时,栈上的变量会⾃动销毁,⽆需程序员⼿动释放。
- 空间有限且固定:栈的⼤⼩⼀般在程序启动时由系统预先设定,并且有限制。如果栈上分配的空间过⼤,可能导致栈溢出。
- ⽆碎⽚区分:栈上存储的数据连续,不会产⽣内存碎⽚。
1
2
3void someFunction() {
int stackVariable = 10; // 这个变量存储在栈上
}
- 堆(Heap):
- 堆上存储的数据主要包括:通过 new 操作符动态分配的对象、数组或其他数据结构。
- 存储特点:
- 动态分配和释放:使⽤ new 申请内存后,需要通过 delete 来释放,否则会导致内存泄漏;同样, malloc 与 free 配合使⽤也是相同道理。
- ⼤⼩灵活可变:堆内存空间可以根据需要动态调整,没有固定的上限,但受限于系统资源。
- 可能产⽣内存碎⽚:由于频繁地分配和释放不同⼤⼩的内存块,可能会导致堆内存中存在⽆法利⽤的⼩块内存,即内存碎⽚。
1
2int* heapVariable = new int(20); // 这个对象存储在堆上
delete heapVariable;
前置知识-STL
编译时连接库
1 | g++ main.cpp -o main -l<library_name> |
链接
链接的本质是将程序中的符号引⽤(如函数调⽤、变量访问等)与它们的定义关联起来。C++ 程序通
常分为多个源⽂件或模块,每个模块可能会调⽤其他模块中的函数或使⽤其他模块中的变量。链接过
程确保每个符号都有正确的定义和实现,并将它们整合成⼀个最终的可执⾏⽂件或动态库。
- 静态链接:在编译时将库的代码嵌⼊到可执⾏⽂件中,⽣成的可执⾏⽂件不需要依赖外部的库⽂件。
- 动态链接:在运⾏时加载外部的库⽂件(如 .dll 或 .so ⽂件),可执⾏⽂件只包含库的引⽤,库的实际代码存储在外部的动态库中。
链接的原理:
C++ 的编译过程通常分为⼏个步骤,其中链接是⾮常重要的⼀个环节。整个流程可以分为以下⼏个步骤:
- 预处理(Preprocessing):对源代码进⾏宏展开、头⽂件包含等预处理操作。
- 编译(Compilation):将预处理后的 C++ 源代码转换为⽬标代码(机器码),⽣成.o 或.obj ⽂件(⽬标⽂件)。
- 链接(Linking):将所有⽬标⽂件与库⽂件进⾏链接,⽣成最终的可执⾏⽂件。
静态链接与动态链接的区别
泛型编程
1 | // 泛型函数模板,允许不同类型的参数 T 和 U |
前置知识-C++11/14/17 新特性(auto lambda shared_ptr)
C++ 编译过程
- 预处理(Preprocessing)
- 宏定义展开:使⽤ #define 定义的宏会被替换为其内容。
- 处理头⽂件: #include 指令会将指定头⽂件的内容插⼊到源代码中。
- 删除注释:预处理器会移除所有注释内容,
- 词法分析(Lexical Analysis)
- 预处理后的⽂本被编译器分解成⼀系列符号或标记(tokens)。这些tokens包括关键字、标识符、常量、运算符和分隔符等。
- ⽰例:对于语句 int a = 5; ,会分解为以下tokens: int 、 a 、 = 、 5 和 ; 。
- 语法分析(Syntax Analysis 或 Parsing)
- 编译器根据C++的语法规则,将tokens组合成结构化的数据结构,即抽象语法树(AST)。
- 语义分析(Semantic Analysis)
- 在构建AST的同时,编译器进⾏语义检查,以确保所有变量和函数的声明与使⽤符合语义规则。
- 包括类型检查、作⽤域解析和其他静态语义规则的验证。例如,如果尝试将字符串赋值给整型变量,编译器将在这⼀阶段报告错误。
- 中间代码⽣成(Intermediate Code Generation)
- 编译器⽣成⼀种与特定机器⽆关的中间表⽰形式,如字节码或三地址码。这种形式便于后续的优化和跨平台移植。
- 优化(Optimization)
- 编译器对中间代码进⾏各种优化,以提⾼运⾏效率和减少资源消耗。
- 优化⽰例:循环优化,死代码删除,常量传播。
- ⽬标代码⽣成(Code Generation)
- 编译器将优化后的中间代码转换为特定计算机架构的⽬标代码(Object Code),⽬标代码通常为汇编语⾔或直接是⼆进制格式,能被CPU执⾏
- 链接(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
- 范围for循环(Range-based for loop):允许⽅便地遍历容器。
- 右值引⽤(Rvalue References)和移动语义(Move Semantics):允许资源的转移⽽⾮拷⻉,提⾼性能。
- 初始化列表(Initializer lists):⽤于容器和对象的统⼀初始化⽅式。
- 线程⽀持库(Threading Library):⽀持多线程编程。
- C++14
- 返回类型推导(Return type deduction):函数返回类型可以⽤ auto 关键字⾃动推导。
- 泛型Lambda表达式(Generic lambdas):Lambda表达式可以使⽤ auto 在参数中实现参数类型推导。
- C++17
- 并⾏算法(Parallel algorithms):标准库算法的并⾏版本,⽤于提⾼性能。
前置知识-Linux基础知识
操作系统
操作系统是⼀个控制和管理计算机硬件与软件资源的软件系统
Linux特性
- ⼀切皆⽂件(Everything is a file): 在Linux中,⽆论是硬件设备、⽬录、常规⽂件还是⽹络套接字等资源,都被抽象为“⽂件”,并可通过统⼀的系统调⽤来操作。这意味着你可以对它们进⾏读写操作,就像对待普通⽂件⼀样。例如,硬件设备可以通过特殊的设备⽂件来访问和控制。
- 强⼤的命令⾏⼯具: Linux提供了丰富的命令⾏⼯具,如 bash shell 、 grep 、 sed 、awk 、 find 等,这些⼯具可以⾼效地处理⽂本、查找信息和管理系统。
- 模块化设计: Linux内核采⽤模块化设计,允许动态加载和卸载驱动程序、⽂件系统以及其他内核模块,使得系统可以根据需要灵活扩展功能。
- 多⽤⼾与多任务:⽀持多个⽤⼾同时操作,能够⾼效管理多个任务
Linux⽂件操作与管理
- ⽂件描述符:⽂件描述符是⼀个抽象指标(⼀个⾮负整数),⽤于表⽰对⽂件或其他I/O资源的访问。
- ⽂件描述符表:每个进程都有⼀张独⽴的⽂件描述符表,确保每个描述符对应着⼀个独⽴的I/O资源。
- 包括文件描述符(fd)和文件指针
- 打开⽂件表、i-node 表。这两张表存储了每个打开⽂件的打开⽂件句柄(open file handle)。⼀个打开⽂件句柄存储了与⼀个打开⽂件相关的全部信息。
- 打开⽂件表(Open File Table):记录了进程如何使⽤特定的打开⽂件,如当前读写位置、访问模式等。这样可以快速定位到进程对⽂件的具体操作状态,提⾼系统处理I/O请求的效率。
- 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 | gcc program.c -o program |
编译选项
只需记住:优化级别越⾼,编译过程越慢,编译出来的程序越快
1 | gcc -o my_program my_program.c -lm -L/path/to/libm |
Makefile
1 | all: program |
- 伪⽬标: all 和 clean 是伪⽬标,它们不代表⽂件,⽽是规则的名字。
- 依赖关系: program 依赖于 program.o , program.o 依赖于 program.c 。
- 规则:每个规则后的⾏定义了如何⽣成⽬标⽂件,例如⽤ gcc 来编译 .c ⽂件或链接 .o ⽂件。
- 清理: clean 规则⽤于删除编译过程中产⽣的⽂件。
- 规则:Makefile由⼀系列的规则构成。每个规则的格式通常为:
1
2⽬标: 依赖
命令
GDB调试
1 | gdb program |
GDB调试⼯具的基本使⽤⽅法:
- 启动GDB:通常使⽤命令 gdb executable 来启动GDB,其中executable是你的程序⽂件。
- 设置断点:在源代码中某⼀⾏设置断点,可以使⽤命令 break filename:linenumber 或者 break function_name。
1
(gdb) break main.cpp:10
- 运⾏程序:使⽤ run [args] 命令运⾏程序,可以传递命令⾏参数给程序。
1
(gdb) run arg1 arg2
- 查看变量:在程序暂停时,可以通过 print variable 查看变量的当前值。
1
(gdb) print myVariable
- 单步执⾏:
- next (n):执⾏下⼀⾏代码,如果下⼀⾏是函数调⽤,则整个函数体将被执⾏。
- step (s):单步执⾏,如果遇到函数调⽤,将进⼊该函数内部。
- 继续执⾏:使⽤ continue (c)命令继续执⾏程序,直到遇到下⼀个断点或者程序结束。
- 查看堆栈信息:使⽤ backtrace (bt)命令查看调⽤堆栈,了解函数调⽤层级及各层的局部变量情况。
虚拟地址空间
每个进程在Linux中拥有独⽴的虚拟地址空间,是对物理内存的抽象。这提供了内存保护和地址隔离的功能。
前置知识-⽹络编程基础
IP 和端⼝
IP (ipv4 && ipv6)
- 定义:IP 地址(Internet Protocol Address)是指分配给⽹络设备的唯⼀地址,⽤于标识⽹络中的每⼀台计算机或设备。
- 作用:在⽹络通信中,IP 地址⽤于定位和识别设备,以确保数据能够准确地从源地址发送到⽬的地址
端⼝号 - 定义:端⼝号是⽤于标识计算机上特定进程或⽹络服务的数字,范围从 0 到 65535。
- 作⽤:通过端⼝号,操作系统能够将收到的数据包准确地交给对应的应⽤程序。
⽹络模型
OSI 七层模型
- 物理层(Physical Layer)
- 功能:传输⽐特流(0 和 1),定义物理设备标准,如电压、电缆类型、传输速率。
- ⽰例:⽹线、集线器、光纤。
- 数据链路层(Data Link Layer)
- 功能:将⽐特流组织成数据帧,进⾏物理地址(MAC 地址)寻址,提供错误检测。
- ⽰例:⽹卡驱动、交换机。
- ⽹络层(Network Layer)
- 功能:负责逻辑地址(IP 地址)寻址和路由选择,实现⽹络间的数据传输。
- ⽰例:IP 协议、路由器。
- 传输层(Transport Layer)
- 功能:提供端到端的可靠或不可靠传输服务,数据传输的错误检测和恢复。
- ⽰例:TCP、UDP 协议。
- 会话层(Session Layer)
- 功能:管理通信会话,建⽴、维护和终⽌会话。
- ⽰例:RPC、SQL 会话。
- 表⽰层(Presentation Layer)
- 功能:数据的格式化、加密、解密、压缩。
- ⽰例:JPEG、MPEG、SSL/TLS。
- 应⽤层(Application Layer)
- 功能:为应⽤程序提供⽹络服务。
- ⽰例:HTTP、FTP、SMTP。
TCP/IP 四层模型
TCP/IP 模型是互联⽹中实际使⽤的模型,更加简化实⽤,将通信过程分为四个层次:
- 链路层(Link Layer)
- 对应 OSI 的物理层和数据链路层。
- 功能:处理硬件设备和数据帧传输。
- ⽰例:以太⽹、Wi-Fi。
- ⽹络层(Internet Layer)
- 对应 OSI 的⽹络层。
- 功能:处理 IP 地址寻址和路由。
- ⽰例:IP 协议、ICMP。
- 传输层(Transport Layer)
- 对应 OSI 的传输层。
- 功能:提供端到端的数据传输服务。
- ⽰例:TCP、UDP 协议。
- 应⽤层(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 |
|
1 | 转换后的⽹络字节序数值: 16885952 |
Socket 基础
什么是 Socket
- 定义:Socket(套接字)是⽹络通信的端点,⽤于描述 IP 地址和端⼝,是应⽤程序与⽹络之间的接⼝。
- 作⽤:通过 Socket,应⽤程序可以向⽹络中发送和接收数据,实现进程间的通信。
Socket 的类型
- 流式套接字(SOCK_STREAM):⽤于⾯向连接的 TCP 通信,提供可靠的数据传输。
- 数据报套接字(SOCK_DGRAM):⽤于⽆连接的 UDP 通信,不保证数据可靠性。
基本流程
- 创建套接字:调⽤ socket() 函数,指定地址族、套接字类型和协议。
- 绑定地址(服务器端):调⽤ bind() 函数,将套接字绑定到特定的 IP 地址和端⼝号。
- 监听连接(服务器端):调⽤ listen() 函数,等待客⼾端的连接请求。
- 接受连接(服务器端):调⽤ accept() 函数,建⽴与客⼾端的连接。
- 连接服务器(客⼾端):调⽤ connect() 函数,向服务器发起连接请求。
- 数据传输:使⽤ send() 、 recv() 或 write() 、 read() 进⾏数据发送和接收。
- 关闭套接字:调⽤ close() 函数,关闭连接。
Sockaddr 数据结构
1 | struct sockaddr { |
sockaddr_in
1 | struct sockaddr_in { |
- sin_family :地址族,IPv4 使⽤ AF_INET 。
- sin_port :端⼝号,注意转换字节序。
- sin_addr :IP 地址,可以使⽤ INADDR_ANY 绑定所有本地地址。
sockaddr_in61
2
3
4
5
6
7struct 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
3struct sockaddr_in addr;
// ... 初始化 addr ...
bind(sock_fd, (struct sockaddr*)&addr, sizeof(addr));
TCP 通信
服务器端
- 创建套接字: socket()
- 指定地址族 AF_INET 、套接字类型 SOCK_STREAM 、协议 0 。
- 绑定地址: bind()
- 将套接字绑定到指定的 IP 地址和端⼝号。
- 监听连接: listen()
- 开始监听客⼾端的连接请求,指定最⼤连接数。
- 接受连接: accept()
- 阻塞等待客⼾端的连接请求,返回⼀个新的套接字⽤于通信。
客⼾端
- 创建套接字: socket()
- 与服务器端相同。
- 连接服务器: connect()
- 指定服务器的 IP 地址和端⼝号,发起连接请求。
- 数据传输
- 发送数据: send() 或 write()
- 接收数据: recv() 或 read()
- 关闭连接
- 关闭套接字: close()
TCP 三次握⼿
建⽴ TCP 连接的过程,确保双⽅都准备好进⾏通信。
第⼀次握⼿
- 客⼾端:发送 SYN 包(同步序列号),请求建⽴连接。
- 包含信息:客⼾端的初始序列号(ISN)。
第⼆次握⼿ - 服务器:收到 SYN 包,发送 SYN-ACK 包,表⽰同意连接。
- 包含信息:服务器的 ISN,确认序列号(客⼾端 ISN + 1)。
第三次握⼿ - 客⼾端:收到 SYN-ACK 包,发送 ACK 包,确认连接建⽴。
- 包含信息:确认序列号(服务器 ISN + 1)。
连接建⽴ - 状态:双⽅进⼊ ESTABLISHED 状态,开始数据传输。
TCP 滑动窗⼝
流量控制机制,⽤于控制发送⽅的发送速率,确保接收⽅有⾜够的缓冲区处理数据。
流量控制
- ⽬的:防⽌发送⽅发送过快,接收⽅处理不过来,导致数据丢失。
滑动窗⼝机制 - 发送窗⼝:发送⽅维护,表⽰允许发送但未确认的数据量。
- 接收窗⼝:接收⽅维护,表⽰可接收数据的缓冲区⼤⼩。
- 动态调整:根据⽹络状况和接收⽅反馈,调整窗⼝⼤⼩。
⼯作原理
- 发送数据:发送⽅根据窗⼝⼤⼩发送数据,不必等待每个数据的确认。
- 接收确认:接收⽅接收数据后,发送 ACK 确认,并告知可⽤窗⼝⼤⼩。
- 窗⼝滑动:收到 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
2int 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 | # FROM ubuntu:latest |
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构建镜像。其中 . 表⽰当前⽬录作为构建上下⽂, -t ⽤来指定标签名。
1
docker build -t my-image-name .
- docker run: 创建并启动⼀个新的容器。-d 表⽰后台运⾏, –name 为容器命名, -p 进⾏端⼝映射。
1
docker run -d --name container-name -p host-port:container-port my-imagename
- docker start/stop/restart: 控制容器的⽣命周期。
1
2
3docker start container-name
docker stop container-name
docker restart container-name - docker ps: 列出正在运⾏的容器。若要查看所有容器(包括未运⾏的),可使⽤ docker ps -a 。
1
docker ps
- 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