学习一门技术有时候需要一个充足的理由

这几天在折腾CoffeeScript有此感想,这门语言(或者说是JS的方言吧),我并不是第一次接触,大概是它刚出来的时候我就开始关注了,这期间有无数次看到有人提到它,每次我就打开它的官网,然后看一看就随手关掉,没有任何下文了

这次想到它是因为公司的翻墙服务最近老是挂掉,我想自己写一个性能好一点的程序替换。显然我所熟悉的PHP是干不了这个的,用JAVA或者Scala来折腾似乎又太重了,听说Node.js比较适合处理io请求,所以我兴冲冲的打开了Node.js的官网开始看手册了(我个人推荐,手册是最好的老师,比第三方网站的介绍靠谱很多)。

嗯,挺简单的。这是我的第一感觉,就是js加上一堆系统库支持。而且大部分都是用来处理网络io的,正合我意,于是我就balabala开始写了。

写完以后我看着这坨东西,不敢相信它出自有洁癖的我之手,更令人沮丧的是我似乎已经无法在结构层面去优化它让它更加清晰了

var http = require('http'),
    httpProxy = require('http-proxy'),
    dns = require('dns')
    url = require('url'),
    net = require('net')
    redis = require('redis')
    md5 = require('MD5'),
    zlib = require('zlib'),
    common = require('./common.js');
    opt = require('node-getopt').create([
        ['R', 'redis=ARG', 'redis connect'],
        ['P', 'port=ARG', 'proxy port'],
        ['H', 'host=ARG', 'proxy host'],
        ['S', 'socks=ARG', 'socks file path'],
        ['h' , 'help', 'display this help']
    ]).bindHelp().parseSystem(), options = {
        'redis' :   '127.0.0.1',
        'socks' :   '/tmp/g2.sock',
        'port'  :   8080,
        'host'  :   'localhost'
    }, list = [], response = [], pool = {};

for (var k in options) {
    if (!!opt.options[k]) {
        options[k] = opt.options[k];
    }
}

process.on('uncaughtException', function (e) {
    console.log(e);
});

function lookup(domain, callback) {
    var key = md5(domain);

    // find record from cache first
    redisClient.get(key, function (err, record) {
        // handle redis error
        if (err) {
            common.log('warn', err);
            callback(false);
            return;
        }

        // hit cache
        if (record) {
            common.log('debug', 'hit ' + domain + ' to ' + record);
            callback(record);
            return;
        }

        // find from dns
        dns.lookup(domain, function (err, ip) {
            if (err) {
                common.log('warn', err);
                callback(false);
                return;
            }

            // write to cache
            redisClient.set(key, ip);
            redisClient.expire(key, 86400);
            callback(ip);
            common.log('debug', 'resolve ' + domain + ' to ' + ip);
        });        
    });
}

function errorRes(res, err) {
    common.log('error', err);
    res.writeHead(500, {
        'Content-Type': 'text/plain'
    });

    res.end('Something went wrong. And we are reporting a custom error message.');
}

var redisClient = redis.createClient(6379, options.redis, {}),
    proxy = httpProxy.createProxyServer(),
    httpServer = http.createServer(function (req, res) {
        common.log('debug', 'http proxy request ' + req.url);
        uri = url.parse(req.url); 

        lookup(uri.host, function (ip) {
            if (!ip) {
                errorRes(res, uri.host + ' can not resolve');
                return;
            }
        
            proxy.web(req, res, {
                target : {
                    host : ip,
                    port : uri.port || 80
                }
            });
        });     
    }),
    requestServer = net.createServer(function (socket) {
        common.log('debug', 'connect from ' + socket.remoteAddress + ':' + socket.remotePort);
        
        socket.on('data', function (data) {
            var length = data.readInt8(0),
                key = new Buffer(length, 'ascii');

            data.copy(key, 0, 1, 1 + length);
            var buff = new Buffer(data.length - 1 - length);
            data.copy(buff, 0, 1 + length);

            list.push([length, key, buff]);
        });
    }),
    responseServer = net.createServer(function (socket) {
        setInterval(function () {
            var task = list.shift();
            if (!!task) {
                setImmediate(function () {
                    var id = task[1].toString(),
                        buff = new Buffer(task[2].toString('binary'), 'base64'),
                        conn = pool[id];

                    if (!conn) {
                        conn = net.connect(options.socks, function () {
                            conn.write(buff);
                        });

                        conn.setTimeout(30000, function () {
                            if (conn) {
                                conn.end();
                            } else {
                                conn.destroy();
                            }
                        });
                        pool[id] = conn;
                    } else {
                        conn.write(buff);
                    }

                    conn.on('data', function (data) {
                        var str = new Buffer(data.toString('base64'), 'binary'),
                            buff = new Buffer(1 + task[0] + str.length);

                        buff.writeInt8(task[0], 0);
                        task[1].copy(buff, 1, 0);
                        str.copy(buff, 1 + task[0], 0);

                        response.push(buff);
                    });
                });
            }
        });

        setInterval(function () {
            var buff = response.shift();

            if (!!buff) {
                socket.write(buff);
            }
        });
    });

