垃圾回收算法总结

最近研读了《垃圾回收的算法与实现》这本书, 对来垃圾回收(GC)的来龙去脉及理论和实践有了一个概括性,深入性的了解,这里分多篇进行总结。首先本文先对GC的理论来一个总览性的回顾.

什么是垃圾回收

我们知道一台服务器的内存是有限的,而程序的运行需要占用内存空间,一个程序内部可能有些内存空间使用后不再使用,这部分不再使用的内从空间就被视为垃圾。而GC就是要

  1. 找到内存空间里的垃圾
  2. 回收垃圾,让程序员能够再次利用这部分空间

如果没有GC的情况下需要程序员自己手动管理内存,例如C/C++等程序。这个过程将会非常麻烦,如果管理不当就会照成内存泄露引起系统崩溃,引发各种恶性bug和安全问题。有了GC就会省去很大一部分精力,降低了开发的难度。

垃圾回收基本概念

要深入了解垃圾回收的理论知识,下面这些关键件信息比必要掌握:

  • 对象/头/域: 这里对象是由头(heder)和域(field)构成的。头是指保持对象本身信息的部分,主要包括对象的大小对象的种类;域是对象使用者可以访问的部分,域的数据类型主要分为指针和非指针两种。
  • 指针: GC根据对象的指针指向去搜寻其他对象,对于非指针不进行任何操作。
  • mutator: 程序运行过程中关系的改变,主要包括生成对象更新指针等操作。
  • 堆: 用于动态存放对象的内存空间。当mutator申请存放对象时,所需的内从空间就是从这个堆中被分配给mutator的。
  • 活动对象/非活动对象: 内存空间中可以通过mutator引用的对象是”活动对象”, 不能通过程序引用的称为”非活动对象”。非活动对象无法重新被引用,所以就是”垃圾”。
  • 分配: 内存空间中分配(allocatio)对象。当mutator需要新对象时,就会向分配器(allocator)申请一个大小合适的空间。
  • 分块: 未利用对象而事先准备的空间。初始状态堆就是一个大分块,根据mutator的需求而分割成合适的大小。
  • 根: 跟是指向对象的指针的起点,通过mutator可以直接调用的调用栈(call stack),寄存器和全局变量都是根。但是调用栈和寄存器中的值是不是指针,需要再做判断。
  • 评价标准: GC算法的性能评价标准主要有
    1. 吞吐量: 单位时间内的处理能力。
    2. 最大暂停时间: 因执行GC和停止mutator的最长时间。
    3. 堆使用效率
    4. 访问的局部性: 局部性原理,数据离得越近越好处理。

垃圾回收算法总览

首先先上一张垃圾回收算法的总概括图:
垃圾回收算法总览
上面列举和好多算法及对应的细节。其实GC最基本的思想就是三种算法(GC标记-清除法, 引用计数法, GC复制算法), 其他算法都算是这几个算法的延伸和组合。

phpenv安装自定义配置

自定义配置

在使用phpenv安装php是,有时候需要对内置扩展进行自定义控制是否开启,比如我要开启zts模块, 源码安装我么可以用./configure --enable-maintainer-zts来安装,但是phpenv不支持直接这么写,这时候就要phpenv自己的方式来安装了。可以在phpenv安装的路径里找到下面这个文件:~/.phpenv/plugins/php-build/bin/php-build, 这个文件就是phpenv install时运行的脚本,可以找到如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
CONFIGURE_OPTIONS=$(cat "$PHP_BUILD_ROOT/share/php-build/default_configure_options")
...
if [ -n "$PHP_BUILD_CONFIGURE_OPTS" ]; then
CONFIGURE_OPTIONS="$CONFIGURE_OPTIONS $PHP_BUILD_CONFIGURE_OPTS"
fi
...
local append_default_libdir='yes'
for option in $CONFIGURE_OPTIONS; do
case "$option" in
"--with-libdir"*) append_default_libdir='no' ;;
esac
done
if [ "$(uname -p)" = "x86_64" ] && [ "${append_default_libdir}" = 'yes' ]; then
argv="$argv --with-libdir=lib64"
fi
...
./configure $argv > /dev/null
...

可见,默认会读取~/.phpenv/plugins/php-build/share/php-build/default_configure_options里面的配置加到./configure的参数里,当存在变量$PHP_BUILD_CONFIGURE_OPTS时,会把这个变量的值也加到./configure的参数里。
所以就存在两种方式实现上面的安装方法:

  1. ~/.phpenv/plugins/php-build/share/php-build/default_configure_options文件末尾加上--enable-maintainer-zts
  2. 运行PHP_BUILD_CONFIGURE_OPTS=--enable-maintainer-zts phpenv install 5.6.2

InnoDB关键特性

本篇博客是《Mysql技术内幕 InnoDB存储引擎(第二版)》的阅读总结.

工作方式

首先Mysql进程模型是单进程多线程的。所以我们通过ps查找mysqld进程是只有一个。

体系架构

InnoDB存储引擎的架构如下图所以,是由多个内存块组成的内存池,同时又多个后台线程进行工作,文件是存储磁盘上的数据。

后台线程

上面看到一共有四种后台线程,每种线程都在不停地做自己的工作,他们的分工如下:

  • Master Thread: 是最核心的线程,主要负责将缓冲池中的数据异步刷新的磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲(INSERT BUFFER),UNDO页的回收等。下面几个线程其实是为了分担主线程的压力而在最新的版本中添加的。
  • IO Thread: InnoDB使用大量的异步IO来处理请求。IO Thread的主要工作就是负责IO请求的回调(call back)处理。异步IO可以分为4个,分别是:write, read, insert buffer 和 log IO thread。
  • Purge Thread: undo log是用来保证事务的,当一个事务正常提交后,这个undo log可能就不再使用了。purge thread就是用来清除这部分log已经分配的undo页的。
  • Page Cleaner Thread: 主要是把脏页的刷新从主线程中拿到单独的线程,减轻主线程的压力,减少用户查询线程的阻塞,提高整体性能。

内存

由于InnoDB是基于磁盘存储的,为了使CPU与磁盘能够快的交互,提升整体性能而采用了缓冲池技术。
读数据简单的说可以用下面的流程图

更新数据的流程则如下:

由缓冲池的作用可以看到,缓冲池越大所容纳的数据就越多,与磁盘的交互就会越少,性能也就越高。所以缓冲池的大小直接影响着数据库的整体性能。
InnoDB在内存中主要有以下几部分组成:

具体来看缓冲池中缓存的数据页类型有:

  • 索引页: 缓存数据表索引
  • 数据页: 缓存数据页,占缓冲池的绝大部分
  • undo页: undo页是保存事务,为回滚做准备的。
  • 插入缓冲(Insert buffer): 上面提到的插入数据时要先插入到缓存池中。
  • 自适应哈希索引(adaptive hash index): 除了B+ Tree索引外,在缓冲池还会维护一个哈希索引,以便在缓冲池中快速找到数据页。
  • InnoDB存储的锁信息(lock info):
  • 数据字典(data dictionary):
    内存中除了缓冲池外外还有:
  • 重做日志缓冲redo log: 为了避免数据丢失的问题,当前数据库系统普遍采用了write ahead log策略,既当事务提交时先写重做日志,再修改写页。当由于发生宕机而导致数据丢失时,可以通过重做日志进行恢复。InnoDB先将重做日志放到这个缓冲区,然后按照一定的频率更新到重做日志文件中。重做日志一般在下列情况下会刷新内容到文件:
    • Master Thread每一秒将重做日志缓冲刷新到重做日志文件
    • 每个事务提交时会将重做日志缓冲刷新到重做日志文件
    • 当重做日志缓冲池剩余空间小于1/2时,重做日志缓冲刷新到重做日志文件
  • 额外内存池: InnoDB存储引擎中,对内存的管理师通过一种称为内存堆的方式进行的,在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当该区域的内存不够时,会从缓冲池中进行申请。

缓冲池是一个很大的内存区域,InnoDB是如何对这些内存进行管理的呢。答案就使用LRU list。
LRU(Latest Recent Used, 最近最少使用)算法默认的是最近使用的放到表头,最早使用的放到表尾,依次排列。当有LRU填满时有新的进来就把最早的淘汰掉。InnoDB则是在这个基础上进行了修改:

  1. 最近使用的不放到表头,而是根据配置放到一定比例处,这个地方叫做midpoint, midpoint之前的成为new列表,之后的成为old列表。淘汰的同样是表尾的页。
  2. 为了保证new列表的不经常使用时能够淘汰,设置了一个超时时间:innodb_old_blocks_time,当数据在midpoint(我理解应该是在old列表中,不然这个点的页就一个,变化也比较频繁)的时间超过找个时间时就会被提升到表头,new列表的表尾页则被置换到old列表中。

