网络编程

Linux前置知识

主要是为了网络编程的考试

因为我是一个Windows选手,所以Linux的恶补了下

Linux下的编译和链接

Linux下的动态链接

在Linux系统中,ldd 命令用于显示一个可执行文件或共享库所依赖的动态链接库信息。

1
2
3
4
aichch@sword-shield:~/桌面$ ldd chall 
linux-vdso.so.1 (0x00007ffe64ded000)
libc.so.6 => ./libc.so.6 (0x00007fb193153000)
./ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007fb193383000)

在Linux中,linux-vdso.so.1libc.so.6ld-linux-x86-64.so.2 类似于 Windows 的一些系统级 DLL 和运行时库。

  • linux-vdso.so.1 是 Linux 虚拟动态共享对象 (VDSO),它帮助用户态程序快速访问某些内核功能,而无需通过系统调用进入内核模式。常见功能包括获取时间(如 gettimeofday)等。 类似于 Windows 内核中的一些优化机制,比如 ntdll.dll 中提供的快速路径函数(如直接与内核交互的函数)。
  • libc.so.6 是 GNU C 库(glibc)的共享库,提供标准 C 函数(如 printfmallocmemcpy 等)。它是绝大多数 Linux 应用程序的基础运行时库。 类似于 Windows 的 C 运行时库(CRT),如 msvcrt.dll 或更现代的 ucrtbase.dll,它们提供标准的 C 函数。
  • ld-linux-x86-64.so.2 是动态链接器(dynamic linker),负责加载和解析程序运行时所需的共享库,并启动程序。 类似于 Windows 的动态链接加载机制,尤其是 LoadLibraryGetProcAddress 相关功能,动态链接器还可以类比为 Windows 中处理 PE 文件动态加载的功能模块。

GCC的编译常见选项

基本编译选项

-c:仅编译,不进行链接,生成目标文件(.o)。

-o :指定输出文件名。

-E:仅进行预处理,不进行编译。

-S:将代码编译成汇编代码,而不是目标文件。

-v:显示详细的编译过程。

--help:显示可用选项的帮助信息

优化选项

-O0:关闭优化(默认)。

-O1:基本优化,编译速度快。

-O2:进一步优化,包括更多代码改进。

-O3:最高级别优化,启用 CPU 密集型优化(如循环展开)。

-Os:优化生成的代码大小。

-Ofast:极致优化,不考虑标准兼容性。

-funroll-loops:展开循环,可能提高性能。

调试相关选项

