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

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


  • 首页

  • 归档

  • 搜索

FPGA ZYNQ Linux内核编译及调试方法

发表于 2021-11-20

1 编译构建

image.png
image.png

  • 最终将生成设备树 DTB 文件、fsbl 文件、U-Boot 文件,Linux 内核和根文件系统映像。编译完成后,生成的映像将位于工程的 images 目录下。
    image.png
    image.png
    image.png
    image.png

2 制作BOOT.BIN 启动文件

image.png
image.png
image.png

  • 最终需要BOOT.BIN和image.ub文件来启动Linux
    image.png

3 制作SD卡

  • 如果使用 SD 卡引导 linux 系统启动,一般需要在 SD 卡上有 2 个分区。一个分区使用 FAT32文件系统,用于放置启动镜像文件(如 BOOT.BIN,linux 镜像等),另一分区使用 EXT4 文件系统,用于存放根文件系统
    image.png
    image.png
    image.png
    image.png
    image.png

image.png

4 SD卡启动

image.png
image.png

本文转载自: 掘金

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

PHP程序员如何简单的开展服务治理架构(一) 治理什么? 依

发表于 2021-11-20

不涉及其他的语言及工具,我们从PHP本身来谈如何实现服务治理

本猿人已经写好的服务治理 github.com/CrazyCodes/…

治理什么?

这个专业名词很容易发现治理的是服务,而服务则是我们的项目。管理这些服务方案则叫服务治理。

现在在Server上有四项服务,分别为

  • UserService
  • ShopService
  • GoodsService
  • LiveService
    这些服务我们叫它服务提供者(既提供对内服务的应用)

调用服务的应用我们称它为服务消费者,例如

  • User-Api
  • Shop-Api
  • Goods-Api
  • Live-Api

Service 是对内服务的而Api是对外服务的

image.png

服务治理考虑的问题就是如何管理这四项服务,让它们如何对外服务,如何监控服务进程

依托实现

在实现服务治理之前,需要了解以下几块知识点

  • thrift
  • rpc
  • swoole

thrift

暂时大可理解为可以通过它去调用其他开发语言的方法

rpc

RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
RPC采用客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续进行。

服务与服务之间通信可以通过RPC通信,当然也可以选择UDP等

swoole

PHP圈内跨世纪的产物,使用他的原因是因为Swoole本身支持RPC通信,所以我们本章通过Swoole Rpc的方法去实现消费者与服务提供者之间的通信

实现RPC通信

服务治理非常重要的一个环节,要在无感知的情况让消费者A调用服务提供者A,B,C,当然实际情况下,这是永远不可能的,根本不在一个内存空间中,我们需要自己模拟出来这种使用方式

1
2
3
ini复制代码$userSerivce = $client->client ('UserService');
$result = $userSerivce->getUserInfo (['user_id' => 100]);
var_dump($result);

在消费者内没有UserService,更没有getUserInfo 方法,这些都在服务提供者的应用中,如何去调用它们?

首先通过php的__call 方法去截取一个不存在的方法

1
2
3
4
5
6
7
8
9
php复制代码public function __call($name, $arguments)
{
$client = new ClientRpc($this->serviceName);

$response = $client->send ($this->serviceName, $name, $arguments);

return (json_decode ($response, true));

}

获取后调用自己写的send 方法,swoole出场

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
php复制代码class ClientRpc
{
protected $client;

public function __construct($service_name, $centerConfig)
{
$this->client = new \swoole_client(SWOOLE_SOCK_TCP);

$center = Dispatcher::loadBalance ($service_name, $centerConfig);
$this->client->connect ($center['ip'], $center['port'], 0.5);
}

public function send($service, $action, $arguments)
{
$request = new Request();

$request->setService ($service);
$request->setAction ($action);
$request->setParameters ($arguments[0]);
// 重组参数,组合成你希望的格式,最后转成json发送到服务提供者
$this->client->send (json_encode ((array)$request));

return $this->client->recv ();
}

public function __destruct()
{
$this->client->close ();
unset($this->client);
}
}

鸣谢

周梦康 [mengkang.net/]

交流

生命不息,编码不止。

微信搜索 【一文秒懂】 传播技术正能量,持续学习新知识。

本文转载自: 掘金

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

性能监控之 JMX 监控 Docker 容器中的 Java

发表于 2021-11-20

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


一、前言

今天在配置 docker 和 JMX 监控的时候,看到有一个细节和非容器环境中的 JMX 配置不太一样。所以在这里写一下,以备其他人查阅。

二、遇到的问题

1、问题现象

一般情况下,我们配置 JMX 只要写上下面这些参数就可以了。

以下是无密码监控时的 JMX 配置参数(有密码监控的配置和常规监控无异)

1
2
3
4
5
bash复制代码-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9998
-Djava.rmi.server.hostname=<serverip>
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false

但是在 docker 容器中这样配置的时候,会出现这个错误。
在这里插入图片描述

2、问题分析

这里就要说明一下逻辑了。为什么会这样呢?

先看 docker 环境的网络结构。

容器使用默认的网络模型,就是 bridge 模式。在这种模式下是 docker run 时做的 DNAT 规则,实现数据转发的能力。所以我们看到的网络信息是以下这样的:

docker 中的网卡信息:

