Just Fine - Story of AlloVince 2015-12-14T16:47:35+0000 Zend_Feed_Writer http://avnpc.com <![CDATA[基于Travis CI搭建Android自动打包发布工作流(支持Github Release及fir.im)]]> 最近付费购买了Travis CI,Travis CI的收费模式很有意思,不是按项目或者用户,而是按工作进程收费,比如初级版本是$129/月,总共提供2个工作进程。在项目不多的情况下,除了用于跑单元测试外,不免想利用的更充分一些,因此抽空搭建了一套基于Travis CI的Android自动发布工作流

未自动化前安卓开发总是避免不了这样的工作流程:

  1. 开发一些新功能,提交代码
  2. 完成一部分功能后,打包一个测试版APK
  3. 将测试版APK上传到QQ群 / 网盘 / Fir.im / 蒲公英
  4. 在QQ群或发布平台解释当前版本所完成的功能
  5. 通知测试人员测试

实现了这套自动化发布后,工作流程被简化成:

  1. 开发新功能,提交代码
  2. 通过git tag对代码打一个内测版的tag,在tag的描述中对写当前完成的功能

Tag提交后Travis CI会自动编译代码,生成APK文件并分发到Github和fir.im,Github和fir.im中会保持Tag的描述信息,分发完成后会有邮件通知所有参与测试的人员。而作为开发人员,只需要专注于对代码打好一个Tag就可以了。

整个流程看似做了不少工作,其实体现在Travis CI只有数行指令而已,以下逐一讲解:

对安卓项目启用Travis CI

Travis CI应该可以算是目前最好用的持续集成服务之一了,如果代码库是基于Github的话,可以很简单的开启。由于本文涉及到了很多Travis CI的基础概念,建议首先对Travis CI的自定义构建一节有所了解。

很早前在介绍PHP项目的持续集成时也写过如何在PHP项目中使用Travis CI。 对于安卓项目来说步骤几乎一致:

首先准备一个.travis.yml文件放在安卓项目根目录下,.travis.yml中记录了Travis CI所需的基础信息:

language: android

sudo: false

android:
  components:
  - build-tools-23.0.1
  - android-23
  - extra-android-m2repository
  - extra-android-support

script:
  - "./gradlew assembleRelease"

无需读文档就可以通过上面的配置大概知道,我们要运行的是一个安卓项目,安卓SDK版本为23,项目所用的BuildTools版本为23.0.1,为编译这个项目我们还引入了一些必须的组件,如Support Library(extra-android-support)、Android Support Repository(extra-android-m2repository)等。

当Travis CI准备好我们所需要的环境后,将自动运行yml文件script部分所设置的指令,上例中运行的是./gradlew assembleRelease,运行成功的话会在项目的主模块下生成build/outputs/apk/app-release.apk

最后进入Travis CI主页,使用有项目Admin权限的Github帐号直接登录。选择要开启Travis CI的项目,将右边的开关设为On即可。

Travis CI目前有2个网站:如果是开源项目,直接进入travis-ci.org即可,如果是私有付费项目,则需要进入travis-ci.com,2个网站除了域名外所有的界面及操作几乎一模一样。

配置中还有一行sudo: false,是为了开启基于容器的Travis CI任务,让编译效率更高。

安卓自动化构建的密码和证书安全

安卓项目发布需要证书文件和若干密码,但无论是开源项目还是私有项目,任何时候都不应该将原始证书或密码放入代码库(原则上来讲证书和密码也不应该交于开发人员,而应该只能通过发布服务器进行编译)。Travis CI为此提供了2种解决方案,一种是对敏感信息、密码、证书等进行对称加密,在CI构建环境时解密,另一种是将密码等通过Travis CI的控制台(即网站)设置为构建时的环境变量。

由于前者会在Travis控制台生成一对环境变量,所以我的做法是尽量选择后者,但由于Travis控制台无法上传文件,因此涉及到文件加密的部分,则只能选择前者。

说了这么多,首先还是需要先对编译脚本进行改造,如果不考虑安全问题,项目的build.gradle文件可能会是这样:

android {
    signingConfigs {
        releaseConfig {
            storeFile file("../keys/evandroid.jks")
            storePassword "123456"
            keyAlias "evandroid_alias"
            keyPassword "654321"
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.releaseConfig
        }
    }
}

而我们最终要的效果,还是希望一份编译脚本既可以用于开发环境,也可以在CI环境下使用,在Travis CI中,可以通过点击项目名称 -> Settings -> Environment Variables中设置环境变量,比如我们可以针对上面的配置,分别设置KEYSTORE_PASSALIAS_NAMEALIAS_PASS三个环境变量,在Travis CI环境下可以通过System.getenv()获得这些环境变量。

本地开发环境中,我的做法是将这几个变量加到gradle.properties文件中,这样就可以在build.gradle内直接使用了。下面是开发环境的gradle.properties

KEYSTORE_PASS=123456
ALIAS_NAME=evandroid_alias
ALIAS_PASS=654321

这样一来build.gradle就变成了

        releaseConfig {
            storeFile file("../keys/evandroid.jks")
            storePassword project.hasProperty("KEYSTORE_PASS") ? KEYSTORE_PASS : System.getenv("KEYSTORE_PASS")
            keyAlias project.hasProperty("ALIAS_NAME") ? ALIAS_NAME : System.getenv("ALIAS_NAME")
            keyPassword project.hasProperty("ALIAS_PASS") ? ALIAS_PASS : System.getenv("ALIAS_PASS")
        }

接下来处理证书文件,为了方便文件加密等功能,Travis CI提供了一个基于ruby的CLI命令行工具,可以直接使用gem安装

gem install travis

安装后进入安卓项目根目录,尝试对证书文件加密:

travis encrypt-file keys/evandroid.jks --add

如果首次运行,travis会提示需要登录,运行travis login --org并输入Github用户名密码即可。(付费版则为travis login --pro

travis encrypt-file指令会做几件事情:

  1. 在Travis CI控制台自动生成一对密钥,形如:encrypted_e41864bb9dab_key, encrypted_e41864bb9dab_iv
  2. 基于密钥通过openssl对文件进行加密,上例中会项目根目录生成evandroid.jks.enc文件
  3. .travis.yml中自动生成Travis CI环境下解密文件的配置,上例运行后可以看到.travis.yml中多了几行:
before_install:
- openssl aes-256-cbc -K $encrypted_e41864bb9dab_key -iv $encrypted_e41864bb9dab_i -in keys/evandroid.jks.enc -out keys/evandroid.jks -d

Travis CI默认在项目根目录下运行,因此注意根据实际需求调整enc文件的路径。

最后别忘了在.gitignore中忽略keys/evandroid.jks以及gradle.properties并在代码库中将其删除。

Travis CI自动发布安卓apk文件到Github Release

Travis CI的script部分运行成功后,可以通过配置文件进入到发布阶段。下面是一个Travis CI发布的示例:

deploy:
  provider: releases
  user: "GITHUB USERNAME"
  password: "GITHUB PASSWORD"
  file: app/build/outputs/apk/app-release.apk
  skip_cleanup: true
  on:
    tags: true

这个例子中配置了这样一些内容:

  • provider:发布目标为Github Release,除了Github外,Travis CI还支持发布到AWS、Google App Engine等数十种provider
  • Github用户名和密码,因为Travis CI要上传APK文件,因此需要有Github项目的写入权限
  • file: 发布文件,输入文件路径即可
  • skip_cleanup: 默认情况下Travis CI在完成编译后会清除所有生成的文件,因此需要将skip_cleanup设置为true来忽略此操作。
  • on: 发布的时机,这里配置为tags: true,即只在有tag的情况下才发布。

虽然这样就能完成自动发布,但是直接暴露了Github密码是我们更加不能接受的。更好的做法是在Github -> settings -> Personal access tokens 生成一个只能访问当前项目并只有读取权限的Github Access Token,并通过Travis CI将Access Token加密。听起来有点繁琐,好在Travis CLI中已经可以通过一行指令做好这一切:

travis setup release

根据提示填写上述配置项目的信息后,Travis CLI会自动在.travis.yml文件中生成好所有的配置项:

deploy:
  provider: releases
  api_key:
    secure: XXX
  file: app/build/outputs/apk/app-release.apk
  skip_cleanup: true
  on:
    tags: true
    all_branches: true

其中api_key下的secure就是加密后的Access Token。

在运行travis setup release时有可能遇到

Invalid scheme format: git@github.com for a full error report, run travis report

这样的报错,看起来是Travis CLI还不支持通过密钥访问Github,因此可以将项目的源临时切换为http形式,运行成功后再切换回来:

git remote set-url origin https://github.com/AlloVince/evandroid.git
git remote set-url origin git@github.com:AlloVince/evandroid.git

在实际部署过程中,发现发布到Github Release比较坑的点是

git push
git push --tags

往往会同时生成2个Travis CI任务,但是在Travis网页中默认界面只能看到最后跑的一个任务,而未打Tag的任务又会报

Skipping a deployment with the releases provider because this is not a tagged commit

这曾让我一度以为自己的脚本哪里写错了,但是又找不到错误原因……

自动发布APK到fir.im

自动发布到Github对于开发人员已经足够,但是考虑到项目实际需要以及国情,还是有必要选择一个国内的App分发服务,fir.im、蒲公英都是不错的选择,不但允许游客下载,还提供了二维码等更适合对接手机的功能,国内下载速度也很快。由于fir.im提供了比较方便的CLI工具,因此本文以fir.im为例,在.travis.yml中添加以下几行:

before_install:
- gem install fir-cli
after_deploy:
- fir p app/build/outputs/apk/app-release.apk -T $FIR_TOKEN -c "`git cat-file tag $TRAVIS_TAG`"

即在环境构建阶段安装fir-cli,在发布成功后通过fir命令行工具将apk上传到fir。

其中$FIR_TOKEN可以在fir.im的用户->API Token中找到,然后在Travis CI控制台中创建环境变量FIR_TOKEN并粘贴即可。

这里有个小技巧,如果我们仅仅上传APK文件到fir.im,看到链接的测试人员其实并不知道这次发布所包含的变动,因此通过git cat-file tag $TRAVIS_TAG将当前发布tag所包含的附加信息一同上传了。其中$TRAVIS_TAG变量是Travis CI每次运行自动附带的环境变量,还有很多其他的Travis环境变量供我们玩出更多花样。

发布完毕后自动发邮件通知

虽然Travis CI也有通知功能,但不能定制模板,通知内容也仅仅为提示CI运行的结果,显然更适合开发人员。我们还是希望最终能以更友好的方式通知团队成员,同时考虑到邮件送达率,可以优先选择如SubmailSendCloud等国内邮件发送服务。

这里以Submail为例,首先需要在Submail内创建邮件模板,比如我们可以创建这样一封触发式邮件模板:

Hi 亲

@var(TRAVIS_REPO_SLUG)新版本@var(TRAVIS_TAG)已经发布了,功能更新:

@var(TAG_DESCRIPTION)

去下载:
http://fir.im/w13s

创建后可以得到邮件模板id,根据Submail手册,将模板中所需要的变量置入,最终可以使用一行Curl指令发送一封邮件:

after_deploy:- curl -d "appid=10948&to=allo.vince@gmail.com&subject=[自动通知] 安卓新版本$TRAVIS_TAG发布&project=u2c0r2&signature=$SUBMAIL_SIGN&vars={\"TRAVIS_REPO_SLUG\":\"$TRAVIS_REPO_SLUG\",\"TRAVIS_TAG\":\"$TRAVIS_TAG\",\"TAG_DESCRIPTION\":\"$(git cat-file tag $TRAVIS_TAG | awk 1 ORS='<br>')\"}" https://api.submail.cn/mail/xsend.json

其中Submail用到的认证凭据signature同样是通过Travis CI控制台配置的。

总结

最终完成的示例项目在此。其实所有的yml文件配置不到30行,就能省去繁琐的日常工作,何乐而不为呢。最后回顾一下自动化后的日常工作:

提交代码:

git add .
git commit -m "这里是注释"
git push origin

打Tag

git tag -a v0.0.1-alpha.1 -m "这里是Tag注释,说清楚这个版本的主要改动,也可以省略-m参数直接写长文本"
git push origin --tags

如果发现打错了tag,可以删除本地及远程tag

git tag -d v0.0.1-alpha.1
git push origin --delete tag v0.0.1-alpha.1

大部分Tag标签虽然仅用于内测,但是仍然建议遵循版本语义化原则。

References

]]>
2015-12-14T16:47:35+0000 2015-12-15T05:42:38+0000 http://avnpc.com/p/197 AlloVince
<![CDATA[Docker在PHP项目开发环境中的应用]]> 环境部署是所有团队都必须面对的问题,随着系统越来越大,依赖的服务也越来越多,比如我们目前的一个项目就会用到:

  • Web服务器:Nginx
  • Web程序:PHP + Node
  • 数据库:MySQL
  • 搜索引擎:ElasticSearch
  • 队列服务:Gearman
  • 缓存服务:Redis + Memcache
  • 前端构建工具:npm + bower + gulp
  • PHP CLI工具:Composer + PHPUnit

