Linux网络编程

一、网络编程的分层模型

示例图片

二、相关基础概念

1、GNU:Linux下的编译器的集成工具

2、gcc:Linux下c语言的编译工具

3、**g++**:Linux下c++语言的编译工具

4、make:用于处理C和C++的编译工作(项目构建工具),管理哪个文件需要更新以及如何更新的。=⇒生成可执行文件

5、makefile:就是一个规则文件,主要解决了多文件编译的问题。它制定了文件编译的顺序和哪些文件需要编译。

6、交叉编译:交叉编译可以理解为,在当前编译平台下,编译出来的程序能运行在体系结构不同的另一种目标平台上,但是编译平台本身却不能运行该程序(就是在一个平台上生成另一个平台上的可执行代码)

7、在Linux中,一切皆文件

三、进程

1、进程

指的是一段程序的执行过程。

2、进程间的通信

进程之间的内存是隔离的,如果多个进程之间需要进行信息交换,常用的方法有:

  • 管道(mkfifo):有名管道(mkfifo)和无名管道(pipe)
  • 消息队列(message queue):进程在消息队列中添加消息,其他进程从中取。其就是个消息容器,我们将消息队列称之为中间件
  • 共享内存(shared memory):多个进程可以访问同一块内存空间
  • 信号(singal):信号用于通知其他进程事件的发生
  • 信号量(semaphore):用于进程之间对共享资源进行加锁
  • 套接字(socket):用于不同计算机(主机)之间的进程间的通信

3、有名管道(FIFO)

1、有名管道:在linux中称为FIFO,即先进先出队列,同一条管道只能应用于单向通信

2、函数使用 (在代码中使用其创建管道文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/types.h>
#include <sys/stat.h>
//创建有名管道文件
int mkfifo(const char *filename, mode_t mode); //文件名+访问权限

#define FIFONAME "fifo_file"
//通过mkfifo函数创建有名管道(利用文件相关操作API读写有名管道文件)
if(mkfifo(FIFONAME, 0664) == -1)
{
if(errno != EEXIST)
{
perror("fail to mkfifo");
exit(1);
}
}

4、共享内存(shared memory)

4.1、共享内存:将共享内存映射到一个文件描述符(二进制文件)

4.2、函数使用POSIX IPC标准,POSIX IPC,注意与System V相关的IPC不一样,主要区别在于,所有POSIX IPC都是线程安全的)

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
#include <sys/mman.h>

//1、创建或者访问共享内存文件
int shm_open(const char *name, int oflag, mode_t mode); //文件名+读写权限+访问权限

//2、建立映射
/*
addr:要将文件映射到的内存地址,一般应该传递NULL来由Linux内核指定。
length:要映射的文件数据长度
port:映射的内存区域的操作权限(读写)
flags:标志位参数,包括:MAP_SHARED、MAP_PRIVATE与MAP_ANONYMOUS。
MAP_SHARED: 建立共享,用于进程间通信,如果没有这个标志,则别的进程即使能打开文件,也看不到数据。
MAP_PRIVATE: 只有进程自己用的内存区域
MAP_ANONYMOUS:匿名映射区
fd:用来建立映射区的文件描述符,用 shm_open打开或者open打开的文件
offset:映射文件相对于文件头的偏移位置,应该按4096字节对齐
返回值:成功返回映射的内存地址指针,可以用这个地址指针对映射的文件内容进行读写操作,读写文件数据如同操作内存一样;如果 失败则返回NULL
*/
void* mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

//3、解除映射
/*
addr:由mmap成功返回的地址
length:要取消的内存长度
*/
int munmap(void *addr, size_t length);

//4、销毁共享内存文件
int shm_unlink(const char *name); //文件名

//5、设置文件大小
int ftruncate(int fd, off_t length);

4.3、测试样例(编译的时候加上需要加上 -lrt选项)

writer.c ,创建内存共享文件并写入数据(gcc write.c -o write -lrt)

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
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#define MMAP_DATA_SIZE 1024

#define USE_MMAP 1

int main(int argc,char * argv[])
{
char * data;
//创建共享内存映射文件(二进制)
int fd = shm_open("shm-file", O_CREAT|O_RDWR, 0777);
if (fd < 0)
{
printf("shm_open failed!\n");
return -1;
}
//设置映射文件大小
ftruncate(fd, MMAP_DATA_SIZE);

if (USE_MMAP)
{
//建立映射
data = (char*)mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (!data)
{
printf("mmap failed\n");
close(fd);
}
//写内容
sprintf(data, "This is a share memory! %d\n", fd);
//解除映射
munmap(data, MMAP_DATA_SIZE);
}
else
{
char buf[1024];
int len = sprintf(buf,"This is a share memory by write! ! %d\n",fd);
if (write(fd, buf, len) <= 0)
{
printf("write file %d failed!%d\n",len,errno);
}
}

close(fd); //关闭文件描述符
getchar(); //运行阻塞

shm_unlink("shm-file"); //删除映射文件(注释掉就可以看到映射文件了)

return 0;
}

reader.c: 打开内存共享文件读数据

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
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#define MMAP_DATA_SIZE 1024

int main(int argc,char * argv[])
{
char * data;
//创建共享内存映射文件(二进制)
int fd = shm_open("shm-file", O_RDWR, 0777);
if(fd < 0)
{
printf("error open shm object\n");
return -1;
}
//建立映射
data = (char*)mmap(NULL, MMAP_DATA_SIZE, PROT_READ, MAP_SHARED, fd, 0);
if (!data) {
printf("mmap failed!\n");
close(fd);
return -1;
}

printf("共享内存中的数据为:%s\n",data);
//解除映射
munmap(data,MMAP_DATA_SIZE);
//关闭映射文件描述符
close(fd);
getchar(); //运行阻塞

return 0;
}

4.4、共享对象数据存储

映射文件:存储在dev/shm目录(这是Linux临时文件系统,tmpfs)下(不执行shm_unlink的情况下,就可以看到)

示例图片

5、消息队列(message queue)

5.1、消息队列:看作是一个存放消息的容器,使用时其常被称之为中间件

消息队列的完整使用场景中至少包含三个角色:

  • 消息处理中心:负责消息的接收、存储、转发等
  • 消息生产者:负责产生和发送消息到消息处理中心
  • 消息消费者:负责从消息处理中心获取消息,并进行相应的处理
示例图片

5.2、函数使用POSIX IPC

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
#include <mqueue.h>

//1、创建或者访问消息队列
/*
name:消息队列名称
oflag:读写权限
mode:访问权限
attr:属性信息,默认为NULL,队列用默认属性创建,也可以指定
返回值:类型为mqd_t,创建成功的话返回的是
*/
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);

//2、消息队列的属性设置
struct mq_attr
{
long mq_flags //消息队列的标志:0或O_NONBLOCK,用来表示是否阻塞
long mq_maxmsg //消息队列的消息容量
long mq_msgsize //消息队列中每个消息的最大字节数
long mq_curmsgs //消息队列中当前的消息数目
}

//3、发送消息
/*
mqdes:消息队列描述符
msg_ptr:指向消息的指针
msg_len:消息长度
msg_prio:消息优先级 它是一个小于MQ_PRIO_MAX的数,数值越大,优先级越高
*/
mqd_t mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio);

//4、接收消息
/*
mqdes:消息队列描述符
msg_ptr:可接收的消息
msg_len:消息长度
msg_prio:返回接受到的消息优先级
返回值:成功返回接收到的消息字节数;失败返回-1
*/
ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned *msg_prio);

//5、关闭消息队列
mqd_t mq_close(mqd_t mqdes); //消息队列描述符

//6、删除消息队列
mqd_t mq_unlink(const char *name); //消息队列名字

//7、建立或删除消息到达通知时间
mqd_t mq_notify(mqd_t mqdes, const struct sigevent *notification); //消息队列描述值+通知设置