这么做的原因主要是因为常见的索引或数据的扫描操作会连续读取大量的页,甚至是全表扫描。如果采用原来的LRU算法就会更新全部的缓冲池,其他查询需要的热点数据就会被冲走,导致更多的磁盘读取操作,降低数据库的性能。
LRU是用来管理已经读取的页,当数据库启动时LRU是空列表,既只有表头,没有内容。这时页都放在Free List中。当需要有数据读写时要进行需要获取分页,这时要从Free List中删除分页,然后添加到LRU list中。到一定时间Free List中的分页就会被分配完毕,这时候就正常使用上面的LRU策略。
LRU列表中的页被修改后,称该页为脏页(dirty page),既缓冲池中的数据和磁盘上的数据产生了不一致,这时脏页会被加入到一个Flush 列表中(注意,同时存在两个列表中)。然后根据刷新的机制定时的刷新到磁盘中。

Checkpoint技术

checkpoint其实就是一个刷新缓冲到磁盘的触发机制,当满足一定的条件时就会刷新缓冲到磁盘,这样做可以解决以下几个问题:

  • 缩短数据库的恢复时间: 数据库恢复可以使用redo log,但是如果要恢复的数据很多就会很慢。如果使用checkpoint刷新到磁盘,只需要从checkpoint开始恢复就可以了,所以速度会变快。
  • 缓冲池不够用时,将脏页刷新到磁盘。我们知道缓冲池的大小是由限制的,为了能够高效的使用缓冲池需要把一部分数据刷新到磁盘。
  • 重做日志不可用时,刷新脏页。重做日志并不是无限增大的,而是循环利用的。当有些已经不需要的页存在时可以覆盖写,当可用的页放不下时就会触发checkpoint,刷新到磁盘一部分脏页到磁盘,这样就能覆盖掉一些不再使用的重做日志。

checkpoint根据触发时间,刷新页的策略又可以分为:

  • sharp checkpoint:刷新所有的脏页到磁盘。一般发生在数据库关闭时,为了保证所有的数据能够正常持久化。
  • fuzzy checkpoint:只刷新部分脏页。运行时使用这种可以保证系统的性能。

    Master Thread的工作方式

关键特性

插入缓存

这里所说的插入缓存也是Insert Buffer, 区别是这个插入缓存不是缓冲池中的插入缓存,这里的插入缓存和数据页一样,业务物理页的组成部分。在介绍插入缓存之前先了解聚集索引和非聚集索引,他们之间最重要的区别就是:聚集索引的叶子节点存储的是数据,而且是按照物理顺序存储的;非聚集索引叶子节点是地址(也就是聚集索引键地址),是按照逻辑顺序存储的(以上言论是从网上了解到的,但是本书P194特别指出,聚集索引也不是按照物理地址连续的,而是逻辑上连续的)。
知道这个差别后就知道,当不停的插入数据时,如果是聚集索引的数据,按照物理顺序(这个应该是一般情况下,因为是一般聚集索引是主键,顺序递增的,所以这时候地址就是顺序的)连续插入,代价比较小。而如果是非聚集索引的插入则物理地址是离散的,会导致很大的系统开销,所以对于非聚集索引InnoDB开创性设计了Insert Buffer。使用InnoDB的Insert Buffer需要以下两个条件:

  • 索引是辅助索引(非聚集索引 secondary index);
  • 索引不是唯一(unique)的。

Insert Buffer的使用流程是:

要求索引不是唯一的是因为如果索引是唯一的,那么每次更新都要坚持是不是已经存在,每次还是要访问数据页,这就失去了使用Insert Buffer的优势。
后面还提到了Update buffer以及Merge的过程和Insert Buffer的实现,这里就不再一一说了。

两次写

上面提到的Insert Buffer是提高了数据库的性能,doublewrite则是提高了数据库的可靠性。一个场景是当一个16k的数据页只写了一部分,比如4k,这时候突然断电,就会导致这个页的数据不全。所以就会导致这个页的数据丢失。我们知道重做日志是用来恢复数据的,但是重做日志记录的是对页的物理操作,如果这个页已经发生了损坏在对其进行重做是没有意义的。

上面这段话,其实我并没有看懂,因为对页操作之前是先写重做日志的,当发生宕机时正在写数据页,证明这时候重做日志已经写完了。这时重做日志的记录的完整的,当用这个记录去恢复数据时,不管页是不是损坏,重做日志直接覆盖不就行了么?为什么不行呢?等到后面我更加深入的了解后再来补充。

doublewrite有两部分组成,一部分是内存中的doublewrite buffer, 大小为2MB,另一部分是物理磁盘上共享表空间中连续的128个页,既两个区,大小同样为2MB
。对缓冲池的脏页进行刷新时,比不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的doublewrite buffer, 之后通过doublewrite buffer再分两次,每次1MB的写入共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘,避免缓冲写带来的问题。完成doublewrite页的写入后,再将doublewrite buffer中的页写入各个表空间文件中。

如果磁盘写入时发生崩溃,可以从共享表空间的doublewrite中找到副本,将其复制到表空间文件,再应用重做日志。

这个地方也有一个疑问,当doublewrite写入的过程中发生了崩溃,这时候数据该怎么办呢?

自适应哈希索引

对于缓冲池中的页,为了能够快速的查找,InnoDB跟情况对其建立了一个hash index。这样对于等值查询就能够利用这个索引更加快速的查找,提高了查找的性能。

异步IO

为了提高磁盘的操作性能,当前的数据库系统都采用异步IO的方式处理磁盘操作。用户可以在发出一个IO请求胡立即再发出另一个IO请求,当全部IO请求发送完毕后,等待所有IO操作完成,这就是AIO。
AIO的另一个优势是可以进行IO Merge操作,也就是将多个IO合并为1个IO, 这样可以提高IOPS的性能。

刷新临近页

Flush Neighbor Page(刷新临近页)是当刷新一个脏页时,InnoDB会检测该页所在区的所有页,如果是脏页,那么一起进行刷新。

apache与nginx对比

apache工作原理

apache httpd通过模块化的设计来适应各种环境,模块化的使用使其变得功能强大而且灵活。最基本的web服务器功能也是通过可选择的多处理模块(MPM),用来绑定到网络端口上,以及调度子程序处理请求。这样做可以带来两个重要的好处:

  • Apache httpd 能更优雅,更高效率的支持不同的平台。尤其是 Apache httpd 的 Windows 版本现在更有效率了,因为 mpm_winnt 能使用原生网络特性取代在 Apache httpd 1.3 中使用的 POSIX 层。它也可以扩展到其它平台 来使用专用的 MPM。
  • Apache httpd 能更好的为有特殊要求的站点定制。例如,要求 更高伸缩性的站点可以选择使用线程的 MPM,即 workerevent; 需要可靠性或者与旧软件兼容的站点可以使用 prefork

下面主要介绍常用的两个MPM工作原理。

perfork

一个单独的控制进程(父进程)负责产生子进程,这些子进程用于监听请求并作出应答。Apache总是试图保持一些备用的 (spare)或是空闲的子进程用于迎接即将到来的请求。这样客户端就无需在得到服务前等候子进程的产生。在Unix系统中,父进程通常以root身份运行以便邦定80端口(注意这里是先绑定再fork的,所以意味着所有的子进程都监听了80端口),而 Apache产生的子进程通常以一个低特权的用户运行。User和Group指令用于配置子进程的低特权用户。运行子进程的用户必须要对他所服务的内容有读取的权限,但是对服务内容之外的其他资源必须拥有尽可能少的权限。
子进程的个数会随着请求量的大小动态调整。调整的策略与perfork的配置息息相关,httpd.conf的配置文件有以下配置:

1
2
3
4
5
6
7
<IfModule prefork.c>
StartServers 5
MinSpareServers 5
MaxSpareServers 10
MaxClients 150
MaxRequestsPerChild 0
</IfModule>

具体的流程这里直接copyApache运行机制剖析这篇博文的介绍。

  1. 控制进程先建立’StartServers’个子进程;
  2. 当空闲进程数小于MinSpareServers时,继续创建子进程,直到满足空闲进程数大于等于MinSpareServers;
  3. 当并发请求高时而空闲进程数小于MinSpareServers时会继续创建子进程,最多可以创建MaxClients个;
  4. 当并发高峰过去时,空闲进程的数量大于MaxSpareServers时会删除多余的子进程,直到剩MaxSpareServers为止;
  5. 当子进程处理的连接数超过MaxRequestsPerChild时,自动关闭,当MaxRequestsPerChild为0时这没有这个限制;

对每个参数的介绍如下:

  • StartServers 指定服务器启动时建立的子进程数量。
  • MinSpareServers 最小的空闲进程数。如果当前空闲进程数少于MinSpareServers时,Apache将以每秒一个的速度产生新的子进程。
  • MaxSpareServers 最大的空闲进程数。如果空闲进程数大于这个值,Apache父进程会自动kill掉一些多余的子进程。
  • MaxRequestsPerChild 每个子进程可处理的请求数。每个子进程处理完MaxRequestsPerChild后将自动销毁。0意味着用户销毁。销毁的好处有以下两个:
    • 可以防止意外的内存泄露
    • 在服务器负载下降的时候会自动减少子进程数
  • MaxClients 设定Apache可以同时处理的请求,是对性能影响最大的参数。如果请求数达到这个限制,那么后来的请求就需要排队,直到某个请求处理完毕。