1
2
3
4
5
6
7
8
9
bash复制代码[root@f627e4cb0dbc /]# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.18.0.3 netmask 255.255.0.0 broadcast 0.0.0.0
inet6 fe80::42:acff:fe12:3 prefixlen 64 scopeid 0x20<link>
ether 02:42:ac:12:00:03 txqueuelen 0 (Ethernet)
RX packets 366 bytes 350743 (342.5 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 358 bytes 32370 (31.6 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

docker 中的路由信息:

1
2
3
4
5
6
bash复制代码[root@a2a7679f8642 /]# netstat -r
Kernel IP routing table
Destination Gateway Genmask Flags MSS Window irtt Iface
default gateway 0.0.0.0 UG 0 0 0 eth0
172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
[root@a2a7679f8642 /]#

宿主机上的对应网卡信息:

1
2
3
4
5
6
7
bash复制代码docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
inet 172.18.0.1 netmask 255.255.0.0 broadcast 0.0.0.0
ether 02:42:44:5a:12:8f txqueuelen 0 (Ethernet)
RX packets 6691477 bytes 498130
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 6751310 bytes 3508684363 (3.2 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

宿主机上的路由信息:

1
2
3
4
5
6
7
8
bash复制代码[root@7dgroup ~]# netstat -r
Kernel IP routing table
Destination Gateway Genmask Flags MSS Window irtt Iface
default gateway 0.0.0.0 UG 0 0 0 eth0
link-local 0.0.0.0 255.255.0.0 U 0 0 0 eth0
172.17.208.0 0.0.0.0 255.255.240.0 U 0 0 0 eth0
172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
192.168.16.0 0.0.0.0 255.255.240.0 U 0 0 0 br-676bae33ff92

所以宿主机和容器是可以直接通信的,即便端口没有映射出来。如下所示:

1
2
3
4
bash复制代码[root@7dgroup ~]# telnet 172.18.0.3 8080
Trying 172.18.0.3...
Connected to 172.18.0.3.
Escape character is '^]'.

另外,因为是桥接的,宿主机上还有类似 veth0b5a080 的虚拟网卡设备信息,如:

1
2
3
4
5
6
bash复制代码eth0b5a080: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
ether 42:c3:45:be:88:1a txqueuelen 0 (Ethernet)
RX packets 2715512 bytes 2462280742 (2.2 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 2380143 bytes 2437360499 (2.2 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

这就是虚拟网卡对 veth pair,docker 容器里一个,宿主机一个。 在这种模式下,有几个容器,主机上就会有几个 veth 开头的虚拟网卡设备。

但是如果不是宿主机访问的话,肯定是不通的。如下图所示:
在这里插入图片描述
当我们用监控机 访问的时候,会是这样的结果:

1
2
3
4
5
bash复制代码Zees-Air-2:~ Zee$ telnet <serverip> 8080
Trying <serverip>...
telnet: connect to address <serverip>: Connection refused
telnet: Unable to connect to remote host
Zees-Air-2:~ Zee$

因为 8080 是容器开的端口,并不是宿主机开的端口,其他机器是访问不了的。 这就是为什么要把端口映射出来给远程访问的原因,映射之后的端口,就会有 NAT 规则来保证数据包可达。

查看下 NAT 规则,就知道。如下:

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
bash复制代码[root@7dgroup ~]# iptables -t nat -vnL
Chain PREROUTING (policy ACCEPT 171 packets, 9832 bytes)
pkts bytes target prot opt in out source destination
553K 33M DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT 171 packets, 9832 bytes)
pkts bytes target prot opt in out source destination

Chain OUTPUT (policy ACCEPT 2586 packets, 156K bytes)
pkts bytes target prot opt in out source destination
205K 12M DOCKER all -- * * 0.0.0.0/0 !60.205.104.0/22 ADDRTYPE match dst-type LOCAL
0 0 DOCKER all -- * * 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT 2602 packets, 157K bytes)
pkts bytes target prot opt in out source destination
265K 16M MASQUERADE all -- * !docker0 172.18.0.0/16 0.0.0.0/0
0 0 MASQUERADE all -- * !br-676bae33ff92 192.168.16.0/20 0.0.0.0/0
0 0 MASQUERADE tcp -- * * 192.168.0.4 192.168.0.4 tcp dpt:7001
0 0 MASQUERADE tcp -- * * 192.168.0.4 192.168.0.4 tcp dpt:4001
0 0 MASQUERADE tcp -- * * 192.168.0.5 192.168.0.5 tcp dpt:2375
0 0 MASQUERADE tcp -- * * 192.168.0.8 192.168.0.8 tcp dpt:8080
0 0 MASQUERADE tcp -- * * 172.18.0.4 172.18.0.4 tcp dpt:3306
0 0 MASQUERADE tcp -- * * 172.18.0.5 172.18.0.5 tcp dpt:6379
0 0 MASQUERADE tcp -- * * 172.18.0.2 172.18.0.2 tcp dpt:80
0 0 MASQUERADE tcp -- * * 172.18.0.6 172.18.0.6 tcp dpt:9997
0 0 MASQUERADE tcp -- * * 172.18.0.6 172.18.0.6 tcp dpt:9996
0 0 MASQUERADE tcp -- * * 172.18.0.6 172.18.0.6 tcp dpt:8080
0 0 MASQUERADE tcp -- * * 172.18.0.3 172.18.0.3 tcp dpt:9995
0 0 MASQUERADE tcp -- * * 172.18.0.3 172.18.0.3 tcp dpt:8080

Chain DOCKER (3 references)
pkts bytes target prot opt in out source destination
159K 9544K RETURN all -- docker0 * 0.0.0.0/0 0.0.0.0/0
0 0 RETURN all -- br-676bae33ff92 * 0.0.0.0/0 0.0.0.0/0
1 40 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:3307 to:172.18.0.4:3306
28 1486 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:6379 to:172.18.0.5:6379
228 137K DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:91 to:172.18.0.2:80
3 192 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9997 to:172.18.0.6:9997
0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9996 to:172.18.0.6:9996
0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9002 to:172.18.0.6:8080
12 768 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9995 to:172.18.0.3:9995
4 256 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9004 to:172.18.0.3:8080

[root@7dgroup ~]#

我们看到了宿主机的 91 端口的数据会传给 172.18.0.2 的 80 端口。宿主机的 3307 端口会传给 172.18.0.4 的3306 端口。

啰啰嗦嗦说到这里,那和 JMX 有啥关系。苦就苦在,JMX 是这样的:
在这里插入图片描述
在注册时使用的是参数 jmxremote.port,然后返回一个新的端口 jmxremote.rmi.port。

在调用服务时使用是参数 jmxremote.rmi.port。 前面提到了,因为 docker 在 bridge 模式下端口是要用 -p 显式指定的,不然没 NAT 规则,数据包不可达。所以在这种情况下,只能把 jmxremote.rmi.port 也暴露出去。所以必须显式指定。因为不指定的话,这个端口会随机开。随机开的端口又没 NAT 规则,所以是不通的了。

三、解决方案

所以,这种以上情况只能指定 jmxremote.rmi.port 为固定值,并暴露出去。 配置如下:

1
2
3
4
5
6
bash复制代码-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9995
-Djava.rmi.server.hostname=<serverip>
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.rmi.port=9995

像上面的设置就是两个都是 9995,这样是允许的,这种情况下注册和调用的端口就合并了。

再启动 docker 容器的时候,就需要这样了。

1
2
3
4
5
6
7
bash复制代码docker run -d -p 9003:8080 -p 9995:9995 --name 7dgroup-tomcat5
-e CATALINA_OPTS="-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9995 \
-Djava.rmi.server.hostname=<serverip> \
-Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.rmi.port=9995" c375edce8dfd

然后就可以连接上 JMX 的工具了。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在有防火墙和其他的设备的网络环境中,也有可能出同样的问题。明白了JMX 的注册调用逻辑之后,就可以解决各种类似的问题了。

网络链路是做性能分析的人必须想明白的技术点,所以前面说了那么多内容。

四、总结

这里对于 JMX 工具的选择啰嗦两句。有人喜欢花哨的,有人喜欢简单的,有人喜欢黑窗口的。我觉得工具选择的时候,要看适用情况,在性能分析的时候,一定要选择合适的工具,而不是选择体现技术高超的工具。

最后留个作业:

  • 如果 docker run 中如果指定 -p 19995:9995,也就是换个端口暴露出去,其他配置都不变。JMX 工具还能连得上吗?
  • 如果 jmxremote.rmi.port 和 jmxremote.port 不合并,并且同时把两个端口都暴露出去,其他配置都不变。JMX 工具还能连得上吗?

有兴趣的可以自己尝试下哦。

本文转载自: 掘金

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

性能监控之常见JDK命令行工具整理

发表于 2021-11-20

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


一、概述

我们在做性能分析的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。这里说的数据包括:运行参数、运行日志、异常堆栈、GC日志、线程快照(threaddump/javacore 文件)、堆转储快照(heapdump/hprof 文件)等。

本文只讨论JDK8的常用参数及工具。

二、JVM 参数类型

参数类型大概可以分为三类:

  • 标准参数
  • X参数
  • XX参数

1、标准参数

JVM各个版本基本上不变的参数

举栗子:

  • -help
  • -server
  • -client
  • -version
  • -showversion
  • -cp
  • -classpath

标准参数

2、-X 参数

一般用的不多,不同版本的JVM可能有变化

举栗子:
-Xint:完全解释执行,不会转换成本地代码

xinit

-Xcomp:第一次使用就编译成本地代码,第一次比较慢

xcomp

-Xmixed:混合模式,JVM自己来决定是否编译成本地代码

xmixed

3、-XXX 参数

非标准化参数,相对不太稳定,主要用于JVM调优和Debug,主要分为Boolean与非Boolean两种类型

3.1、Boolean 类型

格式:-XX:[+-]表示启用或者禁用name属性
比如:

  • -XX:+UseConcMarkSweepGC 启用CMS垃圾回收器
  • -XX:+UseG1GC 启用G1垃圾回收器

垃圾回收器

3.2、非 Boolean 类型

格式:-XX:=表示name的属性的值是value
比如:

  • -XX:MaxGCPauseMilis=500(GC最大停顿时间为500毫秒)
  • XX:GCTimeRatio=19
  • -Xmx -Xms
    • -Xms等价于-XX:InitialHeapSize(初始化堆大小)
      • -Xms等价于-XX:MaxHeapSize(最大堆大小)

三、运行时 JVM 参数查看(最关键)

  • -XX:+PrintFlagsInitial(初始值)
  • -XX:+PrintFlagsFinal(最终值)

这里写图片描述

列表内容

  • =表示默认值
  • :=被用户或JVM修改后的值

保存成文本下载下来大概有700多个参数

打印参数

这里写图片描述

  • -XX:+UnlockExperimentaIVMOptions(解锁实验参数才能赋值)
  • -XX:+UnlockDiagnosticVMoptions(解锁诊断参数)
  • -XX:+PrintCommandLineFlags(打印命令行参数)

1、jps(JVM Process Status Tool)

虚拟机进程状态工具,即可查看运行态的Java进程,使用频度最高的JDK命令行工具之一,因为其他工具大多需要输入它查询到LVMID(Local Virtual machine Identifier)来确定监控哪一个进程

jps命令格式:

jps[options][hostid]jps [ options ] [ hostid ]jps[options][hostid]

jps工具主要选项:

选项 作用
-q 只输出LVMID,省略类名
-m 输出JVM Main函数的参数
-l 输出类名,如果是JAR,输出其路径
-v 输出JVM启动参数

举栗子:
查看运行java进程PID

这里写图片描述

jps -l 查看完全类名

jsp -l

官方参考链接:docs.oracle.com/javase/8/do…

2、jinfo(Configuration Info for Java)

JVM配置信息工具,可实时查看和调整JVM的各项参数

命令格式:

jinfo[option]pidjinfo [ option ] pidjinfo[option]pid

查看运行中java进程的参数

  • jinfo -flag name pid:当前设置的参数
  • jin -flags name pid:赋值的参数

举栗子:
查看最大内存

查看最大内存

查看垃圾回收器,是否启用

查看垃圾回收器

3、jstat(JVM Statistics Monitoring Tool)

监控JVM各种运行状态信息的命令行工具,可以监控JVM中的类加载、内存、GC、JIT编译等运行数据

命令格式:
jstat[generalOption∣outputOptionsvmid[interval[s∣ms][count]]jstat [ generalOption | outputOptions vmid [ interval[s|ms] [ count ] ]jstat[generalOption∣outputOptionsvmid[interval[s∣ms][count]]

举栗子:

  • -class 类加载器
  • -compiler JIT
  • -gc GC状态
  • -printcomplilation HotSpot编译统计
  • -gccapacity 各区大小
  • -gccause 最后一次GC统计和原因
  • -gcnew 年轻代状态
  • -gcnewcapacity 年轻代大小
  • -gcold 年老代大小
  • -gcoldcapacity 年老代大小
  • -gcutil GC汇总统计

查看类装载,卸载数量,总空间以及类装载耗费的时间

类加载

官方参考链接:docs.oracle.com/javase/8/do…

4、垃圾回收(性能监控非常有用)

实时动态查看内存变化

gc

  • S0C、S1C、S0U、S1U:S0和S1的总量与使用量
  • EC、EU:Eden区总量与使用量
  • OC、OU:Old区总量与使用量
  • MC、MU:Metaspace区总量与使用量
  • CCSC、CCSU:压缩类空间总量与使用量
  • YGC、YGCT:YoungGC的次数与时间
  • FGC、FGCT:FullGC的次数与时间
  • GCT:总的GC时间

5、JIT 编译

  • -complier:输出JIT编译器编译过的方法、耗时等信息
  • -printcomplilation:输出已经被JIT编译的方法

6、jmap(Memory Map for Java)

JAVA内存映像工具,一方面用于生成堆转存储快照(heapdump或dump文件),另一方面可以查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前使用的垃圾收集器等

jmap命令格式:
jmap[options]pidjmap [ options ] pidjmap[options]pid
jmap[options]executablecorejmap [ options ] executable corejmap[options]executablecore
jmap[options][pid]server−id@]remote−hostname−or−IPjmap [ options ] [ pid ] server-id@ ] remote-hostname-or-IPjmap[options][pid]server−id@]remote−hostname−or−IP

jmap工具主要选项

选项 作用
-dump 生成JAVA堆转储快照
-finalize 显示在F-Queue中等待finalizer线程执行finalize方法的对象
-heap 显示堆详细信息,如使用的垃圾回收器、参数配置、分代状况等
-histo 显示堆中对象的统计信息,包括类、实例数量、合计容量等
-permstat 以ClassLoader为统计口径显示永久代的内存状态
-F 强制生成dump快照

使用jmap生成dump文件

dump

jmap打印内存直方图或heap信息

内存直方图

官方参考链接:docs.oracle.com/javase/8/do…

7、jstack(Stack Track for Java)

JAVA堆栈跟踪工具,用于生成JVM当前时刻的线程快照(threaddump或javacore文件)。线程快照即当前JVM每一条线程正在执行的方法堆栈集合

主要用途:定位线程长时间停顿的原因,如:线程死锁、死循环、请求外部资源导致的长时间等待等。

jstack命令格式:
jstack[options]pidjstack [ options ] pidjstack[options]pid
jstack[options]executablecorejstack [ options ] executable corejstack[options]executablecore
jstack[options][server−id@]remote−hostname−or−IPjstack [ options ] [ server-id@ ] remote-hostname-or-IPjstack[options][server−id@]remote−hostname−or−IP

jstack工具主要选项:

选项 作用
-F 强制输出线程堆栈
-l 除堆栈外,显示关于锁的附加信息
-m 如果调用本地方法的话,可以显示C++的信息

使用jstack生成线程堆栈,jstack -l 23554 > test.threaddump 保存到文件

jstack生成线程堆栈

官方参考链接:docs.oracle.com/javase/8/do…

举个小栗子:
Java程序抓最耗CPU的线程,可以通过这么几步:

  1. 通过top命令(top之后再按shift+H可以显示线程)查看CPU使用率高的线程;
  2. 将这个线程号转换为16进制,如:printf %x ;
  3. 使用jps查看服务器的Java进程号;
  4. 使用jstack [进程号] 打印当前的进程堆栈;
  5. 从打印的信息中,找到第2步得到的线程号,看看这个线程在做什么。 不一定一次就能抓准线程状态,可以第1步时多记几个线程。

四、JVM 内存结构

JVM内存结构

  • 堆区:S0和S1大小相同,同一个时间点上只有一个启用,另一个是空的
  • 非堆区:操作系统的本地内存,独立于JVM的堆区之外的
  • Metaspace:JDK8新特性
  • CCS:启用短指针才会有
  • CodeCache:把JAVA代码转换为Native代码存在此处,如果没有开启JIT编译,此处内存不存在
  • CCS:启用短指针的时候存到此处

本文转载自: 掘金

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

造轮子必备 什么是优雅关闭?

发表于 2021-11-20

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

引子?

关闭?还特么优雅?

实际上优雅关闭还有另外一个名词, 叫“平滑退出”。如果你打算自己造轮子, 优雅关闭将是你要掌握的第一个知识点。

在生活中,如果有一个名词确实过于难以理解,我们不妨来看这个名词的反面是什么。

举个简单的例子

  • 优雅关闭: 就是使用操作系统关机功能关闭你的计算机
  • 不太优雅的关闭: 直接断电重启

可能有同学会有疑问了: 我平时电脑卡住的时候都是直接断电重启的啊,也没见有啥大问题啊?

计算机与计算机的体质不能一概而论, 如果你的计算机安装的是Linux系统,恰好这又是一台服务器的话,强制重启的话,你大概率会丢掉部分数据,如果是生产环境的话,那就准备提桶跑路吧。

为什么呢?

如果你曾经想过要做MySQL或者Redis的调优, 或多或少接触过以下参数:

  • MySQL的sync_binlog参数, 用来控制持久化binlog数据的存储设备的行为
  • Redis的appendfysnc参数, 用来控制Redis持久化AOF日志到数据存储设备的行为。

出现以上参数的原因是因为将数据持久化到存储设备是一个耗时相对较高的行为, Linux采取的优化措施是,当你往一个文件中数据时会暂存到系统的缓存中, 等待时机再批量持久化到存储设备中。除非进程指定使用DirectIO的方式或者调用fsync,操作系统才会主动将数据写入存储设备。

因此MySQL和Redis纷纷开放了调优参数用来控制日志持久化行为,并将锅甩回给了程序员。

sync_binlog 的值一般为1, 即每次提交事务就立即将binlog数据持久化到存储设备。

优雅关闭

现在,从不太优雅关闭的例子了解到优雅关闭要做什么了:

  • 让程序完成未完成的工作(如:提交事务, 持久化日志等等)

但是,我们还需要加一个限制条件:

  • 当程序决定优雅关闭的时候,就不能再接送任何请求。

不停止处理新请求的话就永远没完没了

线程池的优雅关闭

线程池(ThreadPoolExecutor)在JDK的并发包中占据了重要的位置, 我们可以来看看如此重要的一个基础组件是如何处理优雅关闭的。

该类将是否需要优雅关闭的权限开放给程序员, 并提供了两个方法,分别是:

  • ThreadPoolExecutor.shutdown 该方法会将线程池的状态设置为SHUTDOWN, 并且不再接受新任务的提交,但会让线程池中的线程跑完所有已提交的任务
  • ThreadPoolExecutor.shutdownNow 该方法会将线程池的状态设置为STOP, 并且不再接受新任务的提交, 以及立即向线程池中的所有线程发出中断信号,对于使已提交到线程池中但还未运行的任务直接忽视掉。

优雅关闭进程

如何优雅关闭进程呢? 首先我们需要搞清楚进程什么情况下会关闭:

  • 主动关闭(对于一个对外提供服务的进程来说通常不会主动关闭)
  • 程序崩溃, 如某个业务抛出异常处理不当,导致异常抛到最外层并且没有进行处理导致程序崩溃
  • 进程收到来自操作系统的关闭信号(如按下Ctrl+C)

在企业级应用中,一个进程通常不止有业务逻辑,还有围绕着业务而开发的日志服务/MQ服务/运维服务等等, 那么当某个业务出现可能导致进程崩溃的问题时,我们就需要将进程即将关闭的消息广播给其他服务, 并调用这些服务提供的优雅关闭方法, 以上措施全部完成后再退出进程, 如日志服务的优雅关闭是确保日志落盘, MQ服务的优雅关闭是确保消息被投递出去或者被消费完等等。

如果你是直接杀进程(kill -9)的话, 也就没有必要讨论优雅关闭了

我们以Golang为例来描述如何优雅关闭进程, 首先我们需要对进程中的服务做一个抽象,以便实现生命周期管理, 每个服务提供均需要提供Serve和Shutdown方法。

1
2
3
4
go复制代码type Service interface {
Serve(ctx context.Context) error
Shutdown() error
}

接下来我们定义一个ServiceGroup用来管理Service生命周期, 当任意Service运行出错或接收系统信号SIGINT(Ctrl+C触发)和SIGTREM(kill 不加参数), ServiceGroup将负责关闭关闭由此管理的Service并调用Shutdown方法。

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
go复制代码type ServiceGroup struct {
ctx context.Context
cancel func()
services []Service
}

func NewServiceGroup(ctx context.Context) *ServiceGroup {
g := ServiceGroup{}
g.ctx, g.cancel = context.WithCancel(ctx)
return &g
}

func (s *ServiceGroup) Add(service Service) {
s.services = append(s.services, service)
}

func (s *ServiceGroup) run(service Service) (err error) {
defer func() {
if r := recover(); r != nil {
err = r.(error)
}
}()
err = service.Serve(s.ctx)
return
}

func (s *ServiceGroup) watchDog() {
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
for {
select {
case <- signalChan:
// 接收到系统信号, 通知停止服务
s.cancel()
goto CLOSE
case <- s.ctx.Done():
// 上下文被取消

goto CLOSE
}
}
CLOSE:
for _, service := range s.services {
if err := service.Shutdown(); err != nil {
fmt.Printf("shutdown failed err: %s", err)
}
}
}

func (s *ServiceGroup) ServeAll() {
var wg sync.WaitGroup
for idx := range s.services {
service := s.services[idx]
wg.Add(1)
go func() {
defer wg.Done()
if err := s.run(service); err != nil {
fmt.Println("服务异常, 进入退出流程!")
s.cancel()
}
}()
}
wg.Add(1)
go func() {
defer wg.Done()
s.watchDog()
}()
wg.Wait()
}

接下来,我们定义一个会随机panic的业务服务以及日志服务。

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
go复制代码type BusinessService struct {
}

func (b *BusinessService) Serve(ctx context.Context) (err error) {
times := 0
for {
fmt.Printf("业务运行中 %d\n", times)
select {
case <- ctx.Done():
fmt.Printf("BusinessService receive cancel signal\n")
return
default:
if n := rand.Intn(256); n > 200 {
panic(fmt.Errorf("random panic on %d", n))
}
}
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
times++
}
return
}

func (b *BusinessService) Shutdown() error {
fmt.Println("业务服务, 关闭!")
return nil
}

type LogService struct {
buffer []string
}

func (l *LogService) Serve(ctx context.Context) (err error) {
for {
select {
case <- ctx.Done():
return
default:
// 投递日志到消息队列
time.Sleep(time.Millisecond * time.Duration(rand.Intn(500)))
l.buffer = append(l.buffer, fmt.Sprintf("Time: %d", time.Now().Unix()))
}
}
}

func (l *LogService) Shutdown() (err error) {
fmt.Printf("日志服务, 关闭! 有[%d]条日志待发送\n", len(l.buffer))
if len(l.buffer) == 0 {
return
}
for _, log := range l.buffer {
// 发送日志或者持久化到硬盘
fmt.Printf("Send Log [%s]\n", log)
}
fmt.Println("缓冲区日志清理完毕")
return
}

运行

1
2
3
4
5
6
7
8
go复制代码func main() {
rand.Seed(time.Now().Unix())
ctx := context.Background()
g := NewServiceGroup(ctx)
g.Add(&LogService{})
g.Add(&BusinessService{})
g.ServeAll()
}

运行输出如下所示:

image.png

以上代码还有诸多优化的地方, 读者可自行改进。如可以使用errorgroup对服务进行管理, 以及Shutdwon的时候也可传入上下文做超时管理。

总结

什么是优雅关闭?

  • 让程序完成已提交但未完成的工作
  • 不再接收新的请求

本文转载自: 掘金

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

Java读取文件乱码问题修复记录 问题 原因 解决 总结

发表于 2021-11-20

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

问题

  • 在用Java中读写含中文的文件时,读出或写入的内容经常会出现乱码的问题

原因

  • 系统的编码和程序的编码采用了不同的编码格式
    • windows自身采用的编码格式是gbk. 而gbk和gb2312基本上是一样的编码方式
    • IDEA中Encode默认是utf-8的编码
    • 当在windows中创建并写入的文件编码为gbk. 用程序直接去读会以utf-8方式读取,这样就会乱码
  • 为了避免可能的中文乱码问题,最好在文件写入和读出的时候显式指定编码格式

解决

  • 在Java中,使用java.io.FileReader或者java.io.FileWriter来读写文件
    • 虽然可以通过java.io.BufferedReader和java.io.BufferedWriter来提高效率
    • 但是在FileReader和FileWriter中只能获取编码方式,不能设置编码方式
    • 这样导致在FileReader和FileWriter中的编码设置只能服从底层的编码设置,这样就会在读写多种语言编码的文件时出现乱码
  • 采用java.io.FileInputStream或者java.io.InputStreamReader和java.io.FileOutputStream或者java.io.OutputStreamWriter来解决这个问题
  • InputStreamReader和OutputStreamWriter中,可以通过指定编码方式来完成gbk文件的读写
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 ReadAndWrite {  
private static void test(){
File firstFile = new File("D://fileone.txt");
File secondFile=new File("D://filesecond.txt");
BufferedReader in = null;
BufferedWriter out = null;
try {
//加入编码字符集
in = new BufferedReader(new InputStreamReader(new FileInputStream(firstFile), "gbk"));
//加入编码字符集
out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(secondFile), "gbk"));
String line = "";
while((line = in.readLine())!=null){
System.out.println(line);
out.write(line+"\r\n");
}
} catch (FileNotFoundException e) {
System.out.println("file is not fond");
} catch (IOException e) {
System.out.println("Read or write Exceptioned");
}finally{
if(null!=in){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}}
if(null!=out){
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

总结

  • BufferedWriter最后一定要关闭,否则内容不会被写入到文件中
  • 使用readLine() 方法时,换行写出要添加

本文转载自: 掘金

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

LeetCode412 Fizz Buzz

发表于 2021-11-20

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

题目描述:

412. Fizz Buzz - 力扣(LeetCode) (leetcode-cn.com)

给你一个整数 n ,找出从 1 到 n 各个整数的 Fizz Buzz 示,并用字符串数组 answer(下标从 1 开始)返回结果,其中:

  • answer[i] == "FizzBuzz" 如果 i 同时是 3 和 5 的倍数。
  • answer[i] == "Fizz" 如果 i 是 3 的倍数。
  • answer[i] == "Buzz" 如果 i 是 5 的倍数。
  • answer[i] == i 如果上述条件全不满足。

示例一

1
2
css复制代码输入: n = 3
输出: ["1","2","Fizz"]

示例二

1
2
css复制代码输入: n = 5
输出: ["1","2","Fizz","4","Buzz"]

示例三

1
2
css复制代码输入:n = 15
输出:["1","2","Fizz","4","Buzz","Fizz","7","8","Fizz","Buzz","11","Fizz","13","14","FizzBuzz"]

提示:

  • 1 <= n <= 10^4

思路分析

模拟

抛开题目的背景,这其实就一个int数组转为字符串数组的简单题。

用api的话一个map就搞定了。

我们遍历 1 到 n,当它只是 3 的倍数时,转换为 Fizz,当它只是 5 的倍数时, 转换为Buzz,当它同时是 3 的倍数和 5 的倍数时 FizzBuzz。如果都不满足就转为原数。

逻辑还是非常简单清楚的,直接看代码。

AC代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Kotlin复制代码class Solution {
fun fizzBuzz(n: Int): List<String> {
val answer = ArrayList<String>()
for(i:Int in 1..n) {
var temp:String = ""
if(i%3 == 0) {
temp +="Fizz"
}
if(i%5 == 0) {
temp +="Buzz"
}
if(temp == "") {
temp += i;
}
answer.add(temp);
}
return answer
}
}

总结

FizzBuzz背后的故事比这道题还有意思 Why Can’t Programmers.. Program?

英文有困难的话,可以看看这个中文的,FizzBuzz与写代码的“一万”个细节【图文】_mob604756f56fd6_51CTO博客

看了下,确实有道理,万物来源于生活,leetcode也是。

参考

Fizz Buzz - Fizz Buzz - 力扣(LeetCode) (leetcode-cn.com)

Fizz Buzz 简析 - Fizz Buzz - 力扣(LeetCode) (leetcode-cn.com)

【彤哥来刷题啦】挑战一行代码! - Fizz Buzz - 力扣(LeetCode) (leetcode-cn.com)

本文转载自: 掘金

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

程序员(媛)不懂汉服?岂能让别人小看,咱先靠肉眼大数据识别万

发表于 2021-11-20

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

汉服,是真的漂亮

阅读本篇博客你将收获

  • Python 技术的提升
  • 40000+张汉服照片,或者更多

汉服照片采集术

目标数据源分析

本次要抓取的目标数据参考下图。目标网站为 https://www.hanfuhui.com/,一个以汉服同袍为中心的垂直社区。

程序员(媛)不懂汉服?岂能让别人小看,咱先靠肉眼大数据识别万张穿搭照

本篇博客将要涉及的知识点

  1. requests 读取 json 数据;
  2. json 格式数据解析;
  3. csv 文件存储;
  4. 文件读取+图片保存。

数据来源分析

  • 本次抓取的目标数据为异步传输,即通过服务器接口返回,通过浏览器开发者工具查询接口结构如下:

程序员(媛)不懂汉服?岂能让别人小看,咱先靠肉眼大数据识别万张穿搭照

  • 数据接口为:https://api5.hanfugou.com/Trend/GetTrendListForHot?maxid=3396754&objecttype=album&page=3&count=20,其中重要的参数为 page 与 count,即页码与每页数据量,测试过程发现,count 值可以任意修改,当超过 100 之后,接口返回数据速度变慢,后续该值设置为 500。
  • 接口响应的数据格式为 JSON,如下图所示,接口成功状态为 Status,接口核心数据在 Data 中。
  • 程序员(媛)不懂汉服?岂能让别人小看,咱先靠肉眼大数据识别万张穿搭照
  • 关于 JSON 格式数据的更多学习,可以寻找相应的资料扩展学习,也可以从案例入手,逐步掌握,例如跟随《Python 爬虫 120》专栏,在实践中学习。

需求整理

基于上述分析,整理如下需求:

  • 批量生成接口地址,用于提取图片地址;
  • 为保证效率,将提取到的图片地址批量存储到 csv 文件中;
  • 读取 csv 文件中的图片地址,拼接可下载链接;
  • 下载图片。

案例编码

首先使用 requests 进行基础数据抓取,本步骤比较简单,直接展示代码即可。

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复制代码import requests
from lxml import etree
import time
import re
import random

USER_AGENTS = [
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)",
"其它 USER_AGENT 自己搜集即可"
]


def run(url):
try:
headers = {"User-Agent": random.choice(USER_AGENTS)}
res = requests.get(url=url, headers=headers)
# 直接通过 res.json 方法获取到 JSON 格式数据
json_text = res.json()
# 访问 JSON 中的特定对象,这里没有对接口数据进行验证,如有必要,请通过 Status 属性进行接口请求状态验证
data = json_text["Data"]
img_srcs = []
for item in data:
img_srcs.extend(item["ImageSrcs"])
long_str = "\n".join(img_srcs)
# 保存数据
save(long_str)

except Exception as e:
print(e)


def save(long_str):
try:
with open(f"./imgs.csv", "a+") as f:
f.write("\n"+long_str)
except Exception as e:
print(e)


if __name__ == '__main__':
urls = []
for i in range(1, 10):
print(f"正在爬取第{i}批数据")
run(f"https://api5.hanfugou.com/Trend/GetTrendListForHot?maxid=3396754&objecttype=album&page={i}&count=500")

print("全部爬取完毕")

上述代码缺少接口状态验证逻辑,相关注释已经标注在指定位置,如有必要,可进行扩展。

第一步保存的数据由 40000+,已经满足后续使用,接下来编写获取图片相关逻辑代码。

程序员(媛)不懂汉服?岂能让别人小看,咱先靠肉眼大数据识别万张穿搭照
通过 csv 文件获取的图片连接,直接请求,得到的是一张名称为 hanfuhui-pi-404 的图片,即该请求存在问题。

后续再次测试过程,发现部分图片连接可访问,部分返回 404 图,存在差异化。此处不在进行特殊化处理。

通过页面详情页,获取图片地址,得到如下规律。

当图片地址为 https://pic.hanfugou.com/android/2020/2/30/3b2c6bc54cfa4656a81b4f9b4167e2c3.jpg 时,下载失败,在此基础上,增加图片大小限制,即 https://pic.hanfugou.com/android/2020/2/30/3b2c6bc54cfa4656a81b4f9b4167e2c3.jpg_700x.jpg 得到正确的图片,仅降低了清晰度。

图片抓取代码如下

使用如下代码,需要提前在代码文件目录建立 hanfu 目录,用于存放图片。

由于图片地址的请求协议为 https,顾需要在 requests 发起请求的方法中,新增加一个参数 verify,并设置为 False,该参数表示请求数据时,不验证网站的 ca 证书。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码def save_img(img_src):
try:
url = img_src
headers = {"User-Agent": random.choice(USER_AGENTS)}
# 注意 verify 参数设置为 False ,不验证网站证书
res = requests.get(url=url, headers=headers, verify=False)
data = res.content

with open(f"./hanfu/{int(time.time())}.jpg", "wb+") as f:
f.write(data)
except Exception as e:
print(e)


if __name__ == '__main__':

with open("./imgs.csv","r") as f:
while True:
img_url = f.readline().strip()
if img_url is None:
break
real_url = f"{img_url}_700x.jpg"
save_img(real_url)

代码编写完毕,接下来就是等待程序给我们进货的时间了,出去喝茶啦。

本文转载自: 掘金

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

C 中的 scanf() 和 fscanf() – 简单而强

发表于 2021-11-20

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

我们中的许多人都知道 scanf 的传统用途。好吧,这里有一些鲜为人知的事实

如何只读取我们需要的一部分输入?例如,考虑一些仅包含字符后跟整数或浮点数的输入流。我们只需要扫描那个整数或浮点数。

即,

输入:“这是值 100”,

输出:读取的值是 100

输入:“这是值 21.2”,

输出:读取的值是 21.2

1
2
3
4
5
6
7
8
9
10
11
c++复制代码/* C 程序来演示我们可以忽略 scanf() 中的某些字符串 */
#include <stdio.h>
int main()
{
int a;
scanf("This is the value %d", &a);
printf("Input value read : a = %d", a);
return 0;
}
// Input : This is the value 100
// Output : Input value read : a = 100

现在,假设我们不知道前面的字符是什么,但我们肯定知道最后一个值是一个整数。我们如何将最后一个值扫描为整数?

以下解决方案仅在输入字符串没有空格时才有效。

1
2
3
4
5
6
7
8
9
10
11
12
c++复制代码/* 示例 C 程序来演示 *s 的使用 */
#include<stdio.h>
int main()
{
int a;
scanf("%*s %d", &a);
printf("Input value read : a=%d",a);
return 0;
}

// Input: "blablabla 25"
// Output: Value read : 25

说明: scanf 中的 %*s 用于根据需要忽略某些输入。在这种情况下,它会忽略输入直到下一个空格或换行符。同样,如果你写 %*d 它将忽略整数,直到下一个空格或换行符。

乍一看,上述事实似乎不是一个有用的技巧。为了理解它的用法,我们先来看看fscanf()。

fscanf(): 厌倦了从文件中读取的所有笨拙的语法?好吧, fscanf 来救援。

1
c++复制代码int fscanf(FILE *ptr, const char *format, ...)

fscanf 从 FILE 指针 (ptr) 指向的文件中读取,而不是从输入流中读取。

考虑以下文本文件 abc.txt

1
2
3
4
c++复制代码名称 年龄 城市
美国广播公司 12 海德巴德
bef 25 德里
cce 65 班加罗尔

现在,我们只想读取上述文本文件的城市字段,忽略所有其他字段。fscanf 和上面提到的技巧的组合可以轻松做到这一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c++复制代码/*c 程序演示 fscanf 及其用法*/
#include<stdio.h>
int main()
{
FILE* ptr = fopen("abc.txt","r");
if (ptr==NULL)
{
printf("no such file.");
return 0;
}

/* 假设 abc.txt 包含以下格式的内容
姓名 年龄 城市
abc 12 海德巴
bef 25 德里
cce 65 班加罗尔 */
char buf[100];
while (fscanf(ptr,"%*s %*s %s ",buf)==1)
printf("%s\n", buf);

return 0;
}

输出:

1
2
3
4
c++复制代码城市
海德巴
德里
班加罗尔

本文转载自: 掘金

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

第二十四天、权限管理之JWT

发表于 2021-11-20

1.JWT的简介

Json web token (JWT), 是为了在⽹络应⽤环境间传递声明⽽执⾏的⼀种基于JSON的开放标准((RFC
7519).定义了⼀种简洁的,⾃包含的⽅法⽤于通信双⽅之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使⽤HMAC算法或者是RSA的公私秘钥对进⾏签名。

2.JWT的组成

  • Header 头部
    属于JWT的第一部分。头部包含两部分,token的类型和采用的加密算法,使用Base64编码,也就是说,它可以被翻译回来原来的样子。
1
2
3
4
json复制代码{
"alg": "HS256",
"typ": "JWT"
}
  • Payload负载
    存放信息的地方,常⽤的有 iss(签发者),exp(过期时间),sub(⾯向的⽤户),aud(接收⽅),iat(签发时间)。也是由Base64加密的,所以可以被翻译回原来的样子,所以这里要注意不要把密码放在这部分
  • Signature签名

签名部分,需要使用密钥加上header中的签名算法一起解密才能解开,签名的作用是保证jwt没有被篡改过。如果有⼈对头部以及负载的内容解码之后进⾏修改,再进⾏编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不⼀样的。如果要对新的头部和负载进⾏签名,在不知道服务器加密时⽤的密钥的话,得出来的签名也是不⼀样的。

3.使用JWT的好处

传统的OAuth2在校验token的时候还需要去授权中心去检验token的正确性,从性能上来说会多一步调用,导致性能低下。而使用JWT后,因为JWT由公钥和私钥,我们在授权中心用私钥加密,在资源端用公钥解密,如果能解出来,说明token是有效的。这样就可以减少一部分性能的损耗,而且由于可以在payload端放一些资源信息,在解密成功后也可以直接从jwt中获取一部分信息,减少重复查询的消耗。

4.公钥私钥的生成

使用keytool⼯具⽣成公钥私钥证书

1. 生成密钥证书

使用下面的命令生成密钥证书,采用RSA算法来生成公钥和私钥

1
bash复制代码keytool -genkeypair -alias kaikeba -keyalg RSA -keypass kaikeba -keystore kaikeba.jks -storepass kaikeba

keytool工具是一个java提供的证书管理工具,它的各命令的解释:

  • alias:密钥的别名
  • keyalg:使⽤的hash算法
  • keypass:密钥的访问密码
  • keystore:密钥库⽂件名
  • storepass:密钥库的访问密码

2.导出公钥

使用openSsl来导出公钥信息,下载地址

  1. 配置环境变量

image.png
2. 解密公钥
进入xx.jks 文件所在的文件夹,然后执行下面的命令

1
css复制代码keytool -list -rfc --keystore kaikeba.jks | openssl x509 -inform pem - pubkey

就可以得到公钥信息:

image.png
然后将公钥信息复制到一个public.key的文件夹中。后续放到资源服务器的resources下即可。

5.实际使用

需要使用的依赖

1
2
3
4
5
6
7
8
9
xml复制代码 <!-- OAuth2 鉴权 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>

授权中心的配置

  • 授权中心需要配置一个AuthorizationServerConfiguration继承AuthorizationServerConfigurerAdapter
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
java复制代码package com.study.auth.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

import javax.sql.DataSource;
import java.util.concurrent.TimeUnit;

/**
* 授权信息配置
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private static final Logger logger = LoggerFactory.getLogger(AuthorizationServerConfiguration.class);

@Autowired
@Qualifier("authenticationManagerBean")
AuthenticationManager authenticationManager;

@Autowired
private DataSource dataSource;

@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
//证书路径和密钥库密码
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("t31.jks"), "123456".toCharArray());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//密码别名
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("t31"));
return converter;
}
@Bean
public ClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//配置通过表oauth_client_details,读取客户端数据
clients.withClientDetails(clientDetailsService());
}
/**
* 配置token service和令牌存储⽅式(tokenStore
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//tokenStore
endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtAccessTokenConverter(
)).authenticationManager(authenticationManager);
//tokenService
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(false);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService()
);
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
// 过期时间30天
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30));
endpoints.tokenServices(tokenServices);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 允许表单认证
security.allowFormAuthenticationForClients()
//放⾏oauth/token_key(获得公钥)
.tokenKeyAccess("permitAll()")
//放⾏ oauth/check_token(验证令牌)
// .checkTokenAccess("isAuthenticated()");
.checkTokenAccess("permitAll()");
}
}
  • 授权中心需要配置一个SecurityConfiguration继承WebSecurityConfigurerAdapter
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
scala复制代码package com.study.auth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* security 配置
*/
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

/**
* 注⼊⾃定义UserDetailService,读取rbac数据
*/
@Autowired
private UserDetailsService userDetailsService;


@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers().anyRequest()
//开放/oauth/开头的所有请求
.and().authorizeRequests().antMatchers("/oauth/**").permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//注⼊⾃定义的UserDetailsService,采⽤BCrypt加密
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}

}

资源中心的配置

注:资源中心配置后,在启动的时候会拿公钥去访问一次授权中心,判断公钥是否是正确的。访问的地址为授权中心服务的地址:http://localhost:9098/oauth/token_key 其中 oauth/token_key 是授权框架的接口。

  1. application.yml需要配置如下配置
1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码security:
oauth2:
client:
access-token-uri: http://localhost:9098/oauth/token #令牌端
user-authorization-uri: http://localhost:9098/oauth/authorize #授权端点
client-id: admin-service # 客户端的id 授权会进行客户端的校验,如果客户端不通过,则会提示启动失败
client-secret: 123456 # 客户端的密钥
grant-type: password
scope: read,write
resource:
jwt:
key-uri: http://localhost:9098/oauth/token_key #如果使用JWT,可以获取公钥用于 token 的验签
  1. JWTConfig的配置
    用来做token的解密配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
java复制代码package com.study.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.util.FileCopyUtils;

import java.io.IOException;

@Configuration
public class JWTConfig {

public static final String public_cert = "public.key";

@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;

@Bean
@Qualifier("tokenStore")
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter);
}
@Bean
protected JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource(public_cert);
String publicKey;
try {
publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
}catch (IOException e) {
throw new RuntimeException(e);
}
// 设置校验公钥
converter.setVerifierKey(publicKey);
// 设置证书签名密码,否则报错
converter.setSigningKey("123456");
return converter;
}

}
  1. ResourceServerConfiguration配置
    用来做拦截的资源配置
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
scala复制代码package com.study.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

@Autowired
private TokenStore tokenStore;

@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/user/**").permitAll()
//⽤于测试
.antMatchers("/book/**").hasRole("ADMIN")
.antMatchers("/**").authenticated();
}

@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore);
}

}

1
复制代码

本文转载自: 掘金

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

1…265266267…956

开发者博客

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