配合nghttpx修改nginx配置

上次介绍了用nghttp2软件包中的nghttpx反向代理让普通的网站支持HTTP2协议。在安装配置完成后实际使用的过程中遇到了一些问题,本文总结了一下解决方案。

开始遇到的两个问题是:经过nghttpx转发之后,request header中的host字段没有了;还有PHP脚本无法得到远端的IP地址。解决第一个问题的方法是在nghttpx的启动参数中加入--no-host-rewrite来通知nghttpx不要修改host标头;第二个问题可以加入--add-x-forwarded-for参数来通知nghttpx加入代理转发IP记录。

修改后的启动命令如下:

# nghttpx -D -f *,443 -b 127.0.0.1,80 --add-x-forwarded-for \
 --pid-file=/path/to/pidfile.pid --no-host-rewrite --errorlog-syslog \
 /path/to/private.key /path/to/certification.crt

加入-D参数后nghttpx会以守护进程方式运行,加入--errorlog-syslog可以把错误信息输出到系统的syslog中,而不再使用STDOUT作为错误日志输出;加入--pid-file之后可以输出一个nghttpx的进程号文件,方便制作成initrc脚本用于管理。

后来又遇到一个问题是,nginx向PHP的fastcgi服务器发送参数时,不知道前面使用了https协议,导致许多PHP项目自动侦测https的代码无法正常工作。这个问题可以修改nginx的配置来解决:

因为nghttpx会在转发的请求头中加入一个X-Forwarded-Proto标头,所以我们可以在nginx.conf中加入一个if语句来确认是否使用了https:

# under server or location scope
if ( $http_x_forwarded_proto ~* https ) {
    set $var_https on;
}

然后在fastcgi.conf中修改传给PHP解释器的参数:

#fastcgi_param  HTTPS    $https if_not_empty;
fastcgi_param  HTTPS    $var_https if_not_empty;

把原来的https判断方法注释掉,用新的$var_https参数。如果使用的不是PHP,而是Python,也可以用同样的方式修改uwsgi_params配置文件。

PS:另外还有一种方法是判断请求标头via中是否包含nghttpx字样,如果包含就认为是https访问的,方法和前面的相似就不再赘述。

让你的网站支持HTTP2

HTTP2和SPDY

SPDY是Google的研究人员为了加速Web浏览体验搞出来的一个试验性协议,旨在降低网络延迟。据这帮研究人员报告1,使用SPDY协议的网站平均等待时间只有用HTTP/1.1协议网站的几分之一。SPDY协议发展到2.0版的时候突然声名雀起,几乎每个浏览器都开始支持它——没错,连最顽固的微软IE浏览器也支持了2

然而就在这个SPDY一统天下的大好时代,Google宣布放弃SPDY协议3。当然Google只是放弃SPDY这个名字而已,SPDY协议中绝大部分的优化手段都被Google写进了HTTP2标准草案里。如今HTTP2协议定稿在即,Google才大方出来宣言放弃SPDY,力争推动各厂尽快切换到HTTP2上。

虽然HTTP2在浏览器上的普及势如破竹比当年SPDY不惶多让,可惜主流的开源Web服务器跟进并不迅速,Apache的nginx目前都只有SPDY模块的支持。还好spdylay4的开发团队动作迅速,马上提供了HTTP2的实现,让我们可以不必多加改动地把服务器部署上HTTP2。

下面介绍一下怎么让自己的网站同时支持SPDY和HTTP2协议。

准备工作

因为SPDY只能支持SSL/TLS加密的连接,我假设网站已经有证书和密钥。如果网站还没有这两样东西,请按这里

我在使用的是Ubuntu 14.04 LTS 64bit 系统,因为系统没有自带spdylay的软件包,所以需要从源码编译。如果服务器只想支持HTTP2而不想支持SPDY协议,也可以不安装spdylay。直接开始nghttp2的安装。

首先要安装编译环境和spdylay的依赖库:

# apt-get install build-essential pkg-config zlib1g-dev libssl-dev libxml2-dev libevent-dev libevent-openssl-2.0-5