worker

每个进程能够拥有的线程数量是固定的。服务器会根据负载情况增加或减少进程数量。一个单独的控制进程(父进程)负责子进程的建立。每个子进程能够建立ThreadsPerChild数量的服务线程和一个监听线程,该监听线程监听接入请求并将其传递给服务线程处理和应答。Apache总是试图维持一个备用(spare)或是空闲的服务线程池。这样,客户端无须等待新线程或新进程的建立即可得到处理。在Unix中,为了能够绑定80端口,父进程一般都是以root身份启动,随后,Apache以较低权限的用户建立子进程和线程。User和Group指令用于配置Apache子进程的权限。虽然子进程必须对其提供的内容拥有读权限,但应该尽可能给予他较少的特权。另外,除非使用了suexec ,否则,这些指令配置的权限将被CGI脚本所继承。
相对于prefork,worker是2.0 版中全新的支持多线程和多进程混合模型的MPM。由于使用线程来处理,所以可以处理相对海量的请求,而系统资源的开销要小于基于进程的服务器。但是,worker也使用了多进程,每个进程又生成多个线程,以获得基于进程服务器的稳定性。这种MPM的工作方式将是Apache 2.0的发展趋势。

http.conf中也有关于worker的配置项:

1
2
3
4
5
6
7
8
9
10
<IfModule worker.c>
StartServers 3
MaxClients 2000
ServerLimit 25
MinSpareThreads 50
MaxSpareThreads 200
ThreadLimit 200
ThreadsPerChild 100
MaxRequestsPerChild 0
</IfModule>

由主控制进程生成“StartServers”个子进程,每个子进程中包含固定的ThreadsPerChild线程数,各个线程独立地处理请求。同样,为了不在请求到来时再生成线程,MinSpareThreads和MaxSpareThreads设置了最少和最多的空闲线程数;而MaxClients设置了所有子进程中的线程总数。如果现有子进程中的线程总数不能满足负载,控制进程将派生新的子进程。
参数介绍:
StartServer 服务器启动时建立的子进程数。
ServerLimit 服务器允许配置的进程数上限。这个指令和ThreadLimit结合使用配置了MaxClients最大允许配置的数值。
MinSpareThreads 最小空闲线程数。这个MPM将基于整个服务器监控空闲线程数。如果服务器中的总线程数太少,子进程将产生新的空闲线程。
MaxSpareThreads 最大空闲线程数。这个MPM将基于整个服务器监控空闲线程数。如果服务器总的线程数太多,子进程将kill掉多余的空闲线程。MaxSpareThreads的取值范围是有限制的。
ThreadLimit 每个子进程可配置的线程数上限。这个指令配置了每个子进程可配置的线程数ThreadsPerChild上限。
ThreadsPerChild 每个子进程建立的常驻的执行线程数。子进程在启动时建立这些线程后就不再建立新的线程了。
MaxRequestsPerChild 每个子进程在其生存期间允许执行的最大请求数量。到达这个限制后子进程将会结束,如果为0则永不结束( 注意,对于KeepAlive链接,只有第一个请求会被计数)。这样做的好处是:

  • 能够防止内存泄露无限进行,从而耗尽内存。
  • 給进程一个有限寿命,从而有助于当服务器负载减轻时减少活动进程的数量。

apache”惊群”现象与解决方案

无论是上面那个MPM被选择,都有一问题就是主进程先监听80端口,然后又fork出子进程。所以可以知道,fork出来的每个子进程都在监听80端口,如果这时候有请求过来就会出现所有的空闲进程都回来抢这个fd,也就是这些进程都被唤醒了,但是最终只有一个进程能够拿到这个fd进行处理,其他进程因为拿不到进程而再次进入休眠状态,这就是”惊群”现象。
apache的prefork模型下的处理方式如下如所示:

apache通过在每个accept()函数上 增加互斥锁和条件变量 来解决这个惊群问题。保证每个请求只会被一个线程刚好拿到,不会影响其他线程;
这里详细介绍下:条件变量与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用;互斥锁提供互斥机制,条件变量提供信号机制;
那么apache是如何利用条件变量和互斥锁来解决每次只有一个空闲线程被唤醒,并且处于监听者角色呢?
每次一个新的客户请求过来,正在监听的线程与该请求建立连接,并变为worker工作者线程。让出监听者角色时它同时发送信号到条件变量,并释放锁。这样在空闲(idle)状态的一个线程将被唤醒并获得锁。
也就是说:条件变量保证了其他线程在等待条件变化期间处于睡眠;互斥锁保证一次只有一个线程被唤醒
这个是参考客户/服务器程序设计范式来的,但是有一个明显的问题是prefork是多进程模型不是多线程模型,由于现在还没读过apache源码,姑且认为总体的流程和思想是对的。有机会再深入阅读回来补充。

nginx工作原理

nginx使用的是多进程模型,类似于apache的prefork,不同的是nginx的子进程个数是固定的。nginx的进程模型可以用下图来表示:

可以看到nginx进程模型是由一个mater进程和多个worker进程组成的,master进程主要用来管理worker进程,包含:接收来自外界的信号,向各worker进程发送信号,监控worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。 worker进程则是来处理请求用的。

异步非阻塞

上面提到nginx的worker进程用来处理请求,而worker的个数是有限的,当并发高的时候nginx是如何应对的呢?这里不得不提到一个概念异步非阻塞(参考UNP卷一第三版P160页的介绍)关于这个过程nginx平台初探介绍的很好,直接COPY过来:

为什么nginx可以采用异步非阻塞的方式来处理呢,或者异步非阻塞到底是怎么回事呢?我们先回到原点,看看一个请求的完整过程。首先,请求过来,要建立连接,然后再接收数据,接收数据后,再发送数据。具体到系统底层,就是读写事件,而当读写事件没有准备好时,必然不可操作,如果不用非阻塞的方式来调用,那就得阻塞调用了,事件没有准备好,那就只能等了,等事件准备好了,你再继续吧。阻塞调用会进入内核等待,cpu就会让出去给别人用了,对单线程的worker来说,显然不合适,当网络事件越多时,大家都在等待呢,cpu空闲下来没人用,cpu利用率自然上不去了,更别谈高并发了。好吧,你说加进程数,这跟apache的线程模型有什么区别,注意,别增加无谓的上下文切换。所以,在nginx里面,最忌讳阻塞的系统调用了。不要阻塞,那就非阻塞喽。非阻塞就是,事件没有准备好,马上返回EAGAIN,告诉你,事件还没准备好呢,你慌什么,过会再来吧。好吧,你过一会,再来检查一下事件,直到事件准备好了为止,在这期间,你就可以先去做其它事情,然后再来看看事件好了没。虽然不阻塞了,但你得不时地过来检查一下事件的状态,你可以做更多的事情了,但带来的开销也是不小的。所以,才会有了异步非阻塞的事件处理机制,具体到系统调用就是像select/poll/epoll/kqueue这样的系统调用。它们提供了一种机制,让你可以同时监控多个事件,调用他们是阻塞的,但可以设置超时时间,在超时时间之内,如果有事件准备好了,就返回。这种机制正好解决了我们上面的两个问题,拿epoll为例(在后面的例子中,我们多以epoll为例子,以代表这一类函数),当事件没准备好时,放到epoll里面,事件准备好了,我们就去读写,当读写返回EAGAIN时,我们将它再次加入到epoll里面。这样,只要有事件准备好了,我们就去处理它,只有当所有事件都没准备好时,才在epoll里面等着。这样,我们就可以并发处理大量的并发了,当然,这里的并发请求,是指未处理完的请求,线程只有一个,所以同时能处理的请求当然只有一个了,只是在请求间进行不断地切换而已,切换也是因为异步事件未准备好,而主动让出的。这里的切换是没有任何代价,你可以理解为循环处理多个准备好的事件,事实上就是这样的。与多线程相比,这种事件处理方式是有很大的优势的,不需要创建线程,每个请求占用的内存也很少,没有上下文切换,事件处理非常的轻量级。并发数再多也不会导致无谓的资源浪费(上下文切换)。更多的并发数,只是会占用更多的内存而已。 我之前有对连接数进行过测试,在24G内存的机器上,处理的并发请求数达到过200万。现在的网络服务器基本都采用这种方式,这也是nginx性能高效的主要原因。

所以推荐设置worker的个数为cpu的核数,在这里就很容易理解了,更多的worker数,只会导致进程来竞争cpu资源了,从而带来不必要的上下文切换。而且,nginx为了更好的利用多核特性,提供了cpu亲缘性的绑定选项,我们可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来cache的失效。

nginx”惊群”现象与解决方案

