RSS

REST API的身份验证(Authentication)

在Amazon就职两周半了,很有意思,和微软截然不同的感觉。不过这个以后再说,今天想聊聊刚才在InfoQ上看到的一篇文章:RESTful API Authentication Schemes,当然InfoQ上的其实只是转载,原文是George Reese写的Principles for Standardized REST Authentication

我在微软做的第一个Project就是一个基于REST协议API集已经一个Authentication引擎,所以此后对这两个话题一直很有兴趣,即使手边做的是其他类型的项目。这里我就结合当年自己做的那个项目来聊聊George Reese文章中所提出的3个观点。

George Reese在文中概括了他认为REST API Authentication所应该遵循的3条原则:

1. All REST API calls must take place over HTTPS with a certificate signed by a trusted CA. All clients must validate the certificate before interacting with the server.

译:所有REST API请求都必须通过加密的HTTPS协议来传递,加密所用的证书应由一个trusted CA (certificate authority)签发。所有客户端必须验证服务器端的身份证书,然后才开始进行会话。

关于这点我想应该是毫无疑义的。所有客户端和服务器端的通信内容应该都要通过加密通道(HTTPS)传输,明文的HTTP通道将会是man-in-the-middle及其各种变种攻击的温床。所谓man-in-the-middle攻击简单讲就是指恶意的黑客可以在客户端和服务器端的明文通信通道上做手脚,黑客可以监听通信内容,偷取机密信息,甚至可以篡改通信内容,而通过注入RSA公匙加密后的通信内容理论上是无法被破译的(除非对应的private key被偷了…)。George很幽默地说道:如果你没有对你的API调用请求加密,你甚至没有在假装很安全。

不过另外有一点经常被忽略的就是:即使使用了加密的HTTPS协议,客户端也必须对服务器端的身份进行验证。每当说到身份验证(Authentication),人们总是先想到服务器端对客户端进行的身份验证(以确定你是哪个用户),但其实反向的客户端对服务器端的验证也是极为重要的。在这个飞贼横行,假冒遍地的年代,你怎么知道为你提供服务的对象真的是你所指定的服务器呢?一个简单的spoof,或者被恶意篡改过的hosts文件,都可能让你在不知不觉中把各种机密信息心甘情愿地交给躲在暗处的偷儿。但请注意我这里并不是说我们用的authentication模式一定是传统意义上基于电子证书的Mutual SSL Authenatication,因为客户端不一定会拥有或使用独立电子证书,事实了,基于经济成本的考虑,大多数end-user并没有由一个trusted CA发布的电子证书(这玩意儿可不是太便宜,至少大多数情况下是这样,嗯,当然有些例外,比如GoDaddy这种的,记得那时10美元就可以搞一个,汗…)

 
2. All REST API calls should occur through dedicated API keys consisting of an identifying component and a shared, private secret. Systems must allow a given customer to have multiple active API keys and de-activate individual keys easily.

译:所有REST API必须由专门的API密匙来验证。API密匙(注意:这里的密匙是广义的,不一定是指public key authentication中的key)必须有一个身份确认段,以及一个客户端/服务器端共享的私有秘密。服务器端的系统必须允许每个用户拥有一个或多个API密匙,并且可以方便地禁用某些密匙(使之不再有效)。

这段话可能是文中写的最模糊的内容,我还是结合我们当年设计的方案来解释一下吧:

我们当年提供的客户端验证手段主要是基于mutual ssl(如果客户拥有自己的电子证书的话)和我们自己设计的一种authentication token。客户可以随时向我们申请任意个authentication token,我们会通过一个HTTPS通道将authentication token发送给客户,客户拿到token以后应该妥善保存,不应该和任何人分享(因为这是他们的身份证明,如果别人知道了,就可以冒充他们了)。之后,在客户向我们递交请求时(同样还是在一个HTTPS通道上),他必须同时递交一个authentication token来证明他的身份,我们在服务器端会进行验证。

Authentication token的设计大致是这样的:

AuthenticationToken = RealmString “:” TimeStamp “:”Domain “:”SignedPackage

而其中最关键的SignedPackage是这样定义的:Base64(S(Gpri,  RealmString + “:” + TimeStamp + “:” + DNS + OwnerPUID)),其中,Gpri是我们的private key,S代表签名操作,而Base64就是Base64编码。也就是说,SignedPackage是这样生成的:先取得这个字符串:RealmString + “:” + TimeStamp + “:” + DNS + OwnerPUID,然后用我们的private key对该字符串进行签名加密,最后用Base64编码一下(这个只是为了HTTP协议传输的需要,和安全性无关)。

对照George Reese的文章,就可以看出,他所谓的identifying component在我们的设计中就是DNS部分,而shared, private secret就是整个SignedPackage部分。请注意AuthenticationToken中的TimeStamp部分,也就是说,客户在不同时刻申请的token都会是不同的,所以这使得用户可以拥有多个token。而我们在服务器端也用很简单的机制可以revoke任何已经发出的token。

最后可能有人要问这里的OwnerPUID是干什么的,这个其实是我们服务中的一个特殊的设计,和我们所提供的服务细节有关,这里就先不讨论了,也许下次有机会。

3. All REST queries must be authenticated by signing the query parameters sorted in lower-case, alphabetical order using the private credential as the signing token. Signing should occur before URL encoding the query string.

所有REST的查询请求都必须经过验证,验证的方式是客户端需对所有查询参数以小写字母顺序排序后用上文所提到的shared, private secret进行签名加密。签名加密的步骤应该在对查询字符串进行URL编码之前执行

Ok, 这点是我不敢苟同的。对于查询字串加密会造成很多问题:比如所有的代理服务器的功能都无效了,比如服务器端缓存就无法实现了,最搞笑的是用户如果想把某个REST查询在Facebook上分享给朋友都不可能了。

