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

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


  • 首页

  • 归档

  • 搜索

MyBatis之动态SQL的使用

发表于 2021-11-15

MyBatis之动态SQL的使用

这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战
什么是动态SQL:简单来说就是根据不同的的条件生成不同的sql语句

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。

如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。
主要有

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

数据库准备,创建一个blok的表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码create table blog(
id int primary key comment '博客id',
title varchar(100) not null comment '博客标题',
author varchar(30) not null comment '博客作者',
create_time varchar(50) not null comment '创建时间',
views int(30) not null comment '浏览量'
)

insert into blog values(1,'javaWeb教程','黑马程序员',now(),1000)
insert into blog values(2,'安卓软件开发','周世凯,陈小龙',now(),1000)
insert into blog values(3,'数据结构','清华大学出版社',now(),10000)
insert into blog values(4,'人文基础与应用','毛灿月',now(),560)
insert into blog values(5,'java教程','小钱',now(),123456)
insert into blog values(6,'C语言','谭浩强',now(),10000)
insert into blog values(7,'C语言','小毛',now(),10000)

image.png

编写实体类

1
2
3
4
5
6
7
8
java复制代码@Data
public class Blog {
private int id;
private String title;
private String author;
private String create_Time;
private int views;
}

1、if语句

编写接口

1
2
java复制代码//    通过if,查询博客
List<Blog> queryBlogIF(Map map);

编写Mapper.xml的sql语句

1
2
3
4
5
6
7
8
9
10
11
xml复制代码    <select id="queryBlogIF" parameterType="map" resultType="pojo.Blog">
select * from blog
<where>
<if test="title!=null">
and title=#{title}
</if>
<if test="author!=null">
and author=#{author}
</if>
</where>
</select>

如果不传入 “title”,那么所有的 Blog 都会返回;如果传入了 “title” 参数,那么就会对 “title” 一列进行查找并返回对应的 Blog结果

where标签会在下面介绍

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    @Test
public void queryBlogIF(){
SqlSession sqlSession = Mybatisutil.getSqlSession();
BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);

HashMap map = new HashMap();
map.put("title","C语言");
map.put("author","谭浩强");
List<Blog> blogs = mapper.queryBlogIF(map);
for (Blog blog : blogs) {
System.out.println(blog);
}
sqlSession.close();
}

结果

在这里插入图片描述

2、choose、when、otherwise

有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。

编写接口

1
2
java复制代码   //    通过choose,查询博客
List<Blog> queryBlogChoose(Map map);

编写Mapper.xml的sql语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码   <select id="queryBlogChoose" parameterType="map" resultType="pojo.Blog">
select * from blog
<where>
<choose>
<when test="title!=null">
title=#{title}
</when>
<when test="author!=null">
and author=#{author}
</when>
<otherwise>
and views=#{views}
</otherwise>
</choose>
</where>
</select>

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码    //通过choose查询
@Test
public void queryBlogChoose(){
SqlSession sqlSession = Mybatisutil.getSqlSession();
BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);

HashMap map = new HashMap();
map.put("title","C语言");
map.put("author","谭浩强");
List<Blog> blogs = mapper.queryBlogChoose(map);
for (Blog blog : blogs) {
System.out.println(blog);
}
sqlSession.close();
}

结果
在这里插入图片描述

当title满足要求时,即’break‘退出choose选择,就不会执行下面sql语句的拼接,和switch一样,所以这里查出来的有两条记录,当我只传入views参数时候,就会拼接 otherwise标签的语句,结果如下

image.png

3、trim、where、set

在我们拼接语句的时候,会有标点符号,前缀后缀符号等问题,这个时候可以用trim来解决。

如下面的sql语句,假如没有使用where标签,那么如果if语句中没有满足条件的拼接语句,这个时候我们的sql语句就是select * from blog where,这样的sql语句就是错误的,那么where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。

1
2
3
4
5
6
7
8
9
10
xml复制代码    <select id="queryBlogIF" parameterType="map" resultType="pojo.Blog">
select * from blog
where
<if test="title!=null">
and title=#{title}
</if>
<if test="author!=null">
and author=#{author}
</if>
</select>

如果 where 元素与你期望的不太一样,你也可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:

1
2
3
xml复制代码<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>

prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。

用于动态更新语句的类似解决方案叫做 set。set 元素可以用于动态包含需要更新的列,忽略其它不更新的列。比如:

1
2
3
4
5
6
7
8
9
10
xml复制代码<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>

这个例子中,set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。

来看看与 set 元素等价的自定义 trim 元素吧:

1
2
3
xml复制代码<trim prefix="SET" suffixOverrides=",">
...
</trim>

3.1、set修改数据

编写接口

1
2
java复制代码    //使用set修改数据
int updateBlog(Map map);

编写Mapper.xml的sql语句

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<update id="updateBlog" parameterType="map">
update blog
<set>
<if test="title!=null">
title=#{title},
</if>
<if test="author!=null">
author=#{author}
</if>
</set>
where id=#{id}
</update>

set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码    @Test
public void updateBlog(){
SqlSession sqlSession = Mybatisutil.getSqlSession();
BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);

HashMap map = new HashMap();
//map.put("title","C语言");
map.put("author","小浩强");
map.put("id",6);
mapper.updateBlog(map);

sqlSession.close();
}

结果

image.png

4、Foreach

动态 SQL 的另一个常见使用场景是对集合进行遍历

编写接口

1
2
java复制代码  //使用foreach查询3 4 5 条博客
List<Blog> queryBlogForeach(Map map);

编写Mapper.xml的sql语句

1
2
3
4
5
6
7
8
xml复制代码    <select id="queryBlogForeach" parameterType="map" resultType="pojo.Blog">
select * from blog
<where>
<foreach collection="ids" item="id" open="and (" close=")" separator="or">
id=#{id}
</foreach>
</where>
</select>

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码    @Test
public void queryBlogForeach(){
SqlSession sqlSession = Mybatisutil.getSqlSession();
BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);

HashMap map = new HashMap();
ArrayList<Integer> idlist = new ArrayList<>();
idlist.add(3);
idlist.add(4);
idlist.add(5);
map.put("idlist",idlist);
List<Blog> blogs = mapper.queryBlogForeach(map);
for (Blog blog : blogs) {
System.out.println(blog);
}
sqlSession.close();
}

结果:

在这里插入图片描述

foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符,看它多智能!

提示 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。

5、SQL片段

我们会将公共的部分提取出来,方便复用

使用:

  1. 使用SQL标签提取公共的部分
1
2
3
4
5
6
7
8
9
xml复制代码<!--    将公共部分,需要重复使用的sql提取出来,使用sql标签-->
<sql id="if-titlt-author">
<if test="title!=null">
title=#{title},
</if>
<if test="author!=null">
and author=#{author}
</if>
</sql>
  1. 在需要使用的地方使用include标签
1
2
3
4
5
6
7
xml复制代码<!--    在需要使用的地方使用include标签-->
<select id="queryBlogIF" parameterType="map" resultType="pojo.Blog">
select * from blog
<where>
<include refid="if-titlt-author"></include>
</where>
</select>

注意事项:

  • 最好基于单表来定义SQL片段
  • 不要存在where标签

总结:

  • 动态SQl就是在拼接SQL语句,我们只要保证SQL的正确性,按照SQL的格式,去排列就行
  • 书写sql语句之前先在sql查询环境中测试一下,避免写错,在编写Mappre.xml的时候找BUG。

本文转载自: 掘金

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

LeetCode 133 克隆图【c++/java详细题解

发表于 2021-11-15

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

1、题目

给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。

图中的每个节点都包含它的值 val(int) 和其邻居的列表(list[Node])。

1
2
3
4
kotlin复制代码class Node {
public int val;
public List<Node> neighbors;
}

测试用例格式:

简单起见,每个节点的值都和它的索引相同。例如,第一个节点值为 1(val = 1),第二个节点值为 2(val = 2),以此类推。该图在测试用例中使用邻接列表表示。

邻接列表 是用于表示有限图的无序列表的集合。每个列表都描述了图中节点的邻居集。

给定节点将始终是图中的第一个节点(值为 1)。你必须将 给定节点的拷贝 作为对克隆图的引用返回。

示例 1:

1
2
3
4
5
6
7
8
lua复制代码输入:adjList = [[2,4],[1,3],[2,4],[1,3]]
输出:[[2,4],[1,3],[2,4],[1,3]]
解释:
图中有 4 个节点。
节点 1 的值是 1,它有两个邻居:节点 2 和 4 。
节点 2 的值是 2,它有两个邻居:节点 1 和 3 。
节点 3 的值是 3,它有两个邻居:节点 2 和 4 。
节点 4 的值是 4,它有两个邻居:节点 1 和 3 。

示例 2:

在这里插入图片描述

1
2
3
lua复制代码输入:adjList = [[]]
输出:[[]]
解释:输入包含一个空列表。该图仅仅只有一个值为 1 的节点,它没有任何邻居。

示例 3:

1
2
3
ini复制代码输入:adjList = []
输出:[]
解释:这个图是空的,它不含任何节点。

示例 4:

在这里插入图片描述

1
2
lua复制代码输入:adjList = [[2],[1]]
输出:[[2],[1]]

提示:

  • 节点数不超过 100 。
  • 每个节点值 Node.val 都是唯一的,1 <= Node.val <= 100。
  • 无向图是一个简单图,这意味着图中没有重复的边,也没有自环。
  • 由于图是无向的,如果节点 p 是节点 q 的邻居,那么节点 q 也必须是节点 p 的邻居。
  • 图是连通图,你可以从给定节点访问到所有节点。

2、思路

(哈希,dfs) O(n)O(n)O(n)

给定一个无向连通图,要求复制这个图,但是其中的节点不再是原来图节点的引用。我们可以从题目给定的节点引用出发,深度优先搜索遍历整个图,在遍历的过程中完成图的复制。


为了防止多次遍历同一个节点,我们需要建立一个哈希表hash, 来记录源节点到克隆节点之间的映射关系。在dfs搜索过程中,如果当前正在搜索的节点node出现在了哈希表中,就说明我们已经遍历完了整个无向图,此时就可以结束搜索过程。


dfs函数设计:

1
c复制代码 Node* dfs(Node* node)

node是当前搜索到的节点,函数的返回值为Node类型。

搜索边界:

  • if(hash[node]) return hash[node],如果node节点已经被访问过了,此时就可以直接从哈希表hash中取出对应的克隆节点返回。

具体过程如下:

  • 1、从node节点开始dfs遍历整个图。
  • 2、克隆当前节点node,并使用哈希表hash存贮源节点到克隆节点之间的映射。
  • 3、递归调用当前节点node的邻接节点neighbors,并进行克隆,最后将这些克隆的邻接节点加入克隆节点的邻接表中。
  • 4、最后返回已经被访问过的节点的克隆节点。

时间复杂度分析: O(n)O(n)O(n),其中 nnn 表示节点数量。dfs遍历图的过程中每个节点只会被访问一次。

3、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
c复制代码/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> neighbors;
Node() {
val = 0;
neighbors = vector<Node*>();
}
Node(int _val) {
val = _val;
neighbors = vector<Node*>();
}
Node(int _val, vector<Node*> _neighbors) {
val = _val;
neighbors = _neighbors;
}
};
*/

class Solution {
public:
unordered_map<Node* ,Node*>hash;
Node* cloneGraph(Node* node) {
if(!node) return NULL;
return dfs(node);
}
Node* dfs(Node* node)
{
//node节点已经被访问过了,直接从哈希表hash中取出对应的克隆节点返回。
if(hash[node]) return hash[node];
Node* clone = new Node(node->val); //克隆节点
hash[node] = clone; //建立源节点到克隆节点的映射
for(Node* ver: node->neighbors) //克隆边
{
clone->neighbors.push_back(dfs(ver));
}
return clone;
}
};

4、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
40
41
42
43
44
java复制代码/*
// Definition for a Node.
class Node {
public int val;
public List<Node> neighbors;

public Node() {
val = 0;
neighbors = new ArrayList<Node>();
}

public Node(int _val) {
val = _val;
neighbors = new ArrayList<Node>();
}

public Node(int _val, ArrayList<Node> _neighbors) {
val = _val;
neighbors = _neighbors;
}
}
*/

class Solution {
Map<Node,Node> map = new HashMap<>();
public Node cloneGraph(Node node)
{
if(node == null) return null;
return dfs(node);
}

Node dfs(Node node)
{
//node节点已经被访问过了,直接从哈希表hash中取出对应的克隆节点返回。
if(map.containsKey(node)) return map.get(node);
Node clone = new Node(node.val); //克隆节点
map.put(node,clone); //建立源节点到克隆节点的映射
for(Node ver: node.neighbors) //克隆边
{
clone.neighbors.add(dfs(ver));
}
return clone;
}
}

原题链接: 133. 克隆图
在这里插入图片描述

本文转载自: 掘金

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

Mysql基础篇:必知必会(中)

发表于 2021-11-15

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

示例表

1
2
3
4
5
6
7
8
9
10
mysql复制代码mysql> DESC one_piece;
+---------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+-------------+------+-----+---------+-------+
| id | char(10) | NO | | NULL | |
| pirates | char(10) | NO | | NULL | |
| name | char(10) | NO | | NULL | |
| age | int(11) | YES | | NULL | |
| post | varchar(10) | YES | | NULL | |
+---------+-------------+------+-----+---------+-------+

接着上篇继续!

一、创建计算字段

1.拼接字段

将 name , sex 两列进行合并。并通过 AS 关键字进行给新列赋予别名。

1
2
sql复制代码mysql> SELECT Concat(name, '(', sex, ')') AS new_column
-> FROM one_piece;

2.执行算数计算

通过 quantity (数量)、 price (价格)来计算 total_price (总价)

1
2
3
sql复制代码mysql> SELECT quantity, price,
-> quantity * price AS total_price
-> FROM test

二、函数

常用文本处理函数

函数 说明
LEFT(str, length) 返回指定长度的字符串的左边部分
RIGHT(str, length) 返回指定长度的字符串右边部分
LTRIM(str) 去掉字符串左边的空格
RTRIM(str) 去掉字符串右边的空格
LOWER(str) 将字符串转换为小写
UPPER(str) 将字符串转换为大写
LENGTH(str) 返回字符串的长度

使用 LENGTH(str) 获取字符串的长度。

1
2
sql复制代码mysql> SELECT name, LENGTH(name) AS length
-> FROM one_piece;

日期和时间处理函数

查询在 2000年 出生的人员信息。

1
2
3
sql复制代码mysql> SELECT *
-> FROM test
-> WHERE YEAR(brithday)=2000;

数值处理函数

函数 说明
ABS() 返回一个数的绝对值
COS() 返回一个角度的余弦
SIN() 返回一个角度的正弦
TAN() 返回一个角度的正切
PI() 返回圆周率
EXP() 返回一个数的指数值
SQRT() 返回一个数的平方根

以 ABS() 函数为例

1
2
3
4
5
6
sql复制代码sql> SELECT ABS(-1);
+---------+
| ABS(-1) |
+---------+
| 1 |
+---------+

三、数据聚集

聚集函数

函数 说明
AVG() 返回某列的平均值
COUNT() 返回某列的行数
MAX() 返回某列的最大值
MIN() 返回某列的最小值
SUM() 返回某列值之和

1.AVG() 函数

查询平均 age 。

1
2
sql复制代码mysql> SELECT AVG(age) AS avg_age
-> FROM one_piece

2.COUNT() 函数

两种使用方式:

  • COUNT(*) 对表中行的数目进行计数,包括空值。
1
2
sql复制代码mysql> SELECT COUNT(*) AS num_person
-> FROM one_piece;
  • COUNT(column) 对特定列中非 NULL 行进行计数。
1
2
sql复制代码mysql> SELECT COUNT(name) AS num_name
-> FROM one_piece;

