应用消息推送解决方案 MiPush

Android 中实现消息推送的主流方案有下面几种。

方案 原理 优点 缺点
Cloud to Device Messaging,云端推送,是 Android 系统级别的消息推送服务(Google 出品) Push 简单的、轻量级的机制,允许服务器可以通知移动应用程序直接与服务器进行通信,以便于从服务器获取应用程序更新和用户数据 依赖于 Google 官方提供的 C2DM 服务器,需要用户手机安装 Google 服务
轮询 基于 Pull 方式 实时性好 成本大,需要自己实现与服务器之间的通信 ; 到达率不确定,考虑轮询的频率:太低可能导致消息的延迟;太高,更费资源
SMS 信令推送 Push 完全的实时操作 成本高
第三方平台 Push 小米推送、华为推送 友盟推送、极光推送、云巴(基于 MQTT) 阿里云移动推送、腾讯信鸽推送、百度云推送 成本低,抵达率高 安全性低,服务会被杀

2018-08-10 mipush , android , ios , push , notification

每天学习一个命令:iotop 查看 Linux 下每个进程 IO 占用

iotop 是一个用来监控磁盘 I/O 的类似 top 的工具,Linux 下 IO 统计工具,比如 iostat, nmon 等只能统计到每个设备的读写情况,如果想要知道哪一个进程占用比较高的 IO 就要使用 iotop。 iotop 使用 Python 语言编写,要求 Python >= 2.5,Linux Kernel >= 2.6.20.

使用这个命令最主要的一个原因就是快速找到 IO 占用比较高的程序,以便进行排查。

安装

sudo apt install iotop
sudo yum install iotop

快捷键

  • 交互式快捷键,a 用来切换累积使用量和 IO 读写速率。
  • 左右箭头用来改变排序,默认按照 IO 排序,可以切换为读或者写排序等等。
  • r 改变排序顺序
  • o 只显示有 IO 输出的进程
  • q 退出

reference


2018-08-07 io , iotop , top , linux , htop

InfluxDB 使用

InfluxDB 数据库是用 Go 语言实现的一个开源分布式时序、事件和指标数据库。InfluxDB 提供类 SQL 语法。

需要注意的是 InfluxDB 单节点是免费的,但是集群版是要收费的。

安装

sudo apt install influxdb

数据库设计

正因为 InfluxDB 是一个时序数据库,在实际使用的时候有些概念需要提前知道。InfluxDB 数据库中的每一个数据都有一列 time 保存时间戳 (RFC3339 形式显示)。

time butterflies honeybees location scientist
2015-08-18T00:00:00Z 12 23 1 langstroth
2015-08-18T00:00:00Z 1 30 1 perpetua

以该数据说明,butterflieshoneybeesField keys 用来保存元数据,每一个 key 都对应这 value,下面的数字都是要存储的值。

locationscientistTags,用来存储元数据。location 有两种取值,scientist 也有两种取值。所以组合可以有四种 tag 集合。tag 是可选的。但是推荐给数据加上 tags。和 field 不同,tags 都是索引,这意味着在查询时可以更快。

measurement 可以理解成 tags, fields 和 time 列的容器。measurement 的名字是数据的描述,和关系型数据库中的表可以做对应。在 InfluxDB 中可以创建多个数据库,不同数据库中的数据文件是隔离存放的,存放在磁盘上的不同目录

每一个 measurement 可以属于不同的 retention policies(存储策略),一个 retention policy 描述了 InfluxDB 保存数据多久(DURATION)和多少副本被保存到 cluster 中(REPLICATION)。(Replication factors 不对单节点开放)

在 InfluxDB 中 series 是一组数据的集合,这些数据共享一个 retention policymeasurementtag set。理解 series 的概念才能设计好数据库的 schema

最后 point 是在相同 series 中相同 timestamp 的数据。

InfluxDB Shell

如果需要使用 shell 需要安装

sudo apt install influxdb-client

然后在终端输入

influx

