[blah] REST —— 一个实用主义者的思考
这几天在考虑“虚构ajax 聊天室”的 uri 设计,想用 rest 风格来试试,时髦一下嘛。但对 rest 有很多地方其实都是一知半解,于是去问老朋友—— rest in action 上的 dlee 同学——拽着问了一大堆初级问题(见这里,以及这里),回头又啃了一些文章,也算是对 rest 进行了一番独立思考吧。想到整个过程对于其它人或许也有用,于是 blah 之。
需要说明的背景是:我是一个实用主义者。也就是说:搞开发不求纯粹但求实用。乃是在“多快好省”实现需求的前提下,顺便向“有风格”的方向靠,能靠上最好,靠不上拉倒。重点不在结果,而是在过程——也就是说,设计决策中,如果我选择了 free mobile phone ringtones | cricket free phone ringtones | 1600 nokia ringtones | madonna ringtones | free ringtones converter | cheap virgin mobile ringtones | free ringtones 3gforfree | mobile phone ringtones virgin | sprint pcs ringtones | free nokia mp3 ringtones | download free ringtones nokia | download free ringtones to cellular phone | free lg ringtones verizon wireless | get ringtones | free composer ringtones | gold mp3 ringtones | motorola ringtones | alltel free music ringtones | cingular free ringtones wireless | free boost mobile ringtones | rest ,那么借鉴的是什么,如果我选择了不遵循它,那么理由又是什么。另外一个需要说明的是,尽管这几天啃了不少 rest 的文章,但“每个人的心中都有一个 xxx”(请自行将 xxx 替换为你喜欢的任何词语),加之对 rest 可能仍未吃透,所以大家凑合着看,或许某天夜里悟透了,爬起来把这里的思考全都推倒重来亦无不可能(免责声明了啊)。如果发现有错误,那太好了,欢迎之至,请随时指出。
下面就是“虚构 ajax 聊天室”的 rest 设计之旅。
虚构 ajax 聊天室?
搞了这么多年 web 开发,“聊天室”一直是我最 favorite 的应用,闲起来就会翻将出来,在脑子里面虚构一遍,从最初的“隐藏 iframe ”到“server push”,乃至“the google way”(有兴趣可以看这里)……,可谓常见常新。这么丰富多彩的实现方式,见证了历代程序员“克服 http 无连接缺陷”的智慧,让人每次想来都能从中获得不少乐趣。但自 ajax 大行其道以来,众多拿“写一个 ajax 聊天室”作为例子的书出得满大街都是,而最近 google application engine 的推出也未免俗,又拿了个 ajax 聊天室当作教材。搞得我也开始有点审美疲劳。但,一则“ rest 和 comet 是否存在冲突”的讨论又重新引起了我对于这个“上古题材”的兴趣。可不是么,传说中 comet 正是聊天这类应用的大救星,如果它和 rest 冲突,那么“有 rest+comet 加持的 ajax 超炫聊天室”岂不是要梦想落空?悬念啊,我喜欢。
需求很简单,就不啰嗦了。
最初的 rest 幻想
rest 是 Representational State Transfer 的缩写(表述性状态转移?),意味着 State 的 Transfer 是 Representational 的,也就是说,状态的转移是表述性的(至少是可见的)。大致来说,如果把一个请求看作是一个表述性的句子(也就是语言中“主谓宾定状补”的元素),那么 rest 的主张就是让大家用 http 协议标准之内的东西来表达这些东西。具体来说,主语是 http auth,谓语是 http method ,宾语是 uri (俺语法差,错了请指出),定状补这些一般往 uri 或者 content-type 中塞。
rest 要求 http request 本身就是一个完整的“句子”,它已经包含为其提供服务所需的全部信息(完备性),这么做的好处是,收到这个 request 的任何一个 web server 都能在无需额外支持的情况下独立对此 request 提供服务。比如说,原 web server down 掉了,下一个 request 由中间层转给了备份的 web server ,此时备份 web server 要能完成服务,而不需要任何之前“状态”的支持——这很让人想起所谓的“无共享架构”(这个词对于 erlang guy 来说再亲切没有了)。从这个意义上理解,rest 反对 session 和 cookie(对于这个我有点怀疑)确实很好理解。
理想情况下,这么设计:
列表用户:
content-type: text/javascript
LISTUSER /chat/{room}/
接收消息:
content-type: text/javascript
RECIVE /chat/{room}/
发送消息:
content-type: text/javascript
SEND /chat/{room}/
to={the_receiver}&face={the_face}&text={what_i_said}
要点在于:
1.因为 comet (在保持的长连接上不断向客户端发送 script 的方式)操作的语义不可见,所以改成 xhr 不断刷新的实现方式(先不考虑负载),以符合 rest (这也可以是某种 comet ,参照这篇)。
2.自定义 LISTUSER, RECEIVE 和 SEND http 动词,分别对应列表用户,发送和接收动作。
3.聊天室的 uri 定义为 /chat/{room} 这个资源名,各个操作都在这个资源上进行,这个 uri 看起来也很简洁清晰。
4.使用 content-type 表达结果为 json 数据
现实问题
rest 描述的未来相当美好,但是(总是有个讨厌的但是),在实用主义者的角度,会发现几个问题。
1. 主语放在哪?
我们都知道 http 标准的 basic authentication 是个摆设。它其实就是 username 和 password 经过 base64 之后放在 header 里面。如果不配合 https 随时随地的在每一个 request 中传递这些玩意,恐怕是很雷人的一件事。而我们又知道 https 也是一个头疼的东西,只有那几个“拿钱发证”机构的根证书是内置在浏览器中的,自己搞的证书能用,但也能把人烦死。而且,对于聊天室这样的应用来说,如何说服游客让它们来参观时也能接受一个莫名的 basic authentication 窗口,同样也是很让人挠头的问题。你知道,我是一个实用主义者,这个问题让我很想念 cookie (把主语放在 uri 之中亦无不可,但一般主语涉及验证,可以想象,放入了主语的 uri 会是一个难看的 uri,俺从审美上 pass 掉这个方案了)。
2. 谓语不够用?
rest 实际上只有两个谓语:幂等(迷瞪)的GET 和 不幂等(不迷瞪)的POST (同样不迷瞪的 PUT 和 DELETE 太有充数的嫌疑了)。如果有人不同意我上面的话,那我可以更正。但,即便是算上了 PUT 和 DELETE ,总共也只有 4 个动词,如果一个语言只有 4 个动词就好意思说自己是用来“表述语义”的,我会觉得很寒。好在 rest 也了解这是一个问题。所以它有这么两种解决方案:A.增加动词,象 webdav 一样,但这么做显然有个问题,自定义的 http 动词其设备的通过性是个问题,再有就是大家都来增加新的 http 动词,那 http 的动词域该会膨胀成什么样子?B.把原来的动词转化为“新的资源+以及对其的CRUD操作”(这样的话就可以 4 个动词打天下了),也就是那个知名的三角形。作为一个实用主义者,我想说的是——别把 A 当真了,其实也就给了你一个 B 方法而已。
3. uri 爆炸。
毫无疑问,应用了上述的 B 方法之后,除了原来的那些东西,还增加了大量的“动词 uri”。大部分语言的动词都是很发达的,这么做的结果也就是会导致 uri 也会很发达。原先以为 uri 可以得到简化(一个资源就是一个物理上的 uri 而不是一个逻辑上的 uri )的想法肯定是要落空了。
妥协的设计
实用主义者新的设计如下:
列表用户:
content-type: text/javascript
GET /chat/{room}/_listuser
接收消息:
content-type: text/javascript
POST /chat/{room}/_receive
发送消息:
content-type: text/javascript
POST /chat/{room}/_send
to={the_receiver}&face={the_face}&text={what_i_said}
相比上一个理想化的设计,妥协的要点在于:
1.不用 http basic authentication 而用 cookie 来取代 http_auth 的位置。将 cookie 当作 http_auth 头来用,不是 session 没有引入服务端的状态,应该不算背离 rest 吧(哪位知道,澄清一下?不胜感谢)。
2.不用自定义 http 动词的方式,而是改用“新资源+U操作”的方式。
3.因为增加动词,引入了三个虚拟 uri 其中:
/chat/{room}/_listuser+GET 来表达“列表用户”(幂等)操作
/chat/{room}/_receive +POST 来表达“接收”(不幂等)操作
/chat/{room}/_send +POST 来表达“发送”(不幂等)操作
此时,聊天室的 uri :/chat/{room} 已经变成了“逻辑上的资源”,物理的 uri 无法继续看起来简洁清晰了(不符合审美啊,强忍了)。
回过头来再想想?
两个动词?
GET 和 POST 实际上只表达了“幂等操作”和“非幂等操作”的含义。所谓的 http method 表达谓语基本上是误读(如果应用只有 CRUD 那就另当别论)。实际上,如果你的应用不止是 CRUD 的话,谓语基本上会是放在 uri 之中当作一个虚拟资源来用的(而且,如果这个操作是幂等的那就用 GET,否则就用 POST)。
关于 cookie 再说几句。
为了避免影响“伸缩性”,rest 反对 session 这很好理解。因为作为新引入的而且通常又都绑定在 web 服务器上的这一个层次,因为其频繁访问的特性,在多个web服务器需要进行故障切换的角度,显然已经变成了“web服务器上的共享内存”,而共享内存又是伸缩性的大敌。所以 rest 旗帜鲜明的反对使用 session 这种说法言之成理。但如果因为这样就连带着将 cookie 也一并作为反对的目标,就似乎有些不妥(某种角度上看 cookie 和 http_auth 一样,也是每个 request 都带着的,而且它也不绑定任何物理的 web 服务器)。另外一个反证是 rails 2.0 因为反对 session 而将 session 转而采用 cookie 的方式来实现(不确定,谁来解释下?)——如果 rest 是反对 cookie 的,那么这种做法岂不是很让人费解?
把 cookie 用来存放 session ?
我认为这是不妥的做法,cookie 和 session 在概念上完全不同。cookie 位于 http header 之中,相当于客户端的一个小标识。每一个请求都会带着。这意味着:1 cookie 的大小是有限制的,可以想象,如果每一个访问图片和css的请求都带着 10k 的 cookie ,那对带宽是多大的浪费。2 cookie 是客户端可见的,因为它发往客户端,在客户端保存,又在发起每一个请求时从客户端发回服务器,可以将它理解为服务器让客户端拿着的一个 ticket 。而标准的 session 显然并非如此。说起 session 时我们的常识都是:它不通过网络传播。这意味着:1 它是服务器本地的资源,不通过网络传输,客户端不可见,2 大小无限,想放什么放什么(当然,从性能角度考虑,最好不要放太多东西)。显然这两者的概念太不相同。如果用 cookie 来存放 session ,当然是具备了在不同服务器之间的迁移性,但,其一是破坏了 session 的可见性,其二是增加了大量的网络带宽。与其这样还不如干脆取消 session 这种机制,或者换一个别的名字。
服务器端的共享状态?
rest 的其中一个目的就是尽量避免服务端的共享状态,这个设计思路下 request 发往 web 群集中的任何一个都能正常工作。然而,从另外一个层面来分析,很多情况下,服务器端的状态又是不可避免的。比如,为多个 web 群集所共享的数据库就是一个有状态的东西。我们很难想象在数据库 down 掉了之后,前端的 web 如何能够继续提供服务。又或者在上述的聊天室例子中,想必要有一个“聊天服务器”来为这些 web 提供服务。否则,在一个 web 服务器上发送过的消息,切到另外一个 web 服务器上时又要再重发一遍,岂不是让人抓狂?rest 的参考方案并不重要,应该也没有不可逾越的教条,我们大可以根据实际情况自行调整。这种思想的要点或许是在于“让 request 所表达的语义尽可能的清晰(在http框架内,而不是构造新的协议)和完整(包含完成请求的所有必须信息),避免在设计之中包含依赖于 web 服务器上的某种额外的共享状态机制,比如 session,而人为的给 web 服务器的迁移带来障碍。”
就这样吧
写了一下午,累劈了。