worker进程之间是平等的,每个进程,处理请求的机会也是一样的。当我们提供80端口的http服务时,一个连接请求过来,每个进程都有可能处理这个连接,怎么做到的呢?首先,每个worker进程都是从master进程fork过来,在master进程里面,先建立好需要listen的socket(listenfd)之后,然后再fork出多个worker进程。所有worker进程的listenfd会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该连接。当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了。我们可以看到,一个请求,完全由worker进程来处理,而且只在一个worker进程中处理。

因为这里主要是对比apache与nginx的原理的不同,所以更深入的探讨nginx这里先不做介绍更深入的探讨nginx这里先不做介绍,以后有机会学习nginx源码的时候再写。

参考文献

多处理模块(MPM)
Apache运行机制剖析
nginx平台初探
客户/服务器程序设计范式
“惊群”,看看nginx是怎么解决它的
web服务器nginx和apache的对比分析

PHP数组的key溢出问题

作为PHP最重要的数据类型HashTable其key值是有一定的范围的,如果设置的key值过大就会出现溢出的问题,下面根据其内部结构及实现原理详细探讨一下key值溢出问题。

下面先给出一个key溢出的例子:

1
2
3
4
5
6
7
8
<?php
$arr[1] = '1';
$arr[18446744073708551617333333333333] = '18446744073708551617333333333333';
$arr[] = 'test';
$arr[4294967296] = 'test';
$arr[9223372036854775807] = 'test';
$arr[9223372036854775808] = 'test';
var_dump($arr);

上面代码的输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
array(6) {
[1]=>
string(1) "1"
[-999799117276250112]=>
string(32) "18446744073708551617333333333333"
[2]=>
string(4) "test"
[4294967296]=>
string(4) "test"
[9223372036854775807]=>
string(4) "test"
[-9223372036854775808]=>
string(4) "test"
}

我们可以看到当key值比较小是没有问题,当key值很大时输出的值溢出了,临界点是9223372036854775807这个数字。
下面分析一下原因 。首先我们先分析一下HashTable的结构(本文分析的是php-5.5.15版本的源码),可以通过源码看一下:

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
/* file: Zend/zend_hash.h */
typedef struct bucket {
ulong h; /* Used for numeric indexing */ /*对char *key进行hash后的值,或者是用户指定的数字索引值,可能会溢出*/
uint nKeyLength; /*hash关键字的长度,如果数组索引为数字,此值为0*/
void *pData; /*指向value,一般是用户数据的副本,如果是指针数据,则指向pDataPtr*/
void *pDataPtr; /*如果是指针数据,此值会指向真正的value,同时上面pData会指向此值*/
struct bucket *pListNext; /*整个hash表的下一个元素*/
struct bucket *pListLast; /*整个hash表该元素的上一个元素*/
struct bucket *pNext; /*存放在同一个hash Bucket的下一个元素*/
struct bucket *pLast; /*同一个hash bucket的上一个元素*/
const char *arKey; /*保存当前值所对于的key字符串,这个字段只能定义在最后,实现变长结构体*/
} Bucket;

typedef struct _hashtable {
uint nTableSize; /*hash Bucket的大小,最小为8,最以2*x增长*/
uint nTableMask; /*nTableSize-1, 索引取值的优化*/
uint nNumOfElements; /*hash Bucket中当前存在的元素个数, count()函数会直接返回此值*/
ulong nNextFreeElement; /*下一个数字索引的位置*/
Bucket *pInternalPointer; /* Used for element traversal ,当前遍历的指针,foreach比for快的原因之一,这个指针指向当前激活的元素*/
Bucket *pListHead; /*存储数组头元素指针*/
Bucket *pListTail; /*存储数组尾元素指针*/
Bucket **arBuckets; /*存储hash数组*/
dtor_func_t pDestructor; /*在删除元素时执行的回调函数,用于资源的释放*/
zend_bool persistent; /*指出了Bucket内存分配的方式。如果persistent为TRUE,则使用操作系统本身的内存分配函数为Bucket分配内存,否则使用PHP的内存分配函数*/
unsigned char nApplyCount; /*标记当前hash Bucket被递归访问的次数(防止多次递归)*/
zend_bool bApplyProtection; /*标记当前hash桶允许不允许多次访问。不允许时,最多只能递归3次*/
# if ZEND_DEBUG
int inconsistent;
# endif
} HashTable;

假设我们已经对源码有了一定的了解了,我们可以知道bucket.h就是我们存储的key值,bucket.h的生成方法是根据time33算法获取的,对应到代码实现如下:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//对于字符串类型的key
ZEND_API int _zend_hash_add_or_update(HashTable *ht, const char *arKey, uint nKeyLength, void *pData, uint nDataSize, void **pDest, int flag ZEND_FILE_LINE_DC)
{
ulong h;
uint nIndex;
Bucket *p;
# ifdef ZEND_SIGNALS
TSRMLS_FETCH();
# endif

IS_CONSISTENT(ht);

ZEND_ASSERT(nKeyLength != 0);

CHECK_INIT(ht);

//算出来hash key后需要根据hashTable的长度,把nIndex限制在这个长度内(通过nTableMask)
h = zend_inline_hash_func(arKey, nKeyLength);
nIndex = h & ht->nTableMask;

p = ht->arBuckets[nIndex];
...
}
//对于数字类型的key
ZEND_API int _zend_hash_index_update_or_next_insert(HashTable *ht, ulong h, void *pData, uint nDataSize, void **pDest, int flag ZEND_FILE_LINE_DC)
{
uint nIndex;
Bucket *p;
# ifdef ZEND_SIGNALS
TSRMLS_FETCH();
# endif

IS_CONSISTENT(ht);
CHECK_INIT(ht);

/* 如果是新增元素(如$arr[] = 'hello'), 则使用nNextFreeElement值作为hash值,否则直接使用传入的key h 最为hash值 */
if (flag & HASH_NEXT_INSERT) {
h = ht->nNextFreeElement;
}
nIndex = h & ht->nTableMask;

p = ht->arBuckets[nIndex];
...
}
//字符串的hash函数
static inline ulong zend_inline_hash_func(const char *arKey, uint nKeyLength)
{
register ulong hash = 5381; //这个常量是哪儿来的?

/* variant with the hash unrolled eight times */
for (; nKeyLength >= 8; nKeyLength -= 8) {
hash = ((hash << 5) + hash) + *arKey++;
hash = ((hash << 5) + hash) + *arKey++;
hash = ((hash << 5) + hash) + *arKey++;
hash = ((hash << 5) + hash) + *arKey++;
hash = ((hash << 5) + hash) + *arKey++;
hash = ((hash << 5) + hash) + *arKey++;
hash = ((hash << 5) + hash) + *arKey++;
hash = ((hash << 5) + hash) + *arKey++;
}
switch (nKeyLength) {
case 7: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
case 6: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
case 5: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
case 4: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
case 3: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
case 2: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
case 1: hash = ((hash << 5) + hash) + *arKey++; break;
case 0: break;
EMPTY_SWITCH_DEFAULT_CASE()
}
return hash;
}

上面函数主要是插入或更新hashTable的函数,当插入的key是数字时,这个数字就是hastTable的索引值,其key值不经过hash算法,只经过nIndex = h & ht->nTableMask;来确保存储的值范围属于hastTable的范围内,所以可以看出索引值key ,与其对应的时nIndex这个值,正在存储的槽位就是nIndex这个地方。

这个key类型是ulong,也就是unsigned long类型。由于我们的机器是64位的,所以unsigned long类型的取值范围应该是0~1844674407370955161。PHP有两个预定义的变量PHP_INT_MAXPHP_INT_SIZE对于64位的机器他们的值分别是9223372036854775807和8,这恰好是hasttable所能表示key的最大值,到这里也许你会有一个疑问:为什么PHP_INT_MAX的值比key的范围不一致?
要回答这个问题首先要知道,hastTable的key输出可以是负值,这是怎么做到的呢?其实一个hashTable的hash值一定是一个正整数才行,但是输出的数和hash值只是一个对应关系,不需要都为正整数, 虽然我们定义的参数为unsigned long,其实我们却可以传一个负数,比如$arr[-1] = 'test',这时候也是和传递一个正数的处理过程是一样的。这时候h的值其实是-1的补码。再回到上面的问题,为什么PHP_INT_MAX的值比key范围不一致。当我们负值 PHP_INT_MAX时,其值是9223372036854775807,当赋值再比这个大时,输出的却是负数。这其实跟我们使用var_dump这个函数有关系, 下面代码是使用var_dump输出数组时所使用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int php_array_element_dump(zval **zv TSRMLS_DC, int num_args, va_list args, zend_hash_key *hash_key) /* {{{ */
{
int level;

level = va_arg(args, int);

if (hash_key->nKeyLength == 0) { /* numeric key */
php_printf("%*c[%ld]=>\n", level + 1, ' ', hash_key->h);
} else { /* string key */
php_printf("%*c[\"", level + 1, ' ');
PHPWRITE(hash_key->arKey, hash_key->nKeyLength - 1);
php_printf("\"]=>\n");
}
php_var_dump(zv, level + 2 TSRMLS_CC);
return 0;
}

