记一次 JNI 导致 Java fd 泄露的排查过程

前几天接到反馈,线上某机器上的服务在进行后端调用的时候失败了。查看日志是端口分配失败。通过 netstat -nulp  看到大量端口占用,用户段端口49152 到 65535 全部被占满。于是通过 awk sort 和 uniq 统计出每个进程的端口占用情况,发现某些 Java 服务占用了 2w+ 端口,于是对该服务展开分析。

首先考虑的是应用有大量 Socket 对象没有关闭释放,于是将堆 dump 出来,使用 VisualVM 加载分析。由于泄露的是 UDP 端口,于是考虑查找 Java 中 UDP socket 对应的对象 DatagramSocket 。可是一顿操作之后发现堆中并不存在该类对象,倒是找到了几个 DatagramSocketAdaptor  对象。查看了框架发现使用了 Netty 做 NIO,因此实际使用的是 NioDatagramChannel 和 DatagramSocketAdaptor 对象。经过对比,这些对象都是应用启动时创建 Netty channel 时创建的,数量完全正确,所以排除这方面的问题。

接着考虑 Netty 版本和 JVM 版本的问题。通过搜索发现 Netty 和 JVM 都出现过 fd 没有正确关闭的问题,于是进行升级。升级后发现问题依然存在,排除相关组件的问题。

在排除了几个可能之后,还是没有什么头绪,于是准备从 FD 本身入手。在 Java 中,所有的系统 FD 对应的都是一个 Java 中的 FileDescriptor  类,于是使用各种工具对该类的创建和删除进行了监控。结果通过排查发现,除了第一步中查到的 Netty Channel 持有的 fd 实例外,没有任何的有关网络 Socket 的 fd 实例。这下就很尴尬了,JVM 中的 fd 没有变化,按理说应该不会有 Socket 创建才对。仔细思考了一下,想到了最后一种可能,那就是 native 方法。

在 Java 中,为了支持调用其它语言所编写的方法,提供了 Java Native Interface 即 JNI 作为入口。当我们需要调用一个闭源的 C++ 编写的类库的时候,我们就可以使用 JNI 来进行调用。同时,由于 JNI 实际执行的是第三方类库中的代码,因此这部分代码进行的 fd 操作都不会被 JVM 管理,自然也就不会出现在 Dump 文件中。

既然猜到了可能的问题,接下来就需要排查了。可是由于一些原因,该服务中存在很多的 JNI 类库都会进行网络调用,无法最终确定。于是想了这么一个办法来进行排查:

  1. 在机器上起一个程序不断的对 lsof -a -n -P -i udp -p pid  进行采样对比,遇到两次采样期间有新增的 fd 就打印出来
  2. 同时使用 tcpdump -i any udp -w fd_leak.pcap  进行全量抓包,记录机器上所有的 UDP 网络流量,来对比分析流量发出的进程

经过排查,终于抓到了对应的包,找到了对端的 IP 端口,定位到了对应的组件。于是对这个组件编写复现代码进行测试,最终将代码简化为:

package com.maoxian.test;

import com.maoxian.xxx.NativeAPI;

/**
 * 测试类
 */
public class Test {
    public static void main(String[] args) {
        Thread.sleep(10000);
        int i = 0;
        while (i < 10) {
            i++;
            System.out.println("Start loop " + j + " ...");
            new Thread() {

                @Override
                public void run() {
                    try {
                        System.out.println("Thread " +
                                Thread.currentThread().getId() +
                                " is running...");


                        NativeAPI.doSomething();
                    } finally {
                        System.out.println("Thread " +
                                Thread.currentThread().getId() +
                                " finished.");
                    }
                }
            }.start();
            System.out.println("Finish loop " + j + ".");
        }
        Thread.sleep(60000);
    }
}

这段代码的作用非常简单,就是起 10 个线程去调用这个组件,每个线程调用一次就退出。Sleep 的作用是给 lsof 列出 fd 预留时间。在执行这段代码后可以明显的看到 fd 在 Native 方法调用时分配,但是在线程退出后没有释放。咨询了相关同事后得知,由于在 C++ 中,一般不会使用 Java 中的这种线程池模型,而是使用固定线程模型。当一个线程退出的时候通常意味着整个程序的退出。所以这个组件在制作的时候只考虑了线程安全的问题对每个线程独立分配了资源,但是没有考虑线程终止时候的资源释放

在定位到 fd 泄露与线程创建相关后,对相应的业务框架代码进行了分析。发现框架中创建线程池使用的是:

this.tasks = Executors.newCachedThreadPool(new ThreadFactory() {
    final AtomicInteger TID = new AtomicInteger(0);
    final String GROUP_NAME = "TPWS-" + GID.getAndIncrement();
    @Override
    public Thread newThread(Runnable r) {
        return new Thread(r, GROUP_NAME + TID.getAndIncrement());
    }
});

