前因:
公司的新版ios app已经成功上架了、运营们跑过来就说啦:“我们这周五要推送push消息,你们的push后台什么时候能弄好啊”

困难:
其实松林也没有做过苹果push、原来公司有个push后台,看了下源代码,用的是原始的推送(数据库取出所有deviceToken,然后用foreach循环一个苹果的socket连接推送),这种做法是有一个很大的弊端的、
1:苹果的socket连接一次只能推送100~1000左右(原来测试过,忘了是在多少条的时候断开链接的,不会超过1000)
2:一个socket链接循环多条,如果deviceToken有一条失效的,那么在这条deviceToken之后的都不会推送成功

公司原来的这种做法排除php超时的情况下、deviceToken肯定不会都推送完毕的。当时公司的deviceToken获取到3000左右~。问了下公司的同事,他们也是foreach循环推送。

既然知道一个socket对象推送有什么弊端之后就很容易做了,以下是松林的解决方案

公司app上架没两个星期,deviceToken已经保存了近16w的数据。肯定是需要php进程执行了。
涉及到进行,那么放到队列里面是最合适的,松林用的是Redis的list类型进行存储推送消息内容

当运营人员在后台编辑完推送内容之后,php会将所有deviceToken在mysql查询出来foreach存入Redis中(包括推送内容),然后编写一个php从Redis中取数据并发送的脚本(每次都建立苹果连接),php脚本每次开启运行5分钟、5分钟以后自动退出。然后编辑个shell脚本,循环执行php脚本20次(次数根据deviceToken多少进行增加),最后将shell脚本编辑到crontab中运行、至于运行时间设置为php脚本执行时间最好、这样就相当于不间断取数据推送。这样做的好处就是预防php超时

代码如下
php后台编辑内容入redis的代码我就不发了,发一下php脚本代码

<?php
require_once("MQTaskHelper.php");
class Push{

	/**
	*回调推送消息
	*@params json $info push的json值、
	*/
	public function sendPush($info){
		$info = json_decode($info,true);
		$fp = self::getIosObj();
		if($fp==false)
			return false;


		//消息内容
		$message = $info['sContext'];
		//badge我也不知是什么
		$badge = 4;
		//sound我也不知是什么(或许是推送消息到手机时的提示音)
		$sound = 'default';
		//建设的通知有效载荷(即通知包含的一些信息)
		$body = array();
		$body['aps'] = array(
				'alert' => $message,
				'sound' => 'default',
				'openType' => $info['iType'],
				'openId'	=> strval($info['iPushid']),
				'link'		=> $info['iPushid'],
			);


		$payload = json_encode($body);
		$msg = chr(0) . pack("n",32) . pack('H*', str_replace(' ', '', $info['token'])) . pack("n",strlen($payload)) . $payload;
		fwrite($fp, $msg);
		fclose($fp);
	}

	/**
	*获取IOS推送对象
	*/
	public static function getIosObj($pass='123456'){
		$ctx = stream_context_create();
		$pem = dirname(__FILE__) .'/'.'push-dev2.pem';
		stream_context_set_option($ctx, 'ssl', 'local_cert', $pem);  
		stream_context_set_option($ctx, 'ssl', 'passphrase', $pass);
		$fp = stream_socket_client('ssl://gateway.push.apple.com:2195',$err,$errstr, 60, STREAM_CLIENT_CONNECT, $ctx);
		if(!$fp)
			return false;
		else
			return $fp;
	}
}
$pushKey = 'ios_push_user'; //Redis Key
$time    = 300;  			//执行时间

$obj = new push;
MQTaskHelper::consume($pushKey,array($obj,'sendPush'),$time,['host'=>'127.0.0.1']);

MQTaskHelper.php 类

   <?php
class MQTaskHelper
{
    public static $redisObj = null;

    public static function publish($mixMessage,$strKey,$arrOptions=array()){
        $oRedis = self::_getRedisObj($arrOptions);
        return $oRedis->listPush($strKey,$mixMessage);
    }

    public static function consume($strKey,$strCallback,$iRunTime = 60,$arrOptions=array()){

        $oRedis = self::_getRedisObj($arrOptions);

        //开始时间
        $iStartTime = time();

        while(true){

            $iCurrentTime = time();
            if($iCurrentTime - $iStartTime >= $iRunTime){
                print_r("[".date('Y-m-d H:i:s')."] I worked from["
                    .date('Y-m-d H:i:s',$iStartTime)."] to ["
                    .date('Y-m-d H:i:s',$iCurrentTime)."]. Quiting!\n");
                exit;
            }

            $mixMessage = $oRedis->lPop($strKey);

            if(!$mixMessage){
                continue;
            }else{

                try{
                    call_user_func($strCallback, $mixMessage);

                }catch(Exception $ex){
                    print_r("mq_consume_exception:".$ex->getMessage()."\n");
                }

            }
        }
    }


    private static function _getRedisObj($arrOptions)
    {
        if(empty(self::$redisObj)){
            self::$redisObj = new Redis();
            self::$redisObj->connect($arrOptions['host'],6379);
        }
        return self::$redisObj;
    }


}

服务器push脚本设置

   */5 * * * * /bin/bash   /data/xxx/push/push.sh

push.sh

for i in {1..10}
do
/data/xxx/php/bin/php -f /data/xxx/push/push.php &
done

基本推送了15w条的时间在1小时左右吧、如果觉得慢的话,多开几个进程、或者多弄几台服务器都是可以的。
至此结束