3.MAX() & MIN() 函数

当 column 列为数值列, MAX(column) / MIN(column) 返回 column 列中的最大值 / 最小值。

当 column 列为文本数据, MAX(column) / MIN(column) 返回 column 列数据排序后的最后一行 / 最前面的行。

4.SUM() 函数

SUM() 用来返回指定列值的和(总计)(忽略列值为 NULL 的行)。

1
2
sql复制代码mysql> SELECT SUM(price * quantity) AS total_price
-> FROM test

组合聚集函数

计算 one_piece 表中数据的条数,年龄的最小值、最大值和平均值。

1
2
3
4
5
sql复制代码mysql> SELECT COUNT(*) AS num_person,
-> MIN(age) AS age_min,
-> MAX(age) AS age_max,
-> AVG(age) AS age_avg
-> FROM one_piece;

四、数据分组

数据分组

使用分组将数据分为多个逻辑组, 对每个组进行聚集计算。
例:统计各个海贼团( pirates )的人数。

1
2
3
sql复制代码mysql> SELECT pirates, COUNT(*) AS num_person
-> FROM one_piece
-> GROUP BY pirates;

group by 注意事项:

  • GROUP BY 可以嵌套使用。
  • GROUP BY 子句中列出的每一列都必须是检索列或有效的表达式(但不能是聚集函数)。如果在 SELECT 中使用表达式,则必须在 GROUP BY 子句中指定相同的表达式。不能使用别名。
  • 除聚集计算语句外,SELECT 语句中的每一列都必须在 GROUP BY 子句 中给出。
  • 如果分组列中包含具有 NULL 值的行,则 NULL 将作为一个分组返回。 如果列中有多行 NULL 值,它们将分为一组。
  • GROUP BY 子句必须出现在 WHERE 子句之后,ORDER BY 子句之前。

过滤分组

使用 HAVING 子句在数据分组后进行过滤。

查询海贼团人数在500人以上的 海贼团名称 及 人数。

1
2
3
4
sql复制代码mysql> SELECT pirates, COUNT(*) AS num_person
-> FROM one_piece
-> GROUP BY pirates
-> HAVING COUNT(*) >= 500;

WHERE 与 HAVING 的主要区别:

  • WHERE 在数据分组前进行过滤,HAVING 在数据分组后进行过滤。

SELECT 子句顺序:

子句 说明 是否必须使用
SELECT 要返回的列或表达式 是
FROM 从中检索数据的表 仅在从表选择数据时使用
WHERE 行级过滤 否
GROUP BY 分组说明 仅在按组计算聚集时使用
HAVING 组级过滤 否
ORDER BY 输出排序顺序 否

五、子查询

利用子查询进行过滤

现在查询 草帽海贼团 的排名信息。

1
2
3
4
5
sql复制代码mysql> SELECT rank
-> FROM rank_info
-> WHERE id IN (SELECT id
-> FROM one_piece
-> WHERE pirates = '草帽海贼团');

注意:

  • 在 SELECT 语句中,子查询总是从内向外处理。
  • 作为子查询的 SELECT 语句只能查询单个列。检索多个列会报错。

作为计算字段使用子查询

查询海贼团排名和任务信息,首先从 one_piece 表中根据 id 检索出排名信息,再统计每个冒险团的人数。

1
2
3
4
5
6
sql复制代码mysql> SELECT rank,
-> (SELECT COUNT(*)
-> FROM one_piece AS oe
-> WHERE oe.id = ro.id) AS num_person
-> FROM rank_info AS ro
-> ORDER BY rank;

注意:上面的例子中使用的是 oe.id 和 ro.id ,而不是直接使用 id ,因为在两个表中都有 id 列,在有可能混淆列名时必须使用这种语法。

六、表联结

自联结

假如现在有人不知道 乔巴 所属的海贼团, 想要知道 乔巴 所属海贼团的所有成员名称与赏金。
先看一下子查询的方式:

1
2
3
4
5
sql复制代码mysql> SELECT name, bounty
-> FROM one_piece
-> WHERE pirates = (SELECT pirates
-> FROM one_piece
-> WHERE name = '乔巴');

接下来使用自联结的方式:

1
2
3
4
sql复制代码mysql> SELECT c1.name, c1.bounty
-> FROM Customers AS c1, Customers AS c2
-> WHERE c1.pirates = c2.pirates
-> AND c2.name = '乔巴';

通常情况下,自联结的方式比子查询的方式要快很多。

等值联结

联结是一种机制,用来在一条 SELECT 语句 中关联表,因此称为联结。使用特殊的语法,可以联结多个表返回一组输出,联结在运行时关联表中正确的行。联结不是物理实体。换句话说,它在实际的数据库表 中并不存在。它只在查询执行期间存在。

两表 table1, table2 中数据如下:

1
2
3
4
5
6
7
sql复制代码table1                      table2
+------+------+------+ +------+------+------+
| A | B | C | | C | D | E |
+------+------+------+ +------+------+------+
| 1 | 2 | 3 | | 2 | 3 | 4 |
| 4 | 5 | 6 | | 6 | 7 | 8 |
+------+------+------+ +------+------+------+

现在通过表联结,获取两个表中的数据。

1
2
3
4
5
6
7
8
sql复制代码mysql> SELECT *
-> FROM table1 AS t1, table2 AS t2
-> WHERE t1.C = t2.C;
+------+------+------+------+------+------+
| A | B | C | C | D | E |
+------+------+------+------+------+------+
| 4 | 5 | 6 | 6 | 7 | 8 |
+------+------+------+------+------+------+

注意:上例中WHERE 中限制了联结条件,如果没有条件的话,返回的结果就是两表的笛卡尔积,返回 6 × 9 共 54条数据

内联结

上面的联结准确来说是等值联结,也可以称为内联结,它还有另一种语法。返回的结果以上面相同。

1
2
3
4
5
6
7
8
sql复制代码mysql> SELECT *
-> FROM table1 AS t1 INNER JOIN table2 AS t2
-> ON t1.C = t2.C;
+------+------+------+------+------+------+
| A | B | C | C | D | E |
+------+------+------+------+------+------+
| 4 | 5 | 6 | 6 | 7 | 8 |
+------+------+------+------+------+------+

一般内联结可以用如下图进行表示,取两个表关联字段相同的部分。

自然联结

自然连接是一种特殊的等值连接,它在两个关系表中自动比较相同的属性列,无须添加连接条件,并且在结果中消除重复的属性列。

1
2
3
4
5
6
7
sql复制代码mysql> SELECT *
-> FROM table1 AS t1 NATURAL JOIN table2 t2;
+------+------+------+------+------+
| C | A | B | D | E |
+------+------+------+------+------+
| 6 | 4 | 5 | 7 | 8 |
+------+------+------+------+------+

外联结

左外联结

左外联结,左表( table1 )的记录将会全部表示出来,而右表( table2 )只会显示符合搜索条件的记录。右表记录不足的地方均为 NULL 。

1
2
3
4
5
6
7
8
9
sql复制代码mysql> SELECT *
-> FROM table1 AS t1 LEFT JOIN table2 AS t2
-> ON t1.C = t2.C;
+------+------+------+------+------+------+
| A | B | C | C | D | E |
+------+------+------+------+------+------+
| 4 | 5 | 6 | 6 | 7 | 8 |
| 1 | 2 | 3 | NULL | NULL | NULL |
+------+------+------+------+------+------+

右外联结

右外联结,右表( table2 )的记录将会全部表示出来,而右左表( table1 )只会显示符合搜索条件的记录。左表记录不足的地方均为 NULL 。

1
2
3
4
5
6
7
8
9
sql复制代码mysql> SELECT *
-> FROM table1 AS t1 RIGHT JOIN table2 AS t2
-> ON t1.C = t2.C;
+------+------+------+------+------+------+
| A | B | C | C | D | E |
+------+------+------+------+------+------+
| 4 | 5 | 6 | 6 | 7 | 8 |
| NULL | NULL | NULL | 2 | 3 | 4 |
+------+------+------+------+------+------+

四种联结对比图

内联结 自然联结(去重)
左外联结 右外联结

这就是今天要分享的内容,微信搜 Python新视野,每天带你了解更多有用的知识。更有整理的近千套简历模板,几百册电子书等你来领取哦!

本文转载自: 掘金

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

Java泛型中的类型擦除以及Type接口

发表于 2021-11-15

Java 泛型(generics)是JDK1.5中引入的一个新特性,其本质是参数化类型,解决不确定具体对象类型的问题;其所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

但是在Java中并不是真正的泛型,实际上是“伪泛型”

类型擦除(type Erasure)

为了与之前的版本兼容,JDK1.5中通过类型擦除来增加的泛型功能。Java泛型只是在编译器层次上,在编译后生成的字节码中是不包含泛型中类型的信息的。

通过一个例子来证明类型擦除

1
2
3
4
5
6
7
8
java复制代码public class main {

public static void main(String[] args) {
ArrayList<String> sList = new ArrayList<String>();
ArrayList<Integer> iList = new ArrayList<Integer>();
System.out.println(sList.getClass() == iList.getClass());
}
}

上面定义了两个ArrayList,一个是ArrayList泛型类型的,一个是ArrayList类型的,但是最后打印的是true,说明两个类型相同。

用javap -c看一下生成的生成的字节码

可以看到在字节码中,ArrayList和ArrayList都被编译成了ArrayList类型,可见编译后发生了类型擦除。

  1. 既然编译后发生了类型擦除,那么虚拟机解析、反射等场景是怎么获取到正确的类型的?

在JDk1.5中增加泛型的同时,JCP组织修改了虚拟机规范,增加了Signature、LocalVariableTypeTable新属性。

用javap -v查看一下字节码,在main方法中包含一段

1
2
3
4
bash复制代码LocalVariableTypeTable:
Start Length Slot Name Signature
8 31 1 sList Ljava/util/ArrayList<Ljava/lang/String;>;
16 23 2 iList Ljava/util/ArrayList<Ljava/lang/Integer;>;

LocalVariableTypeTable是一个可选属性,如果存在泛型,则会出现这个属性。在Signature下包含了泛型的信息。

  1. 接下来,看这段代码
1
2
3
java复制代码ArrayList<String> sList = new ArrayList<String>();
sList.add("111");
String s = sList.get(0);

类型擦除之后,当调用sList.get(0)是如何确保返回的值不会和String不匹配呢?

用javap -c查看一下字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
yaml复制代码public class com.example.demo.test.main {
// .....省略
public static void main(java.lang.String[]) throws java.lang.NoSuchFieldException;
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: ldc #4 // String 111
11: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
14: pop
15: aload_1
16: iconst_0
17: invokevirtual #6 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
20: checkcast #7 // class java/lang/String
23: astore_2
24: return
}

在#7处有一个checkcast指令,checkcast用于检查类型强制转换是否可以进行,也就是泛型在获取值的时候进行了强制类型转换。

  1. 再来看看下面这段代码

首先定义一个Java泛型类

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class GenericClass<T> {

private T value;

public T getValue() {
return value;
}

public void setValue(T value) {
this.value = value;
}
}

再定义一个子类继承它

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class GenericClassTest extends GenericClass<Integer> {

@Override
public void setValue(Integer value) {
super.setValue(value);
}

@Override
public Integer getValue(){
return super.getValue();
}
}

在GenericClassTest中将GenericClass的泛型定义为Integer类型,并重写了get和set方法,因为存在类型擦除,父类GenericClass的泛型被擦除了。

用javap -c 查看一下GenericClass编译后的字节码

可以看到类型擦除后泛型变为了Object。那么GenericClass也就变为了

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class GenericClass {

private Object value;

public Object getValue() {
return value;
}

public void setValue(Object value) {
this.value = value;
}
}

这样,父类GenericClass中set和get方法操作的是Object对象,而子类GenericClassTest 操作的是Integer对象,为什么还可以重写?按照正常的继承关系中,这应该是重载。

按照重载的方式试一下

可以看到设置Object对象出现了红波浪线,不允许这样设置,看来确实是重写,而不是重载。为什么会时重写,这不是跟Java多态冲突么?继续往下研究。

现在用javap -c看一下子类GenericClassTest的字节码文件

在GenericClassTest中get和/set方法都有两个,一个是操作Object对象一个是操作Integer对象。

操作Integer对象的是GenericClassTest定义的,操作Object对象的是由编译器生成的。

再用javap -v 查看一下字节码更详细的信息。

编译器生成的两个操作Object对象的方法中多了两个ACC_BRIDGE、ACC_SYNTHETIC标志。

这就是虚拟机解决类型擦除和多态冲突问题的方法:使用桥接方法。

桥接方法方法是由编译器生成的,我们在代码中并不能直接使用,但是可以通过反射拿到桥接方法再使用。

泛型一旦编译过后,类型就被擦除了,那到了运行时,怎么获取泛型信息?这就要使用JDK提供的Type类型接口了。

Type类型

在没有泛型之前,所有的类型都通过Class类进行抽象,Class类的一个具体对象就代表了一个类型。

在JDK1.5增加了泛型之后,扩充了数据类型,将泛型也包含了。

JDK在原来的基础上增加了一个Type接口,它是所有类型的父接口,它的子类有

  • Class类: 原始/基本类型,包括平时我们所有的类、枚举、数组、注解,还有int、float等基本类型
  • ParameterizedType接口:参数化类型,比如List
  • TypeVariable接口:类型变量,比如List中的T就是参数化变量
  • GenericArrayType接口: 数组类型,比如List[]、T[]
  • WildcardType接口:泛型表达式类型,比如List< ? extends Number>

ParameterizedType

参数化类型,即带有参数的类型,也就是带有<>的类型

1
2
3
4
5
6
7
java复制代码public interface ParameterizedType extends Type {
Type[] getActualTypeArguments();

Type getRawType();

Type getOwnerType();
}
  • getActualTypeArguments(): 获取类型内部的参数化类型 比如Map<K,V>里面的K,V类型。
  • getRawType(): 类的原始类型,比如Map<K,V>中的Map类型。
  • getOwnerType(): 获取所有者类型(只有内部类才有所有者,比如Map.Entry他的所有者就是Map),若不是内部类,此处返回null。

实例:

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
java复制代码public class GenericClass<T> {
private List<String> list;
private List<T> tList;

public static void main(String[] args) {
Class<GenericClass> genericClassClass = GenericClass.class;
Field[] declaredFields = genericClassClass.getDeclaredFields();
for (Field declaredField : declaredFields) {
Type genericType = declaredField.getGenericType();
if (genericType instanceof ParameterizedType) {
System.out.println("==========" + genericType.getTypeName() + "======ParameterizedType类型=====");
ParameterizedType parameterizedType = (ParameterizedType) genericType;
System.out.println("getActualTypeArguments:");
Type[] actualTypeArguments = (parameterizedType).getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments) {
System.out.println(" " + actualTypeArgument);
}
Type rawType = (parameterizedType).getRawType();
System.out.println("getRawType:");
System.out.println(" " + rawType);

}
}
}
}

输出

1
2
3
4
5
6
7
8
9
10
vbnet复制代码==========java.util.List<java.lang.String>======ParameterizedType类型=====
getActualTypeArguments:
java.lang.String
getRawType:
interface java.util.List
==========java.util.List<T>======ParameterizedType类型=====
getActualTypeArguments:
T
getRawType:
interface java.util.List

TypeVariable

类型变量,即泛型中的变量,例如:T、K、V等变量,可以表示任何类;

注意: 与ParameterizedType的区别,TypeVariable代表着泛型中的变量,而ParameterizedType则代表整个泛型。比如List中,T是TypeVariable类型,List是ParameterizedType类型