因此团队的开发环境部署随之暴露出若干问题:

  1. 依赖服务很多,本地搭建一套环境成本越来越高,初级人员很难解决环境部署中的一些问题
  2. 服务的版本差异及OS的差异都可能导致线上环境BUG
  3. 项目引入新的服务时所有人的环境需要重新配置

对于问题1,可以用Vagrant这样的基于虚拟机的项目来解决,团队成员共享一套开发环境镜像。对于问题2,可以引入类似PHPBrew这样的多版本PHP管理工具来解决。但两者都不能很好地解决问题3,因为虚拟机镜像没有版本管理的概念,当多人维护一个镜像时,很容易出现配置遗漏或者冲突,一个很大的镜像传输起来也不方便。

Docker的出现让上面的问题有了更好的解决方案,虽然个人对于Docker大规模应用到生产环境还持谨慎态度,但如果仅仅考虑测试及开发,私以为Docker的容器化理念已经是能真正解决环境部署问题的银弹了。

下面介绍Docker构建PHP项目开发环境过程中的演进,本文中假设你的操作系统为Linux,已经安装了Docker,并且已经了解Docker是什么,以及Docker命令行的基础使用,如果没有这些背景知识建议先自行了解。

Hello World

首先还是从一个PHP在Docker容器下的Hello World实例开始。我们准备这样一个PHP文件index.php

<?php
echo "PHP in Docker";

然后在同目录下创建文本文件并命名为Dockerfile,内容为:

# 从官方PHP镜像构建
FROM       php

# 将index.php复制到容器内的/var/www目录下
ADD        index.php /var/www/

# 对外暴露8080端口
EXPOSE     8080

# 设置容器默认工作目录为/var/www
WORKDIR    /var/www/

# 容器运行后默认执行的指令
ENTRYPOINT ["php", "-S", "0.0.0.0:8080"]

构建这个容器:

docker build -t allovince/php-helloworld .

运行这个容器

docker run -d -p 8080:8080 allovince/php-helloworld

查看结果:

curl localhost:8080
PHP in Docker

这样我们就创建了一个用于演示PHP程序的Docker容器,任何安装过Docker的机器都可以运行这个容器获得同样的结果。而任何有上面的php文件和Dockerfile的人都可以构建出相同的容器,从而完全消除了不同环境,不同版本可能引起的各种问题。

想象一下程序进一步复杂,我们应该如何扩展呢,很直接的想法是继续在容器内安装其他用到的服务,并将所有服务运行起来,那么我们的Dockerfile很可能发展成这个样子:

FROM       php
ADD        index.php /var/www/

# 安装更多服务
RUN        apt-get install -y \
           mysql-server \
           nginx \
           php5-fpm \
           php5-mysql

# 编写一个启动脚本启动所有服务
ENTRYPOINT ["/opt/bin/php-nginx-mysql-start.sh"]

虽然我们通过Docker构建了一个开发环境,但觉不觉得有些似曾相识呢。没错,其实这种做法和制作一个虚拟机镜像是差不多的,这种方式存在几个问题:

  • 如果需要验证某个服务的不同版本,比如测试PHP5.3/5.4/5.5/5.6,就必须准备4个镜像,但其实每个镜像只有很小的差异。
  • 如果开始新的项目,那么容器内安装的服务会不断膨胀,最终无法弄清楚哪个服务是属于哪个项目的。

使用单一进程容器

上面这种将所有服务放在一个容器内的模式有个形象的非官方称呼:Fat Container。与之相对的是将服务分拆到容器的模式。从Docker的设计可以看到,构建镜像的过程中可以指定唯一一个容器启动的指令,因此Docker天然适合一个容器只运行一种服务,而这也是官方更推崇的。

分拆服务遇到的第一个问题就是,我们每一个服务的基础镜像从哪里来?这里有两个选项:

选项一、 统一从标准的OS镜像扩展,比如下面分别是Nginx和MySQL镜像

FROM ubuntu:14.04
RUN  apt-get update -y && apt-get install -y nginx
FROM ubuntu:14.04
RUN  apt-get update -y && apt-get install -y mysql

这种方式的优点在于所有服务可以有一个统一的基础镜像,对镜像进行扩展和修改时可以使用同样的方式,比如选择了ubuntu,就可以使用apt-get指令安装服务。

问题在于大量的服务需要自己维护,特别是有时候需要某个服务的不同版本时,往往需要直接编译源码,调试维护成本都很高。

选项二、 直接从Docker Hub继承官方镜像,下面同样是Nginx和MySQL镜像

FROM nginx:1.9.0
FROM mysql:5.6

Docker Hub可以看做是Docker的Github,Docker官方已经准备好了大量常用服务的镜像,同时也有非常多第三方提交的镜像。甚至可以基于Docker-Registry项目在短时间内自己搭建一个私有的Docker Hub。

基于某个服务的官方镜像去构建镜像,有非常丰富的选择,并且可以以很小的代价切换服务的版本。这种方式的问题在于官方镜像的构建方式多种多样,进行扩展时需要先了解原镜像的Dockerfile

出于让服务搭建更灵活的考虑,我们选择后者构建镜像。

为了分拆服务,现在我们的目录变为如下所示结构:

~/Dockerfiles
├── mysql
│   └── Dockerfile
├── nginx
│   ├── Dockerfile
│   ├── nginx.conf
│   └── sites-enabled
│       ├── default.conf
│       └── evaengine.conf
├── php
│   ├── Dockerfile
│   ├── composer.phar
│   ├── php-fpm.conf
│   ├── php.ini
│   ├── redis.tgz
└── redis
    └── Dockerfile

即为每个服务创建单独文件夹,并在每个服务文件夹下放一个Dockerfile。

MySQL容器

MySQL继承自官方的MySQL5.6镜像,Dockerfile仅有一行,无需做任何额外处理,因为普通需求官方都已经在镜像中实现了,因此Dockerfile的内容为:

FROM mysql:5.6

在项目根目录下运行

docker build -t eva/mysql ./mysql

会自动下载并构建镜像,这里我们将其命名为eva/mysql

由于容器运行结束时会丢弃所有数据库数据,为了不用每次都要导入数据,我们将采用挂载的方式持久化MySQL数据库,官方镜像默认将数据库存放在/var/lib/mysql,同时要求容器运行时必须通过环境变量设置一个管理员密码,因此可以使用以下指令运行容器:

docker run -p 3306:3306 -v ~/opt/data/mysql:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 -it eva/mysql

通过上面的指令,我们将本地的3306端口绑定到容器的3306端口,将容器内的数据库持久化到本地的~/opt/data/mysql,并且为MySQL设置了一个root密码123456

Nginx容器

Nginx目录下提前准备了Nginx配置文件nginx.conf以及项目的配置文件default.conf等。Dockerfile内容为:

FROM nginx:1.9

