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

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


  • 首页

  • 归档

  • 搜索

让电脑赢才是胜利?带你领略不一样的C语言三子棋代码详解,小白

发表于 2021-11-21

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

三子棋

在这里插入图片描述

在这里插入图片描述

三子棋游戏代码详解


@TOC


前言

想必大家都玩过五子棋,今天要讲解的三子棋也是一个道理,不管是行还是列,或者是对角线,只要连成一条线就能获得游戏胜利。与平常游戏不同的是,这次的电脑下棋是完全随机的,所以想让电脑赢才是需要技术的。以下将游戏代码简称为 CHESS。


一、代码框架

CHESS主要有三个文件,分别是test.c、game.c,game.h.

game.c:主要负责写具体的游戏函数,例如,Checkwin(检测游戏输赢),Initboard(初始化棋盘),PlayerMove(玩家移动),ComputerMove(电脑移动),DisplayBoard(打印棋盘)。

test.c:主要负责代码主题部分,包含主函数,菜单界面等等。

game.h:主要负责代码头文件,包含需要的game.c的各种函数,例如

Checkwin,InitBoard等等。

二、test.c

game 函数

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
c复制代码#include"game.h"//在这里引上头文件,game.h里包含游戏代码需要的头文件,所以这里就可以一劳永逸了。
void game()
{
char ret = 0;
char board[ROW][COL];// 初始化为空格
InitBoard(board,ROW,COL);//初始化棋盘
DisplayBoard(board, ROW, COL);//打印棋盘
while (1)//一直满足循环条件
{

//玩家走
PlayerMove(board,ROW,COL);
ret = CheckWin(board,ROW,COL);
if (ret != 'c')
{
break;
}
DisplayBoard(board,ROW,COL);
//电脑走
ComputerMove(board,ROW,COL);
ret = CheckWin(board,ROW,COL);
if (ret != 'c')
{
break;
}
DisplayBoard(board, ROW, COL);
//判断输赢 1:电脑赢-'#' 2:玩家赢-'*' 3:平局-'q' 4:继续呗-'c'

}
if (ret == '*')
{
printf("玩家赢\n");
}
else if (ret == '#')
{
printf("电脑赢\n");
}
else if (ret == 'q')
printf("平局\n");
DisplayBoard(board, ROW, COL);

}

三、game.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
c复制代码#include"game.h"
void InitBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
board[i][j] =' ';
}
}
}
void DisplayBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf(" %c ", board[i][j]);
if(j<col-1)
printf("|");
}
printf("\n");
if (i < row - 1)
{
for (j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
printf("|");
}
printf("\n");
}

}
}
void PlayerMove(char board[ROW][COL],int row,int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("玩家走:");
scanf_s("%d %d", &x, &y);
if (x >= 1 && x <= 3 && y >= 1 && y <= 3)
{

if (board[x-1][y-1] != ' ')
{
printf("坐标被占用,重新输入\n");
}
else
{
board[x-1][y-1] = '*';
break;
}

}
else
{
printf("坐标非法,重新输入\n");
}
}
}
void ComputerMove(char board[ROW][COL], int row, int col)
{
printf("电脑走\n");

while (1)//死循环,直到坐标合法为止
{
int x = rand() % row;
int y = rand() % col;
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
int is_full(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i< row; i++)
{
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')
return 0;
}
}
return 1;
}
char CheckWin(char board[ROW][COL], int row,int col)
{
int i = 0;
//三行
for (i = 0; i < row; i++)
{
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][0] != ' ')
{
return board[i][0];
}
}
for(i=0;i<col;i++)
//三列
if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != ' ')
{
return board[0][i];
}
//对角线
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[1][1] != ' ')
{
return board[1][1];
}
if (board[0][2] ==board[1][1] && board[1][1] == board[2][0] && board[1][1] != ' ')
{
return board[1][1];
}
//平局
if (is_full(board,row,col)==1)
{
return 'q';
}
//游戏继续
//前面没有赢,也没有平局就是继续
return 'c';
}

game.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c复制代码#define _CRT_SECURE_NO_WARNNINGS
#include<stdio.h>
#include<stdlib.h>
#include<time.h>//引头文件,是电脑产生随机数,随机下棋
#define ROW 3 //在这里定义行和列,只要包含game.h。就可以直接使用,不需要在到函数里修改,非常方便。
#define COL 3
void InitBoard(char board[ROW][COL], int row, int col);
//玩家下棋
void PlayerMove(char board[ROW][COL], int row, int col);
void DisplayBoard(char board[ROW][COL], int row, int col);
//电脑下棋
void ComputerMove(char board[ROW][COL], int row, int col);
char CheckWin(char board[ROW][COL], int row, int col);
int is_full(char board[ROW][COL], int row, int col);

游戏演示

在这里插入图片描述

总结

提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

本文转载自: 掘金

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

Python的从0到1(第十五天)-Python的条件判断2

发表于 2021-11-21

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

双向判断,if…else…

在Python世界,我们可以利用if的双向判断形式:if…else…去实现这个动作:当剩余钱包的数量小于1时,需要给抢红包的人送上祝福,来晚了,但还是要祝您万事如意!!!

下面,请你直接运行下面的代码,看程序是如何识别的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码# 剩余红包数量

redpacketnumber=0

# 条件:如果剩余红包数量大于等于1

if redpacketnumber >=1:

# 结果:显示‘成功抢到了红包’的结果

print('成功抢到了红包')

else:

# 结果:显示'来晚了,但还是要祝您万事如意!!!'的结果

print('来晚了,但还是要祝您万事如意!!!')

其实生活中也是一样,很多事情都不是非此即彼的,当不满足条件的时候,我们要怎么办。

Python则很贴心地,让我们借用if…else语句,让码农有了另一种选择—[如果…就,否则…就]

在if…else条件语句中,if和else各自抱团,形成两个不同的代码块。表示的是条件和其他条件的互斥关系——如果不满足if条件,就执行else其他条件。

如果用我们上面抢红包的例子讲解的话,if定义的就是剩余红包数量>=1,而else定义的则是剩余红包数量<1。

而由于赋值【redpacketnumber =0】,并不满足【if redpacketnumber >=1:】这个条件,所以不能执行【print('成功抢到了红包')】的命令。

只能走第二条else:的路——执行print('来晚了,还是祝您万事如意!!!')的结果。

下面,我们来做个题巩固一下if…else…的知识点:抢完了红包,我们可以去吃饭了,可是吃什么还是得根据抢红包的多少来决定,如果我们抢的红包大于50元,我们就可以来一吨肉的,如果不超过50元,我们就只能吃素的了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码# 要先为抢到红包的金额赋值,假设抢到了38元

money=38
# 如果红包超过50的条件下,就...(条件后需加冒号)

if money>50:

# 就打印结果:大口吃肉,大口喝酒

print('大口吃肉,大口喝酒')

# 如果红包没有超过50,就...(else条件前无缩进,条件后需加冒号)

else:

# 就打印:素食主义,养生才是硬道理

print('素食注意,养生才是硬道理')

关于if…else…的相关知识点到这里就讲完了,我们来总结一下if…else…语句的知识点。

上面的吃饭的练习中,我们会发现,事情并不是只有两面,可能会有第三面,其实38元我们也可以搭配一点肉菜了,荤素搭配,更营养,更健康。

那么,场景中有三个如果使用Python要如何实现呢?

多向判断:if…elif…else…

上述场景中,在判断3个或3个以上的条件时,我们就需要借助Python中的多向判断命令:if…elif…else…。

这三者共同构成了多向判断的命令逻辑关系:如果if的条件不满足,就按顺序看是否满足elif的条件,如果不满足elif的条件,就执行else的命令。

并且,当判断的条件超过3个时,中间的多个条件都可以使用elif。

那么,这个代码要怎么写呢?我们还是在代码中来感受一下elif的逻辑

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
python复制代码# 要先为抢到红包的金额赋值,假设抢到了38元

money=38

# 如果红包超过50的条件下,就...(条件后需加冒号)

if money > 50:

# 就打印结果:大口吃肉,大口喝酒

print('大口吃肉,大口喝酒')

# 如果红包超过35,没超过50

elif 50 >= money > 35:

print('荤素搭配,更营养,更健康')

# 如果红包没有超过50,就...(else条件前无缩进,条件后需加冒号)

else:

# 就打印:素食主义,养生才是硬道理

print('素食注意,养生才是硬道理')

通过上面if和else的积累,多向判断elif的逻辑是不是很好理解呢?

首先,第一行的赋值,会按照从上到下的顺序挨个试一遍,看自己满足哪个条件,满足了就不往下走,不满足就继续试,试出结果为止。

其次,elif的运行本质上和else一样,已经带有其他条件的意思,所以elif后可以不接else。

比如上面的代码,money=38会对下面的if elif else条件逐个扫描,看看自己满足哪一个,就执行哪个条件底下的命令。

很显然,第二个条件elif 50 >= money > 35刚好能与money=5的前提相匹配,于是,执行elif底下的命令: print('荤素搭配,更营养,更健康')

下面,我们来总结一下elif的知识点:

本文转载自: 掘金

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

RabbitMQ由浅入浅

发表于 2021-11-21

0.RabbitMQ介绍,应用场景

RabbitMQ是一种较为流行的一种消息中间件技术,它与Spring可以良好地整合。

那么RabbitMQ是用来做什么的,又或者说,我们在什么地方需要用到消息中间件。以下说明为消息中间件的应用场景:

0.0 异步处理

场景说明:用户注册后,需要发注册邮件和注册短信,传统的做法有两种,1.串行的方式,2.并行的方式。

串行方式:将注册信息写入数据库后,发送注册邮件,再发送注册短信,以上三个任务全部完成后才返回给客户端。这有一个问题是,邮件,短信并不是必须的,它只是一个通知,而这种做法让客户端等待没有必要等待的东西。

image-20211113161241706.png
并行方式:将注册信息写入数据库后,发送邮件的同时,发送短信,以上三个任务完成后,返回给客户端,并行的方式能提高处理的时间。

image-20211113161452556.png

除了以上两种方式,还可以使用消息队列异步去操作:

消息队列:假设三个业务节点分别使用50ms,串行方式使用时间150ms,并行方式使用时间100ms。虽然并行已经提高了处理时间,但是短信和邮件对于正常使用网站没有任何影响,客户端没有必要等着其发送完成才显示注册成功,应该是写入数据库后就返回。引入消息队列之后,把发送邮件,短信不是必须的业务逻辑异步处理。

image-20211113161830708.png

0.1应用解耦

场景:双11是购物狂节,用户下单后,订单系统需要通知库存系统,传统的做法就是订单系统调用库存系统的接口。

image-20211113162443910.png

但是这种做法有一个缺点:当库存系统出现故障时,订单就会失败。订单系统和库存系统高耦合。引入消息队列之后:

image-20211113162602590.png

订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。

库存系统:订阅下单的消息,获取下单消息,进行库存操作。就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失。

0.2 流量削峰

场景:秒杀活动,一般会因为流量过大,导致应用挂掉,为了解决这个问题,一般在应用前端加入消息队列。

作用:

1.可以控制活动人数,超过设置的数量的订单直接丢弃

2.可以缓解短时间的高流量压垮应用(应用程序按自己的最大处理能力获取订单)

image-20211113163413716.png

1.用户的请求,服务器收到之后,首先写入消息队列,加入消息队列长度超过最大值,则直接抛弃用户请求或跳转到错误页面。

2.秒杀业务根据消息队列中的请求信息,再做后续处理。

1.Rabbitmq安装-以rpm的方式

1.1 安装Erlang环境

1
ruby复制代码curl -s https://packagecloud.io/install/repositories/rabbitmq/erlang/script.rpm.sh | sudo bash

安装erlang:

1
复制代码yum install -y erlang

检查erlang版本:

1
复制代码erl

在安装Rabbitmq之前需要注意的是,一定要保证erlang的版本和rabbitmq的版本是对应的。

查看erlang和rabbitmq版本对应关系

我这里安装的erlang版本是:

image-20211114103247799.png

image-20211114103335750.png

因为我安装的erlang版本是24的,所以接下来我安装RabbitMQ的版本至少也要3.8.16

1.2 安装RabbitMQ

1
2
3
4
5
6
arduino复制代码rpm --import https://packagecloud.io/rabbitmq/rabbitmq-server/gpgkey
rpm --import https://packagecloud.io/gpg.key
rpm --import https://www.rabbitmq.com/rabbitmq-release-signing-key.asc
yum -y install epel-release
yum -y install socat
curl -s https://packagecloud.io/install/repositories/rabbitmq/rabbitmq-server/script.rpm.sh | sudo bash

下载rpm包:

1
ruby复制代码wget https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.8.25/rabbitmq-server-3.8.25-1.el7.noarch.rpm

这里下载可能会很慢,也可以在windows下载rpm包之后再上传到服务器上。

安装:

1
vbscript复制代码rpm -ivh rabbitmq-server-3.8.25-1.el7.noarch.rpm

然后启动管理平台插件,如果需要访问RabbitMQ的web管理页面就需要操作这一步:

1
bash复制代码rabbitmq-plugins enable rabbitmq_management

启动RabbitMQ:

1
sql复制代码systemctl start rabbitmq-server

然后可以访问控制台地址:

http:你的服务器ip:15672

image-20211114103940661.png

如果访问不了,需要检查你的RabbitMQ是否成功启动并且相关端口(15672)是否开放。

使用默认的账号进行登录:guest/guest。

这时候会发现登录不进去,并且会有一个提示”User can only log in via localhost“

出现这个信息表示RabbitMQ web管理页面只允许以localhost的方式进行访问,那么我们在外网如何进行web管理后台的访问呢?

需要进入到/etc/rabbitmq/目录下:

1
bash复制代码cd /etc/rabbitmq
1
arduino复制代码vi rabbitmq.config

在rabbitmq.config配置文件里面加上(最后的那个小点别忘了,不然会重启不成功):

1
css复制代码[{rabbit, [{loopback_users, []}]}].

然后重启:

1
vbscript复制代码systemctl restart rabbitmq-server

image-20211114105213234.png

2.AMQP协议

2.1 什么是AMQP

AMQP(Advanced Message Queuing Protocol)高级消息队列协议,是一种网络协议。用于客户端应用和消息中间件之间的通信。

2.2 AMQP模型简介

image-20211115235228178.png

上图是AMQP模型,主要的工作过程就是:消息由Publisher(发布者或者称为提供者),发送到交换机,交换机将收到的消息根据路由规则转发到不同的队列中去。最后RabbitMQ会将队列中的消息推送给订阅了此队列的消费者,或者消费者主动的从队列中获取消息。

从安全角度考虑,网络是不可靠的。所以消费者在消费消息的时候,可能会因为某种原因处理失败。基于这个原因,RabbitMQ提供了一种消息确认机制(message acknowledgements):当一个消息从队列投递给消费者之后,要么会自动进行消息确认,要么就需要手动进行消息确认。消费者确认之后,队列就会将此条消息进行删除。

消息确认有两种方式:

1.自动确认,当消息被发送到消费者之后,自动删除。

2.待应用发送一个确认回执后再删除消息。确认回执可以是在收到消息后立马发送,也可以将消息存储之后再进行回执发送,也可以在处理完该消息之后,进行回执发送。

在某些情况,例如有一个消息没有被成功路由时。消息或许会被返回给发布者并被丢弃。

2.3 交换机和交换机类型

AMQP提供了四种交换机:

image-20211116000659028.png

2.3.1 默认交换机

默认交换机(default exchange)是RabbitMQ预先声明好的没有名字的交换机。每个新建队列,如果没有指明特定的交换机名称,那么它将会自动绑定到默认交换机上,绑定的路由键名称就是队列的名称。

我看到有些说法,就是说,通道可以把消息直接发送到交换机,或者直接发送到队列。我觉得这样说是不准确的,”直接发送到队列“这种情况应该是发送到默认的交换机,然后由默认交换机根据路由key来路由到对应的与之绑定的队列中。

2.3.2 直连交换机

直连交换机(direct exchange)是根据消息携带的路由键将消息转发给对应队列。直连交换机默认的模式是平均分配,如果一个直连交换机绑定了多个路由键一样的队列,那么直连交换机则会将消息平均的分配给所绑定的符合条件的队列。

image-20211116235349523.png

2.3.3 扇形交换机

扇形交换机(fanout exchange)将消息路由给绑定到它审核的所有队列,而不理会绑定的路由键。如果有多个队列绑定到扇形交换机上,当有消息发送给扇形交换机的时候,交换机会将消息的拷贝分别发送给绑定到扇形交换机上面的所有队列。

image-20211116235643798.png

2.3.4 主题交换机

主题交换机(topic exchanges)通过对消息的路由键和队列到交换机的绑定模式之间的匹配,将消息路由给一个或多个队列。主题交换机经常用来实现各种分发/订阅模式及其变种。主题交换机通常用来实现消息的多播路由(multicast routing)。

2.4 队列

2.4.1 队列持久化

持久化队列(Durable queues)会被存储在磁盘上,当RabbitMQ重启的时候,持久化队列依旧存在。没有被持久化的队列称为暂存队列(Transient queues)。

需要注意的是,队列的持久化,不意味着队列中的消息持久化。如果消息没有被持久化,那么在RabbitMQ重启之后,虽然持久化队列存在,但是消息不会被保留。

2.5 通道

消息是通过通道被发送到交换机和队列上的。是消息传输的介质。

2.6 虚拟主机

RabbitMQ提供虚拟主机来隔离多个环境。当建立连接的时候,需要指定使用哪一个虚拟主机。

3.Rabbitmq web管理页面大致了解

image-20211117000833170.png

4.五种模型的demo实现

4.0 相关依赖

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
xml复制代码    <dependencies>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-amqp</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>org.springframework.amqp</groupId>
           <artifactId>spring-rabbit-test</artifactId>
           <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>junit</groupId>
           <artifactId>junit</artifactId>
           <scope>test</scope>
           <version>4.12</version>
       </dependency>
   </dependencies>

4.1 Hello World

image-20211119233418940.png

这个消息的要素就三个,一个生产者,一个消费者,和一个队列。这个模型很容易的就让人觉得,生产者把消息直接发送到队列中,其实不是的。RabbitMQ中存在一个默认交换机,如果没有显式地把队列和一个具体名称的交换机绑定的话,默认就会把队列和默认交换机进行绑定,绑定的路由key就是队列的名称。所以这个模型的消息发送实质上还是消费者发送到交换机,然后交换机再把消息路由到队列,最后由消费者获取队列消息,或者队列向订阅了的消费者推送消息。

在操作之前需要创建一个虚拟主机和对应的测试用户:

image-20211119233514057.png

消息生产者Provider生产消息:

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
java复制代码/**
* "hello world" 模型-消息提供者
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class Provider {
​
   @Test
   public void sendMessage()throws IOException, TimeoutException {
       //创建连接mq的连接工厂对象
       ConnectionFactory connectionFactory = new ConnectionFactory();
       //设置连接rabbitmq的主机
       connectionFactory.setHost("yourserverip");
       //设置端口号
       connectionFactory.setPort(5672);
       //设置连接的虚拟主机
       connectionFactory.setVirtualHost("/demo1");
       //设置访问虚拟主机的用户名和密码
       connectionFactory.setUsername("qingyuan");
       connectionFactory.setPassword("qingyuan");
       //获取连接对象
       Connection connection = connectionFactory.newConnection();
       //获取通道
       Channel channel = connection.createChannel();
       /**
        * 绑定对应的消息队列
        * @param1 队列名称,如果不存在则会创建
        * @param2 定义队列是否需要持久化,true为持久化
        * @param3 exclusive 是否独占队列,true 独占
        * @param4 额外参数
        */
       channel.queueDeclare("hello",true,false,false,null);
       /**
        * 发布消息
        * @param1 交换机名称
        * @param2 队列名称
        * @param3 传递消息额外设置
        * @param4 发布的消息内容
        */
       channel.basicPublish("","hello", MessageProperties.PERSISTENT_TEXT_PLAIN,"hello rabbitmq".getBytes());
​
       //关闭资源
       channel.close();
       connection.close();
  }
}

执行完毕之后,就可以看到队列中已经存在一条消息了:

1.jpg

消费者Consumer消费消息:

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
java复制代码/**
* "hello world" 模型-消息消费者
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class Consumer {
​
   @Test
   public void Receive()throws IOException, TimeoutException {
       //创建连接mq的连接工厂对象
       ConnectionFactory connectionFactory = new ConnectionFactory();
       //设置连接rabbitmq的主机
       connectionFactory.setHost("yourserverip");
       //设置端口号
       connectionFactory.setPort(5672);
       //设置连接的虚拟主机
       connectionFactory.setVirtualHost("/demo1");
       //设置访问虚拟主机的用户名和密码
       connectionFactory.setUsername("qingyuan");
       connectionFactory.setPassword("qingyuan");
       //获取连接对象
       Connection connection = connectionFactory.newConnection();
       //获取通道
       Channel channel = connection.createChannel();
       /**
        * 绑定对应的消息队列
        * @param1 队列名称,如果不存在则会创建
        * @param2 定义队列是否需要持久化,true为持久化
        * @param3 exclusive 是否独占队列,true 独占
        * @param4 额外参数
        */
       channel.queueDeclare("hello",true,false,false,null);
​
       /**
        * 消费消息
        * @param1 队列名称
        * @param2 是否自动确认 true 是自动确认
        * @param3 接收到消息之后进行回调
        */
       channel.basicConsume("hello",true,new DefaultConsumer(channel){
           @Override
           public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
               System.out.println("message:"+new String(body));
          }
      });
  }
}

执行完毕之后:

2.jpg

可以看到队列中的消息已经被消费了。

4.2 Work queues

3.jpg

可以看到工作队列(work queues)模型与第一个Helloworld模型类似,只不过工作队列模型的消费者由一个变成多个。

那这个工作队列(work queues)模型是在什么背景下使用的?在一个消费者一个生产者的模型下,如果生产者生产消息的速度远大于消费者消费消息的速度,那么消息就不断地累积在队列中,会阻塞队列。

使用工作队列模型,在原有的基础上,增加多个消费者,多个消费者共同消费同一个队列里面的消息,并且使用这个模型的前提下,并不会导致消息被重复消费。

创建消息生产者生产消息:

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
java复制代码/**
* "work queues" 模型-消息提供者
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class Provider {
​
   @Test
   public void sendMessage()throws IOException, TimeoutException {
       //创建连接mq的连接工厂对象
       ConnectionFactory connectionFactory = new ConnectionFactory();
       //设置连接rabbitmq的主机
       connectionFactory.setHost("yourserverip");
       //设置端口号
       connectionFactory.setPort(5672);
       //设置连接的虚拟主机
       connectionFactory.setVirtualHost("/demo1");
       //设置访问虚拟主机的用户名和密码
       connectionFactory.setUsername("qingyuan");
       connectionFactory.setPassword("qingyuan");
       //获取连接对象
       Connection connection = connectionFactory.newConnection();
       //获取通道
       Channel channel = connection.createChannel();
       /**
        * 绑定对应的消息队列
        * @param1 队列名称,如果不存在则会创建
        * @param2 定义队列是否需要持久化,true为持久化
        * @param3 exclusive 是否独占队列,true 独占
        * @param4 额外参数
        */
       channel.queueDeclare("hello",true,false,false,null);
       for (int i = 0; i < 10; i++) {
           channel.basicPublish("", "hello", null, (i+"====>:我是消息").getBytes());
      }
       //关闭资源
       channel.close();
       connection.close();
  }
}

4.jpg

创建消费者1Consumer1:

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
java复制代码/**
* "work queues" 模型-消息消费者1
*/
public class Consumer1 {
​
   public static void main(String[] args) throws IOException, TimeoutException {
       //创建连接mq的连接工厂对象
       ConnectionFactory connectionFactory = new ConnectionFactory();
       //设置连接rabbitmq的主机
       connectionFactory.setHost("yourserverip");
       //设置端口号
       connectionFactory.setPort(5672);
       //设置连接的虚拟主机
       connectionFactory.setVirtualHost("/demo1");
       //设置访问虚拟主机的用户名和密码
       connectionFactory.setUsername("qingyuan");
       connectionFactory.setPassword("qingyuan");
       //获取连接对象
       Connection connection = connectionFactory.newConnection();
       //获取通道
       Channel channel = connection.createChannel();
       /**
        * 绑定对应的消息队列
        * @param1 队列名称,如果不存在则会创建
        * @param2 定义队列是否需要持久化,true为持久化
        * @param3 exclusive 是否独占队列,true 独占
        * @param4 额外参数
        */
       channel.queueDeclare("hello", true, false, false, null);
​
       /**
        * 消费消息
        * @param1 队列名称
        * @param2 是否自动确认 true 是自动确认
        * @param3 接收到消息之后进行回调
        */
       channel.basicConsume("hello", true, new DefaultConsumer(channel) {
           @Override
           public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
               System.out.println("消费者1: " + new String(body));
          }
      });
  }
}