进入,默认的端口是 8086。输入 help 可以查看命令列表。

influx 命令参数

influx 启动命令常用参数

influx -database 'name'  # 数据库
influx -host 'hostname'  # 默认是 localhost
influx -password 'password' # 如果有密码
influx -port 'port'     # 端口

更多参数可以使用 influx --help 查看。

基本操作

查看数据库

show databases

创建和删除数据库

create database test
drop database test

切换使用数据库

use test

表基本操作,查询,插入

显示所有表

show measurements

删除表

drop measurement test_table

插入数据

insert [measurement],[tag],[field],[time]
insert cpu,host=serverA,regin=us value=0.64

解释:

  • 如果 measurement 表不存在,自动创建。
  • hostregion 是 tag
  • value 是 0.64
  • 如果字段存在则会报错

这里还要注意 measurement 和 tag 之间不能有空格,空格后面是真正要存储的值,所以如果要存两个 field,那么就需要

insert temperature,machine=unit42,tpye=assembly external=25,internal=37

注意 type=assembly 和 external 之间的空格。

查询数据

select "host","region","value" from "cpu"
select * from "temperature"

更多的查询可以参考这里

用户管理

查看用户

show users

创建用户

create user "username" with password "password"
create user "username" with password "password" with all privileges

删除用户

drop user "username"

Python Client

安装下面的客户端

pip install influxdb

其实这个 client 也只不过是在 influxdb 提供的 HTTP 接口外封装了一层 API,如果熟悉 HTTP 的接口,可以直接调用 influxdb 提供的接口

curl -sL -I localhost:8086/ping

比如:

创建数据库

import requests
posturl = 'http://localhost:8086/query'
data = {'q': 'create DATABASE mydb'}
response = requests.post(posturl, data=data)

类似于 curl 命令如下:

curl -GET http://localhost:8086/query --data-urlencode "q=CREATE DATABASE mydb"

写数据

import requests
posturl = 'http://localhost:8086/write?db=mydb'
data="cpu_load_short,host=server01,region=us-west value=0.69"
response = requests.post(posturl, data=data)

类似于 curl 命令如下:

curl -i -XPOST 'http://localhost:8086/write?db=mydb' --data-binary 'cpu_load_short,host=server01,region=us-west value=0.64 '

如果使用 python client:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import psutil
import time
from influxdb import InfluxDBClient

host = "localhost"
port = 8086
user = "root"
password = "root"
dbname = "test"


def read_info():
    cpu_time_info = psutil.cpu_percent(1, True)
    data_list = [
        {
            'measurement': 'cpu',
            'tags': {
                'cpu': 'i7'
            },
            'fields': {
                'cpu_info_user': cpu_time_info[0],
                'cpu_info_system': cpu_time_info[1],
                'cpu_info_idle': cpu_time_info[2],
                'cpu_info_interrupt': cpu_time_info[3],
                'cpu_info_dpc': cpu_time_info[4]
            }
        }
    ]
    return data_list


def parse_db(dbs):
    l = []
    for db in dbs:
        l.append(db['name'])
    return l


if __name__ == '__main__':
    client = InfluxDBClient(host, port, user, password)  # 初始化
    dbs = client.get_list_database()
    if dbname not in parse_db(dbs):
        client.create_database(dbname)
    for i in range(20):
        client.write_points(read_info())
        time.sleep(2)

大致是这样,但是这个 client 似乎还有 bug,在使用的时候就发现 HTTP 方法不支持,Method Not Allowed 这样的错误,需要自己手动改一改

reference


2018-08-02 influxdb , database , time-series-db

一周 CP 反编译记录

记录一下反编译一周 CP Android 3.26.0.451 版本,并拿到请求 sign 加密方法的过程。反编译的过程基本上可以划分为几个步骤

  • 将 Android apk 文件反编译得到混淆的 java 代码
  • 在混淆过的代码中找到发起请求的部分
  • 找到请求发起 sign 加密部分加密方式

