五万字长文总结 C/C++ 知识(下)

356次阅读  |  发布于2年以前

网络层

IP 网际协议

IP 地址分类:

IP 地址类别 网络号 网络范围 主机号 IP 地址范围
A 类 8bit,第一位固定为 0 0 —— 127 24bit 1.0.0.0 —— 127.255.255.255
B 类 16bit,前两位固定为 10 128.0 —— 191.255 16bit 128.0.0.0 —— 191.255.255.255
C 类 24bit,前三位固定为 110 192.0.0 —— 223.255.255 8bit 192.0.0.0 —— 223.255.255.255
D 类 前四位固定为 1110,后面为多播地址
E 类 前五位固定为 11110,后面保留为今后所用 .

应用:

内部网关协议

外部网关协议

IP多播

VPN 和 NAT

路由表包含什么?

  1. 网络 ID(Network ID, Network number):就是目标地址的网络 ID。
  2. 子网掩码(subnet mask):用来判断 IP 所属网络
  3. 下一跳地址/接口(Next hop / interface):就是数据在发送到目标地址的旅途中下一站的地址。其中 interface 指向 next hop(即为下一个 route)。一个自治系统(AS, Autonomous system)中的 route 应该包含区域内所有的子网络,而默认网关(Network id: 0.0.0.0, Netmask: 0.0.0.0)指向自治系统的出口。

根据应用和执行的不同,路由表可能含有如下附加信息:

  1. 花费(Cost):就是数据发送过程中通过路径所需要的花费。
  2. 路由的服务质量
  3. 路由中需要过滤的出/入连接列表

运输层

协议:

端口:

应用程序 FTP TELNET SMTP DNS TFTP HTTP HTTPS SNMP
端口号 21 23 25 53 69 80 443 161

受限于公众号文章字数限制,后续部分请看【今天的第二篇推文】,

TCP

特征:

TCP 如何保证可靠传输:

TCP 首部

TCP:状态控制码(Code,Control Flag),占 6 比特,含义如下:

UDP

特征:

TCP 与 UDP 的区别

  1. TCP 面向连接,UDP 是无连接的;
  2. TCP 提供可靠的服务,也就是说,通过 TCP 连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP 尽最大努力交付,即不保证可靠交付
  3. TCP 的逻辑通信信道是全双工的可靠信道;UDP 则是不可靠信道
  4. 每一条 TCP 连接只能是点到点的;UDP 支持一对一,一对多,多对一和多对多的交互通信
  5. TCP 面向字节流(可能出现黏包问题),实际上是 TCP 把数据看成一连串无结构的字节流;UDP 是面向报文的(不会出现黏包问题)
  6. UDP 没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如 IP 电话,实时视频会议等)
  7. TCP 首部开销20字节;UDP 的首部开销小,只有 8 个字节

TCP 黏包问题

原因

TCP 是一个基于字节流的传输服务(UDP 基于报文的),“流” 意味着 TCP 所传输的数据是没有边界的。所以可能会出现两个数据包黏在一起的情况。

解决

TCP 流量控制

概念

流量控制(flow control)就是让发送方的发送速率不要太快,要让接收方来得及接收。

方法

利用可变窗口进行流量控制

TCP 拥塞控制

概念

拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。

方法

TCP 传输连接管理

因为 TCP 三次握手建立连接、四次挥手释放连接很重要,所以附上《计算机网络(第 7 版)-谢希仁》书中对此章的详细描述:https://github.com/huihut/interview/blob/master/images/TCP-transport-connection-management.png

TCP 三次握手建立连接

UDP 报文

【TCP 建立连接全过程解释】

  1. 客户端发送 SYN 给服务器,说明客户端请求建立连接;
  2. 服务端收到客户端发的 SYN,并回复 SYN+ACK 给客户端(同意建立连接);
  3. 客户端收到服务端的 SYN+ACK 后,回复 ACK 给服务端(表示客户端收到了服务端发的同意报文);
  4. 服务端收到客户端的 ACK,连接已建立,可以数据传输。