创建消费者2Consumer2:

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
java复制代码/**
* "work queues" 模型-消息消费者2
*/
public class Consumer2 {
   public static void main(String[] args) throws IOException, TimeoutException {
       //创建连接mq的连接工厂对象
       ConnectionFactory connectionFactory = new ConnectionFactory();
       //设置连接rabbitmq的主机
       connectionFactory.setHost("yourserverip");
       //设置端口号
       connectionFactory.setPort(5672);
       //设置连接的虚拟主机
       connectionFactory.setVirtualHost("/demo1");
       //设置访问虚拟主机的用户名和密码
       connectionFactory.setUsername("qingyuan");
       connectionFactory.setPassword("qingyuan");
       //获取连接对象
       Connection connection = connectionFactory.newConnection();
       //获取通道
       Channel channel = connection.createChannel();
       /**
        * 绑定对应的消息队列
        * @param1 队列名称,如果不存在则会创建
        * @param2 定义队列是否需要持久化,true为持久化
        * @param3 exclusive 是否独占队列,true 独占
        * @param4 额外参数
        */
       channel.queueDeclare("hello", true, false, false, null);
​
       /**
        * 消费消息
        * @param1 队列名称
        * @param2 是否自动确认 true 是自动确认
        * @param3 接收到消息之后进行回调
        */
       channel.basicConsume("hello", true, new DefaultConsumer(channel) {
           @Override
           public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
               try {
                   //处理消息比较慢 一秒处理一个消息
                   Thread.sleep(1000);
              } catch (InterruptedException e) {
                   e.printStackTrace();
              }
               System.out.println("消费者2: " + new String(body));
          }
      });
  }
}

先运行两个消费者,然后再运行生产者,然后可以看到控制台输出:

5.png

6.png

7.png

可以看到消息被消费了。这里要说明一下,虽然在消费者2中的回调方法加了线程休眠,但是目前应该是不生效的,因为这里开启了消息自动确认,就是说,当消费者拿到消息之后,消息就会被自动确认,队列中对应的消息就会被删除,即使是回执方法没有执行完毕,队列也当做消费者已经消费了消息。

而且在控制台中可以看到,工作队列模型(work queues),默认的消息分配策略是轮询的,也可以说是平均的,队列会将消息平均地分配到每一个消费者上。

那么这个平均分配会带来什么问题,假设某一个消费者,处理消息的速度远慢于消息队列分发消息的速度,消费者此时是开启消息自动确认机制,消费者收到消息,但是并没有马上处理完成,如果程序在某个地方执行错误,导致程序崩溃,那么剩下没有被执行的消息将会被丢失。而消息队列中,在收到消息确认之后就会删除相关的消息。

那么如何来解决这个问题:1.消费者不开启消息自动确认,采用手动确认方式。就是消费者要在接收消息,处理消息完毕之后,再进行手动确认。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码channel.basicQos(1);//每一次只能消费一个消息
//@param2 false为关闭消息自动确认
channel.basicConsume("work",false,new DefaultConsumer(channel){
           @Override
           public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
               try{
                   Thread.sleep(2000);
              }catch (Exception e){
                   e.printStackTrace();
              }
               System.out.println("消费者-1: "+new String(body));
               // 参数1:确认队列中那个具体消息 参数2:是否开启多个消息同时确实
               channel.basicAck(envelope.getDeliveryTag(),false);
          }
      });

经过修改之后,就会发现,处理速度 越快的消费者,它所收到的消息就越多。我们的工作队列(work queues)从原来的”平均分配”,变成了如今的”多劳多得”。

4.3 Fanout

8.png

fanout模型,也叫做广播模型。在这个模型下,消费者可以有多个,并且每个消费者有自己的队列。生产者把消息发送到交换机,由交换机来决定要发给哪个队列,交换机把消息发送给绑定过的所有队列。队列的消费者都能拿到消息。实现一条消息被多个消费者消费。

生产者:

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复制代码/**
* fanout模型-生产者
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class Provider {
   @Test
   public void sendMessage()throws IOException, TimeoutException {
       //创建连接mq的连接工厂对象
       ConnectionFactory connectionFactory = new ConnectionFactory();
       //设置连接rabbitmq的主机
       connectionFactory.setHost("yourserverip");
       //设置端口号
       connectionFactory.setPort(5672);
       //设置连接的虚拟主机
       connectionFactory.setVirtualHost("/demo1");
       //设置访问虚拟主机的用户名和密码
       connectionFactory.setUsername("qingyuan");
       connectionFactory.setPassword("qingyuan");
       //获取连接对象
       Connection connection = connectionFactory.newConnection();
       //获取通道
       Channel channel = connection.createChannel();
​
       /**
        * 声明交换机
        * @param1 交换机名称
        * @param2 交换机类型
        */
       channel.exchangeDeclare("logs","fanout");
       //发送消息
       channel.basicPublish("logs","",null,"fanout type message".getBytes());
       //释放资源
       channel.close();
       connection.close();
  }
}

消费者1:

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
java复制代码public class Consumer1 {
   public static void main(String[] args) throws IOException, TimeoutException {
       //创建连接mq的连接工厂对象
       ConnectionFactory connectionFactory = new ConnectionFactory();
       //设置连接rabbitmq的主机
       connectionFactory.setHost("yourserverip");
       //设置端口号
       connectionFactory.setPort(5672);
       //设置连接的虚拟主机
       connectionFactory.setVirtualHost("/demo1");
       //设置访问虚拟主机的用户名和密码
       connectionFactory.setUsername("qingyuan");
       connectionFactory.setPassword("qingyuan");
       //获取连接对象
       Connection connection = connectionFactory.newConnection();
       //获取通道
       Channel channel = connection.createChannel();
​
       //绑定交换机
       channel.exchangeDeclare("logs","fanout");
       //创建临时队列
       String queueName = channel.queueDeclare().getQueue();
       //队列和交换机进行绑定
       channel.queueBind(queueName,"logs","");
       //消费消息
       channel.basicConsume(queueName,true,new DefaultConsumer(channel){
           @Override
           public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
               System.out.println("消费者1: "+new String(body));
          }
      });
  }
}

消费者2:

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
java复制代码public class Consumer2 {
   public static void main(String[] args) throws IOException, TimeoutException {
       //创建连接mq的连接工厂对象
       ConnectionFactory connectionFactory = new ConnectionFactory();
       //设置连接rabbitmq的主机
       connectionFactory.setHost("yourserverip");
       //设置端口号
       connectionFactory.setPort(5672);
       //设置连接的虚拟主机
       connectionFactory.setVirtualHost("/demo1");
       //设置访问虚拟主机的用户名和密码
       connectionFactory.setUsername("qingyuan");
       connectionFactory.setPassword("qingyuan");
       //获取连接对象
       Connection connection = connectionFactory.newConnection();
       //获取通道
       Channel channel = connection.createChannel();
​
       //绑定交换机
       channel.exchangeDeclare("logs","fanout");
       //创建临时队列
       String queueName = channel.queueDeclare().getQueue();
       //队列和交换机进行绑定
       channel.queueBind(queueName,"logs","");
       //消费消息
       channel.basicConsume(queueName,true,new DefaultConsumer(channel){
           @Override
           public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
               System.out.println("消费者2: "+new String(body));
          }
      });
  }
}

消费者3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码public class Consumer3 {
   public static void main(String[] args) throws IOException, TimeoutException {
       //创建连接mq的连接工厂对象
       ConnectionFactory connectionFactory = new ConnectionFactory();
       //设置连接rabbitmq的主机
       connectionFactory.setHost("yourserverip");
       //设置端口号
       connectionFactory.setPort(5672);
       //设置连接的虚拟主机
       connectionFactory.setVirtualHost("/demo1");
       //设置访问虚拟主机的用户名和密码
       connectionFactory.setUsername("qingyuan");
       connectionFactory.setPassword("qingyuan");
       //获取连接对象
       Connection connection = connectionFactory.newConnection();
       //获取通道
       Channel channel = connection.createChannel();
​
       //绑定交换机
       channel.exchangeDeclare("logs","fanout");
       //创建临时队列
       String queueName = channel.queueDeclare().getQueue();
       //队列和交换机进行绑定
       channel.queueBind(queueName,"logs","");
       //消费消息
       channel.basicConsume(queueName,true,new DefaultConsumer(channel){
           @Override
           public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
               System.out.println("消费者3: "+new String(body));
          }
      });
  }
}

运行效果:

9.png

10.png

11.png

创建的交换机:

13.png

4.4 Routing-Direct

14.png

生产者:

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
java复制代码public class Provider {
   public static void main(String[] args) throws IOException, TimeoutException {
       //创建连接mq的连接工厂对象
       ConnectionFactory connectionFactory = new ConnectionFactory();
       //设置连接rabbitmq的主机
       connectionFactory.setHost("yourserverip");
       //设置端口号
       connectionFactory.setPort(5672);
       //设置连接的虚拟主机
       connectionFactory.setVirtualHost("/demo1");
       //设置访问虚拟主机的用户名和密码
       connectionFactory.setUsername("qingyuan");
       connectionFactory.setPassword("qingyuan");
       //获取连接对象
       Connection connection = connectionFactory.newConnection();
       //获取通道
       Channel channel = connection.createChannel();
       //声明交换机
       String exchangeName = "logs_direct";
       channel.exchangeDeclare(exchangeName,"direct");
       //定义路由key
       String routingKey = "info";
       //发送消息
       channel.basicPublish(exchangeName,routingKey,null,("这是direct模型发布的基于route key: ["+routingKey+"] 发送的消息").getBytes());
       //关闭资源
       channel.close();
       connection.close();
  }
}

消费者1:

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
java复制代码/**
* Routing-Direct 消费者1
*/
public class Consumer1 {
   public static void main(String[] args) throws IOException, TimeoutException {
       //创建连接mq的连接工厂对象
       ConnectionFactory connectionFactory = new ConnectionFactory();
       //设置连接rabbitmq的主机
       connectionFactory.setHost("yourserverip");
       //设置端口号
       connectionFactory.setPort(5672);
       //设置连接的虚拟主机
       connectionFactory.setVirtualHost("/demo1");
       //设置访问虚拟主机的用户名和密码
       connectionFactory.setUsername("qingyuan");
       connectionFactory.setPassword("qingyuan");
       //获取连接对象
       Connection connection = connectionFactory.newConnection();
       //获取通道
       Channel channel = connection.createChannel();
​
       String exchangeName = "logs_direct";
       channel.exchangeDeclare(exchangeName,"direct");
       //创建临时队列
       String queue = channel.queueDeclare().getQueue();
       /**
        * 队列和交换机进行绑定
        * @param1 队列名
        * @param2 交换机名
        * @param3 路由key
        */
       channel.queueBind(queue,exchangeName,"error");
       //获取消费的消息
       channel.basicConsume(queue,true,new DefaultConsumer(channel){
           @Override
           public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
               System.out.println("消费者1: "+ new String(body));
          }
      });
  }
}

消费者2:

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
arduino复制代码/**
* Routing-Direct 消费者2
*/
public class Consumer2 {
   public static void main(String[] args) throws IOException, TimeoutException {
       //创建连接mq的连接工厂对象
       ConnectionFactory connectionFactory = new ConnectionFactory();
       //设置连接rabbitmq的主机
       connectionFactory.setHost("yourserverip");
       //设置端口号
       connectionFactory.setPort(5672);
       //设置连接的虚拟主机
       connectionFactory.setVirtualHost("/demo1");
       //设置访问虚拟主机的用户名和密码
       connectionFactory.setUsername("qingyuan");
       connectionFactory.setPassword("qingyuan");
       //获取连接对象
       Connection connection = connectionFactory.newConnection();
       //获取通道
       Channel channel = connection.createChannel();
​
       String exchangeName = "logs_direct";
       channel.exchangeDeclare(exchangeName,"direct");
       String queue = channel.queueDeclare().getQueue();
       channel.queueBind(queue,exchangeName,"info");
       channel.queueBind(queue,exchangeName,"error");
       channel.queueBind(queue,exchangeName,"warning");
       //消费消息
       channel.basicConsume(queue,true,new DefaultConsumer(channel){
           @Override
           public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
               System.out.println("消费者2: "+new String(body));
          }
      });
  }
}

运行结果:

direct交换机:

15.png

16.png

17.png

因为在消费者1中队列和交换机绑定的路由key为error,然而生产者发送消息时指定的路由key为info,所以消费者1自然就收不到消息。

18.png

19.png

4.5 Routing-Topic

20.png

Topic类型的交换机exchange与Direct类型的交换机相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型的交换机Exchange可以让队列在绑定RoutingKey的时候使用通配符。

Topic模型的通配符一般是由一个或者多个单词组成,多个单词之间以“.”分割。例如:item.insert.

