网络寻租

Programmer, Gamer, Hacker

windows下面设置VPN连接

| Comments

image

什么是VPN

简单地说, VPN就是, 网络上面一台主机, 建立了一个虚拟的网络. 然后我们的电脑登录上去, 加入到这个虚拟的网络里面来. 然后我们就可以利用这个主机做跳板, 访问网站, 或者同属于这个虚拟网络的电脑.

这样可以让物理上分布在不同地点的电脑, 看起来好像在一个局域网络下面.

一般来说, VPN用来做几件事:

  • 在外网的电脑, 访问一个局域网络, 方便在家办公.
  • 利用提供服务的主机做跳板, 访问这个主机所在位置的资源. 就是我们说的翻墙.

windows下面设置方法

这里有一个教程: http://netfee.ustc.edu.cn/ylxia/help/faq/faq_howtosetupvpn_winxp.htm

我也自己整理了一下:

  • 点击控制面板->网络连接,
  • 左边建立新连接. 下一步, 连接到我的工作网络
  • 下一步, 虚拟专用网络.
  • 公司名称随便输入
  • 设置IP.
  • 点击完成.
  • 然后输入用户名和密码.
  • 然后连接就可以使用了.

人人都应该掌握一门乐器

| Comments

image

作为一名程序员, 我最近开始下定决心掌握一门乐器, 我跟风一个朋友franky开始学习钢琴. 我去附近的一个琴行报了班, 开始了练习.

现在基本能弹一些非常简单的练习曲, 比起franky能弹<放牛班的春天>, 我还差远了. 但是我已经计划好了学习钢琴的时间: 一辈子.

为什么要掌握乐器

那么为什么我要开始学习乐器了呢? 我觉得音乐是人类社会非常重要的伟大成就, 绝大多数人都喜欢听音乐.

而演奏音乐是欣赏音乐的一个重要组成部分, 音乐在自己手中变化, 更能体验到音乐的魅力.

如果说一生中没有尝试过这个过程, 那么我觉得人生体验是很不完整的. 而学习音乐几乎不需要基础, 只需要时间和若干请老师的钱.

如何掌握一门乐器

如果你决定开始学习乐器了, 你基本上完成了大部分的工作. 下面你只需要去本地的琴行, 选择一个你中意的乐器, 掏钱出来就可以了.

比起掌握乐器带来的乐趣和收益, 这点钱不算什么(除非你真的非常非常缺钱, 你还是专心赚钱去吧..)

结论

我从2011/03/26开始, 已经断断续续学了4个多月了, 工作和精力原因, 我平均每天练半个小时. 我希望和预期能够在单位年底活动的时候, 献艺一曲. 这样绝对能够树立我新的形象. 希望大家也都加入到学习乐器的队伍中来.

Cython介绍

| Comments

image

python是门很强大很易用的动态语言, 像其他动态语言一样, 非静态绑定而是在执行时确定值会造成很多性能损耗, 我们可以用cython来解决这样的性能问题.

cython的策略是这样的: 在python语法的基础上, 加上一些静态语言的特性, 比如确定值类型等, 然后把这种类似python语法的代码, 编译成c代码, 然后利用c编译器, 把代码做成python模块. 这样通过静态编译的方式, 来提高执行代码的性能.

具体如何使用和学习我就不多说了, 官方文档 上面写得很清楚.

我写了一个简单的实例代码: https://bitbucket.org/linjunhalida/code-example/src/tip/python/cython_test/ 但是转变成cython性能提高只有一倍左右, 不知道到底出了什么问题…

程序员在工地

| Comments

image

7/23号, 因为公司有仪器要出货, 最后阶段需要安装程序等等, 我需要随着公司的安装队伍一起出行, 目的地是东莞的沙田镇. 这一行, 对我来说是非常失败, 非常浪费时间, 非常没有效率.

我们7/23号出发, 7/24号到达沙田, 安顿在了一个小旅店里面, 然后去勘测现场. 发现仪器安装需要的种种条件都没有满足: 地基没有搭建好, 电没有上, 于是我们带队的领导和当地的甲方紧急沟通, 等待了2天, 终于可以施工了. 施工过程基本上是我们的安装小组在做, 我作为一个闲人, 前几天完全没有任务, 却也在领导的指示下, 留守工地. 只有最后一天才上场, 花费2/3个小时就搞定了.

总体而言, 这么多天的工地留守对我而言是完全没有效率的. 如果有机会, 下次绝对需要避免这样的状况发生. 留守期间, 我通过看书抢救了部分时间, 但是因为场地关系, 飞沙走石, 暴雨烈日, 书只是看了一部分. 作为一个优秀的代码生产机器, 这样的空转让我十分痛心.

以上是本次出差的总结, 我像以前一样确定, 我不愿意做那些无法体现我特殊价值的的工作, 以及, 我在恶劣条件下完全没有产能. 人生苦短, 我需要通过以我的方式来改变世界来体现我的价值, 以后我也会继续往这个方向努力!

一天时间用python写门语言

| Comments

image

今天我在广州加班, 上午其他同事去干活我一个人在旅店, 没事做就又练习写计算器程序了. 本来打算用python来写一个简单的支持加减乘除括号的计算器, 后来有加上了变量复制, 比较符, 条件判断, 循环, 最后还加上了函数, 几乎可以说是一个简单的语言了. 以后我也可以说写过一门语言了哈哈.

回到正题. 这里面整理一下这个简单的程序用到的方法, 代码在: https://bitbucket.org/linjunhalida/code-example/src/tip/python/calculator/