TCP 为什么要进行三次握手?

【答案一】因为信道不可靠,而 TCP 想在不可靠信道上建立可靠地传输,那么三次通信是理论上的最小值。(而 UDP 则不需建立可靠传输,因此 UDP 不需要三次握手。)

Google Groups . TCP 建立连接为什么是三次握手?{技术}{网络通信}

【答案二】因为双方都需要确认对方收到了自己发送的序列号,确认过程最少要进行三次通信。

知乎 . TCP 为什么是三次握手,而不是两次或四次?

【答案三】为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

《计算机网络(第 7 版)-谢希仁》

【TCP 释放连接全过程解释】

  1. 客户端发送 FIN 给服务器,说明客户端不必发送数据给服务器了(请求释放从客户端到服务器的连接);
  2. 服务器接收到客户端发的 FIN,并回复 ACK 给客户端(同意释放从客户端到服务器的连接);
  3. 客户端收到服务端回复的 ACK,此时从客户端到服务器的连接已释放(但服务端到客户端的连接还未释放,并且客户端还可以接收数据);
  4. 服务端继续发送之前没发完的数据给客户端;
  5. 服务端发送 FIN+ACK 给客户端,说明服务端发送完了数据(请求释放从服务端到客户端的连接,就算没收到客户端的回复,过段时间也会自动释放);
  6. 客户端收到服务端的 FIN+ACK,并回复 ACK 给客户端(同意释放从服务端到客户端的连接);
  7. 服务端收到客户端的 ACK 后,释放从服务端到客户端的连接。
TCP 为什么要进行四次挥手?

【问题一】TCP 为什么要进行四次挥手?/ 为什么 TCP 建立连接需要三次,而释放连接则需要四次?

【答案一】因为 TCP 是全双工模式,客户端请求关闭连接后,客户端向服务端的连接关闭(一二次挥手),服务端继续传输之前没传完的数据给客户端(数据传输),服务端向客户端的连接关闭(三四次挥手)。所以 TCP 释放连接时服务器的 ACK 和 FIN 是分开发送的(中间隔着数据传输),而 TCP 建立连接时服务器的 ACK 和 SYN 是一起发送的(第二次握手),所以 TCP 建立连接需要三次,而释放连接则需要四次。

【问题二】为什么 TCP 连接时可以 ACK 和 SYN 一起发送,而释放时则 ACK 和 FIN 分开发送呢?(ACK 和 FIN 分开是指第二次和第三次挥手)

【答案二】因为客户端请求释放时,服务器可能还有数据需要传输给客户端,因此服务端要先响应客户端 FIN 请求(服务端发送 ACK),然后数据传输,传输完成后,服务端再提出 FIN 请求(服务端发送 FIN);而连接时则没有中间的数据传输,因此连接时可以 ACK 和 SYN 一起发送。

【问题三】为什么客户端释放最后需要 TIME-WAIT 等待 2MSL 呢?

【答案三】

  1. 为了保证客户端发送的最后一个 ACK 报文能够到达服务端。若未成功到达,则服务端超时重传 FIN+ACK 报文段,客户端再重传 ACK,并重新计时。

  2. 防止已失效的连接请求报文段出现在本连接中。TIME-WAIT 持续 2MSL 可使本连接持续的时间内所产生的所有报文段都从网络中消失,这样可使下次连接中不会出现旧的连接报文段。

应用层

DNS

域名:

FTP

TELNET

WWW

URL

标准格式:

完整格式:

其中【访问凭证信息@;:端口号;?查询;#片段ID】都属于选填项 如:https://github.com/huihut/interview#cc

HTTP

HTTP(HyperText Transfer Protocol,超文本传输协议)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP 是万维网的数据通信的基础。

请求方法

