Archive for 十月, 2012

Dissect ActiveX Control Safety

1、介绍

如果你曾经在网页或者ASP中使用过com对象,你可能会发现,有时候会出现这样讨厌的对话框

 

     这是因为你的控件没有被标记为安全的,对于初始化不安全或者对于脚本不安全,甚至兼而有之。你每打开一次这样的网页,这种情况就会发生一次,你怎么办?当然,这可以通过设置IE本身的安全等级为low来解决这样的问题,但是如果你要制作一个可发布的控件,你能想象到每一位用户在使用你制作的控件时都要且列抱怨这种强制行为;或者如果你是其中一个使用者,当你同样遇到这种情况之后不得不将自己的IE安全等级设置为low,与此同时你平时上网过程中那些行为不轨的控件有时也会悄然而至,你的灾难来了!作为一个聪明的程序员,你不能要求任何用户做一些不切实际或者不够安全的改变来适应你的产品,因为你知道那只会使你的控件被逐渐打入冷宫,到最后销声匿迹,那不是你想要的。我们要消灭掉这样的对话框而且还不让用户发现丝毫安全上和使用上的失望。本文就是横向探讨在C++编程环境下如何消除这些问题的。

2、原理

ActiveX控件是一种极其危险的提供功能的方法(目前正在被MS逐渐冷落),因为它是一种组建对象模型(COM)的对象,只要电脑的用户可以完成的任务,它都可以完成。比如它可以存取注册表,可以随意访问本地文件系统等等。一个网页上面的控件一般有2种不安全的状态,一种是脚本的不安全,一种是初始化的不安全。当用户将一个压缩解压缩控件指向一个远程被压缩的包含特洛伊木马的系统文件并且需要控件来解压缩这个文件时,系统安全会被打破。这个状态就是初始化的不安全。从代码的角度来讲,如果控件从IPersist派生,也就是说控件实现了永久性,那么就会触发unsafe for initializing。而在脚本程序安全执行以前,一个控件依赖于特定的系统设置,那么在允许这段代码运行之前,控件的开发人员需要提供一些必要的代码。从代码的角度来讲,如果控件从IDispatch派生,也就是说控件支持脚本,那么就会触发unsafe for scripting。

从用户下载一个ActiveX控件开始,这个控件甚至可能很容易被攻击,因为网络上任何网络程序都可以使用它,无论是出于友好的目的还是恶意的目的。因此IE浏览器(本文只探讨IE内核的浏览器)总是试图弹出一个对话框来告诉你,这个控件可能是不安全的。这几乎总是一个很好的预防网络攻击的好方法,但是对于那些我们认为总是安全的控件,我们仍然要总是接受这种IE产生的干扰,这就使人厌烦了。其实当身为程序员的你写这样的安全控件时,这样的问题是很容易解决的。

3、解决方法

目前,对这个问题的解决方法主要有几种:使用数字签名,继承IObjectSafety接口,修改注册表等。

3.1、使用数字签名

数字签名是一种使控件足够安全的方法,它通过特定的密钥来加密控件的使用,使得使用控件的对象能够根据密钥是否相符来检测控件是否足够安全。通常,拥有自己的可发布的数字签名是要Money的,本文是一篇技术文章,对此并不深入探讨,下面主要介绍代码方面的安全化。

3.2、继承IObjectSafety接口

IObjectSafety接口是在头文件”objsafe.h”中定义的接口,定义如下:

IObjectSafety : public IUnknown
{
    public:
    virtual HRESULT STDMETHODCALLTYPE GetInterfaceSafetyOptions(
    /* [in] */ REFIID riid,
    /* [out] */ DWORD *pdwSupportedOptions,
    /* [out] */ DWORD *pdwEnabledOptions) = 0;

    virtual HRESULT STDMETHODCALLTYPE SetInterfaceSafetyOptions(
    /* [in] */ REFIID riid,
    /* [in] */ DWORD dwOptionSetMask,
    /* [in] */ DWORD dwEnabledOptions) = 0;
};

其中参数意义如下:

//riid                Interface identifier for the object to be made safe.
//
//dwOptionSetMask     Options to be changed.
//
//dwEnabledOptions    Settings for the options that are to be changed. This can be one of the following values.
//                    INTERFACESAFE_FOR_UNTRUSTED_CALLER
//                        Indicates the interface identified by riid should be made safe for scripting.
//                    INTERFACESAFE_FOR_UNTRUSTED_DATA
//                        Indicates the interface identified by riid should be made safe for untrusted data during initialization.

这个接口允许容器询问控件是否安全或者改变控件的安全属性。目前IObjectSafety支持四种安全属性,但是一般我们只使用前两个:脚本安全和初始化安全。这些属性定义如下:

// Option bit definitions for IObjectSafety:
#define    INTERFACESAFE_FOR_UNTRUSTED_CALLER        0x00000001    // Caller of interface may be untrusted
#define    INTERFACESAFE_FOR_UNTRUSTED_DATA             0x00000002    // Data passed into interface may be untrusted
#define    INTERFACE_USES_DISPEX                                             0x00000004    // Object knows to use IDispatchEx
#define    INTERFACE_USES_SECURITY_MANAGER                0x00000008    // Object knows to use IInternetHostSecurityManager

3.2.1、实现方法

首先包含头文件

#include “objsafe.h”

然后是自己的控件类继承IObjectSafetyImpl

class YourClass :
    
    public IObjectSafetyImpl<YourClass, INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA>
{

其中INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA 代表默认这个控件是脚本安全而且初始化安全的。然后在com接口表中添加接口名

BEGIN_COM_MAP(CCGrid)
    
    COM_INTERFACE_ENTRY(IObjectSafety)
END_COM_MAP()

OK,大功告成。这也是一种很简单的方法。

3.3、修改注册表

修改注册表项使控件支持安全类别的原理,归根到底其实就是下面的第三个方法,另外两个方法都是对这个方法的外围包装和更加安全的处理。修改注册表在不同的工程中有着不同的表现:

3.3.1、用于ATL属性工程的com接口

因为VC的属性工程已经加入了安全控件的注册表支持,并且直接加入了关键字implements_category,因此我们仅仅象下面这样既可完成安全属性设置:

[
    coclass/progid/vi_prgid,            //必须至少有其中一个
    
    implements_category(“CATID_SafeForScripting”),
    implements_category(“CATID_SafeForInitializing”),
    
]

不过要注意的是,对于类似下面的代码(往往是非属性工程的)是不能添加的:

[
    uuid(),
    helpstring(“”)
]
coclass YourCtrl
{
    [default] interface ,
        [default,source] dispinterface 
}

因为这种coclass是IDL的属性,而IDL本身并没有安全功能。

而上面的那种coclass是VC的属性,MS提供了对安全控件的支持。

3.3.2、主要用于非属性工程的通用方法

在类声明文件中加入头文件

#include “objsafe.h”

在类声明中添加如下代码:

   BEGIN_CATEGORY_MAP( YourCtrlClassName )
       IMPLEMENTED_CATEGORY( CATID_SafeForScripting )
       IMPLEMENTED_CATEGORY( CATID_SafeForInitializing )
   END_CATEGORY_MAP()

即可使其支持safety属性,当然你也可以选择性的只支持一种安全属性。

3.3.3、主要用于MFC ActiveX Control的通用方法

在$project.cpp文件中添加以下代码:

#include “comcat.h”
#include “Objsafe.h”

// 控件的CLSID,注册表用(一定要是实际使用的控件的)
const GUID CDECL CLSID_SafeItem ={ 0x7AE7497B, 0xCAD8, 0x4E66, { 0xA5,0x8B,0xDD,0xE9,0xBC,0xAF,0x6B,0x61 } };

// 版本控制
const WORD _wVerMajor = 1;

// 次版本号
const WORD _wVerMinor = 0;

//////////////////////////////////////////////////////////////////////
// 创建组件种类
HRESULT CreateComponentCategory(CATID catid, WCHAR* catDescription)
{
    ICatRegister* pcr = NULL ;
    HRESULT hr = S_OK ;

    hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr);
    if (FAILED(hr))
        return hr;

    // Make sure the HKCR\Component Categories\{..catid}
    // key is registered.
    CATEGORYINFO catinfo;
    catinfo.catid = catid;
    catinfo.lcid = 0x0409 ; // english

    // Make sure the provided description is not too long.
    // Only copy the first 127 characters if it is.
    int len = wcslen(catDescription);
    if (len>127)
        len = 127;
    wcsncpy(catinfo.szDescription, catDescription, len);

    // Make sure the description is null terminated.
    catinfo.szDescription[len] = ‘\0′;

    hr = pcr->RegisterCategories(1, &catinfo);
    pcr->Release();

    return hr;
}