查看 Executors 的创建线程池的方法:

/**
 * Creates a thread pool that creates new threads as needed, but
 * will reuse previously constructed threads when they are
 * available, and uses the provided
 * ThreadFactory to create new threads when needed.
 * @param threadFactory the factory to use when creating new threads
 * @return the newly created thread pool
 * @throws NullPointerException if threadFactory is null
 */
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue(),
                                  threadFactory);
}

可以看出,这个方法最终创建了一个最小 0 个线程,最大 Integer.MAX_VALUE 个线程,每个线程 60s 超时时间。于是,当一个业务请求到来时,线程池创建一个新线程处理这个请求,在请求中调用了 Native 方法。当请求处理完后,线程归还线程池。60s 内如果没有新的请求进入,则该线程被线程池销毁,但是 Native 申请的 fd 没有释放。当新的请求到来时,又重新创建线程,重新分配 fd,以此类推,导致 fd 泄露。

这次排查的经理告诉我们,当 Java 调用 Native 方法的时候一定要格外小心。由于虚拟机无法对大多数 Native 方法占用的资源进行管理,因此编写质量差的 Native 类库会直接导致不可预知的奇怪问题。特别是由于 C++ 在多线程、多进程模型上与 Java 有诸多不同,很多库在设计时就没有考虑全面,直接在 Java 内调用的时候可能就会出现在 C++ 中不会遇到的问题。

MySQL 分区表的一些问题

最近在使用 MySQL 分区表的时候,研究了一下多列 Range 分区,也就是

PARTITION BY RANGE COLUMNS(`a`, `b`, `c`) (
    PARTITION p1 VALUES LESS THAN (0, 0, MAXVALUE),
    PARTITION p2 VALUES LESS THAN (10, 10, MAXVALUE),
    PARTITION p3 VALUES LESS THAN (20, 20, MAXVALUE)
)

在多列的情况下,MySQL 的分区策略和单列略有不同,这也是比较坑的地方,查遍所有文档都没人提到。。。

先说说单列 Range 分区。比如,如果这么写:

PARTITION BY RANGE(`a`) (
    PARTITION p1 VALUES LESS THAN (0),
    PARTITION p2 VALUES LESS THAN (10),
    PARTITION p3 VALUES LESS THAN (20)
)

那么,p1 中的数据是 a 值小于 0 的,注意,是小于,不包括 0 。然后,p2 中的数据是 a 值在 [0, 10) 之间的,注意右边是开区间,不包括 10 。同样的,p3 中的数据是 a 值在 [10, 20) 之间的,不包括 20 。

也就是说,如果有这么一条数据:

INSERT INTO test_table (`a`, `b`, `c`) VALUES (10,10,20);

由于 a=10,所以会落入 p3 分区。

再来看多列分区,使用第一个多列分区语句,执行 INSERT,会发现,数据插入到了 p2 分区,而不是想象中的 p3 分区。

这里么的原因,就涉及到 MySQL 内部的比较了。当使用单列分区时,MySQL 的比较方法是:

if a < 0  then p1
if a < 10 then p2
if a < 20 then p3

当采用多列分区的时候,比较方法就相应的变成了:

if (a,b,c) < (0 , 0 , MAXVALUE) then p1
if (a,b,c) < (10, 10, MAXVALUE) then p2
if (a,b,c) < (20, 20, MAXVALUE) then p3

那咱们再来看看直接执行这个比较会怎么样:

mysql> SELECT 10 < 10;
+---------+
| 10 < 10 |
+---------+
|       0 |
+---------+
1 row in set (0.01 sec)

mysql> SELECT 9 < 10;
+--------+
| 9 < 10 |
+--------+
|      1 |
+--------+
1 row in set (0.01 sec)

mysql> SELECT (10,10) < (10,10);
+-------------------+
| (10,10) < (10,10) |
+-------------------+
|                 0 |
+-------------------+
1 row in set (0.00 sec)

mysql> SELECT (10,9) < (10,10);
+------------------+
| (10,9) < (10,10) |
+------------------+
|                1 |
+------------------+
1 row in set (0.00 sec)

惊喜来了!(10,10) < (10,10) 毫不意外的被判定为 false ,但是 (10,9) < (10,10) 确是 true 的!

再来一些尝试:

mysql> SELECT (11,9) < (10,10);
+------------------+
| (11,9) < (10,10) |
+------------------+
|                0 |
+------------------+
1 row in set (0.00 sec)

mysql> SELECT (9,11) < (10,10);
+------------------+
| (9,11) < (10,10) |
+------------------+
|                1 |
+------------------+
1 row in set (0.01 sec)

mysql> SELECT (9,10) < (10,10);
+------------------+
| (9,10) < (10,10) |
+------------------+
|                1 |
+------------------+
1 row in set (0.01 sec)

惊呆了,(9,11) < (10,10) 居然也是 true !