方法 意义
OPTIONS 请求一些选项信息,允许客户端查看服务器的性能
GET 请求指定的页面信息,并返回实体主体
HEAD 类似于 get 请求,只不过返回的响应中没有具体的内容,用于获取报头
POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改
PUT 从客户端向服务器传送的数据取代指定的文档的内容
DELETE 请求服务器删除指定的页面
TRACE 回显服务器收到的请求,主要用于测试或诊断

状态码(Status-Code)

更多状态码:菜鸟教程 . HTTP状态码

其他协议

网络编程

Socket

Socket 中的 read()、write() 函数

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
read()
write()

Socket 中 TCP 的三次握手建立连接

我们知道 TCP 建立连接要进行 “三次握手”,即交换三个分组。大致流程如下:

  1. 客户端向服务器发送一个 SYN J
  2. 服务器向客户端响应一个 SYN K,并对 SYN J 进行确认 ACK J+1
  3. 客户端再想服务器发一个确认 ACK K+1

只有就完了三次握手,但是这个三次握手发生在 Socket 的那几个函数中呢?请看下图:

socket 中发送的 TCP 三次握手

从图中可以看出:

  1. 当客户端调用 connect 时,触发了连接请求,向服务器发送了 SYN J 包,这时 connect 进入阻塞状态;
  2. 服务器监听到连接请求,即收到 SYN J 包,调用 accept 函数接收请求向客户端发送 SYN K ,ACK J+1,这时 accept 进入阻塞状态;
  3. 客户端收到服务器的 SYN K ,ACK J+1 之后,这时 connect 返回,并对 SYN K 进行确认;
  4. 服务器收到 ACK K+1 时,accept 返回,至此三次握手完毕,连接建立。

Socket 中 TCP 的四次握手释放连接

上面介绍了 socket 中 TCP 的三次握手建立过程,及其涉及的 socket 函数。现在我们介绍 socket 中的四次握手释放连接的过程,请看下图:

socket 中发送的 TCP 四次握手

图示过程如下:

  1. 某个应用进程首先调用 close 主动关闭连接,这时 TCP 发送一个 FIN M;
  2. 另一端接收到 FIN M 之后,执行被动关闭,对这个 FIN 进行确认。它的接收也作为文件结束符传递给应用进程,因为 FIN 的接收意味着应用进程在相应的连接上再也接收不到额外数据;
  3. 一段时间之后,接收到文件结束符的应用进程调用 close 关闭它的 socket。这导致它的 TCP 也发送一个 FIN N;
  4. 接收到这个 FIN 的源发送端 TCP 对它进行确认。

这样每个方向上都有一个 FIN 和 ACK。

数据库

范式

设计模式

各大设计模式例子参考:CSDN专栏 . C++ 设计模式 系列博文

设计模式工程目录

单例模式

单例模式例子

抽象工厂模式

抽象工厂模式例子

适配器模式

适配器模式例子

桥接模式

桥接模式例子

观察者模式

观察者模式例子

设计模式的六大原则

链接装载库

内存、栈、堆

一般应用程序内存空间有如下区域:

栈保存了一个函数调用所需要的维护信息,常被称为堆栈帧(Stack Frame)或活动记录(Activate Record),一般包含以下几方面:

堆分配算法:

“段错误(segment fault)” 或 “非法操作,该内存地址不能 read/write”

典型的非法指针解引用造成的错误。当指针指向一个不允许读写的内存地址,而程序却试图利用指针来读或写该地址时,会出现这个错误。

普遍原因:

编译链接

各平台文件格式

平台 可执行文件 目标文件 动态库/共享对象 静态库
Windows exe obj dll lib
Unix/Linux ELF、out o so a
Mac Mach-O o dylib、tbd、framework a、framework