1
2
3
4
5
6
csharp复制代码// 通配符
* 仅仅匹配一个词
# 匹配零个、一个、或多个词
例子:
   qingyuan.#  匹配qingyuan qingyuan.test qingyuan.test.ok
   qingyuan.*  匹配qingyuan.success

生产者:

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
java复制代码/**
* Topic-生产者
*/
public class Provider {
   public static void main(String[] args) throws IOException, TimeoutException {
       //创建连接mq的连接工厂对象
       ConnectionFactory connectionFactory = new ConnectionFactory();
       //设置连接rabbitmq的主机
       connectionFactory.setHost("yourserverip");
       //设置端口号
       connectionFactory.setPort(5672);
       //设置连接的虚拟主机
       connectionFactory.setVirtualHost("/demo1");
       //设置访问虚拟主机的用户名和密码
       connectionFactory.setUsername("qingyuan");
       connectionFactory.setPassword("qingyuan");
       //获取连接对象
       Connection connection = connectionFactory.newConnection();
       //获取通道
       Channel channel = connection.createChannel();
​
       //声明交换机
       channel.exchangeDeclare("topics","topic");
       //路由key
       String routekey = "save.user.delete.qingyuan";
       channel.basicPublish("topics",routekey,null,("这里是topic动态路由模型,routekey: ["+routekey+"]").getBytes());
​
       //关闭资源
       channel.close();
       connection.close();
​
  }
}

消费者1:

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
java复制代码/**
* topic-消费者1
*/
public class Consumer1 {
   public static void main(String[] args) throws IOException, TimeoutException {
       //创建连接mq的连接工厂对象
       ConnectionFactory connectionFactory = new ConnectionFactory();
       //设置连接rabbitmq的主机
       connectionFactory.setHost("yourserverip");
       //设置端口号
       connectionFactory.setPort(5672);
       //设置连接的虚拟主机
       connectionFactory.setVirtualHost("/demo1");
       //设置访问虚拟主机的用户名和密码
       connectionFactory.setUsername("qingyuan");
       connectionFactory.setPassword("qingyuan");
       //获取连接对象
       Connection connection = connectionFactory.newConnection();
       //获取通道
       Channel channel = connection.createChannel();
​
       //声明交换机
       channel.exchangeDeclare("topics","topic");
       //创建临时队列
       String queue = channel.queueDeclare().getQueue();
       //队列和交换机进行绑定
       channel.queueBind(queue,"topics","*.user.*");
       //消费消息
       channel.basicConsume(queue,true,new DefaultConsumer(channel){
           @Override
           public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
               System.out.println("消费者1: "+ new String(body));
          }
      });
  }
}

消费者2:

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
java复制代码/**
* topic-消费者2
*/
public class Consumer2 {
   public static void main(String[] args) throws IOException, TimeoutException {
       //创建连接mq的连接工厂对象
       ConnectionFactory connectionFactory = new ConnectionFactory();
       //设置连接rabbitmq的主机
       connectionFactory.setHost("yourserverip");
       //设置端口号
       connectionFactory.setPort(5672);
       //设置连接的虚拟主机
       connectionFactory.setVirtualHost("/demo1");
       //设置访问虚拟主机的用户名和密码
       connectionFactory.setUsername("qingyuan");
       connectionFactory.setPassword("qingyuan");
       //获取连接对象
       Connection connection = connectionFactory.newConnection();
       //获取通道
       Channel channel = connection.createChannel();
​
       //声明交换机
       channel.exchangeDeclare("topics","topic");
       //创建临时队列
       String queue = channel.queueDeclare().getQueue();
       channel.queueBind(queue,"topics","*.user.#");
       //消费消息
       channel.basicConsume(queue,true,new DefaultConsumer(channel){
           @Override
           public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
               System.out.println("消费者2: "+ new String(body));
          }
      });
  }
}

运行结果:

21.png

22.png

生产者发送消息设置的路由key为:”save.user.delete.qingyuan”

消费者1的临时队列和交换机绑定的路由key为:”.user.“

消费者2的临时队列和交换机绑定的路由key为:”*.user.#”

消费者1只能匹配user后面只带有一个词的,而消费者2可以匹配user后面带有多个词的。

5.SpringBoot中使用RabbitMQ

5.0 配置

1
2
3
4
5
6
7
8
9
ini复制代码server.port=9090
spring.application.name=rabbitmq-demo
​
## rabbitmq
spring.rabbitmq.host=yourserverip
spring.rabbitmq.port=5672
spring.rabbitmq.username=qingyuan
spring.rabbitmq.password=qingyuan
spring.rabbitmq.virtual-host=/demo1

5.1 HelloWorld

生产者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
less复制代码/**
* helloworld provider
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProviderBoot {
​
   @Autowired
   private RabbitTemplate rabbitTemplate;
​
   @Test
   public void contextLoads(){
       /**
        * @param1 队列名称
        * @param2 发送的消息
        */
       rabbitTemplate.convertAndSend("hello","hello world");
  }
​
}

23.png

消费者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码@Component
@RabbitListener(queuesToDeclare = @Queue(value="hello"))
public class ConsumerBoot {
​
   @Autowired
   RabbitTemplate rabbitTemplate;
​
   @RabbitHandler
   public void reveive(String message){
       System.out.println("message = " + message);
  }
​
}
​

运行SpringBoot启动类:

image-20211120000202410.png

5.2 SpringBoot版Work Queues

生产者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class ProviderBoot {
​
   @Autowired
   RabbitTemplate rabbitTemplate;
​
   @Test
   public void convertAndSend(){
       for(int index =0;index<10;index++){
           rabbitTemplate.convertAndSend("work","hello work");
      }
  }
}

消费者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
less复制代码@Component
public class Consumer {
​
   @Autowired
   RabbitTemplate rabbitTemplate;
​
   @RabbitListener(queuesToDeclare = @Queue(value="work"))
   public void receive1(String message){
       System.out.println("work message1 = " + message);
  }
​
   @RabbitListener(queuesToDeclare = @Queue(value = "work"))
   public void receive2(String message){
       System.out.println("work message2 = " + message);
  }
}

image-20211120001841587.png

同样的,在默认情况下,work queues模式是平均地将消息分配的消费者。

5.3 Fanout模式

fanout啊 topic啊其实说的就是消费端,队列和交换机的绑定关系。

生产者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
less复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class ProviderBoot {
​
   @Autowired
   RabbitTemplate rabbitTemplate;
​
   @Test
   public void convetAndSend(){
       /**
        * @param1 交换机名称
        * @param2 路由key,为空表示任意路由
        * @param3 消息
        */
       rabbitTemplate.convertAndSend("logs2","","this is fanout message");
  }
}

消费者:

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
less复制代码@Component
public class FanoutConsumer {
​
   @Autowired
   RabbitTemplate rabbitTemplate;
​
​
   @RabbitListener(
           bindings = @QueueBinding(
                   value = @Queue, //创建临时队列
                   exchange = @Exchange(name="logs2",type="fanout")
          )
  )
   public void receive1(String message){
       System.out.println("message1 = " + message);
  }
​
​
   @RabbitListener(
           bindings = @QueueBinding(
                   value = @Queue,
                   exchange = @Exchange(name="logs2",type = "fanout")
          )
  )
   public void receive2(String message){
       System.out.println("message2 = " + message);
  }
}

image-20211120004309854.png

5.4 Direct

生产者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
less复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class ProviderBoot {
​
   @Autowired
   RabbitTemplate rabbitTemplate;
​
   @Test
   public void convertAndSend(){
       /**
        * @param1 交换机名称
        * @param2 路由key
        * @param3 消息
        */
       rabbitTemplate.convertAndSend("directs","error","error的日志信息");
  }
}

消费者:

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
less复制代码@Component
public class DirectConsumer {
​
​
   @RabbitListener(
           bindings = {
                  @QueueBinding(
                          value = @Queue, //创建临时队列
                          key = {"info","error"}, //路由key
                          exchange = @Exchange(name = "directs",type = "direct")
                  )
          }
  )
   public void receive1(String message){
       System.out.println("message1 = " + message);
  }
​
   @RabbitListener(
           bindings = {
                   @QueueBinding(
                           value = @Queue,
                           key = {"other"},
                           exchange = @Exchange(name="directs",type = "direct")
                  )
          }
  )
   public void receive2(String message){
       System.out.println("message2 = " + message);
  }
}

image-20211120012341696.png

5.5 Topic

生产者:

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class ProviderBoot {
​
   @Autowired
   RabbitTemplate rabbitTemplate;
​
   @Test
   public void convertAndSend(){
       rabbitTemplate.convertAndSend("topics2","user.save.findAll","user.save.findAll 的消息");
  }
}

消费者:

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
less复制代码@Component
public class TopicConsumer {
​
   @RabbitListener(
           bindings = {
                   @QueueBinding(
                           value = @Queue,
                           key = {"user.*"},
                           exchange = @Exchange(name="topics2",type = "topic")
                  )
          }
  )
   public void receive1(String message){
       System.out.println("message1 = " + message);
  }
​
   @RabbitListener(
           bindings = {
                   @QueueBinding(
                           value = @Queue,
                           key = {"user.#"},
                           exchange = @Exchange(name = "topics2",type = "topic")
                  )
          }
  )
   public void receive2(String message){
       System.out.println("message2 = " + message);
  }
}

运行结果:

image-20211120110429313.png

6.参考资料

1.参考视频

2.rabbitmq安装

3.AMQP 0-9-1 Model Explained

4.从前慢-RabbitMQ

本文转载自: 掘金

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

C语言进阶:指针进阶续

发表于 2021-11-21

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

指针进阶续

续前文《C语言进阶:指针进阶》

回调函数

回调函数定义

回调函数:通过函数指针调用的函数,或者说使用函数指针调用函数这样的机制被称为回调函数。回调函数不由实现方直接调用,而是作为特殊条件下的响应。

概念无关紧要,理解并熟练运用这种方法才更为重要。

快速排序 qsort

qsort函数逻辑
1
c复制代码void qsort(void* base, size_t num, size_t width, int (*cmp)(const void* e1, const void* e2));

qsort无返回值,有四个参数。分别为base:起始地址,num:元素个数,width:元素大小以及compare:比较函数。可与冒泡排序作对比。

1
2
3
4
5
6
7
8
9
10
11
12
13
c复制代码//冒泡排序
void Bubble_sort(int arr[], int sz) {
for (int i = 0; i < sz - 1; i++) {
for (int j = 0; j < sz - 1 - i; j++) {
//比较函数
if (arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}

与冒泡排序作对比发现,冒泡排序仅需起始地址和元素个数即可,暗含了其他信息。由于过度具体化,冒泡排序只能排序整型数组,且比较函数过于简单无需单独列出。

因为qsort排序可适用于多种类型如浮点型,字符型,自定义类型的数据,故无法规定具体类型,所以需要多个参数去描述元素的基本信息。

qsort之所以能够适应多种数据,是因为参数void* base再搭配上num和width就描述出任意一种类型。

为什么将参数base的类型定义为void*呢?如下述代码所示。

1
2
3
4
5
6
c复制代码char* p1 = &a;
//从int*到char*类型不兼容
char* p2 = &f;
//从float*到char*类型不兼容
void* p1 = &a;
void* p2 = &f;

确定类型的地址之间直接赋值会提示类型不兼容,强制转化也可能会导致精度丢失。

故使用无(具体)类型void*,又称通用类型,即可以接收任意类型的指针,但是无法进行指针运算(解引用,±±±整数等)。

1
c复制代码p1++;   *p1;   p1 - p2;   p1 > p2;//表达式必须是指向完整对象类型的指针
  1. base:用于存入数据的起始地址。类型定义为void*,可接受任意类型的指针。
  2. num:待排序的元素个数。
  3. width:元素宽度,所占字节大小。

明确了排序的起始位置,元素个数和元素大小,貌似已经够了。但是并无法排序所有类型,因此必须自定义一个抽象的比较函数指定元素的比较方式。

  1. cmp:比较函数,用于指定元素的比较方式。
* `elem1`小于`elem2`,返回值小于0
* `elem1`大于`elem2`,返回值大于0
* `elem1`等于`elem2`,返回值为0
  1. elem1,elem2:进行比较的两个元素的地址作参数。

qsort可以说是一个半库函数半自定义函数。自定义在于其函数最后一个参数为比较函数,该函数内部实现自由,但返回值必须按照规定返回相应的数值。

小结

需要qsort函数排序各种类型的数据,

  • 故base起始地址不可为固定的指针类型,只能用void*。
  • 既然是通用类型还要明确比较元素的个数和大小。
  • 最后,排序最核心的比较大小,为适应不同的类型元素必须自定义专门的比较函数。
qsort实现冒泡排序
1
2
3
4
5
6
7
8
9
10
11
c复制代码//比较函数:整型
#include <stdlib.h>
int int_cmp(const void* e1, const void* e2) {
return *(int*)e1 - *(int*)e2;
}
int main() {
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), int_cmp);
return 0;
}

比较函数int_com不需要传参,作为回调函数由qsort直接调用。比较函数的传参过程由qsort内部实现。

qsort实现结构体排序
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
c复制代码#include <stdlib.h>
struct stu {
char* name;
short age;
float score;
};
//按照成绩排序
int score_cmp(const void* e1, const void* e2) {
//1.升序
return ((struct stu*)e1)->score - ((struct stu*)e2)->score;
//2.降序
return ((struct stu*)e2)->score - ((struct stu*)e1)->score;
}
//按照名字排序
int name_cmp(const void* e1,const void* e2) {
return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);
}
int main() {
struct stu s[3] = {
{ "张三", 22, 99.5f },{ "李四", 21, 66.4f },{ "王五", 18, 80.1f }
};
int sz = sizeof(s) / sizeof(s[0]);
//1.
qsort(s, sz, sizeof(s[0]), name_cmp);
//2.
qsort(s, sz, sizeof(s[0]), score_cmp);
return 0;
}

