2012年2月1日星期三

转贴:实现了一个比nginx速度更快的HTTP服务器


首先承认这个标题标题党了:)。在上次的FreeBSD和linux的nginx静态文件性能对比测试 后,我萌发了自己动手做一个简单的Web Server来搞清楚nginx高性能背后的原理的想法。最后成功实现了一个基于epoll的简单的HTTP服务器,实现了 200,404,400,304响应,并且性能比nginx高了一点点。本文主要介绍这个HTTP服务器的原理和设计过程。

阅读了一些文章(见最后的参考阅读)后,我整理出了以下要点:

实现多并发的socket服务器有这样几个方法:

1. 多进程共享一个监听端口

bind之后使用fork()创建一份当前进程的拷贝,并启动子进程。子进程采用阻塞式accept、read、write,即这些操作会阻塞线程,直到操作完成才继续执行。缺点是进程之间通信速度慢,每个进程占用很多内存,所以并发数一般受限于进程数。

2. 多线程

类似多进程,只不过用线程代替了进程。主线程负责accept,为每个请求建立一个线程(或者使用线程池复用线程)。比多进程速度快,占用更少的内存,稳定性不及多进程。因为每个线程都有自己的堆栈空间,其占用的内存还是无法免除的,所以并发数一般受限于线程数。

一个阻塞式IO程序的流程示例图:
QQ截图20110923131031

3. 事件驱动的非阻塞IO(nonblocking I/O)

单线程,将socket设置为非阻塞模式(accept、read、write会立即返回。如果已经accept完了所有的连接,或读光了缓冲区的 数据,或者写满了缓冲区,会返回-1,而不是进入阻塞状态)。使用select或epoll等机制,同时监听多个IO操作有无事件发生。当其中的一个或多 个处于Ready状态(即:监听的socket可以accept,tcp连接可以read等)后,立即处理相应的事件,处理完后立即回到监听状态(注意这 里的监听是监听IO事件,不是监听端口)。相当于阻塞式IO编程中任意一处都可能回到主循环中继续等待,并能从等待中直接回到原处继续执行;而 accept、读、写都不再阻塞,阻塞全部移动到了一个多事件监听操作中。

一个非阻塞式IO程序的流程示例图:

QQ截图20110923131039

举例来说,如果在A连接的Read request的过程中,缓冲区数据读完了,而请求还没有结束,直接返回到主循环中监听其它事件。而这时如果发现另一个Send了一半的Response 连接B变为了可写状态,则直接处理B连接Send Response事件,从上次B连接写了一半的地方开始,继续写入数据。这样一来,虽然是单线程,但A和B同时进行,互不干扰。

因为流程更加复杂,无法依靠线程的堆栈保存每个连接处理过程中的各种状态信息,我们需要自己维护它们,这种编程方式需要更高的技巧。比方说,原先我 们可以在send_response函数中用局部变量保存发送数据的进度,而现在我们只能找一块其它的地方,为每一个连接单独保存这个值了。

nginx即使用事件驱动的非阻塞IO模式工作。

nginx支持多种事件机制:跨平台的select,Linux的poll和epoll,FreeBSD的kqueue,Solaris的/dev/poll等。在高并发的情况下,在Linux上使用epoll性能最好,或者说select的性能太差了。

事件机制分为水平触发,或译状态触发(level-triggered)和边缘触发(edge-triggered)。前者是用通过状态表示有事件发生,后者通过状态变化表 示事件发生。打个比方来说,使用状态触发的时候,只要缓冲区有数据,你就能检测到事件的存在。而使用边缘触发,你必须把缓冲区的数据全部读完之后,才能进 行下一次事件的检测,否则,因为状态一直处于可读状态,没有发生变化,你将永远收不到这个事件。显然,后者对编写程序的严谨性要求更高。

select和poll属于前者,epoll同时支持这两种模式。值得一提的是,我自己测试了一下,发现即使在20000并发的情况下,epoll使用这两种模式之前性能差异仍可以忽略不计。

另外需要注意的是,对于常规文件设置非阻塞是不起作用的

4. 此外还有异步IO,一般在Windows上使用,这里就不谈了。

另外nginx使用了Linux的sendfile函数。和传统的用户程序自己read和write不同,sendfile接收两个文件描述符,直接在内核中实现复制操作,相比read和write,可以减少内核态和用户态的切换次数,以及数据拷贝的次数。

接下来正式开始设计。我选择了非阻塞IO,epoll的边缘触发模式。先找了个比较完整的使用epoll的一个socket server例子作为参考,然后在它的基础上边修改边做实验:

https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/

这个例子比较简单,而且也没有体现出非阻塞IO编程。不过通过它我了解到了epoll的基本使用方法。为了实现并发通信,我们需要把程序“摊平”。

首先,分析我们的HTTP服务器通信过程用到的变量:

状态 Wait for reading Wait for writing 次数 变量类型 非本地变量 备注
Accept Y N n local

Read request Y N n nonlocal Read buf
Open file N N n nonlocal 文件名
Send response header N Y n nonlocal Response header buf
Read file -> Send response content N Y n*n nonlocal Read&write buf
Write pos
fd
Sock
读满read buf或读到EOF,再发
发送时将read buf
Close file N N n
fd
Close socket N N n
sock

然后,定义一个结构用于保存这些变量:
struct process {
    int sock;
    int status;
    int response_code;
    int fd;
    int read_pos;
    int write_pos;
    int total_length;
    char buf[BUF_SIZE];
};

为了简便,我直接用一个全局数组装所有的process:
static struct process processes[MAX_PORCESS];

另外定义每个连接通信过程中的三个状态:
#define STATUS_READ_REQUEST_HEADER    0
#define STATUS_SEND_RESPONSE_HEADER    1
#define STATUS_SEND_RESPONSE        2

之后,就是按部就班地实现主循环、读取request,解析header,判断文件是否存在、检查文件修改时间,发送相应的header和content了。

下面只把程序中跟epoll有关的关键部分贴出来:

main()函数:

使用epoll_create()创建一个epoll fd,注意,这里的listen_sock已经设置为nonblocking(我使用了这篇文章中的setNonblocking函数)了:
    efd = epoll_create1 ( 0 );
    if ( efd == -1 )
    {
        ...
    }

    event.data.fd = listen_sock;
    event.events = EPOLLIN | EPOLLET;
    s = epoll_ctl ( efd, EPOLL_CTL_ADD, listen_sock, &event );
    if ( s == -1 )
    {
        ...
    }

    /* Buffer where events are returned */
    events = calloc ( MAXEVENTS, sizeof event );

这里的EPOLLIN表示监听“可读”事件。

在主循环中epoll_wait():
    while ( 1 )
    {
        int n, i;

        n = epoll_wait ( efd, events, MAXEVENTS, -1 );
        if ( n == -1 )
        {
            perror ( "epoll_wait" );
        }
        for ( i = 0; i < n; i++ )
        {
            if ( ( events[i].events & EPOLLERR ) ||
                    ( events[i].events & EPOLLHUP ) )
            {
                fprintf ( stderr, "epoll error\n" );
                close ( events[i].data.fd );
                continue;
            }

            handle_request ( events[i].data.fd );

        }
    }

epoll_wait()会在发生事件后停止阻塞,继续执行,并把发生了事件的event的file descriptor放入events中,返回数组大小。注意的是,这里要循环处理所有的fd。

接下来是关键部分:
void handle_request ( int sock )
{
    if ( sock == listen_sock )
    {
        accept_sock ( sock );
    }
    else
    {
        struct process* process = find_process_by_sock ( sock );
        if ( process != 0 )
        {
            switch ( process->status )
            {
            case STATUS_READ_REQUEST_HEADER:
                read_request ( process );
                break;
            case STATUS_SEND_RESPONSE_HEADER:
                send_response_header ( process );
                break;
            case STATUS_SEND_RESPONSE:
                send_response ( process );
                break;
            default:
                break;
            }
        }
    }
}

根据epoll返回的fd,做不同处理:如果是监听的socket,则accept();否则,根据sock的fd查找相应的process结构体,从中取回状态信息,返回到之前的处理状态中。这样就能实现信春哥,死后原地复活的状态恢复机制了。

在accept中,将accept出来的连接也设置为非阻塞,然后在process数组中找一个还没使用的空位,初始化,然后把这个socket存到process结构体中:
struct process* accept_sock ( int listen_sock )
{
    int s;
    // 在ET模式下必须循环accept到返回-1为止
    while ( 1 )
    {
        struct sockaddr in_addr;
        socklen_t in_len;
        int infd;
        char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
        if ( current_total_processes >= MAX_PORCESS )
        {
            // 请求已满,accept之后直接挂断
            infd = accept ( listen_sock, &in_addr, &in_len );
            if ( infd == -1 )
            {
                if ( ( errno == EAGAIN ) ||
                        ( errno == EWOULDBLOCK ) )
                {
                    break;
                }
                else
                {
                    perror ( "accept" );
                    break;
                }
            }
            close ( infd );

            return;
        }

        in_len = sizeof in_addr;
        infd = accept ( listen_sock, &in_addr, &in_len );
        if ( infd == -1 )
        {
            if ( ( errno == EAGAIN ) ||
                    ( errno == EWOULDBLOCK ) )
            {
                break;
            }
            else
            {
                perror ( "accept" );
                break;
            }
        }

        getnameinfo ( &in_addr, in_len,
                      hbuf, sizeof hbuf,
                      sbuf, sizeof sbuf,
                      NI_NUMERICHOST | NI_NUMERICSERV );

        //设置为非阻塞        s = setNonblocking ( infd );
        if ( s == -1 )
            abort ();
        int on = 1;
        setsockopt ( infd, SOL_TCP, TCP_CORK, &on, sizeof ( on ) );
        //添加监视sock的读取状态
        event.data.fd = infd;
        event.events = EPOLLIN | EPOLLET;
        s = epoll_ctl ( efd, EPOLL_CTL_ADD, infd, &event );
        if ( s == -1 )
        {
            perror ( "epoll_ctl" );
            abort ();
        }
        struct process* process = find_empty_process_for_sock ( infd );
        current_total_processes++;
        reset_process ( process );
        process->sock = infd;
        process->fd = NO_FILE;
        process->status = STATUS_READ_REQUEST_HEADER;
    }
}