5.3、测试样例

producer.c (g++ producer.cpp -o producer -lrt)

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
#include <iostream>
#include <fcntl.h> // For O_* constants
#include <sys/stat.h> // For mode constants
#include <mqueue.h>
#include <cstring>
#include <cstdlib>
#include <unistd.h>

int main() {
// 消息队列名称
const char* queueName = "/test_queue";
// 消息队列属性
struct mq_attr attr;
attr.mq_flags = 0;
attr.mq_maxmsg = 10;
attr.mq_msgsize = 256;
attr.mq_curmsgs = 0;

// 打开消息队列
mqd_t mq = mq_open(queueName, O_CREAT | O_WRONLY, 0644, &attr);
if (mq == -1)
{
perror("mq_open");
return -1;
}

// 发送消息
for (int i = 0; i < 10; ++i)
{
std::string message = "Message " + std::to_string(i);
if (mq_send(mq, message.c_str(), message.size() + 1, 0) == -1)
{
perror("mq_send");
return -1;
}
printf("Produced: %s\n", message.c_str());
sleep(1); // 模拟生产间隔
}

// 关闭消息队列
mq_close(mq);

//删除消息队列
//mq_unlink(queueName); //不运行的话就可以在临时文件系统中看到该文件

return 0;
}

生产者持续产生消息

示例图片

consumer.c

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
#include <iostream>
#include <fcntl.h> // For O_* constants
#include <sys/stat.h> // For mode constants
#include <mqueue.h>
#include <cstring>
#include <cstdlib>
#include <unistd.h>

int main() {
// 消息队列名称
const char* queueName = "/test_queue";

// 打开消息队列
mqd_t mq = mq_open(queueName, O_RDONLY);
if (mq == -1) {
std::cerr << "Failed to open message queue: " << strerror(errno) << std::endl;
exit(EXIT_FAILURE);
}

// 消费消息
char buffer[256];
memset(buffer, 0, sizeof(buffer));
while (1)
{

if(mq_receive(mq, buffer, sizeof(buffer), nullptr)>0)
{
printf("Consumed: %s\n", buffer);
}
else
{
perror("mq_receive");
return -1;
}

sleep(1); // 模拟消费间隔
}

// 关闭消息队列
mq_close(mq);

return 0;
}

消费者持续消耗消息

示例图片

5.4、消息队列存储的位置

消息队列文件:存储在dev/mqueue目录(这是Linux临时文件系统,tmpfs)下(不执行shm_unlink的情况下,就可以看到)

示例图片

6、信号量(semaphore)

6.1、信号量:本质上是一个计数器,用于协调多个线程对共享数据对象的读/写。它的作用主要是用来保护共享资源(共享内存、消息队列、socket连接池、数据连接池等),保证共享资源在同一时刻只有一个进程独享

6.2、信号量分类二进制信号量和计数信号量

  • 二进制信号量:其值只能是0或1,用于进程间的互斥。当信号量的值为1时,表示资源可用;当信号量的值为0时,表示资源已被占用,其他进程需要等待。
  • 计数信号量:其值可以是任何非负整数,用于进程间的同步。当信号量的值大于0时,表示资源可用;当信号量的值为0时,表示资源已被占用,其他进程需要等待

6.3、函数使用POSIX IPC标准,有名信号量)

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
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>

//1、有名信号量创建
/*
name:信号量的名称(后缀)
oflag:信号量的读写权限
mode:信号量的执行权限
value:新创建信号量的初始值
*/
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);

//2、信号量的关闭
int sem_close(sem_t *sem); //信号量描述符

//3、有名信号量的删除
int sem_unlink(const char *name);

//信号量的使用
//4、等待信号量(信号量减一)
int sem_wait(sem_t *sem);

//5、发布信号量(信号量加一)
int sem_post(sem_t *sem);

6.4、测试样例(g++ semaphore_test.cpp -o semaphore_test -lpthread)

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
86
87
88
89
90
91
#include <iostream>
#include <pthread.h>
#include <semaphore.h>
#include <fcntl.h> // For O_* constants
#include <sys/stat.h> // For mode constants
#include <unistd.h> // For close()
#include <cstring>

#define SEM_NAME1 "sem_1"
#define SEM_NAME2 "sem_2"

void* thread1_func(void* arg)
{
sem_t* sem1 = sem_open(SEM_NAME1, O_RDWR);
sem_t* sem2 = sem_open(SEM_NAME2, O_RDWR);
int i=3;
while (i)
{
sem_wait(sem1); // 信号量1减一
printf("Thread 1 is running!!!\n");
sem_post(sem2); // 信号量2加一
i--;
}
sleep(3);
return nullptr;
}

void* thread2_func(void* arg)
{
sem_t* sem1 = sem_open(SEM_NAME1, O_RDWR);
sem_t* sem2 = sem_open(SEM_NAME2, O_RDWR);
int i=3;

while (i)
{
sem_wait(sem2); // 信号量2减一
printf("Thread 2 is running!!!\n");
sem_post(sem1); // 信号量1加一
i--;
}
sleep(3);
return nullptr;
}

int main()
{
pthread_t thread1, thread2;
pthread_attr_t thread1_attr, thread2_attr;

// 创建有名信号量
sem_t* sem1 = sem_open(SEM_NAME1, O_CREAT | O_EXCL, 0644, 1); // 初始值为1
sem_t* sem2 = sem_open(SEM_NAME2, O_CREAT | O_EXCL, 0644, 0); // 初始值为0

if (sem1 == SEM_FAILED || sem2 == SEM_FAILED)
{
perror("sem_open");
return 1;
}

// 初始化线程id
memset(&thread1, 0, sizeof(thread1));
memset(&thread2, 0, sizeof(thread2));

// 初始化线程属性
pthread_attr_init(&thread1_attr);
pthread_attr_init(&thread2_attr);

// 设置线程为分离状态
pthread_attr_setdetachstate(&thread1_attr, PTHREAD_CREATE_DETACHED);
pthread_attr_setdetachstate(&thread2_attr, PTHREAD_CREATE_DETACHED);

// 创建线程
pthread_create(&thread1, &thread1_attr, thread1_func, nullptr);
pthread_create(&thread2, &thread2_attr, thread2_func, nullptr);

// 销毁线程属性结构
pthread_attr_destroy(&thread1_attr);
pthread_attr_destroy(&thread2_attr);

sleep(1); // 主线程睡眠,让子线程有机会运行


// 关闭和删除有名信号量
sem_close(sem1);
sem_close(sem2);

//sem_unlink(SEM_NAME1); //删除信号量文件描述符
//sem_unlink(SEM_NAME2);

return 0;
}
示例图片

6.5、信号量存储的位置

POSIX有名信号量是在文件系统中创建的,它们的文件位置通常是在一个临时文件系统中,例如/dev/shm(用于共享内存的文件系统)

示例图片

7、信号(singal)

7.1、信号:就是一个软件中断

7.2、常用信号

  • SIGINT(2):这是当用户在终端按下Ctrl+C时发送给前台进程的信号,通常用
    于请求进程终止
  • SIGKILL (9):这是一种强制终止进程的信号,它会立即终止目标进程,且不能被捕获或忽略
  • SIGTERM (15):这是一种用于请求进程终止的信号,通常由系统管理员或其他进程发送给目标进程
  • SIGUSR1 (10)和SIGUSR2( 12):这两个信号是用户自定义的信号,可以由应用程序使用

7.3、信号注册函数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <signal.h>

/信号处理函数声明
typedef void (*sighandler_t)(int );

/*
signal系统调用会注册某一信号对应的处理函数。如果注册成功,当进程收到这一信号时,将不会调用默认的处理函数,而是调用这里的自定义函数
int signum:要处理的信号
sighandler_t handler:当收到对应的signum_信号时,要调用的函数
return: sighandler_t返回之前的信号处理函数,如果错误会返回SEG_ERR
*/

