私信的设计

  1. 发件人和消息的关系 (一对多)
  2. 消息和收件人的关系 (多对多)
pre_messages

+------------+------------------+------+-----+---------+----------------+
| Field      | Type             | Null | Key | Default | Extra          |
+------------+------------------+------+-----+---------+----------------+
| id         | int(11) unsigned | NO   | PRI | NULL    | auto_increment |
| sender_id  | int(11) unsigned | NO   |     | 0       |                |
| message    | varchar(255)     | NO   |     |         |                |
| type       | varchar(50)      | NO   |     |         |                |
| expires_at | datetime         | YES  |     | NULL    |                |
| created_at | datetime         | NO   |     | NULL    |                |
| updated_at | datetime         | NO   |     | NULL    |                |
| deleted_at | datetime         | YES  |     | NULL    |                |
+------------+------------------+------+-----+---------+----------------+

pre_message_recipients

+--------------+---------------------+------+-----+---------+----------------+
| Field        | Type                | Null | Key | Default | Extra          |
+--------------+---------------------+------+-----+---------+----------------+
| id           | int(11) unsigned    | NO   | PRI | NULL    | auto_increment |
| recipient_id | int(11) unsigned    | NO   |     | 0       |                |
| message_id   | int(11) unsigned    | NO   |     | 0       |                |
| status       | tinyint(1) unsigned | NO   |     | 0       |                |
| created_at   | datetime            | NO   |     | NULL    |                |
| updated_at   | datetime            | NO   |     | NULL    |                |
| deleted_at   | datetime            | YES  |     | NULL    |                |
+--------------+---------------------+------+-----+---------+----------------+

php中通配符的实现

该方法摘自Laravel5的Event getWildcardListeners

作用如下:

`item.*` -> `item.new`
         -> `item.edit`
class Str {
    public static function is($pattern, $value)
    {
        if ($pattern == $value) return true;

        $pattern = preg_quote($pattern, '#');
        $pattern = str_replace('\*', '.*', $pattern) . '\z';
        return (bool) preg_match('#^' . $pattern . '#', $value);
    }
}

该功能使用正则实现

  • * 替换成 .* 匹配所有字符
  • \z 这个转义序列从来没用过,查了手册是说不受修正符的影响,其实这边应该和$是一样的

PHP: Escape sequences - Manual

撸 php 源码

快速定位某个 php 函数在源码中的位置

# 所有php函数
ag 'PHP_FUNCTION\(\w+\)'
# 指定php函数
ag 'PHP_FUNCTION\(array_flip\)'

自定义个 shell 函数,方便搜索

