跳到主要内容

如何处理音乐CUE文件

· 阅读需 2 分钟

最近在倒腾新买的nas,在音乐刮削用到这个开源项目music-tag-web,但是下载好的音乐文件就是无法进行音轨拆分。

无论是把项目删除、数据清除,反复重新安装music-tag-web都还是会保这样的错误。

music-tag-web

扒拉扒拉源码也没有找到对应的地方,估计v1的版本也没有继续开源了。这下只能自己手动进行音轨拆分了。

这里需要用到几个工具:

  • shntool
  • ffmpeg
  • iconv

mac上安装这几个工具,可以使用brew

brew install shntool ffmpeg libiconv

ffmpeg

如果音频是ape格式的,需要使用ffmpeg进行转换

ffmpeg -i CDImage.ape CDImage.wav

shntool

根据cue文件切分音轨的工具

shntool split -t %t -f CDImage.cue -o wav CDImage.wav -d .

-t 参数表示分割出来的文件采用什么文件名,%t 表示用歌曲名字命名;-f 表示输入的 cue 文件;-o 指定输出格式;-d 参数为输出目录,此例用点表示当前目录。

iconv

编码转换工具 大部分CUE文件都是在windows系统下生成的,所以需要将cue文件的编码转换为utf-8。

iconv -f gbk -t utf8 CDImage.cue > CDImage-1.cue

ok,let' t fire~

result

Loki-管理日志从未如此简单

· 阅读需 5 分钟

一个项目中,需要需要管理大量的日志,并且需要快速查询和分析日志。如果使用传统的日志管理方式,比如使用tail -f,或者使用grepawk等命令,查询和分析日志会非常困难。如果使用ELK,需要部署大量的组件,并且需要花费大量的时间进行配置和优化。

如果使用Grafana Loki,可以非常方便地管理日志,并且可以非常方便地查询和分析日志。而且系统资源占用小,部署简单。

什么是Loki

Loki 官方文档

Loki 是一个开源的日志管理系统,它使用 Prometheus 的存储格式来存储日志数据。Loki 的存储格式非常高效,可以存储大量的日志数据,并且可以非常方便地查询和分析日志。

设计理念

  • 简单性:Loki 的一个核心目标是简单。它不需要复杂的索引结构,而是将日志数据按流(stream)存储,以标签(labels)为基础进行查询。这种设计使得它的使用和配置相对容易。
  • 无结构化:与大多数日志系统不同,Loki 不会对日志内容进行索引,而是将其按照时间戳和标签存储。这意味着用户可以更快地写入日志,而不需要担心复杂的索引管理。

配置文件

创建一个loki-config.yaml文件为后续做准备,内容如下:

auth_enabled: false

server:
http_listen_port: 3100
grpc_listen_port: 9096
log_level: debug
grpc_server_max_concurrent_streams: 1000

common:
ring:
instance_addr: 0.0.0.0
kvstore:
store: inmemory
replication_factor: 1
path_prefix: /tmp/loki

query_range:
results_cache:
cache:
embedded_cache:
enabled: true
max_size_mb: 100

limits_config:
allow_structured_metadata: true
volume_enabled: true

schema_config:
configs:
- from: 2025-02-24
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h

storage_config:
tsdb_shipper:
active_index_directory: /tmp/loki/index
cache_location: /tmp/loki/index_cache
filesystem:
directory: /tmp/loki/chunks

pattern_ingester:
enabled: true

什么是Alloy

Grafana Alloy 是 Grafana Labs 开发的一款开源工具,旨在提升数据和监控的可视化体验。

Alloy 官方文档

引用官方的一张图很容易理解Alloy能做什么

Alloy 官方图

Alloy有多个组件,可以轻松支持从应用程序、数据库和OpenTelemetry收集数据,并将其转换为可查询的指标。

配置文件

创建一个config.alloy文件为后续做准备,内容如下:

discovery.docker "linux" {
host = "unix:///var/run/docker.sock"
}

discovery.relabel "containers" {
targets = []

rule {
source_labels = ["__meta_docker_container_label_enable_alloy"]
regex = "^true$"
action = "keep"
}

rule {
source_labels = ["__meta_docker_container_name"]
target_label = "container_name"
}

rule {
source_labels = ["__meta_docker_container_id"]
target_label = "container_id"
}

}

loki.source.docker "default" {
host = "unix:///var/run/docker.sock"
targets = discovery.docker.linux.targets
labels = {"platform" = "docker"}
relabel_rules = discovery.relabel.containers.rules
forward_to = [loki.write.local.receiver]
}


loki.write "local" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
batch_wait = "5s"
batch_size = "1MiB"
min_backoff_period = "1s"
max_backoff_period = "10s"
}
}


loki.process "gflog_fmt" {
stage.regex {
expression = `^(?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \[(?P<level>[A-Z]+)\] \{(?P<request_id>[a-f0-9]+)} \[(?P<duration>\d+ ms)\] (?P<content>.*)$`
}
stage.labels {
values = {
timestamp = "timestamp",
detected_level = "level",
duration = "duration",
}
}
stage.replace {
expression = "WARN"
replace = "warning"
source = "detected_level"
}
stage.replace {
expression = "ERRO"
replace = "error"
source = "detected_level"
}
stage.replace {
expression = "DEBU"
replace = "debug"
source = "detected_level"
}
stage.timestamp {
source = "timestamp"
format = "2006-01-02 15:04:05.000"
}
stage.output {
source = "content"
}
forward_to = [loki.write.local.receiver]
}