一开始, 我希望实现一个简单的支持加减乘除括号的计算器, 我不打算用python自带的方式(eval), 而是自己写一个. 虽然有种种取巧的方式, 我还是采用教科书般的方法, 首先词法分析, 然后语法分析+计算结果.

词法分析

词法分析的函数是syntax_analysis, 输入一个字符串, 拆分成一个个的词, 解析整型和浮点. 比如: “2 * 3 + 1” 就拆分成 [2, ‘*’, 3, ‘+’, 1], 没什么好说的, 从头开始解析即可. 没有用到状态机, 只是简单的根据字符串的首字母做条件判断, 然后一个个往下读, 根据状况拆分.

语法分析

我采用的方法是从the C++ programming language里面那个计算机程序学来的方法: 首先写出基本的语法, 然后每个语法模块写一个函数, 然后通过函数的递归调用来模拟语法树的生成和解析, 不需要显式地生成语法树.

我用了一个类Caculator来保存词法分析后的list, 以及当前处理到的符号位置, 变量表等东西, 语法模块都是这个类的方法, 这样写起来方便一点.

我们一部分一部分来吧. 首先我先实现加减乘除括号. 整理基本的语法:

l0 = (ltop) | n | - n 
l1 = l1 * l0 | l1 / l0 | l0
l2 = l2 + l1 | l2 - l1 | l1
ltop = l2

n 是数字. 里面l0, l1, l2, 是根据优先级做了分割, 保证优先级高的操作首先执行. ltop的目的是为了方便扩充新的运算符号.

l2函数如下, 整体的逻辑是, 当发现下一个符号是’+–‘的时候, 就一直解析和计算v, 然后返回v. self.ls是词法分析后的词列表, self.p是表示当前处理到的词位置. inc()表示解析下一个词, get()获取下一个词, has_next()判断下面还有没有.

所有的函数都类似这样的方式, 不传参数进去, 解析词列表, 返回计算后的结果.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def l2(self):
    v = self.l1()
    while (self.has_next()
           and self.isstr(self.get())
           and self.get() in '+-'):
        if self.get() == '+':
            self.inc()
            v2 = self.l1()
            v += v2
        elif self.get() == '-':
            self.inc()
            v2 = self.l1()
            v -= v2
    return v

赋值

然后我开始实现赋值功能, 同时考虑扩展成为能够解析多个表达式的语法, 这个基本也是抄the C++ programming language的方法, 改了几点:

  • 加了program, exp_list, exp, 更像一个语言了.
  • l0加上NAME, 能够获取NAME对应变量的值.
  • assignment赋值的操作. 为了通用, 赋值也是有返回值的.
l0 = (ltop) | n | NAME 

program = END | exp_list END
exp_list = exp_list ; exp
exp = ltop | assignment

assignment = NAME = exp

赋值和计算都是exp的一种, 代码是这样写的:

1
2
3
4
5
6
7
def exp(self):
    if self.has_next(1):
        if self.get(1) == '=':
            return self.assignment()
        ...

    return self.ltop()

我在Caculator类里面加了一个values的dict, 用来保存赋值的值, 赋值和获取值的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def assignment(self):
    name = self.get()
    self.inc()
    self.inc()
    v = self.exp()
self.values[name] = v
    return v

def l0(self):
    g = self.get()
    ...
    elif self.issymbol(g):
        self.inc()
        return self.values[g]

条件判断

现在我的简单语法变得有点像一门真正的语言了, 但是还缺少很多必要的东西, 我思考如何实现条件判断. 首先实现判断语句, 语法:

l3 = l2 < l2 | l2 > l2 | l2 == l2 | l2
ltop = l3

实现很简单, 判断下一个符号是什么而已, 为了简单, 我没有加上True/False, 而是统一用1/0的方式来做, 真返回1, 假返回0.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def l3(self):
    v = self.l2()

    if not self.has_next():
        return v

    if self.get() == '<':
        self.inc()
        v2 = self.l2()
        if v < v2: return 1
        else: return 0
    elif self.get() == '>':
        self.inc()
        v2 = self.l2()
        if v > v2: return 1
        else: return 0
    elif self.get() == '==':
        self.inc()
        v2 = self.l2()
        if v == v2: return 1
        else: return 0

    return v

然后实现条件判断. 语法如下:

exp = ltop | assignment | condition
condition = if ltop {exp_list} else {exp_list}

exp里面判断第一个词是不是if, 然后跳转到condition函数(循环, 函数也采用类似的方法来判断), 然后解析条件判断的值, 如果大于一, 解析第一个exp_list, 不然解析第2个exp_list. 里面做了一些语法错误的判断. 还有else部分是可选的, 为了简单没有实现elif的方式. 对于不需要解析的exp_list, 我利用goto_next_block来跳转, 要注意的是处理嵌套{}的问题.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def exp(self):
    if self.has_next(1):
        if self.get(1) == '=':
            return self.assignment()
        elif self.get() == 'if':
            return self.condition()
    ...

def condition(self):
    self.inc()
    v = self.ltop()
    if self.get() != '{':
        self.error('exp error : no {')
    self.inc()

    if v > 0:
        v = self.exp_list()
    else:
        self.goto_next_block()

    if self.get() != '}':
        self.error('exp error : not }')
    self.inc()

    if self.has_next() and self.get() == 'else':
        self.inc()
        if self.get() != '{':
            self.error('exp error : no {')
        self.inc()

        if v <= 0:
            v = self.exp_list()
        else:
            self.goto_next_block()

        if self.get() != '}':
            self.error('exp error : not }')
        self.inc()
    return v