1
2
3
4
5
6
7
8
9
10
java复制代码public interface TypeVariable<D extends GenericDeclaration> extends Type, AnnotatedElement {

Type[] getBounds();

D getGenericDeclaration();

String getName();
// JDK8新增的
AnnotatedType[] getAnnotatedBounds();
}
  • getBounds():类型对应的上限,默认为Object 可以有多个。比如List< T extends Number & Serializable>中的Number和Serializable
  • getGenericDeclaration(): 获取声明该类型变量实体,比如GenericClass< T>中的GenericClass
  • getName():获取类型变量在源码中定义的名称;

实例:

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
java复制代码public class GenericClass<T extends Number> {
private T t;

public static void main(String[] args) {
Class<GenericClass> genericClassClass = GenericClass.class;
Field[] declaredFields = genericClassClass.getDeclaredFields();
for (Field declaredField : declaredFields) {
Type genericType = declaredField.getGenericType();
if (genericType instanceof TypeVariable) {
System.out.println("==========" + genericType.getTypeName() + "======TypeVariable类型=====");
TypeVariable typeVariable = (TypeVariable) genericType;
Type[] bounds = typeVariable.getBounds();
System.out.println("getBounds:");
for (Type bound : bounds) {
System.out.println(" " + bound);
}
System.out.println("getGenericDeclaration:");
System.out.println(" " + typeVariable.getGenericDeclaration());
System.out.println("getName:");
System.out.println(" " + typeVariable.getName());


}
}
}
}

输出:

1
2
3
4
5
6
7
kotlin复制代码==========T======TypeVariable类型=====
getBounds:
class java.lang.Number
getGenericDeclaration:
class com.example.demo.test.GenericClass
getName:
T

GenericArrayType

泛型数组类型,用来描述ParameterizedType、TypeVariable类型的数组;例如:List[] 、T[]、List[]等。

注意: GenericArrayType是来描述与泛型相关的数组,与String[]、int[]、float[]这种类型不同。

1
2
3
4
java复制代码public interface GenericArrayType extends Type {

Type getGenericComponentType();
}
  • getGenericComponentType():返回泛型数组中元素的Type类型,比如List[] 中的 List

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class GenericClass<T extends Number> {

private List<String>[] lists;
private T[] ts;

public static void main(String[] args) {
Class<GenericClass> genericClassClass = GenericClass.class;
Field[] declaredFields = genericClassClass.getDeclaredFields();
for (Field declaredField : declaredFields) {
Type genericType = declaredField.getGenericType();

if (genericType instanceof GenericArrayType) {
GenericArrayType genericArrayType = (GenericArrayType) genericType;
System.out.println("==========" + genericType.getTypeName() + "======GenericArrayType类型=====");
Type genericComponentType = genericArrayType.getGenericComponentType();
System.out.println("getGenericComponentType:");
System.out.println(" " + genericComponentType);
}
}
}
}

输出:

1
2
3
4
5
6
vbnet复制代码==========java.util.List<java.lang.String>[]======GenericArrayType类型=====
getGenericComponentType:
java.util.List<java.lang.String>
==========T[]======GenericArrayType类型=====
getGenericComponentType:
T

WildcardType

泛型表达式(通配符表达式)。例如:? extend Number、? super Integer。

注意: WildcardType虽然是Type的子接口,但不代表一种类型,,表示的仅仅是类似 ? extends T、? super K这样的通配符表达式。

1
2
3
4
5
6
java复制代码public interface WildcardType extends Type {

Type[] getUpperBounds();

Type[] getLowerBounds();
}
  • getUpperBounds() 获得泛型表达式上界(上限) 获取泛型变量的上边界(extends)
  • getLowerBounds() 获得泛型表达式下界(下限) 获取泛型变量的下边界(super)

实例:

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复制代码public class GenericClass<T extends Number> {
private List<? extends Number> numbers;

private List<? super Integer> integers;

public static void main(String[] args) {
Class<GenericClass> genericClassClass = GenericClass.class;
Field[] declaredFields = genericClassClass.getDeclaredFields();
for (Field declaredField : declaredFields) {
Type genericType = declaredField.getGenericType();
if (genericType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) genericType;

Type[] actualTypeArguments = (parameterizedType).getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments) {
if(actualTypeArgument instanceof WildcardType){
System.out.println("==========" + actualTypeArgument.getTypeName() + "======WildcardType类型=====");
WildcardType wildcardType = (WildcardType) actualTypeArgument;
System.out.println("getUpperBounds:");
Type[] upperBounds = wildcardType.getUpperBounds();
for (Type upperBound : upperBounds) {
System.out.println(" "+ upperBound);
}
System.out.println("getLowerBounds:");
Type[] lowerBounds = wildcardType.getLowerBounds();
for (Type lowerBound : lowerBounds) {
System.out.println(" "+ lowerBound);
}

}
}
}

}
}
}

输出:

1
2
3
4
5
6
7
8
9
vbnet复制代码==========? extends java.lang.Number======WildcardType类型=====
getUpperBounds:
class java.lang.Number
getLowerBounds:
==========? super java.lang.Integer======WildcardType类型=====
getUpperBounds:
class java.lang.Object
getLowerBounds:
class java.lang.Integer

本文转载自: 掘金

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

MySQL学习-一条SQL语句的查询过程

发表于 2021-11-15

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

作者:汤圆

个人博客:javalover.cc

前言

mysql主要分为服务层和存储引擎层,这个分层跟MVC有点类似;

而一条查询语句的执行过程,就是先在服务层做处理:连接器、分析器、优化器、执行器;

然后再调用引擎接口去存储引擎层拿数据;

下面我们就从一条简单的查询语句来介绍下上面的各个步骤:

1
sql复制代码select * from T where id = 10;

本文的介绍都是基于InnoDB引擎

目录

  1. 连接器
  2. 分析器
  3. 优化器
  4. 执行器

正文

1. 连接器

在执行一条查询语句之前,首要条件就是建立客户端和服务端之间的连接;

这里我们用命令行的方式来建立连接:mysql -u root -h localhost -p;,然后根据提示输入密码即可,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码jalon@xxx ~ % mysql -u root -h localhost -p;     
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 13
Server version: 8.0.21 Homebrew

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

这样我们就建立了客户端和服务器之间的连接;

关于连接又分为短连接和长连接:

  • 长连接【推荐】:就是建立连接后,如果持续有请求进来,就一直保持连接;
  • 短连接:就是建立连接后,执行几次查询就断开连接;

为什么我们要推荐长连接呢?

因为建立连接的过程是复杂的,需要消耗很多资源,所以推荐用长连接;

但是长连接这里也有几个点,需要我们注意一下:

  1. 长连接可能导致典型的8小时问题,就是建立长连接后,超过8小时没有请求进来,连接就会断开;此时再去执行请求,就会报错提示连接已断开;
  2. 长连接可能导致OOM,因为建立连接后,后续mysql操作使用的临时内存都是存在连接对象中,如果长连接过多,可能会导致内存占用过大,从而导致OOM(结果就是mysql异常重启);

不过万事都有解决的办法:

  1. 针对8小时问题,我们可以定时发送一个请求,去让连接活跃起来
  2. 针对OOM问题,我们可以定期初始化连接对象(只支持mysql5.7+,命令为mysql_reset_connection),就是只清除连接对象中的临时内存,不清除连接信息,使得连接恢复到刚开始的样子;如果是mysql5.7以下的版本,可以考虑定期断开长连接,重新建立一次连接;

2. 分析器

等到上面的连接建立完成,就该执行分析器了;

但是mysql8.0之前还要先去查询缓存:就是先去查询缓存中查看,前面是否调用过该查询,如果有,则直接返回结果;如果没有再去执行分析器;

之所以mysql8.0没有这个查询缓存的功能,是因为查询缓存功能有点鸡肋了:因为只要一个表中执行了一次更新语句,那么这个表前面的缓存就都失效了;

分析器的作用就是分析这条语句的目的是什么;

分析器主要分为词法分析和语法分析;

  • 词法分析:分析语句的功能(比如select就是查询语句)、表名、字段名;
  • 语法分析:分析语法是否正确(像我们平时看到的报错提示语法错误就是这个语法分析的功能);

3. 优化器

优化器的作用显而易见,就是负责优化SQL语句的;

优化器的功能主要体现在两个方面:

  1. 如果涉及到多个索引,优化器会自动选择最优的索引(也可能会选错)
  2. 如果涉及到多表联合查询,优化器会自动优化表的连接顺序

关于多表联合查询,我们可以看个简单的例子,比如:

1
sql复制代码select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20

如果不优化,那么默认会先去查询t1.c=10,再去查询t2.d=20;

可是如果满足t1.c=10的结果很多,而满足t2.d=20的结果很少,那么这个默认的查询就会很慢;

此时如果有优化器的存在,他就会自动调整连接的顺序,先查询t2.d=20,再去查询t1.c=10;

优化完之后,就会进行到下一步,执行器

4. 执行器

执行器在执行语句之前,会先进行权限检查,即该用户对该表有没有select权限(或者其他权限)

为什么这个权限检查没有放在前面的阶段进行,要等到最后一步呢?

因为有些时候,SQL语句要操作的表并不只是SQL字面上的那些;

比如如果有个触发器,要在执行器阶段才能确定。前面的阶段是无能为力的。

下面我们还是以开头的那个查询语句为例:

1
sql复制代码select * from T where id = 10;

权限检查通过后,会去判断条件id是不是索引:

  • 如果是索引,就会调用InnoDB引擎去表中取出满足条件的第一行,存到结果集,然后重复取满足条件的行
  • 如果不是索引,就会调用InnoDB引擎去表中取出第一行,判断是否id=10;
    • 如果不是,就继续取下一行进行判断
    • 如果是,就存到结果集,然后重复取下一行进行判断;

至此一条SQL语句的查询过程就算完了。

总结

一条SQL的语句的查询过程主要分4个步骤:建立连接、分析语句、优化语句、执行查询

本文转载自: 掘金

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

查找算法——俄罗斯轮盘赌算法(看谁运气不好)

发表于 2021-11-15

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

俄罗斯轮盘赌的基本思想

俄罗斯轮盘赌(Russian roulette)是一种残忍的赌博游戏。俄罗斯轮盘赌的赌具是左轮手枪]和人的性命。俄罗斯轮盘赌的规则很简单:在左轮手枪的六个弹槽中放入一颗或多颗子弹,任意旋转转轮之后,关上转轮。游戏的参加者轮流把手枪对着自己的头,扣动板机;中枪的当然是自动退出,怯场的也为输,坚持到最后的就是胜者。

只要运气不好,每次都会第一次找到。

步骤

俄罗斯轮盘赌的核心在于装入子弹之后任意旋转转轮,增加了随机性。

  1. 根据随机数生成开始的下标,并进行标记。
  2. 从标记处开始循环顺序查找。
  3. 直到循环一遍还未找到则停止。

算法代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
c++复制代码#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

//俄罗斯轮盘赌算法
void DrawAlgorithm(int nums[],int n,int bullet){
srand((int)time(0));
int begin=rand()%n,mark=begin,count=0; //利用随机数生成第一次比较的下标 begin,mark 对第一次比较的位置进行标记
do{
count++;
if(nums[begin]==bullet){ //查看当前位置的元素是否存在于 bullets 数组
cout<<bullet<<"已找到,查找次数为:"<<count<<endl; //如果存在则命中
return;
}
if(++begin==n){ //判断是否越界
begin=0;
}
}while(mark!=begin); //如果循环一遍代表没有找到
cout<<"没有找到"<<endl;
}

//打印数组
void printNum(int numbers[],int n){
for(int i=0;i<n;i++){
cout<<numbers[i]<<" ";
}
cout<<endl;
}

int main()
{
int numbers[10]={3,44,38,5,47,15,99,32,66,100};
int n=sizeof(numbers)/sizeof(numbers[0]); //数组长度
cout<<"序列为:";
printNum(numbers,n);
DrawAlgorithm(numbers,n,5); //调用 DrawAlgorithm 函数在 numbers 序列中进行抽签查找 5
return 0;
}

算法性能分析

  • 空间复杂度:O(1)O(1)O(1)
  • 时间复杂度
    • 最好情况下时间复杂度:O(1)O(1)O(1)
    • 最坏情况下时间复杂度:O(n)O(n)O(n)

本文转载自: 掘金

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

4-SpringSecurity:CSRF防护

发表于 2021-11-15

背景

本系列教程,是作为团队内部的培训资料准备的。主要以实验的方式来体验SpringSecurity的各项Feature。

接着上一篇文章3-SpringSecurity:自定义Form表单中的项目:spring-security-form,继续演示开启CSRF防护的场景(当时关闭了CSRF:.csrf().disable())。

依赖不变,核心依赖为Web,SpringSecurity与Thymeleaf:

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
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

从官网中可以知道,CSRF防护的关键在于我们发请求时附带一个随机数(CSRF token),而这个随机数不会被浏览器自动携带(eg: Cookie就会被浏览器自动带上)。
2020-12-13-AntiCSRF.png

实验0:登录时的CSRF防护

显然,我们这里的登录请求是个POST方法(SpringSecurity默认忽略”GET”, “HEAD”, “TRACE”, “OPTIONS”等幂等请求的CSRF拦截)。登录时必须携带_csrf参数,与认证信息一并提交,否则报403。

  • 后端安全配置(默认开启CSRF)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/add").hasAuthority("p1")
.antMatchers("/user/query").hasAuthority("p2")
.antMatchers("/user/**").authenticated()
.anyRequest().permitAll() // Let other request pass
.and()
// .csrf().disable() // turn off csrf, or will be 403 forbidden
.formLogin() // Support form and HTTPBasic
.loginPage("/login")
.failureHandler(new AuthenticationFailureHandler(){
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
exception.printStackTrace();
request.getRequestDispatcher(request.getRequestURL().toString()).forward(request, response);
}
});
}
  • 前端模板(新增了_csrf参数):
1
2
3
4
5
6
html复制代码<form action="login" method="post">
<span>用户名</span><input type="text" name="username" /> <br>
<span>密码</span><input type="password" name="password" /> <br>
<span>csrf token</span><input type="text" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> <br>
<input type="submit" value="登录">
</form>

Note:

  1. 当然,实际中可以将新增的_csrf参数作为一个隐藏域进行提交:<input type="text" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" hidden/>
  2. 其实,如果我们使用默认的登录页面,可以在页面元素中看到同样有个隐藏域:

2020-12-13-CSRFHidden.png

实验1:POST接口CSRF防护