三个不同状态对应三个不同函数进行处理,我就不全贴了,以read_request为例:
void read_request ( struct process* process )
{
    int sock = process->sock, s;
    char* buf=process->buf;
    char read_complete = 0;

    ssize_t count;

    while ( 1 )
    {
        count = read ( sock, buf + process->read_pos, BUF_SIZE - process->read_pos );
        if ( count == -1 )
        {
            if ( errno != EAGAIN )
            {
                handle_error ( process, "read request" );
                return;
            }
            else
            {
                //errno == EAGAIN表示读取完毕
                break;
            }
        }
        else if ( count == 0 )
        {
            // 被客户端关闭连接
            cleanup ( process );
            return;
        }
        else if ( count > 0 )
        {
            process->read_pos += count;
        }
    }

    int header_length = process->read_pos;
    // determine whether the request is complete
    if ( header_length > BUF_SIZE - 1 )
    {
    process->response_code = 400;
    process->status = STATUS_SEND_RESPONSE_HEADER;
    strcpy ( process->buf, header_400 );
    send_response_header ( process );
    handle_error ( processes, "bad request" );
    return;
    }
    buf[header_length]=0;
    read_complete = ( strstr ( buf, "\n\n" ) != 0 ) || ( strstr ( buf, "\r\n\r\n" ) != 0 );

    if ( read_complete )
    {
        // ...
        //解析之后,打开文件,把文件描述符存入process,然后进入发送header状态 
        process->status = STATUS_SEND_RESPONSE_HEADER;
        //修改此sock的监听状态,改为监视写状态
        event.data.fd = process->sock;
        event.events = EPOLLOUT | EPOLLET;
        s = epoll_ctl ( efd, EPOLL_CTL_MOD, process->sock, &event );
        if ( s == -1 )
        {
            perror ( "epoll_ctl" );
            abort ();
        }
        //发送header
        send_response_header ( process );
    }
}

这里的注意点如下:

1. 读取的时候要一直循环读取到返回-1为止,然后检查errno,如果errno为EAGAIN,表示缓冲区已经空了,这个socket变为了“不可读”。如果不读完,边缘触发模式的epoll_wait将永远不会再触发这个socket的“可读”事件。

2. 使用epoll_ctl ( efd, EPOLL_CTL_MOD, process->sock, &event )修改epoll的状态,这里在读完后,我们要继续监听“可写”事件,因此要把epoll监听的事件改为EPOLLOUT。