由此可得,提取出一个比较函数,具体交换的方式由qsort内部实现。

模拟实现qsort

用qsort的函数逻辑,实现冒泡排序。

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
c复制代码//打印函数
void print_arr(int arr[],int sz) {
for (int i = 0; i < sz; i++) {
printf("%d ", arr[i]);
}
}
//交换函数
void Swap(char* buf1, char* buf2, size_t width) {
for (size_t i = 0; i < width; i++) {//宽度次
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
//比较函数
int cmp(const void* e1, const void* e2) {
return *(int*)e1 - *(int*)e2;
}
//排序函数
void my_bubble_sort(void* base, size_t num, size_t width, int(*cmp)(const void* e1, const void* e2)) {
for (size_t i = 0; i < num - 1; i++) {
for (size_t j = 0; j < num - 1 - i; j++) {
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0) {//以字节为单位
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
}
}
}
int main() {
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
my_bubble_sort(arr, sz, sizeof(arr[0]), cmp);
print_arr(arr, sz);

return 0;
}

地址统一强转为char*,以最小字节单位一个字节进行比较和交换,使代码更具有普适性。

如果需要排序结构体则只需要在前文代码中主函数里替换my_qsort且把比较函数替换Name_cmp即可。

1
2
3
4
c复制代码//1.
my_qsort(s, sz, sizeof(s[0]), name_cmp);
//2.
my_qsort(s, sz, sizeof(s[0]), score_cmp);

指针和数组笔试题解析

数组辨析题

注意点。数组名代表整个数组:

  1. sizeof(数组名)
  2. &数组名

除此以外,数组名都是代表首元素地址。

一维数组
1
2
3
4
5
6
7
8
9
10
11
c复制代码int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a));//16
printf("%d\n", sizeof(a + 0));//4/8
printf("%d\n", sizeof(*a));//4
printf("%d\n", sizeof(a + 1));//4/8
printf("%d\n", sizeof(a[1]));//4
printf("%d\n", sizeof(&a));//4/8
printf("%d\n", sizeof(*&a));//16
printf("%d\n", sizeof(&a + 1));//4/8
printf("%d\n", sizeof(&a[0]));//4/8
printf("%d\n", sizeof(&a[0] + 1));//4/8
  1. 只有数组名单独放在sizeof内部才是整个数组。

a+0放在sizeof内部表示首元素地址+0。
2. 只要是地址,不管是什么类型的地址大小都是4/8

基本类型指针,数组指针,函数指针大小都是4/8个字节,故sizeof(&a)=sizeof(int(*)[4])=4。sizeof()求指针所占字节而不是解引用访问权限大小。
3. *和&在一起会抵消。

sizeof(*&a),&a为整个数组的地址类型int(*)[4],解引用后int[4]大小为16。

字符数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr));//6
printf("%d\n", sizeof(arr + 0));//4/8
printf("%d\n", sizeof(*arr));//1
printf("%d\n", sizeof(arr[1]));//1
printf("%d\n", sizeof(&arr));//4/8
printf("%d\n", sizeof(&arr + 1));//4/8
printf("%d\n", sizeof(&arr[0] + 1));//4/8

printf("%d\n", strlen(arr));//随机值x
printf("%d\n", strlen(arr + 0));//随机值x
printf("%d\n", strlen(*arr));//报错
printf("%d\n", strlen(arr[1]));//报错
printf("%d\n", strlen(&arr));//随机值x
printf("%d\n", strlen(&arr + 1));//随机值x-6
printf("%d\n", strlen(&arr[0] + 1));//随机值x-1
  1. sizeof(*arr),*arr对首元素地址解引用,计算首元素所占空间大小。

strlen(*arr),*arr依然是首元素,strlen把a也就是97当成地址,访问到非法内存所以报错。

2.strlen(&arr)虽然是整个数组的地址,但依然是从首元素开始的,所以strlen依然从第一个元素开始找。

​ strlen(&arr+1),先计算&arr+1然后再传参过去,也就是跳过了整个数组去找。

sizeof和strlen的区别

  • sizeof — 操作符 — 以字节为单位,求变量或类型所创建变量的所占空间的大小

sizoef不是函数,计算类型是必须带上类型说明符()。sizoef内容不参与运算,在编译期间便转化完成。

  • strlen — 库函数 — 求字符串长度即字符个数,遇\0停止。

库函数,计算字符串长度没有遇到\0就会一直持续下去。返回类型size_t,参数char* str ,接收的内容都会认为是char*类型的地址。

一个求变量所占空间,一个求字符串大小,二者本身是没有关系的,但总有人把二者绑在一起“混淆视听”。

字符串数组

首先明确二者的区别:

1
2
3
4
c复制代码//1.字符初始化数组
char arr[] = { 'a','b','c','d','e','f' };//[a] [b] [c] [d] [e] [f]
//2.字符串初始化数组
char arr[] = "abcdef";//[a] [b] [c] [d] [e] [f] [\0]

字符初始化数组,存了什么元素数组里就是什么元素。而字符串初始化数组,除了字符串中可见的字符外,还有字符串末尾隐含的\0。\0存在于字符串的末尾,是自带的,虽不算字符串内容,但是字符串中的字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码char arr[] = "abcdef";
printf("%d\n", sizeof(arr));//7
printf("%d\n", sizeof(arr + 0));//4/8
printf("%d\n", sizeof(*arr));//1
printf("%d\n", sizeof(arr[1]));//1
printf("%d\n", sizeof(&arr));//4/8
printf("%d\n", sizeof(&arr + 1));//4/8
printf("%d\n", sizeof(&arr[0] + 1));//4/8

printf("%d\n", strlen(arr));//6
printf("%d\n", strlen(arr + 0));//6
printf("%d\n", strlen(*arr));//报错
printf("%d\n", strlen(arr[1]));//报错
printf("%d\n", strlen(&arr));//6
printf("%d\n", strlen(&arr + 1));//随机值
printf("%d\n", strlen(&arr[0] + 1));//5
  1. sizeof计算变量的长度,变量可以是数组,数组元素以及指针。数组就是整个数组的大小,数组元素则是数组元素的大小,指针大小都为4/8。
  2. strlen把传过来的参数都当作地址,是地址就从该地址处向后遍历找\0,不是地址当作地址非法访问就报错。
常量字符串
1
c复制代码char* p = "abcdef";

"abcdef"是常量字符串,用一个字符指针p指向该字符串,实质是p存入了首字符a的地址。由于字符串在内存中连续存放,依此特性便可以遍历访问整个字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码char* p = "abcdef";
printf("%d\n", sizeof(p));//4/8
printf("%d\n", sizeof(p + 1));//4/8
printf("%d\n", sizeof(*p));//1
printf("%d\n", sizeof(p[0]));//1
printf("%d\n", sizeof(&p));//4/8
printf("%d\n", sizeof(&p + 1));//4/8
printf("%d\n", sizeof(&p[0] + 1));//4/8

printf("%d\n", strlen(p));//6
printf("%d\n", strlen(p + 1));//5
printf("%d\n", strlen(*p));//报错
printf("%d\n", strlen(p[0]));//报错
printf("%d\n", strlen(&p));//随机值
printf("%d\n", strlen(&p + 1));//随机值
printf("%d\n", strlen(&p[0] + 1));//5
  1. p,p+1,&p,&p+1,&p[0]+1都是地址对于地址sizeof都求得4/8,*p,p[0]是数组元素,sizeof计算元素大小。
  2. p,p+1,&p,&p+1,&p[0]+1都是地址对于地址strlen都向后遍历访问找\0,*p,p[0]是数组元素其对于ASCII值当作地址会访问到非法内存。

p,p+1,&p[0]+1都是字符串字符的地址,&p,&p+1都是指针变量p或其之后的地址。

二维数组

访问数组元素的方式是数组名+[j]。若将二维数组的每一行可以看成一个一维数组,则a[0],a[1],a[2]可以看成“每行“的数组名,和一维数组的数组名具有同样的效果。

  1. 数组名单独放在sizeof()内部代表整个数组
  2. &数组名同样代表整个数组(每行的数组名同样适用)

1
2
3
4
5
6
7
8
9
10
11
12
c复制代码int a[3][4] = { 0 };
printf("%d\n", sizeof(a));//12X4=48
printf("%d\n", sizeof(a[0][0]));//4
printf("%d\n", sizeof(a[0]));//16
printf("%d\n", sizeof(a[0] + 1));//4/8
printf("%d\n", sizeof(*(a[0] + 1)));//4
printf("%d\n", sizeof(a + 1));//4/8 - 第二行地址不代表第二行数组名
printf("%d\n", sizeof(*(a + 1)));//16
printf("%d\n", sizeof(&a[0] + 1));//4/8 - 第二行地址不代表第二行数组名
printf("%d\n", sizeof(*(&a[0] + 1)));//16 - 第二行数组地址解引用为数组名
printf("%d\n", sizeof(*a));//16
printf("%d\n", sizeof(a[3]));//16
  • 对于二维数组来说,sizeof(a[0])求首行的整个数组大小。若是sizeof(a[0]+1)代表首行数组名没有单独放在sizeof()内部,故a[0]退化成了首元素地址。
  • sizeof(a+1)代表第二行的地址仅为地址,但并不能第二行该“一维数组”的数组名,不可与sizeof(a[1])混淆。&a[1]等价于a+1。
  • sizeof(*(a+1)),对第二行的地址解引用,相当于sizeof(int[4])。
  • *(&a[0]+1)第二行数组地址解引用为数组名。数组地址解引用代表整个数组,相当于数组名。切莫将数组地址和数组名混淆。(*&arr=arr)

总结

搞清楚二维数组数组名的意义,必须搞清楚如下变量的含义。

1
2
3
4
5
6
7
c复制代码a[0]//首行数组名
a[0] + 1//首元素地址+1为第二个元素地址
&a[0] + 1//首行地址+1为第二行数组地址(a+1)
a//二维数组名
a + 1//首行地址+1为第二行数组地址
&a + 1//数组地址+1为第二个数组地址
*(a + 1) <=> *(&a[0] + 1)//第二行数组地址解引用为数组名
  1. a是二维数组名,a[0]是首行数组名。
  2. 参与运算后a退化为首行地址,a[0]退化为首元素地址。
  3. &a+1跳过一个二维数组,&a[0]+1跳过一个一维数组。
指针笔试题
Example 1
1
2
3
4
5
6
7
8
c复制代码int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int* ptr = (int*)(&a + 1);
printf("%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
//程序的结果是什么?

指针运算要考虑指针类型,&a+1跳过了int[4]的长度,得到这个位置的地址后指针转化成int*型,此时再+1就只能跳过一个int。

本题考察指针类型决定指针±整数的长度。

Example 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
c复制代码//由于还没学习结构体,这里告知结构体的大小是20个字节
struct Test
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
int main()
{
p = 0x10000000;
printf("%p\n", (struct Test*)p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}

p本是struct Test*的指针,后分别强制转换成unsigned long和unsigned int*类型分别+1跳过多少字节。struct Test*的指针+1跳过一个struct Test字节长度。unsigned long为整数类型+1即整数+1,不属于指针运算。

Example 3
1
2
3
4
5
6
7
8
c复制代码int main()
{
int a[4] = { 1, 2, 3, 4 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)((int)a + 1);
printf("%x,%x", ptr1[-1], *ptr2);
return 0;
}

ptr1和ptr2都是把不同的意义的变量强转成int*类型的地址。先进行一系列的操作后再读取该地址处的后4个字节。

&a类型为int(*)[4]故+1跳过1个数组;a首先为首元素地址强转为int型整数再+1执行整数加法,由于内存以字节为单位,一个字节一个地址,故+1相当于下一个字节的地址。最后都强制转换为int*的指针,都向后访问4个字节。

由于系统为小端存储方案,也就按小端的方式读取数据。以%x的形式打印故不需要我们再去转换成十进制,答案分别为2000000,4。

Example 4
1
2
3
4
5
6
7
8
9
c复制代码#include <stdio.h>
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int* p;
p = a[0];
printf("%d", p[0]);
return 0;
}

这题相对来说很简单,需注意到()内部为逗号表达式,所以数组元素分别为1,3,5,0,0,0 。

Example 5
1
2
3
4
5
6
7
8
c复制代码int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}