def goto_next_block(self):
    count = 1
    while self.has_next():
        if self.get() == '}':
            count -= 1
            if count <= 0: break
        elif self.get() == '{':
            count += 1
        self.inc()

循环

好了, 条件判断实现了, 下面该是循环了. 语法:

exp = ltop | assignment | condition | loop 
loop = while ltop {exp_list}

具体实现上, 我缓存了loop开始的位置, 先判断ltop, 如果发现满足, 执行exp_list, 然后设置self.p为loop开始, 然后继续判断ltop. 不满足的话就跳出来.. 比我现象中的容易实现.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def loop(self):
    self.inc()
    p = self.p
    while True:
        v = self.ltop()

        if self.get() != '{':
            self.error('exp error : no {')
        self.inc()

        if v > 0:
            v = self.exp_list()
        else:
            self.goto_next_block()

        if self.get() != '}':
            self.error('exp error : not }')
        self.inc()

        if v <= 0:
            break
        else:
            self.p = p

    return v

函数

好了, 没有函数的语言是不完整的. 为了实现函数, 需要做很多工作.

先看语法, l0上面加的是调用函数, function_args是函数调用的参数, exp里面加上函数定义的部分, function_args_names是形参.

l0 = (ltop) | n | NAME | - n | NAME ( function_args )

exp = ltop | assignment | condition | loop | function 

function = def NAME(function-args-names){exp_list}
function_args_names = function_args , NAME | NAME  | None
function_args = function_args , exp | exp | None

根据我看SICP学来的经验, 函数可以采用环境来实现. 所谓环境的概念, 就是函数本身是嵌套的, 每个函数内部就是一个环境, 保存有一些局部变量, 只在函数内部有效. 传给函数的参数就是在函数的环境里面做对应的赋值.

比如: def f1(a, b) {…}; f1(1, 2) , f1的环境是env1, f2的环境是env2, 在调用f1的时候, 执行到f1内部时, Caculator的self.env是env1, env1.values含有a, b 2个变量, 值分别是调用f1(1, 2)时传进来的1和2.

我们看Env类的定义. 现在我们删除掉Caculator的values, 让Env的 get_value()和set_value来获取和设置变量. Env是嵌套的, 因为函数调用的时候, 也可以访问上层赋值过的变量.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Env():
    def __init__(self, parent=None):
        self.values = {}
        self.parent = parent

    def get_value(self, v):
        if v in self.values:
            return self.values[v]

        if not self.parent:
            raise Exception('cannot find value: %s, current_values: %s' % (v, str(self.values)))

        return self.parent.get_value(v)

    def set_value(self, name, v):
        self.values[name] = v

然后是函数赋值, 我把函数抽象成了一个类, 保存函数名, 开始位置, 以及缓存词列表, 因为caculator类是可以执行多次代码的. 最后我利用赋值的方式把函数保存下来.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Func():
    pass

def function(self):
    func = Func()

    self.inc()

    name = self.get()
    if not self.issymbol(name):
        self.error('function error name : %s' % name)
    self.inc()
    func.name = name

    if self.get() != '(':
        self.error('function error not (')
    self.inc()

    func.arg_names = self.function_args_names()

    if self.get() != ')':
        self.error('function error not )')
    self.inc()

    if self.get() != '{':
        self.error('function error not {')
    self.inc()

    func.p = self.p
    func.ls = self.ls

    self.goto_next_block()

    if self.get() != '}':
        self.error('function error not }')
    self.inc()

    self.env.set_value(func.name, func)
    return 0

然后是函数执行的部分了. l0里面根据语法识别到函数调用, 然后执行function_all, 里面首先生成一个新的环境, 然后在新的环境里面, 把实际参数赋值给形参, 然后保存现在的ls, p, env, 然后执行函数体, 然后回复ls, p, env. function_args解析和返回实际参数列表, function_args_names解析和返回形参名称, 保存在Func.names里面, 都比较简单就不列出来了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def l0(self):
    g = self.get()
    ...
    elif (self.has_next(2)
          and self.issymbol(g)
          and self.get(1) == '('):
        func = self.env.get_value(g)
        self.inc()
        self.inc()
        args = self.function_args()

        if not self.get() == ')':
            self.error('l0 function call error')
        self.inc()

        return self.function_call(func, args)

def function_call(self, func, args):
    env = Env(self.env)
    for name, v in zip(func.arg_names, args):
        env.set_value(name, v)

    self.push_p(env, func.p, func.ls)

v = self.exp_list()

    return v

def push_p(self, env, p, ls):
    self.env = env
    self.pre_p = self.p
    self.p = p
    self.pre_ls = self.ls
    self.ls = ls

def pop_p(self):
    if self.env.parent == None:
        self.error('pop_p: no parent for env!')
    self.env = self.env.parent
    self.p = self.pre_p
    self.ls = self.pre_ls

写完后才知道, 真的… 不复杂!

函数可以返回

现在函数还是一直执行到结尾才返回, 我希望能够支持return语句, 原本考虑了半天, 不知道如何通过重重的函数调用, 返回到最上面, 后来经过 岚临 同学的提醒, 用抛出异常, 在上层捕捉的方式实现了, 加的代码只有4行!

语法:

exp = ltop | assignment | condition | loop | function | return
return_exp = return ltop

进到return_exp里面后, 会抛出ReturnException, 在function_call里面捕捉, 正好是当前调用函数的部分.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ReturnException(Exception):
    """for implement return expression"""
    def __init__(self, v):
        Exception.__init__(self)
        self.value = v