ADD  nginx.conf      /etc/nginx/nginx.conf
ADD  sites-enabled/*    /etc/nginx/conf.d/
RUN  mkdir /opt/htdocs && mkdir /opt/log && mkdir /opt/log/nginx
RUN  chown -R www-data.www-data /opt/htdocs /opt/log

VOLUME ["/opt"]

由于官方的Nginx1.9是基于Debian Jessie的,因此首先将准备好的配置文件复制到指定位置,替换镜像内的配置,这里按照个人习惯,约定/opt/htdocs目录为Web服务器根目录,/opt/log/nginx目录为Nginx的Log目录。

同样构建一下镜像

docker build -t eva/nginx ./nginx

并运行容器

docker run -p 80:80 -v ~/opt:/opt -it eva/nginx

注意我们将本地的80端口绑定到容器的80端口,并将本地的~/opt目录挂载到容器的/opt目录,这样就可以将项目源代码放在~/opt目录下并通过容器访问了。

PHP容器

PHP容器是最复杂的一个,因为在实际项目中,我们很可能需要单独安装一些PHP扩展,并用到一些命令行工具,这里我们以Redis扩展以及Composer来举例。首先将项目需要的扩展等文件提前下载到php目录下,这样构建时就可以从本地复制而无需每次通过网络下载,大大加快镜像构建的速度:

wget https://getcomposer.org/composer.phar -O php/composer.phar
wget https://pecl.php.net/get/redis-2.2.7.tgz -O php/redis.tgz

php目录下还准备好了php配置文件php.ini以及php-fpm.conf,基础镜像我们选择的是PHP 5.6-FPM,这同样是一个Debian Jessie镜像。官方比较亲切的在镜像内部准备了一个docker-php-ext-install指令,可以快速安装如GD、PDO等常用扩展。所有支持的扩展名称可以通过在容器内运行docker-php-ext-install获得。

来看一下Dockerfile

FROM php:5.6-fpm

ADD php.ini    /usr/local/etc/php/php.ini
ADD php-fpm.conf    /usr/local/etc/php-fpm.conf

COPY redis.tgz /home/redis.tgz
RUN docker-php-ext-install gd \
    && docker-php-ext-install pdo_mysql \
    && pecl install /home/redis.tgz && echo "extension=redis.so" > /usr/local/etc/php/conf.d/redis.ini
ADD composer.phar /usr/local/bin/composer
RUN chmod 755 /usr/local/bin/composer

WORKDIR /opt
RUN usermod -u 1000 www-data

VOLUME ["/opt"]

在构建过程中做了这样一些事情:

  1. 复制php和php-fpm配置文件到相应目录
  2. 复制redis扩展源代码到/home
  3. 通过docker-php-ext-install安装GD和PDO扩展
  4. 通过pecl安装Redis扩展
  5. 复制composer到镜像作为全局指令

按照个人习惯,仍然设置/opt目录作为工作目录。

这里有一个细节,在复制tar包文件时,使用的Docker指令是COPY而不是ADD,这是由于ADD指令会自动解压tar文件

现在终于可以构建+运行了:

docker build -t eva/php ./php
docker run -p 9000:9000 -v ~/opt:/opt -it eva/php

在大多数情况下,Nginx和PHP所读取的项目源代码都是同一份,因此这里同样挂载本地的~/opt目录,并且绑定9000端口。

PHP-CLI的实现

php容器除了运行php-fpm外,还应该作为项目的php cli使用,这样才能保证php版本、扩展以及配置文件保持一致。

例如在容器内运行Composer,可以通过下面的指令实现:

docker run -v $(pwd -P):/opt -it eva/php composer install --dev -vvv

这样在任意目录下运行这行指令,等于动态将当前目录挂载到容器的默认工作目录并运行,这也是PHP容器指定工作目录为/opt的原因。

同理还可以实现phpunit、npm、gulp等命令行工具在容器内运行。

Redis容器

为了方便演示,Redis仅仅作为缓存使用,没有持久化需求,因此Dockerfile仅有一行

FROM redis:3.0

容器的连接

上面已经将原本在一个容器中运行的服务分拆到多个容器,每个容器只运行单一服务。这样一来容器之间需要能互相通信。Docker容器间通讯的方法有两种,一种是像上文这样将容器端口绑定到一个本地端口,通过端口通讯。另一种则是通过Docker提供的Linking功能,在开发环境下,通过Linking通信更加灵活,也能避免端口占用引起的一些问题,比如可以通过下面的方式将Nginx和PHP链接起来:

docker run -p 9000:9000 -v ~/opt:/opt --name php -it eva/php
docker run -p 80:80 -v ~/opt:/opt -it --link php:php eva/nginx

在一般的PHP项目中,Nginx需要链接PHP,而PHP又需要链接MySQL,Redis等。为了让容器间互相链接更加容易管理,Docker官方推荐使用Docker-Compose完成这些操作。

用一行指令完成安装

pip install -U docker-compose

然后在Docker项目的根目录下准备一个docker-compose.yml文件,内容为:

nginx:
    build: ./nginx
    ports:
      - "80:80"
    links:
      - "php"
    volumes:
      - ~/opt:/opt

php:
    build: ./php
    ports:
      - "9000:9000"
    links:
      - "mysql"
      - "redis"
    volumes:
      - ~/opt:/opt

mysql:
    build: ./mysql
    ports:
      - "3306:3306"
    volumes:
      - ~/opt/data/mysql:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: 123456

redis:
    build: ./redis
    ports:
      - "6379:6379"

然后运行docker-compose up,就完成了所有的端口绑定、挂载、链接操作。

更复杂的实例

上面是一个标准PHP项目在Docker环境下的演进过程,实际项目中一般会集成更多更复杂的服务,但上述基本步骤仍然可以通用。比如EvaEngine/Dockerfiles是为了运行我的开源项目EvaEngine准备的基于Docker的开发环境,EvaEngine依赖了队列服务Gearman,缓存服务Memcache、Redis,前端构建工具Gulp、Bower,后端Cli工具Composer、PHPUnit等。具体实现方式可以自行阅读代码。

经过团队实践,原本大概需要1天时间的环境安装,切换到Docker后只需要运行10余条指令,时间也大幅缩短到3小时以内(大部分时间是在等待下载),最重要的是Docker所构建的环境都是100%一致的,不会有人为失误引起的问题。未来我们会进一步将Docker应用到CI以及生产环境中。

本文首发于我在卧龙阁的专栏PHP与创业的那些事儿,转载请保留。

]]>
2015-06-30T09:19:40+0000 2015-12-17T05:22:02+0000 http://avnpc.com/p/196 AlloVince
<![CDATA[专访 :徐谦——以职人精神打造架构师的工匠之心]]> 卧龙阁最近对我进行了一次专访,主要聊了工作以来的一些经验和对技术的看法,现全文转载如下,希望对新入行的朋友有帮助,如果有兴趣的话也可以移步原文阅读

问:很多人都认为程序员就是无止尽的写代码,是件非常枯燥的工作,您从事这行多年,能说说做这行与其他行业相比具有哪些亮点吗?您认为这一行有哪些乐趣让您乐在其中?

答:会从事开发,其实技术本身并不是最吸引我的。最让我有成就感的事情,还是通过使用各种各样的技术,从无到有去创造一个产品。 一行行代码慢慢转变为用户可以体验、把玩、讨论、有用的产品,这个过程带给我的体验是无与伦比的。从业这么长时间至今,我依然很享受这种创造的过程,这是其他职业很难体验到的事情。

个人非常推崇日本的“职人精神”。本质上来讲,程序开发也是一门手艺活,能专注于自身专精的领域,并穷尽毕生精力去追求更高的技艺,我觉得这样的人是值得尊敬的,而我也在努力成为这样的人。

问:行行出状元,您作为网站技术架构领域的佼佼者一定也有自己一套和别人不一样的工作习惯,您平时的哪些工作习惯在潜移默化中助您提升职业价值?

答:

1、向开源社区贡献代码。比如我曾向Zend Framework等比较大的项目提交过代码,通过接触这些优秀项目的开发者,了解他们解决问题的方法会让人受益终身。

2、 在StackOverflow上回答问题。我的方式可能更有针对性,比如在做ZF2相关开发时,就只回答ZF2相关问题,直至刷到一个奖牌,这对短期内掌握了解某个技术非常有帮助。

3、翻译图书。目前我已经翻译出版了《自制编程语言》和《游戏开发的物理和数学》等书,在翻译过程中会对个人的知识体系起到很好地整理和巩固作用。

4、坚持更新博客。个人博客avnpc.com已经有多年的历史,很多圈子里的朋友虽然不认识我但是都认识我的Blog。坚持将自己的经验和知识整理为博客,一方面可以帮助到很多初学者让他们少走弯路,另一方面对自己也是极好的锻炼。

问:您在网站技术构架方面颇有成就,在您接手的一些关于网站技术架构工作中,哪些给您留下了深刻印象,能具体谈谈为什么会让您难忘吗?

答:惭愧,我们的项目在大公司来看可能还根本称不上架构,只是组合了一些语言和工具能解决我们现有的问题而已。

有句话叫善战者无赫赫之功,我觉得做架构也是同样的道理,一个架构的迭代和演进应该是非常默默无闻的,外界作为用户不应该感受到任何架构层面的变化。我们的服务从我加入时的几万日PV到现在每天数亿次请求,用户基本上没觉得有太大变化。

而好的架构师除了自身技术能力外,眼光更加重要。对于创业公司而言,架构师需要能准确看到公司业务核心需要解决的问题,比如我们公司业务价值在于资讯和行情的速度,那么最开始做架构的时候就花了非常大的精力去提升系统的实时性,比如投入人员去自动化整个信息发布流程,比如使用WebSocket代替有延迟的轮询方案,同时又对低端设备准备好降级的替代方案等。而对于一般架构中比较重要的用户系统,其实我们是等到公司开始涉及金融交易业务时才去将这一块完善的。

个人愚见是,没有什么架构能解决所有问题,大公司的架构也未必能适合创业公司用,如果我们一开始就花精力去做用户,去规划分表分库,系统本身固然会得到更好的扩展性,但是公司的发展速度却会受阻。我总结创业公司做架构优先考虑的应该是用较低的成本满足当前需求,并且保证一定的前瞻性就够了。

问:我们这个专题叫“卓越者计划”,有幸邀请您加入卓越者计划中,您认为您自己能够取得一些成就并脱颖而出的关键点是哪些,用几个关键词概括即可?您是如何解读这些关键词的?

答:很巧,我最喜欢的一句话就是《三个傻瓜》中的“Follow Excellence. Success will chase you”。怎样追求卓越,作为开发人员,我觉得一个最重要的关键词是保持“专注”。比如我是后端开发,但是这两年移动比较火,就扔下后端去做移动,最后很可能什么也做不好。再比如最近股市比较火,那么程序也不好好写了,每天花8个小时去研究股票,这都是不可取的。大多数人包括我在内都不是天才,那么只有长期专注于一件事情才有可能超越其他人。


下面是一些问答,也将其中一些有价值的摘抄出来供参考

Q: 请问想成为顶尖的程序员,或者技术大神,是否需要有很强的算法,或者数学功底吗?

A: 还是需要的,数学和算法是基本功,不一定会在项目中用到,但是基本功扎实会决定你能到达的高度

Q: 前两天和一个创业的朋友谈事儿,问了个问题,如果移动互联网创业公司,薪资空间只够请一个技术大牛或一个产品大牛,如何抉择??您怎么看?

A: 这个问题没有唯一的答案吧,可以看看最近很火的文章《为什么整个互联网行业都缺CTO》。

创业公司真的需要思考一下现阶段真正缺少的是什么,不要把问题只归咎于缺1、2个人上面。

Q: 想请教下,PHP这门语言的优势到底在哪里?

A: 上手快,脚本语言,无需编译。资源自动回收,能容忍一些不够好的代码在线上运行。社区生态成熟,产品稳定。人员招聘相对容易。个人觉得是创业公司首选吧。

Q: 我准备跳槽了,但是我想问问程序猿在选择公司的时候,会比较看重公司的哪些地方,选择怎样的公司才不会令自己后悔?

A: 我觉得新人首要看重的不是公司,而应该是团队,这家公司有什么样的人,在用什么技术,以什么样的方式在工作,这是面试过程中需要了解清楚的,面试不光是公司面试应聘者,也是应聘者在面试这家公司。

Q: 我在知乎也问过这个问题,请问徐老师,怎么成为一个优秀的程序员,而不是一个优秀的码农?

A: 其实决定程序员成长的还是本人的意愿,哪怕再小的项目,都存在可以不断优化并进步的空间。但如果只是为了做业务而做业务,从来不思考,那可能做很多年还是在做很初级的事情。

Q: 请问下如果你们的产品经理一般都是什么需求,你们怎么判断能不能做呢?

A: 我们对产品经理看重的点是他能不能砍需求,一个产品可能有100个需求都可以做,他能不能找到最核心的10个需求并已最低的成本将产品实现出来。

Q: 最近很多人推荐了解和学习GO语言,请问从一个技术领导的角度,怎么看待这个的价值?另外,在自身提升技术的角度,是应该多接触很多语言,增加知识的宽度,还是根据工作内容,不断深挖单一工具,增加技能的深度?

A: 在入行的前几年先专注深度,专精一门语言之后再考虑广度会比较好。两者都很重要,不过需要讲究一下顺序

Q: 您说做技术应该专注,我很赞同,但是我同时也很迷茫,自己马上要30了,如果过了30我还是个写码的是不是就很失败?大家都说程序猿要慢慢开始带团队,如果真的老了自己还做基层工作怎么办?带团队或者升职真的是程序猿成功的标志吗?

A: 我现在也每天写代码,我觉得技术人员应该在意的是今天写的代码有没有比昨天写的更好,而不是今天和明天是不是都写代码。无论写代码还是做管理,如果能保持持续进步的话,职业道路肯定会越走越宽的。

Q: 你觉得HTML、CSS、JavaScript、PHP、 MySQL 的学习顺序是什么?

这几个东西拼在一起才能做成一个完整的网站啊,我自己是直接开始一个小项目,比如写一个Blog,用到哪一块的知识就去学哪一块

Q: 最近刚走上领导岗位,虽说职位比较低,但也碰上了缺人的问题,请问您在上海的工作过程中,有没有碰到过这样的问题?您是怎么处理和解决的呢?

A: 我们从第一天到现在都严重缺人,一个创业公司缺人的现象持续到上市都是正常的。

招聘当然是最直接的解决方案,但是同时也应该注意把有限的力量用在一些核心问题的解决上,避免浪费资源。开源+节流吧。

Q: 看到您说写博客,您说团队开设技术博客这件事靠谱吗?我现在的公司,属于初创型公司,招聘是很现实的问题,大家讨论,想开设一个整个技术团队的博客以吸引人才,但我整个团队里,有人很积极,有人却非常抗拒,您怎么看这件事?

A: 团队开设Blog对技术积累是有一定好处的,但是招聘的话以我的经验应该不会有太大效果。推进一个事情,如果认定这个事情是好的,那么就从小部分人开始实践,慢慢影响到大多数人。

Q: 不知道在您的管理中遇没遇到过,由于招聘任务重,新来的同级别员工会比同级别老员工工资高的现象,您怎么看?

A: 公司稳定是最重要的,如果无原则的招聘导致薪资结构倒挂,我觉得这是CEO需要反省的问题。

Q: 如果让您给初级程序员,在学习道路道路上一点建议,你会给什么建议,或者应该怎么去学习?

A: 个人不建议只看教程或只看书。先发现自己的兴趣所在,结合兴趣从实践中去学习是最快的。

Q: 请问徐总你在招人的时候,主要看重一个程序员的哪些方面?如何才能判断这个人是团队需要的?

A: 对于我们来说,如果应聘者有一个Github账号和一些自己维护的项目就是最好的简历了。

Q: 提问:应该会碰到过产品需求打破架构设计的情况吧,碰到这种状况,你怎么办呢

A: 一个架构总是用来解决一类需求的,如果某些需求整个架构都无法满足,那一定做的是完全不同的另一件事情,是不是可以新开一个项目来完成这些工作,而不是用一套架构满足所有需求。

Q: 提问:徐老师你好,作为一个程序员,请问你有没有特殊的值得推荐的学习方法?另外,你对于PHP的发展持怎样的态度,据说今年十月份PHP7就发布了,请问华尔街会部署吗?

A: 个人是实战派,所以很少去先看书和教程,一般会直接上手项目,边做边学。

线上产品稳定性还是第一位的,如果我们测试新版本的新特性或者性能提升有升级的价值,会考虑线上全面部署。当然会有很大的测试成本。

Q: 每天对技术方面的关注或者研究会花多少时间呢? 你碰到过瓶颈吗, 当时都是怎么提升的?

A: 其实没有刻意计算过时间,不过我觉得10000小时定律是想对靠谱的,没有10000小时的话很难在某个技能上达到一定深度。

瓶颈是周而复始的,每年都会有几次觉得自己好像什么都不懂……主要还是靠坚持吧,不懂的东西坚持去学习也就慢慢懂了:)

Q: 徐总你好,目前项目遇到服务、业务拆分的阶段,请问在拆分的标准方面,你有什么好的建议?

A: 如果只是代码层面的拆分,定义好目录结构按照规定放置代码即可。如果是服务层面的拆分话题会比较大,主要还是要约定接口规范。

个人不建议过早的拆分,拆分的前提是业务需求想对稳定,否则联调会很痛苦

Q: 我想知道技术男你们怎么学习英文,翻译书籍应该需要英文很好的,对吧?

A: 其实我的英语水平一般,因为有在日本的工作经历,翻译的基本都是日语书。

当然无论日语还是英语,技术人员阅读外文资料都是一门基本功,不过个人并没有觉得这件事情非常有难度,因为技术类文章所用词汇都比较固定,叙述方式也很直白,没有特别复杂的语法障碍,所以只要肯花功夫一定能有所成就。

]]>
2015-06-04T14:11:13+0000 2015-09-19T06:00:58+0000 http://avnpc.com/p/195 AlloVince
<![CDATA[EvaOAuth 1.0:统一接口的OAuth登录PHP类库]]> Latest Stable Version License Build Status Coverage Status Scrutinizer Code Quality

EvaOAuth 是一个统一接口设计的PHP OAuth Client库,兼容OAuth1.0与OAuth2.0规范,可以通过10多行代码集成到任意项目中。

项目代码托管在 https://github.com/AlloVince/EvaOAuth ,欢迎Star及Fork贡献代码。

为什么选择EvaOAuth

经过若干项目考验, EvaOAuth1.0 根据实际需求进行了一次完全重构,主要的一些特性如下:

  • 标准接口,无论OAuth1.0或OAuth2.0,同一套代码实现不同工作流,并且获取一致的数据格式,包括用户信息和Token。
  • 充分测试,所有关键代码进行单元测试,同时通过CI保证多版本PHP下的可用性。
  • 容易调试,开启Debug模式后,Log中会记录OAuth流程中所有的URL、Request、Response,帮助定位问题。
  • 开箱即用,项目已经内置了主流的OAuth网址支持,如微博、QQ、Twitter、Facebook等。
  • 方便扩展,可以通过最少3行代码集成新的OAuth服务,工作流程提供事件机制。

快速开始

EvaOAuth可以通过Packagist下载,推荐通过Composer安装。

编辑composer.json文件为:

{
    "require": {
        "evaengine/eva-oauth": "~1.0"
    }
}

然后通过Composer进行安装。

curl -sS https://getcomposer.org/installer | php
php composer.phar install

下面通过一个实例演示如何集成豆瓣登录功能。假设已经在豆瓣开发者创建好一个应用。准备一个request.php如下:

require_once './vendor/autoload.php'; //加载Composer自动生成的autoload
$service = new Eva\EvaOAuth\Service('Douban', [
    'key' => 'You Douban App ID',  //对应豆瓣应用的API Key
    'secret' => 'You Douban App Secret', //对应豆瓣应用的Secret
    'callback' => 'http://localhost/EvaOAuth/example/access.php' //回调地址
]);
$service->requestAuthorize();

在浏览器中运行request.php,如果参数正确则会被重定向到豆瓣授权页面,登录授权后会再次重定向回我们设置的callback。因此再准备好access.php文件:

$token = $service->getAccessToken();

这样就拿到了豆瓣的Access Token,接下来可以使用Token去访问受保护的资源:

$httpClient = new Eva\EvaOAuth\AuthorizedHttpClient($token);
$response = $httpClient->get('https://api.douban.com/v2/user/~me');

这样就完成了OAuth的登录功能。更多细节可以参考代码的示例以及Wiki页面。

OAuth网站支持

EvaOAuth将一个OAuth网站称为一个Provider。目前支持的Provider有:

  • OAuth2.0
    • 豆瓣(Douban)
    • Facebook
    • QQ (Tencent)
    • 微博 (Weibo)
  • OAuth1.0
    • Twitter

新增一个Provider仅需数行代码,下面演示如何集成Foursquare网站:

namespace YourNamespace;

class Foursquare extends \Eva\EvaOAuth\OAuth2\Providers\AbstractProvider
{
    protected $authorizeUrl = 'https://foursquare.com/oauth2/authorize';
    protected $accessTokenUrl = 'https://foursquare.com/oauth2/access_token';
}

然后将Provider注册到EvaOAuth就可以使用了。

use Eva\EvaOAuth\Service;
Service::registerProvider('foursquare', 'YourNamespace\Foursquare');
$service = new Service('foursquare', [
    'key' => 'Foursquare App ID',
    'secret' => 'Foursquare App Secret',
    'callback' => 'http://somecallback/'
]);

数据存储

在OAuth1.0的流程中,需要将Request Token保存起来,然后在授权成功后使用Request Token换取Access Token。因此需要数据存储功能。

EvaOAuth的数据存储通过Doctrine\Cache实现。默认情况下EvaOAuth会将数据保存为本地文件,保存路径为EvaOAuth/tmp

可以在EvaOAuth初始化前任意更改存储方式及存储位置,例如将文件保存位置更改为/tmp

Service::setStorage(new Doctrine\Common\Cache\FilesystemCache('/tmp'));

或者使用Memcache保存:

$storage = new \Doctrine\Common\Cache\MemcacheCache();
$storage->setMemcache(new \Memcache());
Service::setStorage($storage);

事件支持

EvaOAuth 定义了若干事件方面更容易的注入逻辑

  • BeforeGetRequestToken: 获取Request Token前触发。
  • BeforeAuthorize: 重定向到授权页面前触发。
  • BeforeGetAccessToken: 获取Access Token前触发。

比如我们希望在获取Access Token前向HTTP请求中加一个自定义Header,可以通过以下方式实现:

$service->getEmitter()->on('beforeGetAccessToken', function(\Eva\EvaOAuth\Events\BeforeGetAccessToken $event) {
    $event->getRequest()->addHeader('foo', 'bar');
});

技术实现

EvaOAuth 基于强大的HTTP客户端库Guzzle,并通过OOP方式对OAuth规范进行了完整的描述。

为了避免对规范的诠释上出现误差,底层代码优先选择规范描述中的角色与名词,规范间差异则在上层代码中统一。

因此如果没有同时支持两套规范的需求,可以直接使用OAuth1.0、OAuth2.0分别对应的工作流。

详细用例可以参考Wiki:

Debug与Log

开启Debug模式将在Log中记录所有的请求与响应。

$service->debug('/tmp/access.log');

请确保PHP对log文件有写入权限。

API文档

首先通过pear install phpdoc/phpDocumentor安装phpDocumentor,然后在项目根目录下运行phpdoc,会在docs/下生成API文档。

问题反馈及贡献代码

项目代码托管在 https://github.com/AlloVince/EvaOAuth ,欢迎Star及Fork贡献代码。

有问题欢迎在EvaOAuth Issue提出。

]]>
2015-05-01T09:21:07+0000 2015-05-02T09:51:04+0000 http://avnpc.com/p/194 AlloVince
<![CDATA[Mac下安装LNMP(Nginx+PHP5.6)环境]]> 安装Homebrew

最近工作环境切换到Mac,所以以OS X Yosemite(10.10.1)为例,记录一下从零开始安装Mac下LNMP环境的过程

确保系统已经安装xcode,然后使用一行命令安装依赖管理工具Homebrew

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

之后就可以使用

brew install FORMULA

来安装所需要的依赖了。

brew(意为酿酒)的命名很有意思,全部都使用了酿酒过程中采用的材料/器具,名词对应以下的概念:

  • Formula(配方) 程序包定义,本质上是一个rb文件
  • Keg(桶)程序包的安装路径
  • Cellar(地窖)所有程序包(桶)的根目录
  • Tap(水龙头)程序包的源
  • Bottle (瓶子)编译打包好的程序包

最终编译安装完毕的程序就是一桶酿造好的酒

更详细的信息参考Homebrew的官方Cookbook

因此使用Homebrew常见的流程是:

  1. 增加一个程序源(新增一个水龙头) brew tap homebrew/php
  2. 更新程序源 brew update
  3. 安装程序包(按照配方酿酒) brew install git
  4. 查看配置 brew config 可以看到程序包默认安装在/usr/local/Cellar下 (酒桶放在地窖内)

安装PHP5.6(FPM方式)

首先加入Homebrew官方的几个软件源

brew tap homebrew/dupes
brew tap homebrew/versions
brew tap homebrew/php

PHP如果采用默认配置安装,会编译mod_php模块并只运行在Apache环境下,为了使用Nginx,这里需要编译php-fpm并且禁用apache,主要通过参数--without-fpm --without-apache来实现。完整的安装指令为

brew install php56 \
--without-snmp \
--without-apache \
--with-debug \
--with-fpm \
--with-intl \
--with-homebrew-curl \
--with-homebrew-libxslt \
--with-homebrew-openssl \
--with-imap \
--with-mysql \
--with-tidy

由于OSX已经自带了PHP环境,因此需要修改系统路径,优先运行brew安装的版本,在~/.bashrc里加入:

export PATH="/usr/local/bin:/usr/local/sbin:$PATH"

如果要安装新的php扩展,可以直接安装而不用每次重新编译php,所有的扩展可以通过

brew search php56

看到,下面是我自己所需要的扩展,可以支持Phalcon框架

brew install php56-gearman php56-msgpack php56-memcache php56-memcached php56-mongo  php56-phalcon php56-redis php56-xdebug

PHP-FPM的加载与启动

安装完毕后可以通过以下指令启动和停止php-fpm

php-fpm -D
killall php-fpm

同时可以将php-fpm加入开机启动

ln -sfv /usr/local/opt/php56/*.plist ~/Library/LaunchAgents
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.php56.plist

安装Nginx

brew install nginx

安装完毕后可以通过

nginx
nginx -s quit

启动和关闭,同时也支持重载配置文件等操作

nginx -s reload|reopen|stop|quit

nginx安装后默认监听8080端口,可以访问http://localhost:8080查看状态。如果要想监听80端口需要root权限,运行

sudo chown root:wheel /usr/local/Cellar/nginx/1.6.2/bin/nginx
sudo chmod u+s /usr/local/Cellar/nginx/1.6.2/bin/nginx

并使用root权限启动

sudo nginx

开机启动

ln -sfv /usr/local/opt/nginx/*.plist ~/Library/LaunchAgents
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist

Nginx + PHP-FPM配置

Nginx一般都会运行多个域名,因此这里参考了@fish的方法,按Ubuntu的文件夹结构来存放Nginx的配置文件

mkdir -p /usr/local/var/logs/nginx
mkdir -p /usr/local/etc/nginx/sites-available
mkdir -p /usr/local/etc/nginx/sites-enabled
mkdir -p /usr/local/etc/nginx/conf.d
mkdir -p /usr/local/etc/nginx/ssl

编辑Nginx全局配置

vim /usr/local/etc/nginx/nginx.conf
worker_processes  1;
error_log   /usr/local/var/logs/nginx/error.log debug;
pid        /usr/local/var/run/nginx.pid;

events {
    worker_connections  256;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] '
        '"$request" $status $body_bytes_sent '
        '"$http_referer" "$http_user_agent" '
        '"$http_x_forwarded_for" $host $request_time $upstream_response_time $scheme '
        '$cookie_evalogin';

    access_log  /usr/local/var/logs/access.log  main;

    sendfile        on;
    keepalive_timeout  65;
    port_in_redirect off;

    include /usr/local/etc/nginx/sites-enabled/*;
}

这样一来首先可以把一些可复用配置独立出来放在/usr/local/etc/nginx/conf.d下,比如fastcgi的设置就可以独立出来

vim /usr/local/etc/nginx/conf.d/php-fpm

内容为

location ~ \.php$ {
    try_files                   $uri = 404;
    fastcgi_pass                127.0.0.1:9000;
    fastcgi_index               index.php;
    fastcgi_intercept_errors    on;
    include /usr/local/etc/nginx/fastcgi.conf;
}

然后/usr/local/etc/nginx/sites-enabled目录下可以一个文件对应一个域名的配置,比如web服务器目录是/opt/htdocs

vim /usr/local/etc/nginx/sites-enabled/default
server {
    listen       80;
    server_name  localhost;
    root         /opt/htdocs/;

    location / {
        index  index.html index.htm index.php;
        include     /usr/local/etc/nginx/conf.d/php-fpm;
    }
}

此时启动了php-fpm并且启动了Nginx后,就可以通过http://localhost来运行php程序了

安装MySQL

brew install mysql

可以通过

mysql.server start
mysql.server stop

来启动/停止,启动后默认应为空密码,可以通过mysqladmin设置一个密码

mysqladmin -uroot password "mypassword"

但是在操作的时候出现了空密码无法登入的情况,最终只能通过mysqld_safe来设置

sudo mysqld_safe --skip-grant-tables
mysql -u root
mysql> UPDATE mysql.user SET Password=PASSWORD('mypassword') WHERE User='root';
mysql> FLUSH PRIVILEGES;

最后将MySQL加入开机启动

cp /usr/local/Cellar/mysql/5.6.22/homebrew.mxcl.mysql.plist ~/Library/LaunchAgents/

Memcache

brew install memcached

启动/停止指令

memcached -d
killall memcached

加入开机启动

cp /usr/local/Cellar/memcached/1.4.20/homebrew.mxcl.memcached.plist ~/Library/LaunchAgents/

Redis

brew install redis

Redis默认配置文件不允许以Deamon方式运行,因此需要先修改配置文件

vim /usr/local/etc/redis.conf

将daemonize修改为yes,然后载入配置文件即可实现后台进程启动

redis-server /usr/local/etc/redis.conf

加入开机启动

cp /usr/local/Cellar/redis/2.8.19/homebrew.mxcl.redis.plist ~/Library/LaunchAgents/ 

设置别名

最后可以对所有服务的启动停止设置别名方便操作

vim ~/.bash_profile

加入

alias nginx.start='launchctl load -w ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist'
alias nginx.stop='launchctl unload -w ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist'
alias nginx.restart='nginx.stop && nginx.start'
alias php-fpm.start="launchctl load -w ~/Library/LaunchAgents/homebrew.mxcl.php55.plist"
alias php-fpm.stop="launchctl unload -w ~/Library/LaunchAgents/homebrew.mxcl.php55.plist"
alias php-fpm.restart='php-fpm.stop && php-fpm.start'
alias mysql.start="launchctl load -w ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist"
alias mysql.stop="launchctl unload -w ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist"
alias mysql.restart='mysql.stop && mysql.start'
alias redis.start="launchctl load -w ~/Library/LaunchAgents/homebrew.mxcl.redis.plist"
alias redis.stop="launchctl unload -w ~/Library/LaunchAgents/homebrew.mxcl.redis.plist"
alias redis.restart='redis.stop && redis.start'
alias memcached.start="launchctl load -w ~/Library/LaunchAgents/homebrew.mxcl.memcached.plist"
alias memcached.stop="launchctl unload -w ~/Library/LaunchAgents/homebrew.mxcl.memcached.plist"
alias memcached.restart='memcached.stop && memcached.start'

安装其他项目支持

brew install composer node

安装Oh My Zsh

brew install zsh-completions
chsh -s /usr/local/bin/zsh
vim ~/.zshenv

加入内容

export PATH=/usr/local/bin:$PATH

然后

vim ~/.zshrc

加入内容

fpath=(/usr/local/share/zsh-completions $fpath)
autoload -Uz compinit
compinit -u

最后运行

rm -f ~/.zcompdump; compinit

查看正在使用的shell

dscl localhost -read Local/Default/Users/$USER UserShell

安装Oh My Zsh

wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | sh

参考

]]>
2015-01-20T17:55:48+0000 2015-01-22T06:37:11+0000 http://avnpc.com/p/193 AlloVince
<![CDATA[使用xhprof进行线上PHP性能追踪及分析]]> 之前一直使用基于Xdebug进行PHP的性能分析,对于本地开发环境来说是够用了,但如果是线上环境的话,xdebug消耗较大,配置也不够灵活,因此线上环境建议使用xhprof进行PHP性能追踪及分析

xhprof的安装与简易用法

xhprof是Facebook开源的轻量级PHP性能分析工具,Linux环境下可以通过pecl直接安装,比如在Ubuntu下仅需3行指令

pecl install xhprof-beta
echo "extension=xhprof.so" > /etc/php5/fpm/conf.d/xhprof.ini
service php5-fpm restart

之后可以通过phpinfo()检查扩展是否已经加载。

具体如何使用呢,xhprof项目中已经提供了示例以及简易的UI,下载xhprof项目到web服务器,假设可以通过http://localhost/xhprof/访问,那么访问http://localhost/xhprof/examples/sample.php可以看到一些输出,并且提示通过访问http://<xhprof-ui-address>/index.php?run=XXX&source=xhprof_foo查看结果。接下来访问http://localhost/xhprof/xhprof_html/就可以看到已经保存的结果,列出了所有函数的调用以及所消耗的时间。

分析一下示例代码sample.php,关键部分只有2行:

//开启xhprof并开始记录
xhprof_enable();
//运行一些函数
foo();
//停止记录并取到结果
$xhprof_data = xhprof_disable();

$xhprof_data中记录了程序单步运行过程中所有的函数调用时间及CPU内存消耗等,具体记录哪些指标可以通过xhprof_enable的入口参数控制,之后的处理已经与xhprof扩展无关,大致是编写了一个存储类XHProfRuns_Default,将$xhprof_data序列化并保存到某个目录,可以通过XHProfRuns_Default(__DIR__)将结果输出到当前目录,如果不指定则会读取php.ini配置文件中的xhprof.output_dir,仍然没有指定则会输出到/tmp

xhprof_html/index.php将记录的结果整理并可视化,默认的UI里列出了:

  • funciton name : 函数名
  • calls: 调用次数
  • Incl. Wall Time (microsec): 函数运行时间(包括子函数)
  • IWall%:函数运行时间(包括子函数)占比
  • Excl. Wall Time(microsec):函数运行时间(不包括子函数)
  • EWall%:函数运行时间(不包括子函数)

每一项应该不难理解,以项目自带的sample.php为例,示例中编写了一个main()函数,main()函数中调用foo()bar()等一些子函数进行了一点字符处理。整个程序运行过程中,main()函数只运行了一次,并且由于main()函数中包括了所有的逻辑,所以main()函数的IWall%占比为100%,但是由于main()函数的功能都是由子函数实现的,因此main()函数的EWall%只有0.3%,而foo()函数完成了主要的工作,EWall%有98.1%。因此在分析更大型的程序时,往往需要根据这几项指标分别排序,从不同的角度审视性能消耗。

xhprof_html/index.php中还可以看到[View Full Callgraph]链接,点击后可以绘制出一张可视化的性能分析图,如果点击后报错的话,可能是缺少依赖graphviz,ubuntu可以通过apt安装

apt-get install graphviz

更好的注入方式

了解了上面这些,其实就已经可以将xhprof整合到任何我们已有的项目中去了。目前大部分MVC框架都有唯一的入口文件,只需要在入口文件的开始处注入xhprof的逻辑

//开启xhprof
xhprof_enable(XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_CPU);
//在程序结束后收集数据
register_shutdown_function(function() {
    $xhprof_data        = xhprof_disable();

    //让数据收集程序在后台运行
    if (function_exists('fastcgi_finish_request')) {
        fastcgi_finish_request();
    }

    //保存xhprof数据
    ...
});

但是这样免不了要修改项目的源代码,其实php本身就提供了更好的注入方式,比如将上述逻辑保存为/opt/inject.php,然后修改php fpm配置文件

vi /etc/php5/fpm/php.ini

修改auto_prepend_file配置

auto_prepend_file = /opt/inject.php

这样所有的php-fpm请求的php文件前都会自动注入/opt/inject.php文件

如果使用Nginx的话,还可以通过Nginx的配置文件设置,这样侵入性更小,并且可以实现基于站点的注入。

fastcgi_param PHP_VALUE "auto_prepend_file=/opt/inject.php";

更好的分析工具:xhprof.io还是xhpgui

注入代码后我们还需要实现保存xhprof数据以及展示数据的UI,听起来似乎又是一大堆工作,有现成的轮子可以用吗?

经过搜索和比较,貌似比较好的选择有xhprof.io以及xhpgui

两个项目做得事情差不多,都提供了xhprof数据保存功能以及一套索引展示数据的UI,下面是一些比较

xhprof.io

  • ✗ 年久失修
  • ✗ 保存xhprof数据到MySQL
  • ✓ 支持域名、URI等多个维度的数据索引
  • ✓ 函数调用记录完整,内核级别函数都能显示
  • ✗ 无法针对个别URI开启
  • ✗ 注入被分割成两个文件,如果程序被强制中断时xhprof数据将无法收集

xhgui

  • ✓ 保存xhprof数据到MongoDB
  • ✗ 不支持域名索引
  • ✗ 函数调用记录不完整,部分内核级别函数(如扩展内)无法显示
  • ✓ 有配置文件可以控制开启条件
  • ✓ 注入只有一个文件
  • ✓ 狂拽酷炫的基于D3.js的调用关系动态图

可以看到其实两个项目都不够完善,相对而言xhgui不支持域名索引对于线上调试来说是无法忍受的,因此我最后的选择是使用xhprof.io,但是自己进行了微量的调整,修改后的xhprof.io修正版支持:

  • ✓ 增加开启开关配置,可以针对个别URI开启
  • ✓ 注入文件合并为一个

xhprof.io修正版安装与使用

安装及配置方法如下,假设web服务器根目录为/opt/htdocs

cd /opt/htdocs
git clone https://github.com/EvaEngine/xhprof.io.git
cd xhprof.io/
composer install
cp xhprof/includes/config.inc.sample.php xhprof/includes/config.inc.php 
vi xhprof/includes/config.inc.php

在MySQL中建立xhprof.io数据库,假设数据库名为xhprof,然后导入xhprof/setup/database.sql

配置文件config.inc.php中需要调整

  • 'url_base' => 'http://localhost/xhprof.io/', 这是xhprof.io界面所在路径
  • 'pdo' => new PDO('mysql:dbname=xhprof;host=localhost;charset=utf8', 'root', 'password'), 根据MySQL实际情况调整配置
  • enable 这是一个匿名函数,当匿名函数返回true时启用xhprof数据收集

通过配置enable项,就可以实现线上调试的需求,比如

始终开启xhprof

'enable' => function() {
    return true;
}

1/100概率随机开启xhprof

'enable' => function() {
    return rand(0, 100) === 1;
}

网页携带参数debug=1时开启xhprof

'enable' => function() {
    return !empty($_GET['debug']);
}

网页URL为特定路径时开启

'enable' => function() {
    return strpos($_SERVER['REQUEST_URI'], '/testurl') === 0;
}

最后按上文所述,在要配置的项目中包含xhprof.io/inc/inject.php即可。

线上环境操作时务必要胆大心细,如果没有结果尤其注意需要检查xhprof扩展是否安装

附录:xhpgui的安装方法

apt-get install mongodb php5-mongo php5-mcrypt
cp /etc/php5/mods-available/mcrypt.ini /etc/php5/fpm/conf.d/
cp /etc/php5/mods-available/mcrypt.ini /etc/php5/cli/conf.d/
cd /opt/htdocs
git clone https://github.com/perftools/xhgui.git
cd xhgui
composer install
cp config/config.default.php config/config.php
chown www-data.www-data -R cache

编辑Nginx配置文件加入

fastcgi_param PHP_VALUE "auto_prepend_file=/opt/htdocs/xhgui/external/header.php";

收集数据过多时可以清空mongodb

mongo
use xhprof;
db.dropDatabase();
]]>
2014-12-26T15:40:08+0000 2014-12-28T16:19:15+0000 http://avnpc.com/p/192 AlloVince
<![CDATA[Phalcon Framework的Mvc结构及启动流程(部分源码分析)]]> 很久没更新Blog甚是惭愧,但是工作方面还是有不少进展,技术方面一个重大的转变是我选择了Phalcon Framework作为未来一段时间的核心框架。技术选型的原因会单开一篇Blog另说,本次优先对Phalcon的MVC架构与启动流程进行分析说明,如有遗漏还望指出。

Phalcon本身有支持创建多种形式的Web应用项目以应对不同场景,包括迷你应用单模块标准应用、以及较复杂的多模块应用

本次以最复杂的多模块应用为例,Phalcon版本为1.3.2,用一个Phalcon所创建的标准项目来分析

创建项目

Phalcon环境配置安装后,可以通过命令行生成一个标准的Phalcon多模块应用

 phalcon project eva --type modules

入口文件为public/index.php,简化后一共5行,包含了整个Phalcon的启动流程,以下将按顺序说明

require __DIR__ . '/../config/services.php';
$application = new Phalcon\Mvc\Application();
$application->setDI($di);
require __DIR__ . '/../config/modules.php';
echo $application->handle()->getContent();

DI注册阶段

Phalcon的所有组件服务都是通过DI(依赖注入)进行组织的,这也是目前大部分主流框架所使用的方法。通过DI,可以灵活的控制框架中的服务:哪些需要启用,哪些不启用,组件的内部细节等等,因此Phalcon是一个松耦合可替换的框架,完全可以通过DI替换MVC中任何一个组件。

require __DIR__ . '/../config/services.php';

这个文件中默认注册了Phalcon\Mvc\Router(路由)、Phalcon\Mvc\Url(Url)、Phalcon\Session\Adapter\Files(Session)三个最基本的组件。同时当MVC启动后,DI中默认注册的服务还有很多,可以通过DI得到所有当前已经注册的服务:

$services = $application->getDI()->getServices();
foreach($services as $key => $service) {
        var_dump($key);
        var_dump(get_class($application->getDI()->get($key)));
}

打印看到Phalcon还注册了以下服务:

  • dispatcher : Phalcon\Mvc\Dispatcher 分发服务,将路由命中的结果分发到对应的Controller
  • modelsManager : Phalcon\Mvc\Model\Manager Model管理
  • modelsMetadata : Phalcon\Mvc\Model\MetaData\Memory ORM表结构
  • response : Phalcon\Http\Response 响应
  • cookies : Phalcon\Http\Response\Cookies Cookies
  • request : Phalcon\Http\Request 请求
  • filter : Phalcon\Filter 可对用户提交数据进行过滤
  • escaper : Phalcon\Escaper 转义工具
  • security : Phalcon\Security 密码Hash、防止CSRF等
  • crypt : Phalcon\Crypt 加密算法
  • annotations : Phalcon\Annotations\Adapter\Memory 注解分析
  • flash : Phalcon\Flash\Direct 提示信息输出
  • flashSession : Phalcon\Flash\Session 提示信息通过Session延迟输出
  • tag : Phalcon\Tag View的常用Helper

而每一个服务都可以通过DI进行替换。接下来实例化一个标准的MVC应用,然后将我们定义好的DI注入进去

$application = new Phalcon\Mvc\Application();
$application->setDI($di);

模块注册阶段

与DI一样,Phalcon建议通过引入一个独立文件的方式注册所有需要的模块:

require __DIR__ . '/../config/modules.php';

这个文件的内容如下

$application->registerModules(array(
    'frontend' => array(
        'className' => 'Eva\Frontend\Module',
        'path' => __DIR__ . '/../apps/frontend/Module.php'
    )
));

可以看到Phalcon所谓的模块注册,其实只是告诉框架MVC模块的引导文件Module.php所在位置及类名是什么。

MVC阶段

$application->handle()是整个MVC的核心,这个函数中处理了路由、模块、分发等MVC的全部流程,处理过程中在关键位置会通过事件驱动触发一系列application:事件,方便外部注入逻辑,最终返回一个Phalcon\Http\Response。整个handle方法的过程并不复杂,下面按顺序介绍:

基础检查

首先检查DI,如果没有任何DI注入,会抛出错误

A dependency injection object is required to access internal services

然后从DI启动EventsManager,并且通过EventsManager触发事件application:boot

路由阶段

接下来进入路由阶段,从DI中获得路由服务router,将uri传入路由并调用路由的handle()方法

路由的handle方法负责将一个uri根据路由配置,转换为相应的Module、Controller、Action等,这一阶段接下来会检查路由是否命中了某个模块,并通过Router->getModuleName()获得模块名。

如果模块存在,则进入模块启动阶段,否则直接进入分发阶段。

注意到了么,在Phalcon中,模块启动是后于路由的,这意味着Phalcon的模块功能比较弱,我们无法在某个未启动的模块中注册全局服务,甚至无法简单的在当前模块中调用另一个未启动模块。这可能是Phalcon模块功能设计中最大的问题,解决方法暂时不在本文的讨论范围内,以后会另开文章介绍。

模块启动

模块启动时首先会触发application:beforeStartModule事件。事件触发后检查模块的正确性,根据modules.php中定义的classNamepath等,将模块引导文件加载进来,并调用模块引导文件中必须存在的方法

  • Phalcon\Mvc\ModuleDefinitionInterface->registerAutoloaders ()
  • Phalcon\Mvc\ModuleDefinitionInterface->registerServices (Phalcon\DiInterface $dependencyInjector)

registerAutoloaders()用于注册模块内的命名空间实现自动加载。registerServices ()用于注册模块内服务,在官方示例中registerServices ()注册并定义了view服务以及模板的路径,并且注册了数据库连接服务db并设置数据库的连接信息。

模块启动完成后触发 application:afterStartModule事件,进入分发阶段

分发阶段(Dispatch)

分发过程由Phalcon\Mvc\Dispatcher(分发器)来完成,所谓分发,在Phalcon里本质上是分发器根据路由命中的结果,调用对应的Controller/Action,最终获得Action返回的结果。

分发开始前首先会准备View,虽然View理论上位于MVC的最后一环,但是如果在分发过程中出现任何问题,通常都需要将问题显示出来,因此View必须在这个环节就提前启动。Phalcon没有准备默认的View服务,需要从外部注入,在多模块demo中,View的注入官方推荐在模块启动阶段完成的。如果是单模块应用,则可以在最开始的DI阶段注入。

如果始终没有View注入,会抛出错误

Service 'view' was not found in the dependency injection container

导致分发过程直接中断。

分发需要Dispatcher,Dispatcher同样从DI中取得。然后将router中得到的参数(NamespaceName / ModuleName / ControllerName / ActionName / Params),全部复制到Dispatcher中。

分发开始前,会调用View的start()方法。具体可以参考View相关文档,其实Phalcon\Mvc\View->start()就是PHP的输出缓冲函数ob_start的一个简单封装,分发过程中所有输出都会被暂存到缓冲区。

分发开始前还会触发事件application:beforeHandleRequest

正式开始分发会调用Phalcon\Mvc\Dispatcher->dispatch()

Dispatcher内的分发处理

进入Dispatcher后会发现Dispatcher对整个分发过程进行了进一步细分,并且在分发的过程中会按顺序触发非常多的分发事件,可以通过这些分发事件进行更加细致的流程控制。部分事件提供了可中断的机制,只要返回false就可以跳过Dispatcher的分发过程。

由于分发中可以使用Phalcon\Mvc\Dispatcher->forward()来实现Action的复用,因此分发在内部会通过循环实现,通过检测一个全局的finished标记来决定是否继续分发。当以下几种情况时,分发才会结束:

  • Controller抛出异常
  • forward层数达到最大(256次)
  • 所有的Action调用完毕

渲染阶段 View Render

分发结束后会触发application:afterHandleRequest,接下来通过Phalcon\Mvc\Dispatcher->getReturnedValue()取得分发过程返回的结果并进行处理。

由于Action的逻辑在框架外,Action的返回值是无法预期的,因此这里根据返回值是否实现Phalcon\Http\ResponseInterface接口进行区分处理。

当Action返回一个非Phalcon\Http\ResponseInterface类型

此时认为返回值无效,由View自己重新调度Render过程,会触发application:viewRender事件,同时从Dispatcher中取得ControllerName / ActionName / Params作为Phalcon\Mvc\View->render()的入口参数。

Render完毕后调用Phalcon\Mvc\View->finish()结束缓冲区的接收。

接下来从DI获得resonse服务,将Phalcon\Mvc\View->getContent()获得的内容置入response。

当Action返回一个Phalcon\Http\ResponseInterface类型

此时会将Action返回的Response作为最终的响应,不会重新构建新的Response。

返回响应

通过前面的流程,无论中间经历了多少分支,最终都会汇总为唯一的响应。此时会触发application:beforeSendResponse,并调用

  • Phalcon\Http\Response->sendHeaders()
  • Phalcon\Http\Response->sendCookies()

将http的头部信息先行发送。至此,Application->handle()对于请求的处理过程全部结束,对外返回一个Phalcon\Http\Response响应。

发送响应

HTTP头部发送后一般把响应的内容也发送出去:

echo $application->handle()->getContent();

这就是Phalcon Framework的完整MVC流程。

流程控制

分析MVC的启动流程,无疑是希望对流程有更好的把握和控制,方法有两种:

自定义启动

按照上面的流程,我们其实完全可以自己实现$application->handle()->getContent()这一流程,下面就是一个简单的替代方案,代码中暂时没有考虑事件的触发。

//Roter
$router = $di['router'];
$router->handle();

//Module handle
$modules = $application->getModules();
$routeModule = $router->getModuleName();
if (isset($modules[$routeModule])) {
    $moduleClass = new $modules[$routeModule]['className']();
    $moduleClass->registerAutoloaders();
    $moduleClass->registerServices($di);
}

//dispatch
$dispatcher = $di['dispatcher'];
$dispatcher->setModuleName($router->getModuleName());
$dispatcher->setControllerName($router->getControllerName());
$dispatcher->setActionName($router->getActionName());
$dispatcher->setParams($router->getParams());

//view
$view = $di['view'];
$view->start();
$controller = $dispatcher->dispatch();
//Not able to call render in controller or else will repeat output
$view->render(
    $dispatcher->getControllerName(),
    $dispatcher->getActionName(),
    $dispatcher->getParams()
);
$view->finish();

$response = $di['response'];
$response->setContent($view->getContent());
$response->sendHeaders();
echo $response->getContent();

流程清单

为了方便查找,将整个流程整理为一个树形清单如下:

  • 初始化DI (config/services.php) $di = new FactoryDefault();
    • 设置路由 $di['router'] = function () {}
    • 设置URL $di['url'] = function () {}
    • 设置Session $di['session'] = function () {}
  • 初始化Application (public/index.php)
    • 实例化App $application = new Application();
    • 注入DI $application->setDI($di);
    • 注册模块 (config/modules.php) $application->registerModules()
  • 启动Application (ext/mvc/application.c) $application->handle()
    • 检查DI
    • E 触发事件 application:boot
    • 路由启动 $di['router']->handle()
    • 获得模块名 $moduleName = $di['router']->getModuleName(),如果没有则从 $application->getDefaultModule获取
    • 模块启动 (如果路由命中)
      • E 触发事件 application:beforeStartModule
      • 调用模块初始化方法 (Module.php) registerAutoloaders() 以及 registerServices()
      • E 触发事件 application:afterStartModule
    • 分发
      • 初始化View
      • 初始化Dispatcher,将Router中的参数复制到Dispatcher
      • 调用View View->start()开启缓冲区
      • E 触发事件 application:beforeHandleRequest
      • 开始分发 (etc/dispatcher.c) Dispatcher->dispatch()
        • E 触发事件 dispatch:beforeDispatchLoop
        • 循环开始单次分发
          • E 触发事件 dispatch:beforeDispatch
          • 根据Dispatcher携带的Module、Namespace、Controller、Action获得完整的类与方法名,如果找不到则触发事件 E dispatch:beforeException
          • E 触发事件 dispatch:beforeExecuteRoute
          • 调用Controller->beforeExecuteRoute()
          • 调用Controller->initialize()
          • E 触发事件 dispatch:afterInitialize
          • 调用Action方法
          • E 触发事件 dispatch:afterExecuteRoute
          • E 触发事件 dispatch:afterDispatch
        • Action内如果有forward(),开始下一次分发
      • E 全部分发结束,触发事件 dispatch:afterDispatchLoop
      • Application获得分发后的输出 $dispatcher->getReturnedValue()
      • E 触发事件 application:afterHandleRequest 分发结束
    • 渲染,Appliction如果从分发拿到Phalcon\Http\ResponseInterface类型的返回,则渲染直接结束
      • E 触发事件 application:viewRender 分发结束
      • 调用 Phalcon\Mvc\View->render(),入口参数为Dispatcher的 ControllerName / ActionName / Params
      • 调用 Phalcon\Mvc\View->finish()结束缓冲区的接收
    • 准备响应
      • Phalcon\Mvc\View->getContent()通过Phalcon\Http\Response->setContent()放入Response
      • E 触发事件 application:beforeSendResponse
      • 调用Phalcon\Http\Response->sendHeaders()发送头部
      • 调用Phalcon\Http\Response->sendCookies()发送Cookie
      • 将准备好的响应作为$application->handle()的返回值返回
  • 发送响应
    • echo $application->handle()->getContent();

MVC事件

Phalcon作为C扩展型的框架,其优势就在于高性能,虽然我们可以通过上一种方法自己实现整个启动,但更好的方式仍然是避免替换框架本身的内容,而使用事件驱动。

下面梳理了整个MVC流程中所涉及的可被监听的事件,可以根据不同需求选择对应事件作为切入点:

  • DI注入
    • application:boot 应用启动
  • 路由阶段
    • 模块启动
    • application:beforeStartModule 模块启动前
    • application:afterStartModule 模块启动后
  • 分发阶段
    • application:beforeHandleRequest进入分发器前
    • 开始分发
      • dispatch:beforeDispatchLoop 分发循环开始前
      • dispatch:beforeDispatch 单次分发开始前
      • dispatch:beforeExecuteRoute Action执行前
      • dispatch:afterExecuteRoute Action执行后
      • dispatch:beforeNotFoundAction 找不到Action
      • dispatch:beforeException 抛出异常前
      • dispatch:afterDispatch 单次分发结束
      • dispatch:afterDispatchLoop 分发循环结束
    • application:afterHandleRequest 分发结束
  • 渲染阶段
    • application:viewRender 渲染开始前
  • 发送响应
    • application:beforeSendResponse 最终响应发送前
]]>
2014-05-30T15:29:41+0000 2014-12-31T07:50:51+0000 http://avnpc.com/p/191 AlloVince
<![CDATA[《征服C指针》Web版]]> 声明:本文是配合《自制编程语言》一书出版,对书中所引用Web内容的整理及翻译,由北京图灵文化发展有限公司授权刊载,未经许可请勿转载。

原日文网页版“配列とポインタの完全制覇”与出版后的「C言語 ポインタ完全制覇」在内容上并不完全相同,这里对应截取中文简体版「C言語 ポインタ完全制覇」(《征服C指针》)的内容。前桥和弥著,吴雅明译,人民邮件出版社,2013年2月第1版。

写在前面

在C 语言的学习中,指针的运用被认为是最大的难关。

关于指针的学习,我们经常听到下面这样的建议:

“如果理解了计算机的内存和地址等概念,指针什么的就简单了。”

“因为C 是低级语言,所以先学习汇编语言比较好。”

果真如此吗?

正如那些C 语言入门书籍中提到的那样,变量被保存在内存的“某个地方”。为了标记变量在内存中的具体场所,C 语言在内存中给这些场所分配了 编号(地址)。因此,大多数运行环境中,所谓的“指针变量”就是指保存变量地址的变量。

到此为止的说明,所有人都应该觉得很简单吧。

理解“指针就是地址”,可能是指针学习的必要条件,但不是充分条件。 现在,我们只不过刚刚迈出了“万里长征的第一步”。

如果观察一下菜鸟们实际使用C指针的过程,就会发现他们往往会有如下困惑。

  • 声明指针变量int *a;……到这里还挺像样的,可是当将这个变量作为指针使用时,依然悲剧地写成了*a
  • 给出int &a;这样的声明(这里不是指C++编程)。
  • 啥是“指向int的指针”?不是说指针就是地址吗?怎么还有“指向int的指针”,“指向char的指针”,难道它们还有什么不同吗?
  • 当学习到“给指针加1,指针会前进2个字节或者4个字节”时,你可能会有这种疑问:“不是说指针是地址吗?这种情况下,难道指针不应该是前进1个字节吗?”
  • scanf()中,在使用%d的情况下,变量之前需要加上&才能进行传递。 为什么在使用%s的时候,就可以不加&
  • 学习到将数组名赋给指针的时候,将指针和数组完全混为一谈,犯下“将没有分配内存区域的指针当做数组进行访问”或者“将指针赋给数组”这样的错误。

出现以上混乱的情形,并不是因为没有理解“指针就是地址”这样的概念。其实,真正导演这些悲剧的幕后黑手是:

  • C 语言奇怪的语法
  • 数组和指针之间微妙的兼容性

某些有一定经验的C 程序员会觉得C 的声明还是比较奇怪的。当然也有一些人可能并没有这种体会,但或多或少都有过下面的疑问。

  • C 的声明中,[]*的优先级高。因此,char *s[10]这样的声明意为 “指向char的指针的数组”——搞反了吧?
  • 搞不明白double (*p)[3];void (*func)(int a);这样的声明到底应该怎样阅读。
  • int *a 中,声明a 为“指向int 的指针”。可是表达式中的指针变量前*却代表其他意思。明明是同样的符号,意义为什么不同?
  • int *aint a[]在什么情况下可以互换?
  • 空的[]可以在什么地方使用,它又代表什么意思呢?

本书的编写就是为了回答以上这样的问题。

很坦白地说,我也是在使用了C语言好几年之后,才对C的声明语法大彻大悟的。

世间的人们大多不愿意承认自己比别人愚笨,所以总是习惯性地认为“实际上只有极少的人才能够精通C语言指针”,以此安慰一下自己那颗脆弱的心。

例如,你知道下面的事实吗?

  • 在引用数组中的元素时,其实a[i]中的[]和数组毫无关系。
  • C里面不存在多维数组。

如果你在书店里拿起这本书,翻看几页后心想:“什么呀?简直是奇谈怪论!”然后照原样把书轻轻地放回书架。那么你恰恰需要阅读这本书。

有人说:“因为C语言是模仿汇编语言的,要想理解指针,就必须理解内存和地址等概念。”你可能会认为:

“指针”是C语言所特有的、底层而邪恶的功能。

其实并不是这样的。确实,“C指针”有着底层而邪恶的一面,但是,它又是构造链表和树等“数据结构”不可缺少的概念。如果没有指针,我们是 做不出像样的应用程序的。所以,凡是真正成熟的开发语言,必定会存在指针,如Pascal、Delphi、Lisp 和Smalltalk 等,就连Visual Basic 也存在指针。 早期的Perl 因为没有指针而饱受批评,从版本5 开始也引入了指针的概念。 当然,Java 也是有指针的。很遗憾,世上好像对此还存有根深蒂固的误解。

在本书中,我们将体验如何将指针真正地用于构造数据结构。

“指针”是成熟的编程语言必须具有的概念。

尽管如此,为什么C 的指针却让人感觉格外地纠结呢?理由就是,C语言混乱的语法,以及指针和数组之间奇怪的兼容性。

本书旨在阐明C 语言混乱的语法,不但讲解了“C 特有的指针用法”,还针对和其他语言共有的“普遍的指针用法”进行了论述。

解读C的声明

我认为像

int *hoge_p;

还有

int hoge[10];

这样的声明方式很奇怪。

对于这种程序的声明方式,可能也有很多人感觉不到有什么别扭的地方。那就再看下面的这个例子:

char *color_name[] = {
    “red”,
    “green”,
    “blue”,
};

这里声明了一个“指向char的指针的数组”。

正如2.3.2节中介绍的那样,可以像下面这样声明一个“指向将double作为参数并且返回int的函数的指针”,

int (*func_p)(double);

关于这样的声明,在K&R中有下面这样一段说明:

int *f();  /* f:返回指向int指针的函数*/