通过form表单是一种发送POST请求的方式,但我们其他的请求不可能都通过form表单来提交。下面通过原生的JavaScript发起Ajax的POST请求。

  • 后端接口
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Controller
public class HelloController {
@RequestMapping("/")
public String hello(){
return "index";
}

@PostMapping(value = "/ok")
@ResponseBody
public String ok() {
return "ok post";
}
}
  • 前端模板(新增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
html复制代码<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="csrf" th:content="${_csrf.token}">
<meta name="_csrf_header" th:content="${_csrf.headerName}" />
<title>SpringSecurity</title>
</head>

<body>
<a href="/user/add">添加用户</a>
<a href="/user/query">查询用户</a>
<a href="/logout">退出</a>

<script language="JavaScript">
// let token = document.getElementsByTagName('meta')['csrf'].content;
let token = document.querySelector('meta[name="csrf"]').getAttribute('content');
let header = document.getElementsByTagName('meta')['_csrf_header'].content;
console.log("token: ", token);
console.log("header: ", header);

function click() {
let xhr = new XMLHttpRequest();
xhr.open("POST", "http://localhost:8080/ok", true);
xhr.setRequestHeader(header, token);
xhr.onload = function (e) {
console.log("response: ", e.target.responseText);
}
xhr.onerror = function (e) {
console.log("error: ", e)
}
xhr.send(null);
}
click();
</script>
</body>

2020-12-13-RequestHeader.png

2020-12-13-CSRFPrint.png

Note: 前面这两个实验中用到了一些参数:_csrf.parameterName,_csrf.token,_csrf_header等,这些可以从源码中获悉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {
private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";

private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN";

private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class
.getName().concat(".CSRF_TOKEN");

private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;

private String headerName = DEFAULT_CSRF_HEADER_NAME;

private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;
}

实验2:退出时的CSRF防护

退出url在开启CSRF之后,直接以a标签形式请求/logout(即GET方式)会报404;此时logout必须以POST方式才可以正常退出。

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
java复制代码public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>> extends
AbstractHttpConfigurer<LogoutConfigurer<H>, H> {
private List<LogoutHandler> logoutHandlers = new ArrayList<>();
private SecurityContextLogoutHandler contextLogoutHandler = new SecurityContextLogoutHandler();
private String logoutSuccessUrl = "/login?logout";
private LogoutSuccessHandler logoutSuccessHandler;
private String logoutUrl = "/logout";
private RequestMatcher logoutRequestMatcher;
private boolean permitAll;
private boolean customLogoutSuccess;
...

/**
* The URL that triggers log out to occur (default is "/logout"). If CSRF protection
* is enabled (default), then the request must also be a POST. This means that by
* default POST "/logout" is required to trigger a log out. If CSRF protection is
* disabled, then any HTTP method is allowed.
*
* <p>
* It is considered best practice to use an HTTP POST on any action that changes state
* (i.e. log out) to protect against <a
* href="https://en.wikipedia.org/wiki/Cross-site_request_forgery">CSRF attacks</a>. If
* you really want to use an HTTP GET, you can use
* <code>logoutRequestMatcher(new AntPathRequestMatcher(logoutUrl, "GET"));</code>
* </p>
*
* @see #logoutRequestMatcher(RequestMatcher)
* @see HttpSecurity#csrf()
*
* @param logoutUrl the URL that will invoke logout.
* @return the {@link LogoutConfigurer} for further customization
*/
public LogoutConfigurer<H> logoutUrl(String logoutUrl) {
this.logoutRequestMatcher = null;
this.logoutUrl = logoutUrl;
return this;
}
}

可采用form表单或者Ajax的形式发送POST请求,携带_csrf参数,这里以form表单为例,点击POST logout按钮,可成功退出:

1
2
3
4
html复制代码<form action="logout" method="post">
<input type="text" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" hidden/> <br>
<input type="submit" value="POST logout">
</form>

实验3:前后端分离时的CSRF防护

前面是通过在模板引擎中接收后端传回的_csrf,这里演示下前后端分离项目如何实现CSRF防护下的安全请求。

A CsrfTokenRepository that persists the CSRF token in a cookie named “XSRF-TOKEN” and reads from the header “X-XSRF-TOKEN” following the conventions of AngularJS. When using with AngularJS be sure to use withHttpOnlyFalse().

  • 后端安全配置(修改CSRF存储类型:CookieCsrfTokenRepository)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/add").hasAuthority("p1")
.antMatchers("/user/query").hasAuthority("p2")
.antMatchers("/user/**").authenticated()
.anyRequest().permitAll() // Let other request pass
.and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
// .csrf().disable() // turn off csrf, or will be 403 forbidden
.formLogin() // Support form and HTTPBasic
.loginPage("/login")
.failureHandler(new AuthenticationFailureHandler(){
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
exception.printStackTrace();
request.getRequestDispatcher(request.getRequestURL().toString()).forward(request, response);
}
});
}
  • 前端脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
html复制代码</body>
<script>
function getCookie(name) {
let arr = document.cookie.split("; ");
for (let i = 0; i < arr.length; i++) {
let arr2 = arr[i].split("=");
if (arr2[0] == name) {
return arr2[1];
}
}
return "";
}
console.log("XSRF-TOKEN: ", getCookie("XSRF-TOKEN"));
// 之后就可以拿着前面获取到的"XSRF-TOKEN"去请求后端POST等接口了
</script>
</body>

2020-12-13-Cookie.png

Note: 这里大部分同学有个问题:Cookie都被自动带到请求中了,那攻击者不就又可以拿到了吗?

由于Cookie中的信息对于攻击者来说是不可见的,无法伪造的,虽然Cookie被浏览器自动携带了,但攻击者能做的仅仅是用一下Cookie,而Cookie里面到底放了什么内容,攻击者是不知道的,所以将CSRF-TOKEN写在Cookie中是可以防御CSRF的,相比默认的存放在Session中,CSRF-TOKEN写在Cookie中仅仅是换了一个存储位置。

什么时候需要开启CSRF?

2020-12-13-WhenCSRF.png

官方文档建议,但凡涉及到浏览器用户操作,均应启用CSRF防护。

Reference

  • Source Code: Github
  • SpringSecurity官方文档
  • SpringSecurity官方API

If you have any questions or any bugs are found, please feel free to contact me.

Your comments and suggestions are welcome!

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

本文转载自: 掘金

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

Java小知识(五)、异常 异常

发表于 2021-11-15

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

异常

概念

如果某个方法不能按照正常的途径完成任务,就可以通过另一种路径退出方法。在这种情况下会抛出一个封装了错误信息的对象。此时,这个方法会立刻退出同时不返回任何值。另外,调用这个方法的其他代码也无法继续执行,异常处理机制会将代码执行交给异常处理器。

分类

image.png

Throwable 是 Java 语言中所有错误或异常的超类。下一层分为 Error 和 Exception

Error

指程序运行时系统的内部错误和资源耗尽错误。这种是非代码错误,一般由JVM处理,程序不进行捕获

Exception

表示程序可以捕获也可以处理的异常

Exception存在两个分支,一个是运行时异常RuntimeException,一个是CheckedException

RuntimeException

RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。 如果出现RuntimeException,那么一定是程序员的错误。

常见的运行时异常:

异常 描述
ArithmeticException 当出现异常的运算条件时,抛出此异常。例如,一个整数”除以零”时,抛出此类的一个实例。
ArrayIndexOutOfBoundsException 用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。
ArrayStoreException 试图将错误类型的对象存储到一个对象数组时抛出的异常。
ClassCastException 当试图将对象强制转换为不是实例的子类时,抛出该异常。
IllegalArgumentException 抛出的异常表明向方法传递了一个不合法或不正确的参数。
IllegalMonitorStateException 抛出的异常表明某一线程已经试图等待对象的监视器,或者试图通知其他正在等待对象的监视器而本身没有指定监视器的线程。
IllegalStateException 在非法或不适当的时间调用方法时产生的信号。换句话说,即 Java 环境或 Java 应用程序没有处于请求操作所要求的适当状态下。
IllegalThreadStateException 线程没有处于请求操作所要求的适当状态时抛出的异常。
IndexOutOfBoundsException 指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。
NegativeArraySizeException 如果应用程序试图创建大小为负的数组,则抛出该异常。
NullPointerException 当应用程序试图在需要对象的地方使用 null 时,抛出该异常
NumberFormatException 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
SecurityException 由安全管理器抛出的异常,指示存在安全侵犯。
StringIndexOutOfBoundsException 此异常由 String 方法抛出,指示索引或者为负,或者超出字符串的大小。
UnsupportedOperationException 当不支持请求的操作时,抛出该异常。

CheckedException

一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行 try catch,该类异常一般包括几个方面:

  1. 试图在文件尾部读取数据
  2. 试图打开一个错误格式的 URL
  3. 试图根据给定的字符串查找 class 对象,而这个字符串表示的类并不存在

常见的检查异常:

异常 描述
ClassNotFoundException 应用程序试图加载类时,找不到相应的类,抛出该异常。
CloneNotSupportedException 当调用 Object 类中的 clone 方法克隆对象,但该对象的类无法实现 Cloneable 接口时,抛出该异常。
IllegalAccessException 拒绝访问一个类的时候,抛出该异常。
InstantiationException 当试图使用 Class 类中的 newInstance 方法创建一个类的实例,而指定的类对象因为是一个接口或是一个抽象类而无法实例化时,抛出该异常。
InterruptedException 一个线程被另一个线程中断,抛出该异常。
NoSuchFieldException 请求的变量不存在
NoSuchMethodException 请求的方法不存在

处理方式

抛出异常有三个方式:一是 throw,一个 throws,还有一种系统自动抛异常

image.png

throw

只在方法中使用,会指定抛出的具体问题对象,执行throw则一定会抛出具体问题的异常

throws

用在方法后面,可以存在多个异常类;throws只是声明异常,让调用者指定可能存在哪些异常,而不是一定会出现该异常

throw与throws相同点:都只是抛出异常,而不处理异常,异常有上级调用方法处理。

本文转载自: 掘金

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

fastapi微服务系列(1)-之GRPC入门篇 一些微服务

发表于 2021-11-15

一些微服务说明

前言

在转回python之前,其实就对微服务有所尝试,不过当是时也的go-micro-v2来进行了解,当时也只是浅尝辄止,没深入继续深究~

其实微服务这东西没必要为了微服务而微服务吧!除非真的业务需要,其实没必要进行拆分,毕竟加入你只是一个人再干!哈哈那你引入这个微服务的话,估计是要把自己给累趴了!

我这里主要是为了学习而学习的做的示例而已,生产环境的话其实,可能涉及的问题还甚多,我这里主要是总结一些微服务的雏形。

关于微服务

PS:我这里大概率是不会去用nameko,这个框架定格再了19年之后好像就没更新了!而且不具备跨语言的通用性!

参考之前学习笔记大概问的微服务总体的架构就是这样:

图示来源:https://github.com/stack-labs/learning-videos/tree/master/docs/micro-api
image.png

那我们后续的话,使用的是fastapi来做的话,其实它也只是充当我们的里面的聚合服务层。

其实微服务涉及的几个问题点主要有:

  • 如何进行服务的拆分
  • 如何进行服务之间的通信
  • 如何做服务注册和发现(consul,edct)
  • 如何进行服务的配置中心(Nacos,apollo,Spring Cloud Config)
  • API网关做未SLB层处理(goku,kong,apisix)
  • 微服务的相关的链路追踪问题(opentracing)
  • 微服务中的日志聚合问题

所以一个完整的微服务图示应该大概如下:

图示来源:https://github.com/stack-labs/learning-videos/tree/master/docs/micro-api

image.png

fastapi微服务前奏:

1:关于protobuf简述:

  • 1:它是一种清理的高效的接过话的数据存贮格式,是一种数据交换格式
  • 2:高压缩
  • 3:对比XML和JSON的序列化和反序列化压缩传输比较高
  • 4:传输快
  • 5:支持跨语言,跨平台,一种与语言、平台无关,可扩展的序列化结构化数据
  • 6:它只是一个协议可以脱离具体的框架存在
  • 7:接口定义语言(IDL)来描述服务接口和有效负载消息的结构

使用 protobuf 的过程:

1
rust复制代码编写 proto 文件 -> 使用 protoc 编译 -> 添加 protobuf 运行时 -> 项目中集成

更新 protobuf 的过程:

1
rust复制代码修改 proto 文件 -> 使用 protoc 重新编译 -> 项目中修改集成的地方

2:关于GRPC简述

关于RPC

定义:

  • 远程过程调用(Remote Procedure Call)
  • 一台服务器调用另一个服务器上的服务的方法,看起像本地调用一样