def function_call(self, func, args):
    try:
        v = self.exp_list()
        self.pop_p()
    except ReturnException as e:
        v = e.value

def exp(self):
    if self.has_next(1):
        ...
        elif self.get() == 'return':
            return self.return_exp()

def return_exp(self):
    self.inc()
    v = self.exp()
    self.pop_p()
    raise ReturnException(v)

结论

我一开始没有想到能够写那么多的, 结果一个语法一个语法加下来就变成这样了… 虽然没有性能优化, 以及其他的错误处理什么的东西, 能够写出一个简单的语言, 还是让我很有成就感的. 我不是一个很强的程序员, 也能写出一个简单的语言, 相信你也可以写一个来玩玩!

下一步: 虚拟机+字节码+编译 方式的语言, 实现协程+原生同步!

Python下使用epoll

| Comments

image

因为最近想学习如何用epoll写服务器, 于是找到了一篇介绍的文章. 因为我最近一直看不进技术文章, 于是打算通过翻译来强迫自己学习. 原文在这里: http://scotdoyle.com/python-epoll-howto.html

文章里面的代码可以 在这里下载.

介绍

从2.6版本开始, python 提供了使用linux epoll 的功能. 这篇文章通过3个例子来大致介绍如何使用它. 欢迎提问和反馈.

阻塞式socket通讯

第一个例子是一个简单的python3.0版本的服务器代码, 监听8080端口的http请求, 打印结果到命令行, 回应http response给客户端.

  • 行 9: 建立服务器的socket
  • 行 10: 允许11行的bind()操作, 即使其他程序也在监听同样的端口. 不然的话, 这个程序只能在其他程序停止使用这个端口之后的1到2分钟后才能执行.
  • 行 11: 绑定socket到这台机器上所有IPv4地址上的8080端口.
  • 行 12: 告诉服务器开始响应从客户端过来的连接请求.
  • 行 14: 程序会一直停在这里, 直到建立了一个连接. 这个时候, 服务器socket会建立一个新的socket, 用来和客户端通讯. 这个新的socket是accept()的返回值, address对象标示了客户端的IP地址和端口.
  • 行 15-17: 接收数据, 直到一个完整的http请求被接收完毕. 这是一个简单的http服务器实现.
  • 行 18: 为了方便验证, 打印客户端过来的请求到命令行.
  • 行 19: 发送回应.
  • 行 20-22: 关闭连接, 以及服务器的监听socket.

python官方 HOWTO 里面有具体如何使用socket编程的描述.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import socket

EOL1 = b'\n\n'
EOL2 = b'\n\r\n'
response  = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
response += b'Hello, world!'

serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(('0.0.0.0', 8080))
serversocket.listen(1)

connectiontoclient, address = serversocket.accept()
request = b''
while EOL1 not in request and EOL2 not in request:
    request += connectiontoclient.recv(1024)
print(request.decode())
connectiontoclient.send(response)
connectiontoclient.close()

serversocket.close()

第2个例子, 我们在15行加上了一个循环, 用来循环处理客户端请求, 直到我们中断这个过程(在命令行下面输入键盘中断, 比如Ctrl-C). 这个例子更明显地表示出来了, 服务器socket并没有用来做数据处理, 而是接受服务器过来的连接, 然后建立一个新的socket, 用来和客户端通讯.

最后的23-24行确保服务器的监听socket最后总是close掉, 即使出现了异常.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import socket

EOL1 = b'\n\n'
EOL2 = b'\n\r\n'
response  = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
response += b'Hello, world!'

serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(('0.0.0.0', 8080))
serversocket.listen(1)

try:
    while True:
        connectiontoclient, address = serversocket.accept()
        request = b''
        while EOL1 not in request and EOL2 not in request:
            request += connectiontoclient.recv(1024)
            print('-'\*40 + 'n' + request.decode()[:-2])
            connectiontoclient.send(response)
            connectiontoclient.close()
finally:
    serversocket.close()

异步socket和linux epoll的优势

第2个例子里面的socket采用的是阻塞方式, 因为python解释器在出现事件之前都处在停止状态. 16行的accept()一直阻塞, 直到新的连接进来. 19行的recv()也是一直阻塞, 直到从客户端收到数据(或者直到没有数据可以接收). 21行的send()也一直阻塞, 直到所有需要发送给客户端的数据都交给了linux内核的发送队列.

当一个程序采用阻塞socket的时候, 它经常采用一个线程(甚至一个进程)一个socket通讯的模式. 主线程保留服务器监听socket, 接受进来的连接, 一次接受一个连接, 然后把生成的socket交给一个分离的线程去做交互. 因为一个线程只和一个客户端通讯, 在任何位置的阻塞都不会造成问题. 阻塞本身不会影响其他线程的工作.

多线程阻塞socket模式代码清晰, 但是有几个缺陷, 可能很难确保线程间资源共享工作正常, 可能在只有一个CPU的机器上效率低下.

C10K(单机1万连接问题!) 探讨了其他处理并行socket通讯的模式. 一种是采用异步socket. socket不会阻塞, 直到特定事件发生. 程序在异步socket上面进行一个特定操作, 并且立即得到一个结果, 不管执行成功或者失败. 然后让程序决定下一步怎么做. 因为异步socket是非阻塞的, 我们可以不采用多线程. 所有的事情都可以在一个线程里面完成. 虽然这种模式有它需要面对的问题, 它对于特定程序来说还是不错的选择. 也可以和多线程合起来使用: 单线程的异步socket可以当作服务器上面处理网络的一个模块, 而线程可以用来访问阻塞式的资源, 比如数据库.