编译链接过程

  1. 预编译(预编译器处理如 #include#define 等预编译指令,生成 .i.ii 文件)
  2. 编译(编译器进行词法分析、语法分析、语义分析、中间代码生成、目标代码生成、优化,生成 .s 文件)
  3. 汇编(汇编器把汇编码翻译成机器码,生成 .o 文件)
  4. 链接(连接器进行地址和空间分配、符号决议、重定位,生成 .out 文件)

现在版本 GCC 把预编译和编译合成一步,预编译编译程序 cc1、汇编器 as、连接器 ld

MSVC 编译环境,编译器 cl、连接器 link、可执行文件查看器 dumpbin

目标文件

编译器编译源代码后生成的文件叫做目标文件。目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。

可执行文件(Windows 的 .exe 和 Linux 的 ELF)、动态链接库(Windows 的 .dll 和 Linux 的 .so)、静态链接库(Windows 的 .lib 和 Linux 的 .a)都是按照可执行文件格式存储(Windows 按照 PE-COFF,Linux 按照 ELF)

目标文件格式

PE 和 ELF 都是 COFF(Common File Format)的变种

目标文件存储结构
功能
File Header 文件头,描述整个文件的文件属性(包括文件是否可执行、是静态链接或动态连接及入口地址、目标硬件、目标操作系统等)
.text section 代码段,执行语句编译成的机器代码
.data section 数据段,已初始化的全局变量和局部静态变量
.bss section BSS 段(Block Started by Symbol),未初始化的全局变量和局部静态变量(因为默认值为 0,所以只是在此预留位置,不占空间)
.rodata section 只读数据段,存放只读数据,一般是程序里面的只读变量(如 const 修饰的变量)和字符串常量
.comment section 注释信息段,存放编译器版本信息
.note.GNU-stack section 堆栈提示段

其他段略

链接的接口————符号

在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。

如下符号表(Symbol Table):

Symbol(符号名) Symbol Value (地址)
main 0x100
Add 0x123
... ...

Linux 的共享库(Shared Library)

Linux 下的共享库就是普通的 ELF 共享对象。

共享库版本更新应该保证二进制接口 ABI(Application Binary Interface)的兼容

命名

libname.so.x.y.z

路径

大部分包括 Linux 在内的开源系统遵循 FHS(File Hierarchy Standard)的标准,这标准规定了系统文件如何存放,包括各个目录结构、组织和作用。

动态链接器会在 /lib/usr/lib 和由 /etc/ld.so.conf 配置文件指定的,目录中查找共享库

环境变量

so 共享库的编写

使用 CLion 编写共享库

创建一个名为 MySharedLib 的共享库

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MySharedLib)

set(CMAKE_CXX_STANDARD 11)

add_library(MySharedLib SHARED library.cpp library.h)

library.h

#ifndef MYSHAREDLIB_LIBRARY_H
#define MYSHAREDLIB_LIBRARY_H

// 打印 Hello World!
void hello();

// 使用可变模版参数求和
template <typename T>
T sum(T t)
{
    return t;
}
template <typename T, typename ...Types>
T sum(T first, Types ... rest)
{
    return first + sum<T>(rest...);
}

#endif

library.cpp

#include <iostream>
#include "library.h"

void hello() {
    std::cout << "Hello, World!" << std::endl;
}

so 共享库的使用(被可执行项目调用)

使用 CLion 调用共享库

创建一个名为 TestSharedLib 的可执行项目

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(TestSharedLib)

# C++11 编译
set(CMAKE_CXX_STANDARD 11)

# 头文件路径
set(INC_DIR /home/xx/code/clion/MySharedLib)
# 库文件路径
set(LIB_DIR /home/xx/code/clion/MySharedLib/cmake-build-debug)

include_directories(${INC_DIR})
link_directories(${LIB_DIR})
link_libraries(MySharedLib)

add_executable(TestSharedLib main.cpp)

# 链接 MySharedLib 库
target_link_libraries(TestSharedLib MySharedLib)

main.cpp

#include <iostream>
#include "library.h"
using std::cout;
using std::endl;