int (*pt)();  /* pf: 指向返回int的函数的指针*/

这两个声明最能说明问题。在这里,因为*是前置运算符,它的优先度低于(),为了让连接正确地进行,有必要加上括号。

首先,这段文字中有谎言。

声明中*()[]并不是运算符。在语法规则中,运算符的优先顺序是在别的地方定义的。

先将这个问题放在一边。如果你老老实实地去读这段文字,该会嘀咕“是不是搞反了”。如果说

int (*pf)();

是指向函数的指针,使用括弧先将星号(指针)括起来是不是很奇怪?

关于这个问题的答案,等你明白过来就会觉得非常简单。C语言本来是美国人开发的,最好还是用英语来读*

﹡在K&R中,登载了dcl这个解析C的声明的程序,同时也记载了程序的输出结果,但是日语版并没有对这一段进行翻译,而是一成不变地转载了英文原文。

以上的声明,如果将pf作为起点以英语的顺序来读,应该是下面这样,

pf is pointer to function returning int

翻译成中文,则为

pf为指向返回int的函数的指针。

POINT:用英语来读C的声明。

在这里,我告诉大家一个阅读C语言声明的方法:机械地向前读。

为了把问题变得更简单,我们在这里不考虑const和volatile。

  1. 首先着眼于识别符(变量名或者函数名)。
  2. 从距离识别符最近的地方开始,依照优先顺序解释派生类型(指针,数组,函数)。优先顺序说明如下,
    1. 用于整理声明内容的括弧
    2. 用于表示数组的[],用于表示函数的()
    3. 用于表示指针的*
  3. 解释完成派生型,使用“of”或“to”或“returning”将它们连接起来。
  4. 最后,追加类型指定符(在左边,int、double这些)。
  5. 英语不好的人,可以将顺序反过来用日语(或者中文) 解释。

