月度归档:2015年08月

绕过代理服务器获取客户端IP

一般来说,Web应用和客户端之间隔着很多个代理服务器,无论正向代理还是反向代理。这样就给本地资讯类应用(新闻、天气等)或者统计(审计,追踪等)需求带来了麻烦,因为在应用通过request.getRemoteAddr()方法(以Java为例)往往只能拿到与自己直接通信的设备IP。因此,如果用户通过直接或者间接的正向代理(ISP提供的缓存服务器等)上网,Web应用只会取到正向代理服务器地址;如果Web应用前端部署着Nginx或者Apache之类的反向代理,Web应用只会取到反向代理的地址。

面对这样的限制,代理厂商利用HTTP自定义Header规避了问题。比如Nginx,可以添加以下配置:

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

它向后端应用传递了两个HTTP自定义Header,X-Real-IP和X-Forwarded-For。这两个Header作用类似,都是向后端透传客户端源IP,但是又有一些不同,主要区别在于:

1. X-Forwarded-For已有标准RFC文档(RFC 7239)定义,而X-Real-IP没有。
2. X-Forwarded-For Header格式为X-Forwarded-For: client1, proxy1, proxy2,每级代理都会将与自己直接通信的对端IP追加在X-Forwarded-For中,因此应用还需要额外解析IP列表以获取所需IP。而X-Real-IP只会记录与自己直接通信的对端IP。

由此,反向代理后面的应用服务器可以通过X-Real-IP或者X-Forwarded-For请求头来获取客户端IP:

String xRealIp = request.getHeader("X-Real-IP");
String xForwardIps = request.getHeader("X-Forwarded-For");

另外需要注意的是,HTTP Header是很容易被伪造的,因此,X-Forwarded-For Header中的首个IP也不意味着就一定是客户端源IP。从安全性方面来说,如果应用前不加反向代理,则request.getRemoteAddr()拿到的IP便是可信的,无法伪造;如果加了反向代理,那么X-Real-IP和X-Forwarded-For IP的最后一个IP是可信的,无法伪造。

--EOF--

增肥记

自成年以来,我身高一直在180-182CM间浮动,体重最重的时候大概是5年前的冬天了,大约65KG,平时则一直在62KG前后浮动。BMI指数18.7,处于正常和偏瘦之间。所以可以想象,我的体态应该属于偏竹竿的一类。

端午节出去买衣服时再一次发现自己太瘦了,于是也不知道是第几次了下定决心要增肥。之前的增肥计划因为各种原因都失败后,使得我一度怀疑自己是不是肠胃不好,因为我的饭量向来不小。后来在知乎看了一些问答,知道了热量、体重和增肌之间的关系,再回过头来想想,其实自己屡战屡败的原因只有一个:吃得不够多!体重变化这些事儿,一句话来概括就是当你每日摄入的卡路里大于消耗掉的,体重就会增加;反之,体重就会减少。

这里有每日所需热量的计算方式,以我为例:

增肥前体重:62.5KG
基础代谢率:1635 大卡/天
每日所需热量(职业码农,轻量运动):2534 大卡

那么如果需要增重,每日摄入的卡路里必须要达到 3000 - 3500 大卡。要知道一个鸡蛋大概70卡,一碗米饭才100卡,3000+大卡的热量意味着每天至少要吃5顿才能吃够。

于是接下来的一段时间,我每天的基础饮食就变成了这样:

  1. 早餐:600 - 800卡。
  2. 中餐:500 - 800卡。
  3. 下午点心:300 - 500卡。
  4. 晚餐:500 - 800卡。
  5. 夜宵:500 - 600卡。

这段时间,我看到的食物不是食物,而是流动的卡路里。当某一餐吃不够热量时,那就只能去售货机买面包、薯条之类的补补,也不管是不是垃圾食品了,总之坚决不能少吃。这里重点推荐一款国外应用FatSecret,它能量化每天摄入的热量,支持簿记体重和自动生成体重曲线,唯一的不足可能是中文食物种类较少,有时候吃到冷门食物时需要自己添加或者估算。每天卡路里摄入记录和体重曲线对每一个关心体重变化的人来说都非常重要,甚至我觉得是成败的关键。

吃是一方面,另一方面就是练。我没系统的健身计划,比较随意。基本上每周跑步4次,每次10分钟,1-2公里。跑完以后器材锻炼20分钟,主要项目是坐推3组,每组12个,练手臂力量和线条,仰卧起坐100个,分4-5组。

经过一个月的吃和练,体重从原先的62.5KG增加到70KG:

Weight History

体脂率总17.9增至20.2:
体脂率

遗憾的是,随着体重和脂肪的增加,身体年龄也从26岁增至31岁,而且明显感觉到肚子有些隆起,这说明练还是没有跟上吃的节奏,需下一阶段修正。
身体年龄

这次在一个月内完成增重的目标,最大的收获是自我感觉突破了身体极限,扭转了长久以来的惯性思维,证明自己的内心足够强大,这也算是一种认知上的提升吧。

--EOF--

Java IO Stream句柄泄露分析

Java io包封装了常用的I/O操作,流式操作被统一为InputStream、OutputStream、Reader、Writer四个抽象类,其中InputStream和OutputStream为字节流,Reader和Writer为字符流。流式抽象的好处在于程序员无需关心数据如何传输,只需按字节或字符依次操作即可。