常见 RPC 框架

  • gRPC(谷歌)
  • Thrift(脸书-现在改名买它
  • Dubbo(阿里的JAVA系)

定义:

Grpc基于protobuf数据协议rpc框架. 它使用 protobuf 进行数据传输.

grpc的特性:

  • 1:基于c++高性能,且协议基于protobuf序列化和反序列化(和Python中xml和json的rpa框架有别)
  • 2:通同性,跨通用主流的语言(python客户端可以调用Go写的客户端)
  • 3:高性能、通用的开源 RPC 框架
  • 4:更容易地创建分布式应用和服务

grpc-python官方文档:

grpc.github.io/grpc/python…

3: python下进行的grpc框架简单使用体验:

低版本的IDE:

3.1 pychram安装protobuf插件

主要是为了方便识别的对于的protobuf的文件格式:

步骤1- 下载插件ZIP文件::

1
2
3
ruby复制代码https://plugins.jetbrains.com/plugin/8277-protobuf-support
下载地址:
https://plugins.jetbrains.com/plugin/16228-protobuf-support/versions/stable/144595

步骤2- 本地安装插件

步骤3- 重启pychram

重启后就可以正常的识别proto的文件了!

2021版本的话直接搜索:

image.png

安装后可以自动识别:

image.png

3.2 python下的GRPC工具安装:

具体工具包:

1
2
ruby复制代码1:grapio
2:grpcio-tools

安装:

1
2
arduino复制代码pip install grpcio -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install grpcio-tools -i https://pypi.tuna.tsinghua.edu.cn/simple

3.3 官网的 GRPC-PYTHON 体验示例:

相关的示例步骤如下:

1:步骤1 -编写protobuf文件(版本使用3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码syntax = "proto3";

service Greeter {
// 定义PAC对于的具体的服务包含方法
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
string name = 1; //定义我们的服务的一个请求的需要提交的参数
}

message HelloReply {
string message = 1; //我们的请求向移动额报文的字段信息
}

图示:

2:步骤2 -编译 proto 文件

PS:建议注意需要进入的当前的我们的所以在的proto文件下再执行命令:

1
css复制代码python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. hello.proto

关于上述的命令的一些说明:

  • grpc_tools.protoc : 依赖于我们上面安装的grpcio-tools
  • –python_out=. :表示我们的输出编译生成的protobuf文件路径, . 点号 表示的是当前目录下(生成的文件放置到当前目录下)
  • –grpc_python_out=. :表示我们的输出编译生成的grpc的文件路径, . 点号 表示的是当前目录下
  • -I. : 表示输入Input,主要是强调从那个目录下 去找我们的xx.proto 文件 . 点好表示的是从当前的目录下去寻找

PS:只有PY语言会生成两个文件,其他语言都只是一个文件如GO的

上述命令执行后的结果:

PS:需要注意的点,生成的文件的引入的包的路径问题!

生成文件的描述:

  • hello_pb2.py: 是对我们的protobuf 里面定义的请求和响应的等参数数数据封装,使用里面的可以对我们的请求体参数和响应体参数进行实例化的操作等。
  • hello_pb2_grpc.py: 主要是用于针对GRPC服务的生成,当需要生成服务端或者客户端的时候需要依赖这个文件,此文件包含生 客户端(GreeterStub)和服务端(GreeterServicer)的类。

3:步骤3 - 编写grpc的服务端(多线程模式处理并发):

  • 1:基于我们的hello_pb2_grpc实现里面我们的定义的接口

定义一个服务名称,继承我们的hello_pb2_grpc,帮我们的生成的服务名称,并且实现所有的方法

2:把服务注册的rpc服务上

3:进行我们的rpc服务的一些启动配置处理

ps:关于rpc服务的启动有多重方式:

方式1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码def serve():
# 实例化一个rpc服务,使用线程池的方式启动我们的服务
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
# 添加我们服务
hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
# 配置启动的端口
server.add_insecure_port('[::]:50051')
# 开始启动的服务
server.start()、
# --循环-主要是为了目标启动后主进程直接的结束!需要一个循环的方式进行进行进程运行
try:
while True:
time.sleep(60 * 60 * 24) # one day in seconds
except KeyboardInterrupt:
server.stop(0)

方式2:

1
2
3
4
5
6
7
8
9
10
11
scss复制代码def serve():
# 实例化一个rpc服务,使用线程池的方式启动我们的服务
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
# 添加我们服务
hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
# 配置启动的端口
server.add_insecure_port('[::]:50051')
# 开始启动的服务
server.start()
# wait_for_termination --主要是为了目标启动后主进程直接的结束!需要一个循环的方式进行进行进程运行
server.wait_for_termination()

PS:
wait_for_termination 阻塞当前线程,直到服务器停止。

这是一个实验性API。

等待在阻塞期间不会消耗计算资源,它将阻塞直到满足以下两个条件之一:

  1. 停止或终止服务器;
  2. 如果没有超时,则会发生超时。无.

server-完整的服务端实例代码为:

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
python复制代码from concurrent import futures
import time
import grpc
import hello_pb2
import hello_pb2_grpc


# 实现 proto文件中定义的 GreeterServicer的接口
class Greeter(hello_pb2_grpc.GreeterServicer):
# 实现 proto 文件中定义的 rpc 调用
def SayHello(self, request, context):
# 返回是我们的定义的响应体的对象
return hello_pb2.HelloReply(message='hello {msg}'.format(msg=request.name))

def SayHelloAgain(self, request, context):
# 返回是我们的定义的响应体的对象
return hello_pb2.HelloReply(message='hello {msg}'.format(msg=request.name))


def serve():
# 实例化一个rpc服务,使用线程池的方式启动我们的服务
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
# 添加我们服务
hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
# 配置启动的端口
server.add_insecure_port('[::]:50051')
# 开始启动的服务
server.start()
# wait_for_termination --主要是为了目标启动后主进程直接的结束!需要一个循环的方式进行进行进程运行
server.wait_for_termination()

if __name__ == '__main__':
serve()

4:步骤4 - 编写client -grpc的客户端,调用我们的服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
python复制代码#!/usr/bin/evn python
# coding=utf-8

import grpc
import hello_pb2
import hello_pb2_grpc


def run():
# 连接 rpc 服务器
with grpc.insecure_channel('localhost:50051') as channel:
# 通过通道服务一个服务
stub = hello_pb2_grpc.GreeterStub(channel)
# 生成请求我们的服务的函数的时候,需要传递的参数体,它放在hello_pb2里面-请求体为:hello_pb2.HelloRequest对象
response = stub.SayHello(hello_pb2.HelloRequest(name='小钟同学'))
print("SayHello函数调用结果返回:: " + response.message)
response = stub.SayHelloAgain(hello_pb2.HelloRequest(name='欢迎下次光临'))
print("SayHelloAgain函数调用结果的返回: " + response.message)


if __name__ == '__main__':
run()

5:步骤5 - 服务启动:

  • 启动服务端
  • 再启动客户端

客户端最后的输出结果为:

1
2
makefile复制代码SayHello函数调用结果返回:: hello 小钟同学
SayHelloAgain函数调用结果的返回: hello 欢迎下次光临

总结步骤:

  • 1:编写.proto文件定义服务(定义了消息体和服务接口)
  • 2:编译.proto文件,生成具体的服务信息
  • 3:编写客户端和服务端

4: grpc 4个通讯模式(python实现)

不同的业务需求场景,不同的业务模式,不同的通讯模式:

  • 简单模式:请求响应一次调用(也就是客户端请求一次,服务端响应一次)

PS:简单模式也可以叫做一元RPC模式

  • 服务端流模式:客服端一次请求, 服务器多次进行数据流式应答(客户端发送一个对象服务器端返回一个Stream(流式消息))
  • 客户端流模式:客服端多次流式的请求, 发送结束后,服务器一次应答(客户端数据上报)
  • 双向流模式:客服端多次流式的请求,服务器多次进行数据流式应答(类似于WebSocket(长连接),客户端可以向服务端请求消息,服务器端也可以向客户端请求消息))

由于简单模式上面的一有所演示,那么这里我就不演示,下面示例我也是来自官网的示例,我主要是拆分开进行实践体验。

通常情况下流模式主要使用于下面一些场景:

  • 大规模数据包
  • 实时场景数据传输

4.1 服务端流模式示例

定义:

  • 服务端流模式:客服端一次请求, 服务器多次进行数据流式应答(客户端发送一个对象服务器端返回一个Stream(流式消息))

1:步骤1: 编写serverstrem.proto文件定义服务(定义了消息体和服务接口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码syntax = "proto3";

service Greeter {
// 服务端流模式实现
rpc SayHello(HelloRequest) returns (stream HelloReply) {}
}

message HelloRequest {
string name = 1; //定义我们的服务的一个请求的需要提交的参数
}

message HelloReply {
string message = 1; //我们的请求向移动额报文的字段信息
}

2:步骤2 -编译 serverstrem.proto 文件

PS:建议注意需要进入的当前的我们的所以在的proto文件下再执行命令(当前我的示例调整,调整到Demo2包下):

1
css复制代码python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. serverstrem.proto

3:步骤3 - 编写serverstrem_grpc_server.py grpc的服务端:

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
python复制代码
from concurrent import futures
import grpc
from demo2 import serverstrem_pb2_grpc, serverstrem_pb2
import threading
import time
import random

# 实现 proto文件中定义的 GreeterServicer的接口
class Greeter(serverstrem_pb2_grpc.GreeterServicer):
# 实现 proto 文件中定义的 rpc 调用
def SayHello(self, request, context):
# 使用流的方式不断返回给客户端信息
# 检查客户端是否还保持连接状态

while context.is_active():
# 接收到客户端的信息
client_name = request.name
# 使用生成器的方式不安给我们的---返回给客户端发送信息
time.sleep(1)
yield serverstrem_pb2.HelloReply(message=f"{client_name} 啊!我是你大爷!{random.sample('zyxwvutsrqponmlkjihgfedcba',5)}")





def serve():
# 实例化一个rpc服务,使用线程池的方式启动我们的服务
server = grpc.server(futures.ThreadPoolExecutor(max_workers=2))
# 添加我们服务
serverstrem_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
# 配置启动的端口
server.add_insecure_port('[::]:50051')
# 开始启动的服务
server.start()
# wait_for_termination --主要是为了目标启动后主进程直接的结束!需要一个循环的方式进行进行进程运行
server.wait_for_termination()

if __name__ == '__main__':
serve()

上面的流服务的实现的时候,使用的是生成器的方式返回我们的数据流:

1
2
3
4
5
6
python复制代码 while context.is_active():
# 接收到客户端的信息
client_name = request.name
# 使用生成器的方式不安给我们的---返回给客户端发送信息
time.sleep(1)
yield serverstrem_pb2.HelloReply(message=f"{client_name} 啊!我是你大爷!{random.sample('zyxwvutsrqponmlkjihgfedcba',5)}")

PS:上面为了演示关于线程池的问题,我们设定的是只是开启了2两个的线程,这个表示以为的这在这服务端流模式下,我们最多能处理的只有两个客户端连接而已!!!超过2个的话就没办法了!!需要等待!!

4:步骤4 - 编写serverstrem_grpc_client.py grpc的客户端,调用我们的服务端:

客户端拥有一个存根(stub在某些语言中仅称为客户端),提供与服务器相同的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码import grpc
from demo2 import serverstrem_pb2, serverstrem_pb2_grpc


def run():
# 连接 rpc 服务器
with grpc.insecure_channel('localhost:50051') as channel:
# 通过通道服务一个服务
stub = serverstrem_pb2_grpc.GreeterStub(channel)
# 生成请求我们的服务的函数的时候,需要传递的参数体,它放在hello_pb2里面-请求体为:hello_pb2.HelloRequest对象
response = stub.SayHello(serverstrem_pb2.HelloRequest(name='小风学'))
for item in response:
print("SayHello函数调用结果返回:: " + item.message)


if __name__ == '__main__':
run()

注意点:上面我们的接收来自服务端的数据的时候使用的循环方式来接收!:

1
2
3
vbscript复制代码response = stub.SayHello(serverstrem_pb2.HelloRequest(name='小风学'))
for item in response:
print("SayHello函数调用结果返回:: " + item.message)

启动多个客户端的时候,最终我们的客户端输出的信息为:

超过三个则无法输出,需关闭一个客户端后才可以处理:

总结:

1
2
3
scss复制代码1:服务端流其实也是使用某种的循环迭代的方式进行我们的数据的迭代的发送而已!
2:另外根据业务场景来处理是否进行业务服务端业务的中断取消机制,
3:如果需服务端主动的关闭连接的话,需要使用 context.cancel()

补充一个服务端主动的关闭的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码# 实现 proto文件中定义的 GreeterServicer的接口
class Greeter(serverstrem_pb2_grpc.GreeterServicer):
# 实现 proto 文件中定义的 rpc 调用
def SayHello(self, request, context):
# 使用流的方式不断返回给客户端信息
# 检查客户端是否还保持连接状态
idnex = 1
while context.is_active():
# 接收到客户端的信息
idnex=idnex +1
print("服务端的索引:",idnex)
client_name = request.name
# 使用生成器的方式不安给我们的---返回给客户端发送信息
time.sleep(1)
# 如果需要主动的关闭的服务端的话可以使用:
if idnex == 5:
context.cancel()
yield serverstrem_pb2.HelloReply(message=f"{client_name} 啊!我是你大爷!{random.sample('zyxwvutsrqponmlkjihgfedcba',5)}")

当我们的服务端主动的关闭连接后:客户端会进行异常的抛出:

4.2 客户端流模式示例

定义:

  • 客服端多次流式的请求, 发送结束后,服务器一次应答(客户端数据上报)

1:步骤1: 编写serverstrem.proto文件定义服务(定义了消息体和服务接口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码syntax = "proto3";

service Greeter {
// 服务端流模式实现
rpc SayHello(HelloRequest) returns (stream HelloReply) {}
// 新增客户端的流程模式
rpc SayRequestStream(stream HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
string name = 1; //定义我们的服务的一个请求的需要提交的参数
}

message HelloReply {
string message = 1; //我们的请求向移动额报文的字段信息
}

2:步骤2 -更新编译 serverstrem.proto 文件

PS:建议注意需要进入的当前的我们的所以在的proto文件下再执行命令(当前我的示例调整,调整到Demo2包下):

1
css复制代码python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. serverstrem.proto

3:步骤3 - 更新编写serverstrem_grpc_server.py grpc的服务端:

其实只需要新增需要实现的SayRequestStream方法就可以了!

根据我们的对这个模式的定义就是:

  • 客服端多次流式的请求, 发送结束后,服务器一次应答(客户端数据上报),所以我们的服务端需要设计相关的条件,结束客户端的提交,然后返回数据,这个需要结合自己真实的业务场景来处理。

完整代码:

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
python复制代码

from concurrent import futures
import grpc
from demo2 import serverstrem_pb2_grpc, serverstrem_pb2
import threading
import time
import random

# 实现 proto文件中定义的 GreeterServicer的接口
class Greeter(serverstrem_pb2_grpc.GreeterServicer):
# 实现 proto 文件中定义的 rpc 调用
def SayHello(self, request, context):
# 使用流的方式不断返回给客户端信息
# 检查客户端是否还保持连接状态
idnex = 1
while context.is_active():
# 接收到客户端的信息
idnex=idnex +1
print("服务端的索引:",idnex)
client_name = request.name
# 使用生成器的方式不安给我们的---返回给客户端发送信息
time.sleep(1)
# 如果需要主动的关闭的服务端的话可以使用:
if idnex == 5:
context.cancel()
yield serverstrem_pb2.HelloReply(message=f"{client_name} 啊!我是你大爷!{random.sample('zyxwvutsrqponmlkjihgfedcba',5)}")

# 新增处理客户端的流模式的函数,注意下面的request_iterator是一个迭代器的对象
def SayRequestStream(self, request_iterator, context):
pass
# 循环的接收来此客户端每次提交的数据
for curr_request in request_iterator:
# 打印当前客户端的数据信息
print(curr_request.name)
if curr_request.name=="后会有期":
return serverstrem_pb2.HelloReply(message=f"{curr_request.name=} 啊!我们后会有期!")
# 返回最终的服务器一次处理结果




def serve():
# 实例化一个rpc服务,使用线程池的方式启动我们的服务
server = grpc.server(futures.ThreadPoolExecutor(max_workers=2))
# 添加我们服务
serverstrem_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
# 配置启动的端口
server.add_insecure_port('[::]:50051')
# 开始启动的服务
server.start()
# wait_for_termination --主要是为了目标启动后主进程直接的结束!需要一个循环的方式进行进行进程运行
server.wait_for_termination()

if __name__ == '__main__':
serve()

主要是新增服务函数处理:

逻辑说明:

  • 1:服务端一直接收客户端发生的消息,当我接收到后会有期的时候,则结束返回告诉客户端终止提交!
  • 2:并把xxxx 啊!我们后会有期!的结果返回给客户端。

4:步骤4 - 编写serverstrem_grpc_client.py grpc的客户端,调用我们的服务端:

此时是我们的客户端进行流的方式的提交数据给我们的服务端,所以我们的也设计一个迭代的方式自己新年数据的提交:

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
python复制代码#!/usr/bin/evn python
# coding=utf-8

import grpc
from demo2 import serverstrem_pb2, serverstrem_pb2_grpc
import time

def run():
# 连接 rpc 服务器
with grpc.insecure_channel('localhost:50051') as channel:
# 通过通道服务一个服务
stub = serverstrem_pb2_grpc.GreeterStub(channel)
# 生成请求我们的服务的函数的时候,需要传递的参数体,它放在hello_pb2里面-请求体为:hello_pb2.HelloRequest对象
# response = stub.SayHello(serverstrem_pb2.HelloRequest(name='小名同学'))
# for item in response:
# print("SayHello函数调用结果返回:: " + item.message)

def send_action():
for send_name in ['我是你大爷',"我是你小爷",'我是你大舅子',"后会有期"]:
print("send_name:",send_name)
time.sleep(1)
yield serverstrem_pb2.HelloRequest(name=send_name)
# 接收服务端返回的结果
response = stub.SayRequestStream(send_action())
print(response.message)


if __name__ == '__main__':
run()

5:步骤5 - 服务启动:

  • 启动服务端
  • 再启动客户端

客户端最后的输出结果为:

1
2
3
4
5
makefile复制代码send_name: 我是你大爷
send_name: 我是你小爷
send_name: 我是你大舅子
send_name: 后会有期
后会有期 啊!我们后会有期!

服务端输出:

1
2
3
4
复制代码我是你大爷
我是你小爷
我是你大舅子
后会有期

4.2 双向的流模式示例

定义:

  • 客服端多次流式的请求,服务器多次进行数据流式应答(类似于WebSocket(长连接),客户端可以向服务端请求消息,服务器端也可以向客户端请求消息))

1:步骤1: 新增接口-编写serverstrem.proto文件定义服务(定义了消息体和服务接口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码syntax = "proto3";

service Greeter {
// 服务端流模式实现
rpc SayHello(HelloRequest) returns (stream HelloReply) {}
// 新增客户端的流程模式
rpc SayRequestStream(stream HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
string name = 1; //定义我们的服务的一个请求的需要提交的参数
}

message HelloReply {
string message = 1; //我们的请求向移动额报文的字段信息
}

2:步骤2 -更新编译 serverstrem.proto 文件

PS:建议注意需要进入的当前的我们的所以在的proto文件下再执行命令(当前我的示例调整,调整到Demo2包下):

1
css复制代码python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. serverstrem.proto

3:步骤3 - 更新编写serverstrem_grpc_server.py grpc的服务端:

完整代码:

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
python复制代码

from concurrent import futures
import grpc
from demo2 import serverstrem_pb2_grpc, serverstrem_pb2
import threading
import time
import random


# 实现 proto文件中定义的 GreeterServicer的接口
class Greeter(serverstrem_pb2_grpc.GreeterServicer):


# 实现 proto 文件中定义的 rpc 调用
def SayHello(self, request, context):
# 使用流的方式不断返回给客户端信息
# 检查客户端是否还保持连接状态
idnex = 1
while context.is_active():
# 接收到客户端的信息
idnex = idnex + 1
print("服务端的索引:", idnex)
client_name = request.name
# 使用生成器的方式不安给我们的---返回给客户端发送信息
time.sleep(1)
# 如果需要主动的关闭的服务端的话可以使用:
if idnex == 5:
context.cancel()
yield serverstrem_pb2.HelloReply(
message=f"{client_name} 啊!我是你大爷!{random.sample('zyxwvutsrqponmlkjihgfedcba',5)}")


# 新增处理客户端的流模式的函数,注意下面的request_iterator是一个迭代器的对象
def SayRequestStream(self, request_iterator, context):
pass
# 循环的接收来此客户端每次提交的数据
for curr_request in request_iterator:
# 打印当前客户端的数据信息
print(curr_request.name)
if curr_request.name == "后会有期":
return serverstrem_pb2.HelloReply(message=f"{curr_request.name} 啊!我们后会有期!")
# 返回最终的服务器一次处理结果

# 新增双向流的模式处理
def SayRequestAndRespStream(self, request_iterator, context):
pass
# 循环的接收来此客户端每次提交的数据
for curr_request in request_iterator:
# 打印当前客户端的数据信息
print(curr_request.name)
# 对每一个的客户端的数据进行也循环的应答的回复处理
yield serverstrem_pb2.HelloReply(message=f"{curr_request.name} 啊!我是来自服务端的回复!请接收!!")


def serve():
# 实例化一个rpc服务,使用线程池的方式启动我们的服务
server = grpc.server(futures.ThreadPoolExecutor(max_workers=3))
# 添加我们服务
serverstrem_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
# 配置启动的端口
server.add_insecure_port('[::]:50051')
# 开始启动的服务
server.start()
# wait_for_termination --主要是为了目标启动后主进程直接的结束!需要一个循环的方式进行进行进程运行
server.wait_for_termination()


if __name__ == '__main__':
serve()

主要是新增服务函数处理:

逻辑说明:

  • 1:服务端一直接收客户端发生的消息

4:步骤4 - 编写serverstrem_grpc_client.py grpc的客户端,调用我们的服务端:

此时是我们的客户端进行流的方式的提交数据给我们的服务端,所以我们的也设计一个迭代的方式自己新年数据的提交:

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
python复制代码#!/usr/bin/evn python
# coding=utf-8

import grpc
from demo2 import serverstrem_pb2, serverstrem_pb2_grpc
import time

def run():
# 连接 rpc 服务器
with grpc.insecure_channel('localhost:50051') as channel:
# 通过通道服务一个服务
stub = serverstrem_pb2_grpc.GreeterStub(channel)
# 生成请求我们的服务的函数的时候,需要传递的参数体,它放在hello_pb2里面-请求体为:hello_pb2.HelloRequest对象
# response = stub.SayHello(serverstrem_pb2.HelloRequest(name='小名同学'))
# for item in response:
# print("SayHello函数调用结果返回:: " + item.message)

# # ============客户端流模式
# def send_action():
# for send_name in ['我是你大爷',"我是你小爷",'我是你大舅子',"后会有期"]:
# print("send_name:",send_name)
# time.sleep(1)
# yield serverstrem_pb2.HelloRequest(name=send_name)
# # 接收服务端返回的结果
# response = stub.SayRequestStream(send_action())
# print(response.message)
# ============双向流模式
def send_action():
for send_name in ['我是你大爷',"我是你小爷",'我是你大舅子',"后会有期"]:
time.sleep(1)
yield serverstrem_pb2.HelloRequest(name=send_name)
# 接收服务端返回的结果
response_iterator = stub.SayRequestAndRespStream(send_action())
for response in response_iterator:
print(response.message)

if __name__ == '__main__':
run()

5:步骤5 - 服务启动:

  • 启动服务端
  • 再启动客户端

客户端最后的输出结果为:

1
2
3
4
5
6
7
8
makefile复制代码send_name: 我是你大爷
send_name: 我是你小爷
我是你大爷 啊!我是来自服务端的回复!请接收!!
send_name: 我是你大舅子
我是你小爷 啊!我是来自服务端的回复!请接收!!
send_name: 后会有期
我是你大舅子 啊!我是来自服务端的回复!请接收!!
后会有期 啊!我是来自服务端的回复!请接收!!

服务端输出:

1
2
3
4
复制代码我是你大爷
我是你小爷
我是你大舅子
后会有期

5:安全认证

5.1支持的授权机制

以下是来自官方文档的说明:

  • SSL/TLS
+ gRPc 集成 SSL/TLS 并对服务端授权所使用的 SSL/TLS 进行了改良,对客户端和服务端交换的所有数据进行了加密。对客户端来讲提供了可选的机制提供凭证来获得共同的授权。
  • OAuth 2.0
+ RPC 提供通用的机制(后续进行描述)来对请求和应答附加基于元数据的凭证。当通过 gRPC 访问 Google API 时,会为一定的授权流程提供额外的获取访问令牌的支持,这将通过以下代码例子进行展示。 *警告*:Google OAuth2 凭证应该仅用于连接 Google 的服务。把 Google 对应的 OAuth2 令牌发往非 Google 的服务会导致令牌被窃取用作冒充客户端来访问 Google 的服务。
  • API

为了减少复杂性和将混乱最小化, gRPC 以一个统一的凭证对象来进行工作。 凭证可以是以下两类:

+ *频道凭证*, 被附加在 `频道`上, 比如 SSL 凭证。
+ *调用凭证*, 被附加在调用上(或者 C++ 里的 `客户端上下文`)。 凭证可以用`组合频道凭证`来进行组合。一个`组合频道凭证`可以将一个`频道凭证`和一个`调用凭证`关联创建一个新的`频道凭证`。结果在这个频道上的每次调用会发送组合的`调用凭证`来作为授权数据。 例如,一各`频道凭证`可以由一个`Ssl 凭证`和一个`访问令牌凭证`生成。结果是在这个频道上的每次调用都会发送对应的访问令牌。 `调用凭证`可以用 `组合凭证`来组装。组装后的 `调用凭证`应用到一个`客户端上下文`里,将触发发送这两个`调用凭证`的授权数据。

5.1 关于 SSL

通常SSL主要是用于更加的安全进行数据传输,主要作用有:

  1. 进行数据的认证(用户和服务的认证)
  2. 数据的加密传输
  3. 维护数据完整性,确保数据传输过程中不被改变

5.2 携带TSL的实现(python实现)

示例代码来源:www.cnblogs.com/areful/p/10…

使用SSL启动GRPC的服务示例:

  • 服务端:
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
python复制代码# -*- coding: utf-8 -*-
# Author: areful
#
# pip install grpcio
# pip install protobuf
# pip install grpcio-tools
# ...

# Copyright 2015, Google Inc.
# All rights reserved.

"""The Python implementation of the GRPC helloworld.Greeter server."""

import time
from concurrent import futures

from gj.grpc.helloworld.helloworld_pb2 import *
from gj.grpc.helloworld.helloworld_pb2_grpc import *

_ONE_DAY_IN_SECONDS = 60 * 60 * 24


class Greeter(GreeterServicer):

def SayHello(self, request, context):
return HelloReply(message='Hello, %s!' % request.name)


def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
add_GreeterServicer_to_server(Greeter(), server)

with open('server.pem', 'rb') as f:
private_key = f.read()
with open('server.crt', 'rb') as f:
certificate_chain = f.read()
with open('ca.crt', 'rb') as f:
root_certificates = f.read()
server_credentials = grpc.ssl_server_credentials(((private_key, certificate_chain),), root_certificates,True)
server.add_secure_port('localhost:50051', server_credentials)
server.start()
try:
while True:
time.sleep(_ONE_DAY_IN_SECONDS)
except KeyboardInterrupt:
server.stop(0)


if __name__ == '__main__':
serve()
  • 客户端:
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
python复制代码# -*- coding: utf-8 -*-
# Author: areful
#
# pip install grpcio
# pip install protobuf
# pip install grpcio-tools
#
# Copyright 2015, Google Inc.
# All rights reserved.
# ...

"""The Python implementation of the GRPC helloworld.Greeter client."""

from __future__ import print_function

from gj.grpc.helloworld.helloworld_pb2 import *
from gj.grpc.helloworld.helloworld_pb2_grpc import *


def run():
with open('client.pem', 'rb') as f:
private_key = f.read()
with open('client.crt', 'rb') as f:
certificate_chain = f.read()
with open('ca.crt', 'rb') as f:
root_certificates = f.read()
creds = grpc.ssl_channel_credentials(root_certificates, private_key, certificate_chain)
channel = grpc.secure_channel('localhost:50051', creds)
stub = GreeterStub(channel)
response = stub.SayHello(HelloRequest(name='world'))
print("Greeter client received: " + response.message)


if __name__ == '__main__':
run()

6:GRPC 上下文对象相关内容

6.1 抽象基类:

1
python复制代码

class RpcContext(six.with_metaclass(abc.ABCMeta)):
“””Provides RPC-related information and action.”””

@abc.abstractmethod
def is_active(self):
    """Describes whether the RPC is active or has terminated.

    Returns:
      bool:
      True if RPC is active, False otherwise.
    """
    raise NotImplementedError()

@abc.abstractmethod
def time_remaining(self):
    """Describes the length of allowed time remaining for the RPC.

    Returns:
      A nonnegative float indicating the length of allowed time in seconds
      remaining for the RPC to complete before it is considered to have
      timed out, or None if no deadline was specified for the RPC.
    """
    raise NotImplementedError()

@abc.abstractmethod
def cancel(self):
    """Cancels the RPC.

    Idempotent and has no effect if the RPC has already terminated.
    """
    raise NotImplementedError()

@abc.abstractmethod
def add_callback(self, callback):
    """Registers a callback to be called on RPC termination.

    Args:
      callback: A no-parameter callable to be called on RPC termination.

    Returns:
      True if the callback was added and will be called later; False if
        the callback was not added and will not be called (because the RPC
        already terminated or some other reason).
    """
    raise NotImplementedError()
1
2


6.2 实现类:

从上面的示例可以看,我们的几乎每个srv的服务的函数里面都有自带有一个上下问的对象,我们这里看看一下它的源码:
第一个实现RpcContext的类

1
kotlin复制代码class ServicerContext(six.with_metaclass(abc.ABCMeta, RpcContext)):

子类:

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
python复制代码class _Context(grpc.ServicerContext):

def __init__(self, rpc_event, state, request_deserializer):
self._rpc_event = rpc_event
self._state = state
self._request_deserializer = request_deserializer

def is_active(self):
with self._state.condition:
return _is_rpc_state_active(self._state)

def time_remaining(self):
return max(self._rpc_event.call_details.deadline - time.time(), 0)

def cancel(self):
self._rpc_event.call.cancel()

def add_callback(self, callback):
with self._state.condition:
if self._state.callbacks is None:
return False
else:
self._state.callbacks.append(callback)
return True

def disable_next_message_compression(self):
with self._state.condition:
self._state.disable_next_compression = True

def invocation_metadata(self):
return self._rpc_event.invocation_metadata

def peer(self):
return _common.decode(self._rpc_event.call.peer())

def peer_identities(self):
return cygrpc.peer_identities(self._rpc_event.call)

def peer_identity_key(self):
id_key = cygrpc.peer_identity_key(self._rpc_event.call)
return id_key if id_key is None else _common.decode(id_key)

def auth_context(self):
return {
_common.decode(key): value for key, value in six.iteritems(
cygrpc.auth_context(self._rpc_event.call))
}

def set_compression(self, compression):
with self._state.condition:
self._state.compression_algorithm = compression

def send_initial_metadata(self, initial_metadata):
with self._state.condition:
if self._state.client is _CANCELLED:
_raise_rpc_error(self._state)
else:
if self._state.initial_metadata_allowed:
operation = _get_initial_metadata_operation(
self._state, initial_metadata)
self._rpc_event.call.start_server_batch(
(operation,), _send_initial_metadata(self._state))
self._state.initial_metadata_allowed = False
self._state.due.add(_SEND_INITIAL_METADATA_TOKEN)
else:
raise ValueError('Initial metadata no longer allowed!')

def set_trailing_metadata(self, trailing_metadata):
with self._state.condition:
self._state.trailing_metadata = trailing_metadata

def trailing_metadata(self):
return self._state.trailing_metadata

def abort(self, code, details):
# treat OK like other invalid arguments: fail the RPC
if code == grpc.StatusCode.OK:
_LOGGER.error(
'abort() called with StatusCode.OK; returning UNKNOWN')
code = grpc.StatusCode.UNKNOWN
details = ''
with self._state.condition:
self._state.code = code
self._state.details = _common.encode(details)
self._state.aborted = True
raise Exception()

def abort_with_status(self, status):
self._state.trailing_metadata = status.trailing_metadata
self.abort(status.code, status.details)

def set_code(self, code):
with self._state.condition:
self._state.code = code

def code(self):
return self._state.code

def set_details(self, details):
with self._state.condition:
self._state.details = _common.encode(details)

def details(self):
return self._state.details

def _finalize_state(self):
pass

6.3 共享上下文和服务端上下文方法

实现类里面大概有一些方法是我们需要去了解的:

  • is_active() :判断客户端是否还存活
  • time_remaining :超时剩余时间,如果为请求设置了超时时间的话,则可以获取
  • cancel 取消当前请求,主动的进行链接的取消,当服务端调用这个函数后,客户端会直接的抛出以下的异常:
1
2
3
4
ini复制代码grpc._channel._InactiveRpcError: <_InactiveRpcError of RPC that terminated with:
status = StatusCode.CANCELLED
details = "Cancelled"
debug_error_string = "{"created":"@1636954326.072000000","description":"Error received from peer
  • add_callback() :添加一个RPC终止时调用的回调函数(如果链接断开,则不会调用)
  • disable_next_message_compression :禁用下一条响应消息的压缩,此方法将覆盖在服务器创建期间或在调用时设置的任何压缩配置集
  • invocation_metadata: 获取当前自定义一些元数据信息,其实就是获取【请求头】信息
  • set_compression :设置当时数据传输的压缩相关的算法
  • send_initial_metadata:发送元数据信息
  • set_trailing_metadata():设置当前传输的自定义的【响应报文头】元数据信息
  • trailing_metadata: 元数据的获取
  • abort(self, code, details): 打断连接
  • abort_with_status
  • set_code 设置异常的时候抛出的状态码
  • code 获取抛出异常的状态码
  • set_details 和 details 设置和获取异常信息

6.4 客户端上下文方法

下文有对上下文在一下方法有调用的示例,如获取异常信息!

  • code() : 注意是一个方法,不是一个属性,返回服务端的响应码
  • details():返回服务端的响应描述
  • initial_metadata() 获取服务端发送的元数据信息【返回是初值元数据】
  • trailing_metadata() 访问服务器发送的跟踪元数据【返回是尾随元数据】

上述的方法都会将导致阻塞,直到该值可用为止

6.5 异常状态码

分析相关状态码:

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
ini复制代码@enum.unique
class StatusCode(enum.Enum):
"""Mirrors grpc_status_code in the gRPC Core.

Attributes:
OK: Not an error; returned on success
CANCELLED: The operation was cancelled (typically by the caller).
UNKNOWN: Unknown error.
INVALID_ARGUMENT: Client specified an invalid argument.
DEADLINE_EXCEEDED: Deadline expired before operation could complete.
NOT_FOUND: Some requested entity (e.g., file or directory) was not found.
ALREADY_EXISTS: Some entity that we attempted to create (e.g., file or directory)
already exists.
PERMISSION_DENIED: The caller does not have permission to execute the specified
operation.
UNAUTHENTICATED: The request does not have valid authentication credentials for the
operation.
RESOURCE_EXHAUSTED: Some resource has been exhausted, perhaps a per-user quota, or
perhaps the entire file system is out of space.
FAILED_PRECONDITION: Operation was rejected because the system is not in a state
required for the operation's execution.
ABORTED: The operation was aborted, typically due to a concurrency issue
like sequencer check failures, transaction aborts, etc.
UNIMPLEMENTED: Operation is not implemented or not supported/enabled in this service.
INTERNAL: Internal errors. Means some invariants expected by underlying
system has been broken.
UNAVAILABLE: The service is currently unavailable.
DATA_LOSS: Unrecoverable data loss or corruption.
"""
OK = (_cygrpc.StatusCode.ok, 'ok')
CANCELLED = (_cygrpc.StatusCode.cancelled, 'cancelled')
UNKNOWN = (_cygrpc.StatusCode.unknown, 'unknown')
INVALID_ARGUMENT = (_cygrpc.StatusCode.invalid_argument, 'invalid argument')
DEADLINE_EXCEEDED = (_cygrpc.StatusCode.deadline_exceeded,
'deadline exceeded')
NOT_FOUND = (_cygrpc.StatusCode.not_found, 'not found')
ALREADY_EXISTS = (_cygrpc.StatusCode.already_exists, 'already exists')
PERMISSION_DENIED = (_cygrpc.StatusCode.permission_denied,
'permission denied')
RESOURCE_EXHAUSTED = (_cygrpc.StatusCode.resource_exhausted,
'resource exhausted')
FAILED_PRECONDITION = (_cygrpc.StatusCode.failed_precondition,
'failed precondition')
ABORTED = (_cygrpc.StatusCode.aborted, 'aborted')
OUT_OF_RANGE = (_cygrpc.StatusCode.out_of_range, 'out of range')
UNIMPLEMENTED = (_cygrpc.StatusCode.unimplemented, 'unimplemented')
INTERNAL = (_cygrpc.StatusCode.internal, 'internal')
UNAVAILABLE = (_cygrpc.StatusCode.unavailable, 'unavailable')
DATA_LOSS = (_cygrpc.StatusCode.data_loss, 'data loss')
UNAUTHENTICATED = (_cygrpc.StatusCode.unauthenticated, 'unauthenticated')

相关状态码表示的异常描述为:

  • OK 默认都是这个,调用返回成功的时候
  • CANCELLED 表示的是链接已断开的错误状态
  • UNKNOWN 表示未知的错误,当我们的服务端出现了未知的异常错误,类似web500之类的(使用about为正常传参数的时候就会有这错误)
  • INVALID_ARGUMENT 表示对客户端提交的参数校验失败错误
  • DEADLINE_EXCEEDED 表示请求超时的错误
  • NOT_FOUND 表示请求的函数或资源找不到
  • ALREADY_EXISTS 表示请求处理资源已存在,类似数据库的唯一索引的时候那种错误
  • PERMISSION_DENIED 权限错误,无权限访问
  • UNAUTHENTICATED 表示认证失败,无效信息认证
  • RESOURCE_EXHAUSTED 表示请求资源已消耗完毕,无可用资源
  • FAILED_PRECONDITION 表示请求处理被拒绝
  • ABORTED 表示请求被打断终止了请求,操作被中止,通常是由于并发问题,如顺序检查失败、事务中止等。
  • UNIMPLEMENTED 表示暂时不支持此类的请求处理或无法执行请求处理
  • INTERNAL 表示意外异常错误好像和UNKNOWN有点类似
  • UNAVAILABLE 服务无法正常运行,服务不可用
  • DATA_LOSS 表示数据丢失

6.6 异常处理示例

服务端抛异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ruby复制代码# 实现 proto文件中定义的 GreeterServicer的接口
class Greeter(hello_pb2_grpc.GreeterServicer):
# 实现 proto 文件中定义的 rpc 调用
def SayHello(self, request, context):
# 返回是我们的定义的响应体的对象
return hello_pb2.HelloReply(message='hello {msg}'.format(msg=request.name))

def SayHelloAgain(self, request, context):
# 返回是我们的定义的响应体的对象

# 设置异常状态码
context.set_code(grpc.StatusCode.PERMISSION_DENIED)
context.set_details("你没有这个访问的权限")
raise context

return hello_pb2.HelloReply(message='hello {msg}'.format(msg=request.name))

image.png
客户端接收异常:

image.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
python复制代码#!/usr/bin/evn python
# coding=utf-8

import grpc
import hello_pb2
import hello_pb2_grpc


def run():
# 连接 rpc 服务器
with grpc.insecure_channel('localhost:50051') as channel:
# 通过通道服务一个服务
stub = hello_pb2_grpc.GreeterStub(channel)
# 生成请求我们的服务的函数的时候,需要传递的参数体,它放在hello_pb2里面-请求体为:hello_pb2.HelloRequest对象
try:
response = stub.SayHelloAgain(hello_pb2.HelloRequest(name='欢迎下次光临'))
print("SayHelloAgain函数调用结果的返回: " + response.message)
except grpc._channel._InactiveRpcError as e:
print(e.code())
print(e.details())


if __name__ == '__main__':
run()

最终输出:

image.png

6.6 initial_metadata和trailing_metadata

  • 初始元数据 initial_metadata
    • 初始元数据 initial_metadata 其实可以理解文的客户端的请求头信息
  • 尾随元数据 trailing_metadata
    • 尾随元数据 trailing_metadata的方法,其实可以理解是响应报文头信息

6.6.1 服务端设置响应报文头信息

通常我们的如果有特殊的需要,需要返回响应的报文头信息的话,可以通过采取类似的方法来实现需求:

如服务端,返回一个响应报文信息:

1
2
3
python复制代码def set_trailing_metadata(self, trailing_metadata):
with self._state.condition:
self._state.trailing_metadata = trailing_metadata

源码分析:传入的参数格式:

image.png

image.png

我勒个去,传的是元组,Tuple,仔细分析的一下其他意思是:

  • 我需要传一个元组的对象,(MetadataKey,MetadataValue)
  • *args 表示我的我可以接收多个值

于是乎有一下服务端示例:

image.png

但是此时启动客户端请求的时候,客户端就卡死了一直没响应!:
查看服务端输出信息为:
image.png

1
perl复制代码 validate_metadata: {"created":"@1636960201.178000000","description":"Illegal header value","fil

大概意思就是说:你的元素校验不通过!!!
我把中文改为其他的时候,我擦,!!!!!有可以通过!!

image.png

看来是队我们的中文支持是有问题滴啊!想来好像我们的头文件好像似乎也没设置过中文的吧!!!所以呵呵!怪我自己了!

6.6.2 客户端获取响应报文头信息

参考来自:cn.voidcc.com/question/p-…

所以有了以下的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码def run():
# 连接 rpc 服务器
with grpc.insecure_channel('localhost:50051') as channel:
# 通过通道服务一个服务
stub = hello_pb2_grpc.GreeterStub(channel)
# 生成请求我们的服务的函数的时候,需要传递的参数体,它放在hello_pb2里面-请求体为:hello_pb2.HelloRequest对象
try:
response,callbask = stub.SayHelloAgain.with_call(hello_pb2.HelloRequest(name='欢迎下次光临'))
print("SayHelloAgain函数调用结果的返回: " + response.message)
print("SayHelloAgain函数调用结果的返回---响应报文头信息: " ,callbask.trailing_metadata())
except grpc._channel._InactiveRpcError as e:
print(e.code())
print(e.details())

6.6.3 获取服务端获取客户端请求头信息

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
python复制代码#!/usr/bin/evn python
# -*- coding: utf-8 -*-

from concurrent import futures
import time
import grpc
import hello_pb2
import hello_pb2_grpc


# 实现 proto文件中定义的 GreeterServicer的接口
class Greeter(hello_pb2_grpc.GreeterServicer):
# 实现 proto 文件中定义的 rpc 调用
def SayHello(self, request, context):
# 返回是我们的定义的响应体的对象
return hello_pb2.HelloReply(message='hello {msg}'.format(msg=request.name))

def SayHelloAgain(self, request, context):
# 返回是我们的定义的响应体的对象

# # 设置异常状态码
# context.set_code(grpc.StatusCode.PERMISSION_DENIED)
# context.set_details("你没有这个访问的权限")
# raise context

# 接收请求头的信息
print("接收到的请求头元数据信息",context.invocation_metadata())
# 设置响应报文头信息
context.set_trailing_metadata((('name','223232'),('sex','23232')))
return hello_pb2.HelloReply(message='hello {msg}'.format(msg=request.name))


def serve():
# 实例化一个rpc服务,使用线程池的方式启动我们的服务
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
# 添加我们服务
hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
# 配置启动的端口
server.add_insecure_port('[::]:50051')
# 开始启动的服务
server.start()
# wait_for_termination --主要是为了目标启动后主进程直接的结束!需要一个循环的方式进行进行进程运行
server.wait_for_termination()


if __name__ == '__main__':
serve()

客户端提交自定义请求头信息:

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
python复制代码#!/usr/bin/evn python
# -*- coding: utf-8 -*-


import grpc
import hello_pb2
import hello_pb2_grpc


def run():
# 连接 rpc 服务器
with grpc.insecure_channel('localhost:50051') as channel:
# 通过通道服务一个服务
stub = hello_pb2_grpc.GreeterStub(channel)
# 生成请求我们的服务的函数的时候,需要传递的参数体,它放在hello_pb2里面-请求体为:hello_pb2.HelloRequest对象
try:

reest_header = (
('mesasge', '1010'),
('error', 'No Error')
)

response, callbask = stub.SayHelloAgain.with_call(request=hello_pb2.HelloRequest(name='欢迎下次光临'),
# 设置请求的超时处理
timeout=5,
# 设置请求的头的信息
metadata=reest_header,
)
print("SayHelloAgain函数调用结果的返回: " + response.message)
print("SayHelloAgain函数调用结果的返回---响应报文头信息: ", callbask.trailing_metadata())
except grpc._channel._InactiveRpcError as e:
print(e.code())
print(e.details())


if __name__ == '__main__':
run()

输出的结果为:

客户端:

1
2
css复制代码SayHelloAgain函数调用结果的返回: hello 欢迎下次光临
SayHelloAgain函数调用结果的返回---响应报文头信息: (_Metadatum(key='name', value='223232'), _Metadatum(key='sex', value='23232'))

服务端:

1
ini复制代码接收到的请求头元数据信息 (_Metadatum(key='mesasge', value='1010'), _Metadatum(key='error', value='No Error'), _Metadatum(key='user-agent', value='grpc-python/1.41.1 grpc-c/19.0.0 (windows; chttp2)'))

6.7 数据传输大小修改和数据解压缩

通常我们的服务一般会设置能接收的数据的上限和能发送数据的上限,所以我们的可以对我们的服务端和客户端都进行相关的传输的数据的大小的限制。

另外对于传输数据过大的情况下,我们的通信也会对数据进行相关解压缩,加快的数据高效传输,。对于服务端来说我们的可以设置全局压缩和局部压缩。

  • 服务端数据压缩和数据限制:
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
python复制代码#!/usr/bin/evn python
# -*- coding: utf-8 -*-

from concurrent import futures
import time
import grpc
import hello_pb2
import hello_pb2_grpc


# 实现 proto文件中定义的 GreeterServicer的接口
class Greeter(hello_pb2_grpc.GreeterServicer):
# 实现 proto 文件中定义的 rpc 调用
def SayHello(self, request, context):
# 返回是我们的定义的响应体的对象
return hello_pb2.HelloReply(message='hello {msg}'.format(msg=request.name))

def SayHelloAgain(self, request, context):
# 返回是我们的定义的响应体的对象

# # 设置异常状态码
# context.set_code(grpc.StatusCode.PERMISSION_DENIED)
# context.set_details("你没有这个访问的权限")
# raise context

# 接收请求头的信息
print("接收到的请求头元数据信息", context.invocation_metadata())
# 设置响应报文头信息
context.set_trailing_metadata((('name', '223232'), ('sex', '23232')))
# 三种的压缩机制处理
# NoCompression = _compression.NoCompression
# Deflate = _compression.Deflate
# Gzip = _compression.Gzip
# 局部的数据进行压缩
context.set_compression(grpc.Compression.Gzip)
return hello_pb2.HelloReply(message='hello {msg}'.format(msg=request.name))


def serve():
# 实例化一个rpc服务,使用线程池的方式启动我们的服务

# 服务一些参数信息的配置
options = [
('grpc.max_send_message_length', 60 * 1024 * 1024), # 限制发送的最大的数据大小
('grpc.max_receive_message_length', 60 * 1024 * 1024), # 限制接收的最大的数据的大小
]
# 三种的压缩机制处理
# NoCompression = _compression.NoCompression
# Deflate = _compression.Deflate
# Gzip = _compression.Gzip
# 配置服务启动全局的数据传输的压缩机制
compression = grpc.Compression.Gzip
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10),
options=options,
compression=compression)
# 添加我们服务
hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
# 配置启动的端口
server.add_insecure_port('[::]:50051')
# 开始启动的服务
server.start()
# wait_for_termination --主要是为了目标启动后主进程直接的结束!需要一个循环的方式进行进行进程运行
server.wait_for_termination()


if __name__ == '__main__':
serve()
  • 客户端端数据压缩和数据限制:
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
ini复制代码#!/usr/bin/evn python
# -*- coding: utf-8 -*-


import grpc
import hello_pb2
import hello_pb2_grpc


def run():
# 连接 rpc 服务器
options = [
('grpc.max_send_message_length', 60 * 1024 * 1024), # 限制发送的最大的数据大小
('grpc.max_receive_message_length', 60 * 1024 * 1024), # 限制接收的最大的数据的大小
]
# 三种的压缩机制处理
# NoCompression = _compression.NoCompression
# Deflate = _compression.Deflate
# Gzip = _compression.Gzip
# 配置服务启动全局的数据传输的压缩机制
compression = grpc.Compression.Gzip
# 配置相关的客户端一些参数信息
# 配置相关的客户端一些参数信息
# 配置相关的客户端一些参数信息
with grpc.insecure_channel(target='localhost:50051',
options=options,
compression = compression
) as channel:
# 通过通道服务一个服务
stub = hello_pb2_grpc.GreeterStub(channel)
# 生成请求我们的服务的函数的时候,需要传递的参数体,它放在hello_pb2里面-请求体为:hello_pb2.HelloRequest对象
try:

reest_header = (
('mesasge', '1010'),
('error', 'No Error')
)

response, callbask = stub.SayHelloAgain.with_call(request=hello_pb2.HelloRequest(name='欢迎下次光临'),
# 设置请求的超时处理
timeout=5,
# 设置请求的头的信息
metadata=reest_header,
)
print("SayHelloAgain函数调用结果的返回: " + response.message)
print("SayHelloAgain函数调用结果的返回---响应报文头信息: ", callbask.trailing_metadata())
except grpc._channel._InactiveRpcError as e:
print(e.code())
print(e.details())


if __name__ == '__main__':
run()

6.8 客户端重试机制

所谓的重试机制限流机制其实就是客户端请求服务没响应的情况,方式进行重试重连,但是也不是无限循环进行重试,需要有一个度。

以下的一些资料信息参考来自:
blog.csdn.net/chinesehuaz…

一些配置参数说明:

  • grpc.max_send_message_length 限制发送最大数据量
  • grpc.max_receive_message_length 限制最大接收数据量
  • grpc.enable_retries 透明重试机制,默认值是1开启,可以关闭设置为0
  • grpc.service_config -配置重试机制策略
1
2
3
4
5
6
7
8
9
10
json复制代码{
"retryPolicy":{
"maxAttempts": 4,
"initialBackoff": "0.1s",
"maxBackoff": "1s",
"backoffMutiplier": 2,
"retryableStatusCodes": [
"UNAVAILABLE" ]
}
}

PS:retryableStatusCodes 配置重试的错误码情况,上面的情况是当UNAVAILABLE的情况下才会触发重试,

可以指定重试次数等等,具体参数含义可参考官网,简单介绍一下:

1
2
3
4
diff复制代码-   maxAttempts 必须是大于 1 的整数,对于大于5的值会被视为5
- initialBackoff 和 maxBackoff 必须指定,并且必须具有大于0
- backoffMultiplier 必须指定,并且大于零
- retryableStatusCodes 必须制定为状态码的数据,不能为空,并且没有状态码必须是有效的 gPRC 状态码,可以是整数形式,并且不区分大小写

6.9 客户端对冲重试策略

对冲是指

  • 如果一个方法使用对冲策略,那么首先会像正常的 RPC 调用一样发送第一次请求,如果配置时间内没有响应情况下会,那么直接发送第二次请求,以此类推,直到发送了 maxAttempts 次
  • 多次重试情况下,需要留意是后端负载均衡情况下的幂等性问题

6.10 客户端重试限流策略

  • 当客户端的失败和成功比超过某个阈值时,gRPC 会通过禁用这些重试策略来防止由于重试导致服务器过载
  • 实际限流参数配置,需根据服务器性能资源来制定

限流说明:

  • 每一个服务器,gRPC 客户端会维护一个 token_count 变量,最初设置为 maxToken , 值的范围是 0 - maxToken
  • 对于每个 RPC 请求都会对 token_count 产生一下效果
+ 每个失败的 RPC 请求都会递减token\_count 1
+ 成功 RPC 将会递增 token\_count和tokenRatio 如果 token\_count <= ( maxTokens / 2), 则关闭重试策略,直到 token\_count > (maxTokens/2),恢复重试

配置方法在servie config中配置一下信息:

“retryThrottling”:{
“maxTokens”: 10,
“tokenRatio”: 0.1
}

7:利用信号进行grpc 服务进程结束监听

通常我们使用grpc的时候做微服务的srv的时候,都需要一个机制来监听我们的服务进程的情况,用户服务的发现和注册已经注销。

如果服务不在注册中心,进行注销的话,就会引发请求到错误的后端。

这里其实我们主要是理由信号机制来对我们的服务进行监听。

PS:window下支持信号有限,对KeyboardInterrupt也无法捕获,直接从进程管理器结束进程也无法知晓

完整示例:

1
python复制代码

#!/usr/bin/evn python

-- coding: utf-8 --

import sys
from concurrent import futures
import time
import grpc
import hello_pb2
import hello_pb2_grpc
import signal

实现 proto文件中定义的 GreeterServicer的接口

class Greeter(hello_pb2_grpc.GreeterServicer):
# 实现 proto 文件中定义的 rpc 调用
def SayHello(self, request, context):
# 返回是我们的定义的响应体的对象
return hello_pb2.HelloReply(message=’hello {msg}’.format(msg=request.name))

def SayHelloAgain(self, request, context):
    # 返回是我们的定义的响应体的对象

    # # 设置异常状态码
    # context.set_code(grpc.StatusCode.PERMISSION_DENIED)
    # context.set_details("你没有这个访问的权限")
    # raise context

    # 接收请求头的信息
    print("接收到的请求头元数据信息", context.invocation_metadata())
    # 设置响应报文头信息
    context.set_trailing_metadata((('name', '223232'), ('sex', '23232')))
    # 三种的压缩机制处理
    # NoCompression = _compression.NoCompression
    # Deflate = _compression.Deflate
    # Gzip = _compression.Gzip
    # 局部的数据进行压缩
    context.set_compression(grpc.Compression.Gzip)
    return hello_pb2.HelloReply(message='hello {msg}'.format(msg=request.name))

def serve():
# 实例化一个rpc服务,使用线程池的方式启动我们的服务

# 服务一些参数信息的配置
options = [
    ('grpc.max_send_message_length', 60 * 1024 * 1024),  # 限制发送的最大的数据大小
    ('grpc.max_receive_message_length', 60 * 1024 * 1024),  # 限制接收的最大的数据的大小
]
# 三种的压缩机制处理
# NoCompression = _compression.NoCompression
# Deflate = _compression.Deflate
# Gzip = _compression.Gzip
# 配置服务启动全局的数据传输的压缩机制
compression = grpc.Compression.Gzip
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10),
                     options=options,
                     compression=compression)
# 添加我们服务
hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
# 配置启动的端口
server.add_insecure_port('[::]:50051')
#  开始启动的服务
server.start()

def stop_serve(signum, frame):
    print("进程结束了!!!!")
    # sys.exit(0)
    raise KeyboardInterrupt

# 注销相关的信号
# SIGINT 对应windos下的 ctrl+c的命令
# SIGTERM 对应的linux下的kill命令
signal.signal(signal.SIGINT, stop_serve)
# signal.signal(signal.SIGTERM, stop_serve)

# wait_for_termination --主要是为了目标启动后主进程直接的结束!需要一个循环的方式进行进行进程运行
server.wait_for_termination()

if name == ‘main‘:
serve()

1
2


8:使用协程的方式进行服务启动

8.1 安装依赖包

上面的示例中都是基于线程池的方式来处理并发,以下是使用协程的方式进行处理示例。

首先安装新的依赖包:

image.png
相关的版本要对应的上:

1
2
ini复制代码grpcio-reflection==1.41.1
pip install grpcio-reflection -i https://pypi.tuna.tsinghua.edu.cn/simple

最终安装后的:

image.png

8.2 修改服务端启动

修改我们的服务端代码(修改的的是3.3小节的代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
python复制代码#!/usr/bin/evn python
# -*- coding: utf-8 -*-

import grpc
import hello_pb2
import hello_pb2_grpc
from grpc_reflection.v1alpha import reflection
import asyncio

# 实现 proto文件中定义的 GreeterServicer的接口
class Greeter(hello_pb2_grpc.GreeterServicer):
# 实现 proto 文件中定义的 rpc 调用
async def SayHello(self, request, context):
# 返回是我们的定义的响应体的对象
return hello_pb2.HelloReply(message='hello {msg}'.format(msg=request.name))

async def SayHelloAgain(self, request, context):
# 返回是我们的定义的响应体的对象
return hello_pb2.HelloReply(message='hello {msg}'.format(msg=request.name))


async def serve():
# 实例化一个rpc服务,使用线程池的方式启动我们的服务
service_names = (
hello_pb2.DESCRIPTOR.services_by_name["Greeter"].full_name,
reflection.SERVICE_NAME,
)

server = grpc.aio.server()
# 添加我们服务
hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
reflection.enable_server_reflection(service_names, server)
# 配置启动的端口
server.add_insecure_port('[::]:50051')
await server.start()
await server.wait_for_termination()



if __name__ == '__main__':
asyncio.run(serve())

8.3 启动客户端调用

我们的客户端保持原来的3.3小节的客户端不变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
python复制代码#!/usr/bin/evn python
# coding=utf-8

import grpc
import hello_pb2
import hello_pb2_grpc


def run():
# 连接 rpc 服务器
with grpc.insecure_channel('localhost:50051') as channel:
# 通过通道服务一个服务
stub = hello_pb2_grpc.GreeterStub(channel)
# 生成请求我们的服务的函数的时候,需要传递的参数体,它放在hello_pb2里面-请求体为:hello_pb2.HelloRequest对象
response = stub.SayHello(hello_pb2.HelloRequest(name='小钟同学'))
print("SayHello函数调用结果返回:: " + response.message)
response = stub.SayHelloAgain(hello_pb2.HelloRequest(name='欢迎下次光临'))
print("SayHelloAgain函数调用结果的返回: " + response.message)


if __name__ == '__main__':
run()

直接启动也可以正常进行和服务端的通信。

3.总结


以上仅仅是个人结合自己的实际需求,做学习的实践笔记!如有笔误!欢迎批评指正!感谢各位大佬!

结尾

END

简书:www.jianshu.com/u/d6960089b…

掘金:juejin.cn/user/296393…

公众号:微信搜【小儿来一壶枸杞酒泡茶】

小钟同学 | 文 【欢迎一起学习交流】| QQ:308711822

本文转载自: 掘金

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

Spring IOC容器初始化原理分析 (第六节中)

发表于 2021-11-15

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

1.前言

本文主要是续讲Spring IOC容器初始化原理分析 (第六节上),在本篇文章中我们主要详细讲一下 preInstantiateSingletons()中的 getBean(String name) 方法和 AccessController.doPrivileged()方法,这里preInstantiateSingletons()中别的方法已经分析过了,感兴趣的可以看一下。

2.getBean(String name)

老样子我们直接先贴源码

图片.png
可以发现他这里面就直接调了一个 doGetBean() 方法,那我们接着往下看,这个方法有点长,那我们先看一下方法的简介:

图片.png

主要作用:返回指定bean的一个实例,该实例可以是共享的,也可以是独立的。
参数简介:

  • name: 要检索的bean的名称
  • requiredType :要检索的bean的类型
  • args : 建bean实例时使用的Args参数(仅在创建时使用)
  • typeCheckOnly :是否进行类型检查

这里鉴于代码太长了 我就一块块截取下来给大家分析

1.doGetBean第一块代码

图片.png

  1. ransformedBeanName:返回bean名称,必要时去掉工厂解引用前缀,并将别名解析为规范名称。这里返回的处理后的name是直接对应singletonObjects中的key。
  2. getSingleton(beanName) 在上一篇文章 4.3中我详细讲过,感兴趣的话大家可以去看一看,它的主要作用拿到这个单例bean,第一次进来的时候 sharedInstance 肯定为空!
  3. isSingletonCurrentlyInCreation(beanName):返回指定的单例bean当前是否正在创建中
    图片.png
    图片.png
  4. getObjectForBeanInstance():前面返回的 sharedInstance 是最初始的bean,不一定使我们想要的,获取给定bean实例的对象,可以是bean实例本身,也可以是FactoryBean中创建的对象。

图片.png

  • 这里首先判断name 是不是以‘&’开头,如果是说明它是一个FactoryBean的name 判断当 beanInstance instanceof Nullbean 时 直接返回,当它不 属于 FactoryBean 时抛出异常
  • 这里name不是以 ‘&’ 开头, 当它不是 FactoryBean 时直接返回。
  • 当它是 FactoryBean接着往下看,从FactoryBean单例对象缓存中尝试获取它,若不为空,直接返回
  • 强转为FactoryBean,上面已经判断过,此时一定是FactoryBean类型。
    • 当mbd == null ,切保存BeanDefinition的map中可以取到这个 beanName 的BeanDefinition时,调用getMergedLocalBeanDefinition(beanName)
    • 判断mdb是否为空,以及检测是不是系统创建的,然后调用 getObjectFromFactoryBean()方法 (ps:从给定的 Factory 中获取指定的bean 后续有时间再讲这个)

第一块的主要作用就是:当 sharedInstance != null 且 args == null 时获取bean 对象

2.doGetBean第二块代码

图片.png

接着往下看,前面处理sharedInstance != null 且 args == null的情况,这里处理其余情况。

  1. isPrototypeCurrentlyInCreation(beanName),检查是否存在原型模式下的循环依赖,如果存在直接抛异常,spring 解决不了,原型模式下的循环依赖。
    图片.png
    图片.png
    rototypesCurrentlyInCreation 保存的是原型模式正在创建的对象,当它中包含这个beanName 说明存在循环依赖,单例模式下的循环依赖如何解决
  2. 检查当前容器中是否存在指定名称的 beanDefinition
  • 先取得当前容器的父容器不为null 且 保存beanDefinition的Map中取不到 该beanName 对应的 beanDefinition。
  • 解析该beanName 的原始名字
  • 当 parentFactory instanceof AbstractBeanFactory 时,即递归的去父级容器中寻找
  • 否则 当 args 不为null 时,委派父级容器根据名称和参数寻找
  • 否则 当 requiredType 不为 null 时,委派父级容器根据 名称和类型找
  • 否则 委派父级跟剧名称找
  1. typeCheckOnly:是否需要类型检查,不需要时进入
    图片.png
    图片.png

图片.png
这部分代码,主要是当不需要类型判断时,向容器中标记指定的bean被创建。

  • this.aredyCreated : 保存已经创建的bean的name
  • this.mergedBeanDefinitions : 保存 beanName –> RootBeanDefinition

3.doGetBean 第三块代码

图片.png

  1. 首先获取其父类Bean定义,子类合并父类公共属性,getMergedLocalBeanDefinition在上篇文章4.1中讲过,然后检查给定的合并bean定义
    图片.png
  2. mbd.getDependsOn():获取当前Bean依赖的Bean的名称,当它不为null时,取出每一个依赖的bean
    图片.png
    图片.png
    isDependent(beanName,dependentBeanName):检查依赖名为 beanName 的这个bean 依赖的beans 是否有名为dependentBeanNamed,(即判断 dependentBeanName 是否依赖 beanName)
    • alreadySeen : 已经检查过的,这里保存的是所有检查过的beanName
    • 若当前 beanName 已在 alreadySeen 中 直接返回false,否则的话 取出依赖它的的beans的名字集合(即 dependentBeans)
    • 若 dependentBeans 包含 dependentBeanName 返回true ,否则遍历 dependentBeans,把beanName 加到 alreadySeen 中去,递归判断 dependentBeanName 是否依赖 dependentBeans中的每一个beanName
    • 当它存在这种循环依赖时,抛出异常,这也是为啥单例模式构造器相互依赖spring 解决不了的原因。
  3. registerDependentBean(dep, beanName):为指定的bean注入依赖的bean
    • dep: 指定的bean
    • beanName: 依赖的bean
    • this.dependentBeanMap : 保存的是依赖指定bean 的 beanNames 集合
    • this.dependenciesForBeanMap : 保存的是指定bean 所依赖的 beanNames 集合
      图片.png
      图片.png
  • 先锁定 this.dependentBeanMap,从这里面取得key为 canonicalName,若为null,则new一个LinkedHsahSet。 把 dependentBeanName 加入进去。(这里实际是当我们在创建当前bean时,会在bean依赖的bean 所对应的 this.dependentBeanMap 中把 当前bean加进去)
  • 然后锁定 this.dependenciesForBeanMap ,这里实际是,把当前创建的bean 的 this.dependenciesForBeanMap 更新一下,加入它依赖的 canonicalName
  1. getBean(dep)
  • dep:当前创建的bean 依赖的bean,这里是先去创建它。

4. doGetBean 第四块代码

这一部分代主要完成bean的实例化,本来想这篇文章讲完的,但是如果想讲的细的话,实在太多了,所以这篇文章主要讲大概的,下篇文章我会详细的研究这块的代码
图片.png

  • 首先判断它是不是单例的,如果是则完成单例对象实例化
  • 判断是不是模型模式,若是则每次都会新建一个对象
  • 否则,获取它的作用域,也是创建一个实例对象返回出去

5. doGetBean 第五块代码

图片.png
图片.png

  1. 这里主要是进行类型检查 isInstance(Object obj) 是 Class类的一个本地方法,主要用来检查bean能否强转成 requiredType 类型的方法
  2. 当requiredType 不为null 且bean不能转换成 requiredType 时

图片.png

  • 获取类型转换器,先获取用户自己定义的,如果没有,则 spring 会返回默认的 SimpleTypeConverter 类型转换器。
    图片.png
  • convertIfNecessary(bean,requiredType): 把bean转成 requiredType 类型,这里如果是自定义的则,需要实现该接口,重写该方法。如果是默认的 SimpleTypeConverter 类型转换器,调用的是:org.springframework.beans.TypeConverterSupport#convertIfNecessary(java.lang.Object, java.lang.Class),这个后面有时间可以再额外讲一下
  1. 若转换成功则返回该bean,若转换失败,则抛出异常。

总结:这里getBean(name) 方法算是讲完了,总结起来就是,获取该bean 实例。

  1. AccessController.doPrivileged(PrivilegedAction action,AccessControlContext context)

图片.png
这个方法我的理解就是,用于获取特权,绕过java的权限检查,它在 preInstantiateSingletons() 中出现两处。

2021-11-15_165047.jpg

4.总结

今天就先写到这里,下篇文章主要讲,前面提到的 单例模式,原型模式,其他scope 对象创建的具体细节。

本文转载自: 掘金

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

1…336337338…956

开发者博客

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