来,实际测试一下:

CREATE TABLE `test_table` (
    `a` INT(20) NOT NULL,
    `b` INT(11) NOT NULL
) ENGINE=INNODB DEFAULT CHARSET=UTF8MB4
PARTITION BY RANGE COLUMNS(`a`, `b`) (
    PARTITION p1 VALUES LESS THAN (0, 0),
    PARTITION p2 VALUES LESS THAN (10, 10),
    PARTITION p3 VALUES LESS THAN (20, 20)
);

INSERT INTO `test_table` VALUES (10,10);
INSERT INTO `test_table` VALUES (10,9);
INSERT INTO `test_table` VALUES (9,11);

执行之后发现,第一条记录毫不意外的在 p3 ,但是第二条记录和第三条记录却都在 p2 !

那么这时候执行查询会发生什么呢?

mysql> SELECT * FROM `test_table`;
+----+----+
| a  | b  |
+----+----+
| 10 |  9 |
|  9 | 11 |
| 10 | 10 |
+----+----+
3 rows in set (0.00 sec)

mysql> EXPLAIN PARTITIONS SELECT * FROM `test_table` WHERE a=10;
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
| id   | select_type | table      | partitions | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
|    1 | SIMPLE      | test_table | p2,p3      | ALL  | NULL          | NULL | NULL    | NULL |    3 | Using where |
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

mysql> EXPLAIN PARTITIONS SELECT * FROM `test_table` WHERE b=10;
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
| id   | select_type | table      | partitions | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
|    1 | SIMPLE      | test_table | p1,p2,p3   | ALL  | NULL          | NULL | NULL    | NULL |    5 | Using where |
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

可以看到,当我们使用多列中的第一列做查询时,MySQL 会识别出 p1 分区一定没有数据,所以优化中直接去掉了这个分区,但是搜索了 p2 p3 两个分区。

这也是疑惑点之一,按照 MySQL 的规则,似乎 p2 也一定不会有数据,为啥要搜 p2

再来看下面一个查 b 的语句,会发现根本没有用分区,直接全分区搜索。。。看来 MySQL 也知道可能有一些 b 值并不是存在相应的分区中,需要全表扫描。

具体原因可能需要深入分析 MySQL 源码,这里就先说这么一个需要注意的现象,防止踩坑。。。

Apache HttpClient 使用代理服务器 Proxy 的一个小坑

今天在调试一个用 HttpClient 写的 Demo 的时候遇到了一个问题:

org.apache.http.impl.execchain.RetryExec execute
INFO: I/O exception (org.apache.http.conn.UnsupportedSchemeException) caught when processing request to {tls}->http://proxyserver:port->https://servername:443: http protocol is not supported

也就是在通过 HTTP Proxy 进行 HTTPS 连接的时候,HttpClient 报了一个不支持 HTTP 协议。查了一下发现问题在于我使用 HttpClient 的方法。

由于我在使用 HttpClient 的时候是手动创建的 Registry ,而在创建的时候没有注册 HTTP 协议,导致报了上面那个错误。之前错误的代码是:

        // Create a registry of custom connection socket factories for supported
        // protocol schemes.
        Registry socketFactoryRegistry = RegistryBuilder.create()
                .register("https", sslsf)
                .build();

        // Create a connection manager with custom configuration.
        PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);

需要修改为:

        // Create a registry of custom connection socket factories for supported
        // protocol schemes.
        Registry socketFactoryRegistry = RegistryBuilder.create()
                .register("https", sslsf)
                .register("http", PlainConnectionSocketFactory.INSTANCE)
                .build();

        // Create a connection manager with custom configuration.
        PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);

加的这句保证了这个ConnectionSocketFactory 也可以处理 HTTP 协议,从而避免这个问题。

MySQL 隐式转化整理

前几天在微博上看到一篇文章:价值百万的 MySQL 的隐式类型转换感觉写的很不错,再加上自己之前也对MySQL的隐式转化这边并不是很清楚,所以就顺势整理了一下。希望对大家有所帮助。

当我们对不同类型的值进行比较的时候,为了使得这些数值「可比较」(也可以称为类型的兼容性),MySQL会做一些隐式转化(Implicit type conversion)。比如下面的例子:

mysql> SELECT 1+'1';
        -> 2
mysql> SELECT CONCAT(2,' test');
        -> '2 test'

很明显,上面的SQL语句的执行过程中就出现了隐式转化。并且从结果们可以判断出,第一条SQL中,将字符串的“1”转换为数字1,而在第二条的SQL中,将数字2转换为字符串“2”。

MySQL也提供了CAST()函数。我们可以使用它明确的把数值转换为字符串。当使用CONCA()函数的时候,也可能会出现隐式转化,因为它希望的参数为字符串形式,但是如果我们传递的不是字符串呢:

mysql> SELECT 38.8, CAST(38.8 AS CHAR);
        -> 38.8, '38.8'