本题一眼就可以看到二维数组a[5][5],本应用int(*)[5]的数组指针接收,为什么用4个元素的数组指针接收呢?

其实可以看出,数组在内存中都是连续存放的,对于这“一排“的数据,怎么看是我们的事,把它当成3列的4列的5列甚至是10列的都可以。所以数组指针大小仅仅决定一次访问几个元素,或是说决定了所指数组的列数。

本质上列数的改变并不会影响该二维数组,仅仅影响的是编译器如何看待该数组。

可以看出指针ptr1-ptr2为-4,故%d打印为-4,若以%p打印,因内存中存储的是-4的补码,再以无符号数的十六进制形式打印:

1
2
3
4
复制代码10000000 00000000 00000000 00000100
11111111 11111111 11111111 11111011
11111111 11111111 11111111 11111100
FF FF FF FC
Example 6
1
2
3
4
5
6
7
8
c复制代码int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* ptr1 = (int*)(&aa + 1);
int* ptr2 = (int*)(*(aa + 1));
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}

&aa取出数组地址并+1跳过整个数组,aa相当于首行地址+1为第二行地址并解引用得第二行数组名,数组地址解引用得数组名。

aa+1得到第二行数组的数组地址,再解引用即对数组地址解引用,得到整个数组也就是数组名,*(&arr)=arr 。

Example 7
1
2
3
4
5
6
7
8
9
c复制代码#include <stdio.h>
int main()
{
char* a[] = { "work","at","alibaba" };
char** pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}

指针数组a分别存入"work","at","alibaba"三个字符串的首字符地址。又将指针数组名即首元素地址存入二级指针变量中。指针++访问第二个元素a的地,随后%s打印整个字符串。

Example 8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c复制代码int main()
{
char* c[] = { "ENTER","NEW","POINT","FIRST" };
char** cp[] = { c + 3,c + 2,c + 1,c };
char*** cpp = cp;
//1.
printf("%s\n", **++cpp);
//2.
printf("%s\n", *-- * ++cpp + 3);
//3.
printf("%s\n", *cpp[-2] + 3);
//4.
printf("%s\n", cpp[-1][-1] + 1);
return 0;
}

首先字符指针数组c存有字符串首地址,其次指针数组cp存有”与指针c相关“的二级指针,最后三级指针cpp指向二级指针cp。

本题是最有难度的一题,需要注意到的是指针++--属于自增自减,会影响到本值。

  1. cpp+1指向了数组cp的第二个元素,并解引用得到c+2。再解引用得到"POINT"的首地址。

  1. cpp+1指向数组cp的第三个元素并解引用得到c+1,(c+1)--后将数组cp的第三个元素修改为c,解引用访问数组c的首元素即"ENTER"的首地址再+3,打印出ER。

  1. cp[-2]=*(cp-2)即cpp前移2个元素指向并访问了c+3,并解引用得数组c的第4个元素也就得到了"FIRST"的首地址+3,访问到ST并打印。

cpp-1不是cpp--,虽然效果一样,但是对cpp的意义不同。(cpp-2)并没有改变cpp,所以cpp仍指向cp的第三个元素。

  1. cpp[-1][-1]就相当于*(*(cpp-1)-1)即cpp-1解引用访问到了c+2-1再解引用访问到了数组c的第二个元素再+1,打印出EW。

研究清楚之后再回头看代码其实非常简单,首先了解cpp和cp和c的关系:都是指针并从左向右一次指向,再看有关的操作。

1
2
3
4
c复制代码**++cpp;
*--*++cpp+3;
*cpp[-2]+3;
cpp[-1][-1]+1;

这四行代码其实本质上完全相同,都是1.cpp±整数并解引用;2.cp元素±整数并解引用;3.c元素±整数并解引用。如图所示:

本文转载自: 掘金

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

基于Springboot实现流浪猫救助网站 基于Spring

发表于 2021-11-21

基于Springboot实现流浪猫救助网站

一起用代码吸猫!本文正在参与【喵星人征文活动】。

文章使用技术Spring Boot+MyBatisPlus+MySQL+Redis+Layui

流浪猫狗救助协会,成立于2008年,是一个致力于流浪猫狗救助、领养、文明宣传的非盈利性社会团体我们自成立以来,遵照市农业局、民政局的指导思想,在遵守宪法、法律、法规和国家政策的前提下,尊重生命、保护动物、维护动物的生存权利,保障它们的健康与福利,减少杀戮、虐待、残害、遗弃动物的行为,并通过教育引导正确科学对待伴侣动物的观念,达到人与动物和谐共处的愿景。

网页实现截图:

前端

网站首页 :

image-20211121171156708

关于我们:

image-20211121171228477

领养中心:

image-20211121180021725

领养流程:

image-20211121173101756

活动分享:
image-20211121180103917

团队展示:

点击并拖拽以移动

后台(管理员界面):

人员管理:

image-20211121171806614

活动管理:

image-20211121171940025

主要源码结构:

主代码结构

image-20211121174021937

主要源码展示:

index.html

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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
html复制代码<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Home</title>
<!-- Meta tag Keywords -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8" />
<meta name="keywords" content=""/>
<!--// Meta tag Keywords -->
<!-- css files -->
<link rel="stylesheet" th:href="@{/bootstrap/css/bootstrap.min.css}" type="text/css" media="all">
<!-- Bootstrap-Core-CSS -->
<link rel="stylesheet" th:href="@{/css/font-awesome.css}" type="text/css" media="all">
<!-- Font-Awesome-Icons-CSS -->
<link rel="stylesheet" th:href="@{/css/owl.carousel.css}" type="text/css" media="all" />
<!-- Owl-Carousel-CSS -->
<link rel="stylesheet" th:href="@{/css/style.css}" type="text/css" media="all" />
<!--layui-->
<link th:href="@{/admin/assets/layui/css/layui.css}" rel="stylesheet" type="text/css" media="all"/>
<!-- Style-CSS -->
<!-- //css files -->
<!-- web fonts -->
<link href="http://fonts.googleapis.com/css?family=Molle:400i&amp;subset=latin-ext" rel="stylesheet">
<link href="http://fonts.googleapis.com/css?family=Lato:100,100i,300,300i,400,400i,700,700i,900,900i&amp;subset=latin-ext" rel="stylesheet">
<link href="http://fonts.googleapis.com/css?family=Raleway:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,700i,800,800i,900,900i&amp;subset=latin-ext" rel="stylesheet">