这里有几个关键点:

  • discovery.docker:用于发现Docker容器,并将其作为日志源。
  • loki.source.docker:用于将Docker容器的日志写入Loki。
  • loki.process:用于将日志格式化,并写入Loki。
  • loki.write:用于将日志写入Loki。

discovery.relabel这个配置中加入了docker label过滤,只有enable_alloytrue的容器才会被采集。

loki.process "gflog_fmt"中,使用了GoFrame框架,所以需要对日志的level进行处理,以满足Loki的level要求。

只有level格式一直的情况下,在Grafana中才能正确显示日志颜色。

Grafana 日志颜色

部署

接下来使用DockerCompose部署Loki和Alloy。

version: "3.8"

services:
loki:
container_name: loki
image: grafana/loki:3.4
ports:
- "3100:3100"
volumes:
- ./loki-config.yaml:/etc/loki/loki-config.yaml
command: -config.file=/etc/loki/loki-config.yaml
alloy:
container_name: alloy
image: grafana/alloy:latest
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./config.alloy:/etc/alloy/config.alloy
command: run --server.http.listen-addr=0.0.0.0:12345 /etc/alloy/config.alloy
ports:
- "12345:12345"
depends_on:
- loki

我这边的Grafana是独立部署的,我也个出一个参考的docker-compose.yaml文件,仅供参考。

version:  '3'
services:
grafana:
image: grafana/grafana:10.1.9
deploy:
resources:
limits:
cpus: '0.5'
memory: 2G
container_name: grafana
restart: always
volumes:
- ./grafana.ini:/etc/grafana/grafana.ini
- ./volumes/grafana:/var/lib/grafana
network_mode: "host"
expose:
- "3000"

接下来一个命令docker-compose up -d一把梭服务正常就能起来了。

Docker ps

配置Grafana

打开Grafana,添加Loki数据源。路径: connections -> data sources -> add data source -> Loki

添加一个Dashboard,添加一个名为Live logs的panel。类型选择Logs,然后选择刚刚添加的Loki数据源。

Grafana Live logs

了解HD Wallet

· 阅读需 4 分钟

数字钱包是进入加密货币世界的核心工具,本质是一套精密的密钥管理系统,而非简单的"存储容器"。

数字钱包

数字钱包是一种用于管理加密货币的工具,核心功能包括生成密钥对、存储资产、签署交易以及与区块链网络交互。其本质是密钥管理系统:

  • ​私钥:控制资产的唯一凭证(所有权证明)。
  • ​公钥:生成接收地址,公开可见。
  • ​地址:由公钥衍生(如Base58Check编码),用于接收资金。

BIP32:分层确定性钱包(HD Wallet)

原理 BIP32(Bitcoin Improvement Proposal 32)提出了一种分层确定性钱包(HD Wallet)​的结构,允许从单个种子(Seed)生成树状结构的密钥对。其核心思想是通过分层推导实现密钥的无限扩展,同时仅需备份一个主种子即可恢复所有密钥。

关键流程:

  • ​根种子生成:通过随机熵源生成一个根种子(通常为128-256位)。
  • ​主密钥派生:使用HMAC-SHA512算法将根种子拆分为主私钥(m)和主链编码(Chain Code)。
  • 子密钥派生:通过父私钥/公钥、链编码和索引号,逐层生成子密钥树。

特点:

  • ​确定性:相同种子生成的密钥树完全一致。
  • 单向性:子密钥无法推导父密钥或兄弟密钥。
  • 强化衍生:索引号≥2³¹时使用父私钥推导,增强安全性。

BIP39:助记词与种子生成

原理 BIP39通过助记词将随机熵转化为易备份的单词序列,并通过PBKDF2算法生成种子。

生成步骤:

  • ​熵生成:生成128/160/192/224/256位的随机熵。
  • 校验和:取熵的SHA256哈希前n位(n=熵长度/32)作为校验和。
  • 分割单词:将"熵+校验和"按11位分组,映射到2048个预定义单词
  • ​种子生成:使用PBKDF2(助记词 + 盐值 "mnemonic")生成512位种子

BIP44:多币种钱包路径规范

原理

BIP44基于BIP32定义了标准化的分层路径,支持多币种、多账户的钱包管理。

路径结构:

m / purpose' / coin_type' / account' / change / address_index

  • ​purpose':固定为44(表示BIP44标准)。
  • coin_type':币种标识(如0=比特币,60=以太坊)完整的币种列表地址
  • account':账户索引(从0开始)。
  • change:0=外部地址(收款),1=内部地址(找零)。
  • address_index:地址序号(从0递增)。

示例路径:

  • 以太坊主账户第一个地址:m/44'/60'/0'/0/0
  • 比特币测试网第二个地址:m/44'/1'/0'/0/1