数组元素个数和函数的参数属于类型的一部分。应该将它们作为附属于类型的属性进行解释。

比如,

int (*func_p)(double);

1) 首先着眼于识别符。

int (*func_p)(double);

英语的表达为:

func_p is

2) 因为存在括号,这里着眼于*。

int (*func_p)(double);

英语的表达为:

func_p is pointer to

3) 解释用于函数的(),参数是double。

int (*func_p)(double);

英语的表达为:

func_p is pointer to function(double) returning

4) 最后,解释类型指定符int。

int (*func_p)(double);

英语的表达为:

func_p is pointer to function(double) returning int

5) 翻译成中文:

func_p是指向返回int的函数的指针。

使用和上面相同的方式,我们在下面的表中解读各种各样的声明(表3-1),

表3-1 解读各种各样的C语言声明

C语言 英语的表达 中文的表现
int hoge; hoge is int hoge是int
int hoge[10]; hoge is array(元素数10) of int hoge是int的数组(元素数10)
int hoge[10][3]; hoge is array(元素数10) of array(元素数3) of int hoge是int数组(元素数10)的数组(元素数3)
int *hoge[10]; hoge is array(元素数10) of pointer to int hoge是指向int的指针的数组(元素数10)
int (*hoge)[3]; hoge is pointer to array(元素数3) of double hoge是指向int的数组(元素数3)的指针
int func(int a); func is function(参数为int a) returning int func是返回int的函数(参数是int a)
int (*func)(int a) func is pointer to function(参数为int a) returning int func_p是指向返回int的函数(参数为int a)的指针