Linux 2.6有一些方式来管理异步socket, python API能够用的有3种: select, poll和epoll. epoll和poll比select性能更好, 因为python程序不需要为了特定的事件去查询单独的socket, 而是依赖操作系统来告诉你什么socket产生了什么事件. epoll比poll性能更好, 因为它不需要每次python程序查询的时候, 操作系统都去检查所有的socket, 在事件产生的时候, linux跟踪他们, 然后在python程序调用的时候, 返回具体的列表. 所以epoll在大量(上千)并行连接下, 是一种更有效率, 伸缩性更强的机制. 图示.

采用epoll的异步socket编程示例

采用epoll的程序一般这样操作:

  1. 建立一个epoll对象
  2. 告诉epoll对象, 对于一些socket监控一些事件.
  3. 问epoll, 从上次查询以来什么socket产生了什么事件.
  4. 针对这些socket做特定操作.
  5. 告诉epoll, 修改监控socket和/或监控事件.
  6. 重复第3步到第5步, 直到结束.
  7. 销毁epoll对象.

采用异步socket的时候第3步重复了第2步的事情. 这里的程序更复杂, 因为一个线程需要和多个客户端交互.

  • 行 1: select模块带有epoll功能
  • 行 13: 因为socket默认是阻塞的, 我们需要设置成非阻塞(异步)模式.
  • 行 15: 建立一个epoll对象.
  • 行 16: 注册服务器socket, 监听读取事件. 服务器socket接收一个连接的时候, 产生一个读取事件.
  • 行 19: connections表映射文件描述符(file descriptors, 整型)到对应的网络连接对象上面.
  • 行 21: epoll对象查询一下是否有感兴趣的事件发生, 参数1说明我们最多等待1秒的时间. 如果有对应事件发生, 立刻会返回一个事件列表.
  • 行 22: 返回的events是一个(fileno, event code)tuple列表. fileno是文件描述符, 是一个整型数.
  • 行 23: 如果是服务器socket的事件, 那么需要针对新的连接建立一个socket.
  • 行 25: 设置socket为非阻塞模式.
  • 行 26: 注册socket的read(EPOLLIN)事件.
  • 行 31: 如果读取事件发生, 从客户端读取新数据.
  • 行 33: 一旦完整的http请求接收到, 取消注册读取事件, 注册写入事件(EPOLLOUT), 写入事件在能够发送数据回客户端的时候产生.
  • 行 34: 打印完整的http请求, 展示即使通讯是交错的, 数据本身是作为一个完整的信息组合和处理的.
  • 行 35: 如果写入事件发生在一个客户端socket上面, 我们就可以发送新数据到客户端了.
  • 行s 36-38: 一次发送一部分返回数据, 直到所有数据都交给操作系统的发送队列.
  • 行 39: 一旦所有的返回数据都发送完, 取消监听读取和写入事件.
  • 行 40: 如果连接被明确关闭掉, 这一步是可选的. 这个例子采用这个方法是为了让客户端首先断开, 告诉客户端没有数据需要发送和接收了, 然后让客户端断开连接.
  • 行 41: HUP(hang-up)事件表示客户端断开了连接(比如 closed), 所以服务器这端也会断开. 不需要注册HUP事件, 因为它们都会标示到注册在epoll的socket.
  • 行 42: 取消注册.
  • 行 43: 断开连接.
  • 行s 18-45: 在这里的异常捕捉的作用是, 我们的例子总是采用键盘中断来停止程序执行.
  • 行s 46-48: 虽然开启的socket不需要手动关闭, 程序退出的时候会自动关闭, 明确写出来这样的代码, 是更好的编码风格.

例子3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import socket, select

EOL1 = b'\n\n'
EOL2 = b'\n\r\n'
response  = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
response += b'Hello, world!'

serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(('0.0.0.0', 8080))
serversocket.listen(1)
serversocket.setblocking(0)

epoll = select.epoll()
epoll.register(serversocket.fileno(), select.EPOLLIN)

try:
   connections = {}; requests = {}; responses = {}
   while True:
      events = epoll.poll(1)
      for fileno, event in events:
         if fileno == serversocket.fileno():
            connection, address = serversocket.accept()
            connection.setblocking(0)
            epoll.register(connection.fileno(), select.EPOLLIN)
            connections[connection.fileno()] = connection
            requests[connection.fileno()] = b''
            responses[connection.fileno()] = response
         elif event & select.EPOLLIN:
            requests[fileno] += connections[fileno].recv(1024)
            if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
               epoll.modify(fileno, select.EPOLLOUT)
               print('-'*40 + '\n' + requests[fileno].decode()[:-2])
         elif event & select.EPOLLOUT:
            byteswritten = connections[fileno].send(responses[fileno])
            responses[fileno] = responses[fileno][byteswritten:]
            if len(responses[fileno]) == 0:
               epoll.modify(fileno, 0)
               connections[fileno].shutdown(socket.SHUT_RDWR)
         elif event & select.EPOLLHUP:
            epoll.unregister(fileno)
            connections[fileno].close()
            del connections[fileno]
finally:
   epoll.unregister(serversocket.fileno())
   epoll.close()
   serversocket.close()

epoll有2种模式, 边沿触发(edge-triggered)和状态触发(level-triggered). 边沿触发模式下, epoll.poll()在读取/写入事件发生的时候只返回一次, 程序必须在后续调用epoll.poll()之前处理完对应事件的所有的数据. 当从一个事件中获取的数据被用完了, 更多在socket上的处理会产生异常. 相反, 在状态触发模式下面, 重复调用epoll.poll()只会返回重复的事件, 直到所有对应的数据都处理完成. 一般情况下不产生异常.