sighandler_t signal(int signum, sighandler_t handler);

7.4、测试样例

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
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void signal_handler(int signum)
{
printf("\n收到信号%d,停止程序!\n",signum);
exit(signum);
}

int main()
{

if(signal(SIGINT, signal_handler) == SIG_ERR) //信号的异常检测
{
perror("signal");
return -1;
}

while(1)
{
printf("Hello, World!\n");
sleep(1);
}
return 0;
}
示例图片

四、线程

1、线程的定义

线程是进程内的一个执行单元,它们共享相同的地址空间和其他资源,包括文件描述符信号处理等,但每个线程都有自己的栈空间。

2、进程与线程的区别

进程是正在运行的程序的实例,而线程是是进程中的实际运作单位。(例如:进程是打开浏览器,线程是浏览器中的各种操作)

3、线程相关API(POSIX标准)

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
#include <pthread.h>
//1、线程的创建
/*
thread:线程标识符指针
attr:设置线程属性,例如优先级、栈大小和分离状态等
start_routine:程序运行的入口函数
arg:指向线程执行函数的参数
返回值:线程创建的结果(成功为0)
*/
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void *),void *arg);

//2、线程退出(只能在线程函数内部调用)
void pthread_exit(void *retval); //参数retval是一个指针,用于传递线程的退出状态

//3、线程分离(在线程终止时,系统会自动回收线程的资源)
int pthread_detach(pthread_t thread); //线程标识符

//4、线程属性初始化
int pthread_attr_init(pthread_attr_t *attr); //线程属性结构体指针

//5、线程的分离状态
int pthread_attr_setdetachstate(pthread_attr_t *attr,int *detachstate); //设置线程分离状态

//6、线程属性销毁
int pthread_attr_destroy(pthread_attr_t *attr);

//7、线程属性结构体
typedef struct
{
int detachstate; // 线程的分离状态
int schedpolicy; // 线程调度策略
structsched_param schedparam; // 线程的调度参数
int inheritsched; // 线程的继承性
int scope; // 线程的作用域
size_t guardsize; // 线程栈末尾的警戒缓冲区大小
int stackaddr_set; // 线程的栈设置
void* stackaddr; // 线程栈的位置
size_t stacksize; // 线程栈的大小
} pthread_attr_t;

4、测试样例

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
#include <iostream>
#include <pthread.h>
#include <unistd.h> // For sleep()

void* thread_func(void* arg)
{
int thread_id = *((int*)arg+1);
printf("Thread %d is running!!\n", thread_id);
return nullptr;
}

int main()
{
pthread_t thread1, thread2;
pthread_attr_t thread_attr;
//参数列表(可以用结构体进行参数的封装)
int thread_id1[2] = {1,3};
int thread_id2[2] = {2,4};

// 初始化线程属性
pthread_attr_init(&thread_attr);

// 设置线程为分离状态
pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);

// 创建线程1
pthread_create(&thread1, &thread_attr, thread_func, &thread_id1);

// 创建线程2
pthread_create(&thread2, &thread_attr, thread_func, &thread_id2);

// 等待一段时间,让线程有机会运行
sleep(2);

// 销毁线程属性结构
pthread_attr_destroy(&thread_attr);

printf("Main thread exiting!!\n");
return 0;
}
/*
Thread 3 is running!!
Thread 4 is running!!
Main thread exiting!!
*/

5、线程同步

5.1、线程同步:指的是多个线程同时访问同一公共(共享)资源

5.2、常见的锁机制

锁主要用于互斥,即同一时间只允许一个线程访问共享资源,常见的锁机制有三种

  • 互斥锁(Mutex):互斥锁:保证同一时刻只有一个线程可以执行临界区的代码
  • 读写锁(Reader/Writer Locks):允许多个读者同时读共享数据,但写者的访问是互斥的
  • 自旋锁(Spinlocks):在获取锁之前,线程在循环中忙等待,适用于锁持有时间非常短的场景

5.2、互斥锁

实现线程间的同步,确保线程之间对共享资源的访问按照预定的顺序进行

5.2.1、相关API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <pthread.h>
//1、互斥锁标识
pthread_mutex_t id;

//2、互斥锁初始化
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

//3、加锁(线程中使用)
int pthread_mutex_lock(pthread_mutex_t *mutex);

//4、解锁(线程中使用)
int pthread_mutex_unlock(pthread_mutex_t *mutex);

//5、销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

测试样例

1
2
3
4
static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;  //互斥锁静态初始化
//临界区操作(在线程函数内使用)
pthread_mutex_lock(&mutex); //加锁
pthread_mutex_unlock(&mutex); //解锁

5.3、读写锁

实现:只有一个线程写,允许多个线程读

5.3.1、相关API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <pthread.h>
//1、读写锁标识
pthread_rwlock_t rwlock;

//2、读写锁初始化(动态初始化)
int pthread_rwlock_init(pthread_rwlock_t *restrict_rwlock, const pthread_rwlockattr_t *restrict_attr); //读写锁标识+属性
//静态初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

//3、加锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock ); //读锁
int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock ); //写锁

//4、解锁
int pthread_rwlock_unlock(pthread_rwlock_t*rwlock );

//5、销毁
int pthread_rwlock_destroy(pthread_rwlock_t*rwlock. );

5.4、自旋锁

5.4.1、自旋锁主要用于内核模块或驱动程序中,避免上下文切换的开销。不能在用户空间使用

5.4.2、在Linux内核中,自旋锁是一种用于多处理器系统中的低级同步机制,主要用于保护非常短的代码段或数据结构,以避免多个处理器同时访问共享资源

5.5、条件变量

5.5.1、条件变量的使用必须与互斥锁配合,以保证对共享资源的访问是互斥的

5.5.2、条件变量提供了一种线程间的通信机制,允许一个线程等待另一个线程满足某个条件后再继续执行(先运行线程1,满足条件变量后,运行线程2)

5.5.3、条件变量是一种线程同步机制,条件变量通常与互斥锁一起使用,互斥锁上锁条件变量用于在多线程环境中等待特定事件发生当某个特定事件满足后线程会发出通知唤醒等待队列上的所有线程,唤醒的线程会判断自身的条件是否满足,如果满足,获取锁,执行函数体,然后唤醒下一个等待线程,使程序以同步的方式执行

5.6、信号量

信号量本质上是一个非负整数变量,可以被用来控制对共享资源的访问。

6、线程池

6.1、线程池的定义:线程池是一种用于管理和重用多个线程的设计模式。它通过维护一个线程池(线程的集合),可以有效地处理并发任务无需每次都创建和销毁线程。这种方法可以减少线程创建和销毁的开销,提高性能和资源利用率。

6.2、相关数据结构

1、GFunc

1
2
//此处的 data是在启动任务时,传递给每个任务的,而user_data是在创建线程池时传入的共享数据,对于每个任务都是一样的
typedef void (*GFunc)(gpointer data,gpointer user_data);

2、gpointer

1
typedef void *gpointer;

3、gint

1
typedef int gint;

4、gboolean

1
2
3
typedef gint gboolean
#define TRUE 1
#define FALSE 0

6.2、相关API(Glib库,第三方API)

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
#include <glib.h>
//1、线程池的创建
/*
func:池中线程执行的函数
user_data:传递给func的数据,可以为NULL,这里的user_data最终会被存储在GThreadPool结构体的user_data中
max_threads:线程池的容量,最大可运行的线程数
exclusive:独占标记位。决定当前的线程池独占所有的线程还是与其他线程共享这些线程,一般为TRUE
error:用于报告错误信息
返回值:线程池实例指针
*/
GThreadPool *g_thread_pool_new(GFunc func,gpointer user_data,gint max_threads,gboolean exclusive,GError**error );