// 注册组件种类
HRESULT RegisterCLSIDInCategory(REFCLSID clsid, CATID catid)
{

    // Register your component categories information.
    ICatRegister* pcr = NULL ;
    HRESULT hr = S_OK ;
    hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr);
    if (SUCCEEDED(hr))
    {

        // Register this category as being implemented by the class.
        CATID rgcatid[1] ;
        rgcatid[0] = catid;
        hr = pcr->RegisterClassImplCategories(clsid, 1, rgcatid);
    }
    if (pcr != NULL)
        pcr->Release();
    return hr;
}

// 卸载组件种类
HRESULT UnRegisterCLSIDInCategory(REFCLSID clsid, CATID catid)
{
    ICatRegister* pcr = NULL ;
    HRESULT hr = S_OK ;

    hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr);
    if (SUCCEEDED(hr))
    {

        // Unregister this category as being implemented by the class.
        CATID rgcatid[1] ;
        rgcatid[0] = catid;
        hr = pcr->UnRegisterClassImplCategories(clsid, 1, rgcatid);
    }

    if (pcr != NULL)
        pcr->Release();

    return hr;
}

修改以下函数:

// DllRegisterServer – Adds entries to the system registry
STDAPI DllRegisterServer(void)
{
    HRESULT hr;

    AFX_MANAGE_STATE(_afxModuleAddrThis);

    if (!AfxOleRegisterTypeLib(AfxGetInstanceHandle(), _tlid))
        return ResultFromScode(SELFREG_E_TYPELIB);

    if (!COleObjectFactoryEx::UpdateRegistryAll(TRUE))
        return ResultFromScode(SELFREG_E_CLASS);

    // 标记控件初始化安全.
    // 创建初始化安全组件种类
    hr = CreateComponentCategory(CATID_SafeForInitializing, L”Controls safely initializable from persistent data!”);
    if (FAILED(hr))
        return hr;

    // 注册初始化安全
    hr = RegisterCLSIDInCategory(CLSID_SafeItem, CATID_SafeForInitializing);
    if (FAILED(hr))
        return hr;

    // 标记控件脚本安全
    // 创建脚本安全组件种类
    hr = CreateComponentCategory(CATID_SafeForScripting, L”Controls safely scriptable!”);
    if (FAILED(hr))
        return hr;

    // 注册脚本安全组件种类
    hr = RegisterCLSIDInCategory(CLSID_SafeItem, CATID_SafeForScripting);
    if (FAILED(hr))
        return hr;

    return NOERROR;
}

//////////////////////////////////////////////////////////////////
// DllUnregisterServer – Removes entries from the system registry
STDAPI DllUnregisterServer(void)
{
    HRESULT hr;
    AFX_MANAGE_STATE(_afxModuleAddrThis);

    if (!AfxOleUnregisterTypeLib(_tlid, _wVerMajor, _wVerMinor))
        return ResultFromScode(SELFREG_E_TYPELIB);

    if (!COleObjectFactoryEx::UpdateRegistryAll(FALSE))
        return ResultFromScode(SELFREG_E_CLASS);

    // 删除控件初始化安全入口.
    hr=UnRegisterCLSIDInCategory(CLSID_SafeItem, CATID_SafeForInitializing);
    if (FAILED(hr))
        return hr;

    // 删除控件脚本安全入口
    hr=UnRegisterCLSIDInCategory(CLSID_SafeItem, CATID_SafeForScripting);
    if (FAILED(hr))
        return hr;

    //////////////////////////
    return NOERROR;
}