比如, 一个服务器socket注册了读取事件, 边沿触发程序需要调用accept建立新的socket连接直到一个socket.error错误产生, 然后状态触发下只需要处理一个单独的accept(), 然后继续epoll查询新的事件来判断是否有新的accept需要操作.

例子3采用默认的状态触发模式, 例子4展示如何用边沿触发模式. 例子4中的25, 36和45行引入了循环, 直到错误产生(或者所有的数据都处理完了), 32, 38 和48行捕捉socket异常. 最后16, 28, 41 和51行添加EPOLLET mask用来设置边沿触发.

例子4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import socket, select

EOL1 = b'\n\n'
EOL2 = b'\n\r\n'
response  = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
response += b'Hello, world!'

serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(('0.0.0.0', 8080))
serversocket.listen(1)
serversocket.setblocking(0)

epoll = select.epoll()
epoll.register(serversocket.fileno(), select.EPOLLIN | select.EPOLLET)

try:
   connections = {}; requests = {}; responses = {}
   while True:
      events = epoll.poll(1)
      for fileno, event in events:
         if fileno == serversocket.fileno():
            try:
               while True:
                  connection, address = serversocket.accept()
                  connection.setblocking(0)
                  epoll.register(connection.fileno(), select.EPOLLIN | select.EPOLLET)
                  connections[connection.fileno()] = connection
                  requests[connection.fileno()] = b''
                  responses[connection.fileno()] = response
            except socket.error:
               pass
         elif event & select.EPOLLIN:
            try:
               while True:
                  requests[fileno] += connections[fileno].recv(1024)
            except socket.error:
               pass
            if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
               epoll.modify(fileno, select.EPOLLOUT | select.EPOLLET)
               print('-'*40 + '\n' + requests[fileno].decode()[:-2])
         elif event & select.EPOLLOUT:
            try:
               while len(responses[fileno]) > 0:
                  byteswritten = connections[fileno].send(responses[fileno])
                  responses[fileno] = responses[fileno][byteswritten:]
            except socket.error:
               pass
            if len(responses[fileno]) == 0:
               epoll.modify(fileno, select.EPOLLET)
               connections[fileno].shutdown(socket.SHUT_RDWR)
         elif event & select.EPOLLHUP:
            epoll.unregister(fileno)
            connections[fileno].close()
            del connections[fileno]
finally:
   epoll.unregister(serversocket.fileno())
   epoll.close()
   serversocket.close()

因为比较类似, 状态触发经常用在转换采用select/poll模式的程序上面, 边沿触发用在程序员不需要或者不希望操作系统来管理事件状态的场合上面.

除了这两种模式以外, socket经常注册为EPOLLONESHOT event mask, 当用到这个选项的时候, 事件只有效一次, 之后会自动从监控的注册列表中移除.

性能考虑

Listen Backlog Queue Size

在1-4的例子中, 12行显示了调用serversocket.listen()方法. 参数是监听等待队列的大小. 它告诉了操作系统, 在python代码accept前, 缓存多少TCP/IP连接在队列中. 每次python代码调用accept()的时候, 一个连接从队列中移除, 为新的连接进来空出一个位置. 如果队列满了, 新的连接自动放弃, 给客户端带来不必要的网络延迟. 一个生产环境下的服务器经常处理几十或者几百的同时连接数, 所以参数不应该设置为1. 比如, 当采用 ab 来对这些测试程序进行并发100个http1.0客户端请求时, 少于50的参数容易造成性能下降.

TCP Options

TCP_CORK 参数可以设置缓存消息直到一起被发送, 这个选项, 在例子5的34和40行, 适合给一个实现 http/1.1pipelining 的服务器来使用.

例子5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import socket, select

EOL1 = b'\n\n'
EOL2 = b'\n\r\n'
response  = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
response += b'Hello, world!'

serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(('0.0.0.0', 8080))
serversocket.listen(1)
serversocket.setblocking(0)

epoll = select.epoll()
epoll.register(serversocket.fileno(), select.EPOLLIN)

try:
   connections = {}; requests = {}; responses = {}
   while True:
      events = epoll.poll(1)
      for fileno, event in events:
         if fileno == serversocket.fileno():
            connection, address = serversocket.accept()
            connection.setblocking(0)
            epoll.register(connection.fileno(), select.EPOLLIN)
            connections[connection.fileno()] = connection
            requests[connection.fileno()] = b''
            responses[connection.fileno()] = response
         elif event & select.EPOLLIN:
            requests[fileno] += connections[fileno].recv(1024)
            if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
               epoll.modify(fileno, select.EPOLLOUT)
               connections[fileno].setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK, 1)
               print('-'*40 + '\n' + requests[fileno].decode()[:-2])
         elif event & select.EPOLLOUT:
            byteswritten = connections[fileno].send(responses[fileno])
            responses[fileno] = responses[fileno][byteswritten:]
            if len(responses[fileno]) == 0:
               connections[fileno].setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK, 0)
               epoll.modify(fileno, 0)
               connections[fileno].shutdown(socket.SHUT_RDWR)
         elif event & select.EPOLLHUP:
            epoll.unregister(fileno)
            connections[fileno].close()
            del connections[fileno]
finally:
   epoll.unregister(serversocket.fileno())
   epoll.close()
   serversocket.close()

另一方面, TCP_NODELAY 可以用来告诉操作系统, 任何发给socket.send()的数据必须不经过操作系统的缓存, 立刻发送给客户端.

