在这个系列里,我们自己动手搭建一个可以向APN发送请求的Provider。为什么要这样做呢?一来,这算是构建App的整个环节中,应该了解的一个技术细节;二来,尽管有不少线上推送的服务,但它们的API各有区别,无论使用哪家,都没有充分的代表性。
所以,还是自己撸一个吧。
服务器环境
首先,从要搭建的环境说起。我们这个最简单的Provider包含三个部分,分别是Nginx, MySQL和PHP。其中:
- Nginx用于接收App上传的Token以及设备信息;
- MySQL用于存储设备数据;
- PHP用于为每一个iOS设备生成APN需要的推送请求数据,并给APN发送请求;
把它们与iOS设备和APN之间的关系以及各自的功能用一张图表示,就是这样的:

为什么没有用Vapor呢
看到这你可能会想,等等,为什么服务端程序选择了PHP而没有用Vapor呢?答案很简单,因为向APN发送通知请求的时候,我们要使用HTTP /2的长连接特性,以保证每台设备的推送数据都通过同一个TCP连接发送。否则,每发送一台设备的推送请求,就要新建一个TCP连接,Apple会认为这是某种形式的DoS攻击,进而阻止我们继续向APN发送消息。
不过遗憾的是,目前Foundation并没有对HTTP /2的长连接支持,而Vapor在底层使用的Swift NIO http2也还处在beta阶段。为了上线的时候确保这个服务可用,我们暂时没有使用Vapor,而是选择了更为成熟的Laravel来实现这部分的功能。
使用Docker搭建环境
虽然在技术上没有选择Vapor,不过我们的环境构建过程,和之前构建Vapor环境是非常类似的。因此,我们索性就把构建这种环境的过程一般化一些,让它变成一个可以部署Vapor/PHP这两种环境的脚本。大家可以在GitHub上获取完整的构建脚本。
目录结构
接下来,我们就从目录结构开始。把它用一张图表示,就是这样的:

其中:
- 根目录的docker-compose.yml是部署MySQL和Nginx容器的脚本,而docker-compose.php.yml和docker-compose.vapor.yml则分别是部署PHP和Vapor容器的脚本,稍后,我们会介绍如何把这些脚本拼接起来构建不同的环境;
- dbs用于存放MySQL数据库的文件,稍后我们会把这个目录作为一个Volume映射到MySQL容器,避免不慎删掉了容器,数据库就丢了;
- mysql / nginx / php / echo-server / redis / vapor子目录中,包含的则是构建各自容器的脚本。可以看到,除了Dockerfile之外,这里还包含了一些用到的辅助脚本。例如:
- nginx目录中的php-default / vapor-default分别是在PHP和Vapor环境中的站点设置文件;
- php目录中的install_composer.sh用于在PHP容器中安装
composer
工具,xdebug.ini用于支持在Host上通过PhpStorm进行调试; - echo-server包含了服务器端推送的配置文件;
- projects子目录中是我们在这个环境中要运行的各种项目;
- 最后,在根目录中还有一个隐藏文件:.env.demo文件,它是这个docker环境的配置文件模板,我们可以基于它复制出多份配置,例如我就创建了apn.env和bx.env。然后,只要在根目录中创建一个.env的符号链接,让它指向不同的配置,就可以方便地切换项目环境了;
现在,大家只要对这些东西有一个印象就行,稍后,我们会在用到的时候,逐一详细地进行解释。
使用.env定义环境
了解了环境的整体结构之后,我们从配置文件模板说起。对于即将构建的APN provider来说,它的配置文件是这样的:
HOST_ROOT=./projects/push-notifications
CONTAINER_ROOT=/var/www/push-notifications/current
HOST_STORAGE_ROOT=./projects/push-notifications/storage
CONTAINER_STORAGE_ROOT=/var/www/push-notifications/current/storage
HOST_DB_ROOT=./dbs/apn
MYSQL_ROOT_PASSWORD=apns
DB_NAME=apn
DB_USER=homestead
DB_PASSWORD=secret
HOST_HTTP_PORT=80
HOST_DB_PORT=33060
HOST_REDIS_PORT=6379
HOST_ECHO_PORT=6001
XDEBUG_CONFIG="remote_host=docker.for.mac.host.internal remote_port=9001"
PHP_IDE_CONFIG="serverName=APN-demo"
CURRENT_PHP_IMG=boxue/php:0.1.0
CURRENT_DB_IMG=boxue/mysql:0.1.1
CURRENT_NGINX_IMG=boxue/nginx:0.1.6
CURRENT_VAPOR_IMG=boxue/vapor:0.1.1
CURRENT_REDIS_IMG=boxue/redis:0.1.0
CURRENT_ECHO_IMG=boxue/echo:0.1.0
在这个配置文件里,有两类值。一类是和项目相关的,例如:
HOST_ROOT
表示host上项目文件所在的目录;CONTAINER_ROOT
表示项目目录映射到容器中的目录;MYSQL_ROOT_PASSWORD
表示MySQL容器数据库的root密码;DB_NAME / DB_USER / DB_PASSWORD
表示项目使用的数据库名称以及访问账号;
另一类,是容器使用的镜像版本信息。当我们修改了容器的构建方式重新build
的时候,可以在这里,基于修改的幅度,调整这些用CURRENT_
开头的版本号。
至于其中我们还没有提到的配置,则和具体的容器应用相关,我们等用到的时候再专门解释。
容器的配置细节
了解了.env的内容之后,我们来看容器自身的配置。这里的绝大多数内容,在基于docker的Vapor开发环境这个系列中已经说过了,因此就不再重复了。如果你还不太熟悉Docker,那么应该先去看一下对应的内容。这里,着重说一些我们会用到的配置细节。
Nginx
首先,是Nginx容器的部分,在nginx/php-default中可以找到下面的配置:
server_name apn.boxue.io;
location / {
try_files $uri $uri/ /index.php$is_args$query_string;
}
这里,我们的Provider域名是apn.boxue.io,把它设置成server_name
。另外,try_files
中,访问/index.php
的部分一定要带上$is_args$query_string
这两个参数。否则,Nginx就无法处理URL中形如?key=value
这样的参数了。
接下来,为了让Nginx把请求转发到PHP容器,我们还要在php-default配置文件中添加下面的内容。其中最关键的,就是fastcgi_pass php:9000;
,这里的php
是我们在docker-compose.php.yml中,为PHP容器定义的名字:
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
PHP
其次,是PHP容器的部分,在php/Dockerfile里,主要是对两个PHP默认配置文件的修改。一个是/etc/php/7.2/fpm/pool.d/www.conf。这里,把PHP-FPM的监听方式从文件改成网络:
RUN sed -i '/listen = \/run\/php\/php7\.2-fpm\.sock/d' \
/etc/php/7.2/fpm/pool.d/www.conf
RUN echo 'listen = php:9000' >> /etc/php/7.2/fpm/pool.d/www.conf
可以看到,listen
的地址,和刚在在Nginx中配置的是一样的。
另外一处要修改的配置文件是/etc/php/7.2/fpm/php.ini,我们设置了时区,并把PHP-FPM的错误日志重定向到控制台:
RUN sed -i '/^;date\.timezone =/d' /etc/php/7.2/fpm/php.ini
RUN sed -i '/^;display_errors = stderr/d' /etc/php/7.2/fpm/php.ini
RUN echo 'date.timezone = Asia/Shanghai' >> /etc/php/7.2/fpm/php.ini
RUN echo 'display_errors = stderr' >> /etc/php/7.2/fpm/php.ini
这样,当前我们会使用的容器配置部分,就说完了。
在Host上安装Laravel
最后一个准备工作,是在Host上创建Laravel应用。如果之前你还没装过Laravel安装工具,可以在这里找到具体的安装方法,我们就不重复了。
接下来,在projects子目录,执行laravel new push-notifications
创建项目模板。完成后,进入push-notifications目录,执行:
composer install -vvv
npm install
安装依赖关系。这样,所有的准备工作就结束了。
启动容器环境
接下来,我们启动这个容器环境试一下。一开始的时候我们说过,在环境的根目录中,我们把docker composer的配置文件拆成了docker-compose.yml和docker-compose.php.yml,如何把这两个文件中的配置当成同一个环境启动呢?
其实很简单,在执行docker-compose
命令的时候,通过-f
参数依次指定它们就好了。例如,构建镜像的时候,可以这样:
docker-compose -f docker-compose.yml -f docker-compose.php.yml build
构建完成后,执行下面的命令启动这个环境:
docker-compose -f docker-compose.yml -f docker-compose.php.yml up
简单来说,就是一旦使用了多个-f
配置,在使用docker-compose
命令的时候,就要始终带着这些配置文件。否则,得到的结果,就有可能丢失信息了。如果一切正常,在容器全部启动完后,先编辑下Host上的/etc/hosts
文件,添加一条下面的记录:
127.0.0.1 apn.boxue.io
然后,在浏览器中访问apn.boxue.io
,如果可以看到下面的结果,一切就都准备就绪了:

What's next?
搞定了这个基础的环境之后,为了方便接下来的开发,下段视频里,我们介绍一个在PhpStorm和VScode中接入XDebug的方法。这样,就可以方便地单步调试代码,而不用反复的var_dump
输出结果了。