# search php funtion in c source
function phpsrc()
{
    if [ $# -eq 0 ]; then
        echo 'usage: phpsrc <function>[ <dirname>]'
        return
    fi
    dirname="$2"
    if [ -z "$dirname" ]; then
        dirname=$(pwd)
    fi
    ag 'PHP_FUNCTION\('"$1"'\)' "$dirname"
}

快速定位 php 中的语言结构

# Zend/zend_compile.c
ag 'void zend_do_' Zend/zend_compile.c

使用命令行获取app的图标

先在桌面上建一个目标文件夹 mkdir -p ~/Desktop/appIcons/48/

#!/bin/zsh

APPNAME=$1

if [[ -z "$APPNAME" ]]; then
    echo '请先指定一个appname'
    exit 0
fi

# 获取图标名称
ICON=$(/usr/libexec/plistbuddy -c Print:CFBundleIconFile: "/Applications/${APPNAME}.app/Contents/Info.plist")
if [[ -z "$ICON" ]]; then
    echo '找不到icon文件'
    exit 0
fi

if [[ "$ICON" == *.icns ]] ICON=${ICON%.*}
ICONPATH="/Applications/${APPNAME}.app/Contents/Resources/${ICON}.icns"
# 转换格式
sips -s format png "${ICONPATH}" --out ~/Desktop/appIcons/48/"${APPNAME}".png -Z 48

为了方便使用,可以直接闹个别名 alias icon='~/Documents/geticon.sh'

访问可视化(续)

之前写过一片文章:访问可视化,但是局限性很大,于是利用周末的时间,改造了一番。

演示地址:http://tool.lu/visitor

打点的改造

老版本:在php里面进行打点

新版本:使用js加载空伪1px gif图片

数据分析和存储的改造

老版本:请求过来的时候,php分析,存进mysql

新版本:请求的时候,js分析,加载1px gif,nginx记录日志,python分析,存进数据库

数据的访问实时dump

老版本:监听mysql的binlog,publish到redis

新版本:分析nginx,存进数据库的同时,publish到redis

IP归属地 和 经纬度的查询

老版本:纯真数据库 + 百度地图地址反解

新版本:ip17mon + 腾讯地图行政区划latlag

预览

QQ20150209-1.png

1. js的打点日志

具体可以参考:网站统计中的数据收集原理及实现

不过我使用的方法由文章简化而来。

2. nginx的日志记录

server {
    listen 80;
    listen 443 ssl;
    server_name analytics.tool.lu;

    ssl_certificate vhosts/tool.lu.chained.crt;
    ssl_certificate_key vhosts/tool.lu.key;

    root /data/html/analytics.tool.lu/public;

    access_log off;
    error_log off;

    location / {
        index index.html;
        try_files $uri $uri/ =404;
    }

    location /__utm.gif {
        expires -1;
        if_modified_since off;
        # add_header Last-Modified "";
        empty_gif;
        access_log /data/log/nginx/analytics.tool.lu.access.log;
    }
}

3. nginx日志的分析

这里有几点要注意的:

  1. nginx日志我是使用logrotate切分的,要达到实时读取nginx日志的目的,需要保证读取的文件是最新切分出来的文件,而且在切分文件的时候要保证之前的文件已经处理完毕。
  2. 记录日志文件处理的offset,又是由于logrotate切分nginx日志的原因,如何保证记录的offset是唯一的,且能对应上日志文件,这里我使用了log文件创建的时间作为标识(在linux下是不储存文件的创建时间的,需要自己记录实现)。
  3. nginx log中时间格式的解析,基本上就是抓瞎了(不是所有版本的python strptime都支持timezone的)
  4. 如何通过腾讯地图 api 返回的数据建立索引,方便快速查找到 地址对应的经纬度

为了安全性考虑,这边就不贴 nginx log 解析的完整代码了。

ua-parser返回version的拼接

    def version(self, vs):
        v = ''
        if not vs['major']:
            return v
        v += vs['major']
        if not vs['minor']:
            return v
        v += '.' + vs['minor']
        if not vs['patch']:
            return v
        v += '.' + vs['patch']
        if not 'patch_minor' in vs or not vs['patch_minor']:
            return v
        v += '.' + vs['patch_minor']
        return v

腾讯地图行政区划数据索引的建立

        # IP17MON返回的数据带有,省市,可以优先市级别的索引,再省级别的索引
    def index(self):
        for province in self.data['result'][0]:
            self.indexed_provinces[province['name']] = province['location']
        for city in self.data['result'][1]:
            self.indexed_cities[city['name'] if 'name' in city else city['fullname']] = city['location']

python

list 的 shift

l = [1, 2]
first = l.pop(0)

parse nginx log的正则

self.pattern = re.compile(r'(?P<ipaddress>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) - - \[(?P<dateandtime>\d{2}\/[a-z]{3}\/\d{4}:\d{2}:\d{2}:\d{2}) [+-]\d{4}\] ((\"(GET|POST) )(?P<url>.+)(http\/1\.1")) (?P<statuscode>\d{3}) (?P<bytessent>\d+) (["](?P<referrer>(\-)|(.+))["]) (["](?P<useragent>.+)["])', re.I)

nginx log时间的解析

datetime.datetime.strptime(fields['dateandtime'], '%d/%b/%Y:%X')

PS: 好久不写python已有些生疏

数字点阵的动画实现

预览

未标题-1.gif

原理

数字0的js数组点阵,7x5个 span 元素拼出一个数字出来,同样的方法实现 1-9 的数字

动画效果使用 css3 的 keyframes 实现

[
    [0, 1, 1, 1, 0],
    [1, 1, 0, 1, 1],
    [1, 1, 0, 1, 1],
    [1, 1, 0, 1, 1],
    [1, 1, 0, 1, 1],
    [1, 1, 0, 1, 1],
    [0, 1, 1, 1, 0]
]

QQ20150119-1.png

源码

numb.zip

java Integer中highestOneBit和bitCount的实现原理

highestOneBit

图中的黑色位都可以不关注,然后基本原理就是这样的 (以8位作为例子)

QQ20150110-1.png

bitCount

0x55555555
    01010101010101010101010101010101
0x33333333
    00110011001100110011001100110011
0xf0f0f0f
    00001111000011110000111100001111
0x3f
    00000000000000000000000000111111

QQ20150110-2.png

chrome插件开发杂记

刷新当前 tab 页的3中方式

// 1. 通过在页面中执行js代码
chrome.tabs.getSelected(null, function(tab) {
    var code = 'window.location.reload();';
    chrome.tabs.executeScript(tab.id, {code: code});
});
// 2. 通过chrome的api更新tab的url
chrome.tabs.update(null, {url: url});
// 3. 通过chrome的api刷新
chrome.tabs.reload();

popup 和 background 之间的通讯

以修改 cookie 为例子:

popup:                            -> 修改cookie                          -> 更新UI
background: -> 监听cookie的修改事件               -> 触发监听,与 popup 通讯
// 在background中
chrome.cookies.onChanged.addListener(function (changeInfo) {
    var cookie = changeInfo.cookie;
    if (cookie.domain == 'tool.lu' && cookie.name == 'X-Backend-Server') {
        chrome.runtime.sendMessage({event: "cookie", data: cookie.value}, function (response) {
            // ...
        });
    }
});
// 在popup中
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
    // do things
});