可以看到,当key为数字时输出的格式时%ld,值是hash_key->h,这就是问题所在了,存储的是一个unsigned long,输出的却是long,当值比long大时,自然输出的就是负数了。

总结: PHP的hastTable是通过链表法实现的,按说是不会存在溢出的问题,但是其索引值表示的范围有限,当超出索引值时就会造成溢出,这个溢出只存在当索引值为数字时,输入的数字为正,输出却为负值的原因是函数参数与输出的类型不一致导致的。

mean primer

MEAN入门

MEAN简介

什么是MEAN?

根据官方文档, MEAN就是MongoDB + Express + AngularJS + Node.js的组合。那么组成MEAN的各个部分又分别是什么?

  • MongoDB: 是一个基于分布式文件存储的NoSQL数据库。具体介绍和使用方法请参考官方文档
  • Express: 是一个简洁、灵活的Node.js Web应用开发框架。其实和PHP的MVC框架作用是一样的。详细介绍参见官方文档
  • AngularJS: 前端的MVC框架,更接近于 MVVM(Model-View-ViewModel)。具体介绍参考官方文档
  • Node.js: javascript的一个解析器,提供js在服务器端的运行环境。官方网站

为什么是这个组合?

大家已经熟悉了LAMP/LNMP的开发模式,这些开发模式已经能够满足了现在web开发的绝大部分需求。而新型的MEAN开发模式则是另外一个尝试,其目的是为了解决现在开发中的一些问题,是开发更加高效。总结起来主要有以下几个优点:

  • Web服务器包含在了应用程序中,可以自动安装,部署过程得到了极大简化。
  • 从传统数据库到NoSQL再到以文档为导向的持久存储MongoDB,使用户花费在编写SQL上的时间减少,有更多的时间编写js中的映射/化简功能。还能节省大量的转换逻辑(因为MongoDB存储的时JSON对象,js可以直接用)
  • 得益于AngularJS,从传统的服务器端页面变为客户端单页面应用程序越来越方便。

以上内容参考来源:http://www.ibm.com/developerworks/cn/web/wa-mean1/

另外,任何开发模式都不是万能的,也就是没有银弹,这种开发模式可以給大家带来很多新的思想,开拓思路,对大家以后应对不同应用场景的需求是提供更多的参考。

MEAN安装

MEAN只是一个组合,可以自己单独安装配置各个模块,也有现成的集成方案,如meanjsmean.io(关于他们之间的区别可以参考stackoverflow上面的讨论)。这里我们选择的是meanjs作为开发框架。

meanjs的安装可以参考官方文档。这里需要提前介绍一下安装时用到的一些工具及安装遇到的问题和解决方案。

常用工具

  • npm: 是Node Package Manage的简称,Node.js的包管理工具,它的主要功能就是管理node包,包括:安装、卸载、更新、查看、搜索、发布等。这个类似于centos系统上的yum工具. 可以通过package.json对npm进行配置。可以访问官网查看相关文档,也可以编写自己的npm包提交上去。(安利一下我写的一个很简单的包:https://www.npmjs.com/package/hexo-tag-plantuml)
  • bower: 也是包管理工具,由twitter推出.他和npm的区别是npm针对服务端的工具进行管理,bower则是主要管理前端页面的js依赖关系。通过bower.json和.bowerrc进行配置.
  • grunt: 构建javascript的工具,可以自动的完成代码规范的检查,文件合并,文件压缩,单元测试等流程(参考这边文档grunt从入门到自定义项目模板).详细信息参考官网

安装流程

这部分上面提到的meanjs官网有详细的步骤,简单概况一下就是:

  1. 安装Node.js&npm, MongoDB, Bower, Grunt等
  2. 下载源码: git clone https://github.com/meanjs/mean.git meanjs
  3. 进入meanjs目录,执行npm install ; bower install,执行bower使用会出现下面的提示:

    1
    2
    3
    4
    5
    6
    7
    Since bower is a user command, there is no need to execute it with superuser permissions.
    If you're having permission errors when using bower without sudo, please spend a few minutes learning more about how your system should work and make any necessary repairs.

    http://www.joyent.com/blog/installing-node-and-npm
    https://gist.github.com/isaacs/579814

    You can however run a command with sudo using --allow-root option

    需要通过bower install --allow-root命令来执行安装。

只需要这些,一个完成的网站就建成了。meanjs自带了一个博客登陆体系和博客浏览发布的功能。
在根目录下运行grunt命令就可以启动服务器了,默认的端口是3000,我们可以通过ip:3000的方式来访问这个网站。

meanjs结构简介

首先进入根目录可以看到如下的文件内容:

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
[@tc_214_162 meanjs]# tree -aL 1
.
├── app #后端MVC的内容目录
├── bower.json #bower配置包管理的文件
├── .bowerrc #配置安装路径
├── config #相关配置目录
├── .csslintrc
├── Dockerfile
├── .editorconfig
├── fig.yml
├── .git
├── .gitignore
├── gruntfile.js #grunt相关配置
├── .jshintrc
├── karma.conf.js
├── LICENSE.md
├── node_modules #node模块的安装目录
├── package.json #npm包管理配置
├── Procfile
├── public #前端内容
├── README.md
├── scripts #独立脚本目录
├── server.js #服务运行的入口文件
├── .slugignore
└── .travis.yml

后端MVC的主要结构如下:

1
2
3
4
5
6
app/
├── controllers #C层
├── models #M层
├── routes #路由规则
├── tests
└── views #V层

前端主要结构如下:

1
2
3
4
5
6
7
public/
├── application.js #应用入口
├── config.js #应用配置
├── humans.txt
├── lib #angular相关库文件
├── modules #angular不同模块
└── robots.txt

angular的模块结构如下:

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
public/modules/
├── articles
│   ├── articles.client.module.js
│   ├── config
│   ├── controllers #angular的C
│   ├── services #angular的服务层
│   ├── tests
│   └── views #V层
├── core
│   ├── config
│   ├── controllers
│   ├── core.client.module.js
│   ├── css
│   ├── img
│   ├── services
│   ├── tests
│   └── views
└── users
├── config
├── controllers
├── css
├── img
├── services
├── tests
├── users.client.module.js
└── views

要相对上面的结构有清晰的了解,必须对熟悉各个模块的用法,还要了解一个页面从访问到展现的流程是怎么样。 下面通过一个页面的访问流程来对整个架构的工作流程有一个大概的认识。

打开一个页面的流程

为了便于我们假设你已经注册登录并创建了几篇文章,下面我们就依据对文章列表页的打开流程进行介绍。

  1. 首先通过menu进入文章列表页:http://localhost:3000/#!/articles我们可以看到文章的列表。通过观察这个url可以看出,其实#是一个锚点,后台的部分只是hash参数,前面的才是真正的url,也就是我们其实访问的是根目录,通过访问日志也可以看出来:

    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
    35
    36
    37
    GET / 200 26.246 ms - -
    GET /modules/core/css/core.css 200 31.710 ms - 354
    GET /modules/users/css/users.css 200 26.138 ms - 211
    GET /lib/bootstrap/dist/css/bootstrap-theme.css 200 41.393 ms - -
    GET /lib/angular-resource/angular-resource.js 200 15.583 ms - -
    GET /lib/bootstrap/dist/css/bootstrap.css 200 81.453 ms - -
    GET /lib/angular-animate/angular-animate.js 200 36.241 ms - -
    GET /lib/angular-ui-utils/ui-utils.js 200 22.724 ms - -
    GET /lib/angular-bootstrap/ui-bootstrap-tpls.js 200 28.636 ms - -
    GET /lib/angular-ui-router/release/angular-ui-router.js 200 44.805 ms - -
    GET /config.js 200 24.392 ms - 791
    GET /application.js 200 37.393 ms - 1016
    GET /modules/articles/articles.client.module.js 200 30.888 ms - 133
    GET /modules/core/core.client.module.js 200 24.737 ms - 129
    GET /modules/users/users.client.module.js 200 18.616 ms - 129
    GET /modules/articles/config/articles.client.config.js 200 13.114 ms - 389
    GET /lib/angular/angular.js 200 161.376 ms - -
    GET /modules/articles/config/articles.client.routes.js 200 36.936 ms - 700
    GET /modules/articles/services/articles.client.service.js 200 25.093 ms - 295
    GET /modules/core/config/core.client.routes.js 200 19.327 ms - 384
    GET /modules/core/controllers/header.client.controller.js 200 13.791 ms - 495
    GET /modules/articles/controllers/articles.client.controller.js 200 34.063 ms - -
    GET /modules/core/controllers/home.client.controller.js 200 43.584 ms - 224
    GET /modules/users/config/users.client.config.js 200 31.473 ms - 708
    GET /modules/core/services/menus.client.service.js 200 39.634 ms - -
    GET /modules/users/config/users.client.routes.js 200 27.816 ms - -
    GET /modules/users/controllers/authentication.client.controller.js 200 22.157 ms - -
    GET /modules/users/controllers/password.client.controller.js 200 17.350 ms - -
    GET /modules/users/controllers/settings.client.controller.js 200 21.926 ms - -
    GET /modules/users/services/authentication.client.service.js 200 15.335 ms - 202
    GET /modules/users/services/users.client.service.js 200 9.742 ms - 244
    GET /lib/bootstrap/dist/css/bootstrap-theme.css.map 200 8.378 ms - 47721
    GET /lib/bootstrap/dist/css/bootstrap.css.map 200 49.468 ms - 390518
    GET /modules/articles/views/list-articles.client.view.html 200 8.756 ms - 819
    GET /modules/core/views/header.client.view.html 200 17.955 ms - -
    GET /articles 200 24.296 ms - 407
    GET /modules/core/img/brand/favicon.ico 200 12.350 ms - 32038
  2. 请求到达之后首先会根据app/routes目录下面的路由规则进行匹配,app/routes/core.server.routes.js匹配到了这个路由,其内容入下:

    1
    2
    3
    4
    5
    module.exports = function(app) {
    // Root routing
    var core = require('../../app/controllers/core.server.controller');
    app.route('/').get(core.index);
    };

可以看出这个请求匹配到后交给了core.index进行处理,app/controllers/core.server.controller.js内容如下:

1
2
3
4
5
6
exports.index = function(req, res) {
res.render('index', {
user: req.user || null,
request: req
});
};

index函数并接收到请求后对'index'模板进行了渲染,模板文件app/views/index.server.view.html内容如下:

1
2
3
4
{% extends 'layout.server.view.html' %}
{% block content %}
<section data-ui-view></section>
{% endblock %}

这个模板继承了layout模板,并且重写了content的block内容。
我们再看一下被继承的模板app/views/layout.server.view.html其主要内容入如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<header data-ng-include="'/modules/core/views/header.client.view.html'" class="navbar navbar-fixed-top navbar-inverse"></header>
<section class="content">
<section class="container">
{% block content %}{% endblock %}
</section>
</section>

<!--Embedding The User Object-->
<script type="text/javascript">
var user = {{ user | json | safe }};
</script>

<!--Application JavaScript Files-->
{% for jsFile in jsFiles %}
<script type="text/javascript" src="{{jsFile}}"></script>
{% endfor %}

{% if process.env.NODE_ENV === 'development' %}
<!--Livereload script rendered -->
<script type="text/javascript" src="http://{{request.hostname}}:35729/livereload.js"></script>
{% endif %}

上面的主要功是加载页面所需的js文件,这些文件的配置都在config里。根据上面的访问日志可以看出主要public下的js文件都被加载了。
到这里服务端的工作已经完成一半了。

  1. 前端js加载后,angular就开始发挥作用了。angular也是一套MVC框架。在进入流程之前我们再次观察一下URL。我们打开主页时会发现url变成了http://localhost:3000/#!/。为什么url会自动加上这部分内容,又为什么需要加这部分内容呢?
    根据官方文档$locaiton Hashbang and HTML5 Modes部分的介绍,这应该是和浏览器对history支持的兼容性有关系。具体介绍可以看文档。 angualr的路由匹配规则其实是从#!之后开始的。再回到本页,public/modules/articles/config/articles.client.routes.js匹配到了当前规则代码如下:
    1
    2
    3
    4
    state('listArticles', {
    url: '/articles',
    templateUrl: 'modules/articles/views/list-articles.client.view.html'
    }).

可以看出当匹配时,angualr会自动加载templateUrl到页面片上来,在观察一下被加载的页面片public/modules/articles/views/list-articles.client.view.html内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<section data-ng-controller="ArticlesController" data-ng-init="find()">
<div class="page-header">
<h1>Articles</h1>
</div>
<div class="list-group">
<a data-ng-repeat="article in articles" data-ng-href="#!/articles/{{article._id}}" class="list-group-item">
<small class="list-group-item-text">
Posted on
<span data-ng-bind="article.created | date:'mediumDate'"></span>
by
<span data-ng-bind="article.user.displayName"></span>
</small>
<h4 class="list-group-item-heading" data-ng-bind="article.title"></h4>
<p class="list-group-item-text" data-ng-bind="article.content"></p>
</a>
</div>
<div class="alert alert-warning text-center" data-ng-if="articles.$resolved && !articles.length">
No articles yet, why don't you <a href="/#!/articles/create">create one</a>?
</div>
</section>

页面加载后执行find()函数,这个函数在控制层文件public/modules/articles/controllers/articles.client.controller.js里可以看到:

1
2
3
$scope.find = function() {
$scope.articles = Articles.query();
};

这个函数调用了Articels.query()方法,这个方法是Angualr的一个注册的service(参考文档Services), 位于文件public/modules/articles/services/articles.client.service.js中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
//Articles service used for communicating with the articles REST endpoints
angular.module('articles').factory('Articles', ['$resource',
function($resource) {
return $resource('articles/:articleId', {
articleId: '@_id'
}, {
update: {
method: 'PUT'
}
});
}
]);

看到并没有定义query方法,是因为这方法是$resource默认的(参考$resource),代码中articleId是参数,本次查询并没有传递参数,所以实际访问的url是/articles,这个是一个RESTful接口,返回的结果赋值給$scope.articles,就可以在前端正常展示文章列表了。

  1. 第3步的最后提到了访问/articles接口,这个接口的作用就是从数据库取数据然后在返回到前端。当访问接口时,服务器接到请求,文件app/routes/articles.server.routes.js匹配到路由规则:
    1
    2
    3
    app.route('/articles')
    .get(articles.list)
    .post(users.requiresLogin, articles.create);

由于是get方法,所以需求转给了articles.list方法进行处理,app/controllers/articles.server.controller.jslist方法代码如下:

1
2
3
4
5
6
7
8
9
10
11
exports.list = function(req, res) {
Article.find().sort('-created').populate('user', 'displayName').exec(function(err, articles) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(articles);
}
});
};