mysql> SELECT 38.8, CONCAT(38.8);
        -> 38.8, '38.8'

隐式转化规则

官方文档中关于隐式转化的规则是如下描述的:

If one or both arguments are NULL, the result of the comparison is NULL, except for the NULL-safe <=> equality comparison operator. For NULL <=> NULL, the result is true. No conversion is needed.

  • If both arguments in a comparison operation are strings, they are compared as strings.
  • If both arguments are integers, they are compared as integers.
  • Hexadecimal values are treated as binary strings if not compared to a number.
  • If one of the arguments is a TIMESTAMP or DATETIME column and the other argument is a constant, the constant is converted to a timestamp before the comparison is performed. This is done to be more ODBC-friendly. Note that this is not done for the arguments to IN()! To be safe, always use complete datetime, date, or time strings when doing comparisons. For example, to achieve best results when using BETWEEN with date or time values, use CAST() to explicitly convert the values to the desired data type.A single-row subquery from a table or tables is not considered a constant. For example, if a subquery returns an integer to be compared to a DATETIME value, the comparison is done as two integers. The integer is not converted to a temporal value. To compare the operands as DATETIME values, use CAST() to explicitly convert the subquery value to DATETIME.
  • If one of the arguments is a decimal value, comparison depends on the other argument. The arguments are compared as decimal values if the other argument is a decimal or integer value, or as floating-point values if the other argument is a floating-point value.
  • In all other cases, the arguments are compared as floating-point (real) numbers.

翻译为中文就是:

  • 两个参数至少有一个是 NULL 时,比较的结果也是 NULL,例外是使用 <=> 对两个 NULL 做比较时会返回 1,这两种情况都不需要做类型转换
  • 两个参数都是字符串,会按照字符串来比较,不做类型转换
  • 两个参数都是整数,按照整数来比较,不做类型转换
  • 十六进制的值和非数字做比较时,会被当做二进制串
  • 有一个参数是 TIMESTAMPDATETIME,并且另外一个参数是常量,常量会被转换为 timestamp
  • 有一个参数是 decimal 类型,如果另外一个参数是 decimal 或者整数,会将整数转换为 decimal 后进行比较,如果另外一个参数是浮点数,则会把 decimal 转换为浮点数进行比较
  • 所有其他情况下,两个参数都会被转换为浮点数再进行比较

注意点

安全问题:假如 password 类型为字符串,查询条件为 int 0 则会匹配上。

mysql> select * from test;
+----+-------+-----------+
| id | name  | password  |
+----+-------+-----------+
|  1 | test1 | password1 |
|  2 | test2 | password2 |
+----+-------+-----------+
2 rows in set (0.00 sec)

mysql> select * from test where name = 'test1' and password = 0;
+----+-------+-----------+
| id | name  | password  |
+----+-------+-----------+
|  1 | test1 | password1 |
+----+-------+-----------+
1 row in set, 1 warning (0.00 sec)

mysql> show warnings;
+---------+------+-----------------------------------------------+
| Level   | Code | Message                                       |
+---------+------+-----------------------------------------------+
| Warning | 1292 | Truncated incorrect DOUBLE value: 'password1' |
+---------+------+-----------------------------------------------+
1 row in set (0.00 sec)

相信上面的例子,一些机灵的同学可以发现其实上面的例子也可以做sql注入。

假设网站的登录那块做的比较挫,使用下面的方式:

SELECT * FROM users WHERE username = '$_POST["username"]' AND password = '$_POST["password"]'

如果username输入的是 a’ OR 1=’1 ,那么password随便输入,这样就生成了下面的查询:

SELECT * FROM users WHERE username = 'a' OR 1='1' AND password = 'anyvalue'

就有可能登录系统。其实如果攻击者看过了这篇文章,那么就可以利用隐式转化来进行登录了。如下:

mysql> select * from test;
+----+-------+-----------+
| id | name  | password  |
+----+-------+-----------+
|  1 | test1 | password1 |
|  2 | test2 | password2 |
|  3 | aaa   | aaaa      |
|  4 | 55aaa | 55aaaa    |
+----+-------+-----------+
4 rows in set (0.00 sec)

mysql> select * from test where name = 'a' + '55';
+----+-------+----------+
| id | name  | password |
+----+-------+----------+
|  4 | 55aaa | 55aaaa   |
+----+-------+----------+
1 row in set, 5 warnings (0.00 sec)

之所以出现上述的原因是因为:

mysql> select '55aaa' = 55;
+--------------+
| '55aaa' = 55 |
+--------------+
|            1 |
+--------------+
1 row in set, 1 warning (0.00 sec)

mysql> select 'a' + '55';
+------------+
| 'a' + '55' |
+------------+
|         55 |
+------------+
1 row in set, 1 warning (0.00 sec)

下面通过一些例子来复习一下上面的转换规则:

mysql> select 1+1;
+-----+
| 1+1 |
+-----+
|   2 |
+-----+
1 row in set (0.00 sec)

mysql> select 'aa' + 1;
+----------+
| 'aa' + 1 |
+----------+
|        1 |
+----------+
1 row in set, 1 warning (0.00 sec)

mysql> show warnings;
+---------+------+----------------------------------------+
| Level   | Code | Message                                |
+---------+------+----------------------------------------+
| Warning | 1292 | Truncated incorrect DOUBLE value: 'aa' |
+---------+------+----------------------------------------+
1 row in set (0.00 sec)

把字符串“aa”和1进行求和,得到1,因为“aa”和数字1的类型不同,MySQL官方文档告诉我们:

When an operator is used with operands of different types, type conversion occurs to make the operands compatible.

查看warnings可以看到隐式转化把字符串转为了double类型。但是因为字符串是非数字型的,所以就会被转换为0,因此最终计算的是0+1=1

上面的例子是类型不同,所以出现了隐式转化,那么如果我们使用相同类型的值进行运算呢?

mysql> select 'a' + 'b';
+-----------+
| 'a' + 'b' |
+-----------+
|         0 |
+-----------+
1 row in set, 2 warnings (0.00 sec)

mysql> show warnings;
+---------+------+---------------------------------------+
| Level   | Code | Message                               |
+---------+------+---------------------------------------+
| Warning | 1292 | Truncated incorrect DOUBLE value: 'a' |
| Warning | 1292 | Truncated incorrect DOUBLE value: 'b' |
+---------+------+---------------------------------------+
2 rows in set (0.00 sec)

是不是有点郁闷呢?

之所以出现这种情况,是因为+为算术操作符arithmetic operator 这样就可以解释为什么ab都转换为double了。因为转换之后其实就是:0+0=0了。

再看一个例子:

mysql> select 'a'+'b'='c';
+-------------+
| 'a'+'b'='c' |
+-------------+
|           1 |
+-------------+
1 row in set, 3 warnings (0.00 sec)

mysql> show warnings;
+---------+------+---------------------------------------+
| Level   | Code | Message                               |
+---------+------+---------------------------------------+
| Warning | 1292 | Truncated incorrect DOUBLE value: 'a' |
| Warning | 1292 | Truncated incorrect DOUBLE value: 'b' |
| Warning | 1292 | Truncated incorrect DOUBLE value: 'c' |
+---------+------+---------------------------------------+
3 rows in set (0.00 sec)

现在就看也很好的理解上面的例子了吧。a+b=c结果为1,1在MySQL中可以理解为TRUE,因为’a’+’b’的结果为0,c也会隐式转化为0,因此比较其实是:0=0也就是true,也就是1.

第二个需要注意点就是防止多查询或者删除数据

mysql> select * from test;
+----+-------+-----------+
| id | name  | password  |
+----+-------+-----------+
|  1 | test1 | password1 |
|  2 | test2 | password2 |
|  3 | aaa   | aaaa      |
|  4 | 55aaa | 55aaaa    |
|  5 | 1212  | aaa       |
|  6 | 1212a | aaa       |
+----+-------+-----------+
6 rows in set (0.00 sec)

mysql> select * from test where name = 1212;
+----+-------+----------+
| id | name  | password |
+----+-------+----------+
|  5 | 1212  | aaa      |
|  6 | 1212a | aaa      |
+----+-------+----------+
2 rows in set, 5 warnings (0.00 sec)

mysql> select * from test where name = '1212';
+----+------+----------+
| id | name | password |
+----+------+----------+
|  5 | 1212 | aaa      |
+----+------+----------+
1 row in set (0.00 sec)

上面的例子本意是查询id为5的那一条记录,结果把id为6的那一条也查询出来了。我想说明什么情况呢?有时候我们的数据库表中的一些列是varchar类型,但是存储的值为‘1123’这种的纯数字的字符串值,一些同学写sql的时候又不习惯加引号。这样当进行select,update或者delete的时候就可能会多操作一些数据。所以应该加引号的地方别忘记了。

关于字符串转数字的一些说明

mysql> select 'a' = 0;
+---------+
| 'a' = 0 |
+---------+
|       1 |
+---------+
1 row in set, 1 warning (0.00 sec)

mysql> select '1a' = 1;
+----------+
| '1a' = 1 |
+----------+
|        1 |
+----------+
1 row in set, 1 warning (0.00 sec)

mysql> select '1a1b' = 1;
+------------+
| '1a1b' = 1 |
+------------+
|          1 |
+------------+
1 row in set, 1 warning (0.00 sec)

mysql> select '1a2b3' = 1;
+-------------+
| '1a2b3' = 1 |
+-------------+
|           1 |
+-------------+
1 row in set, 1 warning (0.00 sec)

