不久前php的小伙伴过来说需要推送消息到几个app上,php单进程推送的效率不高,希望能尝试用c来实现。网上查了一下,有php、python、java的,没找到c的实现,可能对于c的开发者来说,这只是个小功能,不值得一提吧。而我还是想做下笔记,因为我也曾迷惑过几天。
我的实现也很简单,大概如下:
- ios开发的同学给我pem格式的证书和密码,不同app对应不同的pem证书,该证书里有证书内容和私钥
- web后台管理的同学会生成一条推送消息,推送消息里会指定给哪个app下发;不同app的推送消息,需要使用该app对应的证书
- 建立多线程,可配置,目前是15个
- 数据库里已经有不同app的token,因此我只需根据推送消息指定的app,从db里找出该app的所有token,再把token平均分配到每个线程
- 每个线程获得token,每个token在该线程里分配一个唯一的id
- 在每个线程里,使用pem证书跟苹果的推送服务器建立ssl长连接,使用的是socket的阻塞模型(blocking)
- 根据苹果的协议,组装推送消息。一个token组装一个消息包,消息内容包含有该token的id,能根据id反向查找该id对应的token
- 线程里逐个发送消息,发送之后通过select来等待socket fd的可读事件,select的等待超时时间目前设置为25ms,这个时间可配置
- 若该消息被苹果接纳,select是等不到读事件的,直到超时返回(所以这个超时时间会影响推送效率),超时后返回(7)继续发送下一个token
- 若是select返回,解包获取id。需要重新建立ssl长连接(返回(6)),并且该id之后的token需要重新发送。因为等待时间为25ms,导致写的速度比读要快多了,该次select等待获得的回复消息,可能是前几个token的
- 若是写失败,继续尝试写
ps:
- ssl里使用非阻塞模型比较麻烦,所以最后使用了阻塞等待,因此每次ssl_write写socket之后,都需要通过select来超时等待读事件
- 苹果回复消息之后会同时断开连接,下次发送需要重新连接ssl连接
- 多线程里使用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个:
- 无效token的数量。一旦遇到无效token,苹果返回消息,断开连接,重新建立ssl连接的时间开销很明显。至于无效token的清理,对效率不高,php那边的小伙伴心情好的时候会做清理:)
- select超时等待的时间
- 线程数量
另外,我尝试使用openssl的session,可惜获取不到session,初步怀疑是苹果服务器上没有启用session。
其他的内容,苹果已经说得很详细了: Local and Push Notification Programming Guide
apns的详细流程: Local and Push Notification Programming Guide
– EOF –