//2、向线程池里面添加线程
/*
pool:线程池实例的指针
data:传递给每个任务的独享数据
*/
gboolean g_thread_pool_push(GThreadPool *pool,gpointer data,GError**error) ;

//3、释放线程池分配的所有资源
/*
pool:线程池实例的指针
immediate:是否立即释放线程池
wait_:当前函数是否阻塞等待所有任务完成
*/
void g_thread_pool_free (GThreadPool* pool,gboolean immediate,gboolean wait_);

6.3、测试样例

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
#include <glib.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h> // For sleep()

// 任务函数
/*
data:传递给每个任务的数据
user_data:传递给task的所有线程共享数据
*/
void task(gpointer data, gpointer user_data) {
int task_num = *static_cast<int*>(data);
printf("Executing task %d\n", task_num);
sleep(task_num); // 模拟任务执行时间(task_num越大,每个任务执行的时间越久)
printf("Task %d completed\n", task_num);
}

int main() {
// 初始化GLib
//g_thread_init(NULL);

// 创建一个包含4个线程的线程池
GError *error = NULL;
GThreadPool *pool = g_thread_pool_new(task, NULL, 4, FALSE, &error);
if (error)
{
printf("Error creating thread pool: %s\n", error->message);
g_error_free(error);
return 1;
}

// 提交任务到线程池
int task_data[8];
for (int i = 0; i < 8; ++i)
{
task_data[i] = i + 1;
g_thread_pool_push(pool, &task_data[i], &error);
if (error)
{
printf("Error pushing task to thread pool: %s\n", error->message);
g_error_free(error);
return 1;
}
}

// 等待所有任务完成,释放资源
g_thread_pool_free(pool, FALSE, TRUE);
printf("All tasks completed\n");

return 0;
}

编译和运行

1
2
3
4
5
6
7
8
1、安装glib库(第三方库)
sudo apt-get install libglib2.0-dev

2、编译
g++ -o thread_pool_test thread_pool_test.cpp `pkg-config --cflags --libs glib-2.0`

3、运行
./thread_pool_test
示例图片

五、内核原理

1、操作系统内核

  • 内核是操作系统最基本的一部分,它本质上指的是一个提供硬件抽象层、磁盘及文件系统控制、多任务等功能的系统软件
  • 内核为应用程序提供了一系列命令、API和图形化界面操作硬件
示例图片

2、CPU

  • 中央处理器(CPU)是一种硬件组件,它是服务器的核心计算单元
  • 负责处理操作系统和应用程序运行所需的所有计算任务
  • 每个进程执行的时候都会占用CPU
  • 哪个任务在运行,CPU就被哪个任务所占用

3、虚拟和物理内存

3.1、虚拟内存:虚拟内存是计算机系统内存管理的一种技术,它为每个进程提供了一种“虚拟”的地址空间,这个地址空间对于每个程序来说看起来都是连续的,但实际上可能被分散地存储在物理内存和磁盘上(如交换空间或页面文件)

3.2、物理内存:物理内存指的是计算机中安装的实际RAM(随机访问存储器)模块(内存条)。它是系统用来存储正在运行的程序和数据的硬件资源。

3.3、MMU(内存管理单元):是CPU的一个组成部分,负责处理虚拟地址到物理地址的转换(一个映射桥梁)

六、网络编程

1、网络基础知识

1.1、局域网(LAN):在小范围内(内网)实现高速数据传输和资源共享。示例:以太网(Ethernet)、WiFi

1.2、互联网(Internet):提供全球范围的通信和信息共享,也称外网