反编译 Android apk

关于第一步 Android 反编译的教程和工具 可以参考之前的文章

查看混淆的代码

在反编译得到混淆过后的代码之后,这个时候不能盲目的去看,之前可以抓包看下应用内发出去的请求 path,找到关注的 path,比如在这里,找到一个请求的 path,然后沿着这个请求的 path 找到了发起请求的通用方法 prepareRequest(),看到这里就能清晰的看到请求发出去的时候带的几个通用参数 user_idsm_device_id ,然后签名部分 timestampTokensign,最关键的部分就是这里的 sign 的生成方式。

prepare request

然后到这里看到生成 sign 的方法,第一眼看过去就看到了希望 SHA-1 他使用的是这个哈希算法,这个算法也主要就是用于签名校验。

sign sha1

然后可以沿着这个思路去看各个参数的值,比如这里他用到了 RequestData.buildQueryString() 这个方法

request data

大致看一下这个代码大概能猜到是将请求参数的 key value 拼接起来连成字符串返回。这个时候大致思路已经清晰,所以我用 Java 大致实现了一下 sign 生成的代码。

Java 实现

import com.jutils.base.RequestData;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

public class WeekCPTest {
	private static String sha1(String str) throws NoSuchAlgorithmException, UnsupportedEncodingException {
		MessageDigest instance = MessageDigest.getInstance("SHA-1");
		byte[] bytes = str.getBytes("UTF-8");
		instance.update(bytes, 0, bytes.length);
		return O000000o(instance.digest());
	}

	private static String O000000o(byte[] bArr) {
		StringBuilder stringBuilder = new StringBuilder();
		for (byte b : bArr) {
			int i = (b >>> 4) & 15;
			int i2 = 0;
			while (true) {
				char c = (i < 0 || i > 9) ? (char) ((i - 10) + 97) : (char) (i + 48);
				stringBuilder.append(c);
				int i3 = b & 15;
				i = i2 + 1;
				if (i2 >= 1) {
					break;
				}
				i2 = i;
				i = i3;
			}
		}
		return stringBuilder.toString();
	}

	public static String makeSign(Map<String, Object> map, Map<String, Object> map2, String str) {
		Map treeMap = new TreeMap();
		if (!(map == null || map.isEmpty())) {
			treeMap.putAll(map);
		}
		if (!(map2 == null || map2.isEmpty())) {
			treeMap.putAll(map2);
		}
		treeMap.remove("token");
		treeMap.remove("sign");
		try {
			return sha1(RequestData.buildQueryString(treeMap, null, false) + str);
		} catch (Exception e) {
			e.printStackTrace();
			return "";
		}
	}



	public static void main(String[] args) {
		HashMap<String, Object> query = new HashMap<>();
		query.put("d", "a");
		query.put("user_id", "5624198");
		query.put("timestamp", "1533102328");
		query.put("sm_device_id", "201807021511598922958a5fbc8a5de09cb9019d34a48b01c51f66d9435c31");
//		query.put("Token", "1576511275532288_5624198_1559013109_58f16d8bd82677acc30d87542f5504b0");

		query.put("start", "80");
		query.put("last_object_id", "1742");
		query.put("keyword", "");
		query.put("num", "20");
		String s = makeSign(query, new HashMap<>(), "025d25f5a69eb2818b6811ff6edb51b4");
		System.out.println(s);
	}
}

然后使用抓包工具 抓出一个请求,将参数凭借起来使用这个 Test 跑一下,发现是一致的。这个时候再使用 Python 实现一下。

def __sha1(self, str):
    m = hashlib.sha1()
    m.update(str)
    return m.hexdigest()