正如大家看到的这样,C语言的声明不能从左往右顺序解读(无论是英语、中文,还是日语),而是左右来回地解读。

K&R中指出,在C语言中,变量的声明仿效结果表达式的语法。可是,勉强地去模拟本质上完全不同的事物,结果就是“四不像”。

“使声明的形式和使用的形式相似”是C(还有从C派生的C++,Java*等语言)特有的奇怪的语法

*其实,大部分Java的声明语法还是能做到这点的。

K&R中同时也记载了下面这段文字,

C的声明语法,特别是包含指向函数指针的语法,受到了严厉的批评。

在Pascal中,C的int hoge[10]可以这样声明,

var
    hoge: array[0..9] of integer

这种声明,从左向右用英语按顺序解读是完全没有问题的。

续・解读C的声明——const修饰符

const是通过ANSI C被追加的修饰符,它将类型修饰为“只读”。

名不符实的是,const__不一定代表常量__。const最主要被用于修饰函数的参数,将一个常量传递给函数是没有意义的。无论怎样,使用const修饰符(变量名),只意味着使其“只读”。

/* const参数的范例 */
char *strcpy(char *dest, const char *src);

strcpy是持有被const修饰的参数的范例,此时,所谓的“只读”是如何表现的呢?

做个实验应该很快就会明白,上面例子中的src这个变量没有定义为只读