其实最最重要的是我觉得要求在REST查询的URL上夹带authentication信息是对REST原则的一种违反。我不敢说我完全理解Roy Fielding大师论文的真义,但我觉得REST协议的一个基本原则就是低耦合,也就是专项专用。URL就是用来决定resource的位置的,而不应该也不必要再夹带其他功能,比如身份验证。身份验证的信息完全可以通过其他标准的HTTP协议的组件来实现(比如最简单的Authorization请求报头 – request header)。

 

George Reese的文章总体上还是很有意思的,但我唯独对第三点有所异议,不过当然这只是一些我个人的看法。什么是REST,REST应该怎样实现,REST的根本在哪里,这些内容似乎是blogsphere上吵不完的话题,我就不参与了,呵呵。

 

Posted by on 2010 年 01 月 27 日 in 未分类

1 Comment

Last day at Microsoft

明天(北美太平洋时间)是我在微软的最后一天。从2006年8月14日起至2010年1月8日止,我的微软生涯要告一段落了。

不过幸好我下一站的停靠是去Amazon,所以这个blog的抬头:Sleepless in Seattle,倒还是不用修改。我将会加入Amazon的Elastic Compute Cloud项目组,也就是俗称“云计算”的那个地方。

我从没有想过会一辈子都在微软工作,但也没有想过会这么快的离开。原来的打算是想待个5年再考虑其他的,现在只是3年多一些就决定要离开了,其中的种种变化和曲折暂时还不足为人道,我想我需要相当一段时间去消化和体会职场生存的种种明波暗流。总之在微软的3年总体而言还是收获颇丰的,前2年的确是一路都顺风顺水轻舟而过,但很大程度上也助长了自己自以为是等一些的坏习惯,而近9个月的徘徊也让自己有机会重新审视很多自身存在的缺陷和过去错误的决定。好在最终还是能及早脱身去一个自己真正感兴趣的项目重新开始,已经觉得实在是很幸运。

我不敢妄论自己在微软的3年能有什么感悟,至多,也可能不过是一些朦胧的经验教训。我很不知深浅地写出来,也许会对其他什么人偶尔有个启发:

1. 永远选择你最感兴趣的项目而不是升职空间等所谓的职业发展前景。真正的职场提升,是指在技术上的日积月累潜修默练。若是你对所做的东西不敢兴趣,你是做不到静下心来去钻研的。不要为了一点点待遇上的提高或一些所谓的上升空间的拓展绞尽脑汁。中国有句老话,叫机关算尽反误了卿卿性命,颇得其中三味。

2. 职场行走,先学做人。而做人最好的途径,我觉得,就是 1) 换位思考,助人为乐, 2) 制造双赢,互相帮助,3) 谦虚大度,平易近人。

3. 找一个好老板。找不到,想办法改善与现在老板的关系。改善不了,还是另谋职位吧。

4. 大部分合格的manager一般都喜欢这样的手下:1. 有大局观,以团队为重。2. 有能力,有执行力。3. 永远和他/她同进同退。

5. 交流的最终目的是Build Trust,不是show off,也不是制造紧张或矛盾。所以下一次你想让人家帮你什么忙时,可以首先想想能让对方从结果中收获点什么,因为没有比输送利益更好的Build Trust的方法了。

6. 不好听的话要多听。不好听的话要少说。

 

好了,去Amazon看看吧。

 

Posted by on 2010 年 01 月 08 日 in 未分类

9 Comments

利用Powershell做简单的单元测试

上周为测试组写了一个用来对测试环境进行配置的小工具,在编写的时候要不停地调试看看功能是否运行正确。我们平时写项目是有unit test framework的,但写个小工具我就难得去加什么正式的unit test了。

不过没unit test,写程序的时候又浑身不舒服,所以就临时想在Powershell里写几个小scrip来做些简单的测试。因为这个工具是用C#写的,所以用Powershell直接调用assembly里的那些函数其实很方便。

在Powershell里要调用一个assembly里的类以及其方法,首先要做的是把这个assembly加载到当前的AppDomain里来。Powershell启动的时候是有一个默认的AppDomain的,其中也已经默认加载了一些.NET的基础assembly,可以用如下命令查看:

image

然后我写了一个简单的脚本来加载需要测试的assembly:

//load.ps1
 
if ($args.Length -ne 1 -or $args[0] -eq "/?" -or $args[0] -eq "-?")
{
  Write-Host "Usage:"
  Write-Host "  load.ps1 <Path to .NET assembly>"
  return
}
 
if (!(Test-Path $args[0]))
{
  Write-Host "ERROR: Specified .NET assembly does not exist"
  return
}
 
$assemblyPath = resolve-path $args[0]
Write-Host "$assemblyPath"
[System.Reflection.Assembly]::LoadFile($assemblyPath)

这样调用:

PS C:\me\Projects\Powershell> .\load.ps1 MathLib.dll

之后你可以再运行一下:[AppDomain]::CurrentDomain.GetAssemblies()来看看结果如何。

Assembly加载进来后就可以调用其中的类型以及函数了,比如你有如下的C#类:

namespace MathLib
{
    public class Math
    {
        public static double Add(double n1, double n2)
        {
            return n1 + n2;
        }
        public static double Subtract(double  n1, double n2)
        {
            return n1 - n2;
        }
        public double Multiply(double n1, double n2)
        {
            return n1 * n2;
        }
        public double Divide(double n1, double n2)
        {
            return n1 / n2;
        }
    }
}

你就可以在Powershell下直接这样调用:

PS C:\me\Projects\Powershell> [MathLib.Math]::Add(1, 2)

如果需要调用的不是static的成员函数也很简单,只要用“New-Object”先生成一个对象就可以调用了:

PS C:\me\Projects\Powershell> $obj = New-Object MathLib.Math

PS C:\me\Projects\Powershell> $obj.Multiply(3, 8)

下面这个小脚本可以快速看一下某个类所提供的成员函数:

//list-methods.ps1
 
$obj = New-Object $args[0]
$methods = $obj.GetType().GetMethods()
foreach ($m in $methods)
{
  Write-Host " - " $m.ReturnParameter.Member
}

 