1.3、广播网络(Broadcast Network):一个节点发送的数据包可以被网络中所有节点接收到。示例:以太网,Wi-Fi(用的比较多的就是互联网+以太网+WiFi

1.4、计算机网络分层模型:将复杂的网络通信过程划分为若干层,每一层都执行特定的功能,并为上一层提供服务

1.5、OSI七层模型是学术和法律上的国际标准,而TCP/IP四层模型是现实生活中被广泛遵循的分层模型(注重协议栈)

1.6、 协议:协议是一套规则和标准,用于控制同一层次内的实体如何相互通信。协议定义了通信的格式、时序、错误处理等。例如,TCP(传输控制协议)定义了如何在网络中的两个点之间可靠地传输数据(点对点传输)。

2、OSI七层模型

2.1、分层框架

示例图片 示例图片
  • 物理层:负责传输原始比特流。它涉及的是物理设各及介质,如电缆类型、电信号传输和接收等
  • 数据链路层:确保物理链路上的无误传输。它提供了如帧同步、流量控制和错误检测等功能
  • 网络层:负责数据包从源到目的地的传输和路由选择(决定数据在网络中的游走路径)。它定义了地址和路由的概念,如IP协议(利用IP寻找对方的主机,例如IPv4IPv6
  1. IPv4 使用 32 位地址表示 IP 地址,它由 4 个十进制数(0-255)组成,每个数之间使用句点分隔。例如,192.168.0.1 是一个 IPv4 地址(常用)
  2. IPv6 使用 128 位地址表示 IP 地址,它由 8 组四位十六进制数(0-9、a-f)组成,每个组之间使用冒号分隔。例如:2001:0db8:85a3:0000:0000:8a2e:0370:7334 是一个 IPv6 地址
  • 传输层:提供端到端的数据传输服务,保证数据的完整性。它定义了如TCP和UDP协议(利用端口号寻找对应的应用程序)

    例如:要将我的电脑上微信的消息发送到对方电脑的微信上,根据网络层提供的IP地址找到对方的主机,然后根据传输层提供的端口号找对对应的微信,实现数据的准确传输

  • 会话层:管理会话,控制建立、维护和终止应用程序之间的会话(类似于提供了客户端和服务端建立联系的过程)

  • 表示层:处理数据的表示、编码和解码,如加密和解密

  • 应用层:提供网络服务给终端用户的应用程序,如HTTP(网站)、FTP、SMTP(邮件)等协议

3、TCP/IP四层模型

3.1、TCP/IP四层模型,亦称互联网协议套件(nternet Protocol Suite),是一种按照功能标准组织互联网及类似计算机网络中使用的一系列通信协议的框架。该套件中的基础协议包括传输控制协议(TCP)、用户数据报协议(UDP)和互联网协议(IP)。

​ 应用层+传输层+网络层+数据链路层

示例图片

4、网络传输中的数据单元

4.1、PUD

  • 协议数据单元(Protocol Data Unit,PDU),计算机网络各层对等实体间交换信息的数据单位。PDU包括头部(PCI)和负载(SDU)—-> PDU=PCI+SDU
  • 不同层次的PDU有专门的术语,例如在网络层,PDU称为数据包,在传输层,PDU称为报文段或数据报,在数据链路层,PDU称为(frame)
    1. 数据包:通常指网络层(如IP网络层)的数据单位
    2. 报文段:通常用于描述传输层(TCP传输层)的数据单位
    3. 数据报:通常用于描述UDP协议的数据单位,它也是传输层的一个概念

4.2、数据传输的组包和解包流程

示例图片

5、TCP协议

5.1、TCP(传输控制协议,Transmission Control Protocol)是一种面向连接的可靠的基于字节流的传输层通信协议,广泛应用于互联网中。它旨在提供可靠的端到端通信,在发送数据之前,需要在两个通信端点之间建立连接。TCP通过一系列机制确保数据的可靠传输,这些机制包括序列号、确认应答、重传控制、流量控制拥塞控制

5.2、TCP协议的特点

  • 面向连接:在进行数据交换前,两个通信端先必须建立连接,该连接通过三次握手(SYN、SYN-ACK、ACK)来建立,确保双方都准备好数据交换
  • 可靠传输:TCP通过序列号和确认应答机制确保数据的可靠传输。发送方为每个报文段分配一个序列号,接收方通过发送确认应答(ACK)来确认已经收到特定序列号的报文段。如果发送方没有在合理的超时时间内(2MSL)收到确认应答,它将重传该报文段
  • 流量控制:TCP使用窗口大小调整机制(滑动窗口)来进行流量控制,防止发送方过快地发送数据,导致接收方来不及处理。通过调整窗口大小,TCP能够动态地管理数据的传输速率,避免网络拥塞和数据丢失
  • 阻塞控制:TCP实现了拥塞控制算法(如慢启动、拥塞避免、快重传和快恢复),以避免网络中的过度拥塞。这些算法可以根据网络条件动态调整数据的发送速率,从而提高整个网络的效率和公平性。
  • 数据排序:由于网络延迟和路由变化,TCP报文段可能会乱序到达接收方。TCP能够根据序列号重新排序乱序到达的报文段,确保数据以正确的顺序交付给应用层。T
  • 端到端通信:TCP提供端到端的通信。每个TCP连接由四个关键元素唯一确定:源IP地址(确定了哪一台电脑)、源端口号(确定了那一个应用程序)、目标P地址目标端口号。这种方式确保了数据能够在复杂的网络环境中准确地从一个端点传输到另一个端点

5.3、TCP三次握手(建立连接)+TCP四次挥手(断开连接)

5.4、TCP数据传输

5.4.1、可靠传输保障

  • 累计确认:TCP的累积确认(Cumulative Acknowledgment)是指接收方发送的ACK报文中的确认号表示的是接收方期望接收的下一个字节的序列号。这意味着所有比这个确认号小的字节都已经被成功接收(通过序列号判断是否丢包)
  • 延时确认:接收方在接收到每个报文段后,不会立即发送确认报文(ACK),而是会等待一段时间,看是否有其他报文段到达或者接收方是否有数据要发送。在这种情况下,接收方可以将多个确认合并为一个ACK报文。这种机制被称为延迟确认,目的是减少ACK报文的数量,从而降低网络开销
  • 超时重传:当发送方发送数据后,如果在预定时间内未收到接收方的确认(ACK),发送方会假设该数据段丢失,并重新发送该数据段

5.4.2、滑动窗口

  • 在TCP通信中,双方各自维护一个缓冲区。当发送速率大于接收速率时,接收方的缓冲区可能会被填满。如果发送方继续发送数据,后面的数据只能被丢弃,造成资源浪费。为了避免这种情况,TCP提供了滑动窗口机制来控制发送方的发送速率
  • TCP报文段的头部有一个字段是窗口大小(Window Size),这里的窗口是指接收窗口(Receive Window,rwnd)。接收方可以在返回的ACK报文段中通过窗口大小字段告诉发送方当前可用的缓冲区即接收窗口大小。接收窗口大小随数据传输动态变化,当窗口大小变为0时,发送方会暂停数据发送。一段时间后,接收方腾出了足够的空间来接收数据,它会发送报文段通知发送方继续发送
  • 滑动窗口机制通过动态调整窗口大小确保发送的数据不会超出接收方的处理能力从而避免数据丢失和资源浪费,同时保证了TCP连接的可靠性,实现了流量控制

6、socket套接字

6.1、套接字(socket):是计算机网络数据通信的基本概念和编程接口,允许不同主机上的进程通过网络进行数据交换。Socket 是在应用层和传输层之间的一个抽象层,它把 TCP/IP 层复杂的操作抽象为几个简单的接口,供应用层调用实现进程在网络中的通信

6.2、套接字的组成

一个套接字主要由以下三个属性组成(通过配置以下内容,设计下三层的结构)

  • 网络地址:通常是IP地址(常用IPv4,如192.168.116.19,点分十进制),用于标识网络上的设备(主机)。
  • 端口号:用于标识设备上的特定应用或进程。端口号是一个16位的数字,范围从0到65535
  • 协议:如TCP(传输控制协议)或UDP(用户数据报协议),定义了数据传输的规则和格式。

6.3、套接字的类型

根据数据传输方式的不同,主要有两种类型的套接字:

  • 流套接字(Stream Sockets):基于TCP协议,提供面向连接、可靠的数据传输服务。数据像流水一样连续传输,接收方按发送顺序接收数据,适用于需要准确无误传输数据的应用,如网页服务器
  • 数据报套接字(Datagram Sockets):基于UDP协议,提供无连接的数据传输服务。每个报文段独立传输,可能会丢失或无法保证顺序,适用于对传输速度要求高但可以容忍一定丢包率的应用,如在线视频会议。

6.4、套接字常用API

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
#include <sys /socket.h>
#include <sys/types.h>

//1、创建socket
/*
domain:指定要创建套接字的通信域。(例如:AF_UNIX:本地通信,通常用于UNIX系统间的进程间通信。AF_INET:IPv4互联网协议(常用)和AF_INET6:IPv6互联网协议)
type:指定要创建的socket类型(SOCK_STREAM:基于TCP和SOCK_DGRAM:基于UDP)
protocol:指定要与socket一起使用的特定协议(补充协议),一般默认为0
返回值:socket文件描述符(socket句柄,起连接作用)
*/
int socket(int domain, int type, int protocol );

//2、绑定地址+端口号
struct socketaddr_in addr
{
sa_family_t sin_family; /*地址族:AF_INET */
in_port_t sin_port; /*端口号,网络字节顺序*/
struct in_addr sin_addr; /*互联网地址*/
}; //用该结构体指代所有的协议
/*
sockfd:socket句柄
addr:IP地址和端口号相关配置结构体
addrlen:结构体长度
返回值:成功为0,失败为-1
*/
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

//3、监听(进入监听状态,准备接收客户端的连接请求)
/*
sockfd:套接字文件描述符
backlog:指定还未accpet但是已经完成链接的队列长度
@return int:成功为0,失败-1
*/
int listen(int sockfd, int backlog);

//4、接受(接受客户端发来的连接请求,并返回通信文件描述符)
/*
sockfd:连接socket套接字
addr:
addrlen:
返回值:socket文件描述符(socket句柄,起通信作用)
*/
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);

//5、建立连接(由客户端调用,来与服务端建立连接)
/*
@param sockfd:客户端套接字的文件描述符
@param addr:指向sockaddr 结构体的指针,包含目的地地址信息
@param addrlen:指定addr指向的结构体的大小
@return int:成功为0,失败为-1,并设置errno_以指示错误原因
*/
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

//6、发送和接受消息(读写)
/*
@param sockfd套接字文件描述符
@param buf接收缓冲区,同样地,此处也并非内核维护的缓冲区
@param len缓冲区长度,即 buf可以接收的最大字节数
@param flags:一般默认为0
返回值:发送或者接受成功的字节
*/
ssize_t send(int sockfd, const void *buf, size_t len,int flags);
ssize_t recv(int sockfd, const void *buf, size_t len,int flags);

//7、关闭
/*
_fd:这是一个整数值,表示要关闭的文件描述符
返回值:成功关闭文件描述符时,close()函数返回0
发送失败,例如试图关闭一个已经关闭的文件描述符或系统资源不足,close()会返回-1
*/
int close (int _fd);

6.5、网络字节序和主机字节序的转化

6.5.1、网络字节序:也称大端字节序低字节存储在内存的高地址处,高字节存储在内存的低地址

6.5.2、主机字节序:一般intel x86-64架构以及其他一些现代处理器普遍采用的字节序,也称小端字节序低位字节存储存储在内存的低地址高位字节序存储在内存的高地址

6.5.3、相关转换函数

在网络通信中,为了让不同字节序的主机能够相互理解对方的数据,常常需要进行字节序转换

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
//转化方式一(以字节为单位进行转换)
#include <arpa/inet.h>

//将无符号长整数hostlong从主机字节序(host)转化成网络字节序(net)
uint32_t htonl(uint32_t hostlong);

//将无符号短整数hostshort从主机字节序(h)转化成网络字节序(n)
uint32_t htons(uint16_t hostshort);

//将无符号长整数netlong从网络字节序(n)转化成主机字节序(h)
uint32_t ntohl(uint32_t netlong);

//将无符号短整数netshort从网络字节序(n)转化成主机字节序(h)
uint32_t ntohs(uint16_t hostshort);

//转化方式二(优先使用这个)
#include <netinet/in.h>
#include <arpa/inet.h>

//1、将来自IPv4点分十进制表示法的 Internet 主机地址cp 转换为二进制形式(以网络字节顺序)并将其存储在inp指向的结构体中
/*
cp:IPv4点分十进制表示法的 Internet 主机地址cp(只能用IP协议)
inp:存储转换后的字节序
*@return int成功返回0,失败返回-1
*/
int inet_aton(const char *cp, struct in_addr *inp);

//2、函数将字符串cp(以IPv4点分十进制表示法表示)转换为适合用作Internet网络地址的主机字节顺序中的数字
/*
cp:用IPv4点分十进制表示的字符串
返回值: in_addr_t成功时,返回转换后的地址。如果输入无效,则返回 -1。
*/
in_addr_t inet_network( const char *cp);

//3、字符串格式转换成sockaddr_in格式(任意协议)
/*
@param int af:通常为 AF_INET用于IPv4地址,或AF_INET6用于IPv6地址
@param char *src:包含IP地址字符串的字符数组,如果是IPv4地址,格式为点分十进制(如“192.168.1.1")﹔如果是IPv6地址,格式为冒号分隔的十六进制表示(如"2001:0db8:85a3:00e0:0e08:8a2e:0370:7334")
@param void *dst:指向一个足够大的缓冲区(对于IPv4是一个struct in_addr结构体,对于IPv6是一个struct in6_addr结构体),用于存储转换后的二进制IP地址
@return int:成功转换返回0;输入地址错误返回-1;发生错误返回-1
*/
int inet_pton(int af, const char *src, void *dst);

//4、将in_addr结构体的内容转换成字符串
/*
in:in_addr结构体的内容
@return: char*缓冲区指针
*/
char *inet_ntoa(struct in_addr in);

6.5.4、测试样例

1、主机字节序与网络字节序的转化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
unsigned short host_ip = 0x1F,net_ip=0;
printf("主机字节序为=0x%x\n",host_ip); //主机字节序为=0x1F

//转换的时候是以字节为单位的
net_ip = htons(host_ip);
printf("转换的网络字节序为=0x%X\n",net_ip); //转换的网络字节序为=0x1F00

host_ip = ntohs(net_ip);
printf("转换的主机字节序为=0x%X\n",host_ip); //转换的主机字节序为=0x1F

return 0;
}

