用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

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

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

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