mysql> select 'a1b2c3' = 0;
+--------------+
| 'a1b2c3' = 0 |
+--------------+
|            1 |
+--------------+
1 row in set, 1 warning (0.00 sec)

从上面的例子可以看出,当把字符串转为数字的时候,其实是从左边开始处理的。

  • 如果字符串的第一个字符就是非数字的字符,那么转换为数字就是0
  • 如果字符串以数字开头
  • 如果字符串中都是数字,那么转换为数字就是整个字符串对应的数字
  • 如果字符串中存在非数字,那么转换为的数字就是开头的那些数字对应的值

如果你有其他更好的例子,或者被隐式转化坑过的情况,欢迎分享。

参考资料

  • http://dev.mysql.com/doc/refman/5.7/en/cast-functions.html
  • https://blog.eood.cn/mysql_params
  • http://dev.mysql.com/doc/refman/5.7/en/type-conversion.html

转自:http://www.cnblogs.com/rollenholt/p/5442825.html

利用API揪出抢占Win8窗口焦点的熊孩纸

起因:谁在抢镜头?!

为了开始Windows Phone 8的开发,这两天将系统升级到了Windows8.1,安装倒是又快又方便的,解压ISO镜像双击Setup一路下一步(嘛其实还输了密钥之类的)就安装好了~
不过不知道为什么去年买的正版密钥激活不了了,于是按照指示拨打了微软的客服电话,客服妹纸问了几个问题就给激活了~然后花了两三个小时进行各种配置美化和常用软件安装,看起来好极了~

01

可耻的晒桌面(其实和之前用Win7时没啥区别……已经一年多没换过主题了……)

刚开始用着感觉没啥问题啊,速度也挺快,可是打开VS写了几行代码就发现有些不对劲了,智能感应总是会莫名其妙的半路消失,然后发现聊QQ时经常打一半字输入法不见了,更糟糕的是逛B站全屏视频时会时不时跳出到窗口模式……根据十几年(啊天哪我怎么这么老了= -)的视窗操作系统(其实就是Windows~听起来逼格高那么一点?)使用经验,首先想到的是焦点被抢占了~之后用一般窗口程序观察了一下,发现的确是这样,时不时就会失去焦点半秒然后再恢复,网上查了一下,大多数说是支付宝安全控件之类的,可是我貌似不是因为这个引起的……那么究竟是谁干的呢?

04

经过:捕捉熊孩子,我有特殊的技巧

作为半个程序猿,当然就要用程序猿的方式解决咯(事实上是因为我找不到其它方法……),很久很久以前就知道了Windows有提供获得焦点窗口句柄(handle,话说为什么要翻译成“句柄”这种不明所以的东西? )的API,看来这次是可以用的上咯~
网上查了查发现不难而且有相关例子,直接打开VS,新建一个WinForm程序,拖了一个Label一个TextBox和一个Timer

00

就这么简单~

Timer设为10ms检测一次焦点窗口并输出相关信息,包括句柄号、进程PID号、进程名称、窗口名称和窗口类名,Label显示句柄号,TextBox记录相关变化
以下为代码

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Diagnostics;


namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        [DllImport("user32", EntryPoint = "GetWindowThreadProcessId")]
        private static extern int GetWindowThreadProcessId(
            IntPtr hwnd, //窗口句柄
            out int pid  //输出所在进程的PID
        );//通过句柄获取PID

        [DllImport("user32", CharSet = CharSet.Auto, ExactSpelling = true)]
        public static extern IntPtr GetForegroundWindow();//获取当前激活窗口

        [DllImport("user32", SetLastError = true)]
        public static extern int GetWindowText(
            IntPtr hWnd, //窗口句柄
            StringBuilder lpString, //标题
            int nMaxCount  //最大值
        );

        [DllImport("user32")]
        private static extern int GetClassName(
            IntPtr hWnd, //句柄
            StringBuilder lpString, //类名
            int nMaxCount //最大值
        );

        public Form1()
        {
            InitializeComponent();
            timer1.Start();
        }

        private void Form1_Load(object sender, EventArgs e)
        {

        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            IntPtr myPtr = GetForegroundWindow();
            int pid = 0;

            // 窗口进程PID
            GetWindowThreadProcessId(myPtr, out pid);

            // 窗口标题
            StringBuilder title = new StringBuilder(512);
            GetWindowText(myPtr, title, title.Capacity);

            // 窗口进程
            Process localById = Process.GetProcessById(pid);

            // 窗口类名
            StringBuilder className = new StringBuilder(512);
            GetClassName(myPtr, className, className.Capacity);

            //如果焦点窗体发生变化则记录
            if (label1.Text != myPtr.ToString())
            {
                label1.Text = myPtr.ToString();
                textBox1.Text = DateTime.UtcNow
                    + "\r\n" + pid.ToString()
                    + "\r\n" + localById.ProcessName
                    + "\r\n" + title.ToString()
                    + "\r\n" + className.ToString()
                    + "\r\n\r\n===================================================\r\n\r\n"
                    + textBox1.Text;
            }
        }
    }
}