-g:生成调试信息,用于调试工具(如 gdb

调试的时候,不要开优化

1
2
3
4
5
gcc -c test.c -g -O0 -o test.o
gcc test.o -o test

#或者
gcc test.c -g -O0 -o test

标准和架构支持

-std=<standard>:指定使用的 C 或 C++ 标准,如:
-std=c89-std=c99-std=c11(C 标准)。
-std=c++98-std=c++11-std=c++17(C++ 标准)。

-m32:生成 32 位代码。

-m64:生成 64 位代码

makefile

Makefile 文件描述了 Linux 系统下 C/C++ 工程的编译规则,它用来自动化编译 C/C++ 项目。一旦写编写好 Makefile 文件,只需要一个 make 命令,整个工程就开始自动编译,不再需要手动执行 GCC 命令。

具体参考这篇文章吧,用到了再用AI生成
浅显易懂 Makefile 入门 (01)— 什么是Makefile、为什么要用Makefile、Makefile规则、Makefile流程如何实现增量编译_makefile是干什么的-CSDN博客

和Windows的一些类比

鉴于本人技能树全点Windows了,看到Linux的一些专业术语真的是懵的一批,下面介绍下我认为的类比

Linux的文件描述符:

是Linux操作系统内核用来标识和管理文件资源的整型标识符。它是一个非负整数,用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符来实现。文件描述符不仅用于文件,还可以表示其他资源,如套接字、管道等。
在Windows操作系统中,与Linux的文件描述符相似的概念是文件句柄(File Handle)。Windows文件句柄是Windows操作系统用于表示文件资源的抽象,每一个文件、设备、管道等系统资源在打开后会获得一个唯一的句柄,作为进程访问该资源的标识。句柄本质上是对对象的指针

一些函数

forkexec

fork 是一个系统调用,用于创建一个新的进程。新进程(子进程)父进程 的副本,包括代码段、数据段、文件描述符等,但两者运行在独立的内存空间中。

子进程从 fork

  • 父进程中,fork 返回子进程的 PID。
  • 子进程中,fork 返回 0。

exec 是一组系统调用,用于替换当前进程的内存空间并执行新的程序。

调用 exec 后,当前进程的代码、数据段被新程序替换,但 PID 不变。

适合用于加载新的二进制文件,配合 fork 实现父子进程的分工。

实现类似Windows的CreateProcess的代码
a 程序调用 fork 创建子进程,然后在子进程中用 exec 加载并运行 b 程序

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
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>

int main() {
pid_t pid = fork(); // 创建子进程

if (pid < 0) {
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程:加载 b 程序
printf("Child process: Starting program b\n");
execl("/path/to/b", "b", NULL); // 替换为 b 的路径
perror("execl failed"); // 如果execl失败,打印错误
exit(EXIT_FAILURE);
} else {
// 父进程:等待子进程完成
printf("Parent process: Waiting for child process\n");
int status;
waitpid(pid, &status, 0); // 等待子进程结束
if (WIFEXITED(status)) {
printf("Child process exited with status %d\n", WEXITSTATUS(status));
}
}

return 0;
}

wait函数:

wait 函数用于使父进程暂停执行,直到一个或多个子进程结束。这个函数通常与 fork 函数一起使用,以确保父进程在继续执行之前等待其子进程完成它们的任务。wait 函数可以获取子进程的终止状态,这对于获取子进程的退出代码或了解子进程是否因为信号而终止非常有用。

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
31
int main() {
pid_t pid1 = fork(); // 创建第一个子进程
pid_t pid2 = fork(); // 创建第二个子进程

if (pid1 == 0 || pid2 == 0) {
// 子进程
printf("This is a child process.\n");
sleep(2); // 子进程休眠2秒
return 0; // 子进程结束
} else {
// 父进程
int status;
pid_t wpid;

wpid = waitpid(pid1, &status, 0); // 等待第一个子进程结束
if (wpid == -1) {
perror("waitpid error");
return 1;
}
printf("First child process with PID %d has finished.\n", wpid);

wpid = waitpid(pid2, &status, 0); // 等待第二个子进程结束
if (wpid == -1) {
perror("waitpid error");
return 1;
}
printf("Second child process with PID %d has finished.\n", wpid);
}

return 0;
}

在Windows与之对应的是WaitForSingleObjectWaitForMultipleObjects

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <windows.h>
#include <iostream>

int main() {
// 创建多个子进程
STARTUPINFO si1 = { 0 }, si2 = { 0 };
PROCESS_INFORMATION pi1 = { 0 }, pi2 = { 0 };
si1.cb = sizeof(si1);
si2.cb = sizeof(si2);

const char* processName1 = "notepad.exe";
const char* processName2 = "calc.exe"; // 启动计算器

// 创建第一个子进程
if (!CreateProcess(
NULL, (LPSTR)processName1, NULL, NULL, FALSE, 0, NULL, NULL, &si1, &pi1)) {
std::cerr << "Failed to create first process. Error code: " << GetLastError() << std::endl;
return 1;
}

// 创建第二个子进程
if (!CreateProcess(
NULL, (LPSTR)processName2, NULL, NULL, FALSE, 0, NULL, NULL, &si2, &pi2)) {
std::cerr << "Failed to create second process. Error code: " << GetLastError() << std::endl;
return 1;
}

std::cout << "Processes created. Waiting for both to finish..." << std::endl;

// 进程句柄数组
HANDLE handles[2] = { pi1.hProcess, pi2.hProcess };

// 等待多个进程结束
DWORD waitResult = WaitForMultipleObjects(2, handles, TRUE, INFINITE);

if (waitResult >= WAIT_OBJECT_0 && waitResult < WAIT_OBJECT_0 + 2) {
std::cout << "Both processes finished." << std::endl;

// 获取每个子进程的退出码
DWORD exitCode1 = 0, exitCode2 = 0;
if (GetExitCodeProcess(pi1.hProcess, &exitCode1)) {
std::cout << "First process exit code: " << exitCode1 << std::endl;
} else {
std::cerr << "Failed to get first process exit code. Error code: " << GetLastError() << std::endl;
}

if (GetExitCodeProcess(pi2.hProcess, &exitCode2)) {
std::cout << "Second process exit code: " << exitCode2 << std::endl;
} else {
std::cerr << "Failed to get second process exit code. Error code: " << GetLastError() << std::endl;
}
} else {
std::cerr << "Failed to wait for processes. Error code: " << GetLastError() << std::endl;
}

// 关闭进程和线程句柄
CloseHandle(pi1.hProcess);
CloseHandle(pi1.hThread);
CloseHandle(pi2.hProcess);
CloseHandle(pi2.hThread);

return 0;
}

Linux 信号(Signal)

信号是一个轻量级的机制,它允许进程通过发送一个信号来通知另一个进程发生了某些事件。每个信号都与特定的事件相关联,例如:

  • SIGINT:终端中断(通常是 Ctrl+C)。
  • SIGTERM:请求终止进程。
  • SIGKILL:强制终止进程。
  • SIGSEGV:段错误(访问无效内存)。
  • SIGCHLD:子进程状态变化(如子进程退出)。

信号的类型

  1. 标准信号:例如 SIGKILLSIGTERMSIGINT 等,它们用于通知进程发生的事件。
  2. 自定义信号:应用程序可以使用 SIGUSR1SIGUSR2 作为用户定义的信号来执行某些自定义行为。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

void signal_handler(int signal) {
if (signal == SIGINT) {
printf("Caught SIGINT, exiting program...\n");
exit(0);
}
}

int main() {
signal(SIGINT, signal_handler); // 注册 SIGINT 信号处理程序

while (1) {
// 程序继续运行,等待信号
printf("Running... Press Ctrl+C to send SIGINT\n");
sleep(1);
}

return 0;
}

Windows 中的类似机制是 事件异常处理

Linux下的同步机制

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define BUFFER_SIZE 1 // 缓冲区大小

// 缓冲区和互斥锁
int buffer = 0;
pthread_mutex_t mutex;
pthread_cond_t cond;

// 生产者线程函数
void* producer(void* arg) {
while (1) {
// 模拟生产数据
sleep(1); // 生产过程等待一秒
pthread_mutex_lock(&mutex); // 获取互斥锁

// 如果缓冲区已满,等待消费者消费数据
while (buffer == BUFFER_SIZE) {
printf("Producer: Buffer is full, waiting for consumer...\n");
pthread_cond_wait(&cond, &mutex); // 等待消费者唤醒
}

// 生产数据
buffer = 1;
printf("Producer: Produced data, buffer = %d\n", buffer);

// 通知消费者可以消费数据了
pthread_cond_signal(&cond); // 唤醒消费者

pthread_mutex_unlock(&mutex); // 释放互斥锁
}

return NULL;
}

// 消费者线程函数
void* consumer(void* arg) {
while (1) {
// 模拟消费数据
sleep(2); // 消费过程等待两秒
pthread_mutex_lock(&mutex); // 获取互斥锁

// 如果缓冲区为空,等待生产者生产数据
while (buffer == 0) {
printf("Consumer: Buffer is empty, waiting for producer...\n");
pthread_cond_wait(&cond, &mutex); // 等待生产者唤醒
}

// 消费数据
buffer = 0;
printf("Consumer: Consumed data, buffer = %d\n", buffer);

// 通知生产者可以继续生产数据了
pthread_cond_signal(&cond); // 唤醒生产者

pthread_mutex_unlock(&mutex); // 释放互斥锁
}

return NULL;
}

int main() {
pthread_t producer_thread, consumer_thread;

// 初始化互斥锁和条件变量
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);

// 创建生产者和消费者线程
pthread_create(&producer_thread, NULL, producer, NULL);
pthread_create(&consumer_thread, NULL, consumer, NULL);

// 等待线程结束
pthread_join(producer_thread, NULL);
pthread_join(consumer_thread, NULL);

// 销毁互斥锁和条件变量
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);