char *my_strcpy(char *dest, const char *src)
{
    src = NULL;     ←即使对src赋值,编译器也没有报错
}

此时,成为只读的不是“src”,而是src所指向的对象。

char *my_strcpy(char *dest, const char *src)
{
    *src = ‘a’;     ←ERROR!!
}

如果将“src”和“src指向的对象”都定义为“只读”,可以写成下面这样,

char *my_strcpy(char *dest, const char * const src)
{
    src = NULL;     ←ERROR!!
    *src = ‘a’;     ←ERROR!!
}

在现实中,在指针作为参数的时候,const常用于将指针指向的对象设定为只读。

通常,C的参数都是传值。因此,无论被调用方将参数进行怎样的修改,都不会对调用方造成任何影响。如果想要影响调用方的变量(通过函数参数将函数内的一些值返回),你可以将指针作为参数传递给函数。

可是,上面的例子(my_strcpy)中,传递的是src这个指针。其本来的意图是想要传递字符串(也就是char的数组)的值,由于在C中数组是不能作为参数传递的,情非得已才不得不将指向初始元素的指针传递给函数(因为数组可能会很大,所以传递指针有益于提高程序的效率)。

产生的问题是,为了达到从函数返回值的这个目的,需要向函数传递一个指针,这种方式让人感觉有些混乱。

此时,考虑在原型声明中加入const,

尽管函数接受了作为参数的指针,但是指针指向的对象不会被修改。

也就是说,

函数虽然接受了指针,但是并不是意味着要向调用方返回值。

strcpy()的意图就是——src是它的输入参数,但是不允许修改它所指向的对象。

可以通过以下规则解读const声明,

  1. 遵从上一节中提到的规则,从标识符开始,使用英语由内向外按顺序解释下去。
  2. 一旦解释完毕的部分的左侧出现了const,就在当前位置追加read-only。
  3. 如果解释完毕的部分的左侧出现了类型指定符,并且其左侧存在const,姑且先去掉类型指定符,追加read-only。
  4. 在翻译成中文的过程中,英语不好的同学请注意:const修饰的是紧跟在它后面的单词

因此,

char * const src

可以解释成,

src is read-only pointer to char

src是指向char的只读的指针

char const *src

可以解释成,

src is pointer to read-only char

src是指向只读的char的指针

此外,容易造成混乱的是,

char const *src

const char *src

意思完全相同

将数组解读成指针

正如在前面翻来覆去提到的那样,在表达式中,数组可以解读成指针。

 int hoge[10];

以上的声明中,hoge等同于&hoge[0]

hoge原本的类型为“int的数组(元素数10)”,但并不妨碍将其类型分类“数组”变更为“指针”。

此外,数组被解读成指针的时候,该指针不能作为左值。

这个规则有以下的例外情况,

1) 数组为sizeof运算符的操作数

通过“sizeof表达式”的方式使用sizeof运算符的情况下,如果操作数是“表达式”,数组会被当成指针,此时,即使对数组使用sizeof,得到的结果也只是指针自身的长度。照理来分析,应该是这样的吧?可是,当数组成为sizeof的操作数时,“数组解读为指针”这个规则会被抑制,此时返回的是数组全体的尺寸。

2) 数组为&运算符的操作数

通过对数组使用&,可以返回指向整体数组的指针。在3.2.4中已经介绍了“指向数组的指针”。 这个规则已经被追加到ANSI C规则之中。此前的编译器,在对数组使用&的时候,大多会报错。因此,当时的程序在这一点上不会出现问题的。那么这个规则的制定有什么好处呢?我想应该是为了保持统一吧。

3) 初始化数组时的字符串常量

我们都知道字符串常量是“char的数组”,在表达式中它通常被解读成“指向char的指针”。其实,初始化char的数组时的字符串常量,作为在花括号中将字符分开写的初始化符的省略形式,会被编译器特别解释。

在初始化char的指针的时候,关于字符串常量的特别之处,需要引起注意。

数组和指针相关的运算符

本节内容请下载PDF查看。

]]>
2013-08-30T06:53:57+0000 2013-09-04T09:13:27+0000 http://avnpc.com/p/190 AlloVince
<![CDATA[《自制编程语言》相关资料]]> 自制编程语言

《自制编程语言》原书名为「プログラミング言語を作る」,从编程语言的原理讲起,手把手地带你从零开始自制编程语言:crowbar和Diksam。前者为基于语法树的无类型语言,后者为基于字节代码的静态语言。二者均具备四则运算、变量、条件转移、循环、函数说明、垃圾收集、面向对象、异常处理机制等功能。