三者协作关系

  • ​BIP39生成助记词,转化为种子。
  • BIP32用种子生成主密钥和树状子密钥。
  • BIP44定义路径规则,实现多币种、多账户管理。

优势总结:

  • 易备份:仅需保存助记词或种子。
  • ​安全性:种子冷存储,子密钥可热生成。
  • ​标准化:跨钱包兼容(如MetaMask、Ledger)

写到最后

理解完原理,进行实操才是王道。

为此,我开发了一个简单的网页应用,用于生成和展示HD Wallet的密钥。

可以访问https://cryptohub.lol/tools/hd-wallet进行体验。

使用Cloudflare CDN加速博客资源

· 阅读需 3 分钟

Cloudflare是一个广泛使用的CDN服务,它不仅可以加速博客资源,还能提供额外的安全保护。本文将介绍如何使用Cloudflare CDN加速你的博客及其好处。

什么是CDN?

内容分发网络(CDN)是一组分布在多个地理位置的服务器,通过将内容缓存到离用户更近的位置,来提高网站的加载速度。CDN通过减少用户与服务器之间的距离,确保用户能够快速加载网页和资源。

为什么使用Cloudflare CDN?

  • 全球覆盖:Cloudflare在多个国家和地区拥有数据中心,能够为全球用户提供快速的访问速度。
  • 安全性:Cloudflare提供防火墙、DDoS防护和SSL加密等安全功能,保护你的博客免受网络攻击。
  • 易于使用:Cloudflare的设置过程简单明了,即使是初学者也能轻松上手。
  • 免费计划:Cloudflare提供免费的基础服务,非常适合个人博客和小型网站。

对于白嫖党来说,Cloudflare的免费计划,足够可以满足大部分出场景需求。

配置CDN设置

  • 首先设置一个cdn域名,比如我这里设置一个cdn.wawov.com,CNAME到cdn.jsdelivr.net。 这里有小明就会问,这域名在国内不是访问不了吗?先别急,后面会解决。

CDN设置

重定向规则

  • 接下来我们去配置一下重定向规则,点击规则 -> 重定向规则 -> 创建规则

重定向规则

这里需要简单配置两个规则就可以解决国内访问不了的问题。

  • (http.host eq "cdn.wawov.com" and ip.geoip.country ne "CN")
  • (http.host eq "cdn.wawov.com" and ip.geoip.country eq "CN")

第一个规则,当访问的ip不是中国大陆的时候,重定向到https://cdn.jsdelivr.net

第二个规则,当访问的ip是中国大陆的时候,重定向到https://cdn.jsdmirror.com

具体配置可以参考下图:

cdn_cn

cdn_abord

验证配置

通过代理访问具体资源,验证是否生效。

无代理访问

代理访问

可以看到在Cloudflare设置的规则转发已经生效。

总结

通过使用Cloudflare CDN,与规则设置,这样就可以在博客使用一个统一的域名进行定义资源。希望本文对你有所帮助,让你能够更好地利用Cloudflare CDN加速你的博客。

Bye Travis , Hello Github Action

· 阅读需 2 分钟

最近折腾了一下Github的Action,发现还挺好用的。那就折腾到底吧,把Travis的构建Blog的CI迁移到Github的Action。毕竟有时间打开Travis还挺慢的。

先理一下整体流程

  1. npm install
  2. hexo g
  3. 进入public目录push到对应的仓库

创建一个Action,默认的会在当前仓库下.github/workflows目录下创建一个xxx.yml文件。这个Workflow的语法主干有几个元素nameonenvjobs

/**
workflows的名字
*/
name:

/**
如何触发当前workflow,多个触发条件可以使用数组的方式,例如:
on: [push,pull_request]
*/
on:

/**
环境变量,以key-valued方式
*/
env:

/**
一个workflow由一个或者多个job组成,job默认都是并行运行的,如果有job之间的依赖可以使用jobs.<job_id>.needs关键字。
每一个job可以必须使用关键字jobs.<job_id>.runs-on制定运行环境。可用的环境目前有:
ubuntu-latest, ubuntu-18.04, or ubuntu-16.04
windows-latest, windows-2019, or windows-2016
macOS-latest or macOS-10.14
*/
jobs

按照之前整理的流程可以写出一个workflows:

name: DeployBlog

on:
push:
branches:
- master
paths:
- '!.github/**'

env:
GH_REF: github.com/ledboot/ledboot.github.io.git
GitHub_Token: ${{ secrets.token }}

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master

- name: npm install & hexo g
run: |
npm install
./node_modules/.bin/hexo g

- name: publish
run: |
if [ -d 'public' ];then
cd ./public
git init
git config user.name "xxx"
git config user.email "xxx@gmail.com"
git add .
git commit -m "Update docs"
git push --force --quiet "https://${GitHub_Token}@${GH_REF}" master:master
fi

这里添加了一些细节处理:

  • on中指定触发的分支,并且忽略.github目录,因为我编辑workflow文件,提交的时候,并不想触发构建。
  • 添加secrets,可以在Setting Tab页中添加,引用方式{${{secrets.xxxx}}}