return 0;
}

执行流程:

生产者线程执行(producer 函数)

  • 进入循环:生产者线程不断地执行生产过程。
  • 模拟生产:每次生产数据前,调用sleep(1)模拟生产需要的时间,暂停1秒。
  • 获取互斥锁:使用pthread_mutex_lock(&mutex)获取互斥锁,确保缓冲区的访问是安全的。
  • 缓冲区已满:如果缓冲区已满(buffer == BUFFER_SIZE),生产者线程会进入while循环,调用pthread_cond_wait(&cond, &mutex)等待消费者消费数据。
    • pthread_cond_wait时,生产者线程会自动释放mutex锁,并阻塞,直到消费者线程调用pthread_cond_signal来唤醒生产者。
  • 生产数据:生产者将buffer设为1,表示缓冲区有数据可以消费。
  • 通知消费者:生产者通过调用pthread_cond_signal(&cond)来唤醒消费者线程,通知消费者有数据可以消费了。
  • 释放互斥锁:生产者线程通过pthread_mutex_unlock(&mutex)释放互斥锁,允许其他线程访问缓冲区。

消费者线程执行(consumer 函数)

  • 进入循环:消费者线程不断地执行消费过程。
  • 模拟消费:每次消费数据前,调用sleep(2)模拟消费过程中的等待时间,暂停2秒。
  • 获取互斥锁:使用pthread_mutex_lock(&mutex)获取互斥锁,确保缓冲区的访问是互斥的。
  • 缓冲区为空:如果缓冲区为空(buffer == 0),消费者线程会进入while循环,调用pthread_cond_wait(&cond, &mutex)等待生产者生产数据。
    • pthread_cond_wait时,消费者线程会自动释放mutex锁,并阻塞,直到生产者线程通过调用pthread_cond_signal唤醒消费者。
  • 消费数据:消费者将buffer设为0,表示缓冲区已经被消费。
  • 通知生产者:消费者通过调用pthread_cond_signal(&cond)来唤醒生产者线程,通知生产者可以继续生产数据了。
  • 释放互斥锁:消费者线程通过pthread_mutex_unlock(&mutex)释放互斥锁,允许其他线程访问缓冲区。