其中Articles这个对象是这个文件前面定义的:

1
Article = mongoose.model('Article')

其中mongoose是MongoDB的一个js封装库,这个module是在app/models/article.server.model.js下定义并注册的,代码如下:

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
var mongoose = require('mongoose'),
Schema = mongoose.Schema;

/**
* Article Schema
*/
var ArticleSchema = new Schema({
created: {
type: Date,
default: Date.now
},
title: {
type: String,
default: '',
trim: true,
required: 'Title cannot be blank'
},
content: {
type: String,
default: '',
trim: true
},
user: {
type: Schema.ObjectId,
ref: 'User'
}
});

mongoose.model('Article', ArticleSchema);

到这里,整个页面从访问到处理到返回数据和渲染页面的流程就完毕了。

HTTP缓存机制

HTTP缓存是web开发中经常碰到的问题,但是之前虽然看过,那时候web开发刚开始做,概念有点儿模糊不清,所以重读了《HTTP权威指南》的缓存部分。这里对自己的理解做一下记录。

为什么要缓存

为什么要做缓存?如果你看到了这篇文章,其实心中肯定有了一个作缓存的需求和目标。缓存的目的无非就是提高用户体验,节省资源两方面。

  • 提高用户体验:
    从提高用户体验来讲,就是用户能够看到反馈的时间越短越好,速度越快越好。那么如何才能提高速度呢?当然是数据和用户越近越好,最好就在本地;还有就是网速越快越好,最好连接网络就能访问。我们都知道本地应用是最快的,因为所有的都在你的电脑里了,不需要再费劲去网上下载了。缓存的目的就是为了尽量完成这个工作而设计的。
  • 节省资源:
    这个怎么说呢,如果你要访问一个网站,第一次你肯定没有这些数据,但是如果访问后所有的数据都保存在了本地,或者离你很近的地方,就省得在网络的海洋漫游过来了,这当然就节省了不少流量,当然你的计费方式是按带宽来的,多少流量无所谓,可是对于提供网站服务的供应商来说这些都是白花花的银子啊,能剩就剩,节约每一个铜板嘛。

    缓存的机制

    这里只介绍基于HTTP协议的缓存。也许做web开发的都听过expires,cache-control,If-Modified-Since等缓存控制的HTTP头,这些眼花缭乱的头真让让人头晕啊,到底他们之间是什么关系,写了那么多,但是缓存是不是生效了?怎么生效的啊?带着这些疑问我也仔细阅读了一下书里的内容。其实一幅图可以说明很多问题:

通过这个流程图我们可以看出有三个方向:

  1. 未找到缓存(黑色线)
    当没有找到缓存时,说明我们本地并没有这些数据,这种情况一般发生在我们首次访问网站,或者以前访问过,但是清除过缓存后。这时浏览器没有这些数据,浏览器就会先访问服务器,然后把服务器上的内容取回来,内容取回来以后,就要根据情况来决定是否要保留到缓存中了。这个地方与cache-control字段的内容有关系。
  2. 缓存未过期(蓝色线)
    缓存未过期,指的是本地缓存没有过期,不需要访问服务器了,直接就可以拿本地的缓存作为响应在本地使用了。这样节省了不少网络成本,提高了用户体验过。这个内容跟expires字段和cache-control有密切的联系。
  3. 缓存已过期(红色线)
    缓存过期,是根据expirescache-control来判断的,当满足过期的条件时,会向服务器发送请求,发送的请求一般都会进行一个验证,目的是虽然缓存文档过期了,但是文档内容不一定会有什么改变,所以服务器返回的也许是一个新的文档,这时候的HTTP状态码是200,或者返回的只是一个最新的时间戳和304状态码。