这之后我其实发现这个方法有一个很大的问题,那就是加载到Powershell默认AppDomain中的assembly是无法动态卸载的,唯一的方法是关闭当前的Powershell窗口。这样的话,如果我们对代码进行了一些改动,但重新编译后却无法覆盖先前的assembly(因为它还是处于被加载的状态,想copy的话系统会报Access Denied)!这确实是很不方便(每次重新编译后,都要关掉Powershell窗口,copy新的assembly,然后再打开一个Powershell)。为了解决这个问题我看了很多资料,试用了N多的方法,不过最终没有找到一个好的解答。

首先要明确的是.NET中没有提供动态卸载单个Assembly的函数(动态加载是可以的,如上文所示)。所以只有通过卸载Assembly所在的那整个AppDomain才可以。不过我们显然不可能卸载Powershell自己所在的当前默认AppDomain,那么自然的答案就是先建立一个新的AppDomain,然后在新的AppDomain里加载所需的Assembly,使用完毕后再卸载整个AppDomain。

这个思路似乎是可行的,但在实际试验中我没有发现任何一种行得通的做法,我试过AppDomain::Load, AppDomain::CreateInstance, AppDomain::CreateInstanceAndUnwrap, AppDomain::CreateInstanceFrom, AppDomain::CreateInstanceFromAndUnwrap等各种方法,都没有成功。其中CreateInstance这一系列函数确实可以生成运行时的一个对象,但因为这些方法返回的结果是一个Object,而在没有将Assembly加载进来前,我又没有相关的类型定义可以用来做typecast,所以空有一个对象却无法调用所需的函数(如果你看完这段还能理解的话,我很佩服;不懂的话也没关系,自己动手试一下很快就明白我的意思了)。

我也查到有些资料说关键是要将你的Aseembly编译为强命名的(strong-named)并且将其加入GAC(过后再从GAC里清理出去即可),比如这篇文章:

http://www.eggheadcafe.com/software/aspnet/30766269/how-to-load–unload-dll.aspx

但我过后发现实际情况并非如此。事实上,你不用把你的assembly加入GAC(strong-named只是允许将assembly加入GAC前的先决条件)也有办法将它加载到一个新的AppDomain中来,下面是我写的代码:

// load-in-new-appdomain.ps1
 
$appDomain = [System.AppDomain]::CreateDomain("TempDomain")
Write-Host "Assemblies in the temp AppDomain"
$appDomain.GetAssemblies()
 
Write-Host "Assemblies in the default AppDomain"
[AppDomain]::CurrentDomain.GetAssemblies()
 
$appDomain.GetAssemblies()[0].GetType()::LoadFile("C:\me\Projects\Powershell\MathLib.dll")
Write-Host "Assemblies in the temp AppDomain after loading"
$appDomain.GetAssemblies()
 
Write-Host "Assemblies in the default AppDomain after loading"
[AppDomain]::CurrentDomain.GetAssemblies()
 
[AppDomain]::Unload($appDomain)
 
Write-Host "Assemblies in the temp AppDomain after unloading"
$appDomain.GetAssemblies()
 
Write-Host "Assemblies in the default AppDomain after unloading"
[AppDomain]::CurrentDomain.GetAssemblies()

看上去,我确实是对新建的$appDomain在调用LoadFile,但结果呢:

PS C:\me\Projects\Powershell> .\load-in-new-appdomain.ps1

Assemblies in the temp AppDomain

GAC    Version        Location

—    ——-        ——–

True   v2.0.50727     C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorlib.dll

Assemblies in the temp AppDomain after loading

True   v2.0.50727     C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorlib.dll

Assemblies in the default AppDomain after loading

True   v2.0.50727     C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorlib.dll

True   v2.0.50727     C:\Windows\assembly\GAC_MSIL\Microsoft.PowerShell.ConsoleHost\1.0.0.0__31bf3856ad364e35\Micros…

True   v2.0.50727     C:\Windows\assembly\GAC_MSIL\System.Xml\2.0.0.0__b77a5c561934e089\System.Xml.dll

False  v2.0.50727     C:\me\Projects\Powershell\MathLib.dll

Assemblies in the temp AppDomain after unloading

Exception calling "GetAssemblies" with "0" argument(s): "Attempted to access an unloaded AppDomain."

At C:\me\Projects\Powershell\load-in-new-appdomain.ps1:18 char:25

+ $appDomain.GetAssemblies( <<<< )

Assemblies in the default AppDomain after unloading

True   v2.0.50727     C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorlib.dll

True   v2.0.50727     C:\Windows\assembly\GAC_MSIL\Microsoft.PowerShell.ConsoleHost\1.0.0.0__31bf3856ad364e35\Micros…

True   v2.0.50727     C:\Windows\assembly\GAC_MSIL\System.Xml\2.0.0.0__b77a5c561934e089\System.Xml.dll

False  v2.0.50727     C:\me\Projects\Powershell\MathLib.dll

我们的Assembly还是被加载在了默认的AppDomain里!所以我们又回到了square 1,就是我们是无法卸载默认的这个AppDomain的,除非我们关掉Powershell的窗口。

所以最终我没能很好地解决每次重新编译都要关掉Powershell才能copy新的assembly的问题。如果你找到了一个很好的解决方法的话,请不吝赐教。

=========================================================================================================================

后记:写完这篇文章后,我才偶尔看到以下的一篇blog,原来说的是和我同样的意思,呵呵:

Why AppDomains are not a Magic Bullet

 

Posted by on 2009 年 10 月 09 日 in 未分类

6 Comments

嵌入并动态加载二进制资源及一个bug的解决