再去spdylay的网站下载spdylay的源代码包:

# wget 'https://github.com/tatsuhiro-t/spdylay/releases/download/v1.3.2/spdylay-1.3.2.tar.xz'

安装spdylay

解包和配置:

# tar xpf spdylay-1.3.2.tar.xz
# cd spdylay-1.3.2
# ./configure

配置完成之后会显示一个配置表:

Version:        1.3.2 shared 9:0:2
Host type:      x86_64-unknown-linux-gnu
Install prefix: /usr/local
C compiler:     gcc
CFLAGS:         -g -O2
LDFLAGS:
LIBS:           -lz
CPPFLAGS:
C preprocessor: gcc -E
C++ compiler:   g++
CXXFLAGS:       -g -O2
CXXCPP:         g++ -E
Library types:  Shared=yes, Static=yes
CUnit:          no
OpenSSL:        yes
Libxml2:        yes
Libevent(SSL):  yes
Src:            yes
Examples:       yes

请注意Libevent(SSL)务必要显示为yes,不然编译出来的软件包里不会包含spdy的反向代理。检查好了就可以编译和安装:

# make && make install
# ldconfig

因为新安装的lib不会马上被系统读到,导致运行spdylay程序出现.so文件找不到的情况,所以需要运行ldconfig让系统重新载入动态链接库。

此时可以运行一下SPDY的反向代理程序,看看是否工作良好:

# shrpx
Usage: shrpx [-Dh] [-s|--client|-p] [-b <HOST,PORT>]
             [-f <HOST,PORT>] [-n <CORES>] [-c <NUM>] [-L <LEVEL>]
             [OPTIONS...] [<PRIVATE_KEY> <CERT>]

A reverse proxy for SPDY/HTTPS.

[FATAL] Too few arguments
       (shrpx.cc:1167)

出现这个提示说明spdylay已经安装正常。

安装nghttp2

先要安装nghttp2依赖的软件包:

# apt-get install libev-dev libjansson-dev libjemalloc-dev python-dev cython

下载nghttp2的源代码包:

# wget 'https://github.com/tatsuhiro-t/nghttp2/releases/download/v0.7.11/nghttp2-0.7.11.tar.xz'

解压和配置:

# tar xpf nghttp2-0.7.11.tar.xz
# cd nghttp2-0.7.11
# ./configure

配置输出一个更长的信息表:

Version:        0.7.11 shared 13:0:8
Host type:      x86_64-unknown-linux-gnu
Install prefix: /usr/local
C compiler:     gcc
CFLAGS:         -g -O2
WARNCFLAGS:
LDFLAGS:
LIBS:
CPPFLAGS:
C preprocessor: gcc -E
C++ compiler:   g++
CXXFLAGS:       -g -O2 -std=c++11
CXXCPP:         g++ -E
Library types:  Shared=yes, Static=yes
Python:
  Python:         /usr/bin/python
  PYTHON_VERSION: 2.7
  pyexecdir:      ${exec_prefix}/lib/python2.7/dist-packages
  Python-dev:     yes
  PYTHON_CPPFLAGS:-I/usr/include/python2.7
  PYTHON_LDFLAGS: -L/usr/lib -lpython2.7
  Cython:         cython
Test:
  CUnit:          no
  Failmalloc:     yes
Libs:
  OpenSSL:        yes
  Libxml2:        yes
  Libev:          yes
  Libevent(SSL):  yes
  Spdylay:        yes
  Jansson:        yes
  Jemalloc:       yes
  Boost CPPFLAGS:
  Boost LDFLAGS:
  Boost::ASIO:
  Boost::System:
  Boost::Thread:
Features:
  Applications:   yes
  HPACK tools:    yes
  Libnghttp2_asio:no
  Examples:       yes
  Python bindings:yes
  Threading:      yes

需要确认Features中的Application是yes。可以编译和安装:

# make && make install
# ldconfig

启动nghttpx反向代理