下面就针对这些情况和其对应的字段及字段的优先级进行性说明:
第一优先级cache-control(expires)。下面的表是从高到低的顺序优先级递减。

字段 备注
Cache-Control no-store 不存储缓存数据,禁止对相应进行复制
Cache-Control no-cache 可以存储在本地缓存区中,但是必须进行新鲜度再验证满足之后才能使用
Pragma no-cache 用在HTTP/1.0协议中,与Cache-control一样
Cache-Control must-revalidate 表示必须进行新鲜度的再验证之后才能使用,与no-cache的区别是,这个是在过期之后才会进行强制校验,一般用在没有使用cache-control等字段明确规定的缓存时,这时会自动使用缓存策略,如果不想自动缓存,则使用这个字段值(实际测试跟no-cache没什么区别)
Cache-Control max-age 相对存活时间,相对与Last-Modified的时间,如果当前时间与Last时间只差小于这个值,则不用访问服务器,直接使用缓存,否者要进行新鲜度校验
Expires date 旧版本的使用方式,date是具体的过期时间,当没有cacche-control时使用

上面这些主要是在本地进行的,主要作用就是决定用本地缓存还是用远程服务器的资源。下面有要介绍的是新鲜度校验阶段要用到的HTTP控制字段。

字段 备注
If-Modified-Science date 初始值与Last-Modified的值一样,当请求服务器时判断文件的Last-Modified,如果比现在的时间晚,证明做过修改,需要冲重新请求文档,返回的Last-Modified为最新的时间,下次次请求时,If-Modified-Science的值会更新到与Last-Modified一致,然后在发送请求.如果时间与现在的一样,证明那个没有更新,直接返回304状态码
Last-Modified date 表示的就是文档在服务器上的最后更新时间
If-None-Match 版本号 前面的If-Modified-Science有一个缺点就是虽然文件的更新时间变了,但是内容并没有改变,也会重新发送文档,为了减少网络传输,这里就需要If-None-Match来判断了。主要是判断版本号与当前etag不一致时,更新文档,当Etag一致时只需更新文件更新时间就可以了
Etag 版本号 标识当前文档内容

优先级: Etag > Last-Modified 也就是说如果有Etag,就用If-None-Match来验证,否者才能用If-Modified-Science验证.

基本上常用的HTTP缓存功能就是这些。下面介绍一下在Nginx服务器先如何配置及前端页面如何访问和查看。

怎么使用

由于现在Nginx用的比较广泛,我用的也是Nginx,所以这里只介绍nginx的配置,其他服务器请google之。
首先是expirescache-control的配置,参看Nginx官方文档

从jekyll到hexo

最近把博客从jekyll迁移到了hexo,告一段落后写个总结,給后来的人一个参考。

为什么要迁移

关于为什么把博客从 jekyll迁移到hexo,其原因其实跟这两个的优劣没有任何关系。每个人都有自己爱好,只要你用好了,其实本质上没什么差别,而我的原因则是因为最近团队前端人员极度缺人,只好让后台开发的人员也开始写js代码,不过自从写了js代码后我才发现js的天地是多么的广阔。通过写js代码我了解了的js语言的特性,以及前端的各种框架,比如require.js, grunt, angular,express等等,以及Node.js开发平台和包管理工具npm.最后当然还有本博客平台hexo
说回原因,其实就是因为我最近在学习js,而且对ruby不熟悉,所有我把博客迁移到了js的博客框架hexo。

迁移过程中有哪些坑

根据hexo官方文档的迁移说明,其实很简单,只不过是把之前的_posts/*下的md文件都copy到hexo的_post下,然后把new_post_name改为new_post_name: :year-:month-:day-:title.md。看似很简单,但是实际情况却比这个要复杂些,主要是之前jekyll写博客的时候md文件会有很多内容在hexo平台不被识别,特别是jekyll用过某些插件后,hexo更不认识了。

问题一: 高亮代码标签不兼容

jekyll中代码的高亮的tag是这样的:

1
2
{% highlight %}
{% endhighlight %}

而hexo中,高亮代码的tag是这样的:

1
2
{% codeblock %}
{% endcodeblock %}

这个问题有两种解决方案,一种是用sed命令替换所有的jekyll高亮tag;第二种是自己写一个解析jekyll的tag插件.显然第一种方案更合适一些,所有我替换了所有的标签。 但是如果你想挑战自己,写一个插件也不错,可以copy hexo的codeblock插件代码,直接用到jekyll的tag上。

问题二: plantuml不支持

为了画流程图我用了开源的流程图解决方案plantuml。但是jekyll其实原生也是不支持plantuml的,可以通过插件来解决,关于jekyll如何使用 plantuml的方法我也写过一篇介绍:jekyll添加plantuml模块.仍在使用jekyll的同学可以参考一下。
显然hexo原生也是不支持plantuml的,但是我一时也找不到支持plantuml的插件,所以如果博客一定要支持流程图摆在面前的选择只有两条路了:

  1. 先画好流程图,然后博客中引入。
  2. 自己写一个支持plantuml的插件。
    关于第一种解决方案,首先是麻烦,你需要提前生成各种图片,然后再引入进来,这会打断写博客的流程,同时也让人觉得很low。第二种解决方案,我研究了一下,发现并不难实现。下面就是plantuml插件实现的过程。

plantuml解析的两种方案:

  1. 首先plantuml提供了java包,可以通过命令行把plantuml文件内容生成svg或png等格式的图片。
  2. plantuml还提供了一个很好的平台,可以吧plantuml编码后直接作为参数访问,比如:http://plantuml.com:80/plantuml/png/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000
    这种方法是实时计算出来后在plantuml平台生成图片后返回过来的。
    下面对比这两种方法:
方法&特点 方法一:本地生成 方法二 :同步生成
优点 页面展示速度快 本地不需要保留任何图片
缺点 本地需要维护图片&本地需要提前生产图片 页面图片展示比较慢

再结合我们写博客的目的,是为了快速方面的使用,所以为了不维护一堆图片,考虑图片的名字等细节问题,我们干脆全放到web端,而且自己的服务器不一定比plantuml的快,所以性能的考虑可以忽略。最终我选择了同步生成的方式来解析plantuml标签。

hexo要添加tag的解析插件,可以参考hexo api文档标签插件的介绍。其实就是注册插件,然后把tag里的内容当成参数传给tag处理函数,然后返回结果在页面上渲染。我们的这个hexo-tag-plantuml插件主要目的是把tag里的内容转化为plantuml的图片地址。正好plantuml提供了内容转化为图片的js API,我其实就是把这些代码搬过来,然后根据hexo的tag插件写法实现的。具体源代码及用法请参考hexo-tag-plantuml插件。

其他问题

有问题时再更新……

php内核系列1:PHP程序运行的入口

最近看了深入理解PHP内核这本书,并结合源码进行阅读。发现刚开始入门的时候很难,往往看了有点儿不知所云,因为虽然说了运行的周期,过程等,但是始终不明白的是:PHP程度的入口到底在哪里?不知道入口就没有起步,我始终走不下去。直到后来看Apache模块这章内容的时候才恍然大悟。这里就PHP的入口问题进行总结一下。

PHP入口

首先回顾一下,我们使用PHP的方式有哪些?最常用的无非两种情况:

  • 与WEB服务器结合,在浏览器下运行。
  • 命令行下直接运行PHP脚本文件。

这两种方式的入口分别是什么呢?其实总结一下,就是下面三种入口。这点还可以从我们的PHP编译后的二进制文件找打足丝马迹,编译完PHP后我们会发现有一下几种二进制文件php, php-cgi等,没错,这些二进制文件就是PHP程序的入口。

CLI

我们知道PHP脚本可以直接在命令行下运行,像下面这样:

1
$php test.php

其中php是PHP源代码编译后的二进制文件,当命令行执行php脚本时,其实是执行了源码sapi/cli/php_cli.c下的main函数, 这个函数大概内容如下:
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# ifdef PHP_CLI_WIN32_NO_CONSOLE
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
# else
int main(int argc, char *argv[])
# endif
{
# ifdef ZTS
void ***tsrm_ls;
# endif
# ifdef PHP_CLI_WIN32_NO_CONSOLE
int argc = __argc;
char **argv = __argv;
# endif

int c;
int exit_status = SUCCESS;
int module_started = 0, sapi_started = 0;
char *php_optarg = NULL;
int php_optind = 1, use_extended_info = 0;
char *ini_path_override = NULL;
char *ini_entries = NULL;
int ini_entries_len = 0;
int ini_ignore = 0;
sapi_module_struct *sapi_module = &cli_sapi_module; //sapi结构

/*
* Do not move this initialization. It needs to happen before argv is used
* in any way.
*/
argv = save_ps_args(argc, argv);