CKA之路

· 阅读需 3 分钟

接触kubernetes已经有2年多了,这段时间里云原生的概念慢慢被普及,许多公司纷纷走上了上云的路上,使用kubernetes去管理生产上面的应用,kubernetes的使用正在爆炸式地增长。Linux基金会和云原生计算基金会(CNCF)为帮助开发Kubernetes生态系统创建了Certified Kubernetes Administrator简称CKA。

刚开始知道这个认证的时候,也没有想过去考,而且但是考的话,国内是没有考场的,只能用自己的电脑科学上网打开web terminal,延迟应该挺厉害的。

直到今年国内才有考场试运行CKA考试。第一阶段的试运行,我知道的时候已经过时了,但是也还是没有想考的想法。时间来到8月,第二阶段的试运行来了,纠结了几天,结果还是按下了支付的按钮。看了一下那些基础课程对我没有什么用,毕竟我是有基础的嘛,搞了2年也不是白混的啦。看了一下时间要在9月30号之前完成考试。接下来的日子可能要熬熬夜了。

接下来我在网上搜了一圈,发现有些不错的视频

看完2个视频,我已经飘了,感觉可以去考试了。因为发现这个认证难度其实并不大,主要是熟练一些命令,深刻了解kubernetes的运作机制。对于已经接触2年kubernetes的我而言,问题已经不大。现在主要就是练速度了,毕竟考试时间是3个小时。

接下来我在网上搜索了一些CKA的考题,不断的练习就是了。下面链接是我为知笔记记录的题目:

CKA题库

考试小技巧:

  • 考试过程可以打开一个记事本,可以把一些常用的模版复制到这里来,便于后续题目修改编辑,快速创建。
  • 熟悉文档https://kubernetes.io/docs/home/ 毕竟很多东西,考试的时候忘记了,也可以在这里搜索出来。
  • 有时候看中文题目可能翻译不太准确,切换英文的看一次,确保自己理解与实际题目一致。

在考试前几天,我发现了一个网站,这个网站简直了,不过是全英文,而且不带字幕的,是一个国外的学习网站,这个网站可以秒杀上面2个视频。我看中它的是有模拟考试,我的天!二话不说,直接支付35刀。前提是你要有一张外币信用卡噢。

KodeKloud

如果你在10月5号看到这篇文章。发邮件(popmusicbbq@126.com)给我,友情分享账号。毕竟我只买了一个月。

意外总会有的,我考试的时候网页502,还有3题没有做,​我也是服了。都打算申请重考了。果然试运行就是这样呀。

考试结果

可是实力不允许呀,过了几天居然我收到了通过的邮件,Surprise!

通过邮件

TLS Bootstrapping Worker Nodes

· 阅读需 2 分钟

step 1

kube-systemnamespace下创建一个secret,名字格式:bootstrap-token-<token>

cat > bootstrap-token-05832d.yaml << EOF
apiVersion: v1
kind: Secret
metadata:
name: bootstrap-token-05832d
namespace: kube-system
type: bootstrap.kubernetes.io/token
stringData:
token-id: 05832d
token-secret: x262bbbe835dx21k
usage-bootstrap-authentication: "true"
usage-bootstrap-signing: "true"
auth-extra-groups: system:bootstrappers:node03
EOF

step 2

授权节点创建CSR

kubectl create clusterrolebinding crb-bootstrappers --clusterrole=system:node-bootstrapper --group=system:bootstrappers

step 3

创建bootstrap-kubeconfig

kubectl config --kubeconfig=/var/lib/kubelet/bootstrap-kubeconfig set-cluster bootstrap --server='https://172.17.0.77:6443' --certificate-authority=/etc/kubernetes/pki/ca.crt
kubectl config --kubeconfig=/var/lib/kubelet/bootstrap-kubeconfig set-credentials kubelet-bootstrap --token=05832d.x262bbbe835dx21k
kubectl config --kubeconfig=/var/lib/kubelet/bootstrap-kubeconfig set-context bootstrap --user=kubelet-bootstrap --cluster=bootstrap
kubectl config --kubeconfig=/var/lib/kubelet/bootstrap-kubeconfig use-context bootstrap

step 4

配置kubelet.service,注意路径在/etc/systemd/system/kubelet.service

[Unit]
Description=Kubernetes Kubelet
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/bin/kubelet \
--bootstrap-kubeconfig=/var/lib/kubelet/bootstrap-kubeconfig \
--kubeconfig=/var/lib/kubelet/kubeconfig \
--register-node=true \
--v=2
Restart=on-failure
StandardOutput=file:/var/kubeletlog1.log
StandardError=file:/var/kubeletlog2.log
RestartSec=5

[Install]
WantedBy=multi-user.target

配置好kubelet.service之后:

systemctl daemon-reload
systemctl enable kubelet
systemctl start kubelet

kubelet正常启动之后,在master节点上kubectl get csr,可以看到一个pending状态的csr,可以通过创建一个clusterrolebinding让csr自动approve csr。

step 5

自动approve csr