运行起来一切正常,就差熊孩纸出现了!

点我下载熊孩子探测器

结果:嘿嘿我捉到你了!

写完代码运行了一段时间才猛然发现,在这期间居然没有出现过被抢占焦点的问题,难道狡猾的熊孩纸发现了我的阴谋?
当然这不可能啦~该出现的迟早是要出现的,于是先让它运行着,然后继续聊QQ看视频……
又过了一个小时,熊孩纸终于按捺不止又开始捣乱啦~赶快切换到探测器,终于发现了……

QQ截图20140831000327

原来是这个名为DrUpdate.exe的进程,看名字就知道,这是我们万恶的校园网登入客户端Dr.Com的自动升级进程,

03

这哪里是熊孩子捣乱,分明是州官放火啊
没办法,又不能删掉,试着用资源监视器挂起了这个进程,没想到这货居然会自动重启,最后一怒之下直接把这个文件重命名了……然后一切都安宁了~~

后记

虽然通过重命名执行文件的方式暂时解决了问题,不过后来发现重启电脑或关闭客户端后会导致无法再次打开客户端,于是只能再重命名回来(还好留了个心眼没有直接删掉),等客户端启动后再次重命名,感觉真是麻烦,不过就暂时先这样咯,反正我一般不关电脑的~

基于HTTP返回头信息的服务器时间同步

因为偶尔遇到由于各种原因,我们在使用ntpdate的时候无法成功的跟服务器同步时间的现象,而一般情况下我们对时间的精度要求都不是太高,所以想到可以使用根据HTTP头里面的信息来校时的做法。
之前看到一个使用php编写的脚本,但是考虑到很多机器是没有php环境的,因此选用shell来进行代码的编写,尽量保证通用。

PS:这篇文章正好测试一下Gist插件,看看效果如何。

C#模拟系统按键

摘自http://msdn.microsoft.com/zh-tw/library/ms171548(v=vs.110).aspx

首先,使用DllImport引入两个函数:

// Get a handle to an application window.
[DllImport("USER32.DLL", CharSet = CharSet.Unicode)]
public static extern IntPtr FindWindow(string lpClassName,
string lpWindowName);

// Activate an application window.
[DllImport("USER32.DLL")]
public static extern bool SetForegroundWindow(IntPtr hWnd);

然后首先使用FindWindow函数获取到需要按键的窗口句柄,以计算器为例。这里体现了这个方法的局限性,就是似乎不能触发全局快捷键。

// Get a handle to the Calculator application. The window class
// and window name were obtained using the Spy++ tool.
IntPtr calculatorHandle = FindWindow("CalcFrame","Calculator");

然后使用SetForegroundWindow函数将这个窗口调到最前。

SetForegroundWindow(calculatorHandle);

接下来就可以直接使用SendKeys.SendWait之类的发送按键了。

SendKeys.SendWait("111");
SendKeys.SendWait("*");
SendKeys.SendWait("11");
SendKeys.SendWait("=");

完整代码如下:

using System;
using System.Runtime.InteropServices;
using System.Drawing;
using System.Windows.Forms;
// Get a handle to an application window.
[DllImport("USER32.DLL", CharSet = CharSet.Unicode)]
public static extern IntPtr FindWindow(string lpClassName,
string lpWindowName);

// Activate an application window.
[DllImport("USER32.DLL")]
public static extern bool SetForegroundWindow(IntPtr hWnd);

// Send a series of key presses to the Calculator application.
private void button1_Click(object sender, EventArgs e)
{
// Get a handle to the Calculator application. The window class
// and window name were obtained using the Spy++ tool.
IntPtr calculatorHandle = FindWindow("CalcFrame","Calculator");

// Verify that Calculator is a running process.
if (calculatorHandle == IntPtr.Zero)
{
MessageBox.Show("Calculator is not running.");
return;
}

// Make Calculator the foreground application and send it
// a set of calculations.
SetForegroundWindow(calculatorHandle);
SendKeys.SendWait("111");
SendKeys.SendWait("*");
SendKeys.SendWait("11");
SendKeys.SendWait("=");
}

注意如果是命令行程序的话需要手动添加System.Windows.Forms引用,否则找不到SendKeys类。

Win7风格的文件夹选择对话框

.NET中默认的FolderBrowserDialog是一个很丑的XP风格的文件夹选择对话框,对用户极其不友好。微软估计也是想到了这个问题,所以提供了一套叫

Windows® API Code Pack for Microsoft® .NET Framework的东西,这里面实现了一个Win7风格的文件夹选择对话框,非常漂亮,而且比较友好。

下载地址及介绍:http://archive.msdn.microsoft.com/WindowsAPICodePack