至此对工程的修改完成了,现在控件就可以在自注册时就注册为安全控件了!

3.3.4、手动修改注册表

我们可以自己手动为控件在注册表中添加安全支持,实际上我们所需要的两个项(Key)就是这样的形式:

\HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\<GUID of control class>\Implemented Categories\<GUID of category>

ActiveX SDK头文件ObjSafe.H 已经为类别CATID_SafeForInitializing和CATID_SafeForScripting定义了GUID值。因此对于特定的工程,如果控件类的GUID是{20048BB3-DB68-11CF-9CAF-00AA006CB425},那么我们就只需要添加两个注册表项:

   REGEDIT4

   [HKEY_CLASSES_ROOT\CLSID\{20048BB3-DB68-11CF-9CAF-00AA006CB425}\Implemented Categories]

   [HKEY_CLASSES_ROOT\CLSID\{20048BB3-DB68-11CF-9CAF-00AA006CB425}\Implemented Categories\{7DD95801-9882-11CF-9FA9-00AA006C42C4}]

   [HKEY_CLASSES_ROOT\CLSID\{20048BB3-DB68-11CF-9CAF-00AA006CB425}\Implemented Categories\{7DD95802-9882-11CF-9FA9-00AA006C42C4}]

将以上代码拷贝到一个*.reg中去,保存后执行就可以将GUID为{20048BB3-DB68-11CF-9CAF-00AA006CB425}的控件类注册为安全控件。有时候注册表中不一定有那两个类别,因此我们还需要自己来描述这两个重要的类别:

REGEDIT4

[HKEY_CLASSES_ROOT\Component Categories]

[HKEY_CLASSES_ROOT\Component Categories\{7DD95801-9882-11CF-9FA9-00AA006C42C4}]
“409”=”Controls that are safely scriptable”

[HKEY_CLASSES_ROOT\Component Categories\{7DD95802-9882-11CF-9FA9-00AA006C42C4}]
“409”=”Controls safely initializable from persistent data”

同样,将以上代码拷贝到*.reg文件中执行后,就可以生成这两个类别。以后就可以使用这两个类别来注册安全控件了。注意:除非你没有别的办法,千万不要使用这个方法,也不要使用这个方法来标记实际上并不安全的控件为安全控件。

那么方法3和方法1和2有什么不同的呢?其实他们最大的不同就是方法1和2实际上都加入各种判断代码在修改注册表之前和之后,实际上也防止了不安全控件注册为安全控件的行为。因此除非万不得已,不要用方法3。就算是方法1和方法2,也要在确定自己控件足够安全的情况下才使用。

3.4、另一个问题

在施行了这些方法后,那个讨厌的对话框没了,但是对于window XP SP2及以上版本来说,用默认安全级别的IE打开任何包括了控件或者脚本的网页时,在运行代码前都会弹出一个这样的条框:

 

 

 

这也是为了不断提醒用户这样的操作可能会有安全上的危险,可是这样也会影响用户的操作,对于一个频繁访问的某一个页面来说,这个横条和对话框都是不易忍受的,如果你正在编写这样包括脚本和控件的页面,那么这里有一个窍门来解决这样的问题。请把下面这句加在网页文件源代码的<html>和<head>之间:

<!– saved from url=(0017)http://localhost/ –>

在你做网页时,如果网页需要运行ActiveX或脚本,并且他们位于客户端以外的地方,那么可以添加这个注释语句,IE当然不会不理他, IE会按照他指出的URL去找脚本的位置。 这句话的作用是让Internet Explorer 使用 Internet 区域的安全设置,而不是本地计算机区域的设置。其中0017代表后面网址的字符个数,后面的网址字符串要指向注册了这个组件或脚本的地方。如果是象上面那样,就表明是在自己的电脑上。把这句话删除,有些脚本就不执行了。

3.5、什么是足够安全的控件