这个选项, 在第6个例子的14行, 可以给SSH客户端或者其他实时性要求比较高的应用来使用.

例子6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import socket, select

EOL1 = b'\n\n'
EOL2 = b'\n\r\n'
response  = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
response += b'Hello, world!'

serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(('0.0.0.0', 8080))
serversocket.listen(1)
serversocket.setblocking(0)
serversocket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

epoll = select.epoll()
epoll.register(serversocket.fileno(), select.EPOLLIN)

try:
   connections = {}; requests = {}; responses = {}
   while True:
      events = epoll.poll(1)
      for fileno, event in events:
         if fileno == serversocket.fileno():
            connection, address = serversocket.accept()
            connection.setblocking(0)
            epoll.register(connection.fileno(), select.EPOLLIN)
            connections[connection.fileno()] = connection
            requests[connection.fileno()] = b''
            responses[connection.fileno()] = response
         elif event & select.EPOLLIN:
            requests[fileno] += connections[fileno].recv(1024)
            if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
               epoll.modify(fileno, select.EPOLLOUT)
               print('-'*40 + '\n' + requests[fileno].decode()[:-2])
         elif event & select.EPOLLOUT:
            byteswritten = connections[fileno].send(responses[fileno])
            responses[fileno] = responses[fileno][byteswritten:]
            if len(responses[fileno]) == 0:
               epoll.modify(fileno, 0)
               connections[fileno].shutdown(socket.SHUT_RDWR)
         elif event & select.EPOLLHUP:
            epoll.unregister(fileno)
            connections[fileno].close()
            del connections[fileno]
finally:
   epoll.unregister(serversocket.fileno())
   epoll.close()
   serversocket.close()

源码

本程序的源码属于共有领域, 在这里下载.

夏日重乐季现场

| Comments

image

我最早开始听死亡金属可能是大学的时候, 到现在也有一段时间了, 但是听的机会不是特别多. 喜欢是喜欢, 但是也没有喜欢到fans的程度. 最近看到豆瓣友邻分享出来的旋律死亡金属的现场演出: http://www.douban.com/event/14129983/discussion/ 我觉得作为一个sensates, 不能错过这样的机会.

于是我天昨天(2011.07.16)就去了. 时间是9:00 – 12:00. 我8:30赶到举办场地育音堂, 门口很多人都到了. 这些人给我的感觉, 怎么说呢, 就是金属fans. 大部分人都穿黑衣T-shirt, 上面印着金属厂牌的名称, 有看到arch enemy, 霜冻前夜等. 我还看到有一位同学T-shirt上面写着大字: 我不是党员. 这种衣服我是不敢穿着上街的. 很多留长头发的, 很多纹身的, 偶尔见到穿孔的同学. 竟然有很多的女生, 真不可思议. 可能因为是旋律死亡吧, 群众接受程度比较高.

然后我们预售票的同学就先进场了, 场地不大, 就是一个普通的酒吧的大小. 刚好放下5人组合的舞台, 以及正对面的一块空地. 我找了一个好位置, 舞台侧方向, 地板是垫高的, 有高座位. 人陆续进场, 然后, 就爆满了. 空地上面挤满了人, 边上, 楼梯上也都是人. 应该有300人吧.

过了一段时间, 我和周围人零散地说了几句话之后, 就开场了. 我其实听得少, 对乐队不熟悉. 他们说这3个乐队都比较有名. 当然最有名的是霜冻前夜. 第一场应该是惊叫基督, 主唱应该是死腔. 死亡金属一开始就是暴躁起来的, 我一会就适应了.

靠近舞台的观众应该都是真正的fans吧, 都比较high. 随着音乐跳动, 甩头. 我开场后就站在凳子上面看, 从我的角度上看上去, 就是真正的人肉构成的海洋和波浪. 后面的人搭着前面人的肩膀, 低下头, 随着音乐甩. 然后人们撞来撞去, 时不时有人爬上舞台, 背往后倒下, 然后人们就把他举在头顶上推来推去.

后面的人都不怎么活跃, 头也不甩. 让我一点感觉都没有, 于是我就在第二场Fearless的时候就跑到前面去了. 很快气氛就上来了. 人挤在一起, 互相碰撞, 甩头, 搭在别人肩膀上低头甩. 非常high. 前面的人身高都超过了1米7, 矮个的人在这里是有危险的. 乐手看起来感觉挺高兴的, 估计是第一次见到这么多人吧?

汗流浃背加上体力不支加上鞋子被踩烂了之后, 第三场我退了回来. 来现场的外国人也很多, 妹子也很多. 站我前面的妹子一直非常high, 几乎一直都在跳着. 到了第三场, 后面的人也渐渐气氛起来了.

最后到12点结束了, 我带着一身臭汗回去了. 参加现场没有像我认为的那么兴奋, 但是体验过后感觉也还不错. 和人拼车回去的时候聊天, 他说上海的金属气氛很差, 今天来了很多人, 大多数还是凑热闹的. 我也觉得金属还是北京才是核心.

ps: 等其他人的照片和视频放出来的时候我再补充上吧.

爱抄才会赢

| Comments

image

现在中国因为山寨而闻名, 不管国外有什么东西, 国内就有山寨的产物出来. 就从电子产品领域来说吧, 自从有了ipad, 无数平板拔地而起, 无数的山寨iphone, 以及其他种种设备. 至于其他领域也不用说了, 大家都比我清楚.

现在很多人认为这个不是什么好事, 都是抄别人的产品, 名声不好. 也不是什么创新产业, 利润也不高. 我个人有一个看法, 山寨是中国的希望. 为什么呢?