注意, 当条件变量收到信号 pthread_cond_signal 会先重新尝试获取互斥锁。也就是说,当条件满足后,线程会再次尝试获得互斥锁,并在成功获得锁后才会继续执行后续的代码。

网络前置知识:

网络模型

1732322341982

物理层理解为网卡驱动,链路层是让数据在局域网传播,网络层让数据满世界传输,传输层理解为决定是打电话还是发短信这样的,会话层类似于QQ的在线与否,表示层类似于中文还是英文,应用层表示是类似网页还是视频…

Ipv4和Mac地址:

特性 IPv4地址 MAC地址
协议层 网络层 数据链路层
作用 标识网络中的设备,主要用于路由 标识局域网中网络接口的硬件地址
范围 可以跨越多个网络(全局可见) 仅在同一个局域网(本地可见)使用
可变性 动态分配(通过DHCP或者手动设置) 静态(网卡烧录,可以伪造)

为什么局域网通信需要 MAC 地址 ?

局域网中设备通信通常是通过 以太网协议(Ethernet) 实现的,而以太网协议运行在 数据链路层。数据链路层的核心是通过 MAC 地址 标识设备。

以太网相对底层,而Ipv4是动态的,MAC是静态的,所以用MAC地址进行局域网通讯没问题,IPV4主要是设备间通讯的