kubectl create clusterrolebinding crb-node-autoapprove-csr --clusterrole=system:certificates.k8s.io:certificatesigningrequests:nodeclient --group=system:bootstrappers

证书过期自动续签

kubectl create clusterrolebinding crb-node-autorotate-csr --clusterrole=system:certificates.k8s.io:certificatesigningrequests:selfnodeclient --group=system:nodes

Done !

protocol buffer 使用

· 阅读需 10 分钟

安装

mac上使用homebrew安装,

brew install protobuf

其他os,可以到官网下载。

编写.proto文件

列子:


package pingan.kanyun.sdk;


option java_package = "com.pingan.kanyun.sdk.proto";



message DeviceInfo{

optional string os_version = 1;//OS版本,

optional string app_version = 2;//APP版本,

optional string imei_meid = 3;//机器码(IMEI/MEID),

optional string bandName = 4;//客户端品牌信息

optional string model = 5;//手机型号

optional string cpuModel = 6;//CPU型号

optional string cpuInstructionSet = 7;//CPU指令集

optional string cpuHardware = 8;//CPU厂商

optional bool isRoot = 9;//是否越狱

optional string displaySize = 10;//屏幕分辨率

optional string language = 11;//语言

optional int32 result_per_page = 12;

repeated int32 samples = 13 [packed=true];

enum PhoneType{

MOBILE = 0;

HOME = 1;

WORK = 2;

}

}

message定义了11个field,每个field都有名称与类型组成。

  • 指定field的类型

可以是基本类型:string int32,也可以是指定的复杂类型属性,包括枚举和其他类型

  • 分配标签

每一个field都是唯一数字的标记,这是用来标记这个field在message二进制格式中的位置,一旦使用就不能在修改顺序

ps:

  • 标记从1-15只有一个字节编码,包括自增长属性

  • 标记从16-2047占用两个字节。因此尽量频繁使用1-15,记住为未来的扩展留下一些位置。

  • 最小的tag你可以定义为1,最大2的29次方-1 536870922.你同样不能使用19000-19999(这个位置已经被Google PB实现)

  • 由于历史原因,repeated字段如果是基本数字类型的话,不能有效地编码。现在代码可以使用特殊选项[packed=true]来得到更有效率的编码。

如果字段的属性值是固定的几个值,可以使用枚举

Enumerations


message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3 [default = 10];
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
optional Corpus corpus = 4 [default = WEB];
}

自定义消息类型


message SearchResponse {
repeated Result result = 1;
}
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}

import 定义

使用import关键字,可以引用另外一个.proto文件


import "myproject/other_protos.proto";

内部类


message SearchResponse {
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}

引用内部类是,使用parent.type,例如:


message SomeOtherMessage {
optional SearchResponse.Result result = 1;
}

Extentions

extensions 声明一个消息中的一定范围的field的顺序数字用于进行扩展。其它人可以在自己的.proto文件中重新定义这些消息field,而不需要去修改原始的.proto文件


message Foo{

extentions 100 to 199;//这说明100~199是保留的。

}

在新的fields添加Foo


extend Foo {
optional int32 bar = 126;
}

内嵌的extensions


message Baz {
extend Foo {
optional int32 bar = 126;
}
}

选择Extension 顺序数字

非常重要的一点是双方不能使用同样数字添加一样的message类型,这样extension会被解释为错误类型。

Packages

可以给一个.protol文件增加一个optional的package描述,来保证message尽量不会出现名字相同的重名。

package会根据选择的语言来生成不同的代码。

package foo.bar;
message Open {
}

package会根据选择的语言来生成不同的代码:

  • C++

生成的classes是用C++的namespace来区分的。举例:Open would be in the namespace foo::bar。

  • Java

package用于Java的package,除非你单独的指定一个option java_package 在.proto文件中。

  • Python

package是被忽略的,因为Python的modules是通过它们的文件位置来组织的。

options

在一个proto文件中,还可以存在一些options。options不能改变一个声明的整体的意义,但是可以影响一定的上下文。

  1. 一些options是第一级的,意味着它们应该被写在顶级范围,而不是在任何message,enum,sercie的定义中。

  2. 一些options是message级别的,意味着它们应该被写入message的描述中。

  3. 一些options是field-level级别的,意味着它们应该被写入field的描述中,options也可以被写入enum类型中,enum的值,service类型 和service方法。

常用的options:

  • java_package (file option)

定义生成的java class的package。如果在proto文件中没有明确的java_package选项,那么默认会使用package关键字指定的package名。

但是proto package通常不会好于Java packages,因为proto packages通常不会以domain名称开始。

如果不生成java代码,此选项没有任何影响。

例子:

option java_package = "com.example.foo";

  • java_outer_classname (file option)

指定想要生成的class名称,如果此参数没有指定的话,那么默认使用.proto文件名来做为类名,并且采用驼峰表示(比如:foo_bar.proto 为 FooBar.java)

如果不生成java代码,此选项没有影响。

例子:

option java_outer_classname = "Ponycopter";

  • optimize_for (file option)

