<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/rss2full.xsl"?><?xml-stylesheet type="text/css" media="screen" href="http://feeds.feedburner.com/~d/styles/itemcontent.css"?><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0" version="2.0">

<channel>
	<title>:wq ~/notes</title>
	
	<link>http://blog.liancheng.info</link>
	<description>学而时嬉之 | 好记性不如烂硬盘</description>
	<lastBuildDate>Sun, 25 Jul 2010 00:07:36 +0000</lastBuildDate>
	
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="self" type="application/rss+xml" href="http://feeds.feedburner.com/liancheng" /><feedburner:info uri="liancheng" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com/" /><item>
		<title>分布式相关文献共享</title>
		<link>http://feedproxy.google.com/~r/liancheng/~3/hjBsziuYGm4/</link>
		<comments>http://blog.liancheng.info/?p=548#comments</comments>
		<pubDate>Sat, 24 Jul 2010 10:41:00 +0000</pubDate>
		<dc:creator>liancheng</dc:creator>
				<category><![CDATA[Work]]></category>
		<category><![CDATA[Distributed System]]></category>
		<category><![CDATA[JabRef]]></category>

		<guid isPermaLink="false">http://blog.liancheng.info/?p=548</guid>
		<description><![CDATA[去年从Google、Amazon分布式架构出发，针对分布式数据一致性和其他的一些分布式基本原理做了一些研究。期间搜集了一批文献，并用JabRef做了索引，现在共享出来供参考：

文献索引，包含文献基本信息及各篇文献的独立下载链接，支持排序和检索
全部文献的压缩包，约37MB，包含所有文献及BibTeX索引

BibTeX索引文件可以直接用JabRef或者其他兼容BibTeX格式的文献管理工具进行编辑。共享出来的文献不仅仅包含分布式的内容，也有少量其他的东西，比如Erlang、LaTeX、魔方什么的，各取所需吧   


(function() {
var jaxlChat = document.createElement("script");
jaxlChat.type = "text/javascript";
jaxlChat.async = true;
jaxlChat.src = "http://im.jaxl.im/ui/jaxl.php";
(document.getElementsByTagName("head")[0]&#124;&#124;document.getElementsByTagName("body")[0]).appendChild(jaxlChat);
})();

]]></description>
			<content:encoded><![CDATA[<p><a href="http://blog.liancheng.info/wp-content/uploads/2010/07/thesis.jpg"><img class="alignright size-medium wp-image-550" title="thesis" src="http://blog.liancheng.info/wp-content/uploads/2010/07/thesis-300x229.jpg" alt="" width="216" height="165" /></a>去年从Google、Amazon分布式架构出发，针对分布式数据一致性和其他的一些分布式基本原理做了一些研究。期间搜集了一批文献，并用<a href="http://jabref.sourceforge.net/">JabRef</a>做了索引，现在共享出来供参考：</p>
<ul>
<li><a href="http://liancheng.info/bib/" target="_blank">文献索引</a>，包含文献基本信息及各篇文献的独立下载链接，支持排序和检索</li>
<li><a href="http://liancheng.info/bib/bib.tar.gz">全部文献的压缩包</a>，约37MB，包含所有文献及BibTeX索引</li>
</ul>
<p>BibTeX索引文件可以直接用JabRef或者其他兼容BibTeX格式的文献管理工具进行编辑。共享出来的文献不仅仅包含分布式的内容，也有少量其他的东西，比如Erlang、LaTeX、魔方什么的，各取所需吧  <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_razz.gif' alt=':-P' class='wp-smiley' /> </p>
<!-- Jaxl IM embed starts -->
<script type="text/javascript">
(function() {
var jaxlChat = document.createElement("script");
jaxlChat.type = "text/javascript";
jaxlChat.async = true;
jaxlChat.src = "http://im.jaxl.im/ui/jaxl.php";
(document.getElementsByTagName("head")[0]||document.getElementsByTagName("body")[0]).appendChild(jaxlChat);
})();
</script>
<!-- Jaxl IM embed ends --><img src="http://feeds.feedburner.com/~r/liancheng/~4/hjBsziuYGm4" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://blog.liancheng.info/?feed=rss2&amp;p=548</wfw:commentRss>
		<slash:comments>2</slash:comments>
		<feedburner:origLink>http://blog.liancheng.info/?p=548</feedburner:origLink></item>
		<item>
		<title>个人数据安全 (2)：保护即时通讯隐私</title>
		<link>http://feedproxy.google.com/~r/liancheng/~3/gyNdV5AnPWE/</link>
		<comments>http://blog.liancheng.info/?p=433#comments</comments>
		<pubDate>Wed, 03 Feb 2010 13:16:15 +0000</pubDate>
		<dc:creator>liancheng</dc:creator>
				<category><![CDATA[Data security]]></category>
		<category><![CDATA[Censorship]]></category>
		<category><![CDATA[IM]]></category>
		<category><![CDATA[OTR]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[XMPP]]></category>

		<guid isPermaLink="false">http://blog.liancheng.info/?p=433</guid>
		<description><![CDATA[
（本文部分链接可能需要翻墙访问）
一般来说，即时通讯（IM）软件都会对客户端到服务器的通讯进行加密，对用户隐私数据安全提供一定程度的保障。但也有例外，比如MSN就完全不加密。所以一些小公司将MSN作为主要IM工具是极为不明智的，借助Wireshark等简单工具对员工间甚至员工和客户间的对话内容进行监听易如反掌，极容易造成商业机密的泄漏。微软坚持使用明文MSN协议的目的让人难以捉摸，其中恐怕难免混有政治因素。使用MSN Shell插件的加密功能或者SSH隧道转发等手段，也可以不同程度地间接加密MSN通讯数据。
即使是那些号称使用加密协议的IM服务，也并不真的就百分之百地安全，在国内的网络环境下尤其如此。一直饱受质疑的QQ（1、2）自不必说，国内其他的IM运营商也多少存在类似的境况。这种行为固然可恶，但作为国内的运营商，若不如此便无法生存——饭否便是个极好的例子。即便是Google，也干出了中文版Gtalk不开启加密（1、2）这样的事情来。如果不采取一些特别的措施，无论你愿不愿意，正如题图一样：The big brother is watching you!

Why?
为什么即使是采用加密协议，仍然逃不过监视？那就要先看看通讯过程中哪些内容被加密，以及是如何被加密的。各种IM系统的加密方式林林总总，有的采用标准的SSL/TLS，有的则自行打造。但总体上说大致还是脱不开SSL/TLS的主体脉络，也就是先用非对称加密交换对称密钥，再用对称密钥对链路上的数据进行加密，同时辅以身份验证。其基本原理可以参见本系列的第一篇。
通常，在发送方和接收方客户端连接到服务器时首先会和服务器协商对称密钥。之后，一条聊天消息从发送到接受大致可以分为一下几个阶段：

发送方客户端用自己的对称密钥加密明文消息并发送至服务器
服务器用和发送方客户端的对称密钥还原出明文消息
服务器用和接收方客户端的对称密钥加密明文消息并发送至接收方客户端
接收方客户端用自己的对称密钥还原出明文消息

所以，被加密的实际上是消息在服务器和客户端之间的传输链路，以防在传输过程中遭到第三方（比如GFW）监听。特别需要注意的是在第2步，消息会在服务器被还原为明文。从功能角度上说，这么做的优势很明显，运营商在服务器端可以提供更多的功能。例如Gtalk的服务器端存储聊天记录，还有QQ的聊天记录漫游，以及对spam消息进行分拣过滤等等。当然，从保护用户隐私的角度考虑，运营商是不应该将明文聊天记录泄漏出去的。然而嘛……大家都知道。
Hosted.IM——搭建自己的IM服务器
既然整个环节的漏洞出在IM运营商处，那么不妨搭建自己的IM服务器吧   这里介绍的由ProcessOne提供的Hosted.IM是一个免费的XMPP托管服务，只需要一个域名即可。提供的功能包括群组聊天、文件传输、域间互通等等，详细说明参见这里。
Hosted.IM的用户界面极精简，配置的过程很简单。首先你得在Hosted.IM上注册帐号，通过邮箱验证后，还需要在自己的域名服务商处为自己的域名配上如下的DNS条目（强烈建议使用国外的域名服务，国内的域名服务很多都不提供SRV设置入口）：
_xmpp-client._tcp.your.name. IN SRV 20 0 5222 xmpp2.hosted.im.
_xmpp-client._tcp.your.name. IN SRV 10 0 5222 xmpp1.hosted.im.
_xmpp-server._tcp.your.name. IN SRV 10 0 5269 xmpp1.hosted.im.
_xmpp-server._tcp.your.name. IN SRV 20 0 5269 xmpp2.hosted.im.
_jabber._tcp.your.name. IN SRV 20 0 5269 xmpp2.hosted.im.
_jabber._tcp.your.name. IN SRV 10 0 5269 xmpp1.hosted.im.
_vhreg_auth.your.name. IN TXT "c500b6278e37092eb24eb71492719a7e68009ac5"
将以上的your.name更改为你自己的域名。最后，添加若干用户，便可用Pidgin、Psi等支持XMPP的客户端登录使用了。
简单快捷的背后，缺点也很明显：

每个域名只支持10个注册用户，适合于小范围的注重即时通讯隐私的团体交流，比如小型创业团队。好在XMPP天然支持域间互通，如果一个域名 不够用，再来几个就是。
管理界面太过简洁了，实际上除了增删用户以外什么功能有没有。如果你需要更多的功能或在一个域内支持更多的用户帐号，就得给ProcessOne发邮件申请付费服务了。
采用这种做法，事实上并没有改变问题的本质，只是做了一个风险转移。前提是我们假设将IM服务托管在身处墙外的、相对小众的ProcessOne的风险要远小于国内的IM服务商。

ProcessOne
顺便说一说ProcessOne。可能很多人都没有听说过这家公司，但在Erlang和XMPP界，ProcessOne可是鼎鼎大名。近年来Erlang在互联网应用中的流行也跟ProcessOne的杀手级应用ejabberd有着密切的联系——Facebook的IM服务器采用的就是定制版的ejabberd。ProcessOne还有其他的一些有趣的项目，比如：

CEAN

Erlang应用管理工具，借鉴自CPAN


Talkr.im

ProcessOne的IM服务，支持AIM、ICQ、MSN/WLM、Yahoo!等多种IM协议网关，近期还推出了Google Wave的协议网关


Tweet.im

Twitter/XMPP网关，借助Gtalk或任意一款XMPP客户端即可免翻墙轻松访问Twitter（最大的缺点是不支持RT）



OTR
如果觉得ProcessOne还是不足以受信怎么办？好吧您可真够多疑的。这时候就可以祭出OTR了。OTR实际上是一类技术的通称，全名是Off The Record，用以指代避免在服务器上以任何形式留存下消息明文的各种技术。
OTR的基本原理其实也很简单，简而言之就是借助公钥加密算法多做一层加密。以XMPP为例，拆解一下就是：

参与IM通讯的每个IM用户都自行生成一对公私钥对，并将自己的公钥发送给对方
若消息发送方打算发送消息M0，则发送方首先用接收方的公钥和自己的私钥对M0进行加密、签名，得到M1
M1被投递给XMPP客户端程序，客户端将使用TLS对其进行二次加密，得到M2
M2经由服务器被发送给消息接收方的XMPP客户端程序
消息接收方客户端程序收到M2后通过TLS解密还原出M1
接收方用自己的私钥和发送方的公钥对M1进行解密、签名校验，最终还原出M0

这样做的好处在于：

由于公钥加密的介入，无论所使用的底层IM协议是否支持加密，消息本身的保密性都可得到保障
即使IM服务器尝试对消息M2进行解密，也只能还原出M1，而无法得到M0；也就是说服务器端无论如何都得不到消息明文

这样一来，即使服务器端尝试对解密出的消息“明文”进行存储，也只能存储由公钥加密保护的M1，而无法触及M0，OTR也正是由此而得名的。
Pidgin Encryption
Pidgin便有一个OTR插件——Pidgin Encryption，可对Pidgin支持的各种IM协议进行OTR保护。Debian/Ubuntu用户可以直接从aptitude安装pidgin-ecnryption包。
使用PE时，需要消息收发双方都安装该插件，并各自生成自己的密钥对（推荐使用4096位密钥）。发起会话时，PE会自动发起密钥交换过程，用户只需确认对方的公钥即可。后续的会话过程便都将处于OTR的保护之下。
使用OTR虽然非常安全，但使用起来也相应地有些麻烦。比如通讯双方必须使用相同的OTR算法（这往往意味着双方必须使用同一种IM客户端及OTR插件），以及必须小心保护和备份密钥对文件以防被盗取或遗失。
XMPP/OpenPGP双剑合璧
如果各个客户端都采用标准的IM协议和OTR算法，那么不同客户端之间的互操作性就可以大大加强。我们已经知道XMPP是一个标准、开放的IM协议，同时也知道了OpenPGP是一个基于公钥加密算法的隐私数据保护标准。那么是否能在XMPP上采用OpenPGP作为OTR算法呢？
事实上XMPP的扩展协议之一，XEP-0027便定义了在XMPP中使用OpenPGP的方法。Psi已支持XEP-0027，Pidgin则尚不支持。不过我在这里发现确实有人在进行Pidgin的GnuPG插件开发。
结语
墙内的网络环境越来越恶劣，让人越来越没有安全感。但其实要对个人隐私进行一些基本的保护，也并不困难。当然不希望在日常生活中也不得不用上这些方法，这篇权且当作是未雨绸缪。


(function() [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://blog.liancheng.info/wp-content/uploads/2010/02/china-censorship-of-the-internet-cartoon.gif"><img class="size-medium wp-image-434 alignleft" src="http://blog.liancheng.info/wp-content/uploads/2010/02/china-censorship-of-the-internet-cartoon-300x188.gif" alt="" width="300" height="188" /></a></p>
<p>（本文部分链接可能需要翻墙访问）</p>
<p>一般来说，即时通讯（IM）软件都会对客户端到服务器的通讯进行加密，对用户隐私数据安全提供一定程度的保障。但也有例外，比如MSN就完全不加密。所以一些小公司将MSN作为主要IM工具是极为不明智的，借助Wireshark等简单工具对员工间甚至员工和客户间的对话内容进行监听易如反掌，极容易造成商业机密的泄漏。微软坚持使用明文MSN协议的目的让人难以捉摸，其中恐怕难免混有政治因素。使用MSN Shell插件的加密功能或者SSH隧道转发等手段，也可以不同程度地间接加密MSN通讯数据。</p>
<p>即使是那些号称使用加密协议的IM服务，也并不真的就百分之百地安全，在国内的网络环境下尤其如此。一直饱受质疑的QQ（<a href="http://rt.ju690.com/rt/15711" target="_blank">1</a>、<a href="http://www.chinagfw.org/2009/09/qq_23.html" target="_blank">2</a>）自不必说，国内其他的IM运营商也多少存在类似的境况。这种行为固然可恶，但作为国内的运营商，若不如此便无法生存——饭否便是个极好的例子。即便是Google，也干出了中文版Gtalk不开启加密（<a href="http://xijie.wordpress.com/2009/08/26/%E3%80%90%E6%B3%A8%E6%84%8F%E3%80%91%E4%B8%AD%E6%96%87%E7%89%88google-talk%E6%98%AF%E6%9C%AA%E5%8A%A0%E5%AF%86%E6%98%8E%E6%96%87%E4%BC%A0%E8%BE%93%E8%81%8A%E5%A4%A9%E5%86%85%E5%AE%B9/" target="_blank">1</a>、<a href="http://www.google.com/support/forum/p/other/thread?tid=5ee3c6dc35225996&amp;hl=zh-CN" target="_blank">2</a>）这样的事情来。如果不采取一些特别的措施，无论你愿不愿意，正如题图一样：The big brother is watching you!</p>
<p><span id="more-433"></span></p>
<h1>Why?</h1>
<p>为什么即使是采用加密协议，仍然逃不过监视？那就要先看看通讯过程中哪些内容被加密，以及是如何被加密的。各种IM系统的加密方式林林总总，有的采用标准的SSL/TLS，有的则自行打造。但总体上说大致还是脱不开SSL/TLS的主体脉络，也就是先用非对称加密交换对称密钥，再用对称密钥对链路上的数据进行加密，同时辅以身份验证。其基本原理可以参见<a href="http://blog.liancheng.info/?p=338" target="_blank">本系列的第一篇</a>。</p>
<p>通常，在发送方和接收方客户端连接到服务器时首先会和服务器协商对称密钥。之后，一条聊天消息从发送到接受大致可以分为一下几个阶段：</p>
<ol>
<li>发送方客户端用自己的对称密钥加密明文消息并发送至服务器</li>
<li><span style="color: #ff0000;">服务器用和发送方客户端的对称密钥还原出明文消息</span></li>
<li>服务器用和接收方客户端的对称密钥加密明文消息并发送至接收方客户端</li>
<li>接收方客户端用自己的对称密钥还原出明文消息</li>
</ol>
<p>所以，被加密的实际上是消息在服务器和客户端之间的传输链路，以防在传输过程中遭到第三方（比如GFW）监听。特别需要注意的是在第2步，消息会在服务器被还原为明文。从功能角度上说，这么做的优势很明显，运营商在服务器端可以提供更多的功能。例如Gtalk的服务器端存储聊天记录，还有QQ的聊天记录漫游，以及对spam消息进行分拣过滤等等。当然，从保护用户隐私的角度考虑，运营商是不应该将明文聊天记录泄漏出去的。然而嘛……大家都知道。</p>
<h1>Hosted.IM——搭建自己的IM服务器</h1>
<p>既然整个环节的漏洞出在IM运营商处，那么不妨搭建自己的IM服务器吧 <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_smile.gif' alt=':-)' class='wp-smiley' />  这里介绍的由<a href="http://www.process-one.net/" target="_blank">ProcessOne</a>提供的<a href="http://hosted.im" target="_blank">Hosted.IM</a>是一个免费的XMPP托管服务，只需要一个域名即可。提供的功能包括群组聊天、文件传输、域间互通等等，详细说明参见<a href="http://hosted.im/portal/features" target="_blank">这里</a>。</p>
<p>Hosted.IM的用户界面极精简，配置的过程很简单。首先你得在Hosted.IM上注册帐号，通过邮箱验证后，还需要在自己的域名服务商处为自己的域名配上如下的DNS条目（强烈建议使用国外的域名服务，国内的域名服务很多都不提供SRV设置入口）：</p>
<pre>_xmpp-client._tcp.<strong>your.name</strong>. IN SRV 20 0 5222 xmpp2.hosted.im.
_xmpp-client._tcp.<strong>your.name</strong>. IN SRV 10 0 5222 xmpp1.hosted.im.
_xmpp-server._tcp.<strong>your.name</strong>. IN SRV 10 0 5269 xmpp1.hosted.im.
_xmpp-server._tcp.<strong>your.name</strong>. IN SRV 20 0 5269 xmpp2.hosted.im.
_jabber._tcp.<strong>your.name</strong>. IN SRV 20 0 5269 xmpp2.hosted.im.
_jabber._tcp.<strong>your.name</strong>. IN SRV 10 0 5269 xmpp1.hosted.im.
_vhreg_auth.<strong>your.name</strong>. IN TXT "c500b6278e37092eb24eb71492719a7e68009ac5"</pre>
<p>将以上的<span style="font-family: courier new,courier;">your.name</span>更改为你自己的域名。最后，添加若干用户，便可用Pidgin、Psi等支持XMPP的客户端登录使用了。</p>
<p>简单快捷的背后，缺点也很明显：</p>
<ul>
<li>每个域名只支持10个注册用户，适合于小范围的注重即时通讯隐私的团体交流，比如小型创业团队。好在XMPP天然支持域间互通，如果一个域名 不够用，再来几个就是。</li>
<li>管理界面太过简洁了，实际上除了增删用户以外什么功能有没有。如果你需要更多的功能或在一个域内支持更多的用户帐号，就得给ProcessOne发邮件申请付费服务了。</li>
<li>采用这种做法，事实上并没有改变问题的本质，只是做了一个风险转移。前提是我们假设将IM服务托管在身处墙外的、相对小众的ProcessOne的风险要远小于国内的IM服务商。</li>
</ul>
<h2>ProcessOne<br class="spacer_" /></h2>
<p>顺便说一说ProcessOne。可能很多人都没有听说过这家公司，但在Erlang和XMPP界，ProcessOne可是鼎鼎大名。近年来Erlang在互联网应用中的流行也跟ProcessOne的杀手级应用<a href="http://www.ejabberd.im/" target="_blank">ejabberd</a>有着密切的联系——Facebook的IM服务器采用的就是定制版的ejabberd。ProcessOne还有其他的一些有趣的项目，比如：</p>
<ul>
<li><a href="http://cean.process-one.net/" target="_blank">CEAN</a>
<ul>
<li>Erlang应用管理工具，借鉴自CPAN</li>
</ul>
</li>
<li><a href="http://talkr.im">Talkr.im</a>
<ul>
<li>ProcessOne的IM服务，支持AIM、ICQ、MSN/WLM、Yahoo!等多种IM协议网关，近期还推出了Google Wave的协议网关</li>
</ul>
</li>
<li><a href="http://tweet.im">Tweet.im</a>
<ul>
<li>Twitter/XMPP网关，借助Gtalk或任意一款XMPP客户端即可免翻墙轻松访问Twitter（最大的缺点是不支持RT）</li>
</ul>
</li>
</ul>
<h1>OTR</h1>
<p>如果觉得ProcessOne还是不足以受信怎么办？好吧您可真够多疑的。这时候就可以祭出OTR了。OTR实际上是一类技术的通称，全名是Off The Record，用以指代避免在服务器上以任何形式留存下消息明文的各种技术。</p>
<p>OTR的基本原理其实也很简单，简而言之就是借助公钥加密算法多做一层加密。以XMPP为例，拆解一下就是：</p>
<ol>
<li>参与IM通讯的每个IM用户都自行生成一对公私钥对，并将自己的公钥发送给对方</li>
<li>若消息发送方打算发送消息M<sub>0</sub>，则发送方首先用接收方的公钥和自己的私钥对M<sub>0</sub>进行加密、签名，得到M<sub>1</sub></li>
<li>M<sub>1</sub>被投递给XMPP客户端程序，客户端将使用TLS对其进行二次加密，得到M<sub>2</sub></li>
<li>M<sub>2</sub>经由服务器被发送给消息接收方的XMPP客户端程序</li>
<li>消息接收方客户端程序收到M<sub>2</sub>后通过TLS解密还原出M<sub>1</sub></li>
<li>接收方用自己的私钥和发送方的公钥对M<sub>1</sub>进行解密、签名校验，最终还原出M<sub>0</sub></li>
</ol>
<p>这样做的好处在于：</p>
<ol>
<li>由于公钥加密的介入，无论所使用的底层IM协议是否支持加密，消息本身的保密性都可得到保障</li>
<li>即使IM服务器尝试对消息M<sub>2</sub>进行解密，也只能还原出M<sub>1</sub>，而无法得到M<sub>0</sub>；也就是说服务器端无论如何都得不到消息明文</li>
</ol>
<p>这样一来，即使服务器端尝试对解密出的消息“明文”进行存储，也只能存储由公钥加密保护的M<sub>1</sub>，而无法触及M<sub>0</sub>，OTR也正是由此而得名的。</p>
<h2>Pidgin Encryption</h2>
<p>Pidgin便有一个OTR插件——<a href="http://pidgin-encrypt.sourceforge.net/" target="_blank">Pidgin Encryption</a>，可对Pidgin支持的各种IM协议进行OTR保护。Debian/Ubuntu用户可以直接从aptitude安装pidgin-ecnryption包。</p>
<p>使用PE时，需要消息收发双方都安装该插件，并各自生成自己的密钥对（推荐使用4096位密钥）。发起会话时，PE会自动发起密钥交换过程，用户只需确认对方的公钥即可。后续的会话过程便都将处于OTR的保护之下。</p>
<p>使用OTR虽然非常安全，但使用起来也相应地有些麻烦。比如通讯双方必须使用相同的OTR算法（这往往意味着双方必须使用同一种IM客户端及OTR插件），以及必须小心保护和备份密钥对文件以防被盗取或遗失。</p>
<h2>XMPP/OpenPGP双剑合璧</h2>
<p>如果各个客户端都采用标准的IM协议和OTR算法，那么不同客户端之间的互操作性就可以大大加强。我们已经知道XMPP是一个标准、开放的IM协议，同时也知道了OpenPGP是一个基于公钥加密算法的隐私数据保护标准。那么是否能在XMPP上采用OpenPGP作为OTR算法呢？</p>
<p>事实上XMPP的扩展协议之一，<a href="http://xmpp.org/extensions/xep-0027.html" target="_blank">XEP-0027</a>便定义了在XMPP中使用OpenPGP的方法。Psi已支持XEP-0027，Pidgin则尚不支持。不过我在<a href="http://blog.chavant.info/2009/06/01/gnupg-plugin-for-pidgin" target="_blank">这里</a>发现确实有人在进行Pidgin的GnuPG插件开发。</p>
<h1>结语</h1>
<p>墙内的网络环境越来越恶劣，让人越来越没有安全感。但其实要对个人隐私进行一些基本的保护，也并不困难。当然不希望在日常生活中也不得不用上这些方法，这篇权且当作是未雨绸缪。</p>
<!-- Jaxl IM embed starts -->
<script type="text/javascript">
(function() {
var jaxlChat = document.createElement("script");
jaxlChat.type = "text/javascript";
jaxlChat.async = true;
jaxlChat.src = "http://im.jaxl.im/ui/jaxl.php";
(document.getElementsByTagName("head")[0]||document.getElementsByTagName("body")[0]).appendChild(jaxlChat);
})();
</script>
<!-- Jaxl IM embed ends --><img src="http://feeds.feedburner.com/~r/liancheng/~4/gyNdV5AnPWE" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://blog.liancheng.info/?feed=rss2&amp;p=433</wfw:commentRss>
		<slash:comments>5</slash:comments>
		<feedburner:origLink>http://blog.liancheng.info/?p=433</feedburner:origLink></item>
		<item>
		<title>个人数据安全 (1)：用GnuPG保护个人隐私数据</title>
		<link>http://feedproxy.google.com/~r/liancheng/~3/xFGeu15TstU/</link>
		<comments>http://blog.liancheng.info/?p=338#comments</comments>
		<pubDate>Sat, 23 Jan 2010 21:10:16 +0000</pubDate>
		<dc:creator>liancheng</dc:creator>
				<category><![CDATA[Data security]]></category>
		<category><![CDATA[Tools]]></category>
		<category><![CDATA[GnuPG]]></category>
		<category><![CDATA[OpenPGP]]></category>
		<category><![CDATA[Security]]></category>

		<guid isPermaLink="false">http://blog.liancheng.info/?p=338</guid>
		<description><![CDATA[之前在Twitter上说过，打算写一个个人数据安全解决方案的系列，内容包括：

基于GnuPG的个人隐私数据保护
自建XMPP服务器保障即时通讯安全
使用Dropbox进行较低密级的文件共享和协作

后记：后来觉得Dropbox这个话题太简单了点，没啥好写的，且重点在共享和协作，而非安全，便取消了。

原本还打算写一写用SSH端口转发隧道建立SOCKS v5代理（俗称SSH翻墙术），鉴于网上已经有不少不错的介绍（1、2），就不再重复劳动了。这里所采用的技术全部基于开源软件、免费软件或免费服务商，同时也兼顾使用体验。除了自建XMPP服务器所需的域名费用外，其余部分的经济成本为零。
跟丫头暂时还维持着北京、杭州两地分居的状况，网络是平时联络和数据交换的最为重要的手段。上述的这些技术都是我们目前正在使用的数据安全保障手段。这几篇文章的前身其实就是我写给丫头的操作手册。内容并不艰深，丫头并非计算机专业也能轻松掌握就是最佳佐证  
另外，由于我平时主要使用Ubuntu，所以这里的所有方案都同时适用于Ubuntu和Windows。对Ubuntu以外的其他Linux发行版，这里就不兼顾了，不过方法都大同小异。
第一篇挑个复杂点儿的，讲讲GnuPG  


本篇将讲述Windows和Ubuntu下的GnuPG解决方案，同时也会对GnuPG背后的信息安全原理做一些简单的科普。GnuPG可用于隐私数据的加密和签名，以便对其进行安全的传输和存储。除GnuPG外，本篇介绍的软件还包括Windows下的GnuPG Shell、GPGee（GnuPG explorer extension）以及Ubuntu下的Seahorse。
对于熟悉GnuPG加密、签名原理的读者，可以直接跳过科普的章节，直接阅读软件安装配置部分。
缘由
丫头平时记性都挺好，唯独记不住密码，各种帐号密码随设随忘。我则刚好反，平时啥啥都忘，唯独对各种随机字符串印象深刻。我自己的各个密码在各大网站的密码强度评估也从来都是绿色。所以她的各种账户密码基本上都靠我来记，想不起来了就给我发个查询请求。有时候不光是密码，偶尔还需要互相备份网银、支付宝之类的证书文件。这些都属于有高保密性要求的隐私数据。
丫头虽然不是计算机专业，不过在软件应用方面还是有相当的水准，我并不太担心她的机器上会有病毒或木马。所以在一定程度上说，密码、证书被盗的概率实际上并不高。然而墙内信息安全形势之恶劣已经是众所周知，通过IM或邮件明文传输密码、证书总是件让人不放心的事情。即使我们所使用的服务都号称对通讯做了加密，但出于众所周知的原因，国内的服务商们为了自身的生存，实际上都不得不在服务器端对解密后的明文数据做内容过滤和审查。因此也就存在着恶意个体或团体有目的的盗取用户明文数据的可能。作为程序员，总有种如坐针毡的不爽，必须防患于未然。这个想法促使我去寻求一个相对严密且切实可行的保障隐私数据安全的解决方案。
问题描述
解决信息安全问题的首要任务，就是确立威胁模型。简单来说就是将现实中存在的诸多威胁信息安全的因素分门别类，找出最需要解决的威胁因素。在这里，我们假设发件人和收件人的主机都是安全的，需要处理的威胁主要是来自数据传输通路上的数据监听、窃取、篡改和仿冒。
更精确一些，数据传输过程必须提供以下保障：

保密性：发件人所发出的消息必须是加密的，且只有特定的收件人才能够对密文进行解密
完整性：发件人所发出的消息必须原封不动地抵达收件人那里，如果沿途惨遭篡改、删节，收件人必须能够发现
身份验证：收件人能够验证发件人的身份，以保证其他人不能仿冒发件人的身份发送虚假消息

加密
要保障保密性，最简单的方法莫过于使用加密压缩包（Zip、7Zip、RAR等常用压缩格式都支持加密）。然而要采用这种方法，首先要把解压密钥交到收件人手中。如何安全地交换解压密钥？这就陷入了一个死循环。
双方使用同样的密钥对数据进行加密和解密，这种加密方式被称作对称加密，对应的密钥就被称作对称密钥。常用的对称加密算法，如RC4、3DES、AES等，都具有高效的优点，即使是普通PC机也可以在短时间内使用这些算法对大量数据进行加解密。然而它们也都不可避免地具有加解密双方必须事先共享对称密钥的先天缺陷。正是这个缺陷导致了上面鸡生蛋蛋生鸡的窘境。
相对于对称加密，非对称加密（或称公钥加密）则很好地克服了这个缺陷。非对称加密需要使用不止一个，而是一对密钥——一个公钥和一个私钥。明文数据使用公私钥对中的一个密钥加密后，只能由对应的另一个密钥解密。应用非对称加密时，用户需要妥善保管私钥，不得外泄或丢失，同时公开公钥。
借助非对称加密，保密性基本得到解决。初始时，收件人生成一对密钥，并将公钥公布给发件人。双方收发消息时遵循一下流程：
 



发件人用收件人的公钥加密消息，生成密文并发送给收件人
收件人用自己的私钥解密消息密文，得到消息明文

看起来不错，但还有个问题：非对称加密虽然克服了需要事先交换对称密钥的问题，但常用的非对称加密算法（如RSA）都非常慢，无法在短时间内加密较长的数据。假设我们要传输的不是密码这样的短文本，而是诸如照片等尺寸上兆的数据，上述过程就行不通了。对于这个问题，解决的方法也很简单。快速加解密是对称加密的强项，那么我们就仍然使用对称加密来对数据进行加密，转而使用非对称加密来对对称密钥进行加密。这样一来，上述过程就变成：

 发件人

随机生成一个对称密钥
用对称密钥加密消息明文得到消息密文
用收件人的公钥加密对称密钥得到对称密钥密文
将消息密文和对称密钥密文一并发给收件人


收件人

用私钥解密对称密钥密文得到对称密钥
用对称密钥解密消息密文得到消息明文 



好吧，我承认有点像绕口令，但其实还是蛮简单的   
数字签名
数字签名是一种用于鉴别发件人身份的技术。它主要应用了非对称加密和消息摘要。
消息摘要是将一段任意长度的消息转化成一个固定长度的短文本串的过程。对于信息安全领域所使用的摘要算法（如常用的MD5、SHA1），极难找到具有相同摘要的两段不同的消息。因此事实上只要消息摘要相同，就可认定消息相同。
对给定消息进行数字签名的过程如下：

计算消息摘要
用自己的私钥加密消息摘要，得到数字签名
将数字签名与消息一起发送给收件人

收件人收到消息后验证数字签名的过程如下：

计算消息摘要
使用发件人的公钥解密数字签名，得到原始消息摘要
比对两个消息摘要

若二者相同，即可证明该消息确实由拥有相应私钥的发件人发出，这就达到了身份验证的目的。
注意，在校验数字签名的过程中，消息完整性也得到了保证——由于数字签名中包含原始消息的摘要值，一旦消息遭到篡改，收件人计算出的消息摘要便会有别于数字签名中的原始消息摘要，从而导致验证失败。
至此，保密性、完整性和身份验证都圆满地得到了解决。完整的数据传输过程如下：

 发件人

对消息进行数字签名

计算消息摘要
用自己的私钥加密消息摘要，得到数字签名


加密签名后的消息

随机生成一个对称密钥
用对称密钥加密消息和数字签名，得到消息密文
用收件人的公钥加密对称密钥，得到对称密钥密文
将对称密钥密文和消息密文合并后发送给收件人




收件人

解密

用自己的私钥解密对称密钥
用对称密钥解密消息密文得到消息明文和数字签名


校验数字签名

计算消息摘要
用发件人的公钥解密数字签名，得到原始消息摘要
比对两个消息摘要





以上简要解释了加密和签名的密码学原理。它们是我们的个人数据安全解决方案中最为核心的两个操作。实际上，日常生活中很多常用的信息安全保障手段，如SSL/TLS证书、网银证书、支付宝证书等等，也都在是以这两个操作为核心的。
在实际应用中，可以根据需要对加密和签名操作进行组合，例如：

只加密不签名
对隐秘文件进行备份时可采用这种方法。因为日后读取该文件的还是你自己，所以签名与否并不重要。值得注意的是，非对称密钥的长度往往很长，一般至少在1024 bit（128字节）以上，而用户选择的对称密钥则往往只有几个或十几个字节。因此一般而言，非对称加密的强度要远远高于对称加密。

只签名不加密
通过Email向公众发布重大消息时可采用这种方法。消息内容本身是公开的，因此不用加密。数字签名则可用于校验消息来源，同时防止他人仿冒发件人身份发布虚假消息。

签名且加密
一切同时强调保密性和发件人身份验证的用例都应采用这种方法。


GnuPG
呼～经过一大段的科普，现在总算要进入正题了   
GnuPG（或称GPG），全称是GNU Personal Guard，它综合了上述的加密和签名技术，可为个人隐私数据提供可靠的保护。GnuPG是GNU对OpenPGP（或简称PGP）标准（RFC 4880）的实现。GnuPG剔除了OpenPGP标准中的专利算法，是完全开放和免费的开源软件。关于OpenPGP、PGP、GnuPG的关系和区别，可以参见RFC 4880 1.1节。另外，在对数据进行加密的同时GnuPG还会顺带进行压缩。
GnuPG软件本身虽然是跨平台的，但它只提供了一套命令行工具，对于普通用户而言操作过于繁琐。因此，各种GnuPG的图形前端被开发出来用以简化用户对隐私数据的加密、签名过程。以下分别对Windows和Ubuntu下的GnuPG图形前端的安装、配置和使用做一个简要介绍。
在Windows下使用GnuPG
软件配置
需要安装的软件如下：

GnuPG
从此处下载。GnuPG是整个解决方案的核心，提供了一系列实现加密、解密、签名、证书管理等功能的命令行工具。GnuPG Shell和GPGee都需要借助GnuPG才能工作。

GnuPG  Shell
从此处下载。GnuPG Shell是GnuPG的一个图形前端，可以较为方便地完成证书管理的工作。GnuPG Shell当然也可以用于文件的加密、解密和签名操作，但是其使用不如GPGee那样来得简单直接。


GPGee
从此处下载。GPGee全称为GPG explorer extension。它在Windows Explorer的右键菜单中增加了GPG相关的菜单项，使得用户可以非常方便地执行加密、解密和签名操作。但GPGee不提供GnuPG证书的管理功能，因此建议和GnuPG Shell配合使用。

菜单项“Sign &#38; Encrypt”和“Sign”的含义不言自明。菜单项“Encrypt (PK)”中的PK是Public Key（公钥）的缩写，选择该菜单项可对文件进行非对称加密（公钥加密）。相应的，“Encrypt (Symmetric)”用于对文件进行对称加密，类似于使用RAR或Zip制作加密压缩包。


如果先安装GnuPG再安装GnuPG Shell，则后者会自动检测出GnuPG的安装路径，无须另外的配置。GPGee则需要手工配置GnuPG相关路径。

在GPGee菜单项中选择“Configure”进入GPGee配置对话框，有三个文件的路径需要配置，包括：

GnuPG主程序 ：即GnuPG安装目录下的gpg.exe
公钥密钥环：位于%APPDATA%\gnupg\pubring.gpg
私钥密钥环：位于%APPDATA%\gnupg\secring.gpg

其中两个密钥环文件用于存储和管理GnuPG系统中所有已知的公钥和私钥。
生成密钥
借助GnuPG Shell可以方便地生成密钥：

选择菜单项“Keys/Generate New&#8230;”，打开密钥生成向导
设置新密钥的基本用户信息
选择“Advanced&#8230;”，设置密钥长度和有效期

密钥长度越长安全性越高，但加密、解密速度越慢。出于安全性考虑，推荐至少设置为2048。
密钥在有效期之后便会失效，不能再用于加密、解密或签名操作，必须重新生成密钥。默认有效期为永久有效。出于安全性考虑，推荐设置有效期为1年。


设置私钥通行码（passphrase）

通行码是用于保护私钥的密码，每次使用私钥进行加密、解密、签名操作时都需要输入通行码。
一旦私钥丢失，通行码将是保护私钥的最后一道屏障，因此应具备一定的长度和复杂度。



录入并确认以上信息后，GnuPG Shell便会开始生成密钥。根据用户选择的密钥长度和机器的性能，密钥生成时间从几十秒到几分钟不等。密钥生成完毕后，便可在GnuPG Shell的密钥列表中看到新密钥了。同时，你会看到新密钥有一个对应的8位的Key ID，它实际上唯一标识了新密钥的公钥。Key ID的作用将在后面提及。
公钥发布与密钥服务器
生成新密钥后，剩下的事情就是和你的朋友们互相交换公钥，然后你们便可以安全地进行数据交换了。
也许你已经意识到一个重要问题，就是公钥的发布过程可能被人做手脚：如果某人制作了一对密钥，并伪装成你的朋友来和你交换公钥，那么基于公私钥认证的整个信任体系便从根基上被瓦解了。因此，必须以某种可信的方式交换公钥——比如将公钥写在纸上交给对方。
然而非对称密钥往往有成百上千个字节那么长，将其抄到纸上或是敲到电脑里无疑是傻得要命的事情。你应该还记得刚才提到的Key ID吧？这时就是它发挥作用的时候了。如果我们把公钥都发布到一个公共的地方，然后只在纸上互相交换8位的Key ID，再用Key ID到这个公共的地方下载对应的公钥，就可以安全且方便地交换公钥了。而这个公共的地方，就是密钥服务器（Key Server）。GnuPG Shell内置了对密钥服务器的支持，可以直接向服务器发布新密钥或通过Key ID获取已经发不到服务器上的其他密钥。其使用方法也比较简单直接，这里就不赘述了。
如果你和你的朋友可以有效地互相确认身份，那么也可以在不借助密钥服务器的情况下手动导出、导入公钥来进行交换。方法是在GnuPG Shell密钥列表中选择新密钥并点击工具栏上的“导出”按钮。然后你便可以将导出的公钥文件发送给其他人，同时索要他们的公钥并导入GnuPG Shell。记住，除非你们能够有效地确认对方的身份，否则不要使用这种方式交换公钥！因为公钥文件在网络传输过程中可能被劫持和篡改。这也是为什么银行总是鼓励用户使用USB硬件证书（比如招行的U盾），因为将密钥以物理方式刻录在硬件中发布给用户在要比通过网络发送密钥安全得多，尤其是在用户随时可能处于不安全的网络中时更是如此。
在Ubuntu下使用GnuPG
Ubuntu用户很幸运，Seahorse作为一个通用的密码/密钥管理器，对GnuPG提供了良好的支持，同时也支持密钥服务器。更棒的是，Seahorse还提供了若干GNOME插件，实现了类似GPGee的右键菜单功能，算是非常整齐划一的解决方案  [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://www.gnupg.org/" target="_blank"><img class="alignright size-full wp-image-343" src="http://blog.liancheng.info/wp-content/uploads/2010/01/gpg-sign.png" alt="" width="120" height="120" /></a>之前在Twitter上说过，打算写一个个人数据安全解决方案的系列，内容包括：</p>
<ul>
<li>基于<a href="http://www.gnupg.org/" target="_blank">GnuPG</a>的个人隐私数据保护</li>
<li>自建XMPP服务器保障即时通讯安全</li>
<li><span style="text-decoration: line-through;">使用Dropbox进行较低密级的文件共享和协作</span></li>
</ul>
<blockquote><p>后记：后来觉得Dropbox这个话题太简单了点，没啥好写的，且重点在共享和协作，而非安全，便取消了。</p>
</blockquote>
<p>原本还打算写一写用SSH端口转发隧道建立SOCKS v5代理（俗称SSH翻墙术），鉴于网上已经有不少不错的介绍（<a href="http://www.chinagfw.org/2009/11/ubuntu.html" target="_blank">1</a>、<a href="http://rashost.com/putty-ssh-tunnel" target="_blank">2</a>），就不再重复劳动了。这里所采用的技术全部基于开源软件、免费软件或免费服务商，同时也兼顾使用体验。除了自建XMPP服务器所需的域名费用外，其余部分的经济成本为零。</p>
<p>跟丫头暂时还维持着北京、杭州两地分居的状况，网络是平时联络和数据交换的最为重要的手段。上述的这些技术都是我们目前正在使用的数据安全保障手段。这几篇文章的前身其实就是我写给丫头的操作手册。内容并不艰深，丫头并非计算机专业也能轻松掌握就是最佳佐证 <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_biggrin.gif' alt=':-D' class='wp-smiley' /> </p>
<p>另外，由于我平时主要使用Ubuntu，所以这里的所有方案都同时适用于Ubuntu和Windows。对Ubuntu以外的其他Linux发行版，这里就不兼顾了，不过方法都大同小异。</p>
<p>第一篇挑个复杂点儿的，讲讲GnuPG <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_cool.gif' alt='8-)' class='wp-smiley' /> </p>
<p><span id="more-338"></span></p>
<hr />
<p>本篇将讲述Windows和Ubuntu下的GnuPG解决方案，同时也会对GnuPG背后的信息安全原理做一些简单的科普。GnuPG可用于隐私数据的加密和签名，以便对其进行安全的传输和存储。除GnuPG外，本篇介绍的软件还包括Windows下的<a href="http://www.tech-faq.com/gnupg-shell.shtml" target="_blank">GnuPG Shell</a>、<a href="http://gpgee.excelcia.org/" target="_blank">GPGee</a>（GnuPG explorer extension）以及Ubuntu下的<a href="http://projects.gnome.org/seahorse/index.html" target="_blank">Seahorse</a>。</p>
<p>对于熟悉GnuPG加密、签名原理的读者，可以直接跳过科普的章节，直接阅读软件安装配置部分。</p>
<h1>缘由</h1>
<p>丫头平时记性都挺好，唯独记不住密码，各种帐号密码随设随忘。我则刚好反，平时啥啥都忘，唯独对各种随机字符串印象深刻。我自己的各个密码在各大网站的密码强度评估也从来都是绿色。所以她的各种账户密码基本上都靠我来记，想不起来了就给我发个查询请求。有时候不光是密码，偶尔还需要互相备份网银、支付宝之类的证书文件。这些都属于有高保密性要求的隐私数据。</p>
<p>丫头虽然不是计算机专业，不过在软件应用方面还是有相当的水准，我并不太担心她的机器上会有病毒或木马。所以在一定程度上说，密码、证书被盗的概率实际上并不高。然而墙内信息安全形势之恶劣已经是众所周知，通过IM或邮件明文传输密码、证书总是件让人不放心的事情。即使我们所使用的服务都号称对通讯做了加密，但出于众所周知的原因，国内的服务商们为了自身的生存，实际上都不得不在服务器端对解密后的明文数据做内容过滤和审查。因此也就存在着恶意个体或团体有目的的盗取用户明文数据的可能。作为程序员，总有种如坐针毡的不爽，必须防患于未然。这个想法促使我去寻求一个相对严密且切实可行的保障隐私数据安全的解决方案。</p>
<h1>问题描述</h1>
<p>解决信息安全问题的首要任务，就是确立威胁模型。简单来说就是将现实中存在的诸多威胁信息安全的因素分门别类，找出最需要解决的威胁因素。在这里，我们假设发件人和收件人的主机都是安全的，需要处理的威胁主要是来自数据传输通路上的数据监听、窃取、篡改和仿冒。</p>
<p>更精确一些，数据传输过程必须提供以下保障：</p>
<ol>
<li><strong>保密性</strong>：发件人所发出的消息必须是加密的，且只有特定的收件人才能够对密文进行解密</li>
<li><strong>完整性</strong>：发件人所发出的消息必须原封不动地抵达收件人那里，如果沿途惨遭篡改、删节，收件人必须能够发现</li>
<li><strong>身份验证</strong>：收件人能够验证发件人的身份，以保证其他人不能仿冒发件人的身份发送虚假消息</li>
</ol>
<h1>加密</h1>
<p>要保障保密性，最简单的方法莫过于使用加密压缩包（Zip、7Zip、RAR等常用压缩格式都支持加密）。然而要采用这种方法，首先要把解压密钥交到收件人手中。如何安全地交换解压密钥？这就陷入了一个死循环。</p>
<p>双方使用同样的密钥对数据进行加密和解密，这种加密方式被称作对称加密，对应的密钥就被称作对称密钥。常用的对称加密算法，如RC4、3DES、AES等，都具有高效的优点，即使是普通PC机也可以在短时间内使用这些算法对大量数据进行加解密。然而它们也都不可避免地具有加解密双方必须事先共享对称密钥的先天缺陷。正是这个缺陷导致了上面鸡生蛋蛋生鸡的窘境。</p>
<p>相对于对称加密，非对称加密（或称公钥加密）则很好地克服了这个缺陷。非对称加密需要使用不止一个，而是一对密钥——一个公钥和一个私钥。明文数据使用公私钥对中的一个密钥加密后，只能由对应的另一个密钥解密。应用非对称加密时，用户需要妥善保管私钥，不得外泄或丢失，同时公开公钥。</p>
<p>借助非对称加密，<strong>保密性</strong>基本得到解决。初始时，收件人生成一对密钥，并将公钥公布给发件人。双方收发消息时遵循一下流程：</p>
<ol> </ol>
<ul>
</ul>
<ol>
<li>发件人用收件人的公钥加密消息，生成密文并发送给收件人</li>
<li>收件人用自己的私钥解密消息密文，得到消息明文</li>
</ol>
<p>看起来不错，但还有个问题：非对称加密虽然克服了需要事先交换对称密钥的问题，但常用的非对称加密算法（如RSA）都非常慢，无法在短时间内加密较长的数据。假设我们要传输的不是密码这样的短文本，而是诸如照片等尺寸上兆的数据，上述过程就行不通了。对于这个问题，解决的方法也很简单。快速加解密是对称加密的强项，那么我们就仍然使用对称加密来对数据进行加密，转而使用非对称加密来对对称密钥进行加密。这样一来，上述过程就变成：</p>
<ul>
<li> 发件人
<ol>
<li>随机生成一个对称密钥</li>
<li>用对称密钥加密消息明文得到消息密文</li>
<li>用收件人的公钥加密对称密钥得到对称密钥密文</li>
<li>将消息密文和对称密钥密文一并发给收件人</li>
</ol>
</li>
<li>收件人
<ol>
<li>用私钥解密对称密钥密文得到对称密钥</li>
<li>用对称密钥解密消息密文得到消息明文 </li>
</ol>
</li>
</ul>
<p>好吧，我承认有点像绕口令，但其实还是蛮简单的  <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_mrgreen.gif' alt=':mrgreen:' class='wp-smiley' /> </p>
<h1>数字签名</h1>
<p>数字签名是一种用于鉴别发件人身份的技术。它主要应用了非对称加密和消息摘要。</p>
<p>消息摘要是将一段任意长度的消息转化成一个固定长度的短文本串的过程。对于信息安全领域所使用的摘要算法（如常用的MD5、SHA1），极难找到具有相同摘要的两段不同的消息。因此事实上只要消息摘要相同，就可认定消息相同。</p>
<p>对给定消息进行数字签名的过程如下：</p>
<ol>
<li>计算消息摘要</li>
<li>用自己的私钥加密消息摘要，得到数字签名</li>
<li>将数字签名与消息一起发送给收件人</li>
</ol>
<p>收件人收到消息后验证数字签名的过程如下：</p>
<ol>
<li>计算消息摘要</li>
<li>使用发件人的公钥解密数字签名，得到原始消息摘要</li>
<li>比对两个消息摘要</li>
</ol>
<p>若二者相同，即可证明该消息确实由拥有相应私钥的发件人发出，这就达到了<strong>身份验证</strong>的目的。</p>
<p>注意，在校验数字签名的过程中，消息<strong>完整性</strong>也得到了保证——由于数字签名中包含原始消息的摘要值，一旦消息遭到篡改，收件人计算出的消息摘要便会有别于数字签名中的原始消息摘要，从而导致验证失败。</p>
<p>至此，<strong>保密性</strong>、<strong>完整性</strong>和<strong>身份验证</strong>都圆满地得到了解决。完整的数据传输过程如下：</p>
<ul>
<li> 发件人
<ul>
<li>对消息进行数字签名
<ol>
<li>计算消息摘要</li>
<li>用自己的私钥加密消息摘要，得到数字签名</li>
</ol>
</li>
<li>加密签名后的消息
<ol>
<li>随机生成一个对称密钥</li>
<li>用对称密钥加密消息和数字签名，得到消息密文</li>
<li>用收件人的公钥加密对称密钥，得到对称密钥密文</li>
<li>将对称密钥密文和消息密文合并后发送给收件人</li>
</ol>
</li>
</ul>
</li>
<li>收件人
<ul>
<li>解密
<ol>
<li>用自己的私钥解密对称密钥</li>
<li>用对称密钥解密消息密文得到消息明文和数字签名</li>
</ol>
</li>
<li>校验数字签名
<ol>
<li>计算消息摘要</li>
<li>用发件人的公钥解密数字签名，得到原始消息摘要</li>
<li>比对两个消息摘要</li>
</ol>
</li>
</ul>
</li>
</ul>
<p>以上简要解释了加密和签名的密码学原理。它们是我们的个人数据安全解决方案中最为核心的两个操作。实际上，日常生活中很多常用的信息安全保障手段，如SSL/TLS证书、网银证书、支付宝证书等等，也都在是以这两个操作为核心的。</p>
<p>在实际应用中，可以根据需要对加密和签名操作进行组合，例如：</p>
<ul>
<li>只加密不签名
<p>对隐秘文件进行备份时可采用这种方法。因为日后读取该文件的还是你自己，所以签名与否并不重要。值得注意的是，非对称密钥的长度往往很长，一般至少在1024 bit（128字节）以上，而用户选择的对称密钥则往往只有几个或十几个字节。因此一般而言，非对称加密的强度要远远高于对称加密。</p>
</li>
<li>只签名不加密
<p>通过Email向公众发布重大消息时可采用这种方法。消息内容本身是公开的，因此不用加密。数字签名则可用于校验消息来源，同时防止他人仿冒发件人身份发布虚假消息。</p>
</li>
<li>签名且加密
<p>一切同时强调保密性和发件人身份验证的用例都应采用这种方法。</p>
</li>
</ul>
<h1>GnuPG</h1>
<p>呼～经过一大段的科普，现在总算要进入正题了  <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_razz.gif' alt=':-P' class='wp-smiley' /> </p>
<p>GnuPG（或称GPG），全称是GNU Personal Guard，它综合了上述的加密和签名技术，可为个人隐私数据提供可靠的保护。GnuPG是GNU对OpenPGP（或简称PGP）标准（<a href="http://tools.ietf.org/html/rfc4880" target="_blank">RFC 4880</a>）的实现。GnuPG剔除了OpenPGP标准中的专利算法，是完全开放和免费的开源软件。关于OpenPGP、PGP、GnuPG的关系和区别，可以参见<a href="http://tools.ietf.org/html/rfc4880#section-1.1" target="_blank">RFC 4880 1.1节</a>。另外，在对数据进行加密的同时GnuPG还会顺带进行压缩。</p>
<p>GnuPG软件本身虽然是跨平台的，但它只提供了一套命令行工具，对于普通用户而言操作过于繁琐。因此，各种GnuPG的图形前端被开发出来用以简化用户对隐私数据的加密、签名过程。以下分别对Windows和Ubuntu下的GnuPG图形前端的安装、配置和使用做一个简要介绍。</p>
<h2>在Windows下使用GnuPG</h2>
<h3>软件配置</h3>
<p>需要安装的软件如下：</p>
<ol>
<li>GnuPG
<p>从<a href="ftp://ftp.gnupg.org/gcrypt/binary/gnupg-w32cli-1.4.10b.exe" target="_blank">此处</a>下载。GnuPG是整个解决方案的核心，提供了一系列实现加密、解密、签名、证书管理等功能的命令行工具。GnuPG Shell和GPGee都需要借助GnuPG才能工作。</p>
</li>
<li>GnuPG  Shell
<p>从<a href="http://www.tech-faq.com/gnupg-shell/gnupgshell-1.0.0.windows.zip" target="_blank">此处</a>下载。GnuPG Shell是GnuPG的一个图形前端，可以较为方便地完成证书管理的工作。GnuPG Shell当然也可以用于文件的加密、解密和签名操作，但是其使用不如GPGee那样来得简单直接。</p>
<p><a href="http://blog.liancheng.info/wp-content/uploads/2010/01/gnupg-shell.bmp"><img class="aligncenter size-full wp-image-392" title="GnuPG Shell" src="http://blog.liancheng.info/wp-content/uploads/2010/01/gnupg-shell.bmp" alt="" /></a></p>
</li>
<li>GPGee
<p>从<a href="http://www.excelcia.org/modules.php?name=Downloads&amp;d_op=getit&amp;lid=58" target="_blank">此处</a>下载。GPGee全称为GPG explorer extension。它在Windows Explorer的右键菜单中增加了GPG相关的菜单项，使得用户可以非常方便地执行加密、解密和签名操作。但GPGee不提供GnuPG证书的管理功能，因此建议和GnuPG Shell配合使用。</p>
<p><a href="http://blog.liancheng.info/wp-content/uploads/2010/01/gpgee-menu.png"><img class="aligncenter size-full wp-image-389" title="GPGee menu" src="http://blog.liancheng.info/wp-content/uploads/2010/01/gpgee-menu.png" alt="" width="359" height="319" /></a></p>
<p>菜单项“Sign &amp; Encrypt”和“Sign”的含义不言自明。菜单项“Encrypt (PK)”中的PK是Public Key（公钥）的缩写，选择该菜单项可对文件进行非对称加密（公钥加密）。相应的，“Encrypt (Symmetric)”用于对文件进行对称加密，类似于使用RAR或Zip制作加密压缩包。</p>
</li>
</ol>
<p>如果先安装GnuPG再安装GnuPG Shell，则后者会自动检测出GnuPG的安装路径，无须另外的配置。GPGee则需要手工配置GnuPG相关路径。</p>
<p style="text-align: center;"><a href="http://blog.liancheng.info/wp-content/uploads/2010/01/gpgee-config1.png"><img class="aligncenter size-full wp-image-387" title="Configure GPGee" src="http://blog.liancheng.info/wp-content/uploads/2010/01/gpgee-config1.png" alt="" width="478" height="324" /></a></p>
<p style="text-align: justify;">在GPGee菜单项中选择“Configure”进入GPGee配置对话框，有三个文件的路径需要配置，包括：</p>
<ul>
<li>GnuPG主程序<span style="font-family: courier new,courier;"> </span>：即GnuPG安装目录下的<span style="font-family: courier new,courier;">gpg.exe</span></li>
<li>公钥密钥环：位于<span style="font-family: courier new,courier;">%APPDATA%\gnupg\pubring.gpg</span></li>
<li>私钥密钥环：位于<span style="font-family: courier new,courier;">%APPDATA%\gnupg\secring.gpg</span></li>
</ul>
<p>其中两个密钥环文件用于存储和管理GnuPG系统中所有已知的公钥和私钥。</p>
<h3>生成密钥</h3>
<p>借助GnuPG Shell可以方便地生成密钥：</p>
<ul>
<li>选择菜单项“<span style="text-decoration: underline;">K</span>eys/Generate <span style="text-decoration: underline;">N</span>ew&#8230;”，打开密钥生成向导</li>
<li>设置新密钥的基本用户信息</li>
<li>选择“Ad<span style="text-decoration: underline;">v</span>anced&#8230;”，设置密钥长度和有效期
<ul>
<li>密钥长度越长安全性越高，但加密、解密速度越慢。出于安全性考虑，推荐至少设置为2048。</li>
<li>密钥在有效期之后便会失效，不能再用于加密、解密或签名操作，必须重新生成密钥。默认有效期为永久有效。出于安全性考虑，推荐设置有效期为1年。</li>
</ul>
</li>
<li>设置私钥通行码（passphrase）
<ul>
<li>通行码是用于保护私钥的密码，每次使用私钥进行加密、解密、签名操作时都需要输入通行码。</li>
<li>一旦私钥丢失，通行码将是保护私钥的最后一道屏障，因此应具备一定的长度和复杂度。</li>
</ul>
</li>
</ul>
<p>录入并确认以上信息后，GnuPG Shell便会开始生成密钥。根据用户选择的密钥长度和机器的性能，密钥生成时间从几十秒到几分钟不等。密钥生成完毕后，便可在GnuPG Shell的密钥列表中看到新密钥了。同时，你会看到新密钥有一个对应的8位的Key ID，它实际上唯一标识了新密钥的公钥。Key ID的作用将在后面提及。</p>
<h3>公钥发布与密钥服务器</h3>
<p>生成新密钥后，剩下的事情就是和你的朋友们互相交换公钥，然后你们便可以安全地进行数据交换了。</p>
<p>也许你已经意识到一个重要问题，就是公钥的发布过程可能被人做手脚：如果某人制作了一对密钥，并伪装成你的朋友来和你交换公钥，那么基于公私钥认证的整个信任体系便从根基上被瓦解了。因此，必须以某种可信的方式交换公钥——比如将公钥写在纸上交给对方。</p>
<p>然而非对称密钥往往有成百上千个字节那么长，将其抄到纸上或是敲到电脑里无疑是傻得要命的事情。你应该还记得刚才提到的Key ID吧？这时就是它发挥作用的时候了。如果我们把公钥都发布到一个公共的地方，然后只在纸上互相交换8位的Key ID，再用Key ID到这个公共的地方下载对应的公钥，就可以安全且方便地交换公钥了。而这个公共的地方，就是密钥服务器（Key Server）。GnuPG Shell内置了对密钥服务器的支持，可以直接向服务器发布新密钥或通过Key ID获取已经发不到服务器上的其他密钥。其使用方法也比较简单直接，这里就不赘述了。</p>
<p>如果你和你的朋友可以有效地互相确认身份，那么也可以在不借助密钥服务器的情况下手动导出、导入公钥来进行交换。方法是在GnuPG Shell密钥列表中选择新密钥并点击工具栏上的“导出”按钮。然后你便可以将导出的公钥文件发送给其他人，同时索要他们的公钥并导入GnuPG Shell。记住，除非你们能够有效地确认对方的身份，否则不要使用这种方式交换公钥！因为公钥文件在网络传输过程中可能被劫持和篡改。这也是为什么银行总是鼓励用户使用USB硬件证书（比如招行的U盾），因为将密钥以物理方式刻录在硬件中发布给用户在要比通过网络发送密钥安全得多，尤其是在用户随时可能处于不安全的网络中时更是如此。</p>
<h2>在Ubuntu下使用GnuPG</h2>
<p>Ubuntu用户很幸运，Seahorse作为一个通用的密码/密钥管理器，对GnuPG提供了良好的支持，同时也支持密钥服务器。更棒的是，Seahorse还提供了若干GNOME插件，实现了类似GPGee的右键菜单功能，算是非常整齐划一的解决方案  <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_biggrin.gif' alt=':-D' class='wp-smiley' /> </p>
<p><a href="http://blog.liancheng.info/wp-content/uploads/2010/01/seahorse.png"><img class="aligncenter size-full wp-image-420" title="Seahorse" src="http://blog.liancheng.info/wp-content/uploads/2010/01/seahorse.png" alt="" width="555" height="286" /></a></p>
<p>安装过程非常简单：</p>

<div class="wp_syntax"><div class="code"><pre class="text" style="font-family:monospace;">sudo aptitude install gnupg seahorse seahorse-plugins</pre></div></div>

<p>注意生成密钥时选择PGP密钥。其他步骤和在Windows下使用GnuPG Shell基本相同，就不赘述了。</p>
<h1>结语</h1>
<p>OpenPGP标准以及GnuPG作为成熟的个人隐私数据安全保障方案，已经有十多年的历史了。它们也经常被集成在邮件客户端中，为Email服务提供加密和数字签名支持。然而各种邮件客户端的PGP支持在质量和兼容性上参差不齐，用起来往往问题多多。比如我曾在Thunderbird中使用过Enigmail，结果遇到了让人头疼不已的中文编码问题，好不容易解决后，又发现跟同事使用的Outlook、Foxmail存在兼容性问题，最终不得不放弃。尔后我又尝试在Firefox中使用FireGPG访问Gmail，更是令Firefox直接崩溃……这也是我不选择Email PGP方案转而直接采用更为通用的GnuPG的原因。</p>
<p>记得前几年，学校里曾经刮起过一阵互换PGP/GPG Key ID的风潮。可怜我当时连PGP/GPG为何物都不知道。不想现在却和丫头用得不亦乐乎。</p>
<p>最后，希望这篇拙文能够在恶劣的大环境下帮助到更多人安全地完成数据交换，也就不枉我码这么多字的辛苦了  <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_cool.gif' alt='8-)' class='wp-smiley' /> </p>
<hr />
<blockquote><p>P.S.: 第一次给图片打马赛克，GIMP的高斯模糊滤镜还真是蛮好用的 <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_mrgreen.gif' alt=':mrgreen:' class='wp-smiley' /> </p>
</blockquote>
<!-- Jaxl IM embed starts -->
<script type="text/javascript">
(function() {
var jaxlChat = document.createElement("script");
jaxlChat.type = "text/javascript";
jaxlChat.async = true;
jaxlChat.src = "http://im.jaxl.im/ui/jaxl.php";
(document.getElementsByTagName("head")[0]||document.getElementsByTagName("body")[0]).appendChild(jaxlChat);
})();
</script>
<!-- Jaxl IM embed ends --><img src="http://feeds.feedburner.com/~r/liancheng/~4/xFGeu15TstU" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://blog.liancheng.info/?feed=rss2&amp;p=338</wfw:commentRss>
		<slash:comments>19</slash:comments>
		<feedburner:origLink>http://blog.liancheng.info/?p=338</feedburner:origLink></item>
		<item>
		<title>尝试用urllib2和PyXMPP同步Twitter和校内状态</title>
		<link>http://feedproxy.google.com/~r/liancheng/~3/hoLegl-PZSI/</link>
		<comments>http://blog.liancheng.info/?p=280#comments</comments>
		<pubDate>Sun, 17 Jan 2010 17:45:06 +0000</pubDate>
		<dc:creator>liancheng</dc:creator>
				<category><![CDATA[For fun]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[RenRen]]></category>
		<category><![CDATA[Twitter]]></category>
		<category><![CDATA[XiaoNei]]></category>
		<category><![CDATA[XMPP]]></category>

		<guid isPermaLink="false">http://blog.liancheng.info/?p=280</guid>
		<description><![CDATA[在翻墙技能还不熟练，同时Twitter上好友也还很稀少的那么一段日子里，一度拿校内状态当作Twitter使用。上周末，看到@Jun_Yu的这么一推：
发现一件有意思的事儿 好多有意思的推被转到校内 然后又被有的推友贴上&#8221;转自校内&#8221;的标签重新在这儿疯狂rt

于是想起之前发现校内桌面采用的是标准XMPP协议，且校内状态的更新是采用XMPP Presence实现的，便回了一句：
校内的IM是基于XMPP的，理论上只需要发一条&#60;presence/&#62;消息就可以修改校内状态，可以做一个校内和Twitter同步的工具  

然后@2325bt便推荐了这篇将Twitter自动同步到Facebook、饭否、校内、海内等网站的方法。不过这个办法严重依赖于嘀哒。然而，在墙内的网络环境下，任何和Twitter关系密切的Web服务只怕都难得长寿。而且，对于Twitter/校内状态同步这个简单需求而言，用于完成多方同步的嘀哒不免牛刀样十足。既然人家校内十分友好地采用了开放的XMPP，那么求人不如求己。 这两年一直对XMPP保持高度关注，却一直没有机会实际接触相关项目，权当练手。而且说实在的，自己在校内和Twitter上好友群体差异比较大，个人对这个功能其实没啥需求，just for fun。  对我而言，完成这个任务的最佳工具是Python。最原始的想法是写一个脚本，用于：

从终端读入帐号信息和一条消息
用urllib2登录Twitter发推
用PyXMPP登录校内发送&#60;presence/&#62;消息更新状态

估摸着复杂度不高，于是放言应该可以在50行以内搞定，心想最多一个晚上也就差不多了。然而事实证明我过于乐观了。仅仅是在Twitter上，我这个可耻的HTTP盲就撞了两次南墙：

居然忘了自己身在墙内——只好把脚本拷到DH服务器上在墙外测试Twitter部分。这意味着要想让该脚本步入实用阶段，至少要支持Twitter API自定义，而此前我对Twitter API一无所知。
非常可耻地完全不了解HTTP Basic Authentication——亏得这篇urllib2手册的指点，花了些时间现学了些HTTP基本知识，总算用urllib2发推成功。

最终实现的Twitter部分的功能：采用最简单的Basic Authentication登录，然后调用API statuses/update发推。代码直截了当：

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/python
&#160;
import urllib2
from sys import argv
&#160;
TWITTER_API_STATUSES_UPDATE = 'http://twitter.com/statuses/update.xml'
TWITTER_BASIC_AUTH_REALM = 'Twitter API'
&#160;
def update&#40; user, password, message &#41;:
    auth_handler = urllib2.HTTPBasicAuthHandler&#40;&#41;
    auth_handler.add_password&#40; realm=TWITTER_BASIC_AUTH_REALM,
               [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://xmpp.org/"><img class="size-full wp-image-282 alignleft" src="http://blog.liancheng.info/wp-content/uploads/2010/01/xmpp-logo.jpeg" alt="" width="104" height="104" /></a>在翻墙技能还不熟练，同时Twitter上好友也还很稀少的那么一段日子里，一度拿校内状态当作Twitter使用。上周末，看到<a href="http://twitter.com/Jun_Yu/" target="_blank">@Jun_Yu</a>的<a href="http://twitter.com/Jun_Yu/status/7553900526" target="_blank">这么一推</a>：</p>
<blockquote><p>发现一件有意思的事儿 好多有意思的推被转到校内 然后又被有的推友贴上&#8221;转自校内&#8221;的标签重新在这儿疯狂rt</p>
</blockquote>
<p>于是想起之前发现<a href="http://im.renren.com/?rrpchomepg=1001" target="_blank">校内桌面</a>采用的是标准XMPP协议，且校内状态的更新是采用XMPP Presence实现的，便回了一句：</p>
<blockquote><p>校内的IM是基于XMPP的，理论上只需要发一条<span style="font-family: courier new,courier;">&lt;presence/&gt;</span>消息就可以修改校内状态，可以做一个校内和Twitter同步的工具 <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley' /> </p>
</blockquote>
<p>然后<a href="http://twitter.com/2325bt/" target="_blank">@2325bt</a>便推荐了这篇<a href="http://www.mixfog.com/blog/2009/05/twitter-sync-to-another-sns.htm" target="_blank">将Twitter自动同步到Facebook、饭否、校内、海内等网站的方法</a>。不过这个办法严重依赖于<a href="http://digufeed.com/">嘀哒</a>。然而，在墙内的网络环境下，任何和Twitter关系密切的Web服务只怕都难得长寿。而且，对于Twitter/校内状态同步这个简单需求而言，用于完成多方同步的嘀哒不免牛刀样十足。既然人家校内十分友好地采用了开放的XMPP，那么求人不如求己。<span id="more-280"></span> 这两年一直对XMPP保持高度关注，却一直没有机会实际接触相关项目，权当练手。而且说实在的，自己在校内和Twitter上好友群体差异比较大，个人对这个功能其实没啥需求，just for fun。  对我而言，完成这个任务的最佳工具是Python。最原始的想法是写一个脚本，用于：</p>
<ul>
<li>从终端读入帐号信息和一条消息</li>
<li>用<a href="http://docs.python.org/library/urllib2.html" target="_blank">urllib2</a>登录Twitter发推</li>
<li>用<a href="http://pyxmpp.jajcus.net/" target="_blank">PyXMPP</a>登录校内发送<span style="font-family: courier new,courier;">&lt;presence/&gt;</span>消息更新状态</li>
</ul>
<p>估摸着复杂度不高，于是放言<a href="http://twitter.com/liancheng/status/7555900211" target="_blank">应该可以在50行以内搞定</a>，心想最多一个晚上也就差不多了。然而事实证明我过于乐观了。仅仅是在Twitter上，我这个可耻的HTTP盲就撞了两次南墙：</p>
<ol>
<li>居然忘了自己身在墙内——只好把脚本拷到DH服务器上在墙外测试Twitter部分。这意味着要想让该脚本步入实用阶段，至少要支持Twitter API自定义，而此前我对Twitter API一无所知。</li>
<li>非常可耻地完全不了解HTTP Basic Authentication——亏得<a href="http://www.voidspace.org.uk/python/articles/urllib2.shtml#id5" target="_blank">这篇urllib2手册</a>的指点，花了些时间现学了些HTTP基本知识，总算用urllib2发推成功。</li>
</ol>
<p>最终实现的Twitter部分的功能：采用最简单的Basic Authentication登录，然后调用API <a href="http://apiwiki.twitter.com/Twitter-REST-API-Method%3A-statuses%C2%A0update" target="_blank">statuses/update</a>发推。代码直截了当：</p>

<div class="wp_syntax"><table><tr><td class="line_numbers"><pre>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="code"><pre class="python" style="font-family:monospace;"><span style="color: #808080; font-style: italic;">#!/usr/bin/python</span>
&nbsp;
<span style="color: #ff7700;font-weight:bold;">import</span> <span style="color: #dc143c;">urllib2</span>
<span style="color: #ff7700;font-weight:bold;">from</span> <span style="color: #dc143c;">sys</span> <span style="color: #ff7700;font-weight:bold;">import</span> argv
&nbsp;
TWITTER_API_STATUSES_UPDATE = <span style="color: #483d8b;">'http://twitter.com/statuses/update.xml'</span>
TWITTER_BASIC_AUTH_REALM = <span style="color: #483d8b;">'Twitter API'</span>
&nbsp;
<span style="color: #ff7700;font-weight:bold;">def</span> update<span style="color: black;">&#40;</span> <span style="color: #dc143c;">user</span>, password, message <span style="color: black;">&#41;</span>:
    auth_handler = <span style="color: #dc143c;">urllib2</span>.<span style="color: black;">HTTPBasicAuthHandler</span><span style="color: black;">&#40;</span><span style="color: black;">&#41;</span>
    auth_handler.<span style="color: black;">add_password</span><span style="color: black;">&#40;</span> realm=TWITTER_BASIC_AUTH_REALM,
                               uri=TWITTER_API_STATUSES_UPDATE,
                               <span style="color: #dc143c;">user</span>=<span style="color: #dc143c;">user</span>,
                               passwd=password <span style="color: black;">&#41;</span>
&nbsp;
    opener = <span style="color: #dc143c;">urllib2</span>.<span style="color: black;">build_opener</span><span style="color: black;">&#40;</span> auth_handler <span style="color: black;">&#41;</span>
    <span style="color: #dc143c;">urllib2</span>.<span style="color: black;">install_opener</span><span style="color: black;">&#40;</span> opener <span style="color: black;">&#41;</span>
    <span style="color: #dc143c;">urllib2</span>.<span style="color: black;">urlopen</span><span style="color: black;">&#40;</span> TWITTER_API_STATUSES_UPDATE, <span style="color: #483d8b;">'status='</span> + message <span style="color: black;">&#41;</span>
&nbsp;
<span style="color: #ff7700;font-weight:bold;">if</span> __name__ == <span style="color: #483d8b;">'__main__'</span>:
    <span style="color: #dc143c;">user</span>, password, message = argv<span style="color: black;">&#91;</span> <span style="color: #ff4500;">1</span> <span style="color: black;">&#93;</span>, argv<span style="color: black;">&#91;</span> <span style="color: #ff4500;">2</span> <span style="color: black;">&#93;</span>, argv<span style="color: black;">&#91;</span> <span style="color: #ff4500;">3</span> <span style="color: black;">&#93;</span>
    update<span style="color: black;">&#40;</span> <span style="color: #dc143c;">user</span>, password, message <span style="color: black;">&#41;</span></pre></td></tr></table></div>

<p>功能算是实现了，不过不支持Twitter API，致使在墙内实用性全无。另外最好能支持<a href="http://apiwiki.twitter.com/OAuth-FAQ" target="_blank">OAuth</a>。然而在打算进一步增加功能时，想起应该先看看有没有现成的，于是发现Twitter官方果然列出了<a href="http://apiwiki.twitter.com/Libraries#Python" target="_blank">若干个Python Twitter API库</a>。考虑到继续做下去最多也就是再捣腾出另一个类似的东西，顿时兴趣索然。于是决定就这么将就着用了，真有高级功能需求的话，直接用现成的库来实现好了。至此，Twitter部分算是告一段落。</p>
<p>折腾PyXMPP的过程却也并不顺利。首先简单介绍一些XMPP的背景知识和校内XMPP服务的部署。按照RFC 3920 XMPP Core的约定，一个XMPP用户可以由一个JID（Jabber ID，Jabber是XMPP的前身）唯一标识，其格式为<span style="font-family: courier new,courier;">id@domain/resource</span>。RFC 3920将形如<span style="font-family: courier new,courier;">id@domain/resource</span>的JID称为完整JID（full JID），而将形如<span style="font-family: courier new,courier;">id@domain</span>的JID称为裸JID（bare JID）。其中<span style="font-family: courier new,courier;">domain</span>（域）唯一标识一套XMPP服务器（这个说法并不准确，但在此处无碍），<span style="font-family: courier new,courier;">id@domain</span>则可标识该服务器账户系统中的一个用户，<span style="font-family: courier new,courier;">而resource</span>可用于在同一帐号的多个登录会话中唯一标识一个登录会话，内容可由用户指定。例如对于同一个XMPP用户<span style="font-family: courier new,courier;">micky@disney.im</span>，可以在办公室和家中分别以<span style="font-family: courier new,courier;">micky@disney.im/office</span>和<span style="font-family: courier new,courier;">micky@disney.im/home</span>同时登录。如果客户端不指定<span style="font-family: courier new,courier;">resource</span>，服务器会为其分配一个，一般是一个随机串。裸JID看起来和Email地址是一模一样的，这对于同时提供Email和XMPP服务的服务商来说就很方便，比如Gmail/Gtalk之于Google。</p>
<p>由于校内后来更名为人人网，校内实际上持有两个XMPP域：<span style="font-family: courier new,courier;">talk.xiaonei.com</span>和<span style="font-family: courier new,courier;">talk.renren.com</span>。二域并存，应该是为了向下兼容更名前发布的旧版本客户端。校内的帐号也是基于Email地址的，这是否意味着你可以直接使用校内的注册Email登录校内的XMPP服务呢？答案是否定的。校内并不限制Email的域，gmail、163、sina等等应有尽有，而校内是绝然不可能拥有这些域的使用权的。解决的办法很简单：校内中每个用户的注册邮箱都被分配了一个全域唯一的数字ID，校内桌面也就是用形如<span style="font-family: courier new,courier;">123456@talk.renren.com</span>这样的裸JID来登录XMPP服务器的。要得到这个数字ID很简单：登录你的校内帐号，个人主页URL末尾的那串数字便是这个ID。</p>
<p>拿到裸JID后，第一步就是借助PyXMPP来登录校内XMPP帐号。头一回用PyXMPP，翻文档，接口如云。心想，对于登录这样的常见任务，应该有简化接口吧。果然让我找到了<a href="http://pyxmpp.jajcus.net/api/pyxmpp.jabber.simple-module.html#xmpp_do" target="_blank"><span style="font-family: courier new,courier;">pyxmpp.jabber.simple.xmpp_do</span></a>：</p>

<div class="wp_syntax"><div class="code"><pre class="python" style="font-family:monospace;">xmpp_do<span style="color: black;">&#40;</span>jid, password, function, server=<span style="color: #008000;">None</span>, port=<span style="color: #008000;">None</span><span style="color: black;">&#41;</span></pre></div></div>

<p>其文档描述为：</p>
<blockquote><p>Connect as client to a Jabber/XMPP server and call the provided function when stream is ready for IM.</p>
</blockquote>
<p>这就好办了，顺手写下一段测试代码（登录指定帐号后打印“hello”）：</p>

<div class="wp_syntax"><div class="code"><pre class="python" style="font-family:monospace;"><span style="color: #ff7700;font-weight:bold;">def</span> foo<span style="color: black;">&#40;</span> stream <span style="color: black;">&#41;</span>:
    <span style="color: #ff7700;font-weight:bold;">print</span> <span style="color: #483d8b;">'hello'</span>
&nbsp;
xmpp_do<span style="color: black;">&#40;</span> JID<span style="color: black;">&#40;</span> <span style="color: #483d8b;">'123546@talk.renren.com/python'</span>, <span style="color: #483d8b;">'secret'</span>, foo <span style="color: black;">&#41;</span> <span style="color: black;">&#41;</span></pre></div></div>

<p>执行、登录失败、挠头、看文档、阅读源码、复习RFC 3920、日志调试……这个问题足足block了我好几个钟头——不过80%的时间花费是由于调试受挫转而去看了大半季的TBBT <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_wink.gif' alt=';-)' class='wp-smiley' />  言归正传，最终我发现上面这短短三行代码其实包含了两个错误：</p>
<ol>
<li>TLS和SASL
<p>校内XMPP服务器要求使用TLS加密。虽然<a href="http://xmpp.org/rfcs/rfc3920.html#tls" target="_blank">RFC 3920</a>规定，XMPP客户端必须使用SASL并应该在SASL认证前使用TLS对数据流进行加密，但默认情况下<a href="http://pyxmpp.jajcus.net/api/pyxmpp.jabber.client.JabberClient-class.html#__init__">PyXMPP不启用TLS</a><span style="font-family: courier new,courier;">，xmpp_do</span>在登录时采用的是默认设置，因此无法通过协商，进而无法登录。这个问题是参考PyXMPP的echobot示例，输出了PyXMPP的调试日志后发现的。</p>
<p>在<a href="http://www.stillhq.com/google/gtalk/" target="_blank">这篇文章</a>的帮助下，从<a href="http://pyxmpp.jajcus.net/api/pyxmpp.jabber.client.JabberClient-class.html" target="_blank">pyxmpp.jabber.client.JabberClient</a>派生了自定义的XMPP客户端类，增加了TLS设置和SASL认证设置，问题解决：</p>

<div class="wp_syntax"><div class="code"><pre class="python" style="font-family:monospace;">    <span style="color: #ff7700;font-weight:bold;">class</span> R2Client<span style="color: black;">&#40;</span> JabberClient <span style="color: black;">&#41;</span>:
        <span style="color: #ff7700;font-weight:bold;">def</span> <span style="color: #0000cd;">__init__</span><span style="color: black;">&#40;</span> <span style="color: #008000;">self</span>, jid, password <span style="color: black;">&#41;</span>:
            tls = streamtls.<span style="color: black;">TLSSettings</span><span style="color: black;">&#40;</span> require=<span style="color: #008000;">True</span>, verify_peer=<span style="color: #008000;">False</span> <span style="color: black;">&#41;</span>
            auth = <span style="color: black;">&#91;</span><span style="color: #483d8b;">'sasl:PLAIN'</span><span style="color: black;">&#93;</span>
            JabberClient.<span style="color: #0000cd;">__init__</span><span style="color: black;">&#40;</span> <span style="color: #008000;">self</span>, jid, password, tls_settings=tls,
                                   auth_methods=auth <span style="color: black;">&#41;</span></pre></div></div>

</li>
<li>XMPP域
<p>修正TLS的问题后再次尝试，却得到一个莫名其妙的<span style="font-family: courier new,courier;">pyxmpp.exceptions.HostMismatch</span>。该异常类的文档中没有任何说明，只好再次翻<a href="http://pyxmpp.jajcus.net/api/pyxmpp.streambase-pysrc.html#StreamBase.stream_start" target="_blank">源码</a>。结合日志，发现问题出在XMPP域上。考虑到校内已经正式更名为人人网，在上面的代码中我采用的XMPP域是<span style="font-family: courier new,courier;">talk.renren.com</span>。观察调试日志，PyXMPP发起连接时向服务器发送的stream header为：</p>

<div class="wp_syntax"><div class="code"><pre class="xml" style="font-family:monospace;"><span style="color: #009900;"><span style="color: #000000; font-weight: bold;">&lt;?xml</span> <span style="color: #000066;">version</span>=<span style="color: #ff0000;">&quot;1.0&quot;</span> <span style="color: #000066;">encoding</span>=<span style="color: #ff0000;">&quot;UTF-8&quot;</span><span style="color: #000000; font-weight: bold;">?&gt;</span></span>
<span style="color: #009900;"><span style="color: #000000; font-weight: bold;">&lt;stream:stream</span></span>
<span style="color: #009900;">  <span style="color: #000066;">xmlns:stream</span>=<span style="color: #ff0000;">&quot;http://etherx.jabber.org/streams&quot;</span></span>
<span style="color: #009900;">  <span style="color: #000066;">xmlns</span>=<span style="color: #ff0000;">&quot;jabber:client&quot;</span></span>
<span style="color: #009900;">  <span style="color: #000066;">to</span>=<span style="color: #ff0000;">&quot;talk.renren.com&quot;</span></span>
<span style="color: #009900;">  <span style="color: #000066;">version</span>=<span style="color: #ff0000;">&quot;1.0&quot;</span><span style="color: #000000; font-weight: bold;">&gt;</span></span></pre></div></div>

<p>服务器回复的stream header为：</p>

<div class="wp_syntax"><div class="code"><pre class="xml" style="font-family:monospace;"><span style="color: #009900;"><span style="color: #000000; font-weight: bold;">&lt;?xml</span> <span style="color: #000066;">version</span>=<span style="color: #ff0000;">'1.0'</span><span style="color: #000000; font-weight: bold;">?&gt;</span></span>
<span style="color: #009900;"><span style="color: #000000; font-weight: bold;">&lt;stream:stream</span></span>
<span style="color: #009900;">  <span style="color: #000066;">from</span>=<span style="color: #ff0000;">'talk.xiaonei.com'</span></span>
<span style="color: #009900;">  <span style="color: #000066;">xmlns</span>=<span style="color: #ff0000;">'jabber:client'</span></span>
<span style="color: #009900;">  <span style="color: #000066;">xmlns:stream</span>=<span style="color: #ff0000;">'http://etherx.jabber.org/streams'</span></span>
<span style="color: #009900;">  <span style="color: #000066;">version</span>=<span style="color: #ff0000;">'1.0'</span><span style="color: #000000; font-weight: bold;">&gt;</span></span></pre></div></div>

<p>注意客户端发送的stream header的<span style="font-family: courier new,courier;">to</span>为<span style="font-family: courier new,courier;">talk.renren.com</span>，而服务器回复的stream header的<span style="font-family: courier new,courier;">from</span>却是<span style="font-family: courier new,courier;">talk.xiaonei.com</span>。而PyXMPP在这里做了一个判断，当客户端发送的stream header中的<span style="font-family: courier new,courier;">to</span>和服务器的stream header中的<span style="font-family: courier new,courier;">from</span>不符时，便抛出<span style="font-family: courier new,courier;">HostMismatch</span>异常。</p>
<p>找到了症结就好解决，把XMPP域改为<span style="font-family: courier new,courier;">talk.xiaonei.com</span>即可。至此，终于登录成功。</p>
<p>不过，在XMPP域的这个问题上，校内和PyXMPP的做法都欠妥。RFC 3920并未规定在stream建立过程中接收方stream header的<span style="font-family: courier new,courier;">from</span>字段必须和发起方stream header的<span style="font-family: courier new,courier;">to</span>字段吻合。因此PyXMPP在判断二者不相符时抛出异常导致连接断开的行为是不合适的。而校内这样做的动机，应该是为了对校内更名为人人网之前发布出去的旧版本客户端做兼容。在这个场景下，更合适的做法是由<span style="font-family: courier new,courier;">talk.renren.com</span>的XMPP服务器向客户端返回一 个<span style="font-family: courier new,courier;">&lt;see-other-host/&gt;</span>错误，同时将正确的XMPP域talk.xiaonei.com告知客户端以进行重定向。相较之下，校内XMPP服务器的行为虽然欠妥，但并未违反RFC，倒是PyXMPP的做法有违标准。</p>
</li>
</ol>
<p>完成登录后，更新校内状态就比较简单了，只需要发送一条<span style="font-family: courier new,courier;">&lt;presence/&gt;</span>消息即可。发送成功后断开连接，脚本执行结束。相关代码如下：</p>

<div class="wp_syntax"><div class="code"><pre class="python" style="font-family:monospace;">    <span style="color: #ff7700;font-weight:bold;">class</span> R2Client<span style="color: black;">&#40;</span> JabberClient <span style="color: black;">&#41;</span>:
        ...
        <span style="color: #ff7700;font-weight:bold;">def</span> session_started<span style="color: black;">&#40;</span> <span style="color: #008000;">self</span> <span style="color: black;">&#41;</span>:
            <span style="color: #008000;">self</span>.<span style="color: black;">stream</span>.<span style="color: black;">send</span><span style="color: black;">&#40;</span> Presence<span style="color: black;">&#40;</span> status=message <span style="color: black;">&#41;</span> <span style="color: black;">&#41;</span>
            <span style="color: #008000;">self</span>.<span style="color: black;">stream</span>.<span style="color: black;">disconnect</span><span style="color: black;">&#40;</span><span style="color: black;">&#41;</span></pre></div></div>

<p>最后是XMPP客户端的主循环：</p>

<div class="wp_syntax"><div class="code"><pre class="python" style="font-family:monospace;">    client = R2Client<span style="color: black;">&#40;</span> JID<span style="color: black;">&#40;</span> <span style="color: #dc143c;">user</span> + <span style="color: #483d8b;">'@talk.xiaonei.com/r2'</span> <span style="color: black;">&#41;</span>, password <span style="color: black;">&#41;</span>
    client.<span style="color: black;">connect</span><span style="color: black;">&#40;</span><span style="color: black;">&#41;</span>
    client.<span style="color: black;">loop</span><span style="color: black;">&#40;</span> <span style="color: #ff4500;">1</span> <span style="color: black;">&#41;</span></pre></div></div>

<p>上述代码中的R2代表RenRen <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley' />  本以为到此就结束了，不想最后又被PyXMPP绊了一跤：<span style="font-family: courier new,courier;">R2Client.session_started</span>最后的<span style="font-family: courier new,courier;">disconnect()</span>调用无法结束主循环。无奈之下再翻源码，在<a href="http://pyxmpp.jajcus.net/api/pyxmpp.client-pysrc.html#Client.loop" target="_blank"><span style="font-family: courier new,courier;">pyxmpp.client.Client.loop</span></a>中看到：</p>

<div class="wp_syntax"><div class="code"><pre class="python" style="font-family:monospace;"><span style="color: #ff7700;font-weight:bold;">while</span> <span style="color: #ff4500;">1</span>:
    stream = <span style="color: #008000;">self</span>.<span style="color: black;">get_stream</span><span style="color: black;">&#40;</span><span style="color: black;">&#41;</span>
    <span style="color: #ff7700;font-weight:bold;">if</span> <span style="color: #ff7700;font-weight:bold;">not</span> stream:
        <span style="color: #ff7700;font-weight:bold;">break</span>
    ...</pre></div></div>

<p>也就是说，只要<span style="font-family: courier new,courier;">stream</span>对象不为<span style="font-family: courier new,courier;">None</span>，无论其连接状态如何，<span style="font-family: courier new,courier;">Client.loop</span>都不会从这个<span style="font-family: courier new,courier;">while</span>中返回。这得算是个bug了，挠头……最后hack了一下，在<span style="font-family: courier new,courier;">disconnect()</span>之后将<span style="font-family: courier new,courier;">self.stream</span>设为<span style="font-family: courier new,courier;">None</span>，终于大功告成。更新校内状态的完整代码如下：</p>

<div class="wp_syntax"><table><tr><td class="line_numbers"><pre>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
</pre></td><td class="code"><pre class="python" style="font-family:monospace;"><span style="color: #808080; font-style: italic;">#!/usr/bin/python</span>
&nbsp;
<span style="color: #ff7700;font-weight:bold;">import</span> <span style="color: #dc143c;">logging</span>
&nbsp;
<span style="color: #ff7700;font-weight:bold;">from</span> pyxmpp <span style="color: #ff7700;font-weight:bold;">import</span> streamtls
<span style="color: #ff7700;font-weight:bold;">from</span> pyxmpp.<span style="color: black;">jabber</span>.<span style="color: black;">client</span> <span style="color: #ff7700;font-weight:bold;">import</span> JabberClient
<span style="color: #ff7700;font-weight:bold;">from</span> pyxmpp.<span style="color: black;">jid</span> <span style="color: #ff7700;font-weight:bold;">import</span> JID
<span style="color: #ff7700;font-weight:bold;">from</span> pyxmpp.<span style="color: black;">presence</span> <span style="color: #ff7700;font-weight:bold;">import</span> Presence
<span style="color: #ff7700;font-weight:bold;">from</span> <span style="color: #dc143c;">sys</span> <span style="color: #ff7700;font-weight:bold;">import</span> argv
&nbsp;
<span style="color: #ff7700;font-weight:bold;">def</span> UpdateR2<span style="color: black;">&#40;</span> <span style="color: #dc143c;">user</span>, password, message <span style="color: black;">&#41;</span>:
    <span style="color: #ff7700;font-weight:bold;">class</span> R2Client<span style="color: black;">&#40;</span> JabberClient <span style="color: black;">&#41;</span>:
        <span style="color: #ff7700;font-weight:bold;">def</span> <span style="color: #0000cd;">__init__</span><span style="color: black;">&#40;</span> <span style="color: #008000;">self</span>, jid, password <span style="color: black;">&#41;</span>:
            tls = streamtls.<span style="color: black;">TLSSettings</span><span style="color: black;">&#40;</span> require=<span style="color: #008000;">True</span>, verify_peer=<span style="color: #008000;">False</span> <span style="color: black;">&#41;</span>
            auth = <span style="color: black;">&#91;</span><span style="color: #483d8b;">'sasl:PLAIN'</span><span style="color: black;">&#93;</span>
            JabberClient.<span style="color: #0000cd;">__init__</span><span style="color: black;">&#40;</span> <span style="color: #008000;">self</span>, jid, password, tls_settings=tls,
                                   auth_methods=auth <span style="color: black;">&#41;</span>
&nbsp;
        <span style="color: #ff7700;font-weight:bold;">def</span> session_started<span style="color: black;">&#40;</span> <span style="color: #008000;">self</span> <span style="color: black;">&#41;</span>:
            <span style="color: #008000;">self</span>.<span style="color: black;">stream</span>.<span style="color: black;">send</span><span style="color: black;">&#40;</span> Presence<span style="color: black;">&#40;</span> status=message <span style="color: black;">&#41;</span> <span style="color: black;">&#41;</span>
            <span style="color: #008000;">self</span>.<span style="color: black;">stream</span>.<span style="color: black;">disconnect</span><span style="color: black;">&#40;</span><span style="color: black;">&#41;</span>
            <span style="color: #008000;">self</span>.<span style="color: black;">stream</span> = <span style="color: #008000;">None</span>
&nbsp;
    client = R2Client<span style="color: black;">&#40;</span> JID<span style="color: black;">&#40;</span> <span style="color: #dc143c;">user</span> + <span style="color: #483d8b;">'@talk.renren.com/r2'</span> <span style="color: black;">&#41;</span>, password <span style="color: black;">&#41;</span>
    client.<span style="color: black;">connect</span><span style="color: black;">&#40;</span><span style="color: black;">&#41;</span>
    client.<span style="color: black;">loop</span><span style="color: black;">&#40;</span> <span style="color: #ff4500;">1</span> <span style="color: black;">&#41;</span>
&nbsp;
<span style="color: #ff7700;font-weight:bold;">if</span> __name__ == <span style="color: #483d8b;">'__main__'</span>:
    logger = <span style="color: #dc143c;">logging</span>.<span style="color: black;">getLogger</span><span style="color: black;">&#40;</span><span style="color: black;">&#41;</span>
    logger.<span style="color: black;">addHandler</span><span style="color: black;">&#40;</span> <span style="color: #dc143c;">logging</span>.<span style="color: black;">StreamHandler</span><span style="color: black;">&#40;</span><span style="color: black;">&#41;</span> <span style="color: black;">&#41;</span>
    logger.<span style="color: black;">setLevel</span><span style="color: black;">&#40;</span> <span style="color: #dc143c;">logging</span>.<span style="color: black;">DEBUG</span> <span style="color: black;">&#41;</span>
&nbsp;
    <span style="color: #dc143c;">user</span>, password, message = argv<span style="color: black;">&#91;</span> <span style="color: #ff4500;">1</span> <span style="color: black;">&#93;</span>, argv<span style="color: black;">&#91;</span> <span style="color: #ff4500;">2</span> <span style="color: black;">&#93;</span>, argv<span style="color: black;">&#91;</span> <span style="color: #ff4500;">3</span> <span style="color: black;">&#93;</span>
    UpdateR2<span style="color: black;">&#40;</span> <span style="color: #dc143c;">user</span>, password, <span style="color: #483d8b;">'[r2] '</span> + message <span style="color: black;">&#41;</span></pre></td></tr></table></div>

<p>将这个脚本和前面的Twitter脚本简单整合一下，便可实现Twitter/校内状态的同步更新了。如文章开头所说，其实我自己对这个同步功能并没有什么需求，纯粹是做着玩。把过程写出来，也许对其他人会有些用处吧  <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_wink.gif' alt=';-)' class='wp-smiley' />  起先吹牛说50行以内搞定，最后还是超了，而且还只能算是个原型。要在墙内达到实用水准，至少还要支持Twitter API，最好还能支持OAuth。</p>
<p>最后再多说点关于校内XMPP服务的问题。只要查明自己的校内数字ID，就可以以<span style="font-family: courier new,courier;"><em>id</em>@talk.xiaonei.com</span>用任意一款支持XMPP协议的客户端（如<a href="http://pidgin.im/" target="_blank">Pidgin</a>、<a href="http://psi-im.org/" target="_blank">Psi</a>、<a href="http://www.miranda-im.org/" target="_blank">Miranda</a>等）登录校内（实验证明使用<span style="font-family: courier new,courier;">talk.renren.com</span>域也可成功登录）。登录后可以使用的主要功能包括：</p>
<ul>
<li>与在线的校内好友聊天</li>
<li>通过自定义在线状态更新校内状态</li>
<li>接收校内新鲜事通知</li>
<li>（可能还有其他我尚未发觉的功能）</li>
</ul>
<p>你会发现自己时不时地收到来自<span style="font-family: courier new,courier;">feed.talk.renren.com@feed.talk.renren.com</span>的莫名其妙的空消息。这个“空”消息并不空，它其实就是校内新鲜事通知。在协议上，新鲜事通知以<span style="font-family: courier new,courier;">&lt;message/&gt;</span>消息的形态被发送到客户端。与普通好友消息不同，新鲜事<span style="font-family: courier new,courier;">&lt;message/&gt;</span>消息的<span style="font-family: courier new,courier;">&lt;body/&gt;</span>元素为空，新鲜事的详细内容则包含于附加的<span style="font-family: courier new,courier;">&lt;xfeed/&gt;</span>字段。该<span style="font-family: courier new,courier;">&lt;xfeed/&gt;</span>字段的格式是校内自定义的，Pidgin等标准XMPP客户端无法解读，因此在客户端界面表现上看来，便是一条“空”消息。如果打开Pidgin的XMPP控制台插件，便可以一窥<span style="font-family: courier new,courier;">&lt;xfeed/&gt;</span>的全貌。</p>
<p>另外，校内的XMPP服务器不支持域间互通，因此你也就没法在你的Gtalk等其他XMPP帐号上添加自己的校内帐号为好友，反之亦然。</p>
<p>再有一点，就是Pidgin等客户端在默认设置下总会在客户端持续空闲一段时间后自动切入“离开”状态。而XMPP在线状态的改变会导致校内状态的改变。所以当你用Pidgin登录校内一段时间后会发现自己的校内状态列表里多出若干条“我现在不在”，可不要觉得奇怪 <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_biggrin.gif' alt=':-D' class='wp-smiley' /> </p>
<!-- Jaxl IM embed starts -->
<script type="text/javascript">
(function() {
var jaxlChat = document.createElement("script");
jaxlChat.type = "text/javascript";
jaxlChat.async = true;
jaxlChat.src = "http://im.jaxl.im/ui/jaxl.php";
(document.getElementsByTagName("head")[0]||document.getElementsByTagName("body")[0]).appendChild(jaxlChat);
})();
</script>
<!-- Jaxl IM embed ends --><img src="http://feeds.feedburner.com/~r/liancheng/~4/hoLegl-PZSI" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://blog.liancheng.info/?feed=rss2&amp;p=280</wfw:commentRss>
		<slash:comments>6</slash:comments>
		<feedburner:origLink>http://blog.liancheng.info/?p=280</feedburner:origLink></item>
		<item>
		<title>Concurrent Programming in Erlang Part 1 中文译稿</title>
		<link>http://feedproxy.google.com/~r/liancheng/~3/RXt1Wd6dzoQ/</link>
		<comments>http://blog.liancheng.info/?p=255#comments</comments>
		<pubDate>Tue, 15 Dec 2009 07:05:51 +0000</pubDate>
		<dc:creator>liancheng</dc:creator>
				<category><![CDATA[Translation]]></category>
		<category><![CDATA[CPiE-CN]]></category>
		<category><![CDATA[Erlang]]></category>

		<guid isPermaLink="false">http://blog.liancheng.info/?p=255</guid>
		<description><![CDATA[链接：《Erlang并发编程》第一部分
从去年年中开始，利用闲暇时间零零散散地翻译Concurrent Programming in Erlang Part 1。完成了序言、致谢、简介和第1章之后由于工作繁忙暂停了很久。今年年初，又重新捡起来，完成了第2章。同时也将原先的reStructuredText格式的译稿迁移到Sphinx上。借助Sphinx，将译稿切分、组织成了合理的工程目录。于是将译稿上传到了SVN，又在Erlang-China和TopLanguage发了帖子，正式发起CPiE-CN项目，召集了一批志愿译者，开始合作翻译剩余章节。
各位志愿者们动作都相当迅速，没多久便相继提交了各自负责章节的译稿。不过有些并非是Sphinx格式，需要再手工适配到Sphinx。有些即使是 Sphinx格式，一些排版和格式的细节处理也还不到位（有些译者还是Sphinx新手）。也有志愿者出于种种原因遗弃了认领的章节——不过没关系，考虑到本人本来就拖拉成性，所以特地在译者须知中注明本项目没有任何进度压力……
于是，适配非Sphinx格式译稿、整理其他译者的Sphinx译稿排版格式、校对所有译稿以及翻译惨遭遗弃的章节，就成了我剩下的工作。期间因为工作原因暂停过很长一段时间。不过说实话，由于暂停得实在太久，以至于后来工作不忙的时候也没能想起来……囧……咳，总之，断断续续一年，总算将全书主体 翻译完毕——“主体”的精确含义是：除附录B、E和参考文献列表以外的所有内容。
在此严重感谢无私贡献译稿的诸位志愿译者，他们是（按参与项目的时间次序排列）：

王飞（第4章、第8章）
Ken Zhao（第6章）
张驰原（第5章）
丁豪（第7章）
赵卫国（附录C、附录D）
吴峻（附录A）

最后……也感谢一下自己    ：

连城（序、致谢、简介、第1章、第2章、第3章、第9章、全文校对）


注1：CPiE-CN全部译稿使用BY-NC-ND的CC协议许可。曾经在erlang-questions邮件组中询问过Joe大叔的意见，大叔称没问题并将邮件转发给了Prentice Hall出版社的编辑，不过出版社并没有回音。

注2：曾经说要在DreamHost上折腾一个Trac用来做勘误，最终可耻地没有搞定。此事无限期顺延……没有寄宿到Google Code等站点的原因是这些站点不支持相应的CC许可协议。

注3：今年3月14号翻译完第2章的时候本写过一篇blog。第二天便正式发布了CPiE-CN项目。在那篇里对长篇技术文档的撰写工具进行了探讨，盛赞了Sphinx，并对Erlang社区表达了无限的憧憬。然而，该篇blog后来由于未知原因（估计是误操作或WordPress升级事故）被不幸截肢，全文只剩下大约30%。可怜我在事发之后不知道多久才发现，遍寻Google Reader、Google Cache和百度Cache也找不到原来的全文，只好忍痛将该篇匿了，秘谋让该篇在全书译稿完成时再次涅磐。然而，昨天重新编辑了这篇blog后，发现RSS却没有更新，可能是跟发布时间等因素有关，只好又新开一篇。这件事情教育我们：一定要定期备份WordPress数据库啊！  



(function() {
var jaxlChat = document.createElement("script");
jaxlChat.type = "text/javascript";
jaxlChat.async = true;
jaxlChat.src = "http://im.jaxl.im/ui/jaxl.php";
(document.getElementsByTagName("head")[0]&#124;&#124;document.getElementsByTagName("body")[0]).appendChild(jaxlChat);
})();

]]></description>
			<content:encoded><![CDATA[<p>链接：<a href="http://svn.liancheng.info/cpie-cn/trunk/.build/html/index.html" target="_blank">《Erlang并发编程》第一部分</a><img class="size-full wp-image-54 alignright" title="erlang-logo" src="http://blog.liancheng.info/wp-content/uploads/2009/03/erlang-logo.jpg" alt="erlang-logo" width="132" height="118" /></p>
<p>从去年年中开始，利用闲暇时间零零散散地翻译Concurrent Programming in Erlang Part 1。完成了<a href="http://svn.liancheng.info/cpie-cn/trunk/.build/html/preface.html" target="_parent">序言</a>、<a href="http://svn.liancheng.info/cpie-cn/trunk/.build/html/acknowledgements.html" target="_blank">致谢</a>、<a href="http://svn.liancheng.info/cpie-cn/trunk/.build/html/introduction.html" target="_blank">简介</a>和<a href="http://svn.liancheng.info/cpie-cn/trunk/.build/html/part-i/chapter-1.html" target="_blank">第1章</a>之后由于工作繁忙暂停了很久。今年年初，又重新捡起来，完成了<a href="http://svn.liancheng.info/cpie-cn/trunk/.build/html/part-i/chapter-2.html" target="_blank">第2章</a>。同时也将原先的reStructuredText格式的译稿迁移到Sphinx上。借助Sphinx，将译稿切分、组织成了合理的工程目录。于是将译稿上传到了SVN，又在Erlang-China和TopLanguage发了<a href="http://groups.google.com/group/erlang-china/browse_thread/thread/3baae94948d7a932" target="_blank">帖子</a>，正式发起<a href="http://svn.liancheng.info/cpie-cn/trunk/.build/html/cpie-cn-project.html#cpie-cn" target="_blank">CPiE-CN项目</a>，召集了一批志愿译者，开始合作翻译剩余章节。</p>
<p>各位志愿者们动作都相当迅速，没多久便相继提交了各自负责章节的译稿。不过有些并非是Sphinx格式，需要再手工适配到Sphinx。有些即使是 Sphinx格式，一些排版和格式的细节处理也还不到位（有些译者还是Sphinx新手）。也有志愿者出于种种原因遗弃了认领的章节——不过没关系，考虑到本人本来就拖拉成性，所以特地在<a href="http://svn.liancheng.info/cpie-cn/trunk/.build/html/cpie-cn-project.html#id6" target="_blank">译者须知</a>中注明本项目没有任何进度压力……</p>
<p>于是，适配非Sphinx格式译稿、整理其他译者的Sphinx译稿排版格式、校对所有译稿以及翻译惨遭遗弃的章节，就成了我剩下的工作。期间因为工作原因暂停过很长一段时间。不过说实话，由于暂停得实在太久，以至于后来工作不忙的时候也没能想起来……囧……咳，总之，断断续续一年，总算将全书主体 翻译完毕——“主体”的精确含义是：除附录B、E和参考文献列表以外的所有内容。</p>
<p>在此严重感谢无私贡献译稿的诸位志愿译者，他们是（按参与项目的时间次序排列）：</p>
<ul>
<li>王飞（第4章、第8章）</li>
<li>Ken Zhao（第6章）</li>
<li>张驰原（第5章）</li>
<li>丁豪（第7章）</li>
<li>赵卫国（附录C、附录D）</li>
<li>吴峻（附录A）</li>
</ul>
<p>最后……也感谢一下自己  <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_mrgreen.gif' alt=':mrgreen:' class='wp-smiley' />  ：</p>
<ul>
<li>连城（序、致谢、简介、第1章、第2章、第3章、第9章、全文校对）</li>
</ul>
<hr style="width: 100%;" />
<blockquote><p>注1：CPiE-CN全部译稿使用<a href="http://creativecommons.org/licenses/by-nc-nd/2.5/cn/" target="_blank">BY-NC-ND的CC协议</a>许可。曾经在erlang-questions邮件组中询问过Joe大叔的意见，大叔称没问题并将邮件转发给了Prentice Hall出版社的编辑，不过出版社并没有回音。</p>
</blockquote>
<blockquote><p>注2：曾经说要在DreamHost上折腾一个Trac用来做勘误，最终可耻地没有搞定。此事无限期顺延……没有寄宿到Google Code等站点的原因是这些站点不支持相应的CC许可协议。</p>
</blockquote>
<blockquote><p>注3：今年3月14号翻译完第2章的时候本写过一篇blog。第二天便正式发布了CPiE-CN项目。在那篇里对长篇技术文档的撰写工具进行了探讨，盛赞了Sphinx，并对Erlang社区表达了无限的憧憬。然而，该篇blog后来由于未知原因（估计是误操作或WordPress升级事故）被不幸截肢，全文只剩下大约30%。可怜我在事发之后不知道多久才发现，遍寻Google Reader、Google Cache和百度Cache也找不到原来的全文，只好忍痛将该篇匿了，秘谋让该篇在全书译稿完成时再次涅磐。然而，昨天重新编辑了这篇blog后，发现RSS却没有更新，可能是跟发布时间等因素有关，只好又新开一篇。这件事情教育我们：一定要定期备份WordPress数据库啊！ <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_eek.gif' alt='8-O' class='wp-smiley' /> </p>
</blockquote>
<!-- Jaxl IM embed starts -->
<script type="text/javascript">
(function() {
var jaxlChat = document.createElement("script");
jaxlChat.type = "text/javascript";
jaxlChat.async = true;
jaxlChat.src = "http://im.jaxl.im/ui/jaxl.php";
(document.getElementsByTagName("head")[0]||document.getElementsByTagName("body")[0]).appendChild(jaxlChat);
})();
</script>
<!-- Jaxl IM embed ends --><img src="http://feeds.feedburner.com/~r/liancheng/~4/RXt1Wd6dzoQ" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://blog.liancheng.info/?feed=rss2&amp;p=255</wfw:commentRss>
		<slash:comments>8</slash:comments>
		<feedburner:origLink>http://blog.liancheng.info/?p=255</feedburner:origLink></item>
		<item>
		<title>snipMate反引号转义补丁</title>
		<link>http://feedproxy.google.com/~r/liancheng/~3/68GlXWnU4Lc/</link>
		<comments>http://blog.liancheng.info/?p=211#comments</comments>
		<pubDate>Mon, 12 Oct 2009 17:04:43 +0000</pubDate>
		<dc:creator>liancheng</dc:creator>
				<category><![CDATA[Tools]]></category>
		<category><![CDATA[Vim]]></category>

		<guid isPermaLink="false">http://blog.liancheng.info/?p=211</guid>
		<description><![CDATA[对于一套IDE来说，一个好的snippet管理工具可以大大提高程序员的工作效率。作为一个适应不了Emacs的Vim geek，Eclipse自带的代码补全、Visual Studio的Visual Assist插件、Emacs下由Pluskid荣誉出品的yasnippet，都让我十分垂涎。之前曾经用过很长一段时间的snippetEmu（这也是Debian/Ubuntu vim-scripts包中所带的snippet插件），虽然确实有助于提高效率，却有诸多不足：视觉效果很不清爽，时不时还出些问题，最难忍的便是其晦涩不堪的snippet定义方式。后来也尝试过同事推荐的另一个已经不记得名字的插件，仍旧不趁手，又换回snippetEmu。
前两天无意中发现snipMate，试用之后大呼惊艳！虽然和snippetEmu同是模仿TextMate，snipMate要精致得多。Snippet的定义方式也非常灵活和人性化。只有一处让人不待见的地方，就是snippet定义必须像Makefile一样以tab开头。通读文档之后依照自己的代码风格改写了默认的C/C++ snippet文件，又录入了Emacs erlang-mode所带的几个OTP behaviour的snippet。把玩一番，爱不释手   
周末闲时接着翻译《Erlang并发编程》第9章，又想到snipMate。于是顺手定义了一个rst.snippets文件，用来简化reStructuredText格式中多种Markup的输入。其中有这么一个用于输入等宽格式文本的snippet：

# Literal text
snippet l
    ``${1}``${2}

写到一半的时候就想起来，反引号在snipMate中是有特殊用途的：snipMate的snippet占位符中可以插入Vim脚本表达式以实现一些高级功能，Vim表达式就需要以一对反引号包围起来，例如默认的_.snippets中：

snippet date
    `strftime(&#34;%Y-%m-%d&#34;)`

就可以将“date”展开为当前日期。这样一来，我的rst.snippets中的反引号会不会被错误地解释呢？如果这么写不行，那么snipMate是否支持反引号的转义呢？试了一下，发现果然出错了。在snipMate文档中也没有找到反引号转义相关的说明。无奈之下只有去翻snipMate的源码。说来可耻，用Vim 4年了，一直都没有仔细学过Vim的脚本语言   除了日常的.vimrc配置以外，也从来没有写过别的Vim脚本。
所幸snipMate的代码并不复杂，很快在autoload/snipMate.vim中找到了这么一段：

78
79
80
81
82
83
84
85
86
87
    &#34; Evaluate eval (`...`) expressions.
    &#34; Using a loop here instead of a regex fixes a bug with nested &#34;\=&#34;.
    if stridx&#40;snippet, '`'&#41; [...]]]></description>
			<content:encoded><![CDATA[<p><img class="size-full wp-image-212 alignleft" title="vim-logo" src="http://blog.liancheng.info/wp-content/uploads/2009/10/vim-logo.jpeg" alt="vim-logo" width="116" height="116" />对于一套IDE来说，一个好的snippet管理工具可以大大提高程序员的工作效率。作为一个适应不了Emacs的Vim geek，Eclipse自带的代码补全、Visual Studio的Visual Assist插件、Emacs下由<a href="http://blog.pluskid.org" target="_blank">Pluskid</a>荣誉出品的<a href="http://code.google.com/p/yasnippet/" target="_blank">yasnippet</a>，都让我十分垂涎。之前曾经用过很长一段时间的<a href="http://www.vim.org/scripts/script.php?script_id=1318" target="_blank">snippetEmu</a>（这也是Debian/Ubuntu vim-scripts包中所带的snippet插件），虽然确实有助于提高效率，却有诸多不足：视觉效果很不清爽，时不时还出些问题，最难忍的便是其晦涩不堪的snippet定义方式。后来也尝试过同事推荐的另一个已经不记得名字的插件，仍旧不趁手，又换回snippetEmu。</p>
<p>前两天无意中发现snipMate，试用之后大呼惊艳！虽然和snippetEmu同是模仿TextMate，snipMate要精致得多。Snippet的定义方式也非常灵活和人性化。只有一处让人不待见的地方，就是snippet定义必须像Makefile一样以tab开头。通读文档之后依照自己的代码风格改写了默认的C/C++ snippet文件，又录入了Emacs erlang-mode所带的几个OTP behaviour的snippet。把玩一番，爱不释手  <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_biggrin.gif' alt=':-D' class='wp-smiley' /> <span id="more-211"></span></p>
<p>周末闲时接着翻译<a href="http://svn.liancheng.info/cpie-cn/trunk/.build/html/index.html" target="_blank">《Erlang并发编程》</a>第9章，又想到snipMate。于是顺手定义了一个<span style="font-family: courier new,courier;">rst.snippets</span>文件，用来简化reStructuredText格式中多种Markup的输入。其中有这么一个用于输入等宽格式文本的snippet：</p>

<div class="wp_syntax"><div class="code"><pre class="text" style="font-family:monospace;"># Literal text
snippet l
    ``${1}``${2}</pre></div></div>

<p>写到一半的时候就想起来，反引号在snipMate中是有特殊用途的：snipMate的snippet占位符中可以插入Vim脚本表达式以实现一些高级功能，Vim表达式就需要以一对反引号包围起来，例如默认的<span style="font-family: courier new,courier;">_.snippets</span>中：</p>

<div class="wp_syntax"><div class="code"><pre class="text" style="font-family:monospace;">snippet date
    `strftime(&quot;%Y-%m-%d&quot;)`</pre></div></div>

<p>就可以将“<span style="font-family: courier new,courier;">date</span>”展开为当前日期。这样一来，我的<span style="font-family: courier new,courier;">rst.snippets</span>中的反引号会不会被错误地解释呢？如果这么写不行，那么snipMate是否支持反引号的转义呢？试了一下，发现果然出错了。在snipMate文档中也没有找到反引号转义相关的说明。无奈之下只有去翻snipMate的源码。说来可耻，用Vim 4年了，一直都没有仔细学过Vim的脚本语言 <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_redface.gif' alt=':oops:' class='wp-smiley' />  除了日常的<span style="font-family: courier new,courier;">.vimrc</span>配置以外，也从来没有写过别的Vim脚本。</p>
<p>所幸snipMate的代码并不复杂，很快在<span style="font-family: courier new,courier;">autoload/snipMate.vim</span>中找到了这么一段：</p>

<div class="wp_syntax"><table><tr><td class="line_numbers"><pre>78
79
80
81
82
83
84
85
86
87
</pre></td><td class="code"><pre class="vim" style="font-family:monospace;">    <span style="color: #C5A22D;">&quot; Evaluate eval (`...`) expressions.
    &quot;</span> Using a loop here instead of a regex fixes a bug with nested <span style="color: #C5A22D;">&quot;<span style="">\=</span>&quot;</span><span style="color: #000000;">.</span>
    <span style="color: #804040;">if</span> <span style="color: #25BB4D;">stridx</span><span style="color: #000000;">&#40;</span>snippet, <span style="color: #C5A22D;">'`'</span><span style="color: #000000;">&#41;</span> <span style="color: #000000;">!</span>= <span style="color: #000000;">-</span><span style="color: #000000; font-weight:bold;">1</span>
        <span style="color: #804040;">while</span> <span style="color: #25BB4D;">match</span><span style="color: #000000;">&#40;</span>snippet, <span style="color: #C5A22D;">'`.<span style="">\{</span>-}`'</span><span style="color: #000000;">&#41;</span> <span style="color: #000000;">!</span>= <span style="color: #000000;">-</span><span style="color: #000000; font-weight:bold;">1</span>
            <span style="color: #804040;">let</span> snippet = <span style="color: #25BB4D;">substitute</span><span style="color: #000000;">&#40;</span>snippet, <span style="color: #C5A22D;">'`.<span style="">\{</span>-}`'</span>,
                        \ <span style="color: #25BB4D;">substitute</span><span style="color: #000000;">&#40;</span><span style="color: #25BB4D;">eval</span><span style="color: #000000;">&#40;</span><span style="color: #25BB4D;">matchstr</span><span style="color: #000000;">&#40;</span>snippet, <span style="color: #C5A22D;">'`<span style="">\z</span>s.<span style="">\{</span>-}<span style="">\z</span>e`'</span><span style="color: #000000;">&#41;</span><span style="color: #000000;">&#41;</span>,
                        \ <span style="color: #C5A22D;">&quot;<span style="">\n</span><span style="">\\</span>%$&quot;</span>, <span style="color: #C5A22D;">''</span>, <span style="color: #C5A22D;">''</span><span style="color: #000000;">&#41;</span>, <span style="color: #C5A22D;">''</span><span style="color: #000000;">&#41;</span>
        <span style="color: #804040;">endw</span>
        <span style="color: #804040;">let</span> snippet = <span style="color: #25BB4D;">substitute</span><span style="color: #000000;">&#40;</span>snippet, <span style="color: #C5A22D;">&quot;<span style="">\r</span>&quot;</span>, <span style="color: #C5A22D;">&quot;<span style="">\n</span>&quot;</span>, <span style="color: #C5A22D;">'g'</span><span style="color: #000000;">&#41;</span>
    <span style="color: #804040;">endif</span></pre></td></tr></table></div>

<p>从第81行的正则表达式<span style="font-family: courier new,courier;">`.\{-}`</span>来看，snipMate的作者只是简单的匹配了成对的反引号及其间的内容，而没有作任何转义处理。简单构思了一下，决定以传统方式用反斜杠来转义反引号，于是动手打了一个简单的patch。</p>
<p>首先将第81行的正则式修改为<span style="font-family: courier new,courier;">[^\\]`.\{-}`</span>，这样snipMate就不会将以反斜杠开头的反引号纳入处理范畴。同时，在第86行之后增加了这么一行：</p>

<div class="wp_syntax"><div class="code"><pre class="vim" style="font-family:monospace;"><span style="color: #804040;">let</span> snippet = <span style="color: #25BB4D;">substitute</span><span style="color: #000000;">&#40;</span>snippet, <span style="color: #C5A22D;">&quot;<span style="">\\</span><span style="">\\</span>`&quot;</span>, <span style="color: #C5A22D;">&quot;`&quot;</span>, <span style="color: #C5A22D;">'g'</span><span style="color: #000000;">&#41;</span></pre></div></div>

<p>用来将所有的<span style="font-family: courier new,courier;">\`</span>再次还原为单个反引号。最后，将之前的snippet改写为：</p>

<div class="wp_syntax"><div class="code"><pre class="text" style="font-family:monospace;"># Literal text
snippet l
    \`\`${1}\`\`${2}</pre></div></div>

<p>简单测试了一下，大功告成！  <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_biggrin.gif' alt=':-D' class='wp-smiley' />  开心之余屁颠地跑到snipMate的Google Code主页上去<a href="http://code.google.com/p/snipmate/issues/detail?id=88&amp;colspec=ID%20Type%20Status%20Priority%20OS%20Summary" target="_blank">提交了这个patch</a>。说来再次可耻，虽然一直享着OpenSource的福，却从未正式提交过补丁，以至于我都不知道应该如何正确地提交一个补丁  <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_redface.gif' alt=':oops:' class='wp-smiley' />  Google了一把倒也迅速搞定。</p>
<p>提交patch的时候写道：我是个Vim脚本新手，不敢说这个补丁有没有什么问题。果不其然，在写这篇blog的时候便发现果然有个bug：第81行的那个修改过的正则式<span style="font-family: courier new,courier;">[^\\]`.\{-}`</span>要求在反引号前必须有一个不是反斜杠的字符，于是当反引号位于行首时，就匹配失败了。上文提到的用于输入当前日期的snippet便会因此而被错误地展开（我也正是因为尝试这个snippet才发现这个bug的）。好在解决起来也简单，需要匹配的反引号对所应满足的条件应该是：第一个反引号位于行首或者前一个字符不是反斜杠。于是将正则式改为改成<span style="font-family: courier new,courier;">\(^|[^\\]\)`.\{-}`就可以了。</span></p>
<!-- Jaxl IM embed starts -->
<script type="text/javascript">
(function() {
var jaxlChat = document.createElement("script");
jaxlChat.type = "text/javascript";
jaxlChat.async = true;
jaxlChat.src = "http://im.jaxl.im/ui/jaxl.php";
(document.getElementsByTagName("head")[0]||document.getElementsByTagName("body")[0]).appendChild(jaxlChat);
})();
</script>
<!-- Jaxl IM embed ends --><img src="http://feeds.feedburner.com/~r/liancheng/~4/68GlXWnU4Lc" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://blog.liancheng.info/?feed=rss2&amp;p=211</wfw:commentRss>
		<slash:comments>11</slash:comments>
		<feedburner:origLink>http://blog.liancheng.info/?p=211</feedburner:origLink></item>
		<item>
		<title>shared_ptr四宗罪</title>
		<link>http://feedproxy.google.com/~r/liancheng/~3/ZD_IqQTdCvo/</link>
		<comments>http://blog.liancheng.info/?p=85#comments</comments>
		<pubDate>Mon, 08 Jun 2009 15:10:39 +0000</pubDate>
		<dc:creator>liancheng</dc:creator>
				<category><![CDATA[Work]]></category>
		<category><![CDATA[Boost]]></category>
		<category><![CDATA[C++]]></category>

		<guid isPermaLink="false">http://blog.liancheng.info/?p=85</guid>
		<description><![CDATA[在基于C++的大型系统的设计实现中，由于缺乏语言级别的GC支持，资源生存周期往往是一个棘手的问题。系统地解决这个问题的方法无非两种：

使用GC库
使用引用计数

严格地说，引用计数其实也是一种最朴素的GC。相对于现代的GC技术，引用计数的实现简单，但相应地，它也存在着循环引用和线程同步开销等问题。关于这二者孰优孰劣，已经有过很多讨论，在此就不搅这股混水了。我一直也没有使用过C++的GC库，在实际项目中总是采用引用计数的方案。而作为Boost的拥趸，首选的自然是shared_ptr。一直以来我也对shared_ptr百般推崇，然而最近的一些项目开发经验却让我在shared_ptr上栽了坑，对C++引用计数也有了一些新的的认识，遂记录在此。
本文主要针对基于boost::shared_ptr的C++引用计数实现方案进行一些讨论。  C++引用计数方案往往伴随着用于自动管理引用计数的智能指针。按是否要求资源对象自己维护引用计数，C++引用计数方案可以分为两类：
侵入式
侵入式的引用计数管理要求资源对象本身维护引用计数，同时提供增减引用计数的管理接口。通常侵入式方案会提供配套的侵入式引用计数智能指针。该智能指针通过调用资源对象的引用计数管理接口来自动增减引用计数。COM对象与CComPtr便是侵入式引用计数的一个典型实例。
非侵入式
非侵入式的引用计数管理对资源对象本身没有任何要求，而是完全借助非侵入式引用计数智能指针在资源对象外部维护独立的引用计数。shared_ptr便是基于这个思路。
初看起来，非侵入式方案由于对资源对象的实现没有任何要求，相较于侵入式方案更具吸引力。然而事实却并非如此。下面就来分析一下基于shared_ptr的非侵入式引用计数。  在使用shared_ptr的引用计数解决方案中，引用计数完全由shared_ptr控制，资源对象对与自己对应的引用计数一无所知。而引用计数与资源对象的生存期息息相关，这就意味着资源对象丧失了对生存期的控制权，将自己的生杀大权拱手让给了shared_ptr。这种情况下，资源对象就不得不依靠至少一个shared_ptr实例来保障自己的生存。换言之，资源对象一旦“沾染”了shared_ptr，就一辈子都无法摆脱！ 考察以下的简单用例：
用例一：

1
2
3
4
5
IResource* p = new CResource;
&#123;
    shared_ptr q&#40; p &#41;;
&#125;
p-&#62;Use&#40;&#41; // CRASH

单纯为了解决上述的崩溃，可以自定义一个什么也不做的deleter：

1
2
3
4
5
struct noop_deleter &#123;
    void operator&#40;&#41;&#40; void* &#41; &#123;
        // NO-OP
    &#125;
&#125;;

然后将上述用例的第三行改为：

shared_ptr q&#40; p, noop_deleter&#40;&#41; &#41;;

但是这样一来，shared_ptr就丧失了借助RAII自动释放资源的能力，违背了我们利用智能指针自动管理资源生存期的初衷（话说回来，这倒并不是说noop_deleter这种手法毫无用处，Boost.Asio中就巧妙地利用shared_ptr、weak_ptr和noop_deleter来实现异步I/O事件的取消）。从这个简单的用例可以看出，shared_ptr就像是毒品一样，一旦沾染就难以戒除。更甚者，染毒者连换用其他“毒品”的权力都没有：shared_ptr的引用计数管理接口是私有的，无法从shared_ptr之外操控，也就无法从shared_ptr迁移到其他类型的引用计数智能指针。
不仅如此，资源对象沾染上shared_ptr之后，就只能使用最初的那个shared_ptr实例的拷贝来维系自己的生存期。考察以下用例：
用例二：

1
2
3
4
5
6
7
&#123;
    shared_ptr p1&#40; CResource &#41;;
   [...]]]></description>
			<content:encoded><![CDATA[<p><img class="alignright size-full wp-image-180" title="boost" src="http://blog.liancheng.info/wp-content/uploads/2009/06/boost.png" alt="boost" />在基于C++的大型系统的设计实现中，由于缺乏语言级别的GC支持，资源生存周期往往是一个棘手的问题。系统地解决这个问题的方法无非两种：</p>
<ol>
<li>使用GC库</li>
<li>使用引用计数</li>
</ol>
<p>严格地说，引用计数其实也是一种最朴素的GC。相对于现代的GC技术，引用计数的实现简单，但相应地，它也存在着循环引用和线程同步开销等问题。关于这二者孰优孰劣，已经有过很多讨论，在此就不搅这股混水了。我一直也没有使用过C++的GC库，在实际项目中总是采用引用计数的方案。而作为Boost的拥趸，首选的自然是<span style="font-family: courier new,courier;">shared_ptr</span>。一直以来我也对<span style="font-family: courier new,courier;">shared_ptr</span>百般推崇，然而最近的一些项目开发经验却让我在<span style="font-family: courier new,courier;">shared_ptr</span>上栽了坑，对C++引用计数也有了一些新的的认识，遂记录在此。</p>
<p>本文主要针对基于<span style="font-family: 'courier new', courier;">boost::shared_ptr</span>的C++引用计数实现方案进行一些讨论。  C++引用计数方案往往伴随着用于自动管理引用计数的智能指针。按是否要求资源对象自己维护引用计数，C++引用计数方案可以分为两类：</p>
<p><strong>侵入式</strong></p>
<p style="padding-left: 30px;"><strong><span style="font-weight: normal;">侵入式的引用计数管理要求资源对象本身维护引用计数，同时提供增减引用计数的管理接口。通常侵入式方案会提供配套的侵入式引用计数智能指针。该智能指针通过调用资源对象的引用计数管理接口来自动增减引用计数。COM对象与<span style="font-family: 'courier new', courier;">CComPtr</span>便是侵入式引用计数的一个典型实例。</span></strong></p>
<p><strong>非侵入式</strong></p>
<p style="padding-left: 30px;">非侵入式的引用计数管理对资源对象本身没有任何要求，而是完全借助非侵入式引用计数智能指针在资源对象<strong>外部</strong>维护独立的引用计数。<span style="font-family: 'courier new', courier;">shared_ptr</span>便是基于这个思路。</p>
<p><span id="more-85"></span>初看起来，非侵入式方案由于对资源对象的实现没有任何要求，相较于侵入式方案更具吸引力。然而事实却并非如此。下面就来分析一下基于<span style="font-family: 'courier new', courier;">shared_ptr</span>的非侵入式引用计数。  在使用<span style="font-family: 'courier new', courier;">shared_ptr</span>的引用计数解决方案中，引用计数完全由<span style="font-family: 'courier new', courier;">shared_ptr</span>控制，资源对象对与自己对应的引用计数一无所知。而引用计数与资源对象的生存期息息相关，这就意味着资源对象丧失了对生存期的控制权，将自己的生杀大权拱手让给了<span style="font-family: 'courier new', courier;">shared_ptr</span>。这种情况下，资源对象就不得不依靠至少一个<span style="font-family: 'courier new', courier;">shared_ptr</span>实例来保障自己的生存。换言之，资源对象一旦“沾染”了<span style="font-family: 'courier new', courier;">shared_ptr</span>，就一辈子都无法摆脱！ 考察以下的简单用例：</p>
<p>用例一：</p>

<div class="wp_syntax"><table><tr><td class="line_numbers"><pre>1
2
3
4
5
</pre></td><td class="code"><pre class="cpp" style="font-family:monospace;">IResource<span style="color: #000040;">*</span> p <span style="color: #000080;">=</span> <span style="color: #0000dd;">new</span> CResource<span style="color: #008080;">;</span>
<span style="color: #008000;">&#123;</span>
    shared_ptr q<span style="color: #008000;">&#40;</span> p <span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
<span style="color: #008000;">&#125;</span>
p<span style="color: #000040;">-</span><span style="color: #000080;">&gt;</span>Use<span style="color: #008000;">&#40;</span><span style="color: #008000;">&#41;</span> <span style="color: #666666;">// CRASH</span></pre></td></tr></table></div>

<p>单纯为了解决上述的崩溃，可以自定义一个什么也不做的<span style="font-family: 'courier new', courier;">deleter</span>：</p>

<div class="wp_syntax"><table><tr><td class="line_numbers"><pre>1
2
3
4
5
</pre></td><td class="code"><pre class="cpp" style="font-family:monospace;"><span style="color: #0000ff;">struct</span> noop_deleter <span style="color: #008000;">&#123;</span>
    <span style="color: #0000ff;">void</span> operator<span style="color: #008000;">&#40;</span><span style="color: #008000;">&#41;</span><span style="color: #008000;">&#40;</span> <span style="color: #0000ff;">void</span><span style="color: #000040;">*</span> <span style="color: #008000;">&#41;</span> <span style="color: #008000;">&#123;</span>
        <span style="color: #666666;">// NO-OP</span>
    <span style="color: #008000;">&#125;</span>
<span style="color: #008000;">&#125;</span><span style="color: #008080;">;</span></pre></td></tr></table></div>

<p><span style="font-family: 'courier new', courier;">然后将上述用例的第三行改为：</span></p>

<div class="wp_syntax"><div class="code"><pre class="cpp" style="font-family:monospace;">shared_ptr q<span style="color: #008000;">&#40;</span> p, noop_deleter<span style="color: #008000;">&#40;</span><span style="color: #008000;">&#41;</span> <span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span></pre></div></div>

<p>但是这样一来，<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>就丧失了借助RAII自动释放资源的能力，违背了我们利用智能指针自动管理资源生存期的初衷（话说回来，这倒并不是说<span style="font-family: courier new,courier;">noop_deleter这种手法</span>毫无用处，Boost.Asio中就巧妙地利用<span style="font-family: courier new,courier;">shared_ptr</span>、<span style="font-family: courier new,courier;">weak_ptr</span>和<span style="font-family: courier new,courier;">noop_deleter</span>来实现异步I/O事件的取消）。从这个简单的用例可以看出，<span style="font-family: courier new,courier;">shared_ptr</span>就像是毒品一样，一旦沾染就难以戒除。更甚者，染毒者连换用其他“毒品”的权力都没有：<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>的引用计数管理接口是私有的，无法从<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>之外操控，也就无法从<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>迁移到其他类型的引用计数智能指针。</p>
<p>不仅如此，资源对象沾染上<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>之后，就只能使用<strong>最初的</strong>那个<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>实例的拷贝来维系自己的生存期。考察以下用例：</p>
<p>用例二：</p>

<div class="wp_syntax"><table><tr><td class="line_numbers"><pre>1
2
3
4
5
6
7
</pre></td><td class="code"><pre class="cpp" style="font-family:monospace;"><span style="color: #008000;">&#123;</span>
    shared_ptr p1<span style="color: #008000;">&#40;</span> CResource <span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
    shared_ptr p2<span style="color: #008000;">&#40;</span> p1 <span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>        <span style="color: #666666;">// OK</span>
    IResource<span style="color: #000040;">*</span> p3 <span style="color: #000080;">=</span> p1.<span style="color: #007788;">get</span><span style="color: #008000;">&#40;</span><span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
    shared_ptr p4<span style="color: #008000;">&#40;</span> p3 <span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>        <span style="color: #666666;">// ERROR</span>
                                           <span style="color: #666666;">// CRASH</span>
<span style="color: #008000;">&#125;</span></pre></td></tr></table></div>

<p>该用例的执行过程如下：</p>
<ol>
<li><span style="font-family: 'courier new', courier;">p1</span>在构造的同时为资源对象创建了一份外部引用计数，并将之置为1</li>
<li><span style="font-family: 'courier new', courier;">p2</span>拷贝自<span style="font-family: 'courier new', courier;">p1</span>，与<span style="font-family: 'courier new', courier;">p1</span>共享同一个引用计数，将之增加为2</li>
<li><span style="font-family: 'courier new', courier;">p4</span>并非<span style="font-family: 'courier new', courier;">p1</span>的拷贝，因此在构造的同时又为资源对象创建了另外一个外部引用计数，并将之置为1</li>
<li>在作用域结束时，<span style="font-family: 'courier new', courier;">p4</span>析构，由其维护的额外的引用计数降为0，导致资源对象被析构</li>
<li>然后<span style="font-family: 'courier new' courier;">p2</span>析构，对应的引用计数降为1</li>
<li>接着<span style="font-family: 'courier new', courier;">p1</span>析构，对应的引用计数也归零，于是<span style="font-family: 'courier new', courier;">p1</span>在临死之前再次释放资源对象</li>
<li>最后，由于资源对象被二次释放，程序崩溃</li>
</ol>
<p>至此，我们已经认识到了<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>的第一宗罪——<strong>传播毒品</strong>：</p>
<ul>
<li>毒性一：一旦开始对资源对象使用<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>，就必须一直使用</li>
<li>毒性二：无法换用其他类型的引用计数之智能指针来管理资源对象生存期</li>
<li>毒性三：必须使用最初的<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>实例拷贝来维系资源对象生存期</li>
</ul>
<p>乘胜追击，再揭露一下<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>的第二宗罪——<strong>散布病毒</strong>。有点耸人听闻了？其实道理很简单：由于使用了<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>的资源对象必须仰仗<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>的存在才能维系生存期，这就意味着使用资源的客户对象也必须使用<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>来持有资源对象的引用——于是<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>的势力范围成功地从资源对象本身扩散到了资源使用者，<strong>侵入了资源客户对象的实现</strong>。同时，资源的使用者往往是通过某种形式的资源分配器来获取资源。自然地，为了向客户转交资源对象的所有权，资源分配器也不得不在接口中传递<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr，于是</span></span><span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>也会<strong>侵入资源分配器的接口</strong>。</p>
<p>有一种情况可以暂时摆脱<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>，例如：</p>

<div class="wp_syntax"><div class="code"><pre class="cpp" style="font-family:monospace;">shared_ptr AllocateResource<span style="color: #008000;">&#40;</span><span style="color: #008000;">&#41;</span> <span style="color: #008000;">&#123;</span>
    shared_ptr pResource<span style="color: #008000;">&#40;</span> <span style="color: #0000dd;">new</span> CResource <span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
    bar<span style="color: #008000;">&#40;</span> pResource.<span style="color: #007788;">get</span><span style="color: #008000;">&#40;</span><span style="color: #008000;">&#41;</span> <span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
    <span style="color: #0000ff;">return</span> pResource<span style="color: #008080;">;</span>
<span style="color: #008000;">&#125;</span>
&nbsp;
<span style="color: #0000ff;">void</span> InitResource<span style="color: #008000;">&#40;</span> IResource<span style="color: #000040;">*</span> r <span style="color: #008000;">&#41;</span> <span style="color: #008000;">&#123;</span>
    <span style="color: #666666;">// Do resource initialization...</span>
<span style="color: #008000;">&#125;</span></pre></div></div>

<p>以上用例中，在<span style="font-family: courier new,courier;">InitResource</span>的执行期间，由于<span style="font-family: courier new,courier;">AllocateResource</span>的堆栈仍然存在，<span style="font-family: courier new,courier;">pResource</span>不会析构，因此可以放心的在<span style="font-family: courier new,courier;">InitResource</span>的参数中使用裸指针传递资源对象。这种基于调用栈的引用计数优化，也是一种常用的手段。但在<span style="font-family: courier new,courier;">InitResource</span>返回后，资源对象终究还是会落入<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>的魔掌。</p>
<p>由此可以看出，<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>打着“非侵入式”的幌子，虽然没有侵入资源对象的实现，却侵入了资源分配接口以及资源客户对象的实现。而沾染上<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>就摆脱不掉，如此传播下去，简直就是侵入了除资源对象实现以外的其他各个地方！这不是病毒是什么？</p>
<p>然而，<strong>基于<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>的引用计数解决方案真的不会侵入资源对象的实现吗？</strong></p>
<p>在一些用例中，资源对象的成员方法（不包括构造函数）需要获取指向对象自身，即包含<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">了this</span></span>指针的<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>。Boost.Asio的<a href="http://www.boost.org/doc/libs/1_39_0/doc/html/boost_asio/examples.html">chat示例</a>便展示了这样一个用例：<span style="font-family: courier new,courier;">chat_session</span>对象会在其成员函数中发起异步I/O操作，并在异步I/O操作回调中保存一个指向自己的<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>以保证回调执行时自身的生存期尚未结束。这种手法在Boost.Asio中非常常见，在不考虑<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>带来的麻烦时，这实际上也是一种相当优雅的异步流程资源生存期处理方法。但现在让我们把注意力集中在<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>上。</p>
<p>通常，使用<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>的资源对象必须动态分配，最常见的就是直接从堆上<span style="font-family: courier new,courier;">new</span>出一个实例并交付给一个<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>，或者也可以从某个资源池中分配再借助自定义的deleter在引用计数归零时将资源放回池中。无论是那种用法，该资源对象的实例在创建出来后，都总是立即交付给一个<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>（记为<span style="font-family: courier new,courier;">p</span>）。有鉴于之前提到的毒性三，如果资源对象的成员方法需要获取一个指向自己的<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>，那么这个<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>也必须是<span style="font-family: courier new,courier;">p</span>的一个拷贝——或者更本质的说，必须与<span style="font-family: courier new,courier;">p</span>共享同一个外部引用计数。然而对于资源对象而言，<span style="font-family: courier new,courier;">p</span>维护的引用计数是外部的陌生事物，资源对象如何得到这个引用计数并由此构造出一个合法的<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>呢？这是一个比较tricky的过程。为了解决这个问题，Boost提供了一个类模板<span style="font-family: courier new,courier;">enable_shared_from_this</span>：</p>
<blockquote><p>所有需要在成员方法中获取指向<span style="font-family: courier new,courier;">this</span>的<span style="font-family: courier new,courier;">shared_ptr</span>的类型，都必须以<a href="http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern">CRTP</a>手法继承自<span style="font-family: courier new,courier;">enable_shared_from_this</span>。即：</p>

<div class="wp_syntax"><div class="code"><pre class="cpp" style="font-family:monospace;"><span style="color: #0000ff;">class</span> CResource <span style="color: #008080;">:</span>
    <span style="color: #0000ff;">public</span> boost<span style="color: #008080;">::</span><span style="color: #007788;">enable_shared_from_this</span>
<span style="color: #008000;">&#123;</span>
    <span style="color: #666666;">// ...</span>
<span style="color: #008000;">&#125;</span><span style="color: #008080;">;</span></pre></div></div>

</blockquote>
<p>接着，资源对象的成员方法就可以使用<span style="font-family: courier new,courier;">enable_shared_from_this::shared_from_this()</span>方法来获取所需的指向对象自身的<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>了。问题似乎解决了。但是，等等！这样的继承体系不就对资源对象的实现有要求了吗？换言之，这不正是<strong>对资源对象实现的赤裸裸的侵入</strong>吗？这正是<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>的第三宗罪——<strong>欺世盗名</strong>。</p>
<p>最后一宗罪，是<strong>铺张浪费</strong>。对了，说的就是性能。</p>
<p>基于引用计数的资源生存期管理，打一出生起就被扣着线程同步开销大的帽子。早期的Boost版本中，<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>是借助Boost.Thread的<span style="font-family: courier new,courier;">mutex</span>对象来保护引用计数。在后期的版本中采用了lock-free的原子整数操作一定程度上降低了线程同步开销。然而即使是lock-free，本质上也仍然是串行化访问，线程同步的开销多少都会存在。也许有人会说这点开销与引用计数带来的便利相比算不得什么。然而在我们项目的异步服务器框架的压力测试中，大量引用计数的增减操作，一举吃掉了5%的CPU。换言之，1/20的计算能力被浪费在了与业务逻辑完全无关的引用计数的维护上！而且，由于是异步流程的特殊性，也无法应用上面提及的基于调用栈的引用计数优化。</p>
<p>那么针对这个问题就真的没有办法了吗？其实仔细检视一下整个异步流程，有些资源虽然会先后被不同的对象所引用，但在其整个生存周期内，每一时刻都只有一个对象持有该资源的引用。用于数据收发的缓冲区对象就是一个典型。它们总是被从某个源头产生，然后便一直从一处被传递到另一处，最终在某个时刻被回收。对于这样的对象，实际上没有必要针对流程中的每一次所有权转移都进行引用计数操作，只要简单地在分配时将引用计数置1，在需要释放时再将引用计数归零便可以了。</p>
<p>对于侵入式引用计数方案，由于资源对象自身持有引用计数并提供了引用计数的操作接口，可以很容易地实现这样的优化。但<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>则不然。<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>把引用计数牢牢地攥在手中，不让外界碰触；外界只有通过<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>的构造函数、析够函数以及<span style="font-family: courier new,courier;">reset()</span>方法才能够间接地对引用计数进行操作。而由于<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>的毒品特性，资源对象无法脱离<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>而存在，因此在转移资源对象的所有权时，也必须通过拷贝<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>的方式进行。一次拷贝就对应一对引用计数的原子增减操作。对于上述的可优化资源对象，如果在一个流程中被传递3次，除去分配和释放时的2次，还会导致6次无谓的原子整数操作。整整浪费了300%！</p>
<p>事实证明，在将基于<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>的非侵入式引用计数方案更改为侵入式引用计数方案并施行上述优化后，我们的异步服务器框架的性能有了明显的提升。</p>
<p>好了，最后总结一下<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>的四宗罪：</p>
<p><strong> 传播毒品</strong></p>
<p style="padding-left: 30px;">一旦对资源对象染上了<span style="font-family: courier new,courier;">shared_ptr</span>，在其生存期内便无法摆脱。</p>
<p><strong>散布病毒</strong></p>
<p style="padding-left: 30px;">在应用了<span style="font-family: courier new,courier;">shared_ptr</span>的资源对象的所有权变换的整个过程中的所有接口都会受到<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>的污染。</p>
<p><strong>欺世盗名</strong></p>
<p style="padding-left: 30px;">在<span style="font-family: courier new,courier;">enable_shared_from_this</span>用例下，基于<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>的解决方案并非是非侵入式的。</p>
<p><strong>铺张浪费</strong></p>
<p style="padding-left: 30px;">由于<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>隐藏了引用计数的操作接口，只能通过拷贝<span style="font-family: courier new,courier;">shared_ptr</span>的方式间接操纵引用计数，使得用户难以规避不必要的引用计数操作，造成无谓的性能损失。</p>
<p>探明这四宗罪算是最近一段时间的项目设计开发过程的一大收获。写这篇文章的目的不是为了将<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr</span></span>一棒子打死，只是为了总结基于<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr的C++非侵入式引用计数</span></span>解决方案的缺陷，也让自己不再盲目迷信<span style="font-family: mceinline;"><span style="font-family: courier new,courier;">shared_ptr。</span></span></p>
<!-- Jaxl IM embed starts -->
<script type="text/javascript">
(function() {
var jaxlChat = document.createElement("script");
jaxlChat.type = "text/javascript";
jaxlChat.async = true;
jaxlChat.src = "http://im.jaxl.im/ui/jaxl.php";
(document.getElementsByTagName("head")[0]||document.getElementsByTagName("body")[0]).appendChild(jaxlChat);
})();
</script>
<!-- Jaxl IM embed ends --><img src="http://feeds.feedburner.com/~r/liancheng/~4/ZD_IqQTdCvo" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://blog.liancheng.info/?feed=rss2&amp;p=85</wfw:commentRss>
		<slash:comments>15</slash:comments>
		<feedburner:origLink>http://blog.liancheng.info/?p=85</feedburner:origLink></item>
		<item>
		<title>:wq 2008 (2)</title>
		<link>http://feedproxy.google.com/~r/liancheng/~3/bEblLTArb8I/</link>
		<comments>http://blog.liancheng.info/?p=20#comments</comments>
		<pubDate>Thu, 19 Feb 2009 17:05:41 +0000</pubDate>
		<dc:creator>liancheng</dc:creator>
				<category><![CDATA[Work]]></category>
		<category><![CDATA[2008]]></category>

		<guid isPermaLink="false">http://blog.liancheng.info/?p=20</guid>
		<description><![CDATA[2009年也快过去两个月了，关于前一年的内容，写完这一篇也便就此打住。一来平日时间有限，写篇长长的博客实在是件奢侈的事情；二来这一年中所学甚多，但要总结起来不免会涉及到大量工作相关的内容，不方便详述。
自知之明
书接上回，从网易离职后，打算休整半个月，然后三月底到百度报到。而在这期间，我又干下了一件自不量力的事情。学校的一位老师找我合作一个项目，由于涉及保密条款，详细内容在此不表。题目很有挑战性，而且之前我对相关方向的经验完全是一片空白。这位老师曾经参加过我本科的毕业设计答辩，并且在网易时也曾经做了蛮长一段时间的同事。虽然项目本身也一定的经济回报，但当时完全是因为承蒙对方看得起，心存感激，才接了下来。当时预期半年内完成。

这个项目原本计划采用IE内核完成。然而我对IE实在是没有什么好印象，同时也很不擅长Windows编程。在MSDN中粗略翻阅之后更是大大加深了对IE 的反感——命名混乱的COM接口让人丈二和尚摸不着头脑、各种标记语言DOM API的不统一也让我万分窝火。
最终，经过一番调研，我决定采用Mozilla Gecko及相关技术来替代IE。虽然对于Gecko也同样是完全陌生，对作为Gecko应用层语言的JavaScript更是从未有过接触（虽然这是个Web横行的时代，我却是一点儿正规的Web项目经验也没有，对JS自然也就无比陌生），但Gecko的架构设计却令我相当欣赏。于是本着学些新东西的想法，我就以Gecko为基础，重新调整了设计并在工作之余开始开发。
向老师通报了设计的调整后，他也表示同意：新设计的开发方式更便捷，同时可扩展性也较之前的设计要好。我想当然地觉得半年之期绰绰有余，于是十分放心地趁着难得的假期和丫头一起到南京旅游了一番。
之后，便正式成为中关村村民。刚入职的时候，主要还是做些熟悉周遭环境与流程的D级、C级任务，工作之余还稍显空闲，差不多每天都有些富余精力来做些练手的实验，同时在Mozilla Wiki上扒拉水银泻地般的各种文档。开源项目往往都有这个毛病：文档残缺、版本凌乱。虽然Mozilla Wiki已经比较完整，还是让我抓瞎了很久。
自在了没多久，便逐渐开始接手繁多的A级任务。每天都忙得一头包，到家的时候，基本上都已经是23点后了。这样一来，只有周末的时间才可以利用。若是碰上加班，连周末也歇菜。
同时，经验匮乏的弊端也越来越深刻地暴露出来。无论是项目问题域本身还是相关的技术手段，我都不熟悉。这样一来，将头脑中的设计转变为硬盘上的代码就更是难上加难。加上可利用的时间只有周末，而隔上一周之后，往往又难以跟上上一周的思路。
于是乎，这个项目做得无比地痛苦——半年多的周末统统放弃不说，项目的进展还极其缓慢。每次老师来催问进度，我都极度地语塞。
最后，在接手了工作上的一个长期且艰巨的S级任务后，我实在是撑不住了……好在由于涉及的问题本身具有相当的难度，这个项目也有具有一定的实验性质。如果无法完成，也不会造成特别重大的后果。
硬着头皮给老师打电话说明了情况，忐忑不安地退出了这个项目，更是完全放弃所有了经济报酬。老师向我询问是否认识合适的人选可以接手，我思前想后，向他推荐了Pluskid。然而时值Kid选研究生导师，也没有精力参与。移交了设计及仅有雏形的Demo代码后，这个项目最终还是不了了之了。此时项目已经延期月余。
对于这个项目本身，唯一值得欣慰的是，我好歹也算是给出了一条新的设计思路，打破了之前的一些思维定势，同时证明了其技术可行性。同时，我自己从中也学到了很多极有价值的东西。其中关于Mozilla相关的技术，后来还给同事做过一次技术交流。而其中非技术的收获，更是终身难忘：
万事要有自知之明！


(function() {
var jaxlChat = document.createElement("script");
jaxlChat.type = "text/javascript";
jaxlChat.async = true;
jaxlChat.src = "http://im.jaxl.im/ui/jaxl.php";
(document.getElementsByTagName("head")[0]&#124;&#124;document.getElementsByTagName("body")[0]).appendChild(jaxlChat);
})();

]]></description>
			<content:encoded><![CDATA[<p><img class="size-full wp-image-68 alignleft" style="margin: 10px;" title="mozilla-logo2" src="http://blog.liancheng.info/wp-content/uploads/2009/02/mozilla-logo2.jpg" alt="mozilla-logo2" width="111" height="104" />2009年也快过去两个月了，关于前一年的内容，写完这一篇也便就此打住。一来平日时间有限，写篇长长的博客实在是件奢侈的事情；二来这一年中所学甚多，但要总结起来不免会涉及到大量工作相关的内容，不方便详述。</p>
<p><span style="font-size: large;"><strong>自知之明</strong></span></p>
<p>书接上回，从网易离职后，打算休整半个月，然后三月底到百度报到。而在这期间，我又干下了一件自不量力的事情。学校的一位老师找我合作一个项目，由于涉及保密条款，详细内容在此不表。题目很有挑战性，而且之前我对相关方向的经验完全是一片空白。这位老师曾经参加过我本科的毕业设计答辩，并且在网易时也曾经做了蛮长一段时间的同事。虽然项目本身也一定的经济回报，但当时完全是因为承蒙对方看得起，心存感激，才接了下来。当时预期半年内完成。</p>
<p><span id="more-20"></span></p>
<p>这个项目原本计划采用IE内核完成。然而我对IE实在是没有什么好印象，同时也很不擅长Windows编程。在MSDN中粗略翻阅之后更是大大加深了对IE 的反感——命名混乱的COM接口让人丈二和尚摸不着头脑、各种标记语言DOM API的不统一也让我万分窝火。</p>
<p>最终，经过一番调研，我决定采用Mozilla Gecko及相关技术来替代IE。虽然对于Gecko也同样是完全陌生，对作为Gecko应用层语言的JavaScript更是从未有过接触（虽然这是个Web横行的时代，我却是一点儿正规的Web项目经验也没有，对JS自然也就无比陌生），但Gecko的架构设计却令我相当欣赏。于是本着学些新东西的想法，我就以Gecko为基础，重新调整了设计并在工作之余开始开发。</p>
<p>向老师通报了设计的调整后，他也表示同意：新设计的开发方式更便捷，同时可扩展性也较之前的设计要好。我想当然地觉得半年之期绰绰有余，于是十分放心地趁着难得的假期和丫头一起到南京旅游了一番。</p>
<p>之后，便正式成为中关村村民。刚入职的时候，主要还是做些熟悉周遭环境与流程的D级、C级任务，工作之余还稍显空闲，差不多每天都有些富余精力来做些练手的实验，同时在Mozilla Wiki上扒拉水银泻地般的各种文档。开源项目往往都有这个毛病：文档残缺、版本凌乱。虽然Mozilla Wiki已经比较完整，还是让我抓瞎了很久。</p>
<p>自在了没多久，便逐渐开始接手繁多的A级任务。每天都忙得一头包，到家的时候，基本上都已经是23点后了。这样一来，只有周末的时间才可以利用。若是碰上加班，连周末也歇菜。</p>
<p>同时，经验匮乏的弊端也越来越深刻地暴露出来。无论是项目问题域本身还是相关的技术手段，我都不熟悉。这样一来，将头脑中的设计转变为硬盘上的代码就更是难上加难。加上可利用的时间只有周末，而隔上一周之后，往往又难以跟上上一周的思路。</p>
<p>于是乎，这个项目做得无比地痛苦——半年多的周末统统放弃不说，项目的进展还极其缓慢。每次老师来催问进度，我都极度地语塞。</p>
<p>最后，在接手了工作上的一个长期且艰巨的S级任务后，我实在是撑不住了……好在由于涉及的问题本身具有相当的难度，这个项目也有具有一定的实验性质。如果无法完成，也不会造成特别重大的后果。</p>
<p>硬着头皮给老师打电话说明了情况，忐忑不安地退出了这个项目，更是完全放弃所有了经济报酬。老师向我询问是否认识合适的人选可以接手，我思前想后，向他推荐了<a title="Free Mind" href="http://blog.pluskid.org">Pluskid</a>。然而时值Kid选研究生导师，也没有精力参与。移交了设计及仅有雏形的Demo代码后，这个项目最终还是不了了之了。此时项目已经延期月余。</p>
<p>对于这个项目本身，唯一值得欣慰的是，我好歹也算是给出了一条新的设计思路，打破了之前的一些思维定势，同时证明了其技术可行性。同时，我自己从中也学到了很多极有价值的东西。其中关于Mozilla相关的技术，后来还给同事做过一次技术交流。而其中非技术的收获，更是终身难忘：</p>
<p>万事要有自知之明！</p>
<!-- Jaxl IM embed starts -->
<script type="text/javascript">
(function() {
var jaxlChat = document.createElement("script");
jaxlChat.type = "text/javascript";
jaxlChat.async = true;
jaxlChat.src = "http://im.jaxl.im/ui/jaxl.php";
(document.getElementsByTagName("head")[0]||document.getElementsByTagName("body")[0]).appendChild(jaxlChat);
})();
</script>
<!-- Jaxl IM embed ends --><img src="http://feeds.feedburner.com/~r/liancheng/~4/bEblLTArb8I" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://blog.liancheng.info/?feed=rss2&amp;p=20</wfw:commentRss>
		<slash:comments>5</slash:comments>
		<feedburner:origLink>http://blog.liancheng.info/?p=20</feedburner:origLink></item>
		<item>
		<title>:wq 2008 (1)</title>
		<link>http://feedproxy.google.com/~r/liancheng/~3/PDo6BNcWbEE/</link>
		<comments>http://blog.liancheng.info/?p=1#comments</comments>
		<pubDate>Fri, 06 Feb 2009 16:44:20 +0000</pubDate>
		<dc:creator>liancheng</dc:creator>
				<category><![CDATA[Work]]></category>
		<category><![CDATA[2008]]></category>

		<guid isPermaLink="false">http://blog.liancheng.info/?p=1</guid>
		<description><![CDATA[去年12月份，和Pluskid、Quark以及马铃鼠一起合租了DreamHost的虚拟主机。然而付款的过程却极其地蜿蜒曲折，在被我无能地拖沓了漫长的整整一个季度后，借助远在美国本土的Victor同学的帮助，终于以支票支付的方式艰难地画上了句点。于是也就有了liancheng.info这个域名以及这个blog。啊……在此向Kid和Quark谢罪  
2008年是本命年。迷信说本命年会发生很多不顺的事情，是韬光养晦的年份。这一年确实碰到了很多非常有挑战的事件，在此一件件总结一下：
跳槽前的挣扎
这件事严格来说是从07年下半年开始，到08年初结束的。从网易杭研院跳槽到百度，其实07年10月就已经是板上钉钉的事情。但是却一直到08年2月才正式离职。之所以拖了这么久，原因是在于想尝试着推动几件事情，以尽力改变一些当时所面临的，同时也是促使我不得不选择跳槽的负面（技术）问题：

在开发人员和测试人员比例严重失调的情况下代码质量低下；
拿Java当作带GC的C++，完全无视Java开源社区的丰富资源、开发工具以及开发方法学的辅助作用；
代码风格混乱、可重用性差、可测性差、可维护性差；
文档严重缺失，导致在人员流动性高的情况下，新人难以快速接手遗留任务，青黄不接；
版本管理不规范。

在加入网易之前，我并没有什么实际的项目工程经验，然而在浙大MSTC的社团活动组织经验以及在这个家一般的优秀团队中的合作经验却给了我莫大的帮助。起初只是觉得碰到很多让人不爽的事情。摸爬滚打了一段时间之后，逐渐意识到了问题所在。其实根本原因还是在于管理层面，在此也就按下不表了。作为一个一线的研发人员，无力撼动其根本，于是也就只能在技术层面尽量地去做一些改良，以期能在阻止情形进一步恶化的同时让更多人意识到问题所在。
当时的局面很不理想，一方面存在管理上的原因；另一方面，保守的思想观念以及恶劣的习惯也造成了极大的障碍：即使大家知道这样是错误的，也知道怎样是正确的，却也没有改正的动力。
我当时的想法很简单：尽力而为。离职的意向已经跟经理表明，没有什么心理负担，能做成做好，做不成也没有什么遗憾。加之我当时所负责的工作相对繁杂，交接过程很长。不妨就在这段时间内去尝试推行一些我认为是正确的做法。
其实在决定干这件事情之前，我也已经花了大半年的时间来准备。我原本并不是一个熟练的Java程序员，但做完这些准备工作之后，这门语言已经用得相当顺手了。当时入手的方面有如下几条：

Java开源社区成熟组件的应用：

使用Apache MINA替代土制山寨版NIO网络框架
使用JMX + Cacti完成服务监控、运营数据采集和数据可视化
使用Spring在保证低耦合的前提下完成应用组装
使用iBatis替代土制山寨版DAO框架
使用OpenLDAP完成集群配置的集中管理
 


因地制宜地推行敏捷开发方法

极力澄清“重构”与“重写”的区别（似乎国内的很多公司都有这样的误区，拿重写当重构），借助IDE（eclipse）快速重构
基于JUnit、DbUnit的单元测试
规范基于SVN的SCM
推行Trac
基于Maven2的项目规范化组织和基于Maven2 + Cotinuum的持续集成


借助binghe同学超群的系统管理技术尽量将开发层面的复杂度降解至系统管理层面：

借助Linux HA Heartbeat解决单点可靠性问题
借助cfengine + SVN完成集群配置的自动分发和版本管理



划掉的四条最终很可惜没能完成。尤其是最后两条，甚至没能来得及做深入的了解，尤为可惜。由于当时除了承担一部分服务器项目的研发工作以外，还负责一整套旧版本服务器集群的运维工作，实际上成了半个SA。于是，既出于工作需要，也出于兴趣，我经常会泡在SA的办公室里向binghe他们几个SA兄弟讨教系统管理技术。在binghe的熏陶之下，我逐渐深刻地认识到：优秀的系统管理工具和技术不仅可以大大简化大型服务器集群的日常运维，在化解研发复杂度方面，往往也可以出奇兵奇效。而Heartbeat和cfengine，正是杭研院SA的两大利器。只可惜我所负责的集群，跑的都是极老的FreeBSD 4.x，当时FreeBSD官方已经停止维护，想装个软件都得满互联网找上大半天。出于服务稳定性和风险考虑，最终还是没能深入实践一下。
所幸对于其他各个条目都有所斩获。我按照手头掌握的工具和方法重写了之前负责的一个模块，在可重用性、可测性、代码可维护性、服务可维护性、模块耦合性、持续集成等方面都有了明显的收效。在做完所有这些之后，我开始拿这个项目为模板，向周围的同事逐个“传教”。在这期间，针对不同的人，我也总结了不同的游说方法：

对于观念开放和激进的人，比较好办。只要稍加推荐，他们便会主动地去尝试。当他们发现这些工具和方法确实行之有效时，便自然而然的接受了。
对于观念保守的人，在他们碰到头大的问题的时候杀入，尝试向他们介绍在正确的工具和方法的帮助下，如何轻松地化解这些问题。有了亲身体会，便可以相对容易地让他们从心里上接受这些新的工具和方法。
对于经理，除了要向其阐明利害关系，最重要的是给出从当前情况逐步迁移到理想情况的循序渐进方案。

不可忽视的一点是：在技术性团体里，做永远比说要来的有效。在想法成熟之前就贸然游说，往往会招致相反的效果。大家都是工程师，不会贸然接纳陌生事物。如果自己都还没有想清楚就开始大肆游说，往往会被大家提出的实际的工程问题驳斥地体无完肤。当你哑口无言之时，大家也已经对你的方案产生了难以磨灭的“不靠谱”的第一印象，这时要再想咸鱼翻身，可就没那么容易了。
相对的，首先自行查阅文档资料并进行试验，制作demo，通过试验发现和解决实际出现的问题。在想法基本成型时，和个别观念开放的同仁进行探讨，这时往往可以发现大量之前自己没有考虑到的问题，再转而细化方案。这个过程反复迭代几次之后，方案和demo逐渐成熟，同时也潜移默化地达到了传教的目的。等到方案完全成熟之后，再拿出实际可工作的demo开始游说，这时自然就成竹在胸了。
好吧，我承认，当时我就是抱着以上这些的想法在做这件事情的。然而，最终还是没能把整件事情做成。原因是我忽视了两件事。
第一件事，是习惯。鲁迅大叔不是说过么，中国的惯性太大，挪个桌子都要流血。加上当时的杭研院还缺乏绩效考核机制，能够在原来的基础上完成工作就不错了。即便是大家都说好，也还是难以改变大家长久以来养成的习惯。实际表明，仅仅是把eclipse从3.1升级到3.2，也是让人难以忍受的。
第二件事，是学习成本。在紧张的工作进度压力下，原本就没有多少人把精力投入到技术、方法改良方面。自己去学习和摸索各种工具和方法，和让团队中的每个人都掌握这些工具和方法，完全是两回事。授人与渔也不是谁都能轻易做到的呀……
最终，到我离职时为止，虽然我所尝试推行的方法和工具都得到了认可，但最终残留下来的只有：

基于Maven2的项目组织方法和自动构建
基于JMX + Cacti实现的服务监控和数据可视化系统。
基于Continuum的持续集成服务还半死不活的跑着，但是已经无人问津
一套被荒废的Trac

这件事情给我带来了相当大的冲击，同时也学习到了大量技术方面和非技术方面的知识。所谓团队合作，如果仅仅只是默契地配合他人，那么只能算是个合格的团队成员。要称得上优秀，同时也必须要能够在团队陷入错误时大声地喊出来，并且以实事求是的态度给出可行的解决方案，同时还要能够随机应变、因地制宜，切实地将方案贯彻下去。
尽管在网易时碰到了很多不如意的事情和现象，让我抱怨多多；但是回想起来，作为我所就职的第一个公司，它带给我的更多的是极其广阔的学习机会，无论是技术方面还是非技术方面。所幸，我也没有荒废这大好的机会。


(function() {
var jaxlChat = document.createElement("script");
jaxlChat.type = "text/javascript";
jaxlChat.async = true;
jaxlChat.src = "http://im.jaxl.im/ui/jaxl.php";
(document.getElementsByTagName("head")[0]&#124;&#124;document.getElementsByTagName("body")[0]).appendChild(jaxlChat);
})();

]]></description>
			<content:encoded><![CDATA[<p><img class="size-full wp-image-71 alignright" style="margin: 10px;" title="dreamhost-logo" src="http://blog.liancheng.info/wp-content/uploads/2009/02/dreamhost-logo.jpg" alt="dreamhost-logo" width="96" height="96" />去年12月份，和<a title="Free Mind" href="http://blog.pluskid.org" target="_blank">Pluskid</a>、Quark以及<a title="肖恩的视觉日记" href="http://hi.baidu.com/chenshuzju" target="_blank">马铃鼠</a>一起合租了<a title="DreamHost" href="http://www.dreamhost.com">DreamHost</a>的虚拟主机。然而付款的过程却极其地蜿蜒曲折，在被我无能地拖沓了漫长的整整一个季度后，借助远在美国本土的Victor同学的帮助，终于以支票支付的方式艰难地画上了句点。于是也就有了liancheng.info这个域名以及这个blog。啊……在此向Kid和Quark谢罪 <img src='http://blog.liancheng.info/wp-includes/images/smilies/icon_sad.gif' alt=':-(' class='wp-smiley' /> </p>
<p>2008年是本命年。迷信说本命年会发生很多不顺的事情，是韬光养晦的年份。这一年确实碰到了很多非常有挑战的事件，在此一件件总结一下：</p>
<p><span style="font-size: large;"><strong>跳槽前的挣扎</strong></span></p>
<p>这件事严格来说是从07年下半年开始，到08年初结束的。从网易杭研院跳槽到百度，其实07年10月就已经是板上钉钉的事情。但是却一直到08年2月才正式离职。之所以拖了这么久，原因是在于想尝试着推动几件事情，以尽力改变一些当时所面临的，同时也是促使我不得不选择跳槽的负面（技术）问题：<span id="more-1"></span></p>
<ol>
<li>在开发人员和测试人员比例严重失调的情况下代码质量低下；</li>
<li>拿Java当作带GC的C++，完全无视Java开源社区的丰富资源、开发工具以及开发方法学的辅助作用；</li>
<li>代码风格混乱、可重用性差、可测性差、可维护性差；</li>
<li>文档严重缺失，导致在人员流动性高的情况下，新人难以快速接手遗留任务，青黄不接；</li>
<li>版本管理不规范。</li>
</ol>
<p>在加入网易之前，我并没有什么实际的项目工程经验，然而在浙大MSTC的社团活动组织经验以及在这个家一般的优秀团队中的合作经验却给了我莫大的帮助。起初只是觉得碰到很多让人不爽的事情。摸爬滚打了一段时间之后，逐渐意识到了问题所在。其实根本原因还是在于管理层面，在此也就按下不表了。作为一个一线的研发人员，无力撼动其根本，于是也就只能在技术层面尽量地去做一些改良，以期能在阻止情形进一步恶化的同时让更多人意识到问题所在。</p>
<p>当时的局面很不理想，一方面存在管理上的原因；另一方面，保守的思想观念以及恶劣的习惯也造成了极大的障碍：即使大家知道这样是错误的，也知道怎样是正确的，却也没有改正的动力。</p>
<p>我当时的想法很简单：尽力而为。离职的意向已经跟经理表明，没有什么心理负担，能做成做好，做不成也没有什么遗憾。加之我当时所负责的工作相对繁杂，交接过程很长。不妨就在这段时间内去尝试推行一些我认为是正确的做法。</p>
<p>其实在决定干这件事情之前，我也已经花了大半年的时间来准备。我原本并不是一个熟练的Java程序员，但做完这些准备工作之后，这门语言已经用得相当顺手了。当时入手的方面有如下几条：</p>
<ol>
<li>Java开源社区成熟组件的应用：
<ol>
<li>使用Apache MINA替代土制山寨版NIO网络框架</li>
<li>使用JMX + Cacti完成服务监控、运营数据采集和数据可视化</li>
<li>使用Spring在保证低耦合的前提下完成应用组装</li>
<li><span style="text-decoration: line-through;">使用iBatis替代土制山寨版DAO框架</span></li>
<li><span style="text-decoration: line-through;">使用OpenLDAP完成集群配置的集中管理<br />
 </span></li>
</ol>
</li>
<li>因地制宜地推行敏捷开发方法
<ol>
<li>极力澄清“重构”与“重写”的区别（似乎国内的很多公司都有这样的误区，拿<strong>重写</strong>当<strong>重构</strong>），借助IDE（eclipse）快速重构</li>
<li>基于JUnit、DbUnit的单元测试</li>
<li>规范基于SVN的SCM</li>
<li>推行Trac</li>
<li>基于Maven2的项目规范化组织和基于Maven2 + Cotinuum的持续集成</li>
</ol>
</li>
<li>借助<a title="氷河/LispWorks 5.1.2" href="http://tianchunbinghe.blog.163.com/">binghe</a>同学超群的系统管理技术尽量将开发层面的复杂度降解至系统管理层面：
<ol>
<li><span style="text-decoration: line-through;">借助Linux HA Heartbeat解决单点可靠性问题</span></li>
<li><span style="text-decoration: line-through;">借助cfengine + SVN完成集群配置的自动分发和版本管理</span></li>
</ol>
</li>
</ol>
<p>划掉的四条最终很可惜没能完成。尤其是最后两条，甚至没能来得及做深入的了解，尤为可惜。由于当时除了承担一部分服务器项目的研发工作以外，还负责一整套旧版本服务器集群的运维工作，实际上成了半个SA。于是，既出于工作需要，也出于兴趣，我经常会泡在SA的办公室里向binghe他们几个SA兄弟讨教系统管理技术。在binghe的熏陶之下，我逐渐深刻地认识到：优秀的系统管理工具和技术不仅可以大大简化大型服务器集群的日常运维，在化解研发复杂度方面，往往也可以出奇兵奇效。而Heartbeat和cfengine，正是杭研院SA的两大利器。只可惜我所负责的集群，跑的都是极老的FreeBSD 4.x，当时FreeBSD官方已经停止维护，想装个软件都得满互联网找上大半天。出于服务稳定性和风险考虑，最终还是没能深入实践一下。</p>
<p>所幸对于其他各个条目都有所斩获。我按照手头掌握的工具和方法重写了之前负责的一个模块，在可重用性、可测性、代码可维护性、服务可维护性、模块耦合性、持续集成等方面都有了明显的收效。在做完所有这些之后，我开始拿这个项目为模板，向周围的同事逐个“传教”。在这期间，针对不同的人，我也总结了不同的游说方法：</p>
<ol>
<li>对于观念开放和激进的人，比较好办。只要稍加推荐，他们便会主动地去尝试。当他们发现这些工具和方法确实行之有效时，便自然而然的接受了。</li>
<li>对于观念保守的人，在他们碰到头大的问题的时候杀入，尝试向他们介绍在正确的工具和方法的帮助下，如何轻松地化解这些问题。有了亲身体会，便可以相对容易地让他们从心里上接受这些新的工具和方法。</li>
<li>对于经理，除了要向其阐明利害关系，最重要的是给出从当前情况逐步迁移到理想情况的循序渐进方案。</li>
</ol>
<p>不可忽视的一点是：在技术性团体里，做永远比说要来的有效。在想法成熟之前就贸然游说，往往会招致相反的效果。大家都是工程师，不会贸然接纳陌生事物。如果自己都还没有想清楚就开始大肆游说，往往会被大家提出的实际的工程问题驳斥地体无完肤。当你哑口无言之时，大家也已经对你的方案产生了难以磨灭的“不靠谱”的第一印象，这时要再想咸鱼翻身，可就没那么容易了。</p>
<p>相对的，首先自行查阅文档资料并进行试验，制作demo，通过试验发现和解决实际出现的问题。在想法基本成型时，和个别观念开放的同仁进行探讨，这时往往可以发现大量之前自己没有考虑到的问题，再转而细化方案。这个过程反复迭代几次之后，方案和demo逐渐成熟，同时也潜移默化地达到了传教的目的。等到方案完全成熟之后，再拿出实际可工作的demo开始游说，这时自然就成竹在胸了。</p>
<p>好吧，我承认，当时我就是抱着以上这些的想法在做这件事情的。然而，最终还是没能把整件事情做成。原因是我忽视了两件事。</p>
<p>第一件事，是习惯。鲁迅大叔不是说过么，中国的惯性太大，挪个桌子都要流血。加上当时的杭研院还缺乏绩效考核机制，能够在原来的基础上完成工作就不错了。即便是大家都说好，也还是难以改变大家长久以来养成的习惯。实际表明，仅仅是把eclipse从3.1升级到3.2，也是让人难以忍受的。</p>
<p>第二件事，是学习成本。在紧张的工作进度压力下，原本就没有多少人把精力投入到技术、方法改良方面。自己去学习和摸索各种工具和方法，和让团队中的每个人都掌握这些工具和方法，完全是两回事。授人与渔也不是谁都能轻易做到的呀……</p>
<p>最终，到我离职时为止，虽然我所尝试推行的方法和工具都得到了认可，但最终残留下来的只有：</p>
<ol>
<li>基于Maven2的项目组织方法和自动构建</li>
<li>基于JMX + Cacti实现的服务监控和数据可视化系统。</li>
<li>基于Continuum的持续集成服务还半死不活的跑着，但是已经无人问津</li>
<li>一套被荒废的Trac</li>
</ol>
<p>这件事情给我带来了相当大的冲击，同时也学习到了大量技术方面和非技术方面的知识。所谓团队合作，如果仅仅只是默契地配合他人，那么只能算是个合格的团队成员。要称得上优秀，同时也必须要能够在团队陷入错误时大声地喊出来，并且以实事求是的态度给出可行的解决方案，同时还要能够随机应变、因地制宜，切实地将方案贯彻下去。</p>
<p>尽管在网易时碰到了很多不如意的事情和现象，让我抱怨多多；但是回想起来，作为我所就职的第一个公司，它带给我的更多的是极其广阔的学习机会，无论是技术方面还是非技术方面。所幸，我也没有荒废这大好的机会。</p>
<!-- Jaxl IM embed starts -->
<script type="text/javascript">
(function() {
var jaxlChat = document.createElement("script");
jaxlChat.type = "text/javascript";
jaxlChat.async = true;
jaxlChat.src = "http://im.jaxl.im/ui/jaxl.php";
(document.getElementsByTagName("head")[0]||document.getElementsByTagName("body")[0]).appendChild(jaxlChat);
})();
</script>
<!-- Jaxl IM embed ends --><img src="http://feeds.feedburner.com/~r/liancheng/~4/PDo6BNcWbEE" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://blog.liancheng.info/?feed=rss2&amp;p=1</wfw:commentRss>
		<slash:comments>9</slash:comments>
		<feedburner:origLink>http://blog.liancheng.info/?p=1</feedburner:origLink></item>
	</channel>
</rss><!-- Dynamic page generated in 1.426 seconds. --><!-- Cached page generated by WP-Super-Cache on 2010-08-06 04:01:59 -->