首先, 以前的中国是封闭于世界的, 什么都不会, 什么东西都做不出来. 后来搞特区, 搞工厂, 廉价劳动力引进过来国外的技术, 搞外包. 这一步走得非常对. 因为什么都不会, 只有出卖现在有的东西, 比如劳动力什么的, 才能过日子. 并且在这个外包的过程中, 可以掌握国外的技术. 中国人是非常聪明的, 那点东西还怕学不会.

然后必然会产生下面一个结果: 学会了技术, 不愿意单纯出卖劳动力了. 于是自己出来做东西. 但是这个阶段我们只会做东西, 设计什么的都是在国外, 抄不到. 于是现在的状况就是到处都是山寨的东西出来. 这是一个非常好的现象, 说明大家学到手艺了.

然后, 随着有手艺的人越来越多, 能做设计的人也会从中产生, 因为一方面有这个需求了, 山寨企业有了多年的积累, 以及为了赚取更多的利润, 就会想办法往上游走. 正如我们前面说的, 中国人非常聪明, 学什么都非常快, 所以… 现在很多服装行业都有自有品牌, 其实是靠着多年的积累的. 我很是反对腾笼换鸟的说法, 没了这个基础, 怎么发展上层建筑?

山寨是一个非常好的东西, 因为山寨其实是一个学习的过程, 如果从头摸索, 那么消耗的人力物力是无法衡量的. 而直接抄, 至少大致是不离谱的. 避免了无数自己摸索可能出现的问题, 也能少走无数的弯路. 别人都搭好桥了, 我们就不要摸着石头过河了.

我觉得现在主要的问题不是我们抄不抄的问题, 而是抄不像的问题. 只是抄了一个产品的外表, 精神上的东西没有抄到. 我们现在那么多抄iphone的手机, 哪个的用户体验上面能和iphone比? 我们现在抄的那么多网站, 哪个是按照那个被抄的网站原有的思路走的? 所以我觉得现在的问题不是抄得太多了, 而是抄得还不够多, 还不够好.

现在这么多山寨公司的人, 你们不要太妄自菲薄, 以为自己做的是多么不好的一件事. 因为抄是学习的过程, 你们应该抄得更像, 把对方所有的东西都抄过来, 而不是只抄形不抄神, 这样你们才能作出世界级的产品出来. 而世界也会因为你们的产品而得到改变.

(ps: 本文回避了知识产权的问题, 那又是另外一个问题了, 等以后有时间讨论.)

兔子流

| Comments

image

hello everyone, today I will introduce you an new kind of science fiction, we call it “兔子流” in Chinese, translate as “rabbit like”.

what is “兔子流”?

In the ancient old days, a writer, name as “小学生”, translate as “schoolchildren”, he write a fiction, name as “兔子尾巴”, and with this characteristic:

  • main character(young adult male) have super power.
  • have no sense of moral.
  • enjoy play around the world.

what is the detail?

for AD&D fans, it is a book about a chaos evil character, how he do the dirty things. here are some example:

  • he kills people for fun.
  • he has one or severial girls as toy.
  • enjoy the life of doing whatever he wants, if he like something, just take it.
  • have sick sense of humor and “moral”, he thinks he is doing something good which actually really bad, such as kill raped girl only because he think the girl will have a bad life in the future.
  • he make social experiment. for maintaining good traffic condition, he make laws that driver can hit anything on the road… even the kid.
  • no friend, no family, no strong feeling except fearing of death.

examples

here are some fiction belows to “兔子流”, and you can google them, because it is so evil that no static links I can provide:

  • 兔子尾巴
  • 人民公敌
  • 我无恶不作
  • 兔子流屠龙纪要

conclusion

“兔子流” is one of the extrime fiction types I enjoy very much, easy to read, and have more fun. currently no new fiction is out, I hope in the future there can be more.

Be_a_better_man

| Comments

image

If one person knows how he spend his life, he will be in shock:

he spent 8 hours a day at work, only 1-2 hours (even less!) were for the jobs really metters. and he lost almost all the time after work: playing game, watch TV, surfing the internet. and so this is a day. a day without perception, a day without plan, and it all coursed by a man without knowning what he is doing.

most of us is this man. is is sad. and on the opposite, some great people have causion, they know what they what to, and knows it so well that for every minute (or at least when he is planning to do something), he can focus on the important things. by doing this, they have better lifes.

so how could we be one of those “great people”? are there anything magical? no. there are some simple rules:

rule 1: plan before you start.

we always eager to start doing things right away, it is human nature, our brain works this way: doing things make ourselves feel better. and planning is hard, make ourselves feel bad. but the issue is: plannig is hard because you don’t really know what you starts! and without knowing the full picture may lead to failure. so make a plan, it is your road map. without it, you will easily get lost.

rule 2: start and finish what you plans.

people controlled by their feelings. things have good part and bad part. we always lost the battle. we always retreat, if we don’t have faith. if planning is like a general, analysis, judges, and make decisions, doing things is like a soldier. not care about how target is hard to accomplish, only concentrate on finish it.

rule 3: examine what you finished.

after finish the job, people just passing through, the job has the feeling of pain, why not folder it, and leave it? the reason is, facing the pain, make we do better in the future, and the job is our a reflection of ourselves, examine it make ourselves better. so it is important.

those are the simple rules. easy to understand, hard to follow. it need disciprine, it need will power to follow. there is no easy way, maybe there are some tips that can make it easier, but it still need you to face the problem.

anyway, hope you can follow it, and be a better man.