可以设置为speed、code_size或者lite_runtime。

  1. SPEED:默认。protocol编译器会生成classes代码,提供了message类的序列化、转换和其它通用操作。这个代码是被高度优化过的。

  2. CODE_SIZE: protocol编译器会生成最小的classes,并且依赖共享、基于反射的代码实现序列化、转换和其它通用操作。生成的classes代码小于speed,但是操作会慢一点。classes会实现跟SPEED模式一样的公共API。这个模式通常用在一个应用程序包含了大量的proto文件,但是并不需要所有的代码都执行得很快

  3. LITE_RUNTIME: protocol编译器会生成仅仅依赖 lite 运行库(libprotobuf-lite代替libprotobuf)。lite运行时比全量库小很多,省略了某种特性(如: descriptors and reflection)这个选项对于运行在像移动手机这种有约束平台上的应用更有效。 编译器仍然会对所有方法生成非常快的代码实现,就像SPEED模式一样。protocol编译器会用各种语言来实现MessageList接口,但是这个接口仅仅提供了其它模式实现的Message接口的一部分方法子集。

例子:

option optimize_for = CODE_SIZE;

  • cc_generic_services, java_generic_services, py_generic_services (file options)

无论如何,protoc编译器会生成基于C++,Java,Python的抽象service代码,这些默认都是true。截至到2.3.0版本,RPC实现提供了代码生成插件去生成代码,不再使用抽象类。


option cc_generic_services = false;
option java_generic_services = false;
option py_generic_services = false;

  • message_set_wire_format (message option)

如果设置为true,消息使用不同的二进制格式来兼容谷歌内部使用的称为MessageSet的旧格式。用户在google以外使用,将不再需要使用这个option。

消息必须按照以下声明:



message Foo {
option message_set_wire_format = true;
extensions 4 to max;
}


  • 自定义options

protocol buffer还允许你自定义options。这是个高级特性,大多数人并不需要。options其实都定义在 google/protobuf/descriptor.proto文件中。

自定义的options是简单的,继承这些messages:



import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
message MyMessage {
option (my_option) = "Hello world!";
}


这里我们定义了一个message级别的消息选项,当使用这个options的时候,选项的名称必须用括号括起来,以表明它是一个extension。

我们在C++中读取my_option的值就像下面这样:



string value = MyMessage::descriptor()->options().GetExtension(my_option);

在Java中:



String value = MyProtoFile.MyMessage.getDescriptor().getOptions().getExtension(MyProtoFile.myOption);


  • 生成class代码

为了生成java、python、C++代码,你需要运行protoc编译器 protoc 编译.proto文件。编译器运行命令:



protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto


使用.代表当前目录。

import_path 查找proto文件的目录,如果省略的话,就是当前目录。存在多个引入目录的话,可以使用--proto_path参数来多次指定,

-I=IMPORT_PATH就是--proto_path的缩写

输出目录

  • --cpp_out 生成C++代码在DST_DIR目录

  • --java_out 生成Java代码在DST_DIR目录

  • --python_out 生成Python代码在DST_DIR目录

有个额外的好处,如果DST是.zip或者.jar结尾,那么编译器将会按照给定名字输入到一个zip压缩格式的文件中。

输出到.jar会有一个jar指定的manifest文件。注意 如果输出文件已经存在,它将会被覆盖;编译器的智能不足以自动添加文件到一个存在的压缩文件中。

你必须提供一个或者多个.proto文件用作输入。虽然文件命名关联到当前路径,每个文件必须在import_path路径中一边编译器能规定它的规范名称。

更新message

如果一个message 不再满足所有需要,需要对字段进行调整。(举例:对message增加一个额外的字段,但是仍然有支持旧格式message的代码在运行)

要注意以下几点:

  1. 不要修改已经存在字段的数字顺序标示

  2. 可以增加optional或者repeated的新字段。这么做以后,所有通过旧格式message序列化的数据都可以通过新代码来生成对应的对象,正如他们不会丢失任何required元素。你应该为这些元素添加合理的默认值,以便新代码可以与旧代码生成的消息交互。 新代码创建的消息中旧代码不存在的字段,在解析的时候,旧代码会忽略掉新增的字段。无论如何,未知的field不会被丢弃,注意未知field对于Python来说当前不可用。

  3. 非required字段都可以转为extension ,反之亦然,只要type和number保持不变。

  4. int32, uint32, int64, uint64, and bool 是全兼容的。这意味着你能改变一个field从这些类型中的一个改变为另一个,而不用考虑会打破向前、向后兼容性。如果一个数字是通过网络传输而来的相应类型转换,你将会遇到type在C++中遇到的问题。(e.g. if a 64-bit number is read as an int32, it will be truncated to 32 bits)

  5. sint32 and sint64 彼此兼容,但是不能兼容其它integer类型。

  6. string and bytes 在UTF-8编码下是兼容的。

  7. 如果bytes包含一个message的编码,内嵌message与bytes兼容。

  8. fixed32 兼容 sfixed32, fixed64 兼容 sfixed64。

  9. optional 兼容 repeated。用一个repeat字段的编码结果作为输入,认为这个字段是可选择的客户端会这样处理,如果是原始类型的话,获得最后的输入作为相应的option值;如果是message 类型,合并所有输入元素。

  10. 更改默认值通常是OK的。要记得默认值并不会通过网络发送,如果一个程序接受一个特定字段没有设置值的消息,应用将会使用自己的版本协议定义的默认值,不会看见发送者的默认值。