以太网:

是一种使用有线连接的局域网(LAN)通信技术 , 以太网通过物理线缆将设备连接到网络交换机或路由器,提供稳定和高带宽的网络通信。

1732324018609

以太网帧(Ethernet Frame)是局域网(LAN)中数据传输的基本单元,遵循以太网协议,通常按照IEEE 802.3标准进行封装。

  1. 帧头(Header)
    • 目的地址(Destination Address):6字节,表示数据帧的接收方MAC地址。
    • 源地址(Source Address):6字节,表示数据帧的发送方MAC地址。
    • 类型(Type):2字节,表示上层协议类型,如0x0800表示IP协议。
  2. 数据负载(Payload)
    • 这是实际传输的数据部分,长度可以是46到1500字节。如果数据部分小于46字节,通常会用填充(padding)来补足到46字节。
  3. 帧校验序列(Frame Check Sequence, FCS)
    • 4字节,用于错误检测。在发送端计算整个帧(包括帧头和数据负载)的CRC(循环冗余校验)值,并附加在帧的末尾。接收端会重新计算接收到的帧的CRC值,并与接收到的FCS进行比较,以验证数据的完整性。

ARP地址解析协议

ARP(Address Resolution Protocol,地址解析协议)是一种网络层协议,用于将网络层的IP地址解析为数据链路层的MAC地址。 通俗来说,就是为了不同机器之间找目标机器的网卡MAC地址进行通讯

具体可以在WireShark进行抓包,发现会向全部端口发送信号,询问某个IPv4地址属于来自哪个网卡,只要有回应,就会做成一张表,记录对应的IP和MAC地址

1732326751103

子网,子网掩码,网关

子网(Subnetwork)是网络的一个子集,它由一个较大的网络划分而成,通常用于提高网络性能、增强安全性、优化广播域和路由效率。 通过创建子网,可以限制广播流量只在子网内部传播,而不是在整个大网络中,这有助于减少网络拥塞和提高效率。

子网掩码是例如255.255.255.0这样的,将它和任意IP地址异或,可以获得子网的基地址

网关的作用:

  1. 局域网内通信:同一子网内的设备可以直接通信,无需经过网关。
  2. 跨子网通信:当两个设备属于不同子网时,需要通过网关进行数据的转发。
  3. 访问外网:当设备需要与互联网通信时,数据会发送到网关,由网关转发到外部网络。

跨子网通信(需要网关)

  • 场景:设备 A(192.168.1.10)想与设备 C(192.168.2.10)通信,它们在不同子网:
    • A 的子网:192.168.1.0/24
    • C 的子网:192.168.2.0/24
    • 网关地址:192.168.1.1(A 的默认网关)。
  • 过程
    1. A 检查目标地址 C 是否在同一子网:
      • A 的网络部分:192.168.1
      • C 的网络部分:192.168.2
      • 结果:不在同一子网。
    2. A 将数据包发送到网关(192.168.1.1):
      • A 通过 ARP 协议获取网关的 MAC 地址。
      • 数据包的目标 MAC 地址设为网关的 MAC,目标 IP 地址保持为 C 的 IP。
    3. 网关接收数据包,检查目标 IP 地址(C 的 IP),发现它不在自己的子网内。
    4. 网关根据路由表决定如何将数据转发到 C 所在的子网:
      • 如果网关与 C 在同一子网,直接转发。
      • 如果不在,则继续向上游网关转发,直到数据到达目标子网。
    5. 数据到达 C。

VMWare的桥接和NAT的区别

桥接的原理相当于是在局域网内加了一台和本机地位一样的机器,例如本机的IP地址是192.168.6.90,那么桥接出来的机器,IP地址可能是192.168.6.88,可以理解为这两个机器都平等地接在一个交换机上