def __sign(self, params, secret_key, data={}):
    """
    通过请求参数和 secret_key 生成 sign
    :param params: 请求参数
    :param secret_key: 加密 key 和 用户绑定,在登录或者刷新 token 中获取
    :return:
    """
    to_sign = ""
    merged_dict = params.copy()
    merged_dict.update(data)
    if "sign" in merged_dict.keys():
        merged_dict.pop("sign")
    if "token" in merged_dict.keys():
        merged_dict.pop("token")
    for key in sorted(merged_dict.iterkeys()):
        value = merged_dict.get(key)
        logger.info("params %s %s" % (key, value))
        pair = str(key) + "=" + str(merged_dict.get(key))
        if to_sign == '':
            to_sign = to_sign + pair
        else:
            to_sign = to_sign + '&' + pair
    return self.__sha1(to_sign + secret_key)

然后这个时候就发现其实破解一周 CP 的难点不在 sign 而在拿到 secret_key,这个 secret_key 只有在登录和刷新 token 的接口中才会返回。


2018-08-01 android , decompiler , crack , sign

修正关于 HTTP Header 的错误认识

HTTP 请求的 Header 是不区分大小写的!,一直以为 HTTP 请求的请求头是有区分大小的,知道今天调试发现 Spring 将 header 全部处理成小写,然后有人提了 Bug 58464 然后看到 Stackoverflow 上面有人回答。

HTTP/1.1 和 HTTP/2 都是 case-insensitivt 都是不区分大小写的。


2018-07-30 http , header , web , java-web , spring

解决 failed to create bus connection no such file or directory 错误

今天在修改 hostname 使用 sudo hostnamectl set-hostname ds 命令时遇到问题:

Failed to create bus connection: No such file or directory

查了一通之后发现缺少 dbus

sudo apt-get install dbus

安装 dbus 然后再修改即可,使用 hostnamectl 方式来修改 hostname 不需要重启,直接推出登录,然后就可以实现了。

D-Bus 是一种高级的进程间通信机制,它由 freedesktop.org 项目提供,使用 GPL 许可证发行。D-Bus 最主要的用途是在 Linux 桌面环境为进程提供通信,同时能将 Linux 桌面环境和 Linux 内核事件作为消息传递到进程。D-Bus 的主要概率为总线,注册后的进程可通过总线接收或传递消息,进程也可注册后等待内核事件响应,例如等待网络状态的转变或者计算机发出关机指令。目前,D-Bus 已被大多数 Linux 发行版所采用,开发者可使用 D-Bus 实现各种复杂的进程间通信任务。


2018-07-29 linux , problem , dbus , hostnamectl

Vim 的颜色主题

Retro groove color scheme for Vim


2018-07-27 vim , color-scheme , color , scheme

Linux Mint 下禁用 Alt 拖拽窗口

问题的出现,Linux Mint 使用了很长时间了,一直也没有啥大的问题,只是最近自定义一些快捷键,Alt + Shift ,发现所有的 Alt 相关的操作,只要按住 Alt 键,然后鼠标在任何窗口中就变成了小手,拖拽会直接拖动窗口。

解决方案一

在 System Settings 中选择 Windows, 然后在 Behavior 下面有 Special key to move and resize windows 选择 Disabled 即可。

解决方案二

安装 dconf

sudo apt install dconf-tools

然后在 org -> cinnamon > desktop > wm > preferences 下面的 mouse-button-modifier 中修改 <Alt> 变为 <Super> 或者 <Ctrl>

reference


2018-07-25 linux , mint , cinnamon , shortcut

威联通折腾篇八:重启服务

家里遇到一次断电,然后 NAS 就这样异常关机了,重启之后提示磁盘有些碎片需要整理,整理的时候 Qnap 会停止 NAS 上所有的服务,包括 Container Station 中的内容,而 Qnap 说了会在检查完磁盘之后重新启动的,然而并没有,所以只能手动来重启这些服务。

幸亏 Qnap 的绝大部分服务都是用启动脚本来启动的,执行下面的命令可以把 NAS 当前运行的所有服务重启。

/etc/init.d/services.sh restart

当然如果要重启单独的比如说 Container Station 也可以使用

/etc/init.d/container-station.sh restart

重启 samba

/etc/init.d/smb.sh restart