终于到最后一步,用nghttpx为网站提供HTTP2支持。因为nghttpx是反向代理服务器,如果现在的Web服务器已经启用了https,需要先把https支持关掉,不然会发生端口冲突哦。我们使用nghttpx的Base模式就可以了,在443端口上接收https请求,转发到本机的80端口,在先安装了spdylay的情况下,nghttpx是可以同时支持HTTP/1.1、SPDY/3.1、HTTP2的。

# nghttpx -f *,443 -b localhost,80 /path/to/private.key /path/to/certification.crt

如果没有什么错误,现在网站应该可以用支持http2的浏览器访问了,当然不支持http2的浏览器会自动降级到spdy或者http1.1上。

nghttpx有一堆高级的参数可以使用,比如变成一个daemon进程,还有指定运行的用户和组,还有输出日志的方法格式等等,就不在这里详述了,可以运行nghttpx -h阅读帮助资料。


  1. http://dev.chromium.org/spdy/spdy-whitepaper#TOC-Preliminary-results 

  2. http://blogs.msdn.com/b/ieinternals/archive/2013/09/24/internet-explorer-11-changelist-change-log.aspx 

  3. http://blog.chromium.org/2015/02/hello-http2-goodbye-spdy-http-is_9.html 

  4. http://tatsuhiro-t.github.io/spdylay/ 

给homebrew加上代理

homebrew大量使用Github的资源,由于众所周知的原因,Github在天朝速度缓慢还经常断线。一个解决办法是用代理访问。下面的命令可以让它下载安装的时候走socks代理。

$ ALL_PROXY=socks5://127.0.0.1:1080 brew install php55

如果不想用代理,去掉前面的ALL_PROXY环境变量就可以了。

ORM对象

ORM概念介绍

ORM(Object Relational Mapping对象关系映射)技术能把PHP对象和数据库记录一一对应起来,这样可以把数据记录对象化,或者把运行时对象持久化。有了ORM的帮助,数据库的增、删、改、查操作就不再是使用SQL语句,而更像平时OOP的各种对象操作了。

xts\Orange直接完成ORM

xts\Orange类是xts框架中负责ORM的类,提供ORM的各项基本功能。这个类依赖xts\Queryxts\SqlBuilder完成数据库操作。它的配置如下:


'orange' => array(
    'class' => '\\xts\\Orange',
    'singleton' => false,
    'conf' => array(
        'tablePrefix' => '',
        'queryId' => 'db',
        'enableCacheByDefault' => false,
        'moldyConf' => array(
            'cacheId' => 'cache',
            'duration' => 3600,
        ),
        'schemaConf' => array(
            'schemaCacheId' => 'cc',
            'useSchemaCache' => true,
            'schemaCacheDuration' => 0,
        ),
    ),
),

配置中的queryId是xts\Query类的组件ID,默认是db。如果项目的数据表有统一的prefix,可以定义tablePrefix字段。moldyConf是交给xts\MoldyOrange使用的配置选项,后者是提供Orange缓存支持的类。schemaConf定义了数据库结构信息缓存的机制,默认使用编译缓存。

下面我们使用用户表作为例子说明:

字段名 数据类型 说明
id int 用户ID
email varchar 电子邮件
screen_name varchar 用户名
first_name varchar 名字
last_name varchar 姓氏

新建

$user = X::orange('user');

xts推荐的方法是用X::orange方法创建ORM对象实例。这个函数会检查是否存在名为User的Orange的派生类,如果有就返回它的实例,如果没有,会返回一个Orange类的实例。X::orange方法在XComponentFactory类中定义,没有使用标准的组件工厂实现。

xts会在内部建立一个数据表user的结构对象,用于记录表的字段和关系。因为表结构不会经常改变,所以默认情况下,xts会打开schemaCache并长期缓存这个信息。如果修改了表结构,可能需要手动删除protected/runtime/compile_cache目录里的缓存文件。

注意:如果定义了tablePrefix配置项,在新建ORM对象的时候只需要写表名称的有效部分,不需要加上前缀。

Orange会自动维护对象属性和数据表字段间的关系,此外还提供驼峰命名法和下划线命名法的转换,用户可以直接以字段名作为属性名,也可以以对应的驼峰命名作为属性名称。