popup弹出时候,更新popup的UI

// 在popup中绑定 window.onload 事件
window.onload = init;

background中获取当前加载页面的网页头信息

chrome.webRequest.onHeadersReceived.addListener(function (o) {
    // 判断是否是主页面, 因为页面中可能包含 iframe, image, css, js 等其他资源的加载
    if (o.frameId != 0 || o.type != "main_frame") {
        return;
    }
    for (var i in o.responseHeaders) {
        var header = o.responseHeaders[i];
        console.log(header.name, header.value);
    }
}, {
    "urls":["http://tool.lu/*"]
}, ["responseHeaders"]);

background在页面加载完成之后触发事件

chrome.webNavigation.onCompleted.addListener(function(o) {
    if (o.frameId == 0) {
        // do something...
    }
}, {
    url: [
        {hostContains: 'tool.lu'}
    ]
});

java杂记

效率

先来个 Apache Commons 里面没有的

xiaozi/java-helpers

/*
[
    {id: 1, name: "name1", gender: 1},
    {id: 2, name: "name2", gender: 1},
    {id: 3, name: "name1", gender: 2},
]
*/

/*
[1, 2]
*/
CollectionUtils.pluck(persons, "id", new ArrayList<Integer>())

/*
{
    1: [
        {id: 1, name: "name1", gender: 1},
        {id: 2, name: "name2", gender: 1},
    ],
    2: [
        {id: 3, name: "name1", gender: 2},
    ]
}
*/
CollectionUtils.groupBy(persons, "gender", Integer.class)

/*
去重
*/
CollectionUtils.unique(integers, new ArrayList<Integer>())

Apache Commons 里面的

StringUtils

StringUtils.html

StringUtils.isEmpty(); // null 和 空字符串
StringUtils.isBlank(); // null 和 空字符串 和 全空白字符串
StringUtils.trim(); // 截取掉字符串两侧的空格
StringUtils.strip(); // 截取掉字符串两侧的空白字符串
StringUtils.equalsIgnoreCase(); // 忽略大小写的比较
StringUtils.indexOf(); // 字符/字符串在指定字符串中第一次出现的位置
StringUtils.indexOfIgnoreCase();
StringUtils.lastIndexOf();
StringUtils.contains(); // 是否包含子符串
StringUtils.left();
StringUtils.right();
StringUtils.mid(); // 截取字符串
StringUtils.split(); // 分割字符串
StringUtils.join(); // join数组为字符串
StringUtils.repeat(); // 重复输出字符串
StringUtils.leftPad();
StringUtils.rightPad(); // 填充字符串
StringUtils.lowerCase();
StringUtils.upperCase();
StringUtils.capitalize(); // 大写首字母
StringUtils.reverse(); // 倒转字符串

额,大多数提供的都跟PHP里面一样

CollectionUtils

CollectionUtils.html

CollectionUtils.union(); // 并集
CollectionUtils.intersection(); // 交集
CollectionUtils.containsAny(); // 是否存在交集
CollectionUtils.disjunction(); // 交集的补集
CollectionUtils.subtract(); // 差集
CollectionUtils.filter(); // 过滤

空List的返回

// new ArrayList<>();
Collections.emptyList();

片段

检测端口可用,(绑定,并关掉),来自 Canal

public static boolean isAvailablePort(int port) {
    ServerSocket ss = null;
    try {
        ss = new ServerSocket(port);
        ss.bind(null);
        return true;
    } catch (IOException e) {
        return false;
    } finally {
        if (ss != null) {
            try {
                ss.close();
            } catch (IOException e) {
            }
        }
    }
}

利用字符串标记,优雅解决内外配置文件的加载