《自制编程语言》原作者为前橋和弥,中文版由刘卓、徐谦(AlloVince)、吴雅明合译完成,北京图灵文化发展有限公司出版,现已出版。由于原版链接的资料多为作者的日语博客,考虑到本书读者大都没有日语基础,因此将《自制编程语言》资源下载以及一些相关日志翻译整理于此,希望可以帮助读者更方便的获取信息。

购买地址:

源代码下载

作者提供的源代码中,错误信息全部为日文,给许多读者的学习带来了一些不便。

因此译者刘卓特别翻译了代码中所有的错误信息,并提供中文版代码的打包下载。

本书所涉及到的所有源代码:

包含文件

压缩包内包含以下内容:

  • calc ……第2章计算器源代码
    • mycalc ……基于yacc/lex的计算器源代码
    • mycalc_ex ……基于yacc/lex的计算器扩展版源代码
    • llparser ……基于递归下降分析法自制计算器源代码
    • llparser_ex ……基于递归下降分析法自制计算器扩展版源代码
  • crowbar_book_0_1 ……基于分析树运行的语言crowbar ver.0.1。包含基本的语句、结构控制、函数
  • crowbar_book_0_2 ……基于分析树运行的语言crowbar ver.0.2。引入数组与GC
  • crowbar_book_0_3 ……基于分析树运行的语言crowbar ver.0.3。内部编码采用宽字符(可支持日语)
  • crowbar_book_0_4 ……基于分析树运行的语言crowbar ver.0.4。其他应用
  • diksam_book_0_1 ……基于静态字节码运行的语言Diksam ver.0.1。包含基本的语句、结构控制、函数
  • diksam_book_0_2 ……基于静态字节码运行的语言Diksam ver.0.2。引入数组
  • diksam_book_0_3 ……基于静态字节码运行的语言Diksam ver.0.3。引入分割源代码及类
  • diksam_book_0_4 ……基于静态字节码运行的语言Diksam ver.0.4。其他应用

编译方法

计算器的编译

Linux运行目录下的make.sh,Windows运行目录下的make.bat即可。

crowbar的编译

在文件夹中运行make(Windows下可参考书中建议,重命名为gmake)即可生成可执行文件。

Diksam的编译

对于diksam_book_0_1及diksam_book_0_2,解压后进入文件夹下的compiler目录,运行make/gmake即可。

对于diksam_book_0_3及diksam_book_0_4,解压后进入文件夹下的main目录,运行make/gmake即可。

MinGW, bison, flex安装说明

下文介绍的所有安装文件均以2013/8/23的最新版本为准。

MinGW的安装

MinGW的官方网站如下:

http://www.mingw.org/

首先进入MinGW的下载页面,会跳转到MinGW在sourceforge的项目页,Windows用户可以点击Installer,然后在列表中继续点击mingw-get-setup.exe。

MinGW MinGW

mingw-get-setup.exe并不包含实际的安装文件,而是通过网络下载MinGW的程序文件。请参考下面步骤安装:

MinGW

点击Install

MinGW

可在Installation Directory处点击Change修改安装路径,然后点击Continue。

MinGW

MinGW会下载初始安装文件,下载完成后点击Continue。

MinGW

安装界面会切换为MinGW Installation Manager,MinGW包含了多个编译组件,选择想要安装的组件,在弹出的菜单中选择Mark for Installation

《自制编程语言》一书中,我们只用到了make,因此只选择mingw32-base即可,当然如果磁盘空间富裕,完全可以全部选择,更加省事。

选择完毕后点击窗口菜单的Installation,选择Apply

MinGW

在弹出的确认框中点击Apply,MinGW会开始下载选择的安装包,整个下载过程视网速会占用数分钟的时间。下载完成即可以开始使用MinGW。

安装完毕后可以在安装路径下看到MinGW创建了一系列文件,其中bin目录是MinGW主要程序所在目录。例如解压crowbar的源码到c:,然后运行cmd如下编译crowbar_book_0_1(假设MinGW的安装目录为D:\MinGW):

cd C:\win_sjis\crowbar_book_0_1
C:\win_sjis\crowbar_book_0_1>D:\MinGW\bin\mingw32-make.exe

但是每次这样运行编译指令有点麻烦,所以作者推荐复制mingw32-make.exe到同目录下,并重命名为gmake.exe。然后将D:\MinGW\bin加入系统目录。就可以直接运行:

 gmake

进行编译了。

Cygwin的安装

Cygwin是一系列自由软件的集合,可以在Windows下实现与Linux几乎一致的运行环境。

为了在Windows上实现UNIX的系统内核,Cygwin提供了名为cygwin.dll的DLL文件来模拟Linux的内核API接口及相似功能。因此Linux源代码编译都依赖cygwin.dll文件,而通过Cygwin编译的exe文件如果脱离了包含cygwin.dll的系统环境也是无法运行的。

与MinGW一样,在Cygwin中也可以安装gcc,而在《自制编程语言》一书中,作者之所以选择MinGW编译,主要是因为作者希望无论crowbar还是Diksam,都可以最终编译为一个exe文件,只需要拷入U盘就能在任意电脑运行,不受cygwin.dll的限制。

而这里介绍Cygwin的安装,是由于bison所使用的m4以及crowbar_ver.0.4所使用的“鬼车”,都依赖于Cygwin环境。

Cygwin的官方主页为:

http://www.cygwin.com/

进入主页后,可根据自己的系统环境选择下载32bit版本setup-x86.exe或64bit版本的setup-x86_64.exe

下载后按以下方法操作(以64bit为例)

Cygwin

点击下一步

Cygwin

选择Install from Internet,点击下一步

Cygwin

在Root Directory中选择安装路径,点击下一步

Cygwin

在Local Package Directory中选择安装包的下载路径,以后卸载或重新安装时可以从安装包直接获取,无需重新下载。点击下一步

Cygwin

一般选择Direct Connection,如果网络环境特殊可根据具体情况设置代理。点击下一步

Cygwin

选择下载节点,国内一般选择163节点即可。Cygwin会首先下载所有安装包信息。

Cygwin

选择要下载的模块安装包,Cygwin已经为我们默认选择了一部分基础功能

Cygwin

这里我们为了使用bison,需要在Interpreters下勾选m4。而为了编译鬼车,需要勾选Devel下的make。

Cygwin会开始下载所有的安装包进行安装,同时在安装目录下生成Linux典型的目录结构。安装完毕后会在桌面生成Cygwin64 Terminal的快捷方式,双击之后就可以运行模拟的Linux环境了。

bison的安装

bison最新版可以在下面的页面下载:

http://gnuwin32.sourceforge.net/packages/bison.htm

一般选择Complete package, except sources一栏的下载即可,下载后为exe格式安装文件,一路next安装后即可运行。

flex的安装

flex最新版可以在下面的页面下载:

http://gnuwin32.sourceforge.net/packages/flex.htm

一般选择Complete package, except sources一栏的下载即可,下载后为exe格式安装文件,一路next安装后即可运行。

鬼车的安装

鬼车是一个正则表达式程序库,官方主页为:

http://www.geocities.jp/kosako3/oniguruma/

截至目前(2013/8/23)为止最新版本为5.9.4。进入主页后点击Latest release version 5.9.4即可下载

鬼车在Linux下的安装

鬼车默认的系统环境为Linux。在Linux下可以很简单的使用几行指令编译安装

wget http://www.geocities.jp/kosako3/oniguruma/archive/onig-5.9.4.tar.gz
tar -xvf onig-5.9.4.tar.gz
cd onig-5.9.4
./configure
make
make install

鬼车在Windows下的安装

对于Windows用户来说,我们需要借助Cygwin环境模拟Linux对其进行编译。

首先按照上文介绍的步骤安装好Cygwin,由于鬼车要求make指令名必须为make,因此首先进入MinGW的bin目录,复制mingw32-make.exe并重命名为make.exe。然后打开cmd。假设onig-5.9.4.tar.gz文件下载到C:\。运行以下指令进行解压并检查环境。

C:\>tar -xvf onig-5.9.4.tar.gz
C:\>cd onig-5.9.4
C:\onig-5.9.4>bash
bash-3.1$  ←命令提示符会切换为bash
./configure
make

理论上只要在Linux环境下,像鬼车这样基于Autoconf打包发布的软件都可以用与Linux相同的指令make install进行安装。

但是由于Windows下MinGW安装路径比较特殊,直接运行make install很可能报错。这里需要手动更新鬼车静态链接用库文件ranlib.a的索引。运行

bash-3.1$ cd .libs/
bash-3.1$ ranlib libonig.a

然后将libonig.a文件复制到MinGW安装目录下的lib文件夹内。最后运行

make install

Hoge一词的由来

在《自制编程语言》一书中,示例程序中经常使用hoge作为无意义字符串输出,而在英语程序则惯用foobar

hoge写成日语假名是“ほげ”。那么究竟hoge是什么意思,为什么要用hoge作为无意义字符的的代表呢?作者对此进行了一系列深入细致的考证与研究。由于其中涉及到很多日本的人名、地名、风俗习惯等等,这里只选择一些中国读者比较容易理解的部分翻译整理如下。

Hoge是什么意思

Hoge是日本自古以来就口口相传的词汇,在为难、困惑、或者遇到麻烦事的时候都可以用Hoge来求救。据传Hoge一词最早的正式登场是在20世纪80年代前半,在日本各地突然同时开始流行。而使用“Hego”一词可供考证的最早记录,则是1984年曽田大明在名古屋大学所使用的某张3.5寸软盘中录入的Hoge,说不定现在还能找到呢。

注:其实上面这些看似一本正经的说明大多都是作者在恶搞而已,Hoge在日语中是一个非常口语化的词,根本无从考证。类比中文相当于要考证“我靠”一词的起源一样。而上文所说的“曽田大明”,应该是作者拿自己的某位同学在开涮吧。

什么时候使用Hoge

  • 在为文件、函数、变量命名而苦恼时可以使用Hoge来命名。
  • 问别人一件事的时候,如果听不懂对方的回答,那么不要重复问题,简单说一个Hoge就可以了。
  • 有时候面对电脑不知道输入些什么,那么不妨试试输入Hoge吧

谁在使用Hoge

  • 藤子不二雄的《哆啦A梦》:胖虎“动人”的歌唱声中必定会伴有Hoge的字眼
  • 动画《多罗罗》的歌词中出现了Hoge
  • 新潟县的方言中一直有Hogeru的说法

《征服C指针》Web版

请点击查看《征服C指针》Web版

声明:

原日文网页版“配列とポインタの完全制覇”与出版后的「C言語 ポインタ完全制覇」在内容上并不完全相同,这里对应截取中文简体版「C言語 ポインタ完全制覇」(《征服C指针》)的内容。前桥和弥著,吴雅明译,人民邮件出版社,2013年2月第1版。

附录:《自制编程语言》日文版页面链接

]]>
2013-08-24T10:15:38+0000 2014-01-15T11:23:04+0000 http://avnpc.com/p/189 AlloVince
<![CDATA[Ubuntu12.04使用Dotdeb安装PHP5.4 / Nginx1.4/Redis2.6等新版本]]> 众所周知,Ubuntu使用apt-get默认安装的软件版本都偏低,目前Ubuntu12.04安装的PHP版本为PHP Version 5.3.10-1ubuntu3.7,Nginx、Redis等常用软件版本也都非常保守。而这对于个人开发而言,要尝试新版本特性还需要编译安装解决依赖问题,实在不够方便。

Dotdeb就为Debian系提供了一个非常好的高版本更新源,由个人维护,但是更新非常快,使用Detdeb在Ubuntu12.04上就可以默认安装PHP5.4 / Nginx1.4 / Redis2.6等更新的版本

用法也非常简单:

vi /etc/apt/sources.list

在更新源中加入

#dotdeb
deb http://packages.dotdeb.org squeeze all
deb-src http://packages.dotdeb.org squeeze all
deb http://packages.dotdeb.org squeeze-php54 all
deb-src http://packages.dotdeb.org squeeze-php54 all

然后添加密钥

wget http://www.dotdeb.org/dotdeb.gpg
cat dotdeb.gpg | sudo apt-key add -

就可以通过apt-get update更新了。

不过在实际使用中发现在安装php5-mysql等一些PHP扩展时报错依赖版本不够,要求libmysqlclient16 (>= 5.1.21-1),此时可能需要手动安装

wget https://launchpad.net/ubuntu/+archive/primary/+files/libzip1_0.9.3-1_amd64.deb
sudo dpkg -i libzip1_0.9.3-1_amd64.deb

wget http://launchpadlibrarian.net/94808408/libmysqlclient16_5.1.58-1ubuntu5_amd64.deb
sudo dpkg -i libmysqlclient16_5.1.58-1ubuntu5_amd64.deb

目前使用Dotdeb在Ubuntu12.04安装的PHP版本为PHP Version 5.4.17-1~dotdeb.0

]]>
2013-08-19T06:13:00+0000 2013-08-19T06:13:22+0000 http://avnpc.com/p/188 AlloVince