同样的方式,在 /etc/init.d/ 目录下有很多的启动脚本,对应着名字找到要重启的内容即可。

解决特定问题

当遇到打开应用出现

This site can’t be reached”(无法访问此站点)或“ refused to connect”(《服务器》 拒绝连接)

基本上重启一下对应的服务即可。


2018-07-21 qnap , qnap-tutorial , services

Java 查漏补缺之泛型

简而言之,泛型使类型(类和接口)在定义类,接口和方法时成为参数。类型参数提供了一种简便的方法,使得不同的输入类型可以使用相同的代码。

使用泛型的代码比非泛型代码有如下好处:

  • 在编译时进行更强大的类型检查
  • 消除了强制类型装换
  • 可以让编程人员更加容易实现通用算法

定义

通常情况下,泛型类 (generic class) 可以如下定义

class name<T1, T2, ..., Tn> { /* ... */ }

尖括号中的类型通常称为类型参数( type parameters 或者称为类型变量 type variables),一旦类定义了类型参数T ,该变量就能在类中使用。

Type Parameter 命名约定

按照惯例, type parameters 的名字都是单一的大写字母,通常和普通的命令规范 区别开来。

经常被使用的泛型变量有:

  • E for Element 被 Java Collections 框架广泛使用
  • K for Key , K V 经常被 Java Collections 框架中的 Map 使用
  • N for Number
  • T for Type
  • V for Value
  • S,U,V etc - 2nd, 3rd, 4th types

调用和实例化泛型类型

泛型的调用其实非常简单,只需要将类型参数替换为具体的类型即可,比如

List<String> list;

Type Parameter 和 Type Argument 术语:大部分情况下这两个术语是可以互换的,但他们的使用场景是不一样的。因此 Foo<T> 中的 T 是 type Parameter,而 Foo 中的 String 是 type argument.

泛型方法

对于静态泛型方法(static generic method), type parameter 定于的区域需要出现在返回值的前面

public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {...}

调用该方法的完整的方式

boolean same = Util.<Integer, String>compare(p1, p2);
// or
boolean same = Util.compare(p1, p2);

有界类型参数

总有一种情况,编程人员想要限制泛型的类型,比如一个操作数字的类或者方法,可能希望泛型只接受 Number 或者其子类的实例。

定义上界,比如<E extends Comparable> 在这个例子中,表示定义的泛型需要实现 Comparable 接口,这里的 extends 只是通用表示 extends (Class) 或者 implements (interfaces)

如果要定义多个界

<T extends B1 & B2 & B3>

在定义多个届时,需要将 Class 类型放到 interface 之前,比如说上面的例子中假如有 Class B2, interface B1 & B3 ,那么 B2 必须是第一个。

通配符 #{wildcards}

在代码中也经常能看到 ? 问号,通常叫做通配符(wildcard),表示是类型未知。通配符可以用在非常多的场景,作为参数,field,或者本地变量,有时候也作为返回值(当然不推荐这么做)。

Upper Bounded Wildcards

public static void process(List<? extends Foo> list) { /* ... */ }

upper bounded wildcard,List<? extends Foo> 其中 Foo 是类型,表示 Foo 和任何子类。

Unbounded Wildcards

无界通配符类型(upper bounded wildcard),通常表示的单纯的使用 ,比如 List<?>,表示的是一个不知道类型的 list。有两种常用的使用场景

  • 当写方法只需要 Object 类中的方法时
  • 在泛型类中定义的方法不依赖于 type parameter 时,比如 List.size() 或者 List.clear ,并不依赖于定义的泛型类型

举一个官方文档中的例子,假设有如下的方法

public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}

本意上是想要实现打印一个任何类型的列表,但是并不能达到目的,他并不能打印 List<Integer>, List<String> 等,因为他们并不是 List<Object> 的子类型,如果要实现通用的 printList 方法需要使用 List<?>

public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}

这个时候 List<Integer> 才是 List<?> 的子类型。