NAT的话是通过一个NAT设备进行地址转换,所以相当于本机是一个服务端(IP地址假设为192.168.6.90),然后接了一个NAT盒子IP地址是192.168.132.1,然后虚拟机192.168.132.128接在这个NAT盒子上。这样就是虚拟机可以访问外网,但是从外面找不到虚拟机

1732327534600

如果路由器坏了,桥接模式之间就不能进行通讯了,但是NAT可以

TCP协议

特点:

  1. 面向连接(有连接)
    • TCP是一个面向连接的协议,这意味着在数据传输开始之前,必须在通信的两个端点之间建立一个连接。这个过程称为三次握手,确保了双方都能发送和接收数据。
  2. 可靠
    • TCP通过使用序列号、确认应答(ACK)、重传机制和数据校验和来确保数据的可靠传输。如果数据包在传输过程中丢失或损坏,TCP会请求重传丢失的数据包,并重新组装数据以确保正确无误。
  3. 有序
    • TCP保证数据包按照发送的顺序到达接收端。即使在网络中数据包可能乱序到达,TCP也会在接收端对数据包进行排序,确保应用程序接收到的数据是有序的。
  4. 端到端
    • TCP提供端到端的通信,这意味着它负责在发送端和接收端之间建立和管理连接,而不需要中间网络设备(如路由器)参与数据传输的控制。
  5. 全双工
    • TCP允许数据在两个方向上同时传输,即发送方和接收方都可以同时发送和接收数据。这种全双工通信模式提高了网络的效率。
  6. 流量控制
    • TCP通过流量控制机制(如滑动窗口协议)来防止发送方发送数据过快,导致接收方来不及处理。这有助于避免网络拥塞和数据丢失。
  7. 拥塞控制
    • TCP还实现了拥塞控制算法(如慢启动、拥塞避免、快速重传和快速恢复),以适应网络条件的变化,减少网络拥塞的影响。
  8. 错误检测
    • TCP使用校验和来检测数据在传输过程中是否发生错误。如果检测到错误,TCP会请求重传损坏的数据包。

四次握手->三次握手

1732330925056

如上图,阿强是Client,阿珍是Server端,两个人要建立通讯,也就是C和S端都要确保管道1,2的读写端都开启

首先C向S发送一条“你好”的信息,这样可以C知道管道1的W端已经开放

然后S端收到来自C端的信息,这样S端可以知道管道1的W,R已经开启

接着S端向C端发送 确认 的信息,这样C端就知道管道1的R开好了
至此,双方都确认了管道1的R,W端开好了

接下来是S端向C端发送一个”你好”的请求,这样S端知道了管道2的W开好了

C端接收到了S端的信息,这样C端就知道了管道2的R,W端都开好了

于是C端返回一个“确认”给S端,S端接收到以后,S端就知道管道2的R开好了

至此,C和S都知道了管道1 2的W R都开放了,就可以开始通讯了

以上是四次握手

但是我们从网上查到的是三次握手啊??

1732332175936

这是因为,S端发送确认和你好,C端接收 其实应该是在同一次完成的

(这里要注意下建立连接不走管道),所以是三次握手

简化来说就是这样:

1732332341555

可靠性的实现

丢包重传:

客户端发出去一个包,服务器要回复一个ACK(表示收到),如果一定时间内没收到ACK,客户端会重新发包

1732332733617

但是服务端没发ACK又有两种情况,一种是服务端收到消息了,但是ACK发不出去,另一种是没收到,那么这个如何解决呢?

1732333137098

TCP引入了序号机制,让接收方区分是新包还是重传包,还解决了有序性

另外一个个包等待接收到才继续发效率太低了,一般都是先不管确认,直接发的,所以ACK也要带上序号信息

1732333383985

当然不确认发信息是有风险的,所以TCP有一个已发未确认是有上限的

Linux下的网络编程

//

Windows下网络编程

//

跨平台网络编程

//