这次做的一个project需要对一个XML文件进行解析,为了保证文件的格式是正确的,于是在进行解析(parsing)之前需要先用schema验证一下,大致的代码如下(C++):

   1: try

   2: {

   3:     HRESULT hr;

   4:     

   5:     hr = pProductListXml.CreateInstance(__uuidof(MSXML2::DOMDocument60), NULL, CLSCTX_INPROC_SERVER);

   6:     if (FAILED(hr))

   7:         return hr;

   8:     

   9:     MSXML2::IXMLDOMDocument2Ptr pSchemaDoc;

  10:     hr = pSchemaDoc.CreateInstance(__uuidof(MSXML2::DOMDocument60), NULL, CLSCTX_INPROC_SERVER);

  11:     if (FAILED(hr))

  12:         return hr;

  13:     

  14:     // TODO: initialize the schema doc

  15:  

  16:     MSXML2::IXMLDOMSchemaCollection2Ptr pSchemaColl;

  17:     hr = pSchemaColl.CreateInstance(__uuidof(MSXML2::XMLSchemaCache60), NULL, CLSCTX_INPROC_SERVER);

  18:     if (FAILED(hr))

  19:         return hr;

  20:  

  21:     hr = pSchemaColl->add(L"", pSchemaDoc.GetInterfacePtr());

  22:     if (FAILED(hr))

  23:         return hr;

  24:  

  25:     pProductListXml->schemas = pSchemaColl.GetInterfacePtr();

  26:     pProductListXml->async = VARIANT_FALSE;

  27:     pProductListXml->validateOnParse = VARIANT_FALSE;

  28:     pProductListXml->resolveExternals = VARIANT_FALSE;

  29:  

  30:     if (VARIANT_TRUE != pProductListXml->load(productListFile))

  31:         return HRESULT_FROM_WIN32(ERROR_XML_PARSE_ERROR);

  32:  

  33:     MSXML2::IXMLDOMParseErrorPtr pError = pProductListXml->validate();

  34:  

  35:     if (S_OK != pError->errorCode)

  36:         return HRESULT_FROM_WIN32(ERROR_XML_PARSE_ERROR);

  37: }

  38: catch (_com_error e)

  39: {

  40:     return e.Error();

  41: }

看起来有点繁复,主要是因为是用C++写的关系。进行schema验证的也就是:

MSXML2::IXMLDOMParseErrorPtr pError = pProductListXml->validate();

这一句。

在处理如何初始化schema文件的时候,遇到了点问题。因为schema文件本质上也是一个XML文件,所以我打算将schema文件当做一个二进制资源加入到工程中,然后在运行时将其作为一个字符串直接从内存中加载进来,然后调用pSchemaDoc->loadXML就可以了。所以上面的

  14:     // TODO: initialize the schema doc

 

展开实现后就是这样的

   1: _bstr_t schemaXml = GetProductsSchemaXml();

   2:  

   3: if (_bstr_t(L"") == schemaXml || VARIANT_TRUE != pSchemaDoc->loadXML(schemaXml))

   4: {

   5:     return HRESULT_FROM_WIN32(ERROR_XML_PARSE_ERROR);

   6: }

没问题吧?

最后就是如何实现GetProductsSchemaXml()了,这个帮助函数的代码如下:

   1: _bstr_t GetProductsSchemaXml()

   2: {

   3:     HMODULE hDll = ::GetModuleHandle(NAME_OF_THIS_DLL);

   4:     if (NULL == hDll)

   5:         return _bstr_t(L"");

   6:  

   7:     HRSRC  hResource = ::FindResource(hDll, MAKEINTRESOURCE(IDR_PRODUCTS_SCHEMA), L"BIN");

   8:     if (NULL == hResource)

   9:         return _bstr_t(L"");

  10:  

  11:     HGLOBAL hSchema = ::LoadResource(hDll, hResource);

  12:     if (NULL == hSchema)

  13:         return _bstr_t(L"");

  14:  

  15:     WCHAR* pSchemaXml = (WCHAR*)::LockResource(hSchema);

  16:     if (NULL == pSchemaXml)

  17:         return _bstr_t(L"");

  18:  

  19:     DWORD size = ::SizeofResource(hDll, hResource);

  20:  

  21:     return _bstr_t(CStringW(pSchemaXml, size));

  22: }

嗯,思路是很简单的,取得带有该二进制资源的DLL的句柄,然后从DLL里加载资源,最后将该资源作为一个字符串返回。

XML schema就是一个xsd文件,我们可以通过资源编辑器来将它嵌入到编译成的DLL中:

image

请注意我把资源的类型定名为"BIN”,这个其实是无所谓的,你爱叫什么都可以,只是记得这里的类型要和FindResource的第三个参数一致(参看上面的代码:::FindResource(.., .., L”BIN”) )。