ASM Bytecode Framework探索与使用

· 阅读需 7 分钟

SM_cover.png) -->

ASM是一款基于java字节码层面的代码分析和修改工具。无需提供源代码即可对应用嵌入所需debug代码,用于应用API性能分析。ASM可以直接产生二进制class文件,也可以在类被加入JVM之前动态修改类行为。

ASM库的结构

.png) -->

  • Core 为其他包提供基础的读、写、转化Java字节码和定义的API,并且可以生成Java字节码和实现大部分字节码的转换
  • Tree提供了Java字节码在内存中的表现
  • Analysis为存储在tree包结构中的java方法字节码提供基本的数据流统计和类型检查算法
  • Commons提供一些常用的简化字节码生成转化和适配器
  • Util包含一些帮助类和简单的字节码修改,有利于在开发或者测试中使用
  • XML提供一个适配器将XML和SAX-comliant转化成字节码结构,可以允许使用XSLT去定义字节码转化。
class文件结构

在了解ASM之前,有必要先了解一下class文件结构。对于每个class文件其实都是有固定的结构信息,而且保留了源码文件中的符号。下图是class文件的格式图。其中带 * 号的表示可重复的结构。

.png)

  • 类结构体中所有的修饰符、字符常量和其他常量都被存储在class文件开始的一个常量堆栈(Constant Stack)中,其他结构体通过索引引用。
  • 每个类必须包含headers(包括:class name, super class, interface, etc.)和常量堆栈(Constant Stack)其他元素,例如:字段(fields)、方法(methods)和全部属性(attributes)可以选择显示或者不显示。
  • 每个字段块(Field section)包括名称、修饰符(public, private, etc.)、描述符号(descriptor)和字段属性。
  • 每个方法区域(Method section)里面的信息与header部分的信息类似,信息关于最大堆栈(max stack)和最大本地变量数量(max local variable numbers)被用于修改字节码。对于非abstract和非native的方法有一个方法指令表,exceptions表和代码属性表。除此之外,还可以有其他方法属性。
  • 每个类、字段、方法和方法代码的属性有属于自己的名称记录在类文件格式的JVM规范的部分,这些属性展示了字节码多方面的信息,例如源文件名、内部类、签名、代码行数、本地变量表和注释。JVM规范允许定义自定义属性,这些属性会被标准的VM(虚拟机)忽略,但是可以包含附件信息。
  • 方法代码表包含一系列对java虚拟机的指令。有些指令在代码中使用偏移量,当指令从方法代码被插入或者移除时,全部偏移量的值可能需要调整。
基于事件字节码处理

在Core包中逻辑上分为2部分:

  • 字节码生产者,例如ClassReader
  • 字节码消费者,例如writers(ClassWriter, FieldWriter, MethodWriter和AnnotationWriter),adapters(ClassAdapter和MethodAdapter)

下图是生产者和消费者交互的时序图:

.png)

通过时序图可以看出ASM在处理class文件的整个过程。ASM通过树这种数据结构来表示复杂的字节码结构,并利用Push模型来对树进行遍历。

  • ASM中提供一个ClassReader类,这个类可以直接由字节数组或者class文件间接的获得字节码数据。它会调用accept方法,接受一个实现了抽象类ClassVisitor的对象实例作为参数,然后依次调用ClassVisitor的各个方法。字节码空间上的偏移被转成各种visitXXX方法。使用者只需要在对应的的方法上进行需求操作即可,无需考虑字节偏移。
  • 这个过程中ClassReader可以看作是一个事件生产者,ClassWriter继承自ClassVisitor抽象类,负责将对象化的class文件内容重构成一个二进制格式的class字节码文件,ClassWriter可以看作是一个事件的消费者
原java类型与class文件内部类型对应关系
Java typeType descriptor
booleanZ
charC
byteB
shortS
intI
floatF
longJ
doubleD
ObjectLjava/lang/Object;
int[][I
Object[][][[Ljava/lang/Object;
原java方法声明与class文件内部声明的对应关系
Method declaration in source fileMethod descriptor
void method(String str,int i,float f)(Ljava/lang/String;IF)V
Object method(byte [] b)([B)Ljava/lang/Object;
int[] method(double d)(D)[I
遍历CLASS字节码类信息

以java.lang.Runnable作为例子

.png)

输出:

superName=java/lang/Object,name=java/lang/Runnable
run()V
end

ClassReader类的accept方法中,有个int类型的flag参数有以下几种:

  • SKIP_DEBUG 用于忽略debug信息,例如,源文件,行数和变量信息。
  • SKIP_FRAMES 用于忽略StackMapTable(栈图)信息。Java 6 之后JVM引入栈图概念。
  • EXPAND_FRAMES 扩展StackMapTable数据,允许访问者获取全部本地变量类型与当前堆栈位置的信息。
  • SKIP_CODE 排除代码访问的所有方法,同时还通过方法参数属性和注释。
通过ASM生产自定义类对应的class

目标class内容:

.png)

生产目标class的代码:

这里需要注意,平时我们写类的时候,默认的构造方法是可以不写的,但使用ASM框架生产class的话,默认的构造方法是需要写的,不然,无法实例化对象。

创建类、构造函数与字段:

.png)

创建showInfo方法

.png)

创建get、set方法

.png)