<script th:inline="javascript">
var context = /*[[@{/}]]*/'';
var ctxPath= [[${#httpServletRequest.getContextPath()}]];
// var ctx = '[[@{/}]]';

function changeCode() {
$("#checkCodeImg").attr({"src":ctxPath+"/user/getCode?w="+Math.random()})
}
</script>
</head>

<script>
function userLogin() {
var username = $("#new_loginName").val();
var password = $("#new_loginPwd").val();
var code = $("#login_code").val();
$.post("/user/login?username="+username+"&password="+password+"&code="+code,function (data) {
if (data == "success"){
location.href="/index";
}else if(data=="error"){
alert("账户或密码错误,请重试!");
$("#new_loginName").val("");
$("#new_loginPwd").val("");
$("#login_code").val("");
}else if (data == "codeError") {
alert("验证码错误,请重新输入!");
$("#login_code").val("");
}else{
alert("网络问题,请稍后重试!");
$("#new_loginName").val("");
$("#new_loginPwd").val("");
$("#login_code").val("");
}
})

}
</script>

<body >

<div class="layui-layout layui-layout-admin">

<!--顶部topbar-->
<div th:replace="html/topbar :: topbar"></div>

<!-- 首页轮播图 -->
<div class="w3l-main" id="home1">
<div class="container">
<!-- header -->
<div class="header">
<div class="logo">
<h1>
<a th:href="@{index}">
<img class="logo-img center-block" th:src="@{../images/logo.png}" alt="" /> 流浪猫狗救助
</a>
</h1>
</div>
<div class="clearfix"> </div>
</div>
<!-- //header -->
</div>
<!-- Slider -->
<div class="slider">
<div class="callbacks_container">
<ul class="rslides" id="slider">
<li>
<div class="slider-img-w3layouts one">
<div class="w3l-overlay">
<div class="container">
<div class="banner-text-info">
<h3>我们随时
<span>欢迎</span> 你们来
<span>咨询</span>!
</h3>
<p> 全面的猫狗护理指南,让您的流浪猫狗感受到您的爱</p>
</div>
</div>
</div>
</div>
</li>
<li>
<div class="slider-img-w3layouts two">
<div class="w3l-overlay">
<div class="container">
<div class="banner-text-info">
<h3>你可以展示你的
<span>爱</span> 向你
<span>的流浪猫狗</span>!</h3>
<p> 全面的猫狗护理指南,让您的流浪猫狗感受到您的爱</p>
</div>
</div>
</div>
</div>
</li>
<li>
<div class="slider-img-w3layouts three">
<div class="w3l-overlay">
<div class="container">
<div class="banner-text-info">
<h3>猫狗 是你的
<span>朋友</span>!&nbsp;&nbsp;猫狗是你的
<span>家人</span> !</h3>
<p> 全面的猫狗护理指南,让您的流浪猫狗感受到您的爱</p>
</div>
</div>
</div>
</div>
</li>
<li>
<div class="slider-img-w3layouts four">
<div class="w3l-overlay">
<div class="container">
<div class="banner-text-info">
<h3>猫狗如此
<span>可爱</span> 你们怎么可能
<span>不爱</span>它们!</h3>
<p>全面的猫狗护理指南,让您的流浪猫狗感受到您的爱</p>
</div>
</div>
</div>
</div>
</li>
</ul>
</div>
<div class="clearfix"></div>
</div>
<!--//Slider-->
</div>

<!--底部footer-->
<section th:replace="html/footer :: footer"></section>

<!--个人资料-->
<form id="window" class="layui-form" th:action="@{/user/findPersonInfo}" th:object="${session.user}" style="margin: 5%;display: none;" method="post">
<div class="layui-form-item">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="username" th:value="*{username}" required readonly lay-verify="required" placeholder="冷潇" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">姓名</label>
<div class="layui-input-block">
<input type="text" name="realname" th:value="*{realname}" required lay-verify="required" placeholder="冷潇" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">性别</label>
<div class="layui-input-block">
<input type="radio" name="sex" th:checked="*{sex=='1'}?true:false" value="1" title="男">
<input type="radio" name="sex" th:checked="*{sex=='0'}?true:false" value="0" title="女" >
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">年龄</label>
<div class="layui-input-block">
<input type="text" name="age" th:value="*{age}" required lay-verify="required" placeholder="23" autocomplete="off" class="layui-input">
</div>
<!--<div class="layui-form-mid layui-word-aux">辅助文字</div>-->
</div>
<div class="layui-form-item">
<label class="layui-form-label">电话号码</label>
<div class="layui-input-block">
<input type="text" name="telephone" th:value="*{telephone}" required lay-verify="required" placeholder="15997855562" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">领养动物</label>
<!--<label class="layui-form-label">是否领养</label>-->
<div class="layui-input-block">
<span th:text="${session.pname}">无</span>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">邮箱</label>
<div class="layui-input-block">
<input type="text" name="email" th:value="*{email}" required lay-verify="required" placeholder="123@qq.com" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">地址</label>
<div class="layui-input-block">
<input type="text" name="address" th:value="*{address}" required lay-verify="required" placeholder="河南郑州" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="formDemo">立即提交</button>&nbsp;&nbsp;&nbsp;&nbsp;
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
<input type="hidden" name="id" th:value="*{id}">
<input type="hidden" name="navId" th:value="index">
<div class="layui-form-item">
<p style="color: red">[[${error}]]</p>
</div>
</form>

<!--修改密码-->
<form id="modifyPwd" class="layui-form" th:action="@{/user/modifyPwd}" th:object="${session.user}" style="margin: 5%;display: none;" method="post">
<div class="layui-form-item">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="username" th:value="*{username}" required readonly lay-verify="required" placeholder="冷潇" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">新密码</label>
<div class="layui-input-block">
<input type="text" id="pwd1" name="password" required lay-verify="required|pass" placeholder="请输入新密码" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">确认密码</label>
<div class="layui-input-block">
<input type="text" id="pwd2" name="password1" required lay-verify="required|pwd" placeholder="请确认新密码" autocomplete="off" class="layui-input">
</div>
</div>

<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="formDemo">立即提交</button>&nbsp;&nbsp;&nbsp;&nbsp;
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
<input type="hidden" name="id" th:value="*{id}">
<input type="hidden" name="navId" th:value="index">
<div class="layui-form-item">
<p style="color: red">[[${error}]]</p>
</div>
</form>
</div>

<!-- js 非得用2.0版本得jQuery-->
<script th:src="@{/js/jquery-2.2.3.min.js}"></script>
<script th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
<!-- Necessary-JavaScript-File-For-Bootstrap -->
<!--layui-->
<script th:src="@{/admin/assets/layui/layui.all.js}"></script>
<script th:src="@{/admin/assets/layui/layui.js}"></script>
<script th:src="@{/admin/assets/layui/lay/modules/layer.js}"></script>
<!-- //js -->

<!-- Banner Slider -->
<script th:src="@{/js/responsiveslides.min.js}"></script>
<script>
$(function () {
$("#slider").responsiveSlides({
auto: true,
pager: true,
nav: true,
speed: 1000,
namespace: "callbacks",
before: function () {
$('.events').append("<li>before event fired.</li>");
},
after: function () {
$('.events').append("<li>after event fired.</li>");
}
});
});
</script>
<!-- //Banner Slider -->
<!--layui弹窗-->
<script type="text/javascript">
//个人信息弹窗
layui.use(['layer','form'], function(){
var layer = layui.layer;
var form = layui.form;
var $ = layui.$;
var id = $("#id").val();
var realname = $("#realname").val();
var sex = $("#sex").val();
var age = $("#age").val();
var telephone = $("#telephone").val();
var state = $("#state").val();
var email = $("#email").val();
var address = $("#address").val();
$('#info').on('click', function(){
//页面层
layer.open({
type: 1 //Page层类型
,area: ["550px", "600px"]
,skin: 'layui-layer-molv'
, closeBtn: 1 // 是否显示关闭按钮
, anim: 0 //动画类型
, icon: 1 // icon
,moveType : 0 //拖拽模式,0或者1
,title: ['个人信息','font-size:18px']
// ,btn: ['提交', '取消']
,btnAlign: 'c' // 按钮居中
,shade : 0.4 // 遮罩层透明度
,content:$('#window'),
// success: function(layero, index){
// form.render();//动态渲染
// form.verify();
// form.on('submit(formDemo)', function(data){
// $.ajax({
// url:"user/findPersonInfo",
// // dataType:"json", //返回格式为json
// async:true,
// data:{id:id,realname:realname,sex:sex,age:age,telephone:telephone,
// state:state,email:email,address:address}, //参数值
// type:"post", //请求方式
// success:function(req){
// //请求成功时处理
// console.log(req);
// if(req == "success"){
// layer.msg('修改成功', {icon: 6});
// // layer.close(index); // 关闭弹窗
// setTimeout(ChangeTime, 10000);
// function ChangeTime() {
// location.href = "/index";
// }
// }else{
// layer.msg('修改失败 '+req.msg, {icon: 5});
// }
// },
// error:function(){
// //请求出错处理
// // console.log("00000")
// layer.msg('修改失败', {icon: 5});
// }
// });
// });
// },
// yes: function(index, layero){
//
// // layero.find('.layui-btn').click(); // 这一句就是点击确认按钮触发form的隐藏提交
// }
});
});

// 密码====================================
//密码校验
form.verify({
pass: function (value) {
if (!new RegExp("^[\\S]{6,12}$").test(value)) {
return '密码必须6到12位,且不能出现空格';
}
}
});
$('#Pwd').on('click', function(){
//页面层
layer.open({
type: 1 //Page层类型
,area: ["450px", "350px"]
,skin: 'layui-layer-molv'
, closeBtn: 1 // 是否显示关闭按钮
, anim: 0 //动画类型
, icon: 1 // icon
,moveType : 0 //拖拽模式,0或者1
,title: ['修改密码','font-size:18px']
,btnAlign: 'c' // 按钮居中
,shade : 0.4 // 遮罩层透明度
,content:$('#modifyPwd'),

});
});


// 确认两次密码是否一致
form.verify({
pwd: function () {
var psw1 = $("#pwd1").val();
var psw2 = $("#pwd2").val();
var message = '';
if(psw1!=psw2){
message="两次密码不一致";
}
//需要注意 需要将返回信息写在ajax方法外
if (message !== '')
return message;
}

});
});
</script>

</body>
</html>

备注:部分作品来自于网络收集、侵权立删

本文转载自: 掘金

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

学习设计模式——享元模式

发表于 2021-11-21

今天我们来学习最后一个设计模式:享元模式。

相对来说,享元模式的原理和实现也比较简单,并且在实际的项目开发中也不怎么常用。

概述

享元模式:(Flyweight Design Pattern)运用共享技术有效的支持大量细粒度的对象。

所谓享元,顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。

当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。这样可以减少内存中对象的数量,起到节省内存的目的。

何时使用:

  • 系统中有大量对象。
  • 这些对象消耗大量内存。
  • 这些对象的状态大部分可以外部化。
  • 这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。
  • 系统不依赖于这些对象身份,这些对象是不可分辨的。

UML 类图:

image.png
角色组成:

  1. 抽象享元(Flyweight)角色: 是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
  2. 具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
  3. 非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
  4. 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

实例解析

假设我们要开发一个象棋游戏。一个游戏厅中有成千上万个房间,每个房间对应一个棋局。棋局要保存每个棋子的数据:棋子类型、棋子颜色、棋子在棋局中的位置。利用这些数据,我们就能显示一个完整的棋局给玩家。

此时,我们就可以使用享元模式来实现。具体代码如下:

享元类 ChessPieceUnit.java

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
java复制代码public class ChessPieceUnit {
private int id;
private String text;
private Color color;

public ChessPieceUnit(int id, String text, Color color) {
this.id = id;
this.text = text;
this.color = color;
}

public static enum Color {
RED, BLACK
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getText() {
return text;
}

public void setText(String text) {
this.text = text;
}

public Color getColor() {
return color;
}

public void setColor(Color color) {
this.color = color;
}
}

棋子 ChessPiece.java

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
java复制代码public class ChessPiece {
private ChessPieceUnit chessPieceUnit;
private int positionX;
private int positionY;

public ChessPiece(ChessPieceUnit chessPieceUnit, int positionX, int positionY) {
this.chessPieceUnit = chessPieceUnit;
this.positionX = positionX;
this.positionY = positionY;
}

public ChessPieceUnit getChessPieceUnit() {
return chessPieceUnit;
}

public void setChessPieceUnit(ChessPieceUnit chessPieceUnit) {
this.chessPieceUnit = chessPieceUnit;
}

public int getPositionX() {
return positionX;
}

public void setPositionX(int positionX) {
this.positionX = positionX;
}

public int getPositionY() {
return positionY;
}

public void setPositionY(int positionY) {
this.positionY = positionY;
}
}

享元工厂 ChessPieceUnitFactory.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
arduino复制代码public class ChessPieceUnitFactory {
private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>();

static {
pieces.put(1, new ChessPieceUnit(1,"車", ChessPieceUnit.Color.BLACK));
pieces.put(1, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK));
pieces.put(1, new ChessPieceUnit(3,"相", ChessPieceUnit.Color.BLACK));
// 省略其他棋子代码。。。
}

public static ChessPieceUnit getChessPiece(int chessPieceId) {
return pieces.get(chessPieceId);
}
}

棋局 ChessBoard.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class ChessBoard {
private Map<Integer, ChessPiece> chessPieces = new HashMap<>();

public ChessBoard() {
init();
}

private void init() {
chessPieces.put(1, new ChessPiece(ChessPieceUnitFactory.getChessPiece(1), 0, 0));
chessPieces.put(2, new ChessPiece(ChessPieceUnitFactory.getChessPiece(2), 1, 0));
chessPieces.put(3, new ChessPiece(ChessPieceUnitFactory.getChessPiece(3), 2, 0));
// 省略其他棋子代码。。。
}

public void move(int chessPieceId, int toPositionX, int toPositionY) {
// 省略。。。
}
}

总结

优缺点

  • 优点: 大大减少对象的创建,降低系统的内存,使效率提高。
  • 缺点: 提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。

应用

  • String 常量池、数据库连接池、缓冲池等等。
  • Java Integer 类中、String 类的 字符串常量池。

实际上,享元模式对 JVM 的垃圾回收并不友好。因为享元工厂类一直保存了对享元对象的引用,这就导致享元对象在没有任何代码使用的情况下,也并不会被 JVM 垃圾回收机制自动回收掉。因此,在某些情况下,如果对象的生命周期很短,也不会被密集使用,利用享元模式反倒可能会浪费更多的内存。所以,除非经过线上验证,利用享元模式真的可以大大节省内存,否则,就不要过度使用这个模式,为了一点点内存的节省而引入一个复杂的设计模式,得不偿失。

本文转载自: 掘金

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

(一)Gateway开发教程之主要功能介绍

发表于 2021-11-21

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

SpringCloud中的网关组件

SpringCloud中的网关组件,最先开发出供开发人员使用的,是Zuul,而在其组件之后,Spring官方基于Spring2.0、SpringBoot2.0、Project Reactor等技术开发出来了新的替代品,那就是Gateway。

在Zuul组件的基础上,更精进了一些功能,接下来的系列文章,我们就要围绕着Gateway组件来分享一下开发经验了。

介绍一下Gateway组件

如果要介绍Gateway组件,要先从其特性开始说起。

Gateway大概具有以下几个特性(也可以去官网看一下英文文档):

  • 基于 Spring Framework 5、Project Reactor 和 Spring Boot 2.0
  • 能够匹配任何请求属性的路由
  • 谓词和过滤器特定于路由
  • 断路器集成
  • Spring Cloud DiscoveryClient集成
  • 易于编写谓词和过滤器
  • 请求速率限制
  • 路径重写

为了避免总结的不够到位,这是我从官网上拿过来的,大家勿怪。在这之后,我们会针对这些特性,去体验一下在真正开发中的如何使用的。

微服务中的网关,是作为一个API架构,用于统一管理API,并且提供安全、路由控制、流量控制、隐藏内部具体实现的功能。

Gateway中的一些必备知识点

SpringCloud Gateway中,区分了路由(Router)和过滤器(Filter),而且内置了很规范的使用方法,下面我们会一一描述的。

这里说几个概念,分别是路由、断言、过滤器,由这三个概念开始,就可以基本进入Gateway了。

路由Router

属于网关的基本模块功能,用来声明API的不同转发规则。

断言Predicate

断言是用来匹配来自HTTP请求的任何内容,比如匹配header头部、cookie、url-path链接等等的信息,最终得到匹配的转发规则,此功能也是配合路由Router来使用的。

过滤器Filter

Gateway的过滤器,提供了两种类型的过滤器,分别是Gateway Filter和Global Filter,用于支持网关的基本功能实现。

Gateway可以集成哪些功能?

那么Gateway可以集成哪些功能呢?下面就来细数一下。

  1. 监控
  2. 日志收集统计
  3. 认证授权
  4. 服务熔断
  5. 服务降级
  6. 负载均衡

微服务架构,网关作为所有服务的入口,其本职功能就占了不少的分量,再加上可以集成众多组件功能,那么就更不用说了。

总结

今天我们就开始学习Gateway网关了,非常重要的知识点,希望能更好的分享给大家。

本文转载自: 掘金

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

【Java入门100例】14字符串排序——compareT

发表于 2021-11-21

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

点赞再看,养成习惯。微信搜索【一条coding】关注这个在互联网摸爬滚打的程序员。

本文收录于技术专家修炼,里面有我的学习路线、系列文章、面试题库、自学资料、电子书等。欢迎star⭐️

题目描述

难度:简单

将下列字符串按照字母表顺序排序,"door","apple","Apple","dooe","boy"

输出实例:

1
2
3
4
5
6
7
> 复制代码Apple
> apple
> boy
> dooe
> door
>
>

知识点

  • 双重循环
  • compareTo()

解题思路

按照逐位对比的思路,用for循环一一比对,但是字符串的比较的不像数字可以直接比较大小,除了转换为char类型比较阿斯克码外,还可以使用compareTo()。

compareTo() 方法用于两种方式的比较:

  • 字符串与对象进行比较。
  • 按字典顺序比较两个字符串。

返回值是整型,它是先比较对应字符的大小(ASCII码顺序),如果第一个字符和参数的第一个字符不等,结束比较,返回他们之间的长度差值,如果第一个字符和参数的第一个字符相等,则以第二个字符和参数的第二个字符做比较,以此类推,直至比较的字符或被比较的字符有一方结束。

  • 如果参数字符串等于此字符串,则返回值 0;
  • 如果此字符串小于字符串参数,则返回一个小于 0 的值;
  • 如果此字符串大于字符串参数,则返回一个大于 0 的值。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码/**
* 将若干个字符串按字典顺序排
* 一条coding
*/
public class question_16 {
public static void main(String[] args) {
String a[]={"door","apple","Apple","dooe","boy"};
for(int i=0;i<a.length-1;i++) {
for(int j=i+1;j<a.length;j++) {
if(a[j].compareTo(a[i])<0) {
String temp=a[i];
a[i]=a[j];
a[j]=temp;
}
}
}
for(int i=0;i<a.length;i++) {
System.out.print(a[i]+"\n");
}
}
}

输出结果

扩展总结

在Java8中,利用新特性stream的sort()方法可能更方便的实现排序。

最后

独脚难行,孤掌难鸣,一个人的力量终究是有限的,一个人的旅途也注定是孤独的。当你定好计划,怀着满腔热血准备出发的时候,一定要找个伙伴,和唐僧西天取经一样,师徒四人团结一心才能通过九九八十一难。
所以,

如果你想学好Java

想进大厂

想拿高薪

想有一群志同道合的伙伴

请加入技术交流

本文转载自: 掘金

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

高速PCB设计DDR3阻抗匹配计算及布线

发表于 2021-11-21

1 阻抗模型(信号1层与3层)

image.png
image.png

2 阻抗计算

image.png
image.png
image.png
image.png

2.1 数据线50欧阻抗匹配(线宽5mil)

image.png
image.png
image.png
image.png

2.2 地址线差分100欧阻抗匹配(间距8mil)

image.png
image.png
image.png
image.png

3 布线(xSignal)

3.1 xSignal单根地址线等长操作

image.png
image.png

  • 同时选中,一组
    image.png
  • 以选中的元素,运行XSignal进行等长
    image.png
  • 同时选中,另外一组
    image.png
    image.png
    image.png

3.2 xSignal向导器批量操作

image.png
image.png
image.png

3.3 地址线要求全部等长(SA0约=SA1约=SA2约=SA3=…..=SA15)

因为先总体调整了ADDR_PP1=ADDR_PP2(SA0_PP1=SA0_PP2,SA1_PP1=SA1_PP2),所以最后仅用调整ADDR_PP1与ADDR_PP2的公共部分

3.3.1 先总体调整了ADDR_PP1=ADDR_PP2,SA0_PP1可以不用等于SA1_PP1,先实现局部等长,最后就只用调整公共部分了(SA0_PP1=SA0_PP2,SA1_PP1=SA1_PP2,SA2_PP1=SA2_PP2……)

  • 排序,选择all XSignals,一对一对实现全部等长(SA0_PP1=SA0_PP2,SA1_PP1=SA1_PP2,SA2_PP1=SA2_PP2……)
    image.png
    image.png

3.3.2 最后仅用调整ADDR_PP1与ADDR_PP2的公共部分,因此只用调整ADDR_PP1这组即可`(SA0约=SA1约=SA2约=SA3=…..=SA15)

  • 规则设置,地址线要求最大不超过最大,最小与最大相差50mil
    image.png
    image.png
  • 表层等长效果(2057mil上下浮动50mil)
    因为先总体调整了ADDR_PP1=ADDR_PP2(SA0_PP1=SA0_PP2,SA1_PP1=SA1_PP2),所以最后仅用调整ADDR_PP1与ADDR_PP2的公共部分
    image.png
    image.png
  • 信号层等长效果(2057mil上下浮动50mil)
    image.png
    image.png
    image.png

3.4 数据线组内等长(即BL内部数据线要求等长, 组间不要求)

3.4.1 BL0(即BL0组内部数据线要求等长,即(D0约=D1约=D2约=D3….))

image.png
image.png
image.png
image.png

3.4.2 BL1(即BL1组内部数据线要求等长,即(D8约=D9约=D10约=D11….))

image.png
image.png
image.png
image.png

3.4.3 BL2(即BL2组内部数据线要求等长,即(D16约=D17约=D18约=D19….)

image.png
image.png
image.png

3.4.4 BL3(即BL3组内部数据线要求等长,即(D24约=D25约=D26约=D27….))

image.png
image.png
image.png
image.png
image.png

4 整体效果及覆铜隔离

image.png
image.png
image.png
image.png
image.png
image.png
image.png

本文转载自: 掘金

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

flink sql 知其所以然(十):大家都用 cumula

发表于 2021-11-21

想啥呢,小宝贝,还不三连???(关注 + 点赞 + 再看),对博主的肯定,会督促博主持续的输出更多的优质实战内容!!!

1.序篇

源码公众号后台回复1.13.2 cumulate window 的奇妙解析之路获取。

此节就是窗口聚合章节的第三篇,上节介绍了 1.13 window tvf tumble window 实现,本节主要介绍 1.13. window tvf 的一个重磅更新,即 cumulate window。

本节从以下几个章节给大家详细介绍 cumulate window 的能力。

  1. 应用场景介绍
  2. 预期的效果
  3. 解决方案介绍
  4. 总结及展望篇

2.应用场景介绍

先来一个简单的小调查:在实时场景中,你见到过最多的指标需求场景是哪一种?

答案:博主相信,占比比较多的不是 PCU(即同时在线 PV,UV),而是周期内累计 PV,UV 指标(如每天累计到当前这一分钟的 PV,UV)。因为这类指标是一段周期内的累计状态,对分析师来说更具统计分析价值,而且几乎所有的复合指标都是基于此类指标的统计(不然离线为啥都要一天的数据,而不要一分钟的数据呢)。

本文要介绍的就是周期内累计 PV,UV 指标在 flink 1.13 版本的最优解决方案。

3.预期的效果

先来一个实际案例来看看在具体输入值的场景下,输出值应该长啥样。

指标:每天的截止当前分钟的累计 money(sum(money)),去重 id 数(count(distinct id))。每天代表窗口大小为 1 天,分钟代表移动步长为分钟级别。

来一波输入数据:

time id money
2021-11-01 00:01:00 A 3
2021-11-01 00:01:00 B 5
2021-11-01 00:01:00 A 7
2021-11-01 00:02:00 C 3
2021-11-01 00:03:00 C 10

预期输出数据:

time count distinct id sum money
2021-11-01 00:01:00 2 15
2021-11-01 00:02:00 3 18
2021-11-01 00:03:00 3 28

转化为折线图长这样:

图片

当日累计

可以看到,其特点就在于,每一分钟的输出结果都是当天零点累计到当前的结果。

4.解决方案介绍

4.1.flink 1.13 之前

可选的解决方案有两种

  1. tumble window(1天窗口) + early-fire(1分钟)
  2. group by(1天) + minibatch(1分钟)

但是上述两种解决方案产出的都是 retract 流,关于 retract 流存在的缺点见如下文章:

[

图片

踩坑记 | flink sql count 还有这种坑!

](mp.weixin.qq.com/s?__biz=Mzk…)

并且 tumble window + early-fire 的触发机制是基于处理时间而非事件时间,具体缺点见如下文章:

mp.weixin.qq.com/s/L8-RSS6v3…

4.2.flink 1.13 及之后

诞生了 cumulate window 解法,具体见官网链接:

nightlies.apache.org/flink/flink…

如下官网文档所示,介绍 cumulate window 的第一句话就是 cumulate window 非常适合于之前使用 tumble window + early-fire 的场景。可以说 cumulate window 就是在用户计算周期内累计 PV,UV 指标时,使用了 tumble window + early-fire 后发现这种方案存在了很多坑的情况下,而诞生的!

图片

cumulate window

其计算机制如下图所示:

图片

cumulate window

还是以刚刚的案例说明,以天为窗口,每分钟输出一次当天零点到当前分钟的累计值,在 cumulate window 中,其窗口划分规则如下:

  • [2021-11-01 00:00:00, 2021-11-01 00:01:00]
  • [2021-11-01 00:00:00, 2021-11-01 00:02:00]
  • [2021-11-01 00:00:00, 2021-11-01 00:03:00] …
  • [2021-11-01 00:00:00, 2021-11-01 23:58:00]
  • [2021-11-01 00:00:00, 2021-11-01 23:59:00]

第一个 window 统计的是一个区间的数据;第二个 window 统计的是第一区间和第二个区间的数据;第三个 window 统计的是第一区间,第二个区间和第三个区间的数据。

那么以 cumulate window 实现上述的需求,具体的 SQL 如下:

1
2
3
4
5
6
7
8
9
10
11
sql复制代码SELECT UNIX_TIMESTAMP(CAST(window_end AS STRING)) * 1000 as window_end, 
      window_start, 
      sum(money) as sum_money,
      count(distinct id) as count_distinct_id
FROM TABLE(CUMULATE(
         TABLE source_table
         , DESCRIPTOR(row_time)
         , INTERVAL '60' SECOND
         , INTERVAL '1' DAY))
GROUP BY window_start, 
        window_end

其中 CUMULATE(TABLE source_table, DESCRIPTOR(row_time), INTERVAL '60' SECOND, INTERVAL '1' DAY) 中的INTERVAL '1' DAY 代表窗口大小为 1 天,INTERVAL '60' SECOND,窗口划分步长为 60s。

其中 window_start, window_end 字段是 cumulate window 自动生成的类型是 timestamp(3)。

window_start 固定为窗口的开始时间。window_end 为一个子窗口的结束时间。

最终结果如下。

输入数据:

row_time id money
2021-11-01 00:01:00 A 3
2021-11-01 00:01:00 B 5
2021-11-01 00:01:00 A 7
2021-11-01 00:02:00 C 3
2021-11-01 00:03:00 C 10

输出数据:

window_end window_start sum_money count_distinct_id
2021-11-21T00:01 1635696000000 2 15
2021-11-21T00:02 1635696000000 3 18
2021-11-21T00:03 1635696000000 3 28

Notes:天级别窗口划分的时候一定要注意时区问题喔!nightlies.apache.org/flink/flink…

4.3.cumulate window 原理解析

首先 cumulate window 是一个窗口,其窗口计算的触发也是完全由 watermark 推动的。与 tumble window 一样。

以上述天窗口分钟累计案例举例:cumulate window 维护了一个 slice state 和 merged state,slice state 就是每一分钟内窗口数据(叫做切片),merged state 的作用是当 watermark 推动到下一分钟时,这一分钟的 slice state 就会被 merge 到 merged stated 中,因此 merged state 中的值就是当天零点到当前这一分钟的累计值,我们的输出结果就是从 merged state 得到的。

4.4.cumulate window 怎么解决 tumble window + early-fire 的问题

  1. 问题1:tumble window + early-fire 处理时间触发的问题。

cumulate window 可以以事件时间推进进行触发。

  1. 问题1:tumble window + early-fire retract 流问题。

cumulate window 是 append 流,自然没有 retract 流的问题。

5.总结

源码公众号后台回复1.13.2 cumulate window 的奇妙解析之路获取。

本文主要介绍了 window tvf 实现的 cumulate window 聚合类指标的场景案例以及其运行原理:

  1. 介绍了周期内累计 PV,UV 是我们最常用的指标场景质疑。
  2. 在 tumble window + early-fire 或者 groupby + minibatch 计算周期内累计 PV,UV 存在各种问题是,诞生了 cumulate window 帮我们解决了这些问题,并以一个案例进行说明。

[

图片

当我们在做流批一体时,我们在做什么?

](mp.weixin.qq.com/s?__biz=Mzk…)

[

图片

flink sql 知其所以然(九):window tvf tumble window 的奇思妙解

](mp.weixin.qq.com/s?__biz=Mzk…)

[

图片

flink sql 知其所以然(八):flink sql tumble window 的奇妙解析之路

](mp.weixin.qq.com/s?__biz=Mzk…)

[

图片

flink sql 知其所以然(七):不会连最适合 flink sql 的 ETL 和 group agg 场景都没见过吧?

](mp.weixin.qq.com/s?__biz=Mzk…)

[

图片

flink sql 知其所以然(六)| flink sql 约会 calcite(看这篇就够了)

](mp.weixin.qq.com/s?__biz=Mzk…)

[

图片

flink sql 知其所以然(五)| 自定义 protobuf format

](mp.weixin.qq.com/s?__biz=Mzk…)

本文使用 文章同步助手 同步

本文转载自: 掘金

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

1…249250251…956

开发者博客

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