sunng’s pastebin

写了一个基本的pastebin放在appengine上:
http://sunoffline.appspot.com/pb/

支持纯文本、Markdown和代码高亮。数据永久保留,推荐大家收藏以备不时之需。

The post is brought to you by lekhonee v0.7

Posted in: 广告 by Sunng No Comments ,

用markdown书写文档

写文档是头疼事,没人愿意写文档。在word里写文档,时间长了都怀疑自己是不是搞技术的。我阅历有限,在我的印象里就没有用word格式看过什么有价值的东西。只要一打开word想到的就是连篇累牍的废话、码字。word最重要的功能是什么?保存。其次呢?字数统计。我所写过的word文档绝大多数都是“只写”的,通常作为流程里的一个附件,没有人真正去看。

以上是人身攻击,接下来比较实际的问题,word文档不是符合unix哲学的东西。格式不开放,你就没有办法进行diff操作,把word文档放到svn里,只能使用最基本的版本控制,没法查看changeset,只知道改了,不知道改了什么。

为了改变这种情况,我试过用docbook格式。docbook用xml书写,定义了一套复杂的Schema,详细到作者的email都有定义。docbook还有丰富的工具集,可以通过xsl把docbook转换成所有你知道的文档格式。Maven: the Definitive Guide就是用docbook写成的。不过用docbook也存在一些问题,docbook太复杂了,用纯文本编辑器很难处理,作者的学习曲线也比较高,需要所见即所得的编辑器支持。与docbook相类似的DITA,也存在这样的问题,它们是重量级的格式。

轻量级的Wiki格式不错,但是Wiki格式很让人头疼就是没有统一的规范。举例,dokuwiki里顶级标题是6个=,而moinmoin里顶级标题恰恰相反是一个=,不portable,文档维护起来就非常麻烦。