我怎么知道我的控件是不是足够安全呢?请好好看看这一节。一旦标注出现在控件上而非网页上时,那些标注为安全的控件就一定要在所有可能的网页上是安全的。因此一个控件被标注为安全的就表示它能够保护自己并与网页制作者可能在初始化或脚本过程中做的不愉快的东西隔离。实际上很容易检验当被用于网页时一个控件是否安全的:当你标注你的控件为初始化安全时,相当于你在发誓无论用什么样的值来初始化你的控件,都不可能发生伤害用户的系统或者威胁到用户的安全;当你标注你的控件为脚本安全时,那就是说你有把握说无论控件的函数和属性如何被网页的脚本操作,控件本身都不会做出危及用户安全的行为。换句话说,它必须接受任何脚本中任何顺序的函数和/或属性调用而不会发生危险。 在设计控件过程中,下面是一些表明此控件是安全的条款:

  • 不要操作用户的文件系统;
  • 不要操作注册表(除非是注册和注销它本身);
  • 不要数组越界或其他的内存错误;
  • 验证(或更正)所有的输入,包括初始化,方法的参数和属性的Set函数;
  • 不要滥用与用户有关的活是用户提供的数据;
  • 做大量的测试。

这份表还远没有结束,但这些至少都是必要的。还有一点很重要,就是千万不要把本来实际上不安全的控件注册为安全的,尽管这很诱人(比如说你没有控件的源代码)。无论控件做什么事情的,一旦控件被标注为安全的,那么所有的网页都会省略对这个控件的安全检测。到目前为止,还没有方法能把一个控件标注为仅对特定网页是安全的。标注不安全控件为安全空间的一个简单安全的选择就是写一个包含了不安全控件的新安全控件。只要确保新安全控件的初始化,方法和属性都是安全的就行了。

4、总结

控件的安全问题可以通过各种各样的方法来巧妙的解决,但是对于一个负责的程序员来说,一定要确保控件本身是绝对安全的。一个欲发布的控件往往是要有用户使用的,如果本身不安全的控件被当成安全的控件流传出去,那就是恶意行为了。为了对用户负责,也为了对自己负责,程序员一定要再三检查自己的控件是否足够安全,而后再决定是否发布及以何种方式发布。

Yaf框架是一款用PHP扩展编写的php框架。其最原始版本是百度内的AP框架,由鸟哥编写并开源为Yaf。由于项目中要用到Ap框架,所以参考Ap文档和1.0.0版本源码,做了一个大致的了解。

按照、配置、目录结构等都可以参考框架文档,直接略过。以下将从一次路由分发入手,首先了解其路由过程。

一个典型的基于Ap框架的web项目入口如下,即指定配置文件位置,并调用bootstrap()和run()两个方法。

define(‘APPLICATION_PATH’, dirname(dirname(__FILE__)));
$application = new Ap_Application( APPLICATION_PATH . “/conf/app.ini”);
$application->bootstrap()->run();

__construct、bootstrap、run三个方法的流程图如下(可点击看大图)。

从__construct结构体看起,虽然调用的是__construct,但是扩展内部利用了zend static池保存了第一次初始化后的实例,所以,若发现该实例已存在,则说明重复初始化,则直接返回false。从而保证了ap_appliciation的单例。

然后根据传入的第二个参数(例子中省略了,可以是类似dev/qa/product的字符串),或者从php.ini的ap.environ配置中获取当前所需的ini配置节。利用该特性,可以做到配置文件自动识别开发、测试、预上线、生产环境等,从而可以将配置文件统一纳入版本管理,避免人工干预,提高自动化程度。

随后是一系列的初始化操作,初始config、request、dispatcher、loader,并建立dispatcher.request和application.request,application.dispatcher,application.config三个指针关系,这就对应Ap_Dispatcher::getRequest、Ap_Application::getDispatcher和Ap_Application::getConfig三个方法。

后续做完一些属性赋值之后,将实例保存在zend static池里,对应第一步的单例判断。完成__construct方法后,没有进行实际的操作,但是大部分实例化工作已完成。

后续通过bootstrap()方法的调用,完成一系列应用层初始化工作。该方法很简单,即根据app.ini中的application.Bootstrap路径配置找到该类,并依次调用其中以_init开头的public方法。在这里可以做很多全局自定义的工作,比如设置router协议(不想用默认的Ap_Router_Static的话),初始化数据库配置等。

