ios消息并发推送

不久前php的小伙伴过来说需要推送消息到几个app上,php单进程推送的效率不高,希望能尝试用c来实现。网上查了一下,有php、python、java的,没找到c的实现,可能对于c的开发者来说,这只是个小功能,不值得一提吧。而我还是想做下笔记,因为我也曾迷惑过几天。

我的实现也很简单,大概如下:

  1. ios开发的同学给我pem格式的证书和密码,不同app对应不同的pem证书,该证书里有证书内容和私钥
  2. web后台管理的同学会生成一条推送消息,推送消息里会指定给哪个app下发;不同app的推送消息,需要使用该app对应的证书
  3. 建立多线程,可配置,目前是15个
  4. 数据库里已经有不同app的token,因此我只需根据推送消息指定的app,从db里找出该app的所有token,再把token平均分配到每个线程
  5. 每个线程获得token,每个token在该线程里分配一个唯一的id
  6. 在每个线程里,使用pem证书跟苹果的推送服务器建立ssl长连接,使用的是socket的阻塞模型(blocking)
  7. 根据苹果的协议,组装推送消息。一个token组装一个消息包,消息内容包含有该token的id,能根据id反向查找该id对应的token
  8. 线程里逐个发送消息,发送之后通过select来等待socket fd的可读事件,select的等待超时时间目前设置为25ms,这个时间可配置
  9. 若该消息被苹果接纳,select是等不到读事件的,直到超时返回(所以这个超时时间会影响推送效率),超时后返回(7)继续发送下一个token
  10. 若是select返回,解包获取id。需要重新建立ssl长连接(返回(6)),并且该id之后的token需要重新发送。因为等待时间为25ms,导致写的速度比读要快多了,该次select等待获得的回复消息,可能是前几个token的
  11. 若是写失败,继续尝试写

ps:

  1. ssl里使用非阻塞模型比较麻烦,所以最后使用了阻塞等待,因此每次ssl_write写socket之后,都需要通过select来超时等待读事件
  2. 苹果回复消息之后会同时断开连接,下次发送需要重新连接ssl连接
  3. 多线程里使用openssl,使用锁吧:openssl和多线程

最新的一次推送,给21w个token推送消息,看日志,大约用了7分钟,暂时效率上还能被接受(起15个线程,服务器cpu为:Intel(R) Xeon(R) CPU E5606 @2.13GHz)。

每个ios设备有一个唯一的mac地址,根据该mac地址可以从苹果获得一个唯一的token,token目前的长度为64字节,按苹果的协议,需要把该64字节转成32字节的二进制格式,转换的demo如下(长度硬编码了):

int get_device_token_binary(const char *token, char *binary)
{
    uint8_t value;

    char *p = (char*)token;
    char *r = binary;

    char high, low;

    int i;
    for(i = 0; i < 64; i += 2) {
        // big endian, get high first
        high = *(p+i);
        low = *(p+i+1);
        //printf("[%c][%c] ", high, low);

        if (high >= '0' && high <= '9'){
            high -= '0';
        }else if (high >= 'a' && high <= 'f'){
            high -= ('a' - 10);
        }

        if (low >= '0' && low <= '9'){
            low -= '0';
        }else if (low >= 'a' && low <= 'f'){
            low -= ('a' - 10);
        }

        value = (high << 4) | (low << 0);
        //printf("value[%d]\n", value);

        memcpy(r++, &value, sizeof(uint8_t));
    }

    return 0;
}

ssl的读写大概为:

顶部reconnect: 建立ssl长连接;

while (还有token没发送)
{
    组装消息包;
    ret = ssl_write(消息)
    sslerrno = SSL_get_error(ssl, ret);
    switch(sslerrno){
        case SSL_ERROR_NONE:
            // 发送成功
            break;
        case SSL_ERROR_WANT_WRITE:
            重新发送;
            break;
        case SSL_ERROR_WANT_READ:
            重新发送;
            break;
        default:          
            遇到ssl错误,回到顶部,重新连接,再重新发送
    }

    select 最多等待25ms;

    if (有消息返回)
    {
        ret = SSL_read(ssl, s2c, MAX_BUFF_SIZE);
        if (ret > 0)
        {
            读到回复消息,解包,获取id;然后回到顶部,重新连接ssl,id之后的token重新发送
        }
        sslerrno = SSL_get_error(ssl, ret);
        switch(sslerrno){
            case SSL_ERROR_ZERO_RETURN:
                连接已断开,回到顶部重连;
            case SSL_ERROR_WANT_READ:
                continue;
            case SSL_ERROR_WANT_WRITE:
                continue;
            default:
                遇到ssl错误,回到顶部,重新连接
        }
    }
}

是的,如果读的时候遇到ssl错误,导致读取失败,那就麻烦了,这时候不知道是哪个id发送失败!!!

目前影响效率的因素有3个:

  1. 无效token的数量。一旦遇到无效token,苹果返回消息,断开连接,重新建立ssl连接的时间开销很明显。至于无效token的清理,对效率不高,php那边的小伙伴心情好的时候会做清理:)
  2. select超时等待的时间
  3. 线程数量

另外,我尝试使用openssl的session,可惜获取不到session,初步怀疑是苹果服务器上没有启用session。

其他的内容,苹果已经说得很详细了: Local and Push Notification Programming Guide

apns的详细流程: Local and Push Notification Programming Guide



– EOF –

Categories: c
Tags: c,ios,openssl