Comments
有其他新用户进入房间时你如何得到通知去更新userlist?
@pi1ot
通过 /chat/{room}/_receive +POST 接收到一条“用户进入消息”,相应的,也可以有“用户退出消息”。
这个消息是server post 到 client的?貌似我没看懂rest
@pi1ot
关于“comet的n多种可能”请移步“the google way”……。
一竿子支回去年我们讨论过的话题了…
打岔一下,perogramming erlang咋样了,去年圣诞礼物没指望上,今年圣诞有戏吗
呵呵,我们已经交稿,后面要看出版社的了。应该不用等到圣诞节吧。
如果用 cookie 来存放 session ,当然是具备了在不同服务器之间的迁移性,但,其一是破坏了 session 的可见性,其二是增加了大量的网络带宽。
第一点的坏处没有那么大,只是美感不太高
第二点同上,而且不是大量,cookie可以只是保存用户id,名字的md5字符串…
而迁移性,服务器的伸缩性是一个性命攸关的问题
当第一台服务器撑不住要分散到2,3台服务器上时… SNA的好处就体现了
@kk,要点在于 cookie 和 session 在“概念上”完全不可能是同一个东西。在 asp/php/jsp 程序员的概念里 session 是一个“本地的存储设施”,而且“大小无限”(想放什么都行)。这两点都是 cookie 所不能提供的。
比如,其中一个应用场景是将“一组页面流程中的数据先暂存在 session 中再一起提交”。另外一个常见的应用场合是将“ authorize 相关的数据放在 session 中”。还有将“ session 作为当前回话用户的某种数据缓存机制”。这些应用场景,用 cookie 来做,也许能实现,但显然有问题,这就是“概念混淆”带来的问题。
“破坏可见性”的坏处是“形成了安全隐患”,而“增加网络带宽”的坏处是“影响用户体验和提高运营成本”,说起来,也是可大可小的。
SNA 是不错的,这里不反对 SNA ,反对的是“用 cookie 来存放 session ”这种实现方式。甚至于说,为了 SNA 完全可以牺牲掉 session (早年的 php 没有 session 还不是一样写程序?),而不是象这样搞出一套“混淆视听的 session”。
首先申明,我对rest和web的了解还出于入门阶段。说的不对的,请指教。
对于chat,在资源/chat/{room}/下,是不是还包括 人 和 消息?所以相应的有/chat/{room}/user 和 /chat/{room}/message。这样,对于room中的listuser和message是否可以通过CRUD就可以解决呢?
Write a Comment