2、字符串IP地址的转化

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
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
char* ip_str="192.168.116.19";
printf("ip地址的16进制表示分别为:0x%X 0x%X 0x%X 0x%X\n",192,168,116,19); //ip地址的16进制表示分别为:0xC0 0xA8 0x74 0x13

struct in_addr server_in_addr;

memset(&server_in_addr,0,sizeof(server_in_addr));

//1、字符串转化成网络字节序
inet_aton(ip_str,&server_in_addr);
printf("inet_aton:ip地址的网络字节序为:0x%X\n",server_in_addr.s_addr); //inet_aton:ip地址的网络字节序为:0x1374A8C0

//2、将字符串转化成网络字节序
inet_pton(AF_INET,ip_str,&server_in_addr.s_addr);
printf("inet_pton:ip地址的网络字节序为:0x%X\n",server_in_addr.s_addr); //inet_pton:ip地址的网络字节序为:0x1374A8C0

//3、将结构体转换成字符串
printf("ip地址转换为字符串:%s\n",inet_ntoa(server_in_addr)); //ip地址转换为字符串:192.168.116.19

return 0;
}

6.5.5、利用Wireshark抓包,查看TCP连接报文交互过程

6.5.6、IO多路复用技术

7、网络层—IP协议

7.1、IP协议:IP ( Internet Protocol,互联网协议)是网络通信中最基础的协议之一,用于在不同的网络设备之间传输数据。P协议定义了数据包的格式和寻址方法,使得数据能够从源地址传输到目标地址。