铺垫了这么多,委屈以上格式了,该markdown出场了。markdown是简单的html原型,用来生成html,它的设计目标就是为了KISS,兼容html。看看一些必要的格式吧:(或者直接看Wikipedia
标题
# 标题
## 二级标题

###### 六级标题

对于一级标题还可以这么写
headline
========
二级标题可以
headline
——–
这个怎么输入呢,我想起来前几天看vim hacks里的一组快捷键
yypoVr=
yypoVr-
谁用谁知道

段落:
一段文本以两个换行结束。
换行:
一行文本行末两个空格。

图片
![alternative text](image-url “image-title”)
用markdown,图片的alt你不写都不行。

链接
[Linktext](link “linktitle”)

列表
ul 无序列表
*
*
ol 有序列表
1.
2.

以上就是主要的格式支持。用标题来划分文档层次,没有多余的格式,没有机会让你五颜六色。

在linux上可以安装markdown的处理脚本:
apt-get install markdown
安装vim的语法文件:

http://www.vim.org/scripts/script.php?script_id=1242

这里是一个简单的例子:

http://github.com/sunng87/exaile-doubanfm-plugin/blob/master/README.mkd

The post is brought to you by lekhonee v0.7

Posted in: 装备 by Sunng 3 Comments ,

bottle & fapws3

Bottle是一个Python web框架,兼容wsgi标准,lightweight,self-contained。

提到web框架,自然要和相类似的python框架相比。

Django是大型框架,包含ORM、Controller、Templating全套,这也是Django的缺点,使用Django意味着必须使用关系型数据库进行存储(尽管有一些Model层的其他实现,但绝大多数都是Hack的方式实现),必须使用Django并不非常出色的Template系统。Pylons针对Django的这些问题,采用了松散的方式,数据层可选择由SQLAlchemy实现,模板系统可以选择mako / jinja等。Pylons用paster来管理项目、创建代码模板。借鉴了rails的哲学,目录结构也相类似。可是pylons仍然显得重量级,把注意力放到web.py。只要定义一个router,定义相应的handler就可以处理web请求,handler对象的GET POST等方法分别对应相应的HTTP请求。看起来不错了,不过与bottle相比,webpy仍然显得繁琐、功能有限,而且它本身的db模块就更加鸡肋了。

看一个实例便知:
定义一个简单的HTTP页面:

from bottle import Bottle, run, mako_view, request
from bottle import FapwsServer

myapp = Bottle()

@myapp.route('/nihao/:name/:count#\\d+#')
@mako_view('nihao')
def nihao(name, count):
    return dict(n=name, c=int(count), ip=request.environ.get('REMOTE_ADDR'))

run(app=myapp, server=FapwsServer)

对应的nihao.tpl模板,用mako引擎实现:

<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">

    <title>Nihao</title>

</head>
<body>
    <div id="ip">
        <h1 id="heading">Request From ${ip}</h1>
    </div>
    <%def name="greeting(n)">
        <div id="name">
            Nihao ${n}
        </div>
    </%def>
    % for i in range(c):
        ${greeting(n)}
    % endfor
</body>
</html>

仅仅是一些简单的内容,接着只需要:
python bottle-test.py
即可运行服务器。

bottle通过decorator定义route规则,还支持url提取参数。通过decorator指定模板、模板引擎。可以说近乎简化到了极致。

bottle遵循单一职责原则,不提供数据层的实现,由用户自己指定,bottle没有任何限制。模板引擎,bottle支持mako / jinja / cheetah,本身还内建一个默认SimpleTemplate引擎。bottle还支持多种wsgi服务器,包括flup / wsgiref / cherrypy / paste / twisted / tornado / fapws3 等等。

最后提一个wsgi的实现fapws3,号称是目前最快的wsgi服务器。fapws3用libev实现,在不同的操作系统上采用不同的多路IO模型以达到高性能。

bottle的作者做过一个关于不同实现的性能比较:
http://bottle.paws.de/page/2009-12-19_Comparing_HelloWorld_Performance

The post is brought to you by lekhonee v0.7

Posted in: 装备 by Sunng No Comments , , ,

Bayeux Protocol

运行一个CometD Demo非常简单,只要创建一个Maven项目即可(CometD Howtos):
$ mvn archetype:generate -DarchetypeCatalog=http://cometd.org

maven会提示用户选择archetype,包括cometd的版本1、版本2,jetty6、jetty7的实现,以及客户端dojo或jquery的实现。这里可以选择最新的:
http://cometd.org -> cometd-archetype-dojo-jetty7 (2.0.0 – CometD archetype for creating a server-side event-driven web application)

项目创建完成后执行mvn jetty:run即可,打开http://127.0.0.1:8080/{artifactId}即可。

CometD的协议包容了各种主要的浏览器,比如在Chromium 5上,dojo采用WebSocket实现;而在不支持WebSocket的Firefox 3上,通过long-polling实现。Bayuex是一个应用协议,CometD是Bayuex的实现,类似鸡与蛋的关系。

有了昨天在Chromium上看WebSocket协议的经验,先看一下CometD的WebSocket实现:
握手。客户端请求/{artifactId}/cometd/handshake
包含Header

GET /cometd-jetty/cometd/handshake HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: 127.0.0.1:8080
Origin: http://127.0.0.1:8080
Cookie: JSESSIONID=12jqq6hbsfkfic8vzqpevxtrw

这是标准的WebSocket握手协议,服务端返回:

HTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://127.0.0.1:8080
WebSocket-Location: ws://127.0.0.1:8080/cometd-jetty/cometd/handshake

双方完成WebSocket连接的建立。客户端通过websocket发送JSON,进行bayuex的握手:

[{"version":"1.0","minimumVersion":"0.9","channel":"/meta/handshake","supportedConnectionTypes":["websocket","long-polling","callback-polling"],”advice”:{“timeout”:60000,”interval”:0},”id”:”1″}]

服务端返回JSON,下发clientId完成握手:

[{"channel":"/meta/handshake","clientId":"8g6dbnlqr2k6jfo1tdpaeb7iw","version":"1.0","successful":true,"minimumVersion":"1.0","id":"1","supportedConnectionTypes":["websocket","long-polling","callback-polling"]}]

握手完成,bayuex连接建立。

在Demo中,客户端添加了一个handshake的listerner

    function _metaHandshake(handshake)
    {
        if (handshake.successful === true)
        {
            cometd.batch(function()
            {
                cometd.subscribe('/hello', function(message)
                {
                    dojo.byId('body').innerHTML += '<div>Server Says: ' + message.data.greeting + '</div>';
                });
                // Publish on a service channel since the message is for the server only
                cometd.publish('/service/hello', { name: 'World' });
            });
        }
    }

所以在完成握手后,客户端发送一个批量请求,subscribe /hello频道,并且向/service/hello发送json格式的消息。向/service channel发送的信息表示客户端与服务端的单独通信,不会被转发给其他客户端。
id用于区分每个请求,bayuex spec规定向/meta和/service发送的请求必须包含id字段,用于标示请求响应。
请求的内容最终聚合为一个Json

[{"channel":"/meta/subscribe","subscription":"/hello","id":"2","clientId":"8g6dbnlqr2k6jfo1tdpaeb7iw"},{"channel":"/service/hello","data":{"name":"World"},"id":"3","clientId":"8g6dbnlqr2k6jfo1tdpaeb7iw"}]

服务端发回响应,id=2的请求成功,订阅/hello频道成功

[{"channel":"/meta/subscribe","successful":true,"id":"2","subscription":"/hello"}]

之后,服务端发回/hello channel的消息

[{"channel":"/hello","data":{"greeting":"Hello, World"}},{"channel":"/service/hello","successful":true,"id":"3"}]

客户端还要定期发送连接请求保持连接

[{"channel":"/meta/connect","connectionType":"websocket","advice":{"timeout":0},"id":"4","clientId":"8g6dbnlqr2k6jfo1tdpaeb7iw"}]

服务端返回,连接成功

[{"channel":"/meta/connect","advice":{"reconnect":"retry","interval":2500,"timeout":15000},"successful":true,"id":"4"}]

connect请求是用于在客户端和服务端维持连接, Bayeux标准中提到(1, 2):

A transport MUST maintain one and only one outstanding connect message. When a HTTP response that contains a /meta/connect response terminates, the client MUST wait at least the interval specified in the last received advice before following the advice to reestablish the connection

The client MUST maintain only a single outstanding connect message. If the server does not have a current outstanding connect and a connect is not received within a configured timeout, then the server SHOULD act as if a disconnect message has been received.

至此,cometd客户端就可以在/hello频道上订阅、发布消息了。
在Chromium上,所有的操作都在一个WebSocket连接上完成。

而当断开连接时,客户端向服务端发送

[{"channel":"/meta/disconnect","id":"188","clientId":"a8iutjvfp7dtwhzrfujeonk5q"}]

服务端响应

[{"channel":"/meta/disconnect","successful":true,"id":"188"}]

Bayuex基本上就可以理解为一个websocket上的应用协议了。

再看看Firefox 3.6上的实现。Firefox 3.6不支持WebSocket,所有的通信只能通过XHR来实现。
握手,通过一个xhr post请求实现:

POST /{artifactId}/cometd/handshake HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: UTF-8,*
Keep-Alive: 115
Connection: keep-alive
Content-Type: application/json;charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: http://127.0.0.1:8080/{artifactId}/
Content-Length: 182
Cookie: JSESSIONID=fjnyxb28raih1cnaljrijl1ic
Pragma: no-cache
Cache-Control: no-cache

服务器端响应:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Set-Cookie: BAYEUX_BROWSER=df92-h8q89f416mutgbpxrwb8185u;Path=/
Content-Length: 213
Server: Jetty(7.1.5.v20100705)

[{"channel":"/meta/handshake","clientId":"9185k23lo482oq1po3ivxup2cj","version":"1.0","successful":true,"minimumVersion":"1.0","id":"1","supportedConnectionTypes":["websocket","long-polling","callback-polling"]}]

握手完成,执行客户端定义的回调。发送bayeux请求,通过一个新的XHR上
[{"channel":"/meta/subscribe","subscription":"/hello","id":"2","clientId":"9185k23lo482oq1po3ivxup2cj"},{"channel":"/service/hello","data":{"name":"World"},"id":"3","clientId":"9185k23lo482oq1po3ivxup2cj"}]

服务端同时返回三个bayuex的请求响应

[{"channel":"/meta/subscribe","successful":true,"id":"2","subscription":"/hello"},{"channel":"/hello","data":{"greeting":"Hello, World"}},{"channel":"/service/hello","successful":true,"id":"3"}]

客户端开始发送连接请求

[{"channel":"/meta/connect","connectionType":"long-polling","advice":{"timeout":0},"id":"4","clientId":"9185k23lo482oq1po3ivxup2cj"}]

注意这里使用的是long-polling方式,这是由dojo针对浏览器特性决定的。

Long-polling server implementations attempt to hold open each request until there are events to deliver; the goal is to always have a pending request available to use for delivering events as they occur, thereby minimizing the latency in message delivery.

如果没有新消息,服务端阻塞十秒后返回

[{"channel":"/meta/connect","successful":true,"id":"7"}]

客户端接收到返回立刻发起新的connect请求

当有新消息时,阻塞在服务器端的connect请求会立即返回,同时带回新的消息,如

[{"channel":"/hello","data":{"name":"555"},"id":"6"},{"channel":"/meta/connect","successful":true,"id":"619"}]

而如果是本客户端publish的新消息,会在请求成功的响应中返回,不会影响connect连接,如:

[{"channel":"/hello","data":{"name":"nihao"},"id":"715"},{"channel":"/hello","successful":true,"id":"715"}]

断开时,仍然是通过xhr post一条bayuex命令到服务端

[{"channel":"/meta/disconnect","id":"750","clientId":"9185k23lo482oq1po3ivxup2cj"}]

服务端响应:

[{"channel":"/meta/disconnect","successful":true,"id":"750"}]

至此,通过long polling方式实现bayuex的cometd客户端也描述清楚了。long-polling仍然是通过connect请求来实现pull的方式准实时,与websocket真正push的方式还是存在区别的。

The post is brought to you by lekhonee v0.7

Posted in: 把戏 by Sunng 1 Comment , , ,

Websocket Protocol

下午用jetty的WekSocketServlet写了一个简单的WebIM程序,正好第一次瞥见WebSocket的狰容。

服务器端
jetty 7.1.5
客户端
Chromium 5.0.375.86

通过wireshark抓包获得这样一些数据:
var _ws = new WebSocket(“ws://127.0.0.1:8080/nothing”)
这个环节创建WebSocket,浏览器与服务器端进行handshake,发送请求

GET /nothing HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: 127.0.0.1:8080
Origin: http://127.0.0.1:8080

客户端发出一个Upgrade头,upgrade头在RFC2616 14.42定义

The Upgrade general-header allows the client to specify what additional communication protocols it supports and would like to use if the server finds it appropriate to switch protocols.

Upgrade必须被放入Connection头中标示这是一个Upgrade请求
Connection定义在RFC2616 14.10中:

The Connection general-header field allows the sender to specify options that are desired for that particular connection and MUST NOT be communicated by proxies over further connections.

Origin头还没有进入RFC,他的标准草案可以在这里找到,W3C的标准草案Cross-Origin Resource Sharing定义Origin Header:

The Origin header indicates where the cross-origin request or preflight request originates from.

Origin头的提出是为了解决CSRF的潜在危险,通过Origin服务器端可以获知请求的来源,进而判断其合法性。也就是说将跨域安全性检查的责任交给了服务器端,浏览器端采取信任的策略,避免了原先对跨域一棍子打死的做法。
Jetty 7的org.eclipse.jetty.servlets.CrossOriginFilter对这个头进行了处理。

此外,handshake请求的header中还允许一个Sec-WebSocket-Protocol,用于对服务器端指定一个子协议(应用协议)。

服务器端应答

HTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://127.0.0.1:8080
WebSocket-Location: ws://127.0.0.1:8080/nothing

Websocket连接建立。此后,服务器端和客户端可以实现bidirectional的通信,消息体即websocket.send(msg)中的纯文本。要实现这样的机制,浏览器和服务器间需要建立至少两个连接。目前,WebSocket协议中还没有规定客户端对服务器端的连接数限制。不过关于这个限制,RFC2616(HTTP1.1)中规定

Clients that use persistent connections SHOULD limit the number of simultaneous connections that they maintain to a given server. A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy.

对此,另一个Bayeux协议倒是已经有了明确的限制:

the Bayeux protocol MUST NOT require any more than 2 HTTP requests to be simultaneously handled by a server in order to handle all application (Bayeux based or otherwise) requests from a client.

到此,客户端和服务器端已经可以建立双工的通信,这也是浏览器级别实现WebSocket协议的最大优点。而对于Firefox 3.x, IE x.x等等,只能在现有的HTTP连接机制上实现WebSocket,如通过long polling和callback polling的方式,但终归无法实现真正双工的通信。

The post is brought to you by lekhonee v0.7

Posted in: 把戏 by Sunng 1 Comment , , ,

clojure recur

clojure是函数是编程语言,本身没有循环的语法。要实现类似循环的效果,需要用递归来实现。比如计算从1加到n的和这样一个函数:

(defn addup
    ([limit] (addup limit 0 0))
    ([limit current sum]
        (if (< limit current)
            sum
            (addup limit (+ 1 current) (+ current sum)))))

调用

(addup 10)

可以得到55

但是调用

(addup 50000)

不出意外,出现了StackOverflowException, 这是递归典型的错误。

为此,需要进行优化。尾递归优化是指如果递归满足一定条件,编译器可以将递归展开,避免出现栈溢出的问题。这里的条件,就是递归的最后一次调用是否是其本身,比如上面的代码,最后一次操作仍然是addup,即满足尾递归的条件,而如果同样的代码写成:

(defn addup2
    ([limit] (addup2 limit 0))
    ([limit current]
        (if (< limit current)
            0
            (+ current (addup2 limit (+ current 1))))))

最后一个操作是+,不满足尾递归循环,无法进行优化。

clojure默认不进行尾递归优化,需要使用关键字recur来保证优化,把第一段代码写成:

(defn addup
    ([limit] (addup limit 0 0))
    ([limit current sum]
        (if (< limit current)
            sum
            (recur limit (+ 1 current) (+ current sum)))))

社区的解释是,如果程序员在无法进行尾递归优化的代码中使用recur会发生报错,这样有助于他们发现代码中潜在的缺陷。

在第二段代码中使用recur,会得到异常:

java.lang.UnsupportedOperationException: Can only recur from tail position

此外,clojure提供loop函数简化书写:

(defn addup3
    ([limit]
    (loop [current 0 sum 0]
        (if (< limit current)
            sum
            (recur (+ current 1) (+ current sum))))))

这种写法同样可以应用尾递归优化而不会导致栈溢出。

The post is brought to you by lekhonee v0

Posted in: 手艺 by Sunng 2 Comments

fixing "libmozjs" missing when using mongodb on Ubuntu lucid

Problem
When running mongod/mongo/mongos, you got message like this:
mongod: error while loading shared libraries: libmozjs.so: cannot open shared object file: No such file or directory

Solution
Make sure you have xulrunner-dev installed:
sudo apt-get install xulrunner-dev

then find libmozjs on your filesystem:
sudo locate libmozjs

in lucid, it’s supposed to locate at:
/usr/lib/xulrunner-1.9.2.6/libmozjs.so

(and some other directories, such as firefox / thunderbird / seamonkey)

Just create a symbol link:
sudo ln -s /usr/lib/xulrunner-1.9.2.6/libmozjs.so /usr/lib/

try to restart mongodb:
sudo service mongodb start

take a look at process list:
ps aux | grep mongo

it works.

The post is brought to you by lekhonee v0.7

Posted in: 把戏 by Sunng 4 Comments ,

Update

  1. 搬家了,搬了一条街,从马路这边搬到马路那边。从二楼搬到四楼,告别了潮湿发霉的小屋,换了新环境迎接工作第二年。本来想搬出这个玉兰香苑,但是看了一圈不是太远就是太贵。都说现在是不炒房价改炒房租了,别的不知道,反正我原来那屋房租就涨了200,而且我搬出去那天就租出去了。
  2. 妈妈来上海,晚上可以一起玩桌游。妈妈对卡卡松还挺感兴趣的,我们一开始玩基础版,然后加河流扩展、旅馆教堂扩展。卡卡松绝对是家庭娱乐的好工具,各位常年在外的童鞋休假回家可以带一个和父母一起玩,调节气氛,增进和谐,居家旅行,必备良方。
  3. 周五盛大创新院,人间网介绍mongodb实践。人间网的用户不多,数据量不大,mongodb用来存储timeline里的status id,至今有不到900M的数据和900M的索引。交流活动持续一个小时,最火爆的环节是Q&A。人间网用mongodb的经验也不是非常丰富,很多问题也解释不清楚,最后多亏了我们组一位上海程序员界的新星救场,除了解决了四面八方有意义无意义的问题以外,也引来了全场的叹为观止,以至于都有人说这是SDO的人嘛。其实不完全是,其实人家才大三。
  4. 工作一年了,下周安静下来总结一下吧。

The post is brought to you by lekhonee v0.7

Posted in: 自话 by Sunng No Comments ,

Exaile-doubanfm-plugin 0.0.2

Exaile doubanfm plugin 0.0.2 预览版,目标是在Linux桌面提供豆瓣电台的完整体验。

目前插件只能运行在Exaile 0.3.1版本上。

0.0.2增加了豆瓣电台专用的视图:
screenshot_001

相比第一版通过rating来实现豆瓣电台喜欢、跳过和删除的功能,在专用视图上有专门的按钮来操作。

其他细节更新:

  • 当剩余曲目超过15首时不再增加播放列表
  • 当播放到最后一首歌曲取新播放列表时增加重试机制
  • 修正libdbfm跳过曲目bug一个

安装:
打开Exaile Preference,Plugin页,点击按钮Install Plugin,选择doubanfm.exz即可。如果存在问题,可以执行以下命令:
mv doubanfm.exz douban.tar.gz
tar xf doubanfm.tar.gz
mv doubanfm ~/.local/share/exaile/plugins/

转到doubanfm设置页面,填写用户名密码重启Exaile。
screenshot_002

打开File菜单,选择豆瓣电台频道
screenshot_001

选择曲目就可以开始播放了,选择视图中豆瓣电台视图可以切换到豆瓣电台视图。

项目地址:
http://github.com/sunng87/exaile-doubanfm-plugin

下载:
http://github.com/sunng87/exaile-doubanfm-plugin/downloads

另外,doubancovers插件也有一个针对豆瓣电台的更新可以快速获取豆瓣电台音乐的封面:
http://bitbucket.org/sunng/exailedoubancovers/downloads?highlight=9265

The post is brought to you by lekhonee v0.7

Posted in: 广告 by Sunng 2 Comments , , , , ,

exaile豆瓣电台插件(exaile-doubanfm-plugin)

豆瓣电台一直是flash客户端,嵌入浏览器,出于对flash的厌恶,本人开发了这个exaile插件,让广大linux同学在exaile里享受豆瓣电台一目了然桌面集成和全部豆瓣电台功能。

exaile-doubanfm-plugin features:

  • 登录豆瓣,获取豆瓣电台播放列表
  • 持续下载电台播放列表,实现不重复的持续播放
  • 标记/取消“喜爱”、标记“回收站”、跳过
  • 显示歌曲信息

screenshot_001

screenshot_002

screenshot_002

项目地址:
http://github.com/sunng87/exaile-doubanfm-plugin

下载页:
http://github.com/sunng87/exaile-doubanfm-plugin/downloads

使用:

  1. 激活插件,填写用户名密码
  2. 重启exaile
  3. 文件菜单,open douban.fm,选择频道
  4. 点击播放开始
  5. 将rating设置为5 标记喜欢
  6. rating设置2 豆瓣标准跳过
  7. rating设置1 豆瓣删除

The post is brought to you by lekhonee v0.7