int main() {

    hello();
    cout << "1 + 2 = " << sum(1,2) << endl;
    cout << "1 + 2 + 3 = " << sum(1,2,3) << endl;

    return 0;
}

执行结果

Hello, World!
1 + 2 = 3
1 + 2 + 3 = 6

Windows 应用程序入口函数

_tWinMain 与 _tmain 函数声明

Int WINAPI _tWinMain(
    HINSTANCE hInstanceExe,
    HINSTANCE,
    PTSTR pszCmdLine,
    int nCmdShow);

int _tmain(
    int argc,
    TCHAR *argv[],
    TCHAR *envp[]);
应用程序类型 入口点函数 嵌入可执行文件的启动函数
处理ANSI字符(串)的GUI应用程序 _tWinMain(WinMain) WinMainCRTSartup
处理Unicode字符(串)的GUI应用程序 _tWinMain(wWinMain) wWinMainCRTSartup
处理ANSI字符(串)的CUI应用程序 _tmain(Main) mainCRTSartup
处理Unicode字符(串)的CUI应用程序 _tmain(wMain) wmainCRTSartup
动态链接库(Dynamic-Link Library) DllMain _DllMainCRTStartup

Windows 的动态链接库(Dynamic-Link Library)

知识点来自《Windows核心编程(第五版)》

用处

注意

加载 Windows 程序的搜索顺序

  1. 包含可执行文件的目录
  2. Windows 的系统目录,可以通过 GetSystemDirectory 得到
  3. 16 位的系统目录,即 Windows 目录中的 System 子目录
  4. Windows 目录,可以通过 GetWindowsDirectory 得到
  5. 进程的当前目录
  6. PATH 环境变量中所列出的目录

DLL 入口函数

DllMain 函数

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch(fdwReason)
    {
    case DLL_PROCESS_ATTACH:
        // 第一次将一个DLL映射到进程地址空间时调用
        // The DLL is being mapped into the process' address space.
        break;
    case DLL_THREAD_ATTACH:
        // 当进程创建一个线程的时候,用于告诉DLL执行与线程相关的初始化(非主线程执行)
        // A thread is bing created.
        break;
    case DLL_THREAD_DETACH:
        // 系统调用 ExitThread 线程退出前,即将终止的线程通过告诉DLL执行与线程相关的清理
        // A thread is exiting cleanly.
        break;
    case DLL_PROCESS_DETACH:
        // 将一个DLL从进程的地址空间时调用
        // The DLL is being unmapped from the process' address space.
        break;
    }
    return (TRUE); // Used only for DLL_PROCESS_ATTACH
}

载入卸载库

FreeLibraryAndExitThread 函数声明

// 载入库
HMODULE WINAPI LoadLibrary(
  _In_ LPCTSTR lpFileName
);
HMODULE LoadLibraryExA(
  LPCSTR lpLibFileName,
  HANDLE hFile,
  DWORD  dwFlags
);
// 若要在通用 Windows 平台(UWP)应用中加载 Win32 DLL,需要调用 LoadPackagedLibrary,而不是 LoadLibrary 或 LoadLibraryEx
HMODULE LoadPackagedLibrary(
  LPCWSTR lpwLibFileName,
  DWORD   Reserved
);

// 卸载库
BOOL WINAPI FreeLibrary(
  _In_ HMODULE hModule
);
// 卸载库和退出线程
VOID WINAPI FreeLibraryAndExitThread(
  _In_ HMODULE hModule,
  _In_ DWORD   dwExitCode
);

显示地链接到导出符号

GetProcAddress 函数声明

FARPROC GetProcAddress(
  HMODULE hInstDll,
  PCSTR pszSymbolName  // 只能接受 ANSI 字符串,不能是 Unicode
);

DumpBin.exe 查看 DLL 信息

VS 的开发人员命令提示符 使用 DumpBin.exe 可查看 DLL 库的导出段(导出的变量、函数、类名的符号)、相对虚拟地址(RVA,relative virtual address)。如:

