开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

HTTP协议多版本间区别 HTTP协议各版本介绍

发表于 2021-11-25

本文正在参与 “网络协议必知必会”征文活动

HTTP协议各版本介绍

HTTP协议是如今互联网与服务端技术的基石,HTTP协议的演进也从侧面反应了互联网技术的快速发展。这两天在准备一次关于HTTP1.1协议特性的技术分享过程中,顺便了解了下各版本HTTP协议的特点,在这里做个简单的总结。

HTTP协议到现在为止总共经历了3个版本的演化,第一个HTTP协议诞生于1989年3月。

img

HTTP 0.9

HTTP 0.9是第一个版本的HTTP协议,已过时。

它的组成极其简单,只允许客户端发送GET这一种请求,且不支持请求头。

由于没有协议头,造成了HTTP 0.9协议只支持一种内容,即纯文本。不过网页仍然支持用HTML语言格式化,同时无法插入图片。

HTTP 0.9具有典型的无状态性,每个事务独立进行处理,事务结束时就释放这个连接。

一次HTTP 0.9的传输首先要建立一个由客户端到Web服务器的TCP连接,由客户端发起一个请求,然后由Web服务器返回页面内容,然后连接会关闭。如果请求的页面不存在,也不会返回任何错误码。

HTTP 1.0

HTTP协议的第二个版本,第一个在通讯中指定版本号的HTTP协议版本,至今仍被广泛采用。相对于HTTP 0.9 增加了如下主要特性:

  • 请求与响应支持头域
  • 响应对象以一个响应状态行开始
  • 响应对象不只限于超文本
  • 开始支持客户端通过POST方法向Web服务器提交数据,支持GET、HEAD、POST方法
  • 支持长连接(但默认还是使用短连接),缓存机制,以及身份认证

HTTP 1.1

HTTP协议的第三个版本是HTTP 1.1,是目前使用最广泛的协议版本 。HTTP 1.1是目前主流的HTTP协议版本。

HTTP 1.1引入了许多关键性能优化:keepalive连接,chunked编码传输,字节范围请求,请求流水线等

  • Persistent Connection(keepalive连接)
    允许HTTP设备在事务处理结束之后将TCP连接保持在打开的状态,一遍未来的HTTP请求重用现在的连接,直到客户端或服务器端决定将其关闭为止。
    在HTTP1.0中使用长连接需要添加请求头 Connection: Keep-Alive,而在HTTP 1.1 所有的连接默认都是长连接,除非特殊声明不支持( HTTP请求报文首部加上Connection: close )
1
less复制代码![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9af0790b320f4a18ba573fd1b37399c1~tplv-k3u1fbpfcp-zoom-1.image)
  • chunked编码传输
    该编码将实体分块传送并逐块标明长度,直到长度为0块表示传输结束, 这在实体长度未知时特别有用(比如由数据库动态产生的数据)
  • 字节范围请求
    HTTP1.1支持传送内容的一部分。比方说,当客户端已经有内容的一部分,为了节省带宽,可以只向服务器请求一部分。该功能通过在请求消息中引入了range头域来实现,它允许只请求资源的某个部分。在响应消息中Content-Range头域声明了返回的这部分对象的偏移值和长度。如果服务器相应地返回了对象所请求范围的内容,则响应码206(Partial Content)
  • Pipelining(请求流水线)
1
bash复制代码![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d7e2804d1f9f4c779a9e112dda98ddaf~tplv-k3u1fbpfcp-zoom-1.image)

另外,HTTP 1.1还新增了如下特性:

  • 请求消息和响应消息都应支持Host头域
    在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。因此,Host头的引入就很有必要了。
  • 新增了一批Request method
    HTTP1.1增加了OPTIONS,PUT, DELETE, TRACE, CONNECT方法
  • 缓存处理
    HTTP/1.1在1.0的基础上加入了一些cache的新特性,引入了实体标签,一般被称为e-tags,新增更为强大的Cache-Control头。

HTTP 2.0

HTTP 2.0是下一代HTTP协议,目前应用还非常少。主要特点有:

为了解决1.1版本利用率不高的问题,提出了HTTP/2.0版本。增加双工模式,即不仅客户端能够同时发送多个请求,服务端也能同时处理多个请求,解决了队头堵塞的问题(HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级)。

HTTP请求和响应中,状态行和请求/响应头都是些信息字段,并没有真正的数据,因此在2.0版本中将所有的信息字段建立一张表,为表中的每个字段建立索引,客户端和服务端共同使用这个表,他们之间就以索引号来表示信息字段,这样就避免了1.0旧版本的重复繁琐的字段,并以压缩的方式传输,提高利用率。

  • 多路复用(二进制分帧)
    HTTP 2.0最大的特点: 不会改动HTTP 的语义,HTTP 方法、状态码、URI 及首部字段,等等这些核心概念上一如往常,却能致力于突破上一代标准的性能限制,改进传输性能,实现低延迟和高吞吐量。而之所以叫2.0,是在于新增的二进制分帧层。在二进制分帧层上, HTTP 2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码 ,其中HTTP1.x的首部信息会被封装到Headers帧,而我们的request body则封装到Data帧里面。
    img
    HTTP 2.0 通信都在一个连接上完成,这个连接可以承载任意数量的双向数据流。相应地,每个数据流以消息的形式发送,而消息由一或多个帧组成,这些帧可以乱序发送,然后再根据每个帧首部的流标识符重新组装。
  • 头部压缩
    当一个客户端向相同服务器请求许多资源时,像来自同一个网页的图像,将会有大量的请求看上去几乎同样的,这就需要压缩技术对付这种几乎相同的信息。
  • 随时复位
    HTTP1.1一个缺点是当HTTP信息有一定长度大小数据传输时,你不能方便地随时停止它,中断TCP连接的代价是昂贵的。使用HTTP2的RST_STREAM将能方便停止一个信息传输,启动新的信息,在不中断连接的情况下提高带宽利用效率。
  • 服务器端推流: Server Push
    客户端请求一个资源X,服务器端判断也许客户端还需要资源Z,在无需事先询问客户端情况下将资源Z推送到客户端,客户端接受到后,可以缓存起来以备后用。
  • 优先权和依赖
    每个流都有自己的优先级别,会表明哪个流是最重要的,客户端会指定哪个流是最重要的,有一些依赖参数,这样一个流可以依赖另外一个流。优先级别可以在运行时动态改变,当用户滚动页面时,可以告诉浏览器哪个图像是最重要的,你也可以在一组流中进行优先筛选,能够突然抓住重点流。

结语

文章首发于微信公众号程序媛小庄,同步于掘金。

码字不易,转载请说明出处,走过路过的小伙伴们伸出可爱的小指头点个赞再走吧(╹▽╹)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

丐版通讯录 基础版 丐版通讯录 静态版本 动态版本

发表于 2021-11-25

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」

丐版通讯录

什么是丐版,就是能达到基本温饱,而微星的丐版是性价比最高的丐版,当然我是高配哈哈哈,这个通讯录就是微星丐版,一些基本用户功能是有的,甚至还多了功能,排序,用户级别的控制内存,当然有些用户友好功能没写(例如输错三次强制退出啊什么的),这些得高配再写,不然就不叫丐版了,我相信大多数人用了没有说不好的,那些高配的不要来装了(文件处理的哈哈哈),高配我们家也有,不过还没到发售时间。不吹了,吃饭了

通讯录他能干什么呢

1.存放1000个好友信息

那么一个人的好友信息包含什么东西呢

包含姓名,电话,性别,住址,年龄

2.还可以增加好友信息

3.删除指定名字的好友信息

4.查找好友信息

5.修改好友信息

6.打印好友信息

7.排序好友信息

静态版本

test.c

测试通讯录各个功能