第三步进入到run()方法。首先确保不要重复run,随后代码主体都是对ap_dispatcher_dispatch()方法的封装。

在ap_dispatcher_dispatch()方法中,首先初始化request对象、response对象、plugins数组。

随后若该request还未被路由,则调用ap_dispatcher_route方法,根据默认的Ap_Router_Static或者配置的一系列路由协议,为request的request_uri找到module、controller、action字符串。当然,在该方法前后,会触发hook,即plugins数组里的routestartup和routeshutdown方法(可能为空)。之后,也会将request的已路由标志位设置为true。这里,之所以需要判断已路由标志位,是因为该ap_dispatcher_dispatch()方法还会被Ap_Dispatcher::dispatch()方法调用,虽然帮助文档里说,一般不需要用户手工触发。

不论之前是否被路由,完成上一步后都调用ap_dispatcher_fix_default()方法处理router解析到的module、controller、action字符串,做默认赋值、大小写转换。

然后获取view引擎(默认就是html,可以更改为smarty等)。之后开始真正的分发操作,只要未达php.ini中的ap.forward_limit上限且action方法又调用Yaf_Controller_Abstract::forward()方法产生新的action,就循环通过ap_dispatcher_handler()方法调用action。当然,在循环开始和完成时,都会触发hook,即plugins数组里的LOOPSTARTUP和LOOPSHUTDOWN方法(可能为空)。在ap_dispatcher_handler()方法前后,触发plugins数组里的PREDISPATCH和POSTDISPATCH方法(可能为空)。之后检查如果依然有未处理的forward请求,但是已达php.ini中的ap.forward_limit上限,则报错退出。

到这里,大部分路由分发工作已完成了,后面就检查_return_response标示位,若设置为true,则产生响应以后, 不自动输出给客户端, 而是返回给调用者. 可以通过Ap_Dispatcher::returnResponse来切换开关状态。所以,若该值为false,则需要发送响应,并设置response对象的body体为空。否则直接返回response对象。

平心而论,这些功能也都是每个php框架实现过的,甚至浅薄如我也写过一个公司内应用的小框架完成这些路由分发的功能。那么它胜在哪里呢?想法,性能,细节。正是这些使yaf与zend、cake等框架有大不同。

TBC。

by MAGENTO程序员 on 2012 年 10 月 12 日

QPS每秒查询率(Query Per Second)
每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准,在因特网上,作为域名系统服务器的机器的性能经常用每秒查询率来衡量。

术语说明:
QPS = req/sec = 请求数/秒

QPS计算PV和机器的方式】

QPS统计方式 [一般使用 http_load 进行统计]
QPS = 总请求数 / ( 进程总数 * 请求时间 )
QPS: 单个进程每秒请求服务器的成功次数

单台服务器每天PV计算
公式1:每天总PV = QPS * 3600 * 6
公式2:每天总PV = QPS * 3600 * 8

服务器计算
服务器数量 = ceil( 每天总PV / 单台服务器每天总PV )

【峰值QPS和机器计算公式】

原理:每天80%的访问集中在20%的时间里,这20%时间叫做峰值时间
公式:( 总PV数 * 80% ) / ( 每天秒数 * 20% ) = 峰值时间每秒请求数(QPS)
机器:峰值时间每秒QPS / 单台机器的QPS = 需要的机器

问:每天300w PV 的在单台机器上,这台机器需要多少QPS?
答:( 3000000 * 0.8 ) / (86400 * 0.2 ) = 139 (QPS)

问:如果一台机器的QPS是58,需要几台机器来支持?
答:139 / 58 = 3

我们的项目中准备尝试CI和敏捷,使用svn作为版本管理工具。在开发方式上有主干开发和分支开发两种方式,我们设想的方式是:

1. 项目启动时,建立svn代码路径,今后(包括各种升级)都是在主干路径上进行

2. 当完成一个迭代的开发与测试后,建立新分支(branch),qa在该分支上进行回归测试,确保没问题之后,使用该分支上线。除非有bug,否则不允许修改该分支代码,并且修改后的代码必须立即merge回主干以避免冲突。