7.2、IP地址:IP地址(Internet Protocol Address,互联网协议地址)是分配给连接到计算机网络的每个设备的唯一标识符,用于在网络中进行通信。IP地址使数据包在网络上能够找到其目标位置,确保数据从源设备传输到目标设备。(IP地址是每台电脑的唯一标识,根据目标IP找到数据包发送的目标主机

7.3、IP协议的种类

  • IPv4:32位地址,例如192.168.116.19(点分十进制表示)
  • IPv6:128位,例如2001:0db8:85a3:0000:0000:8a2e:0370:7334(冒号十六进制表示)

7.4、IP地址分类

  • 私有IP地址
  • 公有IP地址(根据作用范围)
  • 静态IP地址(适用于需要长期保持相同IP地址的设备)
  • 动态IP地址(根据作用范围)

7.5、网络层的硬件设备

  • 路由器:路由器是一种网络设备,用于在计算机网络之间转发数据包。它主要用于连接不同的网络,并确定数据包的最佳路径
  • 路由器通过分析数据包的目标地址,选择最有效的路由路径,将数据包传输到目的地
  • 路由器主要工作网络分层模型的网络层

8、应用层—DNS协议

8.1、域名(Domain Name:是互联网上用于标识网站的易于记忆的名称,代替了难记的IP地址,使用户能够方便地访问网站。域名由一系列标签组成,这些标签由点(.)分隔,每个标签代表域名层次结构中的一个级别。如www.baidu.com

8.2、DNS (Domain Name System,域名系统)协议:是一种用于将人类易读的域名(如www.baidu.com)转换为**计算机可以识别的IP地址**(如 192.0.2.1)的网络协议。它是互联网的关键组件之一,使用户能够使用友好的域名而不是难记的P地址来访问网站和其他互联网资源。

8.3、流程

  • 用于在浏览器输入:www.baidu.com
  • 通过DNS协议,将该域名转换成IP地址
  • 根据该IP地址,查找对应的主机

9、链路层—交换机

9.1、MAC地址

  • MAC地址(Media Access Control Address,介质访问控制地址)是网络设备的硬件地址,用于在局域网(LAN)中唯一标识设备。MAC地址嵌入在设备的网络接口控制器(NIC)中,每个设备在全球范围内都应该具有一个唯一的MAC地址
  • MAC地址工作在数据链路层,用于设备间通信。

9.2、MAC地址格式

MAC地址通常是48位(二进制)的数字,通常表示为12位的十六进制数。例如,00:1A:2B:3C:4D:5E。常见的表示形式有两种:

  • 用冒号分隔的六个十六进制数对(如 00:1A:2B:3C:4D:5E)
  • 用连字符分隔的六个十六进制数对(如00-1A-2B-3C-4D-5E)

9.3、ARP协议(链路层经典协议)

9.3.1、作用Address Resolution Protocol,地址解析协议,用于将IP地址转换为物理MAC地址。这个转换过程在局域网内是必须的,因为以太网帧是通过MAC地址传输的,而网络层数据包使用IP地址

9.3.2、 工作流程

  • ARP请求:当一个设备(如计算机)需要发送数据到另一个设备,但只知道目标设备的IP地址时,它会在网络上广播一个ARP请求帧。这个请求帧包含发送方的IP地址和MAC地址,以及目标设备的IP地址
  • ARP响应:网络上所有设备都接收到ARP请求(ARP广播报文),但只有目标设备会响应。目标设备会发送一个包含其MAC地址的ARP响应帧直接回到发送方。
  • 地址缓存:发送方接收到ARP响应后,会将目标设备的IP地址和MAC地址映射关系缓存到本地的ARP缓存中,以便下次发送数据时无需再进行地址解析

9.3.3、数据链路层硬件设备

交换机

  • 交换机是一种网络设备,主要用于在局域网(LAN)内连接多台设备,并根据设备的MAC地址来转发数据帧。
  • 交换机的主要功能是提高网络性能和管理网络流量
  • 与集线器(Hub)相比,交换机更智能,因为它能够根据数据帧的目标地址进行精确的转发,从而减少网络冲突和提高传输效率
  • 交换机工作在数据链路层

10、物理层

10.1、物理层的职责

  • 数据传输速率控制:定义了数据传输速率(带宽),例如10Mbps、100Mbps、1Gbps 等
  • 传输模式:定义传输的模式,包括单工(单向传输)、半双工(双向但不能同时传输)和全双工(双向同时传输)

10.2、协议

  • IEEE802.3、IEEE802.4、IEEE802.5的物理层协议、RS-232RS-485、RS-449、FDDI等。

10.3、物理层硬件设备

  • 集线器:集线器是一种基本的网络设备,用于将多个以太网设备连接在一起。它在所有连接的端口之间广播收到的比特流,所有端口共享同一信道,数据的冲突和碰撞频繁发生,效率低下,逐渐被交换机取代
  • 调制解调器:将数字信号转换为模拟信号(调制),或将模拟信号转换回数字信号(解调)(AD或者DA转换)
  • 光纤收发器:用于在光纤通信系统中发送和接收光信号。它将电信号转换为光信号,并将光信号转换回电信号。通常用在长距离的数据传输中,用于扩展光纤网络的传输距离
  • 光猫:光猫是光纤接入终端设备,通常用于家庭和小型办公室,连接光纤服务提供商的网络与用户的内部网络

11、IO多路复用技术

11.1、IO多路复用:IO多路复用(IO Multiplexing)是Linux 中用于处理多个IO操作的机制,使得单个线程或进程可以同时监视多个文件描述符,以处理多路IO请求。它主要通过以下系统调用实现:selectpollepoll

11.2、实现机制

  • select:select是早期的IO多路复用机制,它允许程序监视多个文件描述符判断是否可以进行IO操作。程序通过提供三个文件描述符集(读、写、异常)和一个超时时间来调用select,在任何一个文件描述符变得可读、可写或出现错误时返回
  • poll:poll 与select类似,但它使用一个包含文件描述符和事件的结构数组来代替三个文件描述符集。它可以处理更多文件描述符,并且更容易管理
  • epoll:epoll 是Linux特有的、性能优化的IO多路复用机制。它比 select和 poll更高效,特别适用于大规模并发连接。epoll 提供了两种工作模式:水平触发(Level-Triggered,LT)和边沿触发(Edge-Triggered,ET)。ET模式下,epoll 只在状态变化时通知,因此更高效,但也更复杂

11.3、作用和意义

  • 节省资源:IO多路复用允许单个进程或线程同时监视多个文件描述符,而不是为每个IO操作创建一个线程或进程。只需要维护文件描述符,极大地提高了节约了资源,减少了系统开销。
  • 效率高:使用IO多路复用省去了进程或线程上下文切换的开销,提升了处理效率,减少了系统资源(如内存和CPU时间)的消耗,从而提高了应用程序的整体性能和响应速度
  • 简化编程模型:尽管IO多路复用增加了代码的复杂性,但它简化了高并发程序的设计,使得程序员可以更容易地管理多个IO操作,而不必处理大量的线程同步问题

11.4、性能对比

select和poll底层都是基于线性结构实现(数组或者链表)的,需要对文件描述符集执行多次遍历和拷贝,效率低下,而 epoll底层是基于红黑树(一种平衡二叉树)实现的,且通过维护就绪事件链表,效率更高

11.5、epoll触发方式

在epoll的使用中,有两种事件触发模式:边缘触发(Edge Triggered,ET)和水平触发(Level Triggered, LT)。这两种模式决定了epoll如何通知应用程序有事件发生。可以类比单片机的边缘触发和电平触发。

  • 水平触发是epoll的默认模式。在这种模式下,只要文件描述符上有未处理的事件,epoll就会不断通知应用程序
  • 边缘触发模式下,当文件描述符从未就绪状态变为就绪状态时,epoll会通知应用程序。如果应用程序没有在通知后及时处理事件(例如,读出所有可读的数据),epoll 不会再次通知,除非文件描述符再次从未就绪变为就绪状态。即只在状态变化时通知一次,因而叫边缘触发

11.6、相关数据结构和API

创建epoll池(epoll_createl),设置文件描述符感兴趣事件(有可读的数据fd,epoll_ctl),等待感兴趣时间的发生,返回产生感兴趣事件的文件描述符数量(eppll_wait),此外设置文件描述符不为阻塞态(fcntl)

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
//1、epoll实例相关数据的联合体(epoll_data)
/**
*@brief记录内核需要存储的epoll实例相关数据的联合体。fd可用来存储文件描述符。
* l*/
typedef union epoll_data
{
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

//2、监听文件描述符的集合(epoll_event)
/**
* @brief epoll:实例维护了一个文件描述符的集合,称为感兴趣列表,这个列表中的文件描述符会被监听,并在就绪时被加入就绪链表。感兴趣列表存储的是文件描述符和
struct epoll_event 结构体实例组成的entry。
*events:需要监听的就绪事件,是由以下О或更多事件类型或操作得到的:EPOLLIN、EPOLLOUT、EPOLLRDHUP、EPOLLPRI、EPOLLERR、EPOLLHUP、EPOLLET、EPOLLONESHOTPOLLWAKEUP、EPOLLEXCLUSIVE。下面的案例中我们只会用到EPOLLIN、EPOLLET
*EPOLLIN:epoll_event实例相关联的文件描述符可用于读操作
*EPOLLET:将关联的文件描述符设置为边缘触发模式
* data:指明内核应该存储并在文件描述符就绪时返回的数据*/
struct epoll_event
{
int32_t events; /*Epoll events */ //文件描述符发送了什么变化
epoll_data_t data; /* User data variable */ //哪个文件描述符有变化了
};

//3、创建epoll实体
#include <sys/epoll.h>
/**
*@brief如果传入的flags参数为0,则与epoll_create完成的工作相同。*
*@param flags:标记以获得不同的行为,默认为0即可。
*@return int:成功返回指向新的epoll实例的文件描述符,失败返回-1并设置errno指明错误原因
*/
int epoll_createl(int flags );

//4、将文件描述符添加进epoll池中
#include <sys/epol1.h>
/**
@brief epoll_ctl提供了对fd和 event组成的entry执行增、删、改操作的方式。
*
*@param epfd: epoll实例的文件描述符
*@param op:对文件描述符执行的操作(一般是EPOLL CTL_ADD)
EPOLL_CTL_ADD:将fd和event组成的entry添加到感兴趣列表
EPOLL_CTL_MOD:将感兴趣列表中与关联的event替换为此处传入的event
EPOLL_CTL_DEL:从感兴趣列表删除与fd关联的entry,此时event被忽略*@param fd待处理的文件描述符
*@param event:fd关联的描述信息。
*@return int:成功返回0,失败返回-1,并设置errno以指明错误原因
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

//5、 获取就绪的文件描述符的数量
/*
*@brief 等待epfd指向的epoll实例感兴趣列表中的事件发生
*@param epfd:epoll实例文件描述符
*@param events:提供给内核,用于返回已就绪的文件描述符信
*@param maxevents:可以返回的最大文件描述符数量
*@param timeout:超时时间,指明epoll_wait()在事件触发前阻塞等待的最大毫秒数。-1表示一直等待至有事件发生,0表示无论是否有事件发生立即返回
*@return int:成功返回就绪的文件描述符数量,超时时间耗尽仍没有就绪事件返回0。失败则返回-1并设置errno指明错误原因
*/
int eppll_wait(int epfd, struct epoll_event *events, int maxevents,int timeout);

//6、设置fd为不阻塞状态
#include <unistd.h>
#include <fcntl.h>
/*
* @brief 对fd指向的文件描述符执行cmd指明的操作。
*@param fd:文件描述符
*@param cmd:执行的操作,很多,此处不一一列出,常用的有F_GETFL和F_SETFL*F_GETFL:返回文件的权限模式和状态标记,不需要额外的参数
*F_SETFL:将文件的状态标记设置为第三个参数指定的值
*@param ... cmd需要的参数,可以没有
*@return int如果是F_GETFL,成功则返回文件状态标记值,如果是F_SETFL成切返回0。失败均返回-1,同时设置errno指明错误原因
*/
int fcntl(int fd, int cmd, ... );

11.7、测试样例

11.8、

12、子网掩码和IP地址

1、IP地址是互联网上每个子网或每个主机在网络上的唯一身份标签

2、子网掩码决定能够通信主机的数量(又称决定多少主机处于同一网段,锁定网段

​ 假设:IP地址为:192.168.1.1,子网掩码为:255.255.255.0

​ 2.1、子网掩码的255就锁定了IP地址对应的三位:192、168和1,那么这三个值就不能变动

​ 2.2、子网掩码的0则表明IP地址的最后一位不上锁,可以变动—->192.168.1.1一192.168.1.254范围内的IP都属于同一网段,给局域网内部的主机分配以上IP地址,它们就可以通讯(网络地址192.168.1.0和广播地址192.168.1.255不能分配给主机使用

3、假设,需要更多主机通讯,则需要修改子网掩码,来释放更多可用的IP地址

​ 3.1、假设子网掩码变成255.255.0.0,那么理论上可以通讯的IP地址为255✖255=65536个

​ 3.2、但是其中一个是网络地址 (192.168.0.0),另一个是广播地址 (192.168.255.255),这两个地址不能分配给主机使用

​ 3.3、实际可用于主机的地址数量为65536-2=65534

​ 3.4、通讯IP地址的范围为:192.168.0.1至192.168.255.254(只锁定了192和168两位)

4、192.168.1.1/24啥意思

192.168.1.1转化成二进制,前24处于锁定,后面位的值都是处于同一网段内

​ 4.1、主机IP地址为:192.168.1.1—>(二进制转化):11000000(192).10101000(168).00000001(1).00000001(1)

​ 4.2、子网掩码为:255.255.255.0—>(二进制转化):11111111(255).111111111(255).11111111(255).00000000(0),连续24个1,也称24位子网掩码

​ 4.3、将24位子网掩码和IP地址组合—>192.168.1.1/24(表示前24位锁定,后八位可以变动,可以通讯的主机数量)

​ 4.4、因此,可用的IP地址为192.168.1.1192.168.1.254

示例图片

5、根据主机通讯的数量计算子网掩码

假设,我们需要700台主机可以通讯,主机IP可以确定的部分为:192.168.0.0

​ 5.1、700转化成二进制为,1010111100(10位,1024),隔离出最后八位,为10.10111100–>前22位处于锁定状态

​ 5.2、11111111(255).11111111(255).11111100 .00000000,前22处于锁定状态,后10可以变化

​ 5.3、得出子网掩码为:255.255.252.0

​ 5.4、推出IP地址的范围:192.168.0.0(11000000.10101000.00000000.00000000)至192.168.3.255(11000000.10101000.00000011.11111111),可用的IP地址范围为:192.168.0.1192.168.3.254

​ 5.5、IP地址可以和子网掩码组合为:192.168.0.0/22

示例图片

6、已知子网掩码确定通讯IP的范围

假设子网掩码为:255.255.254.0,主机IP可以确定地部分为:192.168.0.0

​ 6.1、可以确定前23位处于锁定状态,后9位是可以变化的

​ 6.2、推出IP地址得范围为:192.168.0.0(11000000.10101000.00000000.00000000)至192.168.1.255(11000000.10101000.00000001.11111111),可用的IP地址范围为:192.168.0.1192.168.1.254

​ 6.3、子网掩码和IP地址组合为,192.168.0.0/23

示例图片

13、路由

  • 路由的作用指路由器从一个接口上收到数据包,根据数据包的目的地址进行定向并转发到另一个接口的过程数据包转发,实现不同网段主机之间的通讯)
  • 路由表:指定了数据包转发的路径和规则
  • 路由工作在OSI参考模型第三层——网络层的数据包转发设备

1、在Linux系统中设置路由表以实现跨网段主机的通讯

​ 1.1、检查当前路由表

1
ip route show

​ 1.2、添加静态路由

假设你有以下网络环境:

​ 主机A:IP地址为 192.168.1.100/24,默认网关为 192.168.1.1
​ 主机B:IP地址为 192.168.2.100/24,默认网关为 192.168.2.1
​ 路由器:连接两个网段,并且有一个接口在每个网段上(例如 192.168.1.254 和 192.168.2.254)

为了使主机A能够与主机B通信,你需要在主机A上添加一条指向主机B所在网段的静态路由,指定路由器作为下一跳。

在主机A上执行以下命令

1
sudo ip route add 192.168.2.0/24 via 192.168.1.254

这条命令的意思是:对于所有目标为 192.168.2.0/24 网络的数据包,通过 192.168.1.254 这个下一跳进行转发。

在主机B上执行以下命令

1
sudo ip route add 192.168.1.0/24 via 192.168.2.254

这条命令的意思是:对于所有目标为 192.168.1.0/24 网络的数据包,通过 192.168.2.254 这个下一跳进行转发。

1.3、验证路由是否生效

1
ip route show

1.4、测试连通性

从主机A尝试ping主机B的IP地址,验证两者之间的连通性:

1
ping 192.168.2.100

从主机B尝试ping主机A的IP地址,验证两者之间的连通性:

1
ping 192.168.1.100
示例图片

14、网关

  • 网关的作用:网关(Gateway)就是一个网络连接到另一个网络的“关口”,实现不同网络主机(应用)之间的通讯
  • 网关只是一种概念,表示一个网络区域对外的出入口。路由器是实现网关的一种设备
  • 网关它可以是路由器交换机或者是PC。只有当主机个非本网段设备进行通信的时候,才需要将数据包全部发给网关设备,再经由网关设备进行转发或者是有路由处理等
  • 网关机上集成了底层物理设备通讯的所有物理接口
  • 网关的工作是在应用层当中,路由则是网络层的。

15、交换机

交换机可以被看作是一种用于扩充网口的设备。它不仅增加了网络中的物理连接端口数量,还智能地管理这些连接,确保数据能够高效、准确地在各个设备之间传输。

16、ARP协议

  • ARP协议:“Address Resolution Protocol”(地址解析协议)的缩写
  • ARP的作用:将IP地址解析为物理地址(如以太网中的MAC地址)的网络层协议(IP地址到MAC地址的映射),工作在网络层(OSI模型的第3层,主要用于局域网(LAN)中,确保数据包能够正确地发送到目标设备
  • 工作原理:IP地址到MAC地址的映射:在局域网内,当一个主机想要向另一个主机发送数据时,它需要知道对方的MAC地址。如果只知道目标主机的IP地址,源主机可以通过ARP请求来获取该IP地址对应的MAC地址
  • 局域网内,MAC地址是硬件级别的唯一标识符,它用于在同一局域网内的设备之间直接通信
  • MAC地址是同一局域网内的设备之间直接通信的唯一标识IP地址用于在不同网段设备间的通讯

17、

18、

19、