DUMPBIN -exports D:\mydll.dll

DLL 头文件

// MyLib.h

#ifdef MYLIBAPI

// MYLIBAPI 应该在全部 DLL 源文件的 include "Mylib.h" 之前被定义
// 全部函数/变量正在被导出

#else

// 这个头文件被一个exe源代码模块包含,意味着全部函数/变量被导入
#define MYLIBAPI extern "C" __declspec(dllimport)

#endif

// 这里定义任何的数据结构和符号

// 定义导出的变量(避免导出变量)
MYLIBAPI int g_nResult;

// 定义导出函数原型
MYLIBAPI int Add(int nLeft, int nRight);

DLL 源文件

// MyLibFile1.cpp

// 包含标准Windows和C运行时头文件
#include <windows.h>

// DLL源码文件导出的函数和变量
#define MYLIBAPI extern "C" __declspec(dllexport)

// 包含导出的数据结构、符号、函数、变量
#include "MyLib.h"

// 将此DLL源代码文件的代码放在此处
int g_nResult;

int Add(int nLeft, int nRight)
{
    g_nResult = nLeft + nRight;
    return g_nResult;
}

DLL 库的使用(运行时动态链接 DLL)

DLL 库的使用(运行时动态链接 DLL)

// A simple program that uses LoadLibrary and 
// GetProcAddress to access myPuts from Myputs.dll. 

#include <windows.h> 
#include <stdio.h> 

typedef int (__cdecl *MYPROC)(LPWSTR); 

int main( void ) 
{ 
    HINSTANCE hinstLib; 
    MYPROC ProcAdd; 
    BOOL fFreeResult, fRunTimeLinkSuccess = FALSE; 

    // Get a handle to the DLL module.

    hinstLib = LoadLibrary(TEXT("MyPuts.dll")); 

    // If the handle is valid, try to get the function address.

    if (hinstLib != NULL) 
    { 
        ProcAdd = (MYPROC) GetProcAddress(hinstLib, "myPuts"); 

        // If the function address is valid, call the function.

        if (NULL != ProcAdd) 
        {
            fRunTimeLinkSuccess = TRUE;
            (ProcAdd) (L"Message sent to the DLL function\n"); 
        }
        // Free the DLL module.

        fFreeResult = FreeLibrary(hinstLib); 
    } 

    // If unable to call the DLL function, use an alternative.
    if (! fRunTimeLinkSuccess) 
        printf("Message printed from executable\n"); 

    return 0;
}

运行库(Runtime Library)

典型程序运行步骤

  1. 操作系统创建进程,把控制权交给程序的入口(往往是运行库中的某个入口函数)
  2. 入口函数对运行库和程序运行环境进行初始化(包括堆、I/O、线程、全局变量构造等等)。
  3. 入口函数初始化后,调用 main 函数,正式开始执行程序主体部分。
  4. main 函数执行完毕后,返回到入口函数进行清理工作(包括全局变量析构、堆销毁、关闭I/O等),然后进行系统调用结束进程。

一个程序的 I/O 指代程序与外界的交互,包括文件、管程、网络、命令行、信号等。更广义地讲,I/O 指代操作系统理解为 “文件” 的事物。

glibc 入口

_start -> __libc_start_main -> exit -> _exit

其中 main(argc, argv, __environ) 函数在 __libc_start_main 里执行。

MSVC CRT 入口

int mainCRTStartup(void)

执行如下操作:

  1. 初始化和 OS 版本有关的全局变量。
  2. 初始化堆。
  3. 初始化 I/O。
  4. 获取命令行参数和环境变量。
  5. 初始化 C 库的一些数据。
  6. 调用 main 并记录返回值。
  7. 检查错误并将 main 的返回值返回。

C 语言运行库(CRT)

大致包含如下功能:

C语言标准库(ANSI C)

包含:

海量数据处理

音视频

其他

书籍

语言

算法

系统

网络

其他

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8