Lower Bounded Wildcards

类型的上界使用 extends,相同的,如果要表示类型的下界,则使用 super,比如 <? super A> 。需要注意的是,对于通配符 ? 可以单独指定上界,也可以指定下界,但是两者不能同时指定。

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

代码可以在 List<Integer>, List<Number>, and List<Object> — anything that can hold Integer values. 上运行。

假设

class A { /* ... */ }
class B extends A { /* ... */ }

都知道 B 是 A 的子类型,所以可以

B b = new B();
A a = b;

所以对于泛型类型呢?

List<B> listB = new ArrayList<>();
List<A> listA = listB;        // 编译错误

List<Number>List<Integer> 都是 List<?> 的子类型,而 List<Number>List<Integer> 这两者并没有任何关系。

为了让两者有关系,就需要用多泛型的上界,这样

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number>  numList = intList;  // OK. List<? extends Integer> is a subtype of List<? extends Number>

下面有一张图来表示 List 类型的父子类关系

a hierarchy of several generic list class declarations

类型擦除

泛型被引入到 Java 语言中,以便在编译时提供更严格的类型检查并支持泛型编程。为了实现泛型,Java 编译器使用类型擦除:

  • 如果类型参数是无界的,则将泛型类型中的所有类型参数替换为其边界或对象。因此,生成的字节码仅包含普通的类,接口和方法。
  • 插入类型转换,以保证类型安全
  • 生成桥接方法以保留扩展泛型类型中的多态性

类型擦除确保不为参数化类型创建新类;因此,泛型不会产生运行时开销。

在类型擦除过程中,Java 编译器会擦除所有的 type parameters,如果是有界的类型参数则替换成第一个类型,如果是无界的类型参数则替换为 Object。

Java 编译器还会擦除泛型方法参数中的类型参数。比如静态方法:

// Counts the number of occurrences of elem in anArray.
//
public static <T> int count(T[] anArray, T elem) {
    int cnt = 0;
    for (T e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

因为 T 是无界的,则会把 T 出现的地方全部替换为 Object。和泛型类相同,有界的方法中的类型参数也会替换为第一个类型参数。

泛型的限制

不能使用原始类型来实例化泛型

比如

Pair<int, char> p = new Pair<>(8, 'a');  // compile-time error

会有编译错误。Java 会自动装箱。

无法创建类型参数的实例

比如

public static <E> void append(List<E> list) {
    E elem = new E();  // compile-time error
    list.add(elem);
}

作为解决方案,可以使用反射来创建对象实例

public static <E> void append(List<E> list, Class<E> cls) throws Exception {
    E elem = cls.newInstance();   // OK
    list.add(elem);
}

如下调用

List<String> ls = new ArrayList<>();
append(ls, String.class);

无法申明类型参数为静态

如下是不对的

public class MobileDevice<T> {
    private static T os;

    // ...
}

不能对 parameterized types 进行类型装换和使用 instanceof

因为 Java 编译器会在编译时擦除类型,所以无法验证参数类型在运行时的类型。下面是错误的例子:

public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {  // compile-time error
        // ...
    }
}

而对于有界的参数化类型,可以使用

public static void rtti(List<?> list) {
    if (list instanceof ArrayList<?>) {  // OK; instanceof requires a reifiable type
        // ...
    }
}

无法创建参数化类型的数组

不能

List<Integer>[] arrayOfLists = new List<Integer>[2];  // compile-time error

无法创建,捕获或抛出参数化类型的对象

泛型类无法直接或者间接继承 Throwable 。

// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ }    // compile-time error

// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // compile-time error

但是可以在 throw 语句后使用

class Parser<T extends Exception> {
    public void parse(File file) throws T {     // OK
        // ...
    }
}

不能有重载的方法使用相同的泛型

比如

public class Example {
    public void print(Set<String> strSet) { }
    public void print(Set<Integer> intSet) { }
}

reference


2018-07-18 java , generics , programming , linux

电子书

最近文章