...
...
//根据参数选择处理程序
while ((c = php_getopt(argc, argv, OPTIONS, &php_optarg, &php_optind, 0, 2))!=-1) {
switch (c) {
case 'c':
if (ini_path_override) {
free(ini_path_override);
}
ini_path_override = strdup(php_optarg);
break;
case 'n':
ini_ignore = 1;
break;
case 'd': {
/* define ini entries on command line */
int len = strlen(php_optarg);
char *val;
...
...

/*
* Do not move this de-initialization. It needs to happen right before
* exiting.
*/
cleanup_ps_args(argv);
exit(exit_status);
}

具体这个函数是怎么执行的,我们先不关心,我们只是知道这个就是命令行下PHP代码执行的入口程序。

Apache Module

Apache Module是按照Apache的编码规范,给Apapche写的module, 这些module在Apache服务器启动时会加载进来,并且加载进来的module可以执行执行的代码。关于这点Apache模块这里讲的更清楚。Apahce启动时其实就加载了mod_php5这个module,然后把请求传递给这个module进行处理,这就是PHP程序的入口。在sapi下我们可以发现一共有几个跟apache相关的目录apache, apache2filter, apache2handler, apache_hooks。这些都是apache相关的module,都会被apache调用。

CGI

上面说了apache调用php的方式,但是还有一种常见的方式就是cgi。我们知道cgi是通用网关接口,其实就是一种通信的协议。我们经常用的另外一个服务器nginx并没有专门的php module,那么这种服务器是怎么调用PHP处理程序的呢?就是用的CGI。
我们在使用nginx的会配置cgi,最常见的就是fastcgi_pass 127.0.0.1:9000;这个配置。其实nginx收到请求之后就会根据配置把请求都转发到了这个端口上,然后等处理完了返回时再交给nginx处理(epoll方式,参见nginx的网络模型)。其实真正调用php脚本的是cgi。php编译后会有一个php-cgi的二进制文件,这个就是php的cgi入口。但是一般情况下我们不是直接使用这个,主要是因为处理的请求很多,一个进程肯定处理不过来,需要对php-cgi进程进行管理财型,所以一般情况下我们使用的是spawn-cgiphp-fpm等,这些程序最大的作用就是管理cgi进程,关于他们的不同,这里就不说了。

总结

开篇讲的就是PHP的入口,知道这个等于是入门了,以后阅读源码就是顺藤摸瓜了。根据PHP源码,我们可以看到,这些入口都放在一个sapi目录下,其实PHP的所有代码的执行都是通过SAPI(Server Application Programming Interface指的是PHP具体应用的编程接口)这个抽象层来完成的。

设计模式-观察者模式

在学习js事件的时候,说js事件其实就是观察者模式,所以打算先把观察者模式了解一下,这样就能更好的理解事件机制了。

由于观察者模式,其实比较好理解,这里就举wikipedia的例子
观察者模式的UML类图如下:

分析一下这个类图。主要分为两种类。一个是观察者类(Observer), 一个是被观察者类(Subject), 被观察者类有三种方法分别是:

  • addObserver(): 添加观察者到被观察者类中
  • deleteObserver(): 从被观察者类中删除观察者
  • notifyObserver(): 通知已经注册过的观察者

这里ConcreteSubjectAConcreteSubjectB是继承Subject的。当有多个被观察者类时可以用这种方式,当只有一个被观察者类时,可以直接用Subject进行实例话,不需要派生类。
Observer是观察者的抽象类。所有的观察者都继承自这个类。每个观察者类都要实现一个notify()函数。这个函数是当被观察者发生变化需要通知观察者的时候调用的,也就是notifyObserver()调用的。

代码事例:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
class Student implements SplObserver{
protected $tipo = "Student";
private $nome;
private $endereco;
private $telefone;
private $email;
private $_classes = array();

public function GET_tipo() {
return $this->tipo;
}

public function GET_nome() {
return $this->nome;
}

public function GET_email() {
return $this->email;
}

public function GET_telefone() {
return $this->nome;
}

function __construct($nome) {
$this->nome = $nome;
}

public function update(SplSubject $object){
$object->SET_log("Comes from ".$this->nome.": I'm a student of ".$object->GET_materia());
}

}

class Teacher implements SplObserver{
protected $tipo = "Teacher";
private $nome;
private $endereco;
private $telefone;
private $email;
private $_classes = array();

public function GET_tipo() {
return $this->tipo;
}

public function GET_nome() {
return $this->nome;
}

public function GET_email() {
return $this->email;
}

public function GET_telefone() {
return $this->nome;
}

function __construct($nome) {
$this->nome = $nome;
}

public function update(SplSubject $object){
$object->SET_log("Comes from ".$this->nome.": I teach in ".$object->GET_materia());
}
}
class Subject implements SplSubject {
private $nome_materia;
private $_observadores = array();
private $_log = array();

public function GET_materia() {
return $this->nome_materia;
}

function SET_log($valor) {
$this->_log[] = $valor ;
}
function GET_log() {
return $this->_log;
}

function __construct($nome) {
$this->nome_materia = $nome;
$this->_log[] = " Subject $nome was included";
}
/* Adiciona um observador */
public function attach(SplObserver $classes) {
$this->_classes[] = $classes;
$this->_log[] = " The ".$classes->GET_tipo()." ".$classes->GET_nome()." was included";
}

/* Remove um observador */
public function detach(SplObserver $classes) {
foreach ($this->_classes as $key => $obj) {
if ($obj == $classes) {
unset($this->_classes[$key]);
$this->_log[] = " The ".$classes->GET_tipo()." ".$classes->GET_nome()." was removed";
}
}
}

/* Notifica os observadores */
public function notify(){
foreach ($this->_classes as $classes) {
$classes->update($this);
}
}
}

$subject = new Subject("Math");
$marcus = new Teacher("Marcus Brasizza");
$rafael = new Student("Rafael");
$vinicius = new Student("Vinicius");

// Include observers in the math Subject
$subject->attach($rafael);
$subject->attach($vinicius);
$subject->attach($marcus);

$subject2 = new Subject("English");
$renato = new Teacher("Renato");
$fabio = new Student("Fabio");
$tiago = new Student("tiago");

// Include observers in the english Subject
$subject2->attach($renato);
$subject2->attach($vinicius);
$subject2->attach($fabio);
$subject2->attach($tiago);

// Remove the instance "Rafael from subject"
$subject->detach($rafael);

// Notify both subjects
$subject->notify();
$subject2->notify();

echo "First Subject <br />";
echo "<pre>";
print_r($subject->GET_log());
echo "</pre>";
echo "<hr>";
echo "Second Subject <br />";
echo "<pre>";
print_r($subject2->GET_log());
echo "</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

//First Subject

Array (

[0] => Subject Math was included
[1] => The Student Rafael was included
[2] => The Student Vinicius was included
[3] => The Teacher Marcus Brasizza was included
[4] => The Student Rafael was removed
[5] => Comes from Vinicius: I'm a student of Math
[6] => Comes from Marcus Brasizza: I teach in Math
)



//Second Subject

Array (

[0] => Subject English was included
[1] => The Teacher Renato was included
[2] => The Student Vinicius was included
[3] => The Student Fabio was included
[4] => The Student tiago was included
[5] => Comes from Renato: I teach in English
[6] => Comes from Vinicius: I'm a student of English
[7] => Comes from Fabio: I'm a student of English
[8] => Comes from tiago: I'm a student of English
)

结合代码我们再具体分析。上面的代码中有SplObserver是观察者基类,观察者类StudentTeacher都继承它。被观察者Subject继承自SplSubject基类,实现了三个函数:添加观察者的attach、删除观察者的detach及通知观察者的notify。我们还注意到每个观察者类都有一个update函数,这个函数是被观察者通知观察者时的接口,由notify函数调用并传递被观察者的信息到观察者。这就是一个完整的观察者模式的代码实例。

我们再看如何使用:
开始的时候初始化观察者和被观察者类:

1
2
3
4
$subject = new Subject("Math");
$marcus = new Teacher("Marcus Brasizza");
$rafael = new Student("Rafael");
$vinicius = new Student("Vinicius");

然后把观察者注册到被观察者中:

1
2
3
$subject->attach($rafael);
$subject->attach($vinicius);
$subject->attach($marcus);

接着又把$rafael从观察者类中删除了:

1
$subject->detach($rafael);

最后由被观察者通知注册的观察者做出改变:

1
$subject->notify();

所以最后的结果如下:

1
2
3
4
5
6
7
8
9
10
Array (

[0] => Subject Math was included
[1] => The Student Rafael was included
[2] => The Student Vinicius was included
[3] => The Teacher Marcus Brasizza was included
[4] => The Student Rafael was removed
[5] => Comes from Vinicius: I'm a student of Math
[6] => Comes from Marcus Brasizza: I teach in Math
)

第二个分析方法相同,不在赘述了。