在使用流式对象进行操作时,特别是文件操作,使用完毕后千万不能忘记调用流对象的close()方法,这也是老生常谈的话题,但是偏偏很容易忘记,或者没忘记但是使用不当导致close()方法没调用到。正确的做法是把close()方法放在finally块中调用,比如这样:

InputStream in = ...;
OutputStream out = ...;
try {
    doSth(in, out);
} catch (Exception e) {
    handleException();
} finally {
    try {
        in.close();
    } catch (Exception e1){
    }
    try {
        out.close();
    } catch (Exception e2){
    }
}

Java流式操作在便捷了I/O操作的同时,也带来了错误处理上复杂性,这也是Java被人诟病的理由之一。Golang在这块的处理就非常优雅,它提供了defer关键字来简化流程。

当文件流未被显式关闭时,会产生怎样的后果?结果就是会引起文件描述符(fd)耗尽。以下代码会打开一个文件10次,向系统申请了10个fd。

public static void main(String[] args) throws Exception {
    for (int i = 0; i < 10; i++) {
        InputStream is = new FileInputStream(new File("file"));
    }
    System.in.read(b); // pause
}

到/proc/{pid}/fd目录下确定已打开的文件描述符:

root@classa:/proc/16333/fd# ls -l
total 0
lrwx------ 1 root root 64 Aug  2 20:43 0 -> /dev/pts/3
lrwx------ 1 root root 64 Aug  2 20:43 1 -> /dev/pts/3
lr-x------ 1 root root 64 Aug  2 20:43 10 -> /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/ext/sunjce_provider.jar
lr-x------ 1 root root 64 Aug  2 20:43 11 -> /root/file
lr-x------ 1 root root 64 Aug  2 20:43 12 -> /root/file
lr-x------ 1 root root 64 Aug  2 20:43 13 -> /root/file
lr-x------ 1 root root 64 Aug  2 20:43 14 -> /root/file
lr-x------ 1 root root 64 Aug  2 20:43 15 -> /root/file
lr-x------ 1 root root 64 Aug  2 20:43 16 -> /root/file
lr-x------ 1 root root 64 Aug  2 20:43 17 -> /root/file
lr-x------ 1 root root 64 Aug  2 20:43 18 -> /root/file
lr-x------ 1 root root 64 Aug  2 20:43 19 -> /root/file
lrwx------ 1 root root 64 Aug  2 20:43 2 -> /dev/pts/3
lr-x------ 1 root root 64 Aug  2 20:43 20 -> /root/file
lr-x------ 1 root root 64 Aug  2 20:43 3 -> /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar

但是根据以往经验,即使流未显式关闭,也没见过文件描述符耗尽的情况。这是因为Java文件流式类做了保护措施,FileInputStream和FileOutputStream类利用Java的finalizer机制向GC注册了资源回收的回调函数,当GC回收对象时,实例对象的finalize()方法被调用。以FileInputStream为例,看看它是怎么处理的:

/**
 * Ensures that the close method of this file input stream is
 * called when there are no more references to it.
 *
 * @exception  IOException  if an I/O error occurs.
 * @see        java.io.FileInputStream#close()
 */
protected void finalize() throws IOException {
    if ((fd != null) &&  (fd != FileDescriptor.in)) {

        /*
         * Finalizer should not release the FileDescriptor if another
         * stream is still using it. If the user directly invokes
         * close() then the FileDescriptor is also released.
         */
        runningFinalize.set(Boolean.TRUE);
        try {
            close();
        } finally {
            runningFinalize.set(Boolean.FALSE);
        }
    }
}

当fd未释放时,finalize()方法会调用close()方法关闭文件描述符。有了这一层保障后,即使程序员粗心忘了关闭流,也能保证流最终会被正常关闭了。以下程序可以验证:

public static void main(String[] args) throws Exception {
    for (int i = 0; i < 10; i++) {
        InputStream is = new FileInputStream(new File("file"));
    }
    byte[] b = new byte[2];
    System.in.read(b); // pause1
    System.gc();
    System.in.read(b); // pause2
}

Java运行参数加上GC信息便于观察:

# java -verbose:gc -XX:+PrintGCDetails -Xloggc:gc.log -XX:+PrintGCTimeStamps StreamTest

程序在pause1处打开了10个fd,接着强制通过System.gc()触发一次GC,等gc.log中GC日志输出后再观察/proc/{pid}/fd目录,发现已打开的文件描述符均已经关闭。

但是即便如此,依然存在资源泄漏导致程序无法正常工作的情况,因为JVM规范并未对GC何时被唤起作要求,而对象的finalize()只有在其被回收时才触发一次,因此完全存在以下情况:在两次GC周期之间,文件描述符被耗尽!这个问题曾经在生产环境中出现过的,起因是某同事在原本只需加载一次的文件读取操作写成了每次用户请求加载一次,在一定的并发数下就导致too many open file的异常:

Exception in thread "main" java.io.FileNotFoundException: file (Too many open files)
        at java.io.FileInputStream.open(Native Method)
        at java.io.FileInputStream.(FileInputStream.java:146)
        ......

--EOF--