$user = X::orange('user');
$user->name = "James Bond";
$user->screen_name = "007";
$user->firstName = "James";
$user->lastName = "Bond";
$user->save();

这个例子中,同时使用了直接字段名和驼峰命名的规则来访问对象的属性,这都是可以的。最后的save方法调用会把数据插入到MySQL中。这里没有给$user->id赋值,因为在数据库中定了了ID为主键自增的字段。当然也可以根据需要指定id的值,不过这样有可能和自增长的值发生冲突。如果没有启用自增主键,则问题不大。

加载

我们会时常需要从数据库中按id读取一条记录,这时可以使用Orange的load方法。

$user = X::orange('user')->load($_SESSION['user_id']);
if ($user instanceof xts\Orange) {
    X::view()->assign('currentUser', $user);
} else {
    X::apple()->redirect('/login')->end();
}

上面是一个登录检查的例子,load方法在找不到记录的时候会返回null,所以可以根据返回值类型来确定是否成功加载出数据库记录。

修改

使用ORM来修改数据库记录,遵循加载、赋值、保存的步骤。前面已经提供了加载的范例代码,修改操作的代码和新建时相似,也是直接对ORM对象赋值。不同点在于修改操作的对象不是新建出来的,而是加载出来的。xts的ORM机制支持按需生成UPDATE语句,这意味着实际操作数据库的时候只有被真正修改的字段会被更新。

保存

在新建的例子代码当中示范了最常见的保存方法——直接调save方法即可。

xts的ORM对象还支持一些更复杂的保存操作。比如有的时候我们不确定是否已经存在重复的记录,可能需要INSERT IGNORE语句,或者INSERT ... ON DUPLICATE KEY UPDATE ...语句,当然还有REPLACE语句。可以利用save方法的第一个参数来通知xts生成不同的查询语句,此参数默认是xts\Orange::INSERT_NORMAL,生成一条普通的INSERT语句。

$user->save(xts\Orange::INSERT_IGNORE); // 生成 INSERT IGNORE 语句
$user->save(xts\Orange::INSERT_UPDATE); // 生成 INSERT ... ON DUPLICATE KEY UPDATE ... 语句
$user->save(xts\Orange::INSERT_REPLACE); // 生成 REPLACE 语句

查询

xts的查询生成SELECT语句。和load总成用主键查询不同,这里的查询可以自定义条件。xts提供的查询方法包括单条查询和多条查询。分别使用onemany方法。

$user = X::orange('user')->one('email=:eml AND password=:pwd', array(
    ':eml' => $_POST['email'],
    ':pmd' => sha1($_POST['password'] . X::$conf['salt']),
));

上面是one方法的使用范例,此方法接收两个参数,条件和数据列表。如果有多个复合条件,需要直接写成字符串。相比one方法,many方法要多两个参数offsetlimit

$users = X::orange('user')->many('first_name LIKE :fn ORDER BY id ASC', array(
    ':fn' => $_GET['q'] . '%',
), 0, 20);

many方法返回一个以主键为Key,xts\Orange对像为Value组成的关联数组。

删除

remove方法提供删除功能,这个方法有两种调用方式,一种接收要删除数据的主键为参数,删除这个主键所对应的一行数据;另一种不用传入参数,删除当前对象在数据库中的映射。

X::orange('user')->remove(47);

上面的例子代码能直接删除ID为47的用户记录,不论他是否存在;下面的例子能把所有名字以Th开头的用户删除。

$users = X::orange('user')->many("first_name LIKE 'Td%'");
foreach ($users as $user) {
    $user->remove();
}

组装和拆包

有的时候我们会用X::db()->query进行复杂的SQL查询,但又想把查询结果以ORM对象的方式使用,这时候需要用到组装功能。

$userArray = X::db()->query("SELECT * FROM `user` WHERE `first_name`='Thomas'");
$users = array();
foreach ($userArray as $userArr) {
    $users[$userArr['id']] = X::orange('user')->setup($userArr);
}

前面是组装的例子,setup方法能把数组组装成ORM对象。

另一种相反的情况,我们可能需要把ORM对象转换为关联数组,这时候可以调用getProperties方法或者直接调properties属性。