在这种主干开发方式中,关键点我认为是,开辟新分支前需要进行完整的UT和自动化测试,保证代码的较高质量。否则一旦开辟新分支测试后,发现bug过多,将导致rd既在主干进行业务功能开发,又在分支上修bug,还得将分支代码merge回主干,反而会中断正常工作、增加工作量。

以下摘录这两种开发方式的定义:http://www.quanlei.com/2012/03/3075.html

1、主干开发

在这种模式下,开发人员几乎总是签入代码到主干,而使用分支的情况极少。主干开发有如下三个好处:

  • 确保所有的代码被持续集成
  • 确保开发人员及时获得他人的修改
  • 避免项目后期的“合并地狱”和“集成地狱”

缺点:

每次向主干签入并不都是可发布状态

2、按发布创建分支

在这种模式下,在某个版本即将发布之前,创建一个分支,该发布版本的测试和验证全部在该分支上进行,而最新的开发工作仍旧在主干上进行。要遵循如下规则:

  • 一直在主干上开发新功能
  • 当待发布版本的所有功能都完成了,且希望继续开发新功能时才创建一个分支
  • 在分支上只允许提交那些修复严重缺陷的代码,并且这些修改必须立即合并回主干
  • 当执行实际的发布时,这个分支可以选择性的打一个标签

除了这种主干开发-分支上线的模式外,还有多分支的开发模式,像http://www.uml.org.cn/softwareprocess/201002094.asp中,就提倡使用三分支,即开发分支、测试分支、发布分支,但是这样在多个分支间的merge会较为频繁。

function wrapper(){
      do{
          if (1){
              echo "first, quit\n";
  
              break;
          }
  
          if (1){
              echo "second, quit\n";
  
              // quit
          }
      } while(0);
  }
  wrapper();

最近有一个项目的重构,由于项目较大,将会采用迭代上线的方式。由于原项目极为陈旧,url和文件组织形式不太规范,故重构后会对此进行调整,但需要保持对原有url的兼容。新版本使用yaf框架,由于项目不需要作SEO,故使用其yaf_route_simple路由协议,即通过url中的module、controller、action参数进行路由解析。

具体路由过程包括以下3步:

  1. 将已重构页面的流量切换到新服务器上。
  2. 进行url的rewrite。
  3. 根据Yaf框架进行路由了。

如上图所示,第一步利用旧服务器上已有的分流器软件,将匹配某些url规则的请求转发到新服务器,完成host的映射(不至此url映射)。其实这里也可以利用apache的rewrite和load balance功能或者直接使用 mod_proxy_balancer模块,这样,甚至可以直接将第二步的url映射合并到这里,从而节省掉第二步。这里rewriterule的P flag意味着内部proxy,即apache充当代理的角色,请求配置的目标url,会触发mod_proxy模块。官方文档上给出了安全和性能方面的警告。个人感觉,稳定性和性能比较合适的还是nginx,其最初的应用场景大多是作为代理服务器,且具备url rewrite的功能。

RewriteEngine on
RewriteMap lb rnd:/path/to/serverlist.txt
RewriteRule ^/(.*) http://${lb:servers}/$1 [P,L]

Security Warning

Take care when constructing the target URL of the rule, considering the security impact from allowing the client influence over the set of URLs to which your server will act as a proxy. Ensure that the scheme and hostname part of the URL is either fixed, or does not allow the client undue influence.

Performance warning

Using this flag triggers the use of mod_proxy, without handling of persistent connections. This means the performance of your proxy will be better if you set it up with ProxyPass or ProxyPassMatch

This is because this flag triggers the use of the default worker, which does not handle connection pooling.

Avoid using this flag and prefer those directives, whenever you can.

第二步,在新服务器的apache配置中添加rewrite规则,完成url的映射(这里需要分析待映射url的规则,写出合适的正则句子即可):

RewriteEngine on
#RewriteRule (.*)_(.*)\.php $1$2.php
RewriteRule ^/([^/]+)/([-_a-zA-Z0-9]+)\.php /dispatch.php?yafc=$1&yafa=$2 [QSA,L]

最后控制权交到应用层,在Yaf框架中,需要将路由协议由默认的static更改为simple的。可以在Bootstrap.php中添加方法:

public function _initRoute(Yaf_Dispatcher $dispatcher) {
$route = new Yaf_Route_Simple(“yafm”, “yafc”, “yafa”);
$router = Yaf_Dispatcher::getInstance()->getRouter();
$router->addRoute(“name”, $route);
}

至此,即完成了整个url兼容过程,当然这里还可以更多的考虑到性能和安全问题,以及与具体项目的结合度。

 

 

在代码编写过程中,我们强调分层的概念,于是DAO或者model层应运而生。但是这些代码之间相似度其实很高,有没有办法用更少的代码实现?或者自动生成可读、可维护的代码呢?

我的初始想法是,写个脚本按需生成代码。之后恰巧读到DSL的一些相关文章,虽然针对我们当前的场景而言,没有使用DSL的必要性,但是猜想每个程序员心里可能都有一个创造新语言的梦想吧?发明一种类似C、Java的流行性语言难度和推广难度都很大,但是在某种应用场景内,发明一种恰恰适合的DSL还是有一丝可能性的。

1. http://www.infoq.com/cn/articles/External-DSL-Vaughn-Vernon  开发复杂的外部DSL

2. http://www.infoq.com/cn/articles/architecture-as-language-a-story 将架构作为语言:一个故事

3. http://en.wikipedia.org/wiki/Domain-specific_language wiki

 

构造一门语言,常用的工具:

lex 词法分析器

bison 语法分析器

PHP有3种mysql访问API:mysql、mysqli、pdo。泛泛而言,mysql是最古老的访问方式,只提供面向过程的函数,对安全性支持一般,目前不建议使用了。mysqli和pdo都是官方建议的方式,稳定性、安全性都较好,且都提供面向对象的方式,其中mysqli也支持面向过程方式。性能3者差别不大,而且对于一般业务来说,这三种API本身的损耗都可以忽略不计。

那么,为什么官方会建议用mysqli和pdo,而逐步淘汰mysql呢?

对于这三者,官方给出了功能方面的比较:

ext/mysqli PDO_MySQL ext/mysql
PHP version introduced 5.0 5.1 2.0
Included with PHP 5.x Yes Yes Yes
Development status Active Active Maintenance only
Lifecycle Active Active Long term deprecation announced
Recommended for new projects Yes Yes No
OOP Interface Yes Yes No
Procedural Interface Yes No Yes
API supports non-blocking, asynchronous queries with mysqlnd Yes No No
Persistent Connections Yes Yes Yes
API supports Charsets Yes Yes Yes
API supports server-side Prepared Statements Yes Yes No
API supports client-side Prepared Statements No Yes No
API supports Stored Procedures Yes Yes No
API supports Multiple Statements Yes Most No
API supports Transactions Yes Yes No
Transactions can be controlled with SQL Yes Yes Yes
Supports all MySQL 5.1+ functionality Yes Most No

其中server-side prepared statements有助于安全性和性能的提升。由于sql句子和参数分开解析,降低了sql注入的风险。对于同一模板sql、不同参数多次执行的sql,由于sql句子本身的解析和优化仅需执行一次,所以有助于性能提升。但是,单次执行sql时,反而由于这种机制略微降低性能。并且,根据wiki上的阐述,某些mysql版本还会由于不cache query结果等原因,可能会降低性能。

PDO扩展还支持client-side prepared stat,从而可以对应用层提供一致性的prepare接口,并且由于扩展本身的安全性考虑,也可以有效防止sql注入。client-side的方式相当于在扩展层中拼装sql发送给mysql server端,在单次查询时性能可能还会略好于server-side方式,但是多次查询时会略差。(这里的性能区别都可以忽略)

个人感觉上表中其他区别都是编程习惯方面的,可以无视之。

其实除了API层面的区别,PHP官方还搞了mysql的client端C库mysqlnd。之前不论是mysql、mysqli还是PDO,其实都是在PHP扩展层面对mysql提供的libmysql C库的封装,而mysql被oracle收购后,据传闻是由于License的原因,PHP引入了自己的mysql客户端mysqlnd库,并且也作为PHP扩展存在。