另外请注意因为我的代码全部是Unicode的(仔细的朋友应该注意到了诸如:L”…” 以及 "WCHAR*” 这类明显的Unicode代码的特征),所以products.xsd这个文件也是以Unicode格式存盘的:

image

好了,到此为止工作基本上就完成了。

———————————————————————— 自以为工作已经结束的分割线 —————————————————————————

但是,在实际测试的时候,pSchemaDoc->loadXML却一直返回VARIANT_FALSE这个失败的结果。怎么回事呢。

开动debugger来检视pSchemaXml(或者hResource,或者hSchema,这三者在这里其实都指向同一个内存地址,当然从语义上它们是不同的)指向的内存区,觉得内容看起来是对的呀:

 

image

调试了很久,一直到打开内存观察器(从菜单的Debug->Windows->Memory那里打开)我才意识到问题所在:

image

大家注意到红色框中的内容了么?也还记得刚才我说因为程序是Unicode的,所以我把products.xsd也以Unicode编码存盘的么?这就是问题所在了。因为UTF-16编码(UTF-16是Unicode的一种)文件的开头都有这个两字节的文件头(2-byte header),主要是用来区分UTF-16和UTF-8的,但就是这个“自动”多加出来的两个字节,使得pSchemaDoc->loadXML无法正确理解字符串(因为完全无法理解开头的FFFE这两个字节),所以当然就失败了。

另外顺便说一句:FFFE这个2 byte header也叫Byte Order Mark,也就是用来标记文件是low-endian的还是big-endian的。FFFE指的是low-endian,而FEFF指的是big-endian,有兴趣的同学可以试试在notepad里制定用Unicode big endian来存盘,然后再用任何十六进制编辑器看看开头的两个字节为何。

事情到这里也有了解决方案了,我只要在实际取得pSchemaXml的时候在LockResource返回的内存地址上再加2个bytes就行了。

可是,最后我发现其实还有更简单的方法,因为各种的混乱,我自己疏忽了。

事实上,你是可以用一个单字节的字符串(也就是ANSI编码的字符串)来初始化一个CStringW的对象的,所以根本就没有必要特意将products.xsd存为Unicode,所以最后我直接将products.xsd以ANSI编码存盘,然后GetProdutsSchemaXml这么写就可以了:

   1: _bstr_t GetProductsSchemaXml()

   2: {

   3:     HMODULE hDll = ::GetModuleHandle(NAME_OF_THIS_DLL);

   4:     if (NULL == hDll)

   5:         return _bstr_t(L"");

   6:  

   7:     HRSRC  hResource = ::FindResource(hDll, MAKEINTRESOURCE(IDR_PRODUCTS_SCHEMA), L"BIN");

   8:     if (NULL == hResource)

   9:         return _bstr_t(L"");

  10:  

  11:     HGLOBAL hSchema = ::LoadResource(hDll, hResource);

  12:     if (NULL == hSchema)

  13:         return _bstr_t(L"");

  14:  

  15:     // the products.xsd resource is stored in ANSI encoding, so we cast the pointer to char* instead of WCHAR*

  16:     char* pSchemaXml = (char*)::LockResource(hSchema);

  17:     if (NULL == pSchemaXml)

  18:         return _bstr_t(L"");

  19:  

  20:     DWORD size = ::SizeofResource(hDll, hResource);

  21:  

  22:     return _bstr_t(CStringW(pSchemaXml, size));

  23: }

 

总之不算什么了不起的软件技巧啦,只是所有这些小东西加起来可能还有点意思,因此写下来和大家分享。

 

Posted by on 2009 年 09 月 09 日 in 未分类

Leave a comment

为什么要goto? 为什么不要goto?

几日前在Cafe午餐的时候,大家聊起一些在Windows操作系统源代码库中曾经看到过的一些趣闻逸事,比如那个著名的“because Exchange is a moron”(正好这天公司的Exchange服务器巨慢,所以大家更是大发一笑)的注释。这其中有人提到Windows代码中大量使用goto语句的这个事,这让我想起这样一个有趣的问题:

在程序代码中,我们为什么使用goto,或者,我们为什么不该使用goto呢?

我曾经不止一次地听某某义正言辞地向我宣传goto是邪恶的,但如果我追问这么说的理由为何时,通常的答案都是模模糊糊的人云亦云之类的回答。大部分的理由都会指出goto破坏了程序的可读性和可维护性,如果代码里到处都是goto来goto去,到最后谁都很难搞清程序goto到哪一个地方了。

这看似颇有道理的说辞其实充满了迂腐的书生气。稍微有点常识的程序员,难道真会如此到处使用goto么?显然不会。如果说真的有那么一位程序员是到处在用goto把他的程序逻辑拼接起来的话,那我想他不是天才(汇编写太多了,到处都要自己跳转)就是无知(完全无法结构化自己的算法思路)。而软件开发作为一个工程行业经过这么多年的发展,现实中已经很少会真的有这种滥用goto的现象了。这当然也要感谢于那些关于goto邪恶性的大力宣传,大家上procedural programming第一课开始,就被反复灌输了“不要用goto,不要用goto”的观念。

那为什么Windows操作系统代码中大量使用了goto?是不是微软总部都雇佣了些烂人,大家都在混饭吃?还是说对于goto的使用是其实很有选择性的?而从当年goto的大量出现到今天这个关键词在使用C#或Java写就的程序中几乎绝迹,这一切,其实都是有其历史背景和含义的?

要回答这些问题,我们首先讨论一下goto在Windows操作系统源码中的使用。如果仔细观察一下的话,你会发现goto的使用其实都是在一种很特定的场合,那就是:系统资源的回收和释放。这里,系统资源可能是一块字符串内存,可能是某个内核对象(比如event或mutex)的句柄(handle),也可能是更复杂一些的数据结构。所以,goto出现的代码段,通常有这样的结构:

void Func()
{
  ...
  Magic::Initialize();
  BSTR someString = ::SysAllocString(L"Some random string");
 
  hr = CallSomeAPI();
  if (FAILED(hr))
    goto EXIT;
  ...
 
  hr = CallSomeOtherAPI();
  if (FAILED(hr))
    goto EXIT;
  
  ...
 
EXIT:
  Magic::Uninitialize();
  ::SysFreeString(someString);
  ...
}

如此便不难理解为什么goto在这种特定情况下可以简化代码编写的结构,使之更清晰易懂了。试想如果不试用goto,我们的代码就会变成:

HRESULT Func()
{  
  ...  
  Magic::Initialize();  
  BSTR someString = ::SysAllocString(L"Some random string");  
  hr = CallSomeAPI();  
  if (FAILED(hr))    
  {
    Magic::Uninitialize();  
    ::SysFreeString(someString);
    return hr;
  }
  ...   
  hr = CallSomeOtherAPI();  
  if (FAILED(hr))
  {
    Magic::Uninitialize();  
    ::SysFreeString(someString);    
    return hr;
  }    
  ... 
  return S_OK;
}

需要做回收处理的资源越多,这样的写法就显得越冗长,因此goto在这里是很自然的一种选择。

但随着面向对象的编程模式(Object-Oriented Programming Paradigm)逐渐地开始取代过程式编程(Procedural Programming),程序员开始发现有一种更好的模式(Pattern)可以用来取代goto,那就是RAII(Resource Acquisition Is Initialization)模式(“资源分配与初始化同步”)。RAII的主要思想在于两点:1. 对象在且一定在被分配或构造(construct)的时候同时被初始化,这样就避免了资源在没有被适当初始化前就被用户调用。2. 对象在被析构(destruct)的时候释放所占有的资源,这样就防止了资源泄漏。这个模式最为大家所熟知的应用可能就是C++标准库或者COM编程中随处可见的“聪明指针”(smart pointer)了。比如在上面的例子中,我们就可以定义一个MagicPtr的类,然后在类的构造函数里做Initialize,在析构函数里做Uninitialize。而对于BSTR,微软已经提供了相应的类了,那就是_bstr_t

利用goto来释放资源在procedural programming的时代是一个自然的选择,所以在Windows的源代码中你会看到goto的踪影,因为Windows在OO思想大行其道之前就已经存在多年了。但随着OOP的深入人心,遵循RAII来管理资源就成为了最自然的选择。

另一个重要的原因,就是异常处理(exception handling)概念的兴起。goto虽然可以很干净地解决过程式资源回收的问题,但却对异常这个东东没有很好的解决方法。比如上面的程序要是哪里抛出一个异常的话,那goto的部分就根本不会被执行了。而另一方面,RAII却能很好地解决这个问题,因为在对象离开定义域之前(不管是return了还是exception thrown了),析构函数都会被执行的。

其实写这篇东西的另一个目的也是想说:每一件看似简单的事情背后,如果你花一些时间去思考和研究,也许就会发现很多更深刻的意义和结果。这并不是要我们变成一个多疑的偏执狂,而是我觉得思索和提问的习惯是有益的。对于一个看似简单的道理,我们能不能提出让自己信服的佐证来,我们是否有一种直觉,告诉自己:I am wondering if there is more to it。事实上,这个世界上的偏执狂是少数,多的,是人云亦云的大众。

 

Posted by on 2009 年 08 月 03 日 in 未分类

8 Comments

Watch out, Google, here comes Bing.

18个月前我第一次听到微软准备以62%的溢价收购Yahoo的时候,我以为不是我疯了就是某些人疯了。似乎从AOL收购Time Warner后我就再也没听到过更蠢的收购议案了。我们不仅打算花44600000000的现金和股票来收购一个技术上已毫无优势,团队士气极为低落,企业文化又似乎已经跟不上时代的公司,而且会史无前例地将微软从创立以来第一次置于负债的境地,还不说收购以后对公司内部相关团队的士气影响以及我们将会浪费多少时间来整合对方庞大的工程资源等等不利的因素。

所幸的是,致远.杨同学很有骨气的拒绝了我们的offer,从而再次证明了一个古老的真理:你傻不要紧,只要你的对手更傻就可以。

而今晨的火线头条自然是微软和雅虎达成10年的搜索合作协议的消息,不过不同的是这一次,I feel so good about it.

1年半前收购雅虎,微软的项庄之意显然不是他们的搜索引擎技术。从后来发生的事来推算,Bing引擎那时已经在秘密开发中了。那么那个收购案的目的就很显然,那就是雅虎的搜索市场份额。基于搜索的在线广告这个东东是“赢家通吃”(winner takes all) 的游戏。试想,如果你是一个广告投放商,如果一个投放点能让你的广告被68%(Google的市场份额)的用户看到,而另一个投放点的观众只有8%(Bing的市场份额),你会选择哪一处呢?再进一步,如果你知道一家搜索引擎能针对68%的搜索流量来优化他们的广告显示结果和客户相关性,而另一家只能针对8%的数据来进行优化,你会选择谁呢?

所以说基于搜索的在线广告(注意,不是传统的静态广告。基于搜索的广告是根据用户搜索的关键字来显示广告的,而静态广告则一般就是投放在流量巨大的门户站点上,至于谁会看见谁会点击,只有天知道了)是一个“赢家通吃”或者“赢家制定规则”的游戏。排名靠后的参与者也还是可以参与市场定价,但相对于领头羊,他们必须承担大幅度折扣才能将自己的推销出去(简单说:Google可以要求广告投放的商家:点击一次你给我10分钱,而Bing就只能告诉同样的客户说:点击一次我只要3分钱。)

这也是为什么Google在搜索广告的市场占有率为68%,但却垄断了超过80%的营收。

所以对微软来说,将Yahoo的那20%多的用户群拿到手,确实是一种巨大的诱惑,因为这样一次性地将Bing这个选项(之前是Live Search)的吸引力提升到了另一个层次。即使从数字上看Google的市场份额并没有改变,但它实际上的营收却会收到巨大影响,因为现在它无法再以过去那样3.33倍(以上面假设的数字为例)于第二名的价格来要价客户了。

所以这次的合作协议,微软的目标并没有改变。只不过上一次,微软是急功近利地采取了一种低智的曲线救国的方法(通过并购),所幸的是杨同学发扬毫不利己专门利人的国际主义精神,毅然地让他作为创始人的ego超越了一切商业理智。

这次的协议中,微软无疑是大大的赢家。我们得到了最想要也是最重要的实质内容:流量(尤其是大量的长尾搜索流量,Google在企业级搜索上的优势还是明显的)还有用户元数据(用户的搜索历史,点击习惯,等等)。而后者对于Bing提升返回结果的相关性和进一步优化广告显示逻辑有着巨大的推动作用。Yahoo可以继续经营在线广告的投放渠道和客户关系,但这部分的内容的规模效应是很有限的。换句话说:如果微软想要参与现有的销售过程或者建立新的过程,并非难事。而Yahoo,已经和搜索引擎事业彻底告别了。

故垒西边,江山如画,这场微软和Google的旷世之战真是越来越精彩。

 

Posted by on 2009 年 07 月 30 日 in 未分类

4 Comments

Chrome OS和Android的背后

Google放出了Chrome OS的消息,于是大小媒体都像打了鸡血一样兴奋起来宣布Google正式向微软的核心阵地开始进攻了。

其实只有娱记和书虫才会这么想这么去挖掘新闻材料。思路稍微清晰一点的人都可以看透Google根本就没想搞一个真正完整的用户级操作系统。Linux搞了那么多年没搞成的东西,Google才没有兴趣也去搞一遍,就算它有这个实力有这个财力,它也知道是这件东东很可能是花了大钱还要吃不了兜着走的烫手山芋。

那为啥Google要左一个Android右一个Chrome OS地大张旗鼓呢?当然不是钱太多烧得慌,也不是时间太多闲得慌。Google多面多样的各种客户端应用背后唯一统一的最终思路就是将用户整合到它的服务平台(service back end)上来。Google希望看到一个分裂的原生平台世界(Mac, Linux, Windows, Symbian, iPhone, Android等等),越分裂越好。在一个分裂的客户端世界中,唯一统一的用户体验就是Google的服务平台。客户端的世界越分裂,就有越多的开发者不得不离开某一个原生平台而走向网络应用和网络服务。这才是Google真正想要的。它看似眼花缭乱的各种工作背后,唯一不变的主题和动力就是将越来越多的人(用户或开发者)吸引到它的服务平台(也是它的revenue generator)上来。

因此Google当然喜欢开源社区(当然,只要开源社区离他们的数据中心里的那些核心技术越远越好)。首先,开源社区最擅长的就是提供多样的解决方案,尤其在客户端方面。Google欢迎这样分化多样的选择方案。选择越多,对Google越有利。其次,开源社区还是一文不取的活雷锋,就好像Android平台,除了智能手机之外的内容几乎都是开源社区给创造的。而这次的Chrome OS也不例外。Google所要做的,就是抛出一个初步的想法或雏形,造成很大的声势,自然会有媒体给它免费宣传与关注,以后添砖加瓦的事大多就留给开源社区了。Google自己则将继续集中注意力于他们的核心竞争力上,也就是他们的服务后端。

如果说Google在向微软的核心阵地发起进攻,那也不是从Chrome OS开始的,那是从Google发布Gmail就开始了,从发布App Engine就开始了,从发布Docs就开始了。Chrome OS,只是很自然的下一个走下流水线的半成品。在新闻稿里,Google提到Chrome OS是计划在2010年的下半年发布的,那个时候,Win7都已经发布了一年了,市场上一定已经充斥着以Win7、以Ubuntu、以OS X为基础全面优化过的上网本(netbook),你还真的以为Google指望靠这个迟到了许久的产品在netbook市场上大赚一笔?

 

Posted by on 2009 年 07 月 10 日 in 未分类

9 Comments

托管代码 (managed code)和非托管代码 (native code) 的互操作性 (interoperability) – Part I

 

3月份从Windows Live转到Windows Embedded后,越来越多地需要与C/C++相依为命了。以前在C#里写点字符串处理的东西基本不用动什么脑子,现在单单是把两个字符串连起来,就要调用一个有4个参数的API

最近在做的一个项目顶层的用户界面(UI, User Interface)还是选择用C#/WinForm写了,毕竟再用MFC之类的东西,身心受不了那个摧残。不过底层真正实现功能的API还是必须要用C/C++来写(有很多客观的原因必须如此),所以一个必然需要解决的问题就是如何让用托管代码写成的UI层来调用用非托管代码写成的API层里的函数/类。也不知这样的事大家现在是否还经常需要做,不知会用COM/ATL的还有多少人,不过既然就此做了一些调查,我想还是把结果整理一下,写下来,也许对一些朋友会有用呢。

本文只讨论如何从托管代码调用非托管代码,其实反向操作(非托管代码调用托管代码)也有很多类似的技术,大家可以自己查一下资料。

基本上从托管代码调用非托管代码的函数/类型有一下几类方法:

 

1. 通过Platform Invoke (P/Invoke)

这个在托管代码需要调用Windows API的时候是很常见的,比如要调用kernel32.dll里的SetDllDirectory这个native API,只要这样做就可以了:

using System.Runtime.Interopservices
 
public MyClass
{
  [DllImport("kernel32.dll", SetLastError=true)]
  static extern bool SetDllDirectory(string lpPathName);
 
   ...
 
   public static void CallSetDllDirectory(string path)
   {
       ...
       SetDllDirectory(path);
       ...
   }
}
 
 

利用P/Invoke最大的麻烦可能就在于这个native函数的P/Invoke的签名应该如何申明。最常见的一种方法叫做google,就是你把TheAPIYouWantToCall和P/Invoke这两个关键字放在一起google一把,基本就应该找到了,第一个连接十有八九是来自www.pinvoke.net这个网站。

不过如果你要是说你不喜欢Google怎么办?Google上找不到怎么办?要是Google当掉了怎么办?要是Google因为传播色情信息被封锁了怎么办?当然你可以说:我没有Google我有Bing,那也是可行的。不过其实还有更直接的方法,在2008年1月的MSDN杂志上的CLR Inside Out的专栏里就有这么一篇文章:Marshaling between Managed and Unmanaged Code,其中介绍了P/Invoke Interop Assistant这个工具。你不但可以通过这个工具来查找绝大多数公开的Windows API的P/Invoke的签名,而且也可以用它来产生你自己写的native API的P/Invoke签名,很方便。你可以在这里下载这个工具

 

2. 通过C++/CLI wrapper class

这个方法主要是利用Managed C++来写一个封装函数/类,并以此来调用非托管的函数/类,而在另一边,用Managed C++写成的封装类的dll又可以直接被C#等托管代码来调用。

假设你有如下的C++类:

class __declspec(dllexport) NativeClass
{
public:
    static void Func(LPTCSTR);
    ...
};

你可以在Visual Studio中建立一个Visual C++的CLR空项目:

image

记得调整配置的类型:

image

然后编写如下的Managed C++类:

 
public ref class MCppClass
{
  public:
    static void Func(String^ str)
    {
        NativeClass::Func((LPTSTR)Marshal::StringToHGlobalUni(str).ToPointer());
    }
    ...
};

编译后生成的dll就可以直接被托管代码调用了。当然,关于托管代码变量和非托管代码变量之间marshaling的问题也是很头疼的,不过这个不是本文的讨论内容。

 

3. 通过CALLI instruction以及Reflection.Emit直接调用非托管代码

CALLI是Intermediate Language (IL)的一个指令。老实说我也不太清楚这个东东怎么用,只是看有的文章提到说可以如此做,但没有深入地研究一下,有兴趣的同学可以参考一下David Broman的这篇文章

 

4. 通过COM interop

这个就留待明天的Part II再讲吧,因为我还想顺带提一下COM里的一些相关内容,篇幅可能有些长,并且时间也不早了…

 

Posted by on 2009 年 07 月 01 日 in 未分类

5 Comments

Here we go, Windows Azure!

忙了大半年的project,今天终于publicly release了。

Yes, baby, it’s live from PDC 2008.  Windows Azure, the cloud computing platform from Microsoft.

Azure Services Platform的主站点已经开放:http://www.azure.com (or http://www.microsoft.com/azure ),你可以通过这个站点理解全面的关于Azure Services Platform的信息。

北美时间中午12点,Azure Developer Portal将正式开放: http://lx.azure.microsoft.com。普通用户一开始只能通过创建"Live Services: Existing APIs"来感受Azure Services Platform。其他三种最新的Azure Services App (Storage App, Hosted Services App, and Live Framework App)都必须持有特殊的invitation token才能使用。第一批开放的只有今天晚些时候发送给PDC现场的100个VIP token.

所有最新的Azure Services App中最引人注目的当然是两个Windows Azure的cloud computing application: Storage & Hosted Services. 这是下一代Microsoft自己的云计算平台,也将是将来几年甚至十几年中微软发展的方向。

我下周就要开始休假了,从2月初开始一直都是每天10多个小时的高强度工作,确实应该歇一下了。当然,也让我有更多的时间可以整理很多的技术资料以及更新这个blog。希望能通过这里给大家带来更多关于Windows Azure的信息。

Welcome to Windows Azure.

===============================================================================

Update 1: Channel 9上有两段很好的interview,分别是Manuvir Das (Director of Development in Windows Azure) 和 Steve Marx (Lead Program Manager in Windows Azure)。如果想初步了解一下Windows Azure平台,这两段是很好的入门介绍。

Fun note: 在Manuvir的interview的最后,Charles(就是主持interview的那位只闻其声不见其人的同学)说了句:Of course we cannot get Cutler, he refuses to come up and talk…

这里的"Cutler",自然指的就是Windows Kernel的终极大牛David Cutler。有兴趣了解一下David的可以搜索一下CSDN上一个著名的blog系列“心目中的编程高手”。

既然是新一代的Windows平台,里面自然是少不了D爷爷的手笔,所做的,当然也是他最擅长的OS & Virtualization…

 

 

Posted by on 2008 年 10 月 28 日 in 未分类

3 Comments

Mesh it Up!

 

周五在San Francisco举行的Web 2.0 Expo上,Microsoft正式公布了新的Live Mesh平台。Live Mesh不仅仅是一个用户平台,也更是一个开发者平台。而借此,Microsoft也第一次大声地喊出:PC不再是计算的中心了,而Services才是平台的中心。

对于用户,现在IT的趋势是个人拥有越来越多的终端:手机,PDA,笔记本电脑,台式机,XBox,iPod,等等。而另一方面,各个终端上的社会网络和信息资源又很难保持一致或进行整合。手机上有一堆号码,笔记本上有一堆会议记录,台式机上有一堆照片,XBox上有一堆网友。但如果你什么时候想在开会的时候偷个空看看哪些XBox的玩友正好在线呢,Oops,不好意思,做不到,因为事先没有把这部分内容同步过来。

而对于开发者,他们希望他们所写的应用程序不但能在常用的计算平台上运行,也希望当用户在使用移动设备或者有网络能力的娱乐设备的时候,同样能使用他们的应用程序。设想,如果你开发了一个共享照片的服务,在台式机或者笔记本上运行的好好的,但你是否也希望当你的用户手边只有一个手机的时候,他同样可以轻松地使用你的程序的mobile版本来下载他所想要的照片呢。写个用户界面是小事,难的地方是在于如何在手机平台上无缝地获取存储在cloud (cloud as in cloud computing)中的资料。而Live Mesh就是想为开发者提供解决这样问题的平台,而不是让每个人绞尽脑汁地开发如此底层又吃力不讨好的服务。

Live Mesh的核心概念在于以下几点:

1. 服务是平台的中心,而不是终端(PC也好,手机也罢,服务应该是独立于硬件的)。

2. 统一的API集。无论你的程序是运行在离线的客户端,还是作为浏览器的plug-in,还是作为一个动态网页(后台连接Live Mesh的cloud storage),还是一个连线的桌面程序,所有的模式需要使用的API应该是一致的。

3. 开放的可扩展的数据模型。

4. 弹性的应用程序模型。开发者可以写仅支持连线状态的程序,也可以写同时支持离线和连线状态的程序,也可以写仅作为浏览器插件的程序,等等。Live Mesh不强制某一种应用程序模型,一切完全由开发者决定他们想支持的用户场景(user scenario)。

 

Picture_15

 

CNET上有一些报道可以帮助有兴趣的朋友开始了解Live Mesh:

Live Meshing on the Gillmor Gang

Microsoft Live Mesh platform takes on Google, Adobe

而这里有Ori Amiga的介绍Mesh程序开发的视频:

Ori Amiga: Programming the Mesh

 

=====================================================================================

这半年经历了很多人事变动,周围的事一直不是很安定,希望以后的一段时间能安静地做些技术。关于Live Mesh我还有不少想写的,虽然对它的某些内容我还有自己的看法,但总体来说这是一个好方向。这次微软推出Live Mesh后收到的评价也颇为正面,和以往一出来什么东西都被口诛笔伐得一文不值大为异趣。可见群众的眼光还是雪亮的,大家并不针对你这个人/公司来评价你的产品,Google做得不好的要批评,Microsoft做得好的也可以表扬。做技术的本来就该如此,政治还是留给别人去搞吧。

 

Posted by on 2008 年 04 月 29 日 in 未分类

Leave a comment