接下来不断完善这个程序并进行优化,并实现了304 not modified功能之后,用ab测试性能,并和nginx对比:
Server Software:        clowwindyserver/1.0
Server Hostname:        localhost
Server Port:            8082
Document Path:          /jquery.js
Document Length:        57244 bytes
Concurrency Level:      100
Time taken for tests:   2.241 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      574420000 bytes
HTML transferred:       572440000 bytes
Requests per second:    4462.88 [#/sec] (mean)
Time per request:       22.407 [ms] (mean)
Time per request:       0.224 [ms] (mean, across all concurrent requests)
Transfer rate:          250348.23 [Kbytes/sec] received
Server Software:        nginx/0.7.67
Server Hostname:        localhost
Server Port:            80
Document Path:          /jquery.js
Document Length:        57244 bytes
Concurrency Level:      100
Time taken for tests:   2.490 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      574720000 bytes
HTML transferred:       572440000 bytes
Requests per second:    4016.54 [#/sec] (mean)
Time per request:       24.897 [ms] (mean)
Time per request:       0.249 [ms] (mean, across all concurrent requests)
Transfer rate:          225428.04 [Kbytes/sec] received

结果很令人欣慰的比nginx快了一点点,并且只用了700K内存。不过作为一个功能比nginx少了很多的程序来说这一结果是意料之中的。

然后试图测试上万并发的情况,结果too many open files了。于是修改fd数限制:
# echo 32768 > /proc/sys/fs/file-max
# ulimit -n 32768

再次测试:
Server Software:        clowwindyserver/1.0
Server Hostname:        localhost
Server Port:            8082
Document Path:          /jquery.js
Document Length:        57244 bytes
Concurrency Level:      10000
Time taken for tests:   2.249 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      574420000 bytes
HTML transferred:       572440000 bytes
Requests per second:    4445.59 [#/sec] (mean)
Time per request:       2249.420 [ms] (mean)
Time per request:       0.225 [ms] (mean, across all concurrent requests)
Transfer rate:          249378.52 [Kbytes/sec] received

nginx设置worker_connections  20480以后:
Server Software:        nginx/0.7.67
Server Hostname:        localhost
Server Port:            80
Document Path:          /jquery.js
Document Length:        57244 bytes
Concurrency Level:      10000
Time taken for tests:   2.715 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      574720000 bytes
HTML transferred:       572440000 bytes
Requests per second:    3683.83 [#/sec] (mean)
Time per request:       2714.569 [ms] (mean)
Time per request:       0.271 [ms] (mean, across all concurrent requests)
Transfer rate:          206754.74 [Kbytes/sec] received

结果性能只下降了一点点,并且依然领先nginx。不过,为了承受更多的连接调大process数组以后,进程一共吃了40M内存。而 nginx仅用了5M内存。因为我们使用了极其浪费内存的用数组装连接状态和缓存的方法,所以会吃掉缓存大小(process.buf的大小)乘以 process数组的大小的内存。调小缓存以后内存占用降到6M,不过这并不是根本解决之道,还是存在很大的浪费。如果改为动态内存管理,应该就会小于 nginx了。

这说明事件驱动的非阻塞IO可以顶得住上万并发,并需要远小于阻塞式编程的服务器程序的内存,速度也更快。

最后把源码丢到github了,想看完整源码的同学请移步:

https://github.com/clowwindy/clowwindy_server
参考阅读:

The C10K problem (强烈推荐)

Introduction to non-blocking I/O

Non-blocking I/O with regular files

Linux Files and the Event Poll Interface
 

2012年1月20日星期五

12306性能问题之我见

铁道部12306订票网站可谓是生不逢时。在春运的大潮下,没能经受住考验。既然是网络订票,其使用的主体必然是广大的“网民”,其中不乏身手不凡的“极客”和专家。继令人叹为观止的使用Firebug抢火车票(不知农民工兄弟们会怎么想)以后,又有人本着“得一火车票胜造七级浮屠”的仁慈之心,开发出了”造福万民“的12306 Helper火狐插件。这些极客手段和插件的诞生对12306来说更是雪上加霜。

要解决这个问题也不是不可能,网上除了上述这些损人利己的极客手段外,还涌现出很多为12306”支招“的文章,其中有很多关于大容量web站点架构的思想值得我们去学习和思考。比如酷壳风云这里。作为一个研究网站架构技术的从业人员,我也谈一下这个问题。 

大型购票系统的设计

用过12306.cn的人,尤其是互联网从业人员,均会觉得这个网站很“烂”。依我之见,烂体现在两个方面,一个是界面以及使用体验之烂;另一个是架构理念上的烂 -- 仍然是在用N年前做ERP的思路在做一个web 2.0时代大型的电子商务网站。第一点是顾客能够感受到的,而第二点则不是那么明显了。

那么,这个系统正确的做法是什么呢?上文引用的几篇博文无一例外地为铁道部指明了正确的方向:使用队列系统。我完全赞同他们的思路,只是在实现的细节上有我自己的思考。

使用流程

我设想的购票流程是这样的:

1. 车次查询

客户输入所需的出发地、目的地和出发日期/时间查询列车班次。 在这一步骤中,只有出发和目的地是关键信息。客户可以一次性按照他自己的优先次序选择多个不同日期/时间出发的车次,全部加入“购物篮”。

2. 订单确认和预付款
 
在订单确认环节,客户输入乘车人的身份证件信息,提交订单。系统将首先验证用户输入的手机号确为客户所拥有(发送验证码),然后引导客户进行网上支付。注意:在此步骤中支付的款项为预付款,即押金,支付并不代表已经成功购得车票。在支付之前,系统会请用户确认若购票不成功,押金何时返还。客户可以选择押金在操作后的第二天立即返还,也可以选择押金在若干天后如仍无法成功购得车票才返还。支付以后,系统给出一个6~8位字符的确认码,通过短信发送到客户的手机上。

3. 竞购

系统后台建立一个队列,分下单和订单处理两个阶段运作。例如,设定每个周期为20分钟,其中每个周期的后10分钟为订单处理阶段。没能在周期前10分钟内完成押金支付的客户的订单将累积到下个周期的订单处理阶段中处理。每周期的竞购结束后,若客户购票成功,系统将发送一条短信至客户手机,此短信包含在订单确认环节中生成的确认码;若客户购票不成功,订单将被自动累积到下一个处理周期,同时,若此轮为该客户第一次参与竞购,系统也将发送一条提示短信(后续周期中如果该客户仍然未成功购得车票,系统将不再发送短信提示)。

4. 定位和取票

竞购成功的客户可以凭手机号码(或身份证号码)以及确认码登录取票网站选择自己的座位,也可以在代售点或车站进行座位确认,并取票,取票时需使用身份证。

5. 退款

发生退款的情形有三种:
  • 未成功购得车票,这里又分两种原因:
    • 身份信息不正确(如:姓名和身份证号不匹配):押金将退回到付款所用的银行账户
    • 竞购失败:押金将退回到乘车人所拥有的银行账户
  • 退票:押金将退回到乘车人所拥有的银行账户 
退款操作系统自动执行,无需用户或运营管理人员干预。

6. 查询及退票

客户可以通过专门的网站查询界面通过手机号、身份证号以及确认码来查询订票情况、执行退票操作(包括输入退票所用的账户信息)。

为什么这样设计

铁路售票系统最需要解决的问题就是:在僧多粥少的情况下,如何公平地分配稀缺资源?引申出来的问题是:如何设计一个高效的在线售票系统应对大流量?如何防范黄牛或恶意攻击?在流程设计层面,我的设计方案做到了两点:

有效而且公平

与本文开头所引用的几篇博文的一个小区别是,我在设计队列系统的时候特别考虑了所谓“秒杀”的做法,并在流程设计上加以杜绝。同时,方案中又保留了队列系统的基本特征,即先来后到。

鲁棒而且安全

本系统最大的特色是免注册、免登录(查询除外)。通过简化流程和技术手段的配合,可以极大地提升系统的吞吐量,而又不降低系统的鲁棒性和安全性:
  1. 充分利用车票实名制,完全避免注册和登录过程。
  2. 通过预付款方式,避免黄牛、恶意攻击者和“秒杀”者对服务器产生过大压力。
  3. 通过退款账户限制,从根本上消灭黄牛的生存空间。同时,由于使用了完全异步化的队列系统和唯一的编号(身份证),系统无需想方设法去防止“占着茅坑不拉屎”(或者说DOS攻击)或者重复不合理购买(比如一人购买同一天的多个车次等),这样也就避免了“错杀”(即一个心怀不轨的人乱输入别人的身份证号码,导致真正想购票的人无法购票),提高了用户体验。
设计上的要点

我自信这套购票流程设计是比较完善的(当然,如果你发现了问题或有所欠缺,欢迎留言或通过email和我联系)。但它也不是没有问题的。在设计上,它的主要问题就是和外部系统的互通上。它需要和公安系统联系,验证身份信息的有效性(验证是后台异步进行的,这样可以避免流量压力,也可以保护公民身份信息);还需要和银行支付系统联系,尤其是退款通道上是否会有由于银行系统规范而导致的一些问题,我不得而知。

但与外部系统沟通这个问题现有的12306网站也是要做的,而且我相信这些事情对于铁道部来说是小菜一碟了。这并不是本文所要讨论的内容,不展开了。

技术上的要点

客户端查询

导致当前系统的压力过大的主要原因是购票过程中必不可少的查询,包括车次的查询和剩余票量的查询。我的解决方案是:车次查询完全在客户端完成;不提供实时的剩余票量查询。

具体的解决方案是,客户告诉服务器他的出发和到达地点,凭这点信息,服务器立即返回一套这两个地点之间的车次的所有数据,包括车次、时间、是否有票或剩余票量(不区分车型是否动车、不区分是否始发站,所有数据一次性返回)。这些数据以gzip形式压缩存储于内存数据库(如redis)中,web服务器返回此类数据的速度可以超过读取磁盘文件的速度。客户端取得数据后,以javascript查询本地数据,往“购物篮”中添加订单,直到订单提交时才会再次与服务器沟通。 注意:剩余票量在开始取回以后不实时更新,直到下一个竞购周期才会刷新。

通过这种手段,服务器端的链接数和PV可以至少有数量级以上的下降。用这里提到的数据估算,12306的峰值访问量可能达到两个小时5亿PV,也就是说,每秒钟大约7万个hit。假设新方案降低PV到原来的1/10,也就是不超过7000hits/s。这种量也就一台NginX就可以搞定。当然,实际可能还有HA、热备但也不至于太复杂了。而查询所需的基于JavaScript技术的“列车时刻表”,我没有测算过全国有多少个车站,又有多少种换乘方案,随便估算一下,弄两台128G内存的服务器做Master-Slave,肯定可以搞定了(其实不会有那么多,因为只有热数据才会保存在内存中)。

按功能集群

这个问题我引用的博文中被多次提到,也是架构设计中需注意的“最佳实践”之一。例如,订票网站叫做ticket.12306.cn,提供查询功能,下单服务器是order.12306.cn,而后台还有一堆集群负责订单的处理,这才是本系统工作量集中的地方。后台订单处理系统的任务分配可以根据出发地和到达地进行合理的聚合,即同一(或近似)路线的放在同一服务器上处理。

恰当的防DDOS系统

为了提供最鲁棒的系统,我认为没有必要限制每个用户每次能购买多少票,在设计中也做了种种考虑来实现这种“完全开放”的下单方式。但在IT层面,恰当的防止攻击还是需要的,比如防止同一个IP过快地访问服务器等等。

吾道一以贯之

谈到架构,很多人可能认为是“后端” 的问题,脑子里面浮现出来的可能是CISCO的路由器、IBM的服务器、NginX等等。经验丰富的工程师同时会考虑前端的优化,比如使用AJAX技术、 页面静态化、用YSlow去分析一下等等。但较少有人会将架构和产品设计联系在一起。其实,“架构”是贯穿互联网产品方方面面的灵魂。一个好的架构可以令 你的产品光芒四射,而一个坏的产品设计则会让架构师伤透脑筋,让耗资不菲的硬件设备不堪重负。

上面,我从12306这个活生生的例子阐述了在“架构”这个问题上我的思考:架构问题是一个从需求分析到产品设计到技术实现的大问题。一个优秀的架构师不仅仅要懂得技术实现的手段,而且还必须是一个合格的需求分析人员和产品设计师。

2011年11月2日星期三

PHP Mixin的改进

前段时间写了一篇关于PHP Mixin的文章,用着还挺顺手的。今日对我的Mixin类做了一点改进,让它能够用shell风格的通配符来过滤注入的方法。新的Mixin类代码如下。

class Mixable {
    private $_mixin_methods;
    private $_mixin_classes;
    public function mixin() {
        $behaviors = func_get_args();
        if ((count($behaviors) == 0) ||
            !isset($this->_mixin_methods) ||
            !isset($this->_mixin_classes)) {
            $this->_mixin_methods = array();
            $this->_mixin_classes = array();
        }
        foreach($behaviors as $b) {
            list($c, $m) = explode('-', $b.'-');
            if (empty($m)) {
                list($c, $m) = explode('+', $b.'+');
                $skip = false;
            } else {
                $skip = true;
            }
            $mfilter = empty($m) ? array() : explode(',', $m);
            if(isset($this->_mixin_classes[$c])) {
                foreach($this->_mixin_classes[$c] as $m) {
                    if ($this->_mixin_methods[$m] == $c) unset($this->_mixin_methods[$m]);
                }
                unset($this->_mixin_classes[$c]);
            }
            $methods = get_class_methods($c);
            if (is_array($methods)) {
                if (empty($mfilter)) {
                    foreach($methods as $m) {
                        $m = strtolower($m);
                        $this->_mixin_methods[$m] = $c;
                        $this->_mixin_classes[$c][] = $m;
                    }
                } else {
                    foreach($methods as $m) {
                        $m = strtolower($m);
                        $matched = false;
                        foreach($mfilter as $wildcard) {
                            if (fnmatch($wildcard, $m, FNM_CASEFOLD)) {
                                $matched = true;
                                break;
                            }
                        }
                        if ($matched == $skip) continue;
                        $this->_mixin_methods[$m] = $c;
                        $this->_mixin_classes[$c][] = $m;
                    }
                }
            }
        }
    }
    public function __call($method, $args) {
        $m = strtolower($method);
        if (isset($this->_mixin_methods[$m])) {
            $class = $this->_mixin_methods[$m];
            array_unshift($args, $this);
            return call_user_func_array(array($class, $m), $args);
        }
        trigger_error('Call to undefined function '.$method, E_USER_ERROR);
    }
}

用两段代码来说明用法:

例1:行为的继承

class Course {
    public static function greeting($obj) {
        if (get_class($obj) == 'Teacher')
            echo "Good morning class!\n";
        else
            echo "Good morning teacher!\n";
    }
}
class CourseWork extends Course {
    public static function homeworkReview($obj) {
        echo "I am reviewing homework...\n";
    }
}
class HomeWork extends Course {
    public static function homeworkStudy($obj) {
        echo "I am doing my homework...\n";
    }
}
class Teacher extends Mixable {
}
class Student extends Mixable {
}
$t = new Teacher();
$t->mixin('CourseWork');
$s = new Student();
$s->mixin('HomeWork');
$t->greeting();
$t->homeworkReview();
$s->greeting();
$s->homeworkStudy();

在这个例子中,演示了行为是可以继承的。另外要注意,行为类中的方法一般需要写作static的,虽然不写成static也可以跑,但如果php设置为STRICT模式,会有告警。

例2:选择性注入 

class Behavior {
    public static function adminAction($obj) {
        echo "> This behavior require <admin> privilege\n";
    }
    public static function userAction($obj) {
        echo "> This behavior is normal\n";
    }
}
class Power {
    public static function hyperPower($obj) {
        echo "> PHP Hyper Power...\n";
    }
}
class Main extends Mixable {
}
$m = new Main();
echo "Mixin Behavior without exclusion\n";
$m->mixin('Behavior', 'Power');
echo "Trying user action\n";
$m->userAction();
echo "Trying admin action\n";
$m->adminAction();
$opt = mt_rand() % 3;
switch($opt) {
    case 0:
        echo "Mixin Behavior while excluding admin action\n";
        $m->mixin('Behavior-Admin*');
        break;
    case 1:
        echo "Mixin Behavior while including only user action\n";
        $m->mixin('Behavior+User*');
        break;
    default:
        echo "Mixin Behavior again (clearing Hyper Power)\n";
        $m->mixin();
        $m->mixin('Behavior');
}
echo "Trying hyper power...\n";
$m->hyperPower();
echo "Trying user action\n";
$m->userAction();
echo "Trying admin action\n";
$m->adminAction();

这个例子说明以下几个用法:
  • 通过类名+方法1,方法2...(只注入这些方法)或者类名-方法1,方法2...(注入除这些方法以外的其它方法)这种表达法来选择性地注入行为类中的一部分方法
  • 如果多次注入不同的行为,先前注入的不会被取消,除非以无参数的mixin()调用来清除以前注入的类
  • 在过虑方法名的时候可以用shell通配符(大小写不区分)
  • 最后(代码中没有体现),mixin()方法的参数可变。一次mixin调用可以注入多个类,例如: $obj->mixin('Class1', 'Class2');

2011年10月21日星期五

[转] Ubuntu中设置个人固定文件夹的语言

Ubuntu对中文的支持越来越好,这自然是好事。不过在终端下输入命令的时候,遇到中文文件夹,可不是件好事。多谢谷歌及众多网友,终于让我找到解决的办法。
   
    export LANG=en_US
    xdg-user-dirs-gtk-update
    export LANG=zh_CN.UTF-8

这样基本就解决问题了。


原文地址

2011年10月14日星期五

PHP Closure的一个应用

Closure是一种很灵活的技术,如果运用得当,可以大大提高代码的可维护性,尤其对于象PHP这样的动态语言更是如此。Closure的定义可以在Wikipedia上查到,关于这一技术的学术文章也可以轻易的搜索到,在此就不赘述了。

在工作中,我需要实现一个事件处理系统。我把它设计为一个类,在类的一个方法中,我设计了一个回调函数来处理事件。它的主要逻辑如下:
  1. //main.php
  2. require 'event_handlers/index.php';
  3. $type = 'event1';
  4. function handleEvent() {
  5.     global $type;
  6.     if (function_exists('handle_'.$type)) {
  7.         call_user_func('handle_'.$type);
  8.     }
  9. }
  10.  
  11. //event_handlers/index.php
  12. require 'event1.php';
  13.  
  14. //event1.php
  15. function handle_event1() {...}
在上面的例子中,我使用了一个全局的变量$type,这种做法只是为了少打一些字,突出主要逻辑,在实际代码中handleEvent是一个类的方法。

它的结构很简单,主程序中引用了一个存放所有事件的处理函数的“索引”文件,即event_handlers/index.php,每个种类的事件处理程序都被放置在一个独立的文件中,由索引文件引用。

现在问题来了。由于系统设计中的不确定性,我不能保证事件类型(即$type)将来不会改名。一旦类型的名字改了,就需要改变每个类型处理函数的名字,而一 类事件会有多个不同的处理函数(即上例中event1.php中的函数个数会有多个)。为了使将来的代码维护尽量简单,我从#php学了一个新的方法,即 利用closure使得事件处理函数的名字是动态的。一旦发生事件类型改变,只需要修改事件处理“函数库”的文件名即可。下面的例子演示了这种用法:

//main.php
$event_type = $argv[1];
require_once "$event_type.php";
$handler = 'handle_'.$event_type;
if (is_callable($$handler)) {
    call_user_func($$handler);
} else {
    echo "$handler is not defined\n";
}

//event1.php
$event_type = basename(__FILE__, '.php');
$handle_this = 'handle_'.$event_type;
$$handle_this = function() {
    global $event_type;
    echo "Testing handler of $event_type...\n";
};

在 这个例子中,event1.php可以任意重命名,主程序只需要以那个名字调用它即可以引用其中定义的事件处理函数。需要注意的是这里必须用 is_callable代替function_exists,因为Closure从本质上说是匿名函数,不能用function_exists检测其存在 与否。另外,两次解引用的方法(即$$handler、$$handle_this等)也需要注意不要弄错了。

PHP的Mixin

日前看了一篇在PHP中实现Mixin的博文,感到非常的有用,就仔细研究了一下作者的思路,并且到#php上去咨询了一下泡在上面的PHP牛,然后对原文作者的实现方案进行了一些改良,记录下来备用。

一、何谓Mixin?

Mixin是一种编程的风格,在Lisp等语言里面早已有了,但我首次知道这个概念是在Ruby语言中。我对Mixin的理解是:一种行为注入(Behavior Injection)的方法。要了解Mixin,也就是“行为注入”的好处,需首先了解OOP中的“多重继承”和“接口”的概念。

在我的印象中,接口是在继承这个概念出现以后才进入主流语言的。首先有了C++的多重继承,然后才有了Java中否定多重继承而代之以接口的概念(当然,一些先驱性或革命性的语言如Lisp、Eiffel可能早就有此类概念了)。接口带来的好处不言而喻,但它仍然有不够灵活的地方:如果一个类声明了某个接口,它就必须实现此接口,而如果它没有声明某接口,也就无法在运行时动态地获取此接口相应的行为。

Mixin则灵活的多。它相对于接口的优势在于,无需规定接口,可以随时添加/删除行为。这种动态添加行为的能力非常有用。它可以让我们写出既面向对象而又面向切面 的代码。比如,在ERP系统中有一个员工类。假设“主管”级别的员工有查看报表的权利,我们可以利用Mixin,使得系统中只需要一个员工类,就可以方便 的实现权限控制 -- 只需要在合适的地方注入“主管”行为即可,而无需针对员工类派生出各种具有不同权限的主管类、工人类等等,或者到处写一大堆复杂的、不那么OO的权限判断代码。

二、在PHP中实现Mixin

先来看一下代码:

<?php
class Greeting {
    public function greet($obj) {
        echo $obj->getName()." says hello\n";
    }
    private function __construct() {} //一个private的构造函数使得这个类不能直接被实例化
}

class Mixable {
    private $_mixinlookup = array();
    public function __construct() {
        if (is_array($this->mixins)) {
            foreach($this->mixins as $mixin) {
                $methods = get_class_methods($mixin);
                if (is_array($methods)) {
                    foreach($methods as $method) {
                        $this->_mixinlookup[$method] = $mixin;
                    }
                }
            }
        }
    }
    public function __call($method, $args) {
        if (isset($this->_mixinlookup[$method])) {
            $class = $this->_mixinlookup[$method];
            array_unshift($args, $this);
            return call_user_func_array(array($class, $method), $args);

        }
        trigger_error('Call to undefined function '.$method, E_USER_WARNING);
    }
}

class Mixture extends Mixable {
    protected $mixins = array("Greeting");
    public function getName() {
        return get_class();
    }
}

$o = new Mixture();
$o->greet();
?>

在这个例子中,Mixture类被注入了Greeting这种行为而具备了greet()的能力。这种行为是在运行中可以随时改变的。想象一下这种能力的用途吧。

三、针对原文的改进之处

原文中使用了eval方法来实现Mixin,带来了两个弊端:1)被资深PHP人一致痛斥的eval具有很大的安全隐患和很差的性能;2)我发现按照原方案,这些被注入的方法只能接受简单的数字或字符串参数,而不能使用复杂的如array或object类型的参数。

改进的方法是,避免在需要注入的方法中使用$this,而是学习python,在方法的第一个参数传入当前对象的指针(也就是例子代码中greet的参数$obj)。只要习惯了这种做法,其实它和$this是一样方便的。

给网站加个SSL证书

以NginX为例,本文描述两种证书安装方式:

非正式证书

出于测试的目的,可以自行生成证书,步骤相当的简单:

一、生成密钥和证书
  1. openssl genrsa -des3 -out server.key 2048
  2. openssl req -new -key server.key -out server.csr
  3. cp server.key server.key.org
  4. openssl rsa -in server.key.org -out server.key
  5. openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
以上步骤的要点说明如下:
  • 第一步中需要输入密码,这个密码要记住,因为第二步中需要这个密码。
  • 此证书的密钥设置为2048 bit (第一步)、有效期为一年(第五步)。
  • 最终生成的文件有两个:server.key (密钥)、server.crt(证书)。
二、安装证书
  1. 将server.key、server.crt复制到nginx/conf目录下。
  2. 修改nginx.conf文件,打开被注释掉的SSL服务器配置模板,填入所需的密钥和证书文件名。
此处仅说明一点:如果密钥和证书文件在nginx/conf目录下,配置文件中可以直接使用文件名,不然的话,配置文件中需要使用全路径。

正式证书

正式证书的申请制作与非正式的类似。关键是非正式操作中的第五步在正式操作中是由认证机构完成的。你把server.csr提交给认证机构,他们就会发还一个server.crt(注意,可能收到两个文件,其中一个是认证机构的证书,需用cat命令附加在crt文件末尾)。详情可阅读参考文献,并按照认证机构的指示操作。参考文献中介绍了一个值得尝试的免费证书提供商:StartCom的StartSSL(http://www.startssl.com)。

参考文献

http://love.ulnmp.com/?p=271