百度网盘下载地址:http://pan.baidu.com/s/1i3606m5

本地组策略与安全策略的自动导入

本地组策略与安全策略的自动导入

   昨天接到一个需求,由于公司要求服务器要部署必需的一些安全策略,但是对于未加入域的服务器希望能有一个便捷的部署办法。

首先,提取出需要部署的策略中能通过组策略或安全策略实施的项如表所示(部分演示):

序号 要求
1 “密码必须符合复杂性要求”选择“已启动”
2 “密码最长存留期”设置为“90天”
3 “账户锁定阀值”设置为小于或等于 6次
4 “从远端系统强制关机”设置为“只指派给Administrtors组”
5 “关闭系统”设置为“只指派给Administrators组”
6 “取得文件或其它对象的所有权”设置为“只指派给Administrators组”
7 审核登录事件,设置为成功和失败都审核。
8 “审核策略更改”设置为“成功” 和“失败”都要审核
9 “审核对象访问”设置为“成功”和“失败”都要审核
10 “审核目录服务器访问”设置为“成功” 和“失败”都要审核
11 “审核目录服务器访问”设置为“成功” 和“失败”都要审核
12 “审核系统事件”设置为“成功” 和“失败”都要审核
13 “审核账户管理”设置为“成功” 和“失败”都要审核
14 “审核过程追踪”设置为 “失败”需要审核
15 “Microsoft网络服务器”设置为“在挂起会话之前所需的空闲时间”为15分钟。
16 启用屏幕保护程序,设置等待时间为“5分钟”,启用“在恢复时使用密码保护”。
17 所有驱动器均“关闭自动播放”

上表中前15项属于安全策略,第16项属于组策略中的计算机配置策略,第17项属于用户配置策略。下面仅对Windows 2003平台的操作进行了分析与测试。

    一、 对于安全策略,可以用以下步骤进行应用部署:

::在测试用机上,先使用gpedit.msc手工更改策略(如表中前15面),再用以下命令导出当前策略

secedit /export /cfg sec.inf

::用文本编辑器编辑sec.inf文件,去除不需要调整的内容,仅保留要定制策略

表中15条策略对应的inf文件内容如下:

[Unicode]
Unicode=yes
[Version]
signature=”$CHICAGO$”
Revision=1
[System Access]
MaximumPasswordAge = 90
PasswordComplexity = 1
LockoutBadCount = 6
[Event Audit]
AuditSystemEvents = 3
AuditLogonEvents = 3
AuditObjectAccess = 3
AuditPrivilegeUse = 3
AuditPolicyChange = 3
AuditAccountManage = 3
AuditProcessTracking = 2
AuditDSAccess = 3
[Registry Values]
machine/system/currentcontrolset/services/lanmanserver/parameters/autodisconnect=4,15
[Privilege Rights]
seremoteshutdownprivilege = *S-1-5-32-544
seshutdownprivilege = *S-1-5-32-544
setakeownershipprivilege = *S-1-5-32-544

::用命令生成一个sdb文件

secedit /configure /db  sec.sdb  /cfg sec.inf

::用命令把定制策略更新到目标服务器,不能用/overwrite参数,否则除定制策略外的其它策略丢失

secedit /configure /db sec.sdb

::刷新组策略

gpupdate /force

    二、其他组策略的应用

以前曾经研究过利用gpcvreg与gpscript命令行程序来应用组策略,并且写了autoit3脚本的UDF,这次正好可以利用。
使用gpedit.msc在测试机修改16/17两条策略,在不关闭gpedit.msc的同时用regedit查看HKEY_CURRENT_USER/Software/Microsoft/Windows/CurrentVersion/Group Policy Objects下,分析得到相应设置并存成Reg文件

machine.reg, 禁用所有驱动器自动播放

[HKEY_LOCAL_MACHINE/Software/Microsoft/Windows/CurrentVersion/Policies/Explorer]
“NoDriveTypeAutoRun”=dword:000000FF

user.reg,定制屏幕保护设置

[HKEY_CURRENT_USER/Software/Policies/Microsoft/Windows/Control Panel/Desktop]
“ScreenSaverIsSecure”=”1”
“ScreenSaveActive”=”1”
“ScreenSaveTimeOut”=”300”
“SCRNSAVE.EXE”=”scrnsave.scr”

三、批量应用脚本

有了sec.sdb、machine.reg及user.reg文件,然后利用以前写的poledit.au3 UDF ,只需要以下脚本就可以进行前文所列出的策略的自动应用了。


#RequireAdmin
#NoTrayIcon
#include "PolEdit.au3"

If FileExists("sec.sdb") Then RunWait(@ComSpec & " /c " & "secedit /configure /db sec.sdb", @ScriptDir, @SW_HIDE)
_RegWriteToPol("machine.reg", "MACHINE", 1)
_RegWriteToPol("user.reg")
_gpupdate()