main.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
44
c复制代码int main()
{
int input = 0;
//int size = 0;//用来记录通讯录有多少人的信息变量
//struct PeoInfo con[MAX];//存放1000个人的信息
/*为了下面函数不重复的写con,&size,就把他们集合在 一起了struct Contact*/
struct Contact con;//con就是通讯录,里面包含1000人信息的数组,和size
InitContact(&con);//初始化通讯录

do//首先让他循环起来
{
menu();//显示菜单
printf("请选择:>");
scanf("%d",&input);
switch (input)
{
case ADD:
AddContact(&con);
break;
case DEL:
DelContact(&con);
break;
case SEARCH:
SearchContact(&con);
break;
case MODIFY:
ModifyContact(&con);
break;
case SHOW:
ShowContact(&con);
break;
case SORT:
QsortContact(&con);
break;
case EXIT:
printf("退出通讯录\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}

image-20210923080715957

菜单

1
2
3
4
5
6
7
8
9
c复制代码void menu()
{
printf("*******************************\n");
printf("***** 1.add 2.del ***\n");
printf("***** 3.search 4.modify ***\n");
printf("***** 5.show 6.short ***\n");
printf("***** 0.exit ***\n");
printf("*******************************\n");
}

这里的菜单序号要想到枚举

contact.c

通讯录的实现

初始化通讯录函数InitContact

1
2
3
4
5
6
7
c复制代码//初始化通讯录
void InitContact(struct Contact* ps)
{
assert(ps);
memset(ps->data, 0, sizeof(ps->data));//把data这个空间里全部设置为零
ps->size = 0;//让我们通讯录最初只有0个元素
}

添加通讯录函数AddContact

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
c复制代码//添加通讯录
void AddContact(struct Contact* ps)
{
assert(ps);
if (ps->size == MAX)//通讯录满了就添加不了信息
{
printf("通讯录已满无法添加\n");
}
else
{
printf("请输入名字:");
scanf("%s", ps->data[ps->size].name);//ps->data找到那个数组,
//ps->size找到放在[]里面就是 找到数组里面的某一个元素了,找到后再.name放到名字里面去
printf("请输入电话:");
scanf("%s", ps->data[ps->size].tele);
printf("请输入性别:");
scanf("%s", ps->data[ps->size].sex);
printf("请输入地址:");
scanf("%s", ps->data[ps->size].addr);
printf("请输入年龄:");
scanf("%d", &(ps->data[ps->size].age));//这里和上面不一样,因为上面是字符串,名字就是首地址
//而age是int 所以得取地址
//填好之后size得加加,因为size既是通讯录数组下标,也是人数的标准,添加一人就++;
ps->size++;
printf("添加成功\n");
}
}

显示通讯录函数ShowContact

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
c复制代码//不知道自己操作成没成功就show一下
//显示通讯录
void ShowContact(const struct Contact* ps)//这里为了安全,我们只是看一下里面的内容,不会修改,所以就const
{
assert(ps);
if (!ps->size)
{
printf("通讯录里面没有人,没什么好显示的:\n");
}
else
{
int i = 0;
printf("%-10s\t%-5s\t%-5s\t%-12s\t%-20s\n","名字","年龄","性别","电话","地址");
for (i = 0; i < ps->size; i++)
{
printf("%-10s\t%-5d\t%-5s\t%-12s\t%-20s\n",//为了对齐就把上面的格式拿下来,注意年龄的格式s变成了d
ps->data[i].name,
ps->data[i].age,
ps->data[i].sex,
ps->data[i].tele,
ps->data[i].addr);
}
}
}

image-20210923080753902

找人函数FindByName

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c复制代码//只给本文件内函数用
static int FindByName(const struct Contact* ps, char name[MAX_NAME])
{
assert(ps);
int i = 0;
for (i = 0; i < ps->size; i++)
{
if (0 == strcmp(ps->data[i].name, name))
{
//break;//找到就跳出来
return i;
}
}
return -1;
}

删除指定联系人函数DelContact

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
c复制代码//删除指定的联系人
void DelContact(struct Contact* ps)
{
assert(ps);
if (ps->size == 0)
{
printf("通讯录为空,无法删除.\n");
return 0;
}
char name[MAX_NAME] = {0};
printf("请输入要删除人的姓名:");
scanf("%s", name);
int pos = FindByName(ps,name);//找到把下标拿出来,找不到返回-1
////查找要删除的人在什么位置
////高校做法也就是打工人做法,万物皆遍历
//int i = 0;
//for (i = 0; i < ps->size; i++)
//{
// if (0 == strcmp(ps->data[i].name, name))
// {
// break;//找到就跳出来
// }
//}
//然后再删除
if (pos == -1)//说明他遍历完了也没找到那个名字,也就是说是不存在的
{
printf("要删除的人不存在:");
}
else
{
//删除数据
int j = 0;
for (int j = pos; j < ps->size; j++)
{
ps->data[j] = ps->data[j + 1];//需要删除的数据后面的来补删除数据的空间
}
ps->size--;//删除了,人就减一
printf("删除成功");
}
}

查找联系人函数SearchContact

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
c复制代码//查找联系人  我就是查找不修改,所以要const
void SearchContact(const struct Contact* ps)
{
assert(ps);
char name[MAX_NAME] = { 0 };
printf("请输入 需要查找人的名字:");
scanf("%s",name);
////查找的过程和删除的 查找过程一样 ,这时候再写一样的话就会出现代码冗余,所以我们需要写一个通用的查找功能
//int i = 0;
//for (i = 0; i < ps->size; i++)
//{
// if (0 == strcmp(ps->data[i].name, name))
// {
// break;//找到就跳出来
// }
//}
int pos = FindByName(ps, name);
if (pos == -1)//说明他遍历完了也没找到那个名字,也就是说是不存在的
{
printf("要查找的人不存在:");
}
else
{
//找到就打印
printf("%-10s\t%-5s\t%-5s\t%-12s\t%-20s\n", "名字", "年龄", "性别", "电话", "地址");
printf("%-10s\t%-5d\t%-5s\t%-12s\t%-20s\n",//为了对齐就把上面的格式拿下来,注意年龄的格式s变成了d
ps->data[pos].name,
ps->data[pos].age,
ps->data[pos].sex,
ps->data[pos].tele,
ps->data[pos].addr);
}
}

修改指定联系人函数ModifyContact

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
c复制代码//修改指定联系人
void ModifyContact( struct Contact* ps)
{
assert(ps);
char name[MAX_NAME] = { 0 };
printf("请输入要修改人的名字:");
scanf("%s",name);
int pos = FindByName(ps,name);
if (pos == -1)
{
printf("没有修改人的名字\n");
}
else
{
printf("请输入名字:");
scanf("%s", ps->data[pos].name);//ps->data找到那个数组,
//ps->size找到放在[]里面就是 找到数组里面的某一个元素了,找到后再.name放到名字里面去
printf("请输入电话:");
scanf("%s", ps->data[pos].tele);
printf("请输入性别:");
scanf("%s", ps->data[pos].sex);
printf("请输入地址:");
scanf("%s", ps->data[pos].addr);
printf("请输入年龄:");
scanf("%d", &(ps->data[pos].age));

printf("修改完成:");
}
}

快排通讯录函数(年龄排序)QsortContact

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码//由于中文名在内存中存储方式的问题。所以我们选择年龄排序
static int cmp_peoinfo_age(const void* e1, const void* e2)
{
return ((struct PeoInfo*)e1)->age - ((struct PeoInfo*)e2)->age;
}
//快排通讯录
void QsortContact(struct Contact* ps)
{
assert(ps);
if (ps->size < 2)
{
printf("所需排序人少于2人,无需排序");
return 0;
}
qsort(ps->data, ps->size, sizeof(ps->data[0]), cmp_peoinfo_age);
}

contact.h

通讯录的声明

创建通讯录的一些宏

1
2
3
4
5
c复制代码#define MAX                1000
#define MAX_NAME 20 //为了好修改直接宏定义
#define MAX_TELE 12
#define MAX_SEX 5
#define MAX_ADDR 30

选项(方便删改)

1
2
3
4
5
6
7
8
9
10
11
c复制代码//选项,因为每次输入数字的话还要看看数字的意思,会很麻烦,所以枚举
enum Option
{
EXIT, //0退出
ADD, //1添加
DEL, //2删除
SEARCH, //3查找
MODIFY, //4修改
SHOW, //5查看
SORT //6排序
};

image-20210923080632563

用户信息结构体

1
2
3
4
5
6
7
8
9
10
c复制代码//创建用户信息类型包含姓名,电话,性别,住址,年龄
//然后通讯录就是1000个用户组成的,创建一个数组就好
struct PeoInfo
{
char name[MAX_NAME];
char tele[MAX_TELE];
char sex[MAX_SEX];
char addr[MAX_ADDR];
int age;
};

通讯录结构体

1
2
3
4
5
6
c复制代码//通讯录类型,里面是通讯录信息
struct Contact
{
struct PeoInfo data[MAX];//存放1000个信息
int size;//记录当前个数
};

声明函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c复制代码//声明函数

//初始化通讯录
void InitContact(struct Contact* ps);
//添加通讯录
void AddContact(struct Contact* ps);
//显示通讯录
void ShowContact(const struct Contact* ps);
/*为了通过name查找人,这里用函数写是因为之后删除,查询,修改等都
需要用到find这个功能,写在函数里面的话很多都是重复的会显的很冗余
int FindByName(struct Contact* ps, char name[MAX_NAME]); 这个函数声明就不要声明了,不需要给用户,
我们给用户的功能就那菜单里的那几个,这个函数是为了服务查询名字而出现的,不是为了用户出现,所以不
需要暴露给用户,而且还要加上static,不让其他人用,只给文件 内函数用*/
//删除指定的联系人
void DelContact(struct Contact* ps);
//查找联系人
void SearchContact(const struct Contact* ps);
//修改指定联系人
void ModifyContact(struct Contact* ps);
//排序联系人
void QsortContact(struct Contact* ps);

动态版本

比其他丐版的优势

contact.h

改一:通讯录结构体

1
2
3
4
5
6
7
8
c复制代码//动态版本
//通讯录类型,里面是通讯录信息
struct Contact
{
struct PeoInfo* data;//用指针来维护用户空间
int sz; //记录通讯录当前的元素个数
int capacity ; //通讯录最大容量
};

image-20210923082936339

contact.c

改二:初始化通讯录函数InitContact

1
2
3
4
5
6
7
8
9
10
c复制代码//动态版本
//初始化通讯录
//刚开始的时候通讯录结构体就给了指针但没有指向空间,所以我们得初始化给他
//只有三个人的元素,不够再加呗
void InitContact(struct Contact* ps)
{
ps->sz = 0;//此时有效元素是0
ps->data = (struct PeoInfo*)malloc(DEFAULT_SZ * sizeof(struct PeoInfo));//先给3个元素
ps->capacity = DEFAULT_SZ;
}

image-20210923084747640

改三:增加通讯录函数AddContact

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
c复制代码//动态版本
// 添加通讯录
void AddContact( struct Contact* ps)
{
//当有效元素个数和通讯录最大容量相等时就要扩容了
if (ps->sz == ps->capacity)
{
struct PeoInfo* ptr = (struct PeoInfo*)realloc(ps->data, (ps->capacity + 2) * sizeof(struct PeoInfo));//增加两个元素
if (ptr == NULL)
{
printf("扩容失败\n");
return 0;
}
else
{
ps->data = ptr;//ptr不为空就交给data维护
ps->capacity += 2;//这个不要忘了总容量也要加
printf("扩容成功\n");
}
}
//然后再增加新的信息
printf("请输入名字:");
scanf("%s", ps->data[ps->sz].name);//ps->data找到那个数组,
//ps->size找到放在[]里面就是 找到数组里面的某一个元素了,找到后再.name放到名字里面去
printf("请输入电话:");
scanf("%s", ps->data[ps->sz].tele);
printf("请输入性别:");
scanf("%s", ps->data[ps->sz].sex);
printf("请输入地址:");
scanf("%s", ps->data[ps->sz].addr);
printf("请输入年龄:");
scanf("%d", &(ps->data[ps->sz].age));//这里和上面不一样,因为上面是字符串,名字就是首地址
//而age是int 所以得取地址
//填好之后sz得加加,因为sz既是通讯录数组下标,也是人数的标准,添加一人就++;
(ps->sz)++;
printf("添加成功\n");
}

注意我们在扩容却没有释放,那我们什么时候释放呢,是不是只有程序结束也就是程序退出的时候释放,也就是Exit的时候

所以我们得写个销毁通讯录的函数

添加四:销毁通讯录函数DestroyContact

1
2
3
4
5
6
7
8
9
10
c复制代码//销毁通讯录
void DestroyContact(struct Contact* ps)
{
//释放人员信息空间
free(ps->data);
ps->data = NULL;
//指向的数组都没了,所以sz,capacity也没用了
ps->sz = 0;
ps->capacity = 0;
}

完整代码

test.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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
c复制代码#define _CRT_SECURE_NO_WARNINGS 1

#include"contact.h"


void menu()
{
printf("*******************************\n");
printf("***** 1.add 2.del ***\n");
printf("***** 3.search 4.modify ***\n");
printf("***** 5.show 6.short ***\n");
printf("***** 0.exit ***\n");
printf("*******************************\n");
}

int main()
{
int input = 0;
//int size = 0;//用来记录通讯录有多少人的信息变量
//struct PeoInfo con[MAX];//存放1000个人的信息
/*为了下面函数不重复的写con,&size,就把他们集合在 一起了struct Contact*/
struct Contact con;//con就是通讯录,里面包含1000人信息的数组,和size
InitContact(&con);//初始化通讯录

do//首先让他循环起来
{
menu();//显示菜单
printf("请选择:>");
scanf("%d",&input);
switch (input)
{
case ADD:
AddContact(&con);
break;
case DEL:
DelContact(&con);
break;
case SEARCH:
SearchContact(&con);
break;
case MODIFY:
ModifyContact(&con);
break;
case SHOW:
ShowContact(&con);
break;
case SORT:
QsortContact(&con);
break;
case EXIT:
DestroyContact(&con);
printf("退出通讯录\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}

contact.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
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
c复制代码#define _CRT_SECURE_NO_WARNINGS 1

#include"contact.h"


////静态版本
////初始化通讯录
//void InitContact(struct Contact* ps)
//{
// assert(ps);
// memset(ps->data, 0, sizeof(ps->data));//把data这个空间里全部设置为零
// ps->size = 0;//让我们通讯录最初只有0个元素
//}

//动态版本
//初始化通讯录
//刚开始的时候通讯录结构体就给了指针但没有指向空间,所以我们得初始化给他
//只有三个人的元素,不够再加呗
void InitContact(struct Contact* ps)
{
ps->sz = 0;//此时有效元素是0
ps->data = (struct PeoInfo*)malloc(DEFAULT_SZ * sizeof(struct PeoInfo));//先给3个元素
ps->capacity = DEFAULT_SZ;
}

////静态版本
////添加通讯录
//void AddContact(struct Contact* ps)
//{
// assert(ps);
// if (ps->size == MAX)//通讯录满了就添加不了信息
// {
// printf("通讯录已满无法添加\n");
// }
// else
// {
// printf("请输入名字:");
// scanf("%s", ps->data[ps->size].name);//ps->data找到那个数组,
// //ps->size找到放在[]里面就是 找到数组里面的某一个元素了,找到后再.name放到名字里面去
// printf("请输入电话:");
// scanf("%s", ps->data[ps->size].tele);
// printf("请输入性别:");
// scanf("%s", ps->data[ps->size].sex);
// printf("请输入地址:");
// scanf("%s", ps->data[ps->size].addr);
// printf("请输入年龄:");
// scanf("%d", &(ps->data[ps->size].age));//这里和上面不一样,因为上面是字符串,名字就是首地址
// //而age是int 所以得取地址
// //填好之后size得加加,因为size既是通讯录数组下标,也是人数的标准,添加一人就++;
// ps->size++;
// printf("添加成功\n");
// }
//}

//动态版本
// 添加通讯录
void AddContact( struct Contact* ps)
{
//当有效元素个数和通讯录最大容量相等时就要扩容了
if (ps->sz == ps->capacity)
{
struct PeoInfo* ptr = (struct PeoInfo*)realloc(ps->data, (ps->capacity + 2) * sizeof(struct PeoInfo));//增加两个元素
if (ptr == NULL)
{
printf("扩容失败\n");
return 0;
}
else
{
ps->data = ptr;//ptr不为空就交给data维护
ps->capacity += 2;//这个不要忘了总容量也要加
printf("扩容成功\n");
}
}
//然后再增加新的信息
printf("请输入名字:");
scanf("%s", ps->data[ps->sz].name);//ps->data找到那个数组,
//ps->size找到放在[]里面就是 找到数组里面的某一个元素了,找到后再.name放到名字里面去
printf("请输入电话:");
scanf("%s", ps->data[ps->sz].tele);
printf("请输入性别:");
scanf("%s", ps->data[ps->sz].sex);
printf("请输入地址:");
scanf("%s", ps->data[ps->sz].addr);
printf("请输入年龄:");
scanf("%d", &(ps->data[ps->sz].age));//这里和上面不一样,因为上面是字符串,名字就是首地址
//而age是int 所以得取地址
//填好之后sz得加加,因为sz既是通讯录数组下标,也是人数的标准,添加一人就++;
(ps->sz)++;
printf("添加成功\n");
}

//销毁通讯录
void DestroyContact(struct Contact* ps)
{
//释放人员信息空间
free(ps->data);
ps->data = NULL;
//指向的数组都没了,所以sz,capacity也没用了
ps->sz = 0;
ps->capacity = 0;
}



//不知道自己操作成没成功就show一下
//显示通讯录
void ShowContact(const struct Contact* ps)//这里为了安全,我们只是看一下里面的内容,不会修改,所以就const
{
assert(ps);
if (!ps->sz)
{
printf("通讯录里面没有人,没什么好显示的:\n");
}
else
{
int i = 0;
printf("%-10s\t%-5s\t%-5s\t%-12s\t%-20s\n","名字","年龄","性别","电话","地址");
for (i = 0; i < ps->sz; i++)
{
printf("%-10s\t%-5d\t%-5s\t%-12s\t%-20s\n",//为了对齐就把上面的格式拿下来,注意年龄的格式s变成了d
ps->data[i].name,
ps->data[i].age,
ps->data[i].sex,
ps->data[i].tele,
ps->data[i].addr);
}
}
}
//只给本文件内函数用
static int FindByName(const struct Contact* ps, char name[MAX_NAME])
{
assert(ps);
int i = 0;
for (i = 0; i < ps->sz; i++)
{
if (0 == strcmp(ps->data[i].name, name))
{
//break;//找到就跳出来
return i;
}
}
return -1;
}

//删除指定的联系人
void DelContact(struct Contact* ps)
{
assert(ps);
if (ps->sz == 0)
{
printf("通讯录为空,无法删除.\n");
return 0;
}
char name[MAX_NAME] = {0};
printf("请输入要删除人的姓名:");
scanf("%s", name);
int pos = FindByName(ps,name);//找到把下标拿出来,找不到返回-1
////查找要删除的人在什么位置
////高校做法也就是打工人做法,万物皆遍历
//int i = 0;
//for (i = 0; i < ps->size; i++)
//{
// if (0 == strcmp(ps->data[i].name, name))
// {
// break;//找到就跳出来
// }
//}
//然后再删除
if (pos == -1)//说明他遍历完了也没找到那个名字,也就是说是不存在的
{
printf("要删除的人不存在:");
}
else
{
//删除数据
int j = 0;
for (int j = pos; j < ps->sz; j++)
{
ps->data[j] = ps->data[j + 1];//需要删除的数据后面的来补删除数据的空间
}
ps->sz--;//删除了,人就减一
printf("删除成功");
}
}
//查找联系人 我就是查找不修改,所以要const
void SearchContact(const struct Contact* ps)
{
assert(ps);
char name[MAX_NAME] = { 0 };
printf("请输入 需要查找人的名字:");
scanf("%s",name);
////查找的过程和删除的 查找过程一样 ,这时候再写一样的话就会出现代码冗余,所以我们需要写一个通用的查找功能
//int i = 0;
//for (i = 0; i < ps->size; i++)
//{
// if (0 == strcmp(ps->data[i].name, name))
// {
// break;//找到就跳出来
// }
//}
int pos = FindByName(ps, name);
if (pos == -1)//说明他遍历完了也没找到那个名字,也就是说是不存在的
{
printf("要查找的人不存在:");
}
else
{
//找到就打印
printf("%-10s\t%-5s\t%-5s\t%-12s\t%-20s\n", "名字", "年龄", "性别", "电话", "地址");
printf("%-10s\t%-5d\t%-5s\t%-12s\t%-20s\n",//为了对齐就把上面的格式拿下来,注意年龄的格式s变成了d
ps->data[pos].name,
ps->data[pos].age,
ps->data[pos].sex,
ps->data[pos].tele,
ps->data[pos].addr);
}
}

//修改指定联系人
void ModifyContact( struct Contact* ps)
{
assert(ps);
char name[MAX_NAME] = { 0 };
printf("请输入要修改人的名字:");
scanf("%s",name);
int pos = FindByName(ps,name);
if (pos == -1)
{
printf("没有修改人的名字\n");
}
else
{
printf("请输入名字:");
scanf("%s", ps->data[pos].name);//ps->data找到那个数组,
//ps->size找到放在[]里面就是 找到数组里面的某一个元素了,找到后再.name放到名字里面去
printf("请输入电话:");
scanf("%s", ps->data[pos].tele);
printf("请输入性别:");
scanf("%s", ps->data[pos].sex);
printf("请输入地址:");
scanf("%s", ps->data[pos].addr);
printf("请输入年龄:");
scanf("%d", &(ps->data[pos].age));

printf("修改完成:");
}
}
//由于中文名在内存中存储方式的问题。所以我们选择年龄排序
static int cmp_peoinfo_age(const void* e1, const void* e2)
{
return ((struct PeoInfo*)e1)->age - ((struct PeoInfo*)e2)->age;
}
//快排通讯录
void QsortContact(struct Contact* ps)
{
assert(ps);
if (ps->sz < 2)
{
printf("所需排序人少于2人,无需排序");
return 0;
}
qsort(ps->data, ps->sz, sizeof(ps->data[0]), cmp_peoinfo_age);
}

contact.h

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
c复制代码#pragma once
#include<stdio.h>
#include<string.h>
#include<assert.h>
#include<stdlib.h>

#define MAX 1000
#define MAX_NAME 20 //为了好修改直接宏定义
#define MAX_TELE 12
#define MAX_SEX 5
#define MAX_ADDR 30
#define DEFAULT_SZ 3

//选项,因为每次输入数字的话还要看看数字的意思,会很麻烦,所以枚举
enum Option
{
EXIT, //0退出
ADD, //1添加
DEL, //2删除
SEARCH, //3查找
MODIFY, //4修改
SHOW, //5查看
SORT //6排序
};

//创建用户信息类型包含姓名,电话,性别,住址,年龄
//然后通讯录就是1000个用户组成的,创建一个数组就好
struct PeoInfo
{
char name[MAX_NAME];
char tele[MAX_TELE];
char sex[MAX_SEX];
char addr[MAX_ADDR];
int age;
};
////静态版本
////通讯录类型,里面是通讯录信息
//struct Contact
//{
// struct PeoInfo data[MAX];//存放1000个信息
// int size;//记录当前个数
//};

//动态版本
//通讯录类型,里面是通讯录信息
struct Contact
{
struct PeoInfo* data;//用指针来维护用户空间
int sz; //记录通讯录当前的元素个数
int capacity ; //通讯录最大容量
};

//声明函数

//初始化通讯录
void InitContact(struct Contact* ps);
//添加通讯录
void AddContact(struct Contact* ps);
//显示通讯录
void ShowContact(const struct Contact* ps);
/*为了通过name查找人,这里用函数写是因为之后删除,查询,修改等都
需要用到find这个功能,写在函数里面的话很多都是重复的会显的很冗余
int FindByName(struct Contact* ps, char name[MAX_NAME]); 这个函数声明就不要声明了,不需要给用户,
我们给用户的功能就那菜单里的那几个,这个函数是为了服务查询名字而出现的,不是为了用户出现,所以不
需要暴露给用户,而且还要加上static,不让其他人用,只给文件 内函数用*/
//删除指定的联系人
void DelContact(struct Contact* ps);
//查找联系人
void SearchContact(const struct Contact* ps);
//修改指定联系人
void ModifyContact(struct Contact* ps);
//排序联系人
void QsortContact(struct Contact* ps);
//销毁通讯录函数
void DestroyContact(struct Contact* ps);

跑图

image-20210923094800813

image-20210923101309076

image-20210923101705704

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

比反射更强大的技术,内省技术

发表于 2021-11-25

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

在学习Java过程中,总是层层递进的。差不多从数据类型到IO、异常就算基础部分(不同书不一样)。之后就是Java的进阶,一般我们都会学到泛型、反射之类的。今天要说的东西,就和反射有关系,叫做“内省技术”。“内省技术”是基于反射技术的,提供了更多的便于操作JavaBean的API。一般的话,我们只有学到JavaWeb才会称呼JavaBean,由此可知“内省技术”属于JavaWeb的内容(搞笑逻辑,别介意)。

1、什么是内省

前面已经说了,内省就是JDK提供的JavaBean操作的API,基于反射技术。

2、如何用代码实现内省

内省使用到的类就是Introspector,说到底学习内省就是学习类Introspecteor类的使用。

因为内省技术还是比较简单的,下面就直接用代码举例。先创建一个Person类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
arduino复制代码public class Person{

private String name;

private String sex;

/**
* 省略get、set方法
*/

public Person(){

}

}

类准备好了就可以开始使用内省了。

第一步:获取BeanInfo对象

1
2
3
4
5
6
7
csharp复制代码@Test
public void demo(){

//调用Introspector中的getBeanInfo()方法,传入要获取信息的类的Class对象
BeanInfo beanInfo = Introspector.getBeanInfo(Person.class);

}

上面的beanInfo就是Person类的完整信息。

第二步:获得方法描述器和属性描述器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码@Test
public void demo(){

//调用Introspector中的getBeanInfo()方法,传入要获取信息的类的Class对象
BeanInfo beanInfo = Introspector.getBeanInfo(Person.class);


/*********************分割线****************************/

//获取方法描述器
MethodDescriptor[] methodDescriptors = beanInfo.getMethodDescriptors();

//获取属性描述器,调用getPropertyDescriptors方法,返回一个PropertyDescriptor数组
//其中一个PropertyDescriptor代表一个属性
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();

}

之后就可以进行相应的操作了。学过JavaWeb应该都知道,表单有个自动封装数据的功能。其实它的实现就是内省技术,具体就举一个类似的例子。

要求:把要赋值的数据穿入Map,然后使用方法,把Map中的数据封装到JavaBean当中。

我们话不多说,直接开始写。依次是:创建Map->给Map赋值->创建对象->把Map中的值传给对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typescript复制代码@Test
public void demo(){

//创建一个Map
Map<String, String> p1 = new HashMap<String, String>();

//给Map赋值,其中key为Person中的变量名,value为要赋的值
p1.put("name", "zack");
p1.put("sex", "male");

//创建Person对象
Person person = new Person();

//给对象赋值,待实现方法
setData2Object(p1, person);
}

上面使用了一个setData2Object()方法,现在我们来实现一下这个方法:

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
arduino复制代码private static void setData2Object(Map<String, String> map, Object obj) 
throws Exception {
//获取BeanInfo
BeanInfo beanInfo = Introspector.getBeanInfo(person.getClass());

//获取属性描述器
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();

//遍历描述器
for (PropertyDescriptor descriptor:propertyDescriptors) {

//通过属性名去Map中找对应的key
String name = descriptor.getName();

//找到对应的key后赋值
if (map.containsKey(name)){
String value = map.get(name);

//通过属性描述器获得写入属性的方法
Method writeMethod = descriptor.getWriteMethod();

//利用反射设置Value
writeMethod.invoke(person, value);
}
}
}

里面有一个东西前面没讲,就是通过属性描述器获得写入属性的方法。在api中,属性描述器有两个方法getWriteMethod、getReadMethod。简单来说就是获取get、set方法。之后利用反射就可以了。

再补充一句,内省属性描述器的个数不是通过属性的个数来算的,而是通过get、set方法。如:setAge、getName就是把前面的set和get去掉,获取一个属性名。

​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

C语言深度剖析知识点

发表于 2021-11-25

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」

  1. 在win中双击的本质是运行程序,将我们的程序加载到内存当中。

  2. 任何程序在被运行之前都必须加载到内存当中(为了快),那没有加载之前呢,程序没有加载之前在硬盘当中。image-20210820145620874

什么是变量(是什么)

在内存中开辟特定大小的空间,用来保存数据

关键字:内存

本质上变量是在程序运行的时候开辟,或者有些变量是在编译的时候就确定了

一般我们写的代码变量都是在程序运行的时候确定的

如何定义变量(怎么用)

int x = 10;

…..

类型 变量名 = 默认值

为什么定义变量(为什么)

计算机是为了解决人计算力不足的问题而诞生的,即计算机是为了进行计算的。而计算就需要数据。

而计算,任何时刻不是数据立马就被计算的,因此需要东西来暂时存储,所以变量就是用来暂时保存数据,等待之后处理。

最快的关键字register

image-20210820161809631

寄存器

寄存器与其说是存储器,其实更像是 CPU 本身的一部分,只能存放极其有限的信息,但是速度非常快,和 CPU 同步。

高速缓存(CPU Cache)

Cache存储器,电脑中为高速缓冲存储器,是位于CPU和主存储器DRAM(Dynamic Random Access Memory)之间,规模较小,但速度很高的存储器,通常由SRAM(Static Random Access Memory 静态存储器)组成

内存(memory)

使用 DRAM(Dynamic Random Access Memory,动态随机存取存储器)的芯片,比起 SRAM 来说,它的密度更高,有更大的容量,而且它也比 SRAM 芯片便宜不少。

硬盘

如 SSD(Solid-state drive 或 Solid-state disk,固态硬盘)、HDD(Hard Disk Drive,硬盘)。

什么样的变量可以使用register呢

  1. 局部的(全局会导致CPU寄存器被长时间占用)
  2. 不会被写入的(写入需要写回内存,后续还要读取检测的话,register的意义何在)
  3. 高频被读取的(提高效率所在)
  4. 如果要使用,请不要大量使用,因为寄存器数量有限。

有一个超级重要的一点,就是register修饰的变量,不能取地址(因为已经放到寄存器中了,地址是内存相关的概念)

外部声明extern

  1. 声明没有开辟空间,
  2. =100 是赋值或初始化
  3. 所有的变量声明的时候,不能设置初始值
1
c复制代码例如extern int g_val;绝对不可以写成extern int g_val = 100;

头文件.h

头文件怎么写才是最最省事的

#pragma once //这个是避免重调用

  1. C头文件
  2. 所有的变量的声明
  3. 所有的函数的声明
  4. #define 类型typedef,struct

#include<stdio.h>

#include<windows.h>

静态变量static

static修饰局部变量

更改的是生命周期,作用域不变

image-20210821215721123

image-20210821220245289

static修饰全局变量

static修饰全局变量,该变量只能在本文件内访问,不能被外部文件直接访问

image-20210821225646389

image-20210821231545236

image-20210821231946026

image-20210821232916290

那他变的是什么,还是全局变量吗?是的 他还是全局变量,他是随着整个程序下载到内存中,变成进程之后,这个全局变量一直在,那他本质上是什么,本质上说明生命周期没有变,改变的是作用域。

二进制快速转化口诀

十转二

2^0 <-> 1

2^1 <-> 10

2^2 <-> 100

2^3 <-> 1000

1后面跟n个比特位,就是2^n

1
2
3
4
c复制代码67   
64+2+1
2^6+2^1+2^0
0000 0000 0000 0000 0000 0000 0100 0011

二转十

1
2
3
4
c复制代码0000 0000 0000 0000 0000 0010 0100 0011
2^9+2^6+2^1+2^0
512+64+2+1
579

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

什么是内存函数? 内存函数

发表于 2021-11-25

「这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战」

内存函数

memcpy内存拷贝

  • 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
  • 这个函数在遇到 ‘\0’ 的时候并不会停下来。
  • 如果source和destination有任何的重叠,复制的结果都是未定义的。

原格式

image-20210912150415470

分析

**字面上意思只要是内存里面的东西就都可以进行拷贝,所以就打破了字符串拷贝的魔咒,什么类型都可以进行拷贝,那就不需要想来,肯定回和万能类型(通用类型指针-无类型指针)void^*^有关,因为当时做qsort还是印象深刻的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c复制代码/*num是几个字节的意思*/
void* my_memcpy(void* dest, const void* src, size_t num)
{
assert(dest && src);
void* ret = dest;
while (num--)
{
//和void*连用char*,分成最小然后一个一个传
*(char*)dest = *(char*)src;
((char*)dest)++;//void*无类型不好直接加加,就强转char*再加加
((char*)src)++;
}
return ret;
}

内存拷贝的问题

1.内存相关连的话,就会拷贝错误

image-20210912174618496

那你怎么解决内存相关连还不会有上面的错误,正面赋值交集的内存空间会被操作两次,就会改变原来的值,那我们怎么做呢,如果从后面来呢,前面操作两次会把后面的变了,那就先把后面的拿走赋值,不就间接的改变了原来会变的情况了吗,所以这样上面的代码就得修改了,这是朝后面拷贝的情况,如果提目是朝前面拷贝的话,是不是从后面来就有问题了,反而从前面来会比较完美,所以我们得两种情况都得考虑到

所以为了解决重叠拷贝的问题就有了memmove这个函数

2.内存不够了还要朝里面拷贝直接程序挂了

image-20210912175703527

memmove内存重叠拷贝

用来处理内存重叠的情况

C语言规定

memcpy 只要处理内存不重叠的拷贝就可以

memmove 处理重叠内存拷贝

我们重写memcpy的代码是满足C语言要求的,在vs这个编译器中memcpy实际上是超额完成任务了,他的效果已经和memmove效果一样了

image-20210912201352859

你会发现他们跑出来的效果 是一样的,所以上面那个测试我就是用我自己的代码测试的(已经达到C语言的标准了)

我们再精细点就是memmove的内容了

原格式

image-20210912195704561

分析

image-20210912200345318

image-20210912210111460

image-20210912210718304

image-20210912212544483

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
c复制代码/*num是几个字节的意思*/
void* my_memmove(void* dest,const void* src, size_t num)//memmove和memcpy的参数是一样的
{
assert(dest && src);
void* ret = dest;
if (dest < src)
{
while (num--)
{
//sre内存从前向后拷贝
//和void*连用char*,分成最小然后一个一个传
*(char*)dest = *(char*)src;
((char*)dest)++;//void*无类型不好直接加加,就强转char*再加加
((char*)src)++;
}
}
else
{
while (num--)
{
//sre内存从后向前拷贝
//和void*连用char*,分成最小然后一个一个传
*((char*)dest+num) = *((char*)src+num);
//((char*)dest)++;//void*无类型不好直接加加,就强转char*再加加
//((char*)src)++;
}
}
return ret;
}

image-20210912215713723

memset内存设置

将缓冲区设置为指定的字符。

原格式

image-20210912224129279

分析

image-20210913005714774

image-20210913012002615

1
2
3
4
5
6
7
8
9
10
11
12
13
c复制代码//变量c代表的是字符的ASCII码
void* my_memset(void* dest, int c, size_t count)
{
const unsigned char uc = c; //把c转成字符,且不可变用const
void* ret = dest;
int i = 0;
for (i = 0; i < count; i++)
{
*(char*)dest = uc;
((char*)dest)++;
}
return ret;
}

memcmp内存比较

和strcmp相似,只不过一个是比较字符串,一个比较内存,由于不知道什么类型,所以后面有字节个数限制,准确的说应该和strncmp相似,因为后面都有一个个数的参数

原格式

image-20210912222610487

分析

基本和字符串比较一样,就是变成了内存比较罢了

image-20210912233051911

image-20210912233234287

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c复制代码//buf1内存里的内容比buf2内存里的内容大就>0,反之<0
int my_memcmp(const void* buf1, const void* buf2, size_t count)
{
assert(buf1 && buf2);
while (--count && *(char*)buf1 == *(char*)buf2)//这个先减减就是细节
{
((char*)buf1)++;
((char*)buf2)++;
}
if (*(char*)buf1 - *(char*)buf2 > 0)
return 1;
if (*(char*)buf1 - *(char*)buf2 < 0)
return -1;
return 0;
}

给你count不要乱超,因为他操作的是内存,没有字符串补\0的功能

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java 项目中使用 Resilience4j 框架实现异步

发表于 2021-11-25

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

到目前为止,在本系列中,我们已经了解了 Resilience4j 及其 Retry 和 RateLimiter 模块。在本文中,我们将通过 TimeLimiter 继续探索 Resilience4j。我们将了解它解决了什么问题,何时以及如何使用它,并查看一些示例。

代码示例

本文附有 GitHub 上的工作代码示例。

什么是 Resilience4j?

请参阅上一篇文章中的描述,快速了解 Resilience4j 的一般工作原理。

什么是限时?

对我们愿意等待操作完成的时间设置限制称为时间限制。如果操作没有在我们指定的时间内完成,我们希望通过超时错误收到通知。

有时,这也称为“设定最后期限”。

我们这样做的一个主要原因是确保我们不会让用户或客户无限期地等待。不提供任何反馈的缓慢服务可能会让用户感到沮丧。

我们对操作设置时间限制的另一个原因是确保我们不会无限期地占用服务器资源。我们在使用 Spring 的 @Transactional 注解时指定的 timeout 值就是一个例子——在这种情况下,我们不想长时间占用数据库资源。

什么时候使用 Resilience4j TimeLimiter?

Resilience4j 的 TimeLimiter 可用于设置使用 CompleteableFutures 实现的异步操作的时间限制(超时)。

Java 8 中引入的 CompletableFuture 类使异步、非阻塞编程变得更容易。可以在不同的线程上执行慢速方法,释放当前线程来处理其他任务。 我们可以提供一个当 slowMethod() 返回时执行的回调:

1
2
3
4
5
6
7
java复制代码int slowMethod() {
// time-consuming computation or remote operation
return 42;
}

CompletableFuture.supplyAsync(this::slowMethod)
.thenAccept(System.out::println);

这里的 slowMethod() 可以是一些计算或远程操作。通常,我们希望在进行这样的异步调用时设置时间限制。我们不想无限期地等待 slowMethod() 返回。例如,如果 slowMethod() 花费的时间超过一秒,我们可能想要返回先前计算的、缓存的值,甚至可能会出错。

在 Java 8 的 CompletableFuture 中,没有简单的方法来设置异步操作的时间限制。CompletableFuture 实现了 Future 接口,Future 有一个重载的 get() 方法来指定我们可以等待多长时间:

1
2
3
4
java复制代码CompletableFuture<Integer> completableFuture = CompletableFuture
.supplyAsync(this::slowMethod);
Integer result = completableFuture.get(3000, TimeUnit.MILLISECONDS);
System.out.println(result);

但是这里有一个问题—— get() 方法是一个阻塞调用。所以它首先违背了使用 CompletableFuture 的目的,即释放当前线程。

这是 Resilience4j 的 TimeLimiter 解决的问题——它让我们在异步操作上设置时间限制,同时保留在 Java 8 中使用 CompletableFuture 时非阻塞的好处。

CompletableFuture 的这种限制已在 Java 9 中得到解决。我们可以在 Java 9 及更高版本中使用 CompletableFuture 上的 orTimeout() 或 completeOnTimeout() 等方法直接设置时间限制。然而,凭借 Resilience4J 的 指标 和 事件,与普通的 Java 9 解决方案相比,它仍然提供了附加值。

Resilience4j TimeLimiter 概念

TimeLimiter支持 Future 和 CompletableFuture。但是将它与 Future 一起使用相当于 Future.get(long timeout, TimeUnit unit)。因此,我们将在本文的其余部分关注 CompletableFuture。

与其他 Resilience4j 模块一样,TimeLimiter 的工作方式是使用所需的功能装饰我们的代码 - 如果在这种情况下操作未在指定的 timeoutDuration 内完成,则返回 TimeoutException。

我们为 TimeLimiter 提供 timeoutDuration、ScheduledExecutorService 和异步操作本身,表示为 CompletionStage 的 Supplier。它返回一个 CompletionStage 的装饰 Supplier。

在内部,它使用调度器来调度一个超时任务——通过抛出一个 TimeoutException 来完成 CompletableFuture 的任务。如果操作先完成,TimeLimiter 取消内部超时任务。

除了 timeoutDuration 之外,还有另一个与 TimeLimiter 关联的配置 cancelRunningFuture。此配置仅适用于 Future 而不适用于 CompletableFuture。当超时发生时,它会在抛出 TimeoutException 之前取消正在运行的 Future。

使用 Resilience4j TimeLimiter 模块

TimeLimiterRegistry、TimeLimiterConfig 和 TimeLimiter 是 resilience4j-timelimiter 的主要抽象。

TimeLimiterRegistry 是用于创建和管理 TimeLimiter 对象的工厂。

TimeLimiterConfig 封装了 timeoutDuration 和 cancelRunningFuture 配置。每个 TimeLimiter 对象都与一个 TimeLimiterConfig 相关联。

TimeLimiter 提供辅助方法来为 Future 和 CompletableFuture Suppliers 创建或执行装饰器。

让我们看看如何使用 TimeLimiter 模块中可用的各种功能。我们将使用与本系列前几篇文章相同的示例。假设我们正在为一家航空公司建立一个网站,以允许其客户搜索和预订航班。我们的服务与 FlightSearchService 类封装的远程服务对话。

第一步是创建一个 TimeLimiterConfig:

1
java复制代码TimeLimiterConfig config = TimeLimiterConfig.ofDefaults();

这将创建一个 TimeLimiterConfig,其默认值为 timeoutDuration (1000ms) 和 cancelRunningFuture (true)。

假设我们想将超时值设置为 2s 而不是默认值:

1
2
3
java复制代码TimeLimiterConfig config = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(2))
.build();

然后我们创建一个 TimeLimiter:

1
2
3
java复制代码TimeLimiterRegistry registry = TimeLimiterRegistry.of(config);

TimeLimiter limiter = registry.timeLimiter("flightSearch");

我们想要异步调用
FlightSearchService.searchFlights(),它返回一个 List<Flight>。让我们将其表示为 Supplier<CompletionStage<List<Flight>>>:

1
2
3
java复制代码Supplier<List<Flight>> flightSupplier = () -> service.searchFlights(request);
Supplier<CompletionStage<List<Flight>>> origCompletionStageSupplier =
() -> CompletableFuture.supplyAsync(flightSupplier);

然后我们可以使用 TimeLimiter 装饰 Supplier:

1
2
3
4
java复制代码ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor();
Supplier<CompletionStage<List<Flight>>> decoratedCompletionStageSupplier =
limiter.decorateCompletionStage(scheduler, origCompletionStageSupplier);

最后,让我们调用装饰的异步操作:

1
2
3
4
5
6
7
8
java复制代码decoratedCompletionStageSupplier.get().whenComplete((result, ex) -> {
if (ex != null) {
System.out.println(ex.getMessage());
}
if (result != null) {
System.out.println(result);
}
});

以下是成功飞行搜索的示例输出,其耗时少于我们指定的 2 秒 timeoutDuration:

1
2
3
4
5
shell复制代码Searching for flights; current time = 19:25:09 783; current thread = ForkJoinPool.commonPool-worker-3

Flight search successful

[Flight{flightNumber='XY 765', flightDate='08/30/2020', from='NYC', to='LAX'}, Flight{flightNumber='XY 746', flightDate='08/30/2020', from='NYC', to='LAX'}] on thread ForkJoinPool.commonPool-worker-3

这是超时的航班搜索的示例输出:

1
2
3
4
5
shell复制代码Exception java.util.concurrent.TimeoutException: TimeLimiter 'flightSearch' recorded a timeout exception on thread pool-1-thread-1 at 19:38:16 963

Searching for flights; current time = 19:38:18 448; current thread = ForkJoinPool.commonPool-worker-3

Flight search successful at 19:38:18 461

上面的时间戳和线程名称表明,即使异步操作稍后在另一个线程上完成,调用线程也会收到 TimeoutException。

如果我们想创建一个装饰器并在代码库的不同位置重用它,我们将使用decorateCompletionStage()。如果我们想创建它并立即执行 Supplier<CompletionStage>,我们可以使用 executeCompletionStage() 实例方法代替:

1
2
java复制代码CompletionStage<List<Flight>> decoratedCompletionStage =  
limiter.executeCompletionStage(scheduler, origCompletionStageSupplier);

TimeLimiter 事件

TimeLimiter 有一个 EventPublisher,它生成 TimeLimiterOnSuccessEvent、TimeLimiterOnErrorEvent 和 TimeLimiterOnTimeoutEvent 类型的事件。我们可以监听这些事件并记录它们,例如:

1
2
3
4
5
6
7
java复制代码TimeLimiter limiter = registry.timeLimiter("flightSearch");

limiter.getEventPublisher().onSuccess(e -> System.out.println(e.toString()));

limiter.getEventPublisher().onError(e -> System.out.println(e.toString()));

limiter.getEventPublisher().onTimeout(e -> System.out.println(e.toString()));

示例输出显示了记录的内容:

1
2
3
4
5
shell复制代码2020-08-07T11:31:48.181944: TimeLimiter 'flightSearch' recorded a successful call.

... other lines omitted ...

2020-08-07T11:31:48.582263: TimeLimiter 'flightSearch' recorded a timeout exception.

TimeLimiter 指标

TimeLimiter 跟踪成功、失败和超时的调用次数。

首先,我们像往常一样创建 TimeLimiterConfig、TimeLimiterRegistry 和 TimeLimiter。然后,我们创建一个 MeterRegistry 并将 TimeLimiterRegistry 绑定到它:

1
2
3
java复制代码MeterRegistry meterRegistry = new SimpleMeterRegistry();
TaggedTimeLimiterMetrics.ofTimeLimiterRegistry(registry)
.bindTo(meterRegistry);

运行几次限时操作后,我们显示捕获的指标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码Consumer<Meter> meterConsumer = meter -> {
String desc = meter.getId().getDescription();
String metricName = meter.getId().getName();
String metricKind = meter.getId().getTag("kind");
Double metricValue =
StreamSupport.stream(meter.measure().spliterator(), false)
.filter(m -> m.getStatistic().name().equals("COUNT"))
.findFirst()
.map(Measurement::getValue)
.orElse(0.0);
System.out.println(desc + " - " +
metricName +
"(" + metricKind + ")" +
": " + metricValue);
};
meterRegistry.forEachMeter(meterConsumer);

这是一些示例输出:

1
2
3
4
5
shell复制代码The number of timed out calls - resilience4j.timelimiter.calls(timeout): 6.0

The number of successful calls - resilience4j.timelimiter.calls(successful): 4.0

The number of failed calls - resilience4j.timelimiter.calls(failed): 0.0

在实际应用中,我们会定期将数据导出到监控系统并在仪表板上进行分析。

实施时间限制时的陷阱和良好实践

通常,我们处理两种操作 - 查询(或读取)和命令(或写入)。对查询进行时间限制是安全的,因为我们知道它们不会改变系统的状态。我们看到的 searchFlights() 操作是查询操作的一个例子。

命令通常会改变系统的状态。bookFlights() 操作将是命令的一个示例。在对命令进行时间限制时,我们必须记住,当我们超时时,该命令很可能仍在运行。例如,bookFlights() 调用上的 TimeoutException 并不一定意味着命令失败。

在这种情况下,我们需要管理用户体验——也许在超时时,我们可以通知用户操作花费的时间比我们预期的要长。然后我们可以查询上游以检查操作的状态并稍后通知用户。

结论

在本文中,我们学习了如何使用 Resilience4j 的 TimeLimiter 模块为异步、非阻塞操作设置时间限制。我们通过一些实际示例了解了何时使用它以及如何配置它。

您可以使用 GitHub 上的代码演示一个完整的应用程序来说明这些想法。


本文译自:
reflectoring.io/time-limiti…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Xv6 Lab lazy page allocation

发表于 2021-11-25

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」

Meaning Unknown's Head Image

Lab: xv6 lazy page allocation

pdos.csail.mit.edu/6.S081/2020…

新的 2020 版哦。

1
2
3
sh复制代码$ git fetch
$ git checkout lazy
$ make clean

Eliminate allocation from sbrk()

这道题就是把 sys_sbrk 里的 growproc 调用删了,等用到的时候再去分配内存。如果是空间减小,要取消分配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码uint64
sys_sbrk(void)
{
int addr;
int n;
struct proc *p = myproc(); //(+)

if(argint(0, &n) < 0)
return -1;
addr = p->sz; // old sz
p->sz += n;
if (n < 0) { // 空间减小: 取消分配
uvmdealloc(p->pagetable, addr, p->sz);
}
return addr;
}

Lazy allocation

在 vm.c 里面实现惰性分配(莫忘在 defs.h 中声明函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
c复制代码#include "spinlock.h" //(+)
#include "proc.h" //(+)

// lazy allocation memory va for proc p: handle page-fault.
// return allocated memory (pa), 0 for failed
uint64 lazyalloc(struct proc * p, uint64 va){
if(va >= p->sz || va < PGROUNDUP(p->trapframe->sp)){
return 0;
}
char * mem;
uint64 a = PGROUNDDOWN(va);
mem = kalloc();
if(mem == 0){
return 0;
}
memset(mem, 0, PGSIZE);
if(mappages(p->pagetable, a, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
kfree(mem);
return 0;
}

return (uint64)mem;
}

在 usertrap (trap.c) 里处理缺页错误,尝试惰性分配(掉上面写的那个函数,失败就杀掉进程):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
c复制代码void
usertrap(void)
{
...
if(r_scause() == 8){
// system call
...
} else if((which_dev = devintr()) != 0){
// ok
} else if((r_scause() == 13) || (r_scause() == 15)){ // page fault: (+)
if (lazyalloc(myproc(), r_stval()) <= 0) {
p->killed = 1;
}
} else {
...
}
...
}

最后改一点点细节,把各种缺页会爆出的 panic 干掉(vm.c里)。以前这些情况正常是不会发生的,但现在惰性分配会带来缺页,所以要告诉操作系统遇到这些事情时 don’t panic,继续往下跑就行了:

(这里的代码我懒得改结构就上goto了,但你应该去改if结构,而不是goto)

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
c复制代码// vm.c
...

//(+)
uint64
walkaddr(pagetable_t pagetable, uint64 va)
{
...

if(pte == 0)
goto lzac;
if((*pte & PTE_V) == 0)
goto lzac;
if((*pte & PTE_U) == 0)
goto lzac;
pa = PTE2PA(*pte);

if (0) {
lzac:
if ((pa = lazyalloc(myproc(), va)) <= 0)
pa = 0;
}

return pa;
}

int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
...
for(;;){
...
if(*pte & PTE_V)
// panic("remap");
;
...
}

void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
...
for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
if((pte = walk(pagetable, a, 0)) == 0) {
// panic("uvmunmap: walk");
continue;
}
if((*pte & PTE_V) == 0) {
// panic("uvmunmap: not mapped");
continue;
}
...
}
}

int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
...
for(i = 0; i < sz; i += PGSIZE){
if((pte = walk(old, i, 0)) == 0){
// panic("uvmcopy: pte should exist");
continue;
}
if((*pte & PTE_V) == 0){
// panic("uvmcopy: page not present");
continue;
}
...
}
...
}

测试

写的时候可以按题目跑这些测试:

1
2
3
4
sh复制代码xv6-labs-2020 $ make qemu
$ echo hi
$ lazytests
$ usertests

diff

详细的 git diff (可以用来做 patch 喔):

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
diff复制代码diff --git a/kernel/defs.h b/kernel/defs.h
index 4b9bbc0..7ff16c4 100644
--- a/kernel/defs.h
+++ b/kernel/defs.h
@@ -171,6 +171,7 @@ uint64 walkaddr(pagetable_t, uint64);
int copyout(pagetable_t, uint64, char *, uint64);
int copyin(pagetable_t, char *, uint64, uint64);
int copyinstr(pagetable_t, char *, uint64, uint64);
+uint64 lazyalloc(struct proc * p, uint64 va);

// plic.c
void plicinit(void);
diff --git a/kernel/sysproc.c b/kernel/sysproc.c
index e8bcda9..b799722 100644
--- a/kernel/sysproc.c
+++ b/kernel/sysproc.c
@@ -43,12 +43,15 @@ sys_sbrk(void)
{
int addr;
int n;
+ struct proc *p = myproc();

if(argint(0, &n) < 0)
return -1;
- addr = myproc()->sz;
- if(growproc(n) < 0)
- return -1;
+ addr = p->sz; // old sz
+ p->sz += n;
+ if (n < 0) {
+ uvmdealloc(p->pagetable, addr, p->sz);
+ }
return addr;
}

diff --git a/kernel/trap.c b/kernel/trap.c
index a63249e..fc08231 100644
--- a/kernel/trap.c
+++ b/kernel/trap.c
@@ -67,6 +67,11 @@ usertrap(void)
syscall();
} else if((which_dev = devintr()) != 0){
// ok
+ } else if((r_scause() == 13) || (r_scause() == 15)){ // page fault
+ // lazy allocation
+ if (lazyalloc(myproc(), r_stval()) <= 0) {
+ p->killed = 1;
+ }
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
diff --git a/kernel/vm.c b/kernel/vm.c
index bccb405..f3235c3 100644
--- a/kernel/vm.c
+++ b/kernel/vm.c
@@ -5,6 +5,8 @@
#include "riscv.h"
#include "defs.h"
#include "fs.h"
+#include "spinlock.h"
+#include "proc.h"

/*
* the kernel's page table.
@@ -102,12 +104,19 @@ walkaddr(pagetable_t pagetable, uint64 va)

pte = walk(pagetable, va, 0);
if(pte == 0)
- return 0;
+ goto lzac;
if((*pte & PTE_V) == 0)
- return 0;
+ goto lzac;
if((*pte & PTE_U) == 0)
- return 0;
+ goto lzac;
pa = PTE2PA(*pte);
+
+ if (0) {
+lzac:
+ if ((pa = lazyalloc(myproc(), va)) <= 0)
+ pa = 0;
+ }
+
return pa;
}

@@ -157,7 +166,8 @@ mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
if((pte = walk(pagetable, a, 1)) == 0)
return -1;
if(*pte & PTE_V)
- panic("remap");
+ // panic("remap");
+ ;
*pte = PA2PTE(pa) | perm | PTE_V;
if(a == last)
break;
@@ -180,10 +190,14 @@ uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
panic("uvmunmap: not aligned");

for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
- if((pte = walk(pagetable, a, 0)) == 0)
- panic("uvmunmap: walk");
- if((*pte & PTE_V) == 0)
- panic("uvmunmap: not mapped");
+ if((pte = walk(pagetable, a, 0)) == 0) {
+ // panic("uvmunmap: walk");
+ continue;
+ }
+ if((*pte & PTE_V) == 0) {
+ // panic("uvmunmap: not mapped");
+ continue;
+ }
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
if(do_free){
@@ -314,10 +328,14 @@ uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
char *mem;

for(i = 0; i < sz; i += PGSIZE){
- if((pte = walk(old, i, 0)) == 0)
- panic("uvmcopy: pte should exist");
- if((*pte & PTE_V) == 0)
- panic("uvmcopy: page not present");
+ if((pte = walk(old, i, 0)) == 0){
+ // panic("uvmcopy: pte should exist");
+ continue;
+ }
+ if((*pte & PTE_V) == 0){
+ // panic("uvmcopy: page not present");
+ continue;
+ }
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
if((mem = kalloc()) == 0)
@@ -440,3 +458,40 @@ copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
return -1;
}
}
+
+
+// lazy allocation memory va for proc p: handle page-fault.
+// return allocated memory (pa), 0 for failed
+uint64 lazyalloc(struct proc * p, uint64 va){
+#define lazyalloc_debug 0
+#define lazyalloc_warn(info) { \
+ printf("lazyalloc(): %s", info); \
+ printf(" scause %p pid=%d\n", r_scause(), p->pid); \
+ printf(" sepc=%p stval=%p\n", r_sepc(), r_stval()); \
+}
+ if(va >= p->sz || va < PGROUNDUP(p->trapframe->sp)){
+ #if lazyalloc_debug
+ lazyalloc_warn("vm addr higher then any allocated with sbrk\n");
+ #endif
+ return 0;
+ }
+ char * mem;
+ uint64 a = PGROUNDDOWN(va);
+ mem = kalloc();
+ if(mem == 0){
+ #if lazyalloc_debug
+ lazyalloc_warn("kalloc() == 0\n");
+ #endif
+ return 0;
+ }
+ memset(mem, 0, PGSIZE);
+ if(mappages(p->pagetable, a, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
+ #if lazyalloc_debug
+ lazyalloc_warn("mappages() != 0\n");
+ #endif
+ kfree(mem);
+ return 0;
+ }
+
+ return (uint64)mem;
+}

结果

最后 make grade 看成绩了:

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
sh复制代码xv6-labs-2020 $ make grade
...
== Test running lazytests ==
$ make qemu-gdb
(7.7s)
== Test lazy: map ==
lazy: map: OK
== Test lazy: unmap ==
lazy: unmap: OK
== Test usertests ==
$ make qemu-gdb
(145.8s)
== Test usertests: pgbug ==
usertests: pgbug: OK
== Test usertests: sbrkbugs ==
usertests: sbrkbugs: OK
== Test usertests: argptest ==
usertests: argptest: OK
== Test usertests: sbrkmuch ==
... OK ...
== Test usertests: dirfile ==
usertests: dirfile: OK
== Test usertests: iref ==
usertests: iref: OK
== Test usertests: forktest ==
usertests: forktest: OK
== Test time ==
time: OK
Score: 119/119

CDFMLR

顶部图片来自于网络,系随机选取的图片,仅用于检测屏幕显示的机械、光电性能,与文章的任何内容及观点无关,也并不代表本人局部或全部同意、支持或者反对其中的任何内容及观点。如有侵权,联系删除。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

在Rust中无畏并发(对比c++) 在Rust中无畏并发(对

发表于 2021-11-25

在Rust中无畏并发(对比c++)

在程序中使用多线程非常困难, 我将演示如何通过使用Rust来避免c++中的一些缺陷,从而使多线程编程变的至少更容易一些。
记住一点: 复杂性不会消失只会转移。

在真实的代码库中,多线程编程通常更复杂,并且防止C++中的这些错误可能会更加困难。在Rust中,编译器仍将检查在那些复杂的情况下,提前阻止你犯下这些错误。

  1. 竞态条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cpp复制代码#include <thread>
#include <iostream>
#include <string>
#include <thread>
int main() {
auto data = std::string{"Hello, world!"};
// 启动第一个线程
auto thread1 = std::thread([&] {
data = std::string{"AAAAAAAAAAAAAAAAAAAAAAAA!"};
});
// 启动第二个线程
auto thread2 = std::thread([&] {
for (auto&& c : data) {
c += 1;
}
});
thread1.join();
thread2.join();
std::cout << data << '\n';
}

在C++中,没有什么能阻止你引入竞争条件,并且在最糟糕的情况下甚至可以访问无效内存。
如果Thread1在Thread2循环期间更新数据data,则Thread2很容易访问已释放的数据data。
在比较大的CodeBases中,即使使用std::atomic或std::mutex,也很难区分调用了哪些类的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rust复制代码fn main() {
let mut data = "Hello, world!".to_owned();
// error[E0373]: closure may outlive the current function,
// but it borrows `data`, which is owned by the current function
std::thread::spawn(||
data = "AAAAAAAAAAAAAAAAAAAAAAAA!".to_owned()
);
// error[E0502]: cannot borrow `data` as immutable
// because it is also borrowed as mutable
// error[E0373]: closure may outlive the current function,
// but it borrows `data`, which is owned by the current function
std::thread::spawn(|| {
for x in data.chars() {
println!("{}", x);
}
});
}

Rust不会让我们这样做。
因为在Rust中,任何变量都可以具有无限数量的不可变的引用,或单一的可变引用。

这意味着不会在安全的Rust代码中出现这些竞态条件(race condition)的错误。
在Rust中有很多方法可以修复上面的错误。
先看看互斥锁和通道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Rust复制代码use std::sync::{Arc, Mutex};
fn main() {
let shared_data = Arc::new(Mutex::new(
"码小菜".to_owned()
));
let t1 = {
// 在arc上clone只会增加变量的引用计数,是零成本的
let shared_data = shared_data.clone();
std::thread::spawn(move || {
let mut data = shared_data.lock().unwrap();
*data = "Hello, 码小菜!\n".to_owned();
})
};
let t2 = {
let shared_data = shared_data.clone();
std::thread::spawn(move || {
let mut data = shared_data.lock().unwrap();
*data = "Goodbye, 码小菜!\n".to_owned();
})
};
t1.join().unwrap();
t2.join().unwrap();
println!("{}", shared_data.lock().unwrap());
}

在上面这个例子中,使用了线程安全的引用计数指针(Arc)来存储数据,确保数据生命周期足够长。
如果只传参不可变数据,使用Arc就足够了。
但是由于想要改变共享数据的状态,还需要将其包装在Rust的std::sync::Mutex中。

1
2
3
4
5
6
7
8
9
Rust复制代码use std::{sync::mpsc::channel, thread};
fn main() {
let (s, r) = channel();
let (s2, r2) = channel();
thread::spawn(move || s.send("Hello, ".to_owned()).unwrap());
thread::spawn(move || s2.send("world!").unwrap());
let message = r.recv().unwrap() + r2.recv().unwrap();
println!("{}", message);
}

对于在线程之间传递数据,可以使用通道(channel)来代替。
这些通道(channel)只允许发送线程安全的类型;如果试图发送一个线程不安全的类型跨线程使用(如Rc),
将会得到一个编译器错误。要想更快地实现多生产者多消费者模型,可以看下crossbeam_channel.

2.生命周期和引用

1
2
3
4
5
6
7
8
9
cpp复制代码#include <string>
#include <thread>
int main() {
{
auto data = std::string{"码小菜"};
std::thread([&] { data.push_back('!'); }).detach();
}
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}

在这个例子中,生成的线程可能正在访问无效的内存,数据data的析构函数在另外一个线程开始访问时可能早已经被调用。
现在的问题是: 我们需要确保在线程中使用的数据比线程本身活得更久。
在许多不同的场景中(不仅仅是多线程的场景),我们使用引用传递c++中的数据,而这些引用必须一直存在,直到引用的对方使用完它为止,而且没有自动的方法来检查引用是否存在。

1
2
3
4
5
6
Rust复制代码fn main() {
let data = "AAAAAAAAAAAAAAAAAAAAAAAA!".to_owned();
// error[E0373]: closure may outlive the current function,
// but it borrows `data`, which is owned by the current function
std::thread::spawn(|| println!("{}", data));
}

正如在第1部分的示例中看到的那样,编译器会抱怨,借来的变量生命周期不够长。
因为线程是独立运行的,所以这些线程中所指向的数据引用必须在整个程序运行期间都存在。

1
2
3
4
5
6
7
8
Rust复制代码use std::sync::Arc;
fn main() {
let data = Arc::new("AAAAAAAAAAAAAAAAAAAAAAAA!".to_owned());
{
let data = data.clone();
std::thread::spawn(move || println!("{}", data));
}
}

正如在前面的示例中看到的那样,可以使用Arc来确保数据生命周期足够长。

1
2
3
4
5
6
7
8
9
10
11
12
Rust复制代码use crossbeam_utils::thread;
fn main() {
let data = "DATA".to_owned();
thread::scope(|s| {
let data = &data;
for c in data.chars() {
s.spawn(move |_| {
println!("{}: {}", data, c);
});
}
}).unwrap();
}

还可以使用来自crossbeam_utils的范围线程。
其原理就是: 当派生的线程需要访问栈上的变量时,主动为其创建一个作用域。

3.处理来自线程的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cpp复制代码#include <exception>
#include <iostream>
#include <stdexcept>
#include <thread>
static std::exception_ptr ep = nullptr;
int main() {
std::thread([] {
try {
throw std::runtime_error("error");
} catch (...) {
ep = std::current_exception();
}
}).join();
if (ep) {
try {
std::rethrow_exception(ep);
} catch(std::runtime_error& e) {
std::cout << e.what() << "\n";
}
}
}

上面的示例 我简单的写了下处理单个线程异常的最小代码,在c++中处理线程的错误是相当复杂的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Rust复制代码fn main() {
let thread = std::thread::spawn(|| {
// ...
// 修改这里的0可以观察下其他的结果如何处理
match 0 {
0 => Ok("ok"),
1 => Err("error"),
_ => panic!("something went wrong"),
}
});
match thread.join() {
// 返回ok,没有panic
Ok(Ok(x)) => println!("OK {}", x),
// 返回错误,没有panic
Ok(Err(x)) => println!("Error: {}", x),
Err(_) => println!("Thread panicked"),
}
}

在Rust中,可以选择处理来自线程的panic(或者只是调用.unwrap()来终止,如果可以肯定线程永远不会恐慌的话),
并且可以返回Result<T, E>来表示线程可能会失败。请注意,对于大多数线程,都不需要处理这两个中的任何一个。

4.Join() 和 detach()

1
2
3
4
5
6
cpp复制代码#include <iostream>
#include <thread>
int main() {
std::thread([] { std::cout << “Hello!”; });
return 0;
}

在c++中,如果忘记join线程, 那么主线程会立马退出。
如果线程是joinable状态的,那么线程(std::thread)的析构函数将调用std::terminate。
当线程调用析构函数的时候它可能还在运行。

1
2
3
Rust复制代码fn main() {
std::thread::spawn(|| println!(“Hello!”));
}

在Rust中,线程在其句柄被删除时隐式分离(运行到作用域外),因此不可能犯这种错误。(注意,你可能看不到打印Hello!在主线程终止后)

当我没有在任何地方保存线程句柄时,我希望线程分离,否则我不会删除它。
c++将这种直观的行为视为不可恢复的运行时错误。

1
2
3
4
5
6
7
8
cpp复制代码#include <iostream>
#include <thread>
int main() {
std::thread t {[] { std::cout << "Hello!"; }};
t.detach();
t.join();
return 0;
}

试图join一个分离的线程会导致崩溃。

1
2
3
4
Rust复制代码fn main() {
let thread = std::thread::spawn(|| println!("Hello!"));
thread.join().unwrap();
}

当作用域消失,自动删除线程句柄时线程被分离时,这个问题就不存在了。

  1. 引用传参

1
2
3
4
5
6
7
8
9
10
11
12
cpp复制代码#include <iostream>
#include <thread>
int main() {
std::string hello {"Hello!"};
std::thread {
[&](const std::string& hi) {
std::cout << std::boolalpha << (&hi == &hello);
},
hello
}.join();
return 0;
}

即使在你不希望它用值传递参数的情况下,(std::thread)也会用值传参(std::ref)可以修复这个问题)。

1
2
3
4
5
6
Rust复制代码fn main() {
let hello = "Hello!".to_owned();
std::thread::spawn(move || println!("{}", hello))
.join()
.unwrap();
}

在Rust中,你不能向线程传递参数,而是借用(borrow)(使用scope线程)或将变量移动(move)到闭包中。

  1. 从线程中返回值

如果想在c++中使用线程来正确地返回一个值,需要使用一些额外的同步机制。我这里给出两个最明显的答案: 通过引用或者std::future。

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
cpp复制代码#include <chrono>
#include <future>
#include <thread>
#include <optional>
#include <iostream>
using namespace std::chrono_literals;

void reference_store() {
auto data = std::optional<std::string>{std::nullopt};
auto t = std::thread([&] {
std::this_thread::sleep_for(500ms);
data = "码小菜";
});
t.join();
std::cout << *data << '\n';
}

void future() {
auto promise = std::promise<std::string>{};
auto future = promise.get_future();
auto t = std::thread([](auto promise) {
std::this_thread::sleep_for(500ms);
promise.set_value("码小菜");
}, std::move(promise));
std::cout << future.get() << '\n';
t.join();
}
int main() {
reference_store();
future();
}

Rust的线程提供了一种直接返回值的机制,这种操作内置在线程中。

1
2
3
4
5
6
7
8
Rust复制代码fn main() {
let thread = std::thread::spawn(|| {
std::thread::sleep(std::time::Duration::from_millis(500));
"码小菜".to_owned()
});
let result = thread.join().unwrap();
println!("{}", result);
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

VSCode配置C#运行环境教程 VSCode配置C#运行环

发表于 2021-11-25

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」

最近在学c#,运行环境一般都是在vs中,但是想单纯的写一些短的测试代码还是习惯使用vscode,记录一下配置过程。

VSCode配置C#运行环境

  1. 下载 dotnet-sdk

  • 下载地址:dotnet.microsoft.com/learn/dotne…

懒得去官网下载可以直接拿我下载好的,放在了百度网盘,需要可自行下载
链接:pan.baidu.com/s/1mwpoXmsh…
提取码:f31t

  • 安装的话,直接点开下载好的 exe 文件,点安装就完事,不需要配置其他的
  • 安装好以后,win+r 打开 cmd 测试一下,输入 dotnet -h, 显示如下:

在这里插入图片描述

在这里插入图片描述
这样 dotnet-sdk 就安装好了。

  1. VSCode安装C#插件

  • 先在任意目录创建一个文件夹,用于存放C#项目【随便找个目录创建个文件夹就行,路径中不要带空格!!!】
    在这里插入图片描述
  • 在VSCode中点击 文件,选择刚才创建的文件夹打开
    在这里插入图片描述
  • 下载安装C#插件,安装完成后重启一下VSCode
    在这里插入图片描述
  1. 创建&运行C#程序

  • 打开终端
    在这里插入图片描述
  • 终端中输入 dotnet new console 即可创建项目
    在这里插入图片描述
  • F5运行完事
    在这里插入图片描述

最后,欢迎大家关注我的个人微信公众号 『小小猿若尘』,获取更多IT技术、干货知识、热点资讯

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何快速过滤出一次请求的所有日志? 前言 正文 进阶 总结

发表于 2021-11-25

前言

在现网出现故障时,我们经常需要获取一次请求流程里的所有日志进行定位。如果请求只在一个线程里处理,则我们可以通过线程ID来过滤日志,但如果请求包含异步线程的处理,那么光靠线程ID就显得捉襟见肘了。

华为IoT平台,提供了接收设备上报数据的能力, 当数据到达平台后,平台会进行一些复杂的业务逻辑处理,如数据存储,规则引擎,数据推送,命令下发等等。由于这个逻辑之间没有强耦合的关系,所以通常是异步处理。如何将一次数据上报请求中包含的所有业务日志快速过滤出来,就是本文要介绍的。

正文

SLF4J日志框架提供了一个MDC(Mapped Diagnostic Contexts)工具类,谷歌翻译为映射的诊断上下文,从字面上很难理解,我们可以先实战一把。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
arduino复制代码public class Main {

    private static final String KEY = "requestId";
    private static final Logger logger = LoggerFactory.getLogger(Main.class);
    
    public static void main(String[] args) {

        // 入口传入请求ID
        MDC.put(KEY, UUID.randomUUID().toString());
        
        // 打印日志
        logger.debug("log in main thread 1");
        logger.debug("log in main thread 2");
        logger.debug("log in main thread 3");

        // 出口移除请求ID
        MDC.remove(KEY);

    }

}

我们在main函数的入口调用MDC.put()方法传入请求ID,在出口调用MDC.remove()方法移除请求ID。配置好log4j2.xml文件后,运行main函数,可以在控制台看到以下日志输出:

1
2
3
css复制代码2018-02-17 13:19:52.606 {requestId=f97ea0fb-2a43-40f4-a3e8-711f776857d0} [main] DEBUG cn.wudashan.Main - log in main thread 1
2018-02-17 13:19:52.609 {requestId=f97ea0fb-2a43-40f4-a3e8-711f776857d0} [main] DEBUG cn.wudashan.Main - log in main thread 2
2018-02-17 13:19:52.609 {requestId=f97ea0fb-2a43-40f4-a3e8-711f776857d0} [main] DEBUG cn.wudashan.Main - log in main thread 3

从日志中可以明显地看到花括号中包含了(映射的)请求ID(requestId),这其实就是我们定位(诊断)问题的关键字(上下文)。有了MDC工具,只要在接口或切面植入put()和remove()代码,在现网定位问题时,我们就可以通过grep requestId=xxx *.log快速的过滤出某次请求的所有日志。

进阶

然而,MDC工具真的有我们所想的这么方便吗?回到我们开头,一次请求可能涉及多线程异步处理,那么在多线程异步的场景下,它是否还能正常运作呢?Talk is cheap, show me the code。

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
java复制代码public class Main {

    private static final String KEY = "requestId";
    private static final Logger logger = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) {

        // 入口传入请求ID
        MDC.put(KEY, UUID.randomUUID().toString());

        // 主线程打印日志
        logger.debug("log in main thread");

        // 异步线程打印日志
        new Thread(new Runnable() {
            @Override
            public void run() {
                logger.debug("log in other thread");
            }
        }).start();

        // 出口移除请求ID
        MDC.remove(KEY);

    }

}

代码里我们新起了一个异步线程,并在匿名对象Runnable的run()方法打印日志。运行main函数,可以在控制台看到以下日志输出:

1
2
css复制代码2018-02-17 14:05:43.487 {requestId=e6099c85-72be-4986-8a28-de6bb2e52b01} [main] DEBUG cn.wudashan.Main - log in main thread
2018-02-17 14:05:43.490 {} [Thread-1] DEBUG cn.wudashan.Main - log in other thread

不幸的是,请求ID在异步线程里不打印了。这是怎么回事呢?要解决这个问题,我们就得知道MDC的实现原理。由于篇幅有限,这里就暂不详细介绍,MDC之所以在异步线程中不生效是因为底层采用ThreadLocal作为数据结构,我们调用MDC.put()方法传入的请求ID只在当前线程有效。感兴趣的小伙伴可以自己深入一下代码细节。

知道了原理那么解决这个问题就轻而易举了,我们可以使用装饰器模式,新写一个MDCRunnable类对Runnable接口进行一层装饰。在创建MDCRunnable类时保存当前线程的MDC值,在执行run()方法时再将保存的MDC值拷贝到异步线程中去。代码实现如下:

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
arduino复制代码public class MDCRunnable implements Runnable {

    private final Runnable runnable;

    private final Map<String, String> map;

    public MDCRunnable(Runnable runnable) {
        this.runnable = runnable;
        // 保存当前线程的MDC值
        this.map = MDC.getCopyOfContextMap();
    }

    @Override
    public void run() {
        // 传入已保存的MDC值
        for (Map.Entry<String, String> entry : map.entrySet()) {
            MDC.put(entry.getKey(), entry.getValue());
        }
        // 装饰器模式,执行run方法
        runnable.run();
        // 移除已保存的MDC值
        for (Map.Entry<String, String> entry : map.entrySet()) {
            MDC.remove(entry.getKey());
        }
    }
    
}

接着,我们需要对main函数里创建的Runnable实现类进行装饰:

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
java复制代码public class Main {

    private static final String KEY = "requestId";
    private static final Logger logger = LoggerFactory.getLogger(Main.class);
    private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor();

    public static void main(String[] args) {

        // 入口传入请求ID
        MDC.put(KEY, UUID.randomUUID().toString());

        // 主线程打印日志
        logger.debug("log in main thread");

        // 异步线程打印日志,用MDCRunnable装饰Runnable
        new Thread(new MDCRunnable(new Runnable() {
            @Override
            public void run() {
                logger.debug("log in other thread");
            }
        })).start();

        // 异步线程池打印日志,用MDCRunnable装饰Runnable
        EXECUTOR.execute(new MDCRunnable(new Runnable() {
            @Override
            public void run() {
                logger.debug("log in other thread pool");
            }
        }));
        EXECUTOR.shutdown();

        // 出口移除请求ID
        MDC.remove(KEY);

    }

}

执行main函数,将会输出以下日志:

1
2
3
css复制代码2018-03-04 23:44:05.343 {requestId=5ee2a117-e090-41d8-977b-cef5dea09d34} [main] DEBUG cn.wudashan.Main - log in main thread
2018-03-04 23:44:05.346 {requestId=5ee2a117-e090-41d8-977b-cef5dea09d34} [Thread-1] DEBUG cn.wudashan.Main - log in other thread
2018-03-04 23:44:05.347 {requestId=5ee2a117-e090-41d8-977b-cef5dea09d34} [pool-2-thread-1] DEBUG cn.wudashan.Main - log in other thread pool

Congratulations!经过我们的努力,最终在异步线程和线程池中都有requestId打印了!

总结

本文讲述了如何使用MDC工具来快速过滤一次请求的所有日志,并通过装饰器模式使得MDC工具在异步线程里也能生效。有了MDC,再通过AOP技术对所有的切面植入requestId,就可以将整个系统的任意流程的日志过滤出来。使用MDC工具,在开发自测阶段,可以极大地节省定位问题的时间,提升开发效率;在运维维护阶段,可以快速地收集相关日志信息,加快分析速度。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…199200201…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%