$user = X::orange('user')->load(17);
$userArr = $user->properties;

自定义ORM对象

xts提供的ORM功能只有数据库核心的操作功能。如果需要把一些逻辑代码写到Model层里,也可以编写自己的ORM对象。只需要继承xts\Orange类就可以了。

class User extends xts\Orange {
    public function __construct() {
        parent::__construct('user');
    }
    // Your code here
}

特殊的表名称

自定义ORM对象中,一种常见的情况是需要定义特殊的表名称,比如带有特殊的前缀,这时候可以通过重载getTableName方法来达成目的。

public function getTableName() {
    return 'special_user';
}

上面的例子是直接简单地返回表名称的字符串,但其实可以进行一定的状态判断,并返回适当的表名称。

属性Getter

xts\Orange对象支持Getter和Setter方法,继承它的子类也会自动取得相应支持,Getter和Setter在属性取用时拥有最高的优先权。在xts框架中,Getter方法的名称使用get+属性名的驼峰命名格式,Setter方法名称使用set+属性名的驼峰命名格式。比如以下是一个Getter的实例

public function getAge() {
    list($bYear, $_) = explode('-', $this->birthday, 2);
    return date('Y') - $bYear;
}

有了这个Getter,我们就可以直接用$user->age的方式访问这个属性了。

JSON迭代器

xts\Orange类实现了JsonSerializable接口,所以可以直接被json_encode函数序列化。默认情况下,json_encode会输出所有ORM映射的字段。不过我们也可以根据需要将Getter或者Relation字段也输出出来。只需通过重载getExportProperties就可以。

protected function getExportProperties() {
    return array_merge(array_keys($this->_properties), array('age'));
}

上面的例子在基本的数据库字段外,加入了age字段一并进行json_encode序列化。

数据库操作

终于写到数据库操作。xts框架只支持MySQL一种关系型数据库,对PostgreSQL和sqlite等常用库没有提供支持。我会分三个部分来介绍数据库的相关内容,包括基本的SQL查询,ORM功能以及ORM的Relation-Load功能。

配置MySQL数据库

MySQL基本配置如下

'db' => array(
    'class' => '\\xts\\Query',
    'singleton' => true,
    'conf' => array(
        'host' => 'localhost',
        'port' => 3306,
        'schema' => 'xts',
        'charset' => 'utf8',
        'user' => 'xts',
        'password' => 'xtstest',
        'persistent' => false,
    ),
),

这个配置应该放在component配置中,db是作为组件ID。xts\Query类提供基本的数据库操作功能。可以在conf中指定数据库的主机名、端口号、数据库名、连接字符集、用户名、密码还有是否启用长连接。

执行SQL语句

可以使用xts\Query类的query方法可以运行一条SQL语句并返回执行结果。

X::db()->query(
    "Select * From `user` Where `id`=:id",
    array(':id' => 1)
);

query方法会返回整个结果集,相当于PDO的fetchAll方法。如果SQL运行时发生错误,会抛出xts\OrangeException异常。

xts\Query类另有一个execute方法来运行一条SQL语句。此方法不支持变量绑定,只能书写完整的SQL语句字串。这个方法会返回操作影响的行数,比较适合用来执行Insert等无需返回结果集的函数。

取得上次Insert语句的自增长ID

xts\Query类的getLastInsertId方法可以取得上次插入语句的生成的自增ID。这个方法符合属性getter的格式,也可以以属性的方式访问它。

X::db()->execute(
    "Insert Into `user` (`id`, `name`, `email`) Values ( NULL, 'foobar', 'foobar@example.com'"
);
$id = X::db()->lastInsertId;

SQL Builder

xts提供SQL Builder来帮助书写SQL语句,它会自动做一些检查,不过功能比较弱,没有支持Join语句,所以无法取代Query。

在select的时候,SqlBuilder支持自动生成Select字段表

$sb = new \xts\SqlBuilder(X::db());
$sb->select('id', 'name', 'email')
    ->from('user')
    ->where('name like :n', array(':n'=>'Smit%');
$result = $sb->query();