// ...
private static final String CLASSPATH_URL_PREFIX = "classpath:";
// ...
    String conf = System.getProperty("canal.conf", "classpath:canal.properties");
    Properties properties = new Properties();
    if (conf.startsWith(CLASSPATH_URL_PREFIX)) {
        conf = StringUtils.substringAfter(conf, CLASSPATH_URL_PREFIX);
        properties.load(CanalLauncher.class.getClassLoader().getResourceAsStream(conf));
    } else {
        properties.load(new FileInputStream(conf));
    }

java动态加载jar包

目录结构如下

.
├── main
│   ├── java
│   │   └── lu
│   │       └── tool
│   │           └── jar
│   │               ├── InterfaceRunner.java
│   │               └── Loader.java
│   └── resources
└── test
    ├── java
    └── resources

InterfaceRunner.java 为挂载 jar 中类的实现接口
Loader.java 为jar的加载器和执行器

所有第三方包的jar路径,通过 web 界面管理,然后存储在一个文件中,这里不实现 web 的管理。

InterfaceRunner.java:

package lu.tool.jar;

/**
 * Created by xiaozi on 11/29/14.
 */
public interface InterfaceRunner {

    public void fire();

}

Loader.java

package lu.tool.jar;

import java.io.*;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * Created by xiaozi on 11/29/14.
 */
public class Loader {

    public static void main(String[] args) {
        String configFile = System.getProperty("jar.conf");
        if (configFile == null || configFile.isEmpty()) {
            System.exit(1);
        }
        System.out.println(configFile);
        File file = new File(configFile);
        try {
            BufferedReader in = new BufferedReader(new FileReader(file));
            String s;
            while ((s = in.readLine()) != null) {
                if (s.isEmpty()) continue;
                System.out.println(s);
                URL url = new URL(s);
                URLClassLoader myClassLoader = new URLClassLoader(new URL[] {url}, Thread.currentThread().getContextClassLoader());
                Class<?> myClass = myClassLoader.loadClass("lu.tool.jar.Runner");
                InterfaceRunner action = (InterfaceRunner) myClass.newInstance();
                // 达到指定条件的时候触发,这里仅是个演示
                // 在没有优先级的执行条件下应该使用子进程的方式,防止其中的一个crash掉
                action.fire();
                myClassLoader.close();
                System.out.println("done");
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

}

jar.conf 存放 jar 的本地的绝对路径

file:///Users/xiaozi/Documents/testrunner/out/artifacts/testrunner_jar/testrunner.jar

被挂载jar包中的类实现

package lu.tool.jar;

/**
 * Created by xiaozi on 11/29/14.
 */
public class Runner implements InterfaceRunner {

    @Override
    public void fire() {
        System.out.println("Hello, I'm in another jar!");
    }

}

引用:java动态加载jar包,并运行其中的类和方法

获取Kohana的路由信息

为了debug方便,便想着把Konaha的生效路由都输出到一个页面,说闹就闹。

$routes = Route::all();
// 呵呵,尼玛返回值的属性全是 protected 的,还没有get方法,这是作死呢?
// 于是第一个想到是使用反射来获取值
function getProperty($obj, $prop) {
    $refc = new ReflectionClass($obj);
    $refp = $refc->getProperty($prop);
    $refp->setAccessible(true);
    return $refp->getValue($obj);
}

$routeContent = array();
foreach ($routes as $name => $aRoute) {
    $uri = getProperty($aRoute, '_uri');
    $defaults = getProperty($aRoute, '_defaults');
    $action = $defaults['controller'] . '::' . $defaults['action'];
    $routeContent[] = compact('name', 'uri', 'action');
}

var_dump($routeContent);

其实,刚开始的是我没有发现有setAccessible这个函数,这也是php5.3加上去的,想了个变态的方法:

ob_start();
var_dump($routes); // 这边是比较蛋疼的,Kohana的route居然是循环引用自己的,var_dump/var_export都是会到达一定的层级才会停下来
$contents = ob_get_clean();
# ... 然后这边用正则,呵呵

后来在群里问了下 @奇遇 大神,于是黑魔法就出现了,而且这个黑魔法的效率还是 php 的反射类的2倍,但是黑魔法也会产生一些问题,具体可看 官方的文档

function getProtected($obj, $prop) {
    $arr = (array) $obj;
    $key = "\0*\0" . $prop;
    return isset($arr[$key]) ? $arr[$key] : null;
}

function getPrivate($obj, $prop) {
    $arr = (array) $obj;
    $key = "\0" . get_class($obj) . "\0" . $prop;
    return isset($arr[$key]) ? $arr[$key] : null;
}

下面是演示:

PS: Kohana是个奇葩的框架