// when a CONNECT request comes in, the 'upgrade'
// event is emitted
httpServer.on('connect', function(req, socket, head) {
    common.log('debug', 'socket request ' + req.url);
    var parts = req.url.split(':', 2); 

    socket.on('end', function (end) {
        console.log('end');
    });

    lookup(parts[0], function (ip) {
        if (!ip) {
            errorRes(res, parts[0] + ' can not resolve');
            return;
        }

        var conn = net.connect(parts[1], ip, function() {
            socket.write("HTTP/1.1 200 OK\r\n\r\n");
            socket.pipe(conn);
            conn.pipe(socket);
        });
    });
});

proxy.on('error', function (err, req, res) {
    errorRes(res, err);
});

requestServer.listen(options.port, options.host);
responseServer.listen(options.port + 1, options.host);
common.log('debug', 'G2 proxy is listenning on ' + options.host + ':' + options.port);
httpServer.listen(options.socks);
common.log('debug', 'G2 proxy internal tunnel is listenning on ' + options.socks);

正当我一筹莫展的时候,一道光照进了我的脑海 -- CoffeeScript,这个我曾经无数次踌躇满志地拿起又无数次轻而易举地放下的语言不就是解决这个问题的吗。

我现在强烈地需要它来拯救我的代码,于是这门两三年来只停留在我口头上的语言,我花了不到20分钟就把它学会了。因为它的设计初衷是如此地契合我的需求,我甚至可以感受到作者在写这门语言时是如何问候JS的,有些语法甚至我都不用看手册,自己猜都能猜个八九不离十,真是一门让人舒服的语言。

下面是我改进后的版本,当然不仅仅是换了一个语言,实际上由于结构更加清晰,我可以做更多逻辑层面的优化,一些复杂的逻辑我可以剥离为其他模块,这段代码只展现了其中一个主要模块

opt = require 'node-getopt'
http = require 'http'
net = require 'net'
httpProxy = require 'http-proxy'
url = require 'url'
logger = require 'winston'
pipe = require './pipe'
cluster = require 'cluster'
dns = require 'dns'

class Server
    
    constructor: (options) ->
        @createServer options.socks if options.socks?

    createServer: (socks) ->
        proxy = new httpProxy.createProxyServer()

        proxy.on 'error', (error) ->
            logger.error error

        if cluster.worker
            logger.info "##{cluster.worker.process.pid} listenning at #{socks}"
        else
            logger.info "##{process.pid} listenning at #{socks}"

        @server = http.createServer (req, res) ->
            logger.info req.method + ' ' + req.url

            uri = url.parse req.url
            proxy.web req, res,
                target :
                    host : uri.hostname
                    port : uri.port or 80
            
        @server.on 'connect', (req, socket, head) ->
            logger.info 'CONNECT ' + req.url
            [host, port] = req.url.split ':'

            conn = net.connect port, host, ->
                socket.write "HTTP/1.1 200 OK\r\n\r\n"
                socket.pipe conn
                conn.pipe socket
            
        if socks.indexOf ':'
            [h, p] = socks.split ':'
            @server.listen p, h
        else
            @server.lister socks

options = opt.create([
    ['W', 'workers=ARG', 'child workers count'],
    ['C', 'cache=ARG', 'cache connect'],
    ['S', 'socks=ARG', 'socks file path'],
    ['h', 'help', 'display this help']
]).bindHelp().parseSystem().options

process.on 'uncaughtException', (error) ->
    logger.error error

if options.workers? and options.workers > 1
    if cluster.isMaster
        cluster.fork() for i in [1 .. options.workers]
    else
        new Server options
else
    new Server options

实际上,作为一个开发者,你一定有很多类似的经历。虽然有时候我们会关注很多有趣的新技术,但也只是走马观花般看看。甚至有时候你强迫自己去学习它,效率也一定很低下,这时候你的大脑并没有打开io接口,你硬塞进去的东西最终都会流向/dev/null

但如果你一旦在实际工作中需要它们,你的大脑立刻会如饥似渴地想要吸收这些信息,你会因此有强大的驱动力去喂饱它。这个时候一定是你觉得学习东西最高效的时候。

所以学习的动力并不是凭空而来的,你需要释放大脑能量之锁的钥匙,这把钥匙就是一个充足的理由,一个我为什么要学习它的理由,要不然你永远无法真正开始。而这个理由并不是你刻意去找的,因为那样你自己都明白是在欺骗自己。

真正的理由是从实际工作中来的。这样就简单了,如果你喜欢折腾,就多去接触那些能让你产生学习新技术理由的工作,如果没有那就自己创造一些,总之不断告诉你的大脑,这些东西用得着,非常重要。

已有 6 条评论

  1. loftor loftor

    70大大 赞一个,面对太多的武功(语言),真的是用到了,或有兴趣了去学才是效率最高的!

  2. Bingquan Bao Bingquan Bao

    哈哈,这个大牛开拓了coffeescript,underscone,backbone。。。https://github.com/jashkenas ,,,太牛了,崇拜

  3. 小杨 小杨

    确实如此,很多事情都是实际遇到之后才会产生动力去学习、去折腾。兴趣是最好的老师,这点没有错。

  4. 么西QQ 么西QQ

    渣渣学生一枚,由于自己瞎走,没有专精一门技术,现在各种原因,挺迷茫的-,-学技术,到底为了什么,一个字,乱

  5. Kayode Kayode

    现在还在学Pascal。。。。。。
    不知为什么好像没动力了

    1. Kimwang Kimwang

      感觉现在就算入门也没必要再用PASCAL了吧,严重脱节的教材。

添加新评论