Linux
进程间通信机制IPC
管道
有名管道
可以在任意俩个进程间通信
创建管道文件 mkfifo fifo ,但是它的大小永远为0.因为它只起到传输作用,写入的东西都写到内存里了
a.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<fcntl.h>
int main()
{
int fdw = open("fifo",O_WRONLY);
printf("fdw=%d\n",fdw);
while(1)
{
char buff[128] = {0};
fgets(buff,128,stdin);
if(strncmp(buff,"end",3) == 0)
{
break;
}
write(fdw,buff,strlen(buff+1));
}
close(fdw);
exit(0);
}
b.c
int main()
{
int fdr = open("fifo",O_RDONLY);
printf("fdr=%d\n",fdr);
while(1)
{
char buff[128] = {0};
int n = read(fdr,buff,127);
if(n == 0)
{
break;
}
printf("buff=%s n=%d\n",buff,n);
}
close(fdr);
exit(0);
}
gcc -o a a.c gcc -o b b.c 编译之后运行时阻塞了,因为管道需要在俩个进程中运行,一个读一个写。
读端关闭,再次进行写write会触发信号:SIGPIPE(这个信号的默认处理是终止程序)
若某一端关闭连接,而另一端仍然向它写数据,第一次写数据后会收到RST(复位)响应,此后再写数据,内核将向进程发出SIGPIPE信号,通知进程此连接已经断开。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WSdE5rP1-1648999921840)(E:\my_user\Picture\Typora\Linux\QQ图片20220105164609.png)]
此时俩个指针,一个read指针,一个write指针,同时指向a的位置,当有数据写入时,write往后移动的同时,数据写入,当write写满后,再循环往前,当write等于read,说明管道已经写满,此时处于阻塞状态,只能等read读完数据,才能再次写入,相反如果read指针等于write指针,说明管道为空,没数据可读;
无名管道
只能在父子进程间通信,因为其他进程不能得到fd数组的值,只能通过fork复制,它属于半双工,某一时刻只能处于一种状态。
写入管道的数据存在内存中;
8 int main()
9 {
10 int fd[2];
11 pipe(fd);
12
13 pid_t pid = fork();
14 assert(pid != -1);
15
16 if(pid == 0)
17 {
18 close(fd[1]);
19 while(1)
20 {
21 char buff[128] = {0};
22 int n = read(fd[0],buff,128);
23 if(n==0) break;
24 printf("child read=%s\n",buff);
25 }
26 close(fd[0]);
27 }
28 else
29 {
30 close(fd[0]);
31 while(1)
32 {
33 char buff[128] = {0};
34 fgets(buff,128,stdin);
35 if(strncmp(buff,"end",3) == 0) break;
36 write(fd[1],buff,strlen(buff));
37 }
38 close(fd[1]);
39 }
40 }
信号量机制
信号量: 特殊变量 原子减一§,原子加一(v) (同步进程)
0 1 二值信号量 获取资源 释放资源
0 1 3 计数信号量
临界资源:同一时刻只允许一个进程访问的资源
临界区:访问临界资源的代码段
实现API可以在Linux第四版14.1.3自己看
补充:当只有IPC_CREAT选项打开时,不管是否已存在该信号量,则都返回该信号量的ID,若不存在则创建新的 当只有IPC_EXCL选项打开时,不管有没有该信号量,semget()都返回-1; 所以当IPC_CREAT | IPC_EXCL时, 如果没有该块信号量,则创建,并返回信号量ID,若已有该块信号量,则返回-1,确保新的
sem.h
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<sys/sem.h>
union semun
{
int val;
};
void sem_init();
void sem_p();
void sem_v();
void sem_destroy();
sem.c
static int semid = -1;
void sem_init()
{
semid = semget((key_t)1234,1,IPC_CREAT|IPC_EXCL|0600);
if(semid == -1)
{
if(semget(key_t)1234,1,0600) == -1)
{
printf("semget err\n");
return;
}
}
else
{
union struct a;
a.val = 1;
if(semctl(semid,0,SETVAL,a) == -1)
{
printf("sem_init err\n");
return ;
}
}
}
void sem_p()
{
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op = -1;
buf.sem_flg = SEM_UNDO;
if(semop(semid,&buf,1) == -1)
{
printf("p err\n");
return ;
}
}
void sem_v()
{
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op = 1;
buf.sem_flg = SEM_UNDO;
if(semop(semid,&buf,1) == -1)
{
printf("v err\n");
return ;
}
}
void sem_destroy()
{
if(semctl(semid,0,IPC_RMID) == -1)
{
printf("sem_destroy err\n");
return ;
}
}
a.c
int main()
{
sem_init();
for(int i = 0;i < 5;++i)
{
sem_p();
printf("A");
fflush(stdout);
int n = rand()%2;
sleep(n);
printf("A");
fflush(stdout);
sem_v();
n = rand()%2;
sleep(n);
}
sleep(10);
sem_destroy();
exit(0);
}
b.c
//和a.c一样,知识把输出A改为B就行,最后不加sem_destroy(),只要又一个销毁就行
互斥量和二元信号量很类似,资源仅同时允许一个线程访问,但和信号量不同的是,信号量在整个系统可以被任意线程获取并释放,也就是说,同一个信号量可以被系统中的一个线程获取之后由另一个线程释放。而互斥信号量则要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁,其他线程越俎代庖去释放互斥信号量是无效的。
共享内存
将一块物理内存映射到俩个不同进程空间中,相当于他俩的物理内存重叠,用了同一块,进行数据共享
main.c
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<string.h>
5 #include<sys/shm.h>
6 #include<assert.h>
7 #include"sem.h"
8 int main()
9 {
10 int shmid = shmget((key_t)1234,128,IPC_CREAT|0600);
11 assert(shmid != -1);
12
13 char *s = (char*)shmat(shmid,NULL,0);
14 assert(s != (char*)-1);
15
16 sem_init();
17 while(1)
18 {
19 printf("input:\n");
20 char buff[128] = {0};
21 fgets(buff,128,stdin);
22
23 sem_p(0);
24 strcpy(s,buff);
25 sem_v(1);
26 if( strncmp(buff,"end",3) == 0)
27 {
28 break;
29 }
30 }
31 }
sem.c
1 #include"sem.h"
2 #define SEM_NUM 2
3
4 static int semid = -1;
5 void sem_init()
6 {
7 semid = semget((key_t)1234,SEM_NUM,IPC_CREAT|IPC_EXCL|0600);
8 if(semid == -1)
9 {
10 semid = semget((key_t)1234,SEM_NUM,0600);
11 if (semid == -1)
12 {
13 printf("create sem failed\n");
14 return;
15 }
16 }
17 else
18 {
19 int arr[SEM_NUM] = {1,0};
20 union semun a;
21 for(int i = 0; i < SEM_NUM; ++i)
22 {
23 a.val = arr[i];
24 if(semctl(semid,i,SETVAL,a) == -1)
25 {
26 printf("semtcl setval failed\n");
27 }
28 }
29 }
30 }
31 void sem_p(int index)
32 {
33 struct sembuf buf;
34 buf.sem_num = index;
35 buf.sem_op = -1;
36 buf.sem_flg = SEM_UNDO;
37
38 if(semop(semid,&buf,1) == -1)
39 {
40 printf("semop p err\n");
41 }
42 }
43 void sem_v(int index)
44 {
45 struct sembuf buf;
46 buf.sem_num = index;
47 buf.sem_op = 1;
48 buf.sem_flg = SEM_UNDO;
49
50 if(semop(semid,&buf,1) == -1)
51 {
52 printf("semop v err\n");
53 }
54 }
55 void sem_destroy()
56 {
57 if(semctl(semid,0,IPC_RMID) == -1)
58 {
59 printf("sem del err\n");
60 }
61 }
sem.h
1 #include<unistd.h>
2 #include<stdio.h>
3 #include<stdlib.h>
4 #include<sys/sem.h>
5
6 union semun
7 {
8 int val;
9 };
10
11 void sem_init();
12 void sem_p();
13 void sem_v();
14 void sem_destroy();
消息队列
消息其实就是结构体,第一个成员必须是long type;
写:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<string.h>
5 #include<assert.h>
6 #include<sys/msg.h>
7
8 struct mess
9 {
10 long type;
11 char buff[32];
12 };
13 int main()
14 {
15 int msgid = msgget((key_t)1234,IPC_CREAT|0600);
16 assert( msgid != -1);
17 struct mess dt;
18 dt.type = 1;
19 strcpy(dt.buff,"hello1");
20
21 msgsnd(msgid,(void*)&dt,32,0);
22 }
读:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<string.h>
5 #include<assert.h>
6 #include<sys/msg.h>
7
8 struct mess
9 {
10 long type;
11 char buff[32];
12 };
13 int main()
14 {
15 int msgid = msgget((key_t)1234,IPC_CREAT|0600);
16 assert( msgid != -1);
17 struct mess dt;
18
19 msgrcv(msgid,(void*)&dt,32,1,0);
20 printf("read:%s\n",dt.buff);
21 }
进程
进程是一个正在运行的程序
进程同步
信号量
有名和无名
信号量在进程是以有名信号量进行通信的,在线程是以无名信号进行通信的,因为线程linux还没有实现进程间的通信,所以在int sem_init(sem_t *sem, int pshared, unsigned int value)的第二个参数要为0,而且在多线程间的同步是可以通过有名信号量也可通过无名信号,但是一般情况线程的同步是无名信号量,无名信号量使用简单,而且sem_t存储在进程空间中,有名信号量必须LINUX内核管理,由内核结构struct ipc_ids 存储,是随内核持续的,系统关闭,信号量则删除,当然也可以显示删除,通过系统调用删除, 消息队列,信号量,内存共享,这几个都是一样的原理。,只不过信号量分为有名与无名 无名使用 <semaphore.h>, 有名信号量<sys/sem.h> 无名信号量不能用进程间通信, //无名与有名的区别,有名需要KEY值与IPC标识 所以sem_init的第二个参数必须为0
有名:
在三个不同进程中一次打印ABC,这因为是在进程中,所以使用有名信号量,用到KEY值
如果在线程中调用线程函数打印,就可以用封装好的无名信号量,semaphore.h中的API函数
4 #define SEM_NUM 3
5 void sem_init()
6 {
7 semid = semget((key_t)1234,SEM_NUM,IPC_CREAT|IPC_EXCL|0600);
8 if(semid == -1)
9 {
10 semid = semget((key_t)1234,SEM_NUM,0600);
11 if(semid == -1)
12 {
13 printf("semget err\n");
14 return;
15 }
16 }
17 else
18 {
19 int arr[SEM_NUM] = {1,0,0};
20 union semun a;
21 for(int i = 0; i < SEM_NUM; ++i)
22 {
23 a.val = arr[i];
24 if(semctl(semid,i,SETVAL,a) == -1)
25 { ... }
26 }
27 }
无名:
56 int main()
57 {
58 sem_init(&sem1,0,1);
59 sem_init(&sem2,0,0);
60 sem_init(&sem3,0,0);
61 pthread_t id[SUM_NUM];
62 pthread_create(&id[0],NULL,pthread_funa,NULL);
63 pthread_create(&id[1],NULL,pthread_funb,NULL);
64 pthread_create(&id[2],NULL,pthread_func,NULL);
65 for(int i = 0; i< SUM_NUM;++i)
66 {
67 pthread_join(id[i],NULL);
68 }
69 sleep(5);
70 sem_destroy(&sem1);
71 sem_destroy(&sem2);
72 sem_destroy(&sem3);
73 exit(0);
74 }
线程
很多接口可以直接看帮助手册,或者老师发的那个课件PPT
进程内部的一条执行路径(多线程,单线程)
int main() void thread_fun()
{ {
} }
线程的实现
实现方式
**用户级:**内核并不能感知线程的存在,是用户自己用时间片轮转分为多条执行路径(简单点是:内核认为我只有一条执行路径)
? 优点:用户自己管理,创建,可以创建很多“线程”,开销小;
? 缺点:无法使用多处理器,效率不高;
内核级:
? 优点:可以利用多个处理器
? 缺点:开销较大
? 在内核中,它看起来就是一个普通的进程
**组合模型:**介于上述俩者之间
Linux是内核级线程
利用到了多处理器,才可能引起并行这种操作
API
pthread_creat(); //创建线程函数
pthread_exit(); //只退出当前线程
pthread_join(); //等待
pthread.c
#include<pthread.h>
void *fun(void *arg)
{
for(int i = 0;i < 5; ++i)
{
printf("fun run\n");
}
pthread_exit("fun over");
}
int main()
{
pthread_t id;
pthread_create(&id,NULL,fun,NULL);
for(int i = 0 ;i < 5; ++i)
{
printf("main run\n");
}
pthread_join(id,NULL);
char *s = NULL;
pthread_join(id,(void**)&s);
exit(0);
}
gcc -o main main.c -lpthread
运行结果可能有fun,也可能没有,(有的话顺序也不一定次次一样)没有的话就说明主线程运行太快,运行完直接退出进程了;
要是想让都打印出来,可以让主线程运行完睡眠,或者调用线程等待函数;
void *thread_fun(void* arg)
{
int index = *(int*)arg;
printf("fun index=%d\n",index);
}
int main()
{
pthread_t id[5];
int i= 0;
for(; i<5;++i)
{
pthread_create(&id[i],NULL,thread_fun,(void*)&i);
}
for( i = 0;i<5;++i)
{
pthread_join(id[i],NULL);
}
exit(0);
}
对于index++循环5000次这个例子,尽管index++是一条语句,但是程序是按指令运行的,这个语句可以拆分为多条指令,所以最后值是小于等于5000;
并发和并行
并发:在一个时间段内交替执行,看起来好像一起执行的
并行:在一个时间点一起执行(某一刻),这个需要多核或多处理器
线程同步
信号量,互斥锁,条件变量,读写锁;
信号量
#include<semaphore.h>
sem_init(&sem,0,1);
sem_wait(&sem);
sem_post(&sem);
sem_destroy(&sem);
定义: sem_t sem;
互斥锁
相当于初始值为1的信号量
#include<pthread.h>
pthread_mutex_init(&mutex,NULL);
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);
定义:pthread_mutex_t mutex;
生产者消费者
我们加入一个缓冲池,将生产者消费者的强耦合关系转换为生产者和缓冲区以及消费者和缓冲区的弱耦合关系
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<semaphore.h>
#include<pthread.h>
#define Producter 2
#define Customer 3
#define Size 6
int buff[Size];
int in = 0 ,out = 0; 在缓冲池中生产,消费的下标
sem_t sem_dempty;
sem_t sem_dfull;
pthread_mutex_t mutex;
void* sc(void* arg)
{
int tmp = (int)arg;
for(int i = 0; i < 3;++i)
{
sem_wait(&sem_dempty);
int n = rand()%100;
pthread_mutex_lock(&mutex);
buff[in] = n;
printf("-----生产者%d P生产了%d\n",tmp,n);
in = (in + 1)%Size;
sleep(1);
pthread_mutex_unlock(&mutex);
sem_post(&sem_dfull);
}
}
void* xf(void* arg)
{
int tmp = (int)arg;
for(int i = 0; i < 2;++i)
{
sem_wait(&sem_dfull);
pthread_mutex_lock(&mutex);
int n = 0;
n = buff[out] ;
printf("-----消费者%d C读取了%d\n",tmp,n);
out = (out + 1)%Size;
sleep(1);
pthread_mutex_unlock(&mutex);
sem_post(&sem_dempty);
}
}
int main()
{
sem_init(&sem_dempty,0,Size);
sem_init(&sem_dfull,0,0);
pthread_mutex_init(&mutex,NULL);
pthread_t id[Producter + Customer];
for(int i = 0 ;i < Producter; ++i)
{
pthread_create(&id[i],NULL,sc,(void*)i);
}
for(int i = Producter ; i < Producter + Customer;++i)
{
pthread_create(&id[i],NULL,xf,(void*)i);
}
for(int i = 0; i< Producter + Customer;++i)
{
pthread_join(id[i],NULL);
}
sleep(5);
sem_destroy(&sem_dempty);
sem_destroy(&sem_dfull);
pthread_mutex_destroy(&mutex);
exit(0);
}
线程安全
多线程程序,无论调度顺序怎么样变化,程序都可以正常执行,我们都能得到一个正确的结果;
保证线程安全
1.使用线程同步
2.多线程中使用线程安全函数(可重入函数);
可重入函数:一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static);
典型函数:strtok\strtok_r rand\rand_r 时间转换里面的一些等
多线程一个人调用
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
int main()
{
char buff[] = "a b c d e f";
char * s = strtok(buff," ");
while(s != NULL)
{
printf("s = %s\n",s);
s = strtok(NULL," ");
}
printf("%s\n",buff);
exit(0);
}
xing@xing-virtual-machine:~/liberal/AB$ ./strtok
s = a
s = b
s = c
s = d
s = e
s = f
a
多线程多个人调用
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
void * fun()
{
char arr[] = "1 2 3 4 5 6";
char *s = strtok(arr," ");
while(s != NULL)
{
printf("s = %s\n",s);
s = strtok(NULL," ");
}
pthread_exit(0);
}
int main()
{
char buff[] = "a b c d e f";
char * s = strtok(buff," ");
pthread_t id;
pthread_create(&id,NULL,fun,NULL);
while(s != NULL)
{
printf("s = %s\n",s);
s = strtok(NULL," ");
}
pthread_join(id,NULL);
exit(0);
}
s = a
s = b
s = c
s = d
s = 1
s = 3
s = 4
s = 5
s = 6
s = 2
s = a
s = 1
s = 3
s = 2
s = 4
s = 5
s = 6
为什么strtok 多线程中不安全的,因为调用它的时候,系统自动在数据区定义了一个静态指针,保存着下次分割起止的字符串,对于多线程来说,这块空间是通用的;
所以要想得到百无一失的正确结果,需使用多线程函数:strtok_r,它多了一个二级指针参数(传入一级指针地址,相当于我们自己记住当前分割到哪里,不需要函数在数据区帮我们记),忘记的调用man手册查看
网络
基本概念
**网络:**把我们独立自主的计算机(主机)链接起来;
互联网:把网络连接起来;
**IP地址:**网络中主机标识符(唯一标识一台主机) 网络号+主机号
? IPV4(32位) IPV6(64位) //无符号整形 0-255 . 0-255 . 0-255 . 0-255
? 形成点分十进制的字符串:“127.0.0.1”
? MAX地址 (48位) 也可以唯一标识一台主机,是固化在计算机适配器的 ROM 中的地址 ,当你从一个地方到另一个地方,你 的地理位置变了,但电脑中局域网“地址”没变,所以我们不能知道主机在哪里,所以还需要使用IP地址进行;
**端口号:**主机上标识进程
协议其实就是一堆规则 网卡就是网络中收发数据的
**网络分层:**应用层 表示层 会话层 传输层 网络层 数据链路层 物理层 // osi模型
应用层 网络层 传输层 数据链路层 // tcp模型
? 数据链路层:相邻俩个主机(节点)之间的数据传输;
? 网络层:不相邻的主机之间的数据传输;
? 传输层:给哪个进程传输数据(端口号在这里起作用),提供了进程间通讯的能力
? 为什么要分层? 因为可将复杂问题简单化,一步一步去解决,最终实现进程间通讯;
套接字
套接字:通过网络,进行数据的收发
服务器: 客户端:
socket();
bind();
listen();
accpte();
recv();
send();
close();
linux系统的lislen表示已完成三次握手队列大小(系统不一样,意义不一样),unix上是未完成和已完成之和的大小(linten大小内核最大128)
创建套接字指定服务类型: tcp SOCK_STREAM udp SOCK_DGREM
网卡属于计算机硬件层面的资源,套接字是软件层面资源;
socket:ip+port 称为套接字的地址(俩对,对方也有ip+port)
sockaddr 通用的套接字地址结构 sockaddr_in(IPV4专用)
socket函数的参数,第一个用AF_INET,是IPV4的地址族,第二个用SOCK_STREAM,是流式服务,最后一个恒为0,可认为版本号;
htons是主机转网络字节序列(大端),s是短整型
? 1024以内的端口号只能管理员使用,叫知名端口,其拥有特殊的含义
? 1024-4096(5000)叫保留的端口,为以后需要备留
? 5000以上的叫临时端口,我们可以选择使用
inet_addr是将点分十进制IP地址转为无符号整型
ifconfig查看网络信息(ip地址) ping判断俩个主机是否连通
netstat查看端口号,后可带参数
127.0.0.1代表自己,用于测试
socket是监听套接字
c是连接套接字
门迎(socket)引进来,交给点餐服务于(c),各司其职,门迎又回原位等待新的顾客
send()执行成功,是我们将其成功写入发送缓冲区,并不是直接发给对方;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-haWrFb5I-1648999921843)(E:\my_user\Picture\Typora\Linux\send.png)]
TCP
TCP基本概念
传输层使用TCP协议,我们先启用服务器端,然后再启用客户端,让其去连接服务器端
服务器端的IP端口要固定下来,被动连接,客户端无所谓
我们之前提的进程间通信是在同一台主机,而传输层则是在两台不同主机
TCP特点:面向连接的,可靠的,流式服务
面向连接:三次握手建立连接(connect)
四次挥手断开连接(close)
可靠的:应答确认,超时重传,去重,乱序重排 滑动窗口(进行流量控制)
流式服务:发送端执行的写操作次数和接收端执行的读操作次数之间没有任何数量关系,应用程序对数据的发送和接收是没有边界限制的
UDP特点:无连接,不可靠,数据报服务
UDP的不可靠不代表不成功,只是没有确保成功机制,允许丢数据
所以这俩不代表谁好谁坏,各自用在适应场合,比如我们下载个文件,这肯定一个字节都不能丢,丢了就不可用,所以必须使用TCP协议;如果我们进行电话视频,因为网络差卡掉了几帧,那也没事,大不了让对面重说一下,不会有啥损失,就可以使用UDP协议,它的效率更高效点
流式服务
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HbuHvVG9-1648999921844)(E:\my_user\Picture\Typora\Linux\流式.png)]
粘包:发送端发送的数据,被接收端一次性收走了
会不会产生影响? 不一定。
比如我们下载个文件,对方发送了1000次,我们500次接收完了,这不影响,但是当我们交互的时候,比如对方三次分别发送长宽高,我们一次或者俩次接收完,就会导致结果出错
如何解决? //具体可看 https://www.cnblogs.com/javalinux/p/14307497.html
1、可以设置标志,报文头部加入数据的大小
2、让send分开发送
使用TCP_NODELY,屏蔽nagle优化算法(这个不咋懂)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jrYnj7Hm-1648999921845)(E:\my_user\Picture\Typora\Linux\网络传输.png)]
我们所写的代码是在应用层;
我们从应用层发送个hello,它经过传输层,我们加上tcp的报头,然后再经过网络层,我们加上ip的报头,最后经过数据链路层打包为数据帧,最后传输到另一个进程;
“hello"
tcp "hello"
ip tcp "hello"
帧头 ip tcp "hello" 帧尾 -----> ...
到对方进程再反过来一步步剖析
三次握手
系统自动完成的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NISBFcMc-1648999921846)(E:\my_user\Picture\Typora\Linux\tcp握手.png)]
客户端通过connect发起连接,然后进入listen为未连接队列,等三次握手成功后,它会从未连接队列转到已连接队列,然后服务器的accpet会接收连接,并且返回一个新的连接套接字c,当有新的客户端再次发起连接时,服务器会再次接受,并再次返回一个连接套接字后面就用连接套接字分辨是哪个客户端与我的交互。
四次挥手
执行close时进行的操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qUUwf7ZC-1648999921847)(E:\my_user\Picture\Typora\Linux\挥手.png)]
挥手可以成为三次,因为我们不知道被动断开放的close什么时候执行,当它的时间恰好是主动断开放发过FIN的时候,那我们被动断开方直接发送ACK和自己的FIN;
状态转移
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qiLFRQZu-1648999921848)(E:\my_user\Picture\Typora\Linux\状态转移.png)]
closed是假想的状态,程序从这里开始,粗实线表示典型的客户端状态转移,粗虚线表示典型的服务器端状态转移;RST是出错报文,比如没连接到,重新建立连接
上图中,TIME_WAIT 状态一般情况下是主动关闭的一端才会出现的状态。该状态出现后,会维持一段长为 2MSL(Maximum Segment Life)的时间,才能完全关闭。MSL 是 TCP 报文段在网络中的最大生存时间,标准文档 RFC1122 的建议值是 2min。
TIME_WAIT 状态存在的原因有两点:
? 可靠的终止 TCP 连接:
? 我们进程关闭了,但是连接信息此时并未被内核释放,相反它会保持一段时间,防止我们给对方发送ACK,对方未收到,然后过一会对方再次发FIN,我们由于关闭连接信息接收不到,以及也未能给对方再次发送ACK而导致连接失败;
? 保证让迟来的 TCP 报文有足够的时间被识别并被丢弃:
? 因为在TIME_WAIT状态中,其端口不可被启用,所以这段时间接收延迟数据包并丢弃,确保下次启动端口都是新数据
当我们服务端一启动,程序就到了listen监听状态,然后当客户端启动后,它会给服务端发送SYN报文,然后处于SYN_SENT状态,对方接收到其报文后,并回发自己的SYN报文以及对对方确认的ACK报文,然后处于SYN_RCVD状态,接着客户端再次收到服务端发来的SYN报文和对自己确答的ACK后,回发对方的ACK确答,等服务器收到对自己的ACK确答后,双方三次握手已完成,同处于ESTABLISHED状态;(此状态可长久维持)
关闭时,对于服务器还是客户端顺序没影响,我们假设服务器端先关闭,那么我们就会给客户端发送FIN报文,然后处于FIN_WAIT_1状态,当客户端收到服务器的FIN报文后,回复个ACK确答报文(其实服务器只是通知客户端一声,我已经关闭了),然后自己处于CLOSE_WAIT状态,等服务器端收到对自己确答的ACK后,状态转为FIN_WAIT_2,此时已经完成俩次握手,过了一段时候,等客户端关闭的时候,它向服务器发送自己的FIN报文,然后处于LAST_ACK状态,然后马上就回到CLOSED状态,而服务器端收到对方的FIN后,回复个应答ACK报文,自己就处于TIME_WAIT状态,过一会转到CLOSED状态;
如果我们俩个同时关闭,我们都同时发送FIN,然后一方收到我发送的FIN,然后回复我的确答ACK,另一个也是同样的,然后处于CLOSING状态,等他们都都到对自己的确答ACK后,状态转为TIME_WAIT;(左下虚线框里右上角的路线,服务器和客户端路线一样)
如果三次挥手,就是左下角虚线框的斜线,甲方先关闭,然后发送FIN,乙方收到你的FIN,并且回复你确答ACK的同时,向你发送了自己的FIN报文,然后甲方也就是你,收到乙方的FIN后,回复了对乙方确答ACK后,状态转为TIME_WAIT; Linux高性能服务器编程
TCP服务端
ser.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res == -1) return -1;
res = listen(sockfd,5);
if(res == -1) return -1;
while(1)
{
int len = sizeof(caddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
if( c <= 0 )
{
continue;
}
while(1)
{
char buff[128] = {0};
int n = recv(c,buff,127,0);
if(n <= 0)
{
printf("client close\n");
break;
}
printf("recv(%d) = %d %s\n",c,n,buff);
send(c,"ok",2,0);
}
close(c);
}
close(sockfd);
exit(0);
}
TCP客户端
cli.c
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
assert(res != -1);
while(1)
{
char buff[128] = {0};
printf("Input\n");
fgets(buff,128,stdin);
if(strncmp(buff,"end",3) == 0)
{
break;
}
send(sockfd,buff,strlen(buff)-1,0);
memset(buff,0,128);
recv(sockfd,buff,127,0);
printf("recv(%d) = %s\n",sockfd,buff);
}
close(sockfd);
exit(0);
}
TCP服务端多线程
thread.c
void *fun(void* arg)
{
int c = (int)arg;
while(1)
{
char buff[128] = {0};
int n = recv(c,buff,127,0);
if(n <= 0)
{
printf("client close\n");
break;
}
printf("recv(%d) = %d %s\n",c,n,buff);
send(c,"ok",2,0);
}
close(c);
}
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res == -1) return -1;
res = listen(sockfd,5);
if(res == -1) return -1;
while(1)
{
int len = sizeof(caddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
if( c <= 0 )
{
continue;
}
pthread_t id;
pthread_create(&id,NULL,fun,(void*)c);
}
close(sockfd);
exit(0);
}
TCP服务端多进程
fork.c
void fun(int sig)
{
wait(NULL);
}
int main()
{
signal(SIGCHLD,fun);
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res == -1) return -1;
res = listen(sockfd,5);
if(res == -1) return -1;
while(1)
{
int len = sizeof(caddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
if( c <= 0 )
{
continue;
}
pid_t pid;
pid = fork();
if(pid == -1)
{
close(c);
exit(0);
}
if(pid == 0)
{
while(1)
{
char buff[128] = {0};
int n = recv(c,buff,127,0);
if(n <= 0)
{
printf("client close\n");
break;
}
printf("recv(%d) = %d %s\n",c,n,buff);
send(c,"ok",2,0);
}
close(c);
}
close(c);
}
close(sockfd);
exit(0);
}
UDP
无连接,不可靠,数据报服务
不可靠:无应答确认机制,发出去成不成功看天意-.-
服务端: 客户端:
socket(); socket();
bind(); sendto();
recvfrom(); recvfrom();
sendto(); close();
close();
相对简单,因为不建立连接,当然就不会存在握手与挥手等;
数据报服务,一次只接受一个数据包,且它是一个独立的个体,只要一接收,就拆包了,没接收完的数据就丢了,下次再接收就是拆第二个数据包,因为它有边界,不像TCP那样数据放一块想拿多少拿多少,没拿够下次再拿,send发送的是一个一个的个体,所以接收的话一次要把包拆开都拿走,不然就没了
它也有缓冲区,只不过它的缓冲区放的是一个一个的报文
它俩在传输数据,突然客户端关闭了,然后我们再打开客户端,可以继续发送数据不,服务端不变?
答:可以,因为它俩从始至终一直没建立连接,也就是说服务端可以同时接受多个客户端发送的数据,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E2HlAHdM-1648999921849)(E:\my_user\Picture\Typora\Linux\数据报.png)]
正常sendto发送几次,我们recvfrom就要接收几次,因为它是独立的个体块
TCP和UDP可以使用同一个端口
一个进程内可以创建多个套接字(但不可以一样,最起码端口换一个数字ha),设计一个用来监听,一个用来连接别人等
./ser
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
assert( sockfd != -1);
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
assert(res != -1);
while(1)
{
int len = sizeof(caddr);
char buff[128] = {0};
int num = recvfrom(sockfd,buff,127,0,(struct sockaddr*)&caddr,&len);
if( num <= 0 )
{
printf("cli close\n");
break;
}
printf("buff=%s\n",buff);
sendto(sockfd,"ok",2,0,(struct sockaddr*)&caddr,sizeof(caddr));
}
close(sockfd);
}
./cli
int main()
{
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
assert( sockfd != -1);
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
while(1)
{
char buff[128] = {0};
fgets(buff,128,stdin);
if(strncmp(buff,"end",3) == 0)
{
break;
}
sendto(sockfd,buff,strlen(buff)-1,0,(struct sockaddr*)&saddr,sizeof(saddr));
memset(buff,0,128);
int len = sizeof(saddr);
recvfrom(sockfd,buff,127,0,(struct sockaddr*)&saddr,&len);
printf("buff=%s\n",buff);
}
close(sockfd);
}
HTTP
超文本传输协议,属于应用层协议,我们要自己去组装报文,TCP和UDP属于传输层协议;
我们传输的数据是http的报文,然后这个报文是通过tcp或者udp去传输
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5maQqQoq-1648999921850)(E:\my_user\Picture\Typora\Linux\http.png)]
DNS解析器可以将域名对应的IP地址解析出来,然后与http默认的端口80,通过connect去连接服务器,然后通过三次握手建立连接,接着浏览器发送http的请求报文;
短连接:建立连接后,我发送一次,你接收一次,然后断开;
长连接:发送接收交互超过俩次,就属于长连接,现在大多属于长连接
https属于对其的升级,进行了加密,属于更安全的协议;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LaqGgxWO-1648999921851)(E:\my_user\Picture\Typora\Linux\HTTP请求.png)]
其中最常用的就是GET和POST,get相当于只读,不改变其内容,而post则要求修改,比如我们注册账号修改了数据库,或者删除某些信息,创建个图片,改变用户名,换头像背景等等;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T1ZHbmXn-1648999921852)(E:\my_user\Picture\Typora\Linux\HTTP应答.png)]
对于浏览器请求后,我们回复的当前状态及信息
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<fcntl.h>
#define PATH "/home/xing/liberal/day9"
char* get_filename(char buff[]);
int sock_init();
int main()
{
int sockfd = sock_init();
assert( sockfd != -1 );
while(1)
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
if( c < 0 )
{
continue;
}
char buff[1024] = {0};
int n = recv(c,buff,1023,0);
if( n == 0 )
{
close(c);
continue;
}
printf("recv:%s\n",buff);
char* filename = get_filename(buff);
if( filename == NULL )
{
send(c,"404",3,0);
close(c);
continue;
}
char path[256] = {PATH};
if ( strcmp(filename,"/") == 0 )
{
strcat(path,"/index.html");
}
else
{
strcat(path,filename);
}
int fd = open(path,O_RDONLY);
if( fd == -1 )
{
send(c,"404",3,0);
close(c);
continue;
}
int size = lseek(fd,0,SEEK_END);
lseek(fd,0,SEEK_SET);
char head[256] = {0};
strcpy(head,"HTTP/1.0 200 OK\r\n");
strcat(head,"Server: http\r\n");
sprintf(head+strlen(head),"Content-Length: %d\r\n",size);
strcat(head,"\r\n");
send(c,head,strlen(head),0);
printf("send head:\n%s\n",head);
char data[1024] = {0};
int num = 0;
while((num = read(fd,data,1024))> 0 )
{
send(c,data,num,0);
}
close(fd);
close(c);
}
}
char* get_filename(char buff[])
{
char * s = strtok(buff," ");
if ( s == NULL )
{
return NULL;
}
s = strtok(NULL," ");
return s;
}
int sock_init()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
return -1;
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(80);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res == -1)
{
return -1;
}
res = listen(sockfd,5);
if(res == -1)
{
return -1;
}
return sockfd;
}
简易文件,这属于前端-.-
index.html文件
<html>
<head>
<meta charset=utf-8>
<title>我的主页</title>
</head>
<body>
<center>
<h1>你好
</center>
</body>
</html>
守护进程
运行周期长,在后台运行,不与用户交互
libevent
句柄:I/O框架库要处理的对象,即I/O事件,信号,定时事件,统一称为信号源。Linux环境下,I/O事件对应句柄是文件描述符,信号事件对应句柄就是信号值。
句柄作用:当内核检测到就绪事件时,它将通过句柄来通知应用程序这一事件。
事件多路分发器,我们就认为I/O函数;
事件处理器就认为我们回调函数,具体事件处理器就是哪一个函数
我们定义的事件都会添加到libevent框架库中,它有三个队列来接收三事件,这三个队列收到的事件,我们最终都会注册到I/O函数上,通过I/O函数进行检测,一旦检测到,我们就会通知libevent将其事件拿出放入就绪队列,并调用其回调函数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sml4krHn-1648999921853)(E:\my_user\Picture\Typora\Linux\libevent.png)]
如果有永久性事件,我们处理完就绪队列后,又回到之前的队列继续等待响应,没有的话我们处理完就没了(EV_PERSIST)
include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<event.h>
#include<signal.h>
#include<sys/time.h>
#include<assert.h>
void fun_sig(int fd,short ev,void *arg)
{
printf("sig=%d\n",fd);
}
void fun_timeout(int fd,short ev,void* arg)
{
printf("time out\n");
}
int main()
{
struct event_base * base = event_init();
assert( base != NULL );
struct event* ev_sig = event_new(base,SIGINT,EV_SIGNAL|EV_PERSIST,fun_sig,NULL);
assert(ev_sig != NULL);
event_add(ev_sig,NULL);
struct timeval tv = {5,0};
struct event* ev_time = event_new(base,-1,EV_TIMEOUT|EV_PERSIST,fun_timeout,NULL);
assert(ev_time != NULL);
event_add(ev_time,&tv);
event_base_dispatch(base);
event_free(ev_time);
event_free(ev_sig);
event_base_free(base);
}
我们上面程序逻辑是:我们先定义libevent库实例,然后我们定义信号的事件,然后添加此事件到libevent,然后程序运行到34行就会一直进行循环检测事件,我们中间也可以定义定时器事件,然后再将其添加到我们的libevent;
上面定义事件的函数“evsignal_new”和定义定时器的函数“evtimer_new"都是封装好的,它原本都是event_new(),这个函数第一个参数就是我们指向库的地址,就是base,第二个就是文件描述符,第三个就是事件,第四个是回调函数,第五个是回调函数参数;
event_base_dispatch(base)这个函数是将libevent启动了,一直在循环检测;
我们退出这个循环有俩个方法:
1、没有EV_PERSIST,调用主调函数后自动退出
2、主调函数调用event_base_loopexit函数,自动定义延迟时间[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pVP0Fbnb-1648999921854)(E:\my_user\Picture\Typora\Linux\delay.png)]
这个尽管我们还在循环,但是时间一到就相当于我们手动给退出了;
服务器端
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<event.h>
#include<signal.h>
#include<sys/time.h>
#include<assert.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
struct mess
{
struct event *c_ev;
};
int socket_init();
void recv_cb(int fd,short ev,void *arg)
{
struct mess *p = (struct mess*)arg;
if ( ev & EV_READ )
{
char buff[128] = {0};
int n = recv(fd,buff,127,0);
if(n <= 0)
{
printf("close client\n");
event_free(p->c_ev);
free(p);
close(fd);
return ;
}
printf("recv(%d)=%s\n",fd,buff);
send(fd,"ok",2,0);
}
}
void accept_ab(int fd,short ev,void *arg)
{
struct event_base* base = (struct event_base*) arg;
if( ev & EV_READ )
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(fd,(struct sockaddr*)&caddr,&len);
if(c < 0)
{
return;
}
printf("accept c=%d\n",c);
struct mess *p = (struct mess*)malloc(sizeof(struct mess));
p->c_ev = event_new(base,c,EV_READ|EV_PERSIST,recv_cb,p);
if( p->c_ev == NULL)
{
close(c);
return;
}
event_add(p->c_ev,NULL);
}
}
int main()
{
int sockfd = socket_init();
assert(sockfd != -1);
struct event_base * base = event_init();
assert(base != NULL);
struct event* sock_ev = event_new(base,sockfd,EV_READ|EV_PERSIST,accept_ab,base);
assert(sock_ev != NULL);
event_add(sock_ev,NULL);
event_base_dispatch(base);
event_free(sock_ev);
event_base_free(base);
close(sockfd);
exit(0);
}
int socket_init()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
return -1;
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res == -1)
{
return -1;
}
res = listen(sockfd,5);
if(res == -1)
{
return -1;
}
return sockfd;
}
脚本
shell
基本概念
我们C程序可以直接访问脚本,在写脚本时一般以.sh结尾(不写也是脚本,只是写了好标识一点)
my.sh
#!/usr/bin/bash
echo "hello"
exit 0
bash my.sh
或者给my.sh文件一个执行权限 chmod u+x my.sh 就可以./my.sh
c/c++ 编译型: xx.c 编译生成二进制的可执行程序,直接运行,运行效率高
? 解释型:需要解释器,通过解释器解释执行 开发效率高
为什么C/C++生成可执行文件,运行效率高?
答:因为它可以直接把文件便以为可执行文件,然后在主机运行,而解释型的话,需要先运行解释器,然后在读取文件
执行xx.sh时,俩个方法
chmod u+x xx.sh
bash xx.sh
命令
echo 打印某值
printf 也是打印值,但echo出来的早,一般用echo(需要手动加换行),不论在单引号还是双引号,\n都是会换行的,转义
$ 取某变量的值
‘ ‘ 单引号是原样输出 (与双引号区别是是否取值打印)
read 从键盘读数据
#!/usr/bin/bash
str="hello world"
val=99
echo "val=$val"
echo "str=$str"
printf "val=$val\n"
mystr="$str"
echo "mystr=$mtstr"
read line
echo $line
exit 0
变量
本地变量:自己定义的变量
环境变量:父进程继承的变量
参数变量:给脚本传参得到的变量
不要想变量类型,我们同一个变量名,可以存放不同类型的值,我们只是将这个名字同一块变量空间关联了,不存在变量是什么类型这个概念,和C不一样,C是强类型编程
定义字符串时尽量把引号都加着;
环境变量:PATH(path就是可执行程序存放的地点),bin所有用户使用的命令,sbin特指管理员使用的命令
PS1 一级指示符,可以修改其值
PS2 二级指示符 ,在bash某一行没打完后面加个’\‘然后再按回车,就可以继续上面的接着输入命令
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IXAGkrmi-1648999921855)(E:\my_user\Picture\Typora\Linux\二级指示符.png)]
环境和参数
#环境变量
echo "\$#=$#" 参数个数
echo "\$0=$0" 脚本名字
echo "\$$=$$" 脚本PID(运行解释器bash的pid)
#参数变量
echo "\$1=$1" 脚本第一个参数
echo "\$2=$2" 脚本第二个参数...
条件
if test -f fred.c
then
...
fi
echo "Input"
read filename
if [ -f "$filename" ]
then
echo "$filename 存在"
else
echo "$filename 不存在"
fi
if [ -d "$filename" ]
then
echo "$filename 是目录文件"
elif [ -f "$filename" ]
then
echo "$filename 时普通文件"
else
echo "$filename 不存在"
fi
路径问题 相对路径:在此文件的位置 绝对路径:文件真实的位置(在硬盘的)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kabpVrjN-1648999921856)(E:\my_user\Picture\Typora\Linux\文件条件.png)]
字符串比较
=(等于) != (不等于) -n(不为空) -z(为空)
切记等号俩边要有空格
算数比较
-eq -ne -gt(greater than) -ge -lt(less than) -le
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oDlJ5mu3-1648999921857)(E:\my_user\Picture\Typora\Linux\算数比较.png)]
其左右俩边必须为数字
作业一
打印各个段的作业
#!/usr/bin/bash
echo "Please enter score"
while [ 1 ]
do
read score
case $(expr $score / 10) in
9 | 10 ) echo "A";;
8 ) echo "B";;
7 ) echo "C";;
6 ) echo "D";;
end ) break;;
* ) echo "E";;
esac
done
循环
i=1
let "i+=1"
echo "i=$1"
str=$(ls)
str=`ls`
a=$((i++))
a=`expr $i + 1`
如果是乘的话就需要使用转义字符
a=`expr $i \* 2`
for循环
它的循环次数是根据后面的值所决定的
for i in 1 2 3
do
echo i=$i
sleep 1
done
while循环
满足条件可执行
while [ 1 ]
do
echo "input"
read line
if [ "$line" = end ]
then
break
fi
echo "line=$line"
done
until循环
条件不满足执行
until [ -f "file.txt" ]
do
echo "not find"
sleep 1
done
echo "find"
case语句
read line
case "$line" in
yes | YES) echo "is"
[][] | [] ) echo "..."
no | NOT) echo "isn`t"
*) echo "****"
esac
函数
没有返回值和参数类型,有没有声明,所以函数要写到最上面
我们执行的时候从非函数语句开始,且脚本中也没有主函数
我们的变量都相当于全局变量
搞懂:参数,返回值,定义的变量
fun()
{
echo "fun run"
echo "fun :\$#=$#"
echo "fun :\$1=$1"
echo "fun :\$2=$2"
}
echo "my.sh :\$#=$#"
echo "my.sh :\$1=$1"
echo "my.sh :\$2=$2"
fun hello 123
./my.sh hello 123
my_add()
{
if [ $# != 2 ]
then
echo "参数有误"
return 0
fi
res=`expr $1 + $2`
return $res
}
my_add
echo "$?"
unset str
所以我们不要return也行,因为它函数内定义的这个变量是全局的
脚本调用脚本
b.sh
echo "b.sh run"
mystr=hello
echo "b.sh mystr=$mtstr"
./d.sh
exit 0
d.sh
echo "d.sh run"
echo "d.sh mystr=$mtstr"
exit
问题:我们在d.sh中能打印出mystr值么?
答:不行,因为这是在俩个不同的进程,在b的解释器中有此变量,在另外的解释器中没这个变量,解释器相当于一个进程,b和d的解释器pid不相同
要么我们传参
./d.sh $mtsyr
要么我们加个export,将其变为环境变量
export mystr
因为环境变量可以继承,我们在b解释器运行d解释器,那么d解释器相当于我们b的一个子进程
. ./d.sh
source ./d.sh
.和source意思一样,相当于我们就在b的解释器中运行,记住中间要有空格
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zqoZyMY0-1648999921857)(E:\my_user\Picture\Typora\Linux\source当前脚本执行.png)]
我们执行./my.sh是重新启动个解释器,然后执行其内容,但是在我们现在这个bash中没有mystr,所以打印不出东西;当我们使用. ./my.sh是在当前bash中执行此解释器,然后打印的话,就可以输出mystr的值
配置环境变量
C语言调用脚本
直接替换就行
#include<stdio.h>
#include<stdlib.h>
int main()
{
execl("my.sh","my.sh",(char*)0);
exit(0);
}
git
远程仓库
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vUwSONkz-1648999921858)(E:\my_user\Picture\Typora\Linux\git.png)]
我们创建的文件,通过git add “main.c" 进入到我们仓库的暂存区,然后再使用git commit -m ‘属性信息’,就会到我们的暂存区;可以使用git log查看
当我们再次修改了main函数,并未假如仓库,想让它变回来后,使用 git checkout main.c,当想往后撤时,使用git reset --hard HEAD(一个代表前一个版本),此时HEAD指针也改变了,想再回到当初,只能使用git reflog 查看它前面的序号,然后gti reset --hard 序号;使用git rm main.c进行删除,git diff查看差值变化
分支
git branch dev 创建分支
git checkout -b 分支 创建并切换分支
小结
我们写完代码,然后可以先通过add田将其添加到我们的仓库暂存区,然后再使用commit将其添加到我们的版本库,最后使用 git push origin [分支] 将其推到我们的服务器,当然,如果要从服务器读取资源,我们可以直接往下拉,使用git pull origin [分支],不想拉我们可以直接服务器写完内存我们直接克隆,git clone [这里是从服务器复制来的ssh那个]
我们只有拉和推的时候才和远程仓库建立连接,等这个操作完毕之后,连接就断开了
MySQL数据库
可以看看mysql部分四
数据库概念
数据库就是存放在磁盘上的一个文件
数据库管理系统:操作和管
create databases 数据库名 charset=utf*; //后面是以utf8这种格式
CHAR:实际如果给了三个大小的话,比如char(3),但是我们输入了'ab',那么它最终会变为'ab ',给你填一个
VARCHA: 我们该多少就多少,比如我们也给了三个,varchar(3),但是输入了'ab',那么实际也就是'ab',不会给你补
链接的时候一定写:gcc -o mysql mysql.c -lmysqlclient
文件传输项目
服务器客户端的模式,服务器是TCP类型的,多线程并发处理多个客户端,每有一个连接客户端,就产生一个线程;
我们ser只做一个事情:接收连接,启动线程(一直是个循环)
第一天,我们让客服端和服务端进行了连接,当然此连接我们是多文件编译,最后把连接的数据都存放在my.conf配置文件中,然后在socket.c中进行打开此文件并解析,拼接;
#ser的配置文件
#端口
port=6000
#ip地址
ips=127.0.0.1
#监听队列大小
lis_max=5
socket.c里的解析函数
#define PORT "port="
#define IPS "ips="
#define LIS_MAX "lis_max"
struct conf_info
{
char ips[32];
int port;
int lis_max;
};
bool read_conf(struct conf_info* dt)
{
if(nullptr == dt)
{
return false;
}
FILE *fp = fopen("./my.conf","r");
{
return false;
}
int index = 0;
while(1)
{
char buff[128] = {0};
if(fgets(buff,127,fp)==0)
{
break;
}
++index;
else if(fgets(buff,"#",1)==0)
{
continue;
}
else if(fgets(buff,"\n",1)==0)
{
continue;
}
buff[strlen(buff)-1] = '\0';
else if(fgets(buff,IPS,strlen(IPS))==0)
{
strcpy(dt->ips,buff+strlen(IPS));
}
else if(fgets(buff,PORT,strlen(PORT))==0)
{
dt->port = atoi(buff+strlen(PORT));
}
else if(fgets(buff,LIS_MAX,strlen(LIS_MAX))==0)
{
dt->lis_max = atoi(buff+strlen(LIS_MAX));
}
else
{
printf("第%d行解析失败\n",index);
continue;
}
}
}
thread.c里面我们进行操作,使用无名管道父子配合;
dup2是复制文件描述符 ( int dup2(int oldfd,int newfd) )
用管道写端,覆盖掉标准输出和标准错误输出,然后调用execvp(这个函数执行成功不返回,失败了才返回)
下载/上传
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uyk6pbf0-1648999921859)(E:\my_user\Picture\Typora\Linux\项目下载.png)]
我们下载的流程整体思路就如同上述图片里所描述;
项目整体实现描述:
客户端系统命令解析
比如实现ls,mkdir,pwd,ps…等
./cli
我们先进行创建套接字,然后与其通用地址进行“绑定”,确定我们所要连接的服务端的端口、ip和地址族;
int sockfd = socket(AF_INET,SOCK_ATREAM,0);
然后我们这个套接字不需要进行bind()绑定操作,因为我们客户端只需要连接服务端就行,只需要知道服务端的地址就行,不需要别人来主动连接我们,等和服务端连接接通时,服务端会将客户端地址再保存到一个通用地址里面,然后得到返回一个连接套接字,我们服务端就用这个套接字进行我们们数据传送,你可能会问,我们的客服端没绑定自己的地址,服务端怎么保存自己的地址呢,其实我们客户端在没有bind()的前提下,我们进行connect操作,系统会自动给我们分配一个port+ip,随机的;
然后连接成功,我们客服端皆可以给服务端发送数据了,跟着我们项目这个,我们就会发送ls,ls-l等系统命令,但是我们客户端也要对输入进来的数据进行解析,可以单独写一个read_cmd(char *buff,char*myargv[])函数,第一个函数是我们从终端收到的字符串,里面包括命令以及参数,第二个是我们的保存结果的字符串数组,我们判断,如果buff或者myargv是空的话,我们就返回空,然后我们就调用strtok()字符串分割函数,对其分割,然后将单个的字符串保存在字符串数组myargv中,然后返回myargv[0](这个是命令),然后其他参数都在myargv了,但是有一个,我们分割buff时,我们需要在调用函数再将其拷贝一份,因为我们还要给服务端发送这个完整的字符串,然后我们回到我们调用函数,然后判断cmd是什么,如果是"get"下载的话,执行..操作,如果是"up"上传的话,执行...操作,如果是"exit"我们结退出,否则那就是我们命令了,当然这里面也有可能是错误的命令,那我们就不管了,反正不是下载和上传,我们就将这个命令直接发送给服务端,让它去解析判断是否正确;此时客户端功能结束,然后可以等待接收服务端回来的消息,我们可以协定,发送回来的数据,前三个是"ok#"就表明服务端已经确定收到我们数据,然后我们又可以与其进行交互(打印其发送来的内容等),再次发送数据,然后我们最后可以输入exit退出客户端;
线程系统命令解析
我们服务端执行后,调用线程,也就是我们服务端就是有个初始化socket,然后accept客户端,然后调用线程并将c作为参数给它
我们为了在改变我们服务端地址的时候,不修改代码,所以我们需要先写一个配置文件,里面有着服务端的port、ip以及监听队列大小,然后我们服务端执行时,我们先创建套接字,但是我们没有它的地址,所以我们要解析配置文件,然后我们可以定义一个结构体,里面有它们三个成员,然后我们写个read_conf函数,参数就是我们这个结构体的地址,这样的话我们主函数调用时直接给地址,就能将其保存到我们的结构体对象里,然后我们在这个函数里用fopen打开配置文件,然后就是死循环通过fgets()读取配置文件,然后遇到#以及\n我们就可以直接continue,否则就判断它是不是我们的宏定义port,ip或者lis_max,如果不是,我们可以在循环外定义个index=0,然后等执行后我们++index,这样我们就可以在continue后打印出,哪行什么字符解析错误等提示,然后我们如果得到我们的那三个值了,我们port和lis_max直接使用atoi函数将其从字符串转为整型,ip的话直接用strcpy拷过来,在得到这三个的时候,我们切记我们需要将当前buff值加上此宏定义长度,这样就能从真实的数据开始“拷贝/赋值”。然后我们就可以直接在主函数调用我们写的socket_init()初始化函数,那个定义解析文件结构体是在socket_init()里面解析哈,项目点比较多,容易混淆,然后我们服务端和客户端进行了连接,这时候我们就可以解析命令,得到这个命令到底是啥,我们写个read_cmd函数,参数也是buff和myargv,这时候有个细节我们就得注意,我们需要使用strtok_r线程安全函数,因为我们服务端会调用线程执行接收客户端的操作,我们和客户端连接成功后,将得到的连接套接字直接给线程函数了,所以我们只需要在read_cmd函数里,多加个char*ptr,在使用strtok_r函数时,直接将其地址作为参数传进去就行,然后也返回命令就行;这时候我们就可以进行对比了,如果cmd是是空,下载,是上传等做什么操作,否则我们就是命令,我们可以封装个函数去执行,具体看下面的解析
上述解析文件可以看socket.c里的解析函数,stotok_r线程那也有笔记
线程执行命令解析
我们再封装个run_cmd函数,参数可以写个(int c,char *myargv[]),第一个参数c是连接套接字,便于我们在执行客户端发来的命名后,我们进行操作后将结果发送给客户端,第二个是客户端发来的命令以及参数,然后我们这里面就需要用到父子进程的无名管道,我们父进程负责给客户端回发数据,子进程负责替换进程;
一进来我们先进行判断是否为NULL,然后不是的话,我们就定义父子管道,然后去执行,返回值不为-1继续执行,这些等将来可以man手册看看返回值,具体return value我也记得不太清,然后我们调用fork函数去创建子进程,我们使用条件pid==0进入子进程,然后我们规定了子进程我们进行写,所以一进来我们就关闭子进程的读端,然后我们使用dup2(int oldfd,int newfd)函数,将描述符替换掉,所以这里我们使用的是dup2(1,pipefd[1]);dup2(2,pipefd[1]);也就是我们将此进程的标准输入输入都改为管道的写端,也就是我们本来要打印在屏幕上的数据以及错误数据,我们都写入了管道了,然后我们使用execvp(myargv[0],myargv)进行进程的替换,当然我们前面也说了,命令可能是错误的,所以我们后面再加个perror("run cmd err\n");也就是真的错了,它会打印出错误原因,然后我们退出子进程,之后我们到了父进程,父进程职责是进行读取管道的内容,然后传输给客户端,我们先调用wait函数,防止僵尸进程,然后我们关闭写端,然后定义个buff接收我们管道的信息,切记,我们前面可以归到它发送比如前三个是"ok#",给客户端就表示我们接收成功,然后我们发送给客户端,然后我们此时就可以退出了;
具体实现
run_cmd(int c,char*myargv[])
{
if(myargv==NULL)
{
return -1;
}
int pipefd[2];
if(pipe(pipefd[2]) == -1)
{
return -1;
}
pit_t pid = fork();
if(pid == -1)
{
return -1;
}
if(pid == 0)
{
close(pipefd[0]);
dup2(1,pipefd[1]);
dup2(2,pipefd[2]);
execvp(myargv[0],myargv);
perror("cmd run err\n");
exit(1);
}
wait(NULL);
close(pipefd[1]);
char data[1024] = {"ok#"};
read(pipefd[0],data+3,1021);
send(c,data,strlen(data),0);
return 0;
}
基础
编译分布
gcc -E main.c -o main.i
gcc -S main.i -o main.s
gcc -c main.s -o main.o
gcc main.o -o main
inue后打印出,哪行什么字符解析错误等提示,然后我们如果得到我们的那三个值了,我们port和lis_max直接使用atoi函数将其从字符串转为整型,ip的话直接用strcpy拷过来,在得到这三个的时候,我们切记我们需要将当前buff值加上此宏定义长度,这样就能从真实的数据开始“拷贝/赋值”。然后我们就可以直接在主函数调用我们写的socket_init()初始化函数,那个定义解析文件结构体是在socket_init()里面解析哈,项目点比较多,容易混淆,然后我们服务端和客户端进行了连接,这时候我们就可以解析命令,得到这个命令到底是啥,我们写个read_cmd函数,参数也是buff和myargv,这时候有个细节我们就得注意,我们需要使用strtok_r线程安全函数,因为我们服务端会调用线程执行接收客户端的操作,我们和客户端连接成功后,将得到的连接套接字直接给线程函数了,所以我们只需要在read_cmd函数里,多加个char*ptr,在使用strtok_r函数时,直接将其地址作为参数传进去就行,然后也返回命令就行;这时候我们就可以进行对比了,如果cmd是是空,下载,是上传等做什么操作,否则我们就是命令,我们可以封装个函数去执行,具体看下面的解析
上述解析文件可以看socket.c里的解析函数,stotok_r线程那也有笔记
#### 线程执行命令解析
```c
我们再封装个run_cmd函数,参数可以写个(int c,char *myargv[]),第一个参数c是连接套接字,便于我们在执行客户端发来的命名后,我们进行操作后将结果发送给客户端,第二个是客户端发来的命令以及参数,然后我们这里面就需要用到父子进程的无名管道,我们父进程负责给客户端回发数据,子进程负责替换进程;
一进来我们先进行判断是否为NULL,然后不是的话,我们就定义父子管道,然后去执行,返回值不为-1继续执行,这些等将来可以man手册看看返回值,具体return value我也记得不太清,然后我们调用fork函数去创建子进程,我们使用条件pid==0进入子进程,然后我们规定了子进程我们进行写,所以一进来我们就关闭子进程的读端,然后我们使用dup2(int oldfd,int newfd)函数,将描述符替换掉,所以这里我们使用的是dup2(1,pipefd[1]);dup2(2,pipefd[1]);也就是我们将此进程的标准输入输入都改为管道的写端,也就是我们本来要打印在屏幕上的数据以及错误数据,我们都写入了管道了,然后我们使用execvp(myargv[0],myargv)进行进程的替换,当然我们前面也说了,命令可能是错误的,所以我们后面再加个perror("run cmd err\n");也就是真的错了,它会打印出错误原因,然后我们退出子进程,之后我们到了父进程,父进程职责是进行读取管道的内容,然后传输给客户端,我们先调用wait函数,防止僵尸进程,然后我们关闭写端,然后定义个buff接收我们管道的信息,切记,我们前面可以归到它发送比如前三个是"ok#",给客户端就表示我们接收成功,然后我们发送给客户端,然后我们此时就可以退出了;
具体实现
run_cmd(int c,char*myargv[])
{
if(myargv==NULL)
{
return -1;
}
int pipefd[2];
if(pipe(pipefd[2]) == -1)
{
return -1;
}
pit_t pid = fork();
if(pid == -1)
{
return -1;
}
if(pid == 0)
{
close(pipefd[0]);
dup2(1,pipefd[1]);
dup2(2,pipefd[2]);
execvp(myargv[0],myargv);
perror("cmd run err\n");
exit(1);
}
wait(NULL);
close(pipefd[1]);
char data[1024] = {"ok#"};
read(pipefd[0],data+3,1021);
send(c,data,strlen(data),0);
return 0;
}
基础
编译分布
gcc -E main.c -o main.i
gcc -S main.i -o main.s
gcc -c main.s -o main.o
gcc main.o -o main
|