最后生产出Person.class之后,我们可以使用JD-GUI打开:

.png)

动态加载生产出的class字节码并实例化该类

我们可以通过ClassWriter中的toByteArray() 方法可以获取生成的字节码数据。然后使用ClassLoaderdefineClass()方法进行反射实例化对象,并调用showInfo()方法。

0.png)

动态修改class字节码,进行AOP编程

通过加载上面生成的Person.class文件,在showInfo()方法里面添加一行打印当前时间。

通过继承ClassVisitor,重写visitMethod(),拦截showInfo()方法。

1.png)

然后让继承AdviceAdapter的类中的onMethodEnter()方法修改showInfo()方法。

2.png)

这样就可以实现修改class字节码的操作了。重新生成class文件。使用JD-GUI验证一下。不出意料,结果是我们所预期的。

3.png)

虽然例子简单,但是是进行AOP"无损注入"的基础展示。著名的Spring框架也是利用这种技术实现AOP的。至此,对ASM框架的一些简单的使用就是这样了,其中会涉及到一些JVM操作的理解,可以查看我的另一篇文章:JVM指令

另外,可以到github仓库查看本次的demo工程:ASMTest

JVM指令

· 阅读需 4 分钟
凡是带const的表示将什么数据压操作数栈
  • iconst_2 将int型数据2压入到操作数栈;
  • aconst_null 将null值压入栈;
  • bipushsipush 表示将单字节或者短整形的常量值压入操作数栈;
带ldc的表示将什么类型数据从常量池中压入到操作数栈。
  • ldc_w 将int或者flat或者string类型的数据压入到操作数栈;
  • ldc2_w 将long或者double类型的数据压入到操作数栈;
凡是带load的指令表示将某类型的局部变量数据压入到操作数栈的栈顶。
  • iload 表示将int类型的局部变量压入到操作数栈的栈顶;
  • aload 以a开头的表示将引用类型的局部变量压入到操作数栈的栈顶;
  • iload_1 将局部变量数组里面下标为1的int类型的数据压入到操作数栈;
  • iaload 将int型数组的指定索引的值压入到操作数栈;
凡是带有store指令的表示将操作数栈顶的某类型的值存入指定的局部变量中。
  • istore 表示将栈顶int类型的数据存入到指定的局部变量中;
  • istore_3 表示将栈int类型的数据存入到局部变量数组的下标为3的元素中;
  • pop 将栈顶数据弹出;
  • pop2将栈顶的一个long或者double数据从栈顶弹出来;
  • dup 复制栈顶的数据并将复制的值也压入到栈顶;
  • dup2 复制栈顶一个long或者是double的数据并将复制的值也压入到栈顶;
  • swap 将栈最顶端的两个值互换;
  • iadd 将栈顶两个int型的数据相加然后将结果再次的压入到栈顶;
  • isub 将栈顶两个int型的数据相减然后将结果再次的压入到栈顶;
  • imul 将栈顶两个int型的数据相乘然后将结果再次的压入到栈顶;
  • idiv 将栈顶两个int型的数据相除然后将结果再次的压入到栈顶;
  • irem 将栈顶两个int型的数据取模运算然后将结果再次的压入到栈顶;
  • ineg 将栈顶的int数据取负将结果压入到栈顶;
  • iinc 将指定的int变量增加指定值(i++,i--,i+=2);
  • i2l 将栈顶int类型数据强制转换成long型将结果压入到栈顶;
  • lcmp 将栈顶两long型数据的大小进行比较,并将结果(1,0,-1)压入栈顶;
以if开头的指令都是跳转指令
  • tableswitchlookupswitch 表示用switch条件跳转;
  • ireturn 从当前方法返回int型数据;
  • getstatic 获取指定类的静态域,将将结果压入到栈顶;
  • putstatic 为指定的类的静态域赋值;
  • getfield 获取指定类的实例变量,将结果压入到栈顶;
  • putfield 为指定类的实例变量赋值;
  • invokevirtual 调用实例方法;
  • invokespacial 调用超类构造方法,实例初始化方法,私有方法;
  • invokestatic 调用静态方法;
  • invokeinterface 调用接口方法;
  • new 创建一个对象,并将其引用压入到栈顶;
  • newarray 创建一个原始类型的数组,并将其引用压入到栈顶;
  • arraylength 获得一个数组的长度,将将结果压入到栈顶;
  • athrow 将栈顶的异常抛出;
  • checkcast 检验类型转换,转换未通过,将抛出ClassCastException;
  • instanceof 检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶
  • monitorenter 获得对象的锁,用于同步方法或同步块
  • monitorexit 释放对象的锁,用于同步方法或同步块
  • ifnull 为null时跳转
  • ifnonnull 不为null时跳转