最让撰写推送程序感到头疼的是应对那些无效的设备令牌,不进行清理的话每次都停滞于途中,想要清理却又不清楚哪些是无效的。去年八月所撰写的那个版本运用了阻塞式IO,效率不仅无法提升,还常常卡顿在某次读写上无法动弹,用户体验实在难以让人接受。趁着近期有空余时间,我将其整体进行了重构,转而采用非阻塞SSL读写,总算把这点棘手的问题解决了。
一个最大的问题是阻塞式IO会一直持续下去,要是碰到网络震动情况,或是苹果服务器回应速度迟缓,那整个进程就只能在那里干巴巴地等待着。此次转变为非阻塞读写,其核心的想法是仅仅在数据发送接收的环节运用非阻塞的模式,连接搭建以及SSL握手依旧保持阻塞的方式。如此这般,握手时期的繁杂逻辑不用重新去进行设计,还能够把读写出现卡顿的痛点得以解决。按照实际测试的结果来看,进程阻塞的时间降低了超出百分之八十。
SSL握手过程原本就繁杂,需要往来返回调换证书,商量研讨加密套件,倘若采用非阻塞类型,一回握手就得中断停顿展开多次重试。代码当中得维持维护状态机,处置处理各类多种重入情景情况,复杂度径直直接成倍增加。所以呢我还是使得让连接以及和SSL握手遵循依照阻塞流程程序,等连接稳定平稳之后再将把套接字切换转变到非阻塞模式类型。这样既简化简略化了代码逻辑条理,又可以够享受到非阻塞IO的益处好处。
int do_ssl_connect_blocking(SSL *ssl, int fd)
{
int flags, ret;
SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);
flags = fcntl(fd, F_GETFL, 0);
flags &= ~O_NONBLOCK;
fcntl(fd, F_SETFL, flags);
ret = SSL_set_fd(ssl, fd);
if (ret != 1) {
return -1;
}
ret = SSL_connect(ssl);
if (ret != 1) {
return -1;
}
return 0;
}
有着非阻塞特性的SSL读写,其中最让人觉得棘手的是那个标注为SSL_ERROR_WANT_READ以及SSL_ERROR_WANT_WRITE的错误码,单单从表面上去看,似乎是出现了错误状况,然而实际上它是在向应用层传达底层尚未准备妥当的信息。在这样的时刻,需要借助select或者epoll来对套接字事件展开监控,一直等到可读或者可写的条件得以满足之后,才再次去尝试执行上次遭遇失败的SSL读写操作。我在代码里面特意增设了一个标志位,用于记录上次进入阻塞状态的原因所在,以此来保障在进行重试的时候能够准确无误地进入正确的分支。
int my_ssl_set_non_blocking(SSL *ssl)
{
int fd = SSL_get_fd(ssl);
if (fd < 0) {
return -1;
}
// remove auto-retry from ssl
long mode;
mode = SSL_get_mode(ssl);
mode &= ~SSL_MODE_AUTO_RETRY;
SSL_set_mode(ssl, mode);
// set no-blocking for socket fd
int flags;
flags = fcntl(fd, F_GETFL, 0);
flags |= O_NONBLOCK;
fcntl(fd, F_SETFL, flags);
return 0;
}
苹果所拥有的APNS服务,其设计呈现出颇为粗暴的态势,在发送完应答包过后,便即刻将连接予以关闭。通过抓包能够发现,它在最后一个包当中,同时携带了FIN、PSH、ACK这几种标志,当应用层回复ACK之后,苹果那边直接返回了一个RST,根本不给能够进行优雅关闭的机会。更为关键的是,要是在一批推送里面存在无效令牌,苹果给出的应答仅仅会告知你第几个存在问题,却不会说明你之前已经发送出去的那些里头,究竟哪些需要进行重传。
int check_availability(int sockfd, unsigned int *can_read, unsigned int *can_write)
{
*can_read = 0;
*can_write = 0;
fd_set rset;
fd_set wset;
struct timeval timeout = {60, 0};
int n;
FD_ZERO(&rset);
FD_ZERO(&wset);
FD_SET(sockfd, &rset);
FD_SET(sockfd, &wset);
n = select(sockfd+1, &rset, &wset, NULL, &timeout);
if (n == -1) {
return -1;
}
else if (n) {
if (FD_ISSET(sockfd, &rset))
*can_read = 1;
if (FD_ISSET(sockfd, &wset))
*can_write = 1;
return 1;
}
else {
// timeout
return 0;
}
}
假如有一次发出了100个推送,当中第50个令牌是无效的,苹果会于第50个之后返回错误应答。然而问题存在,应用层有可能已经发到第80个了,此时收到应答表明第50个无效,那么前面50到80之间的那些令牌究竟应不应该重发呢?要是丢失了应答,那就会更加麻烦,根本没法知道从哪一个位置开始重传。我曾经试过记录每个令牌的状态,可是SSL层频繁返回WANT错误时,很容易陷于死循环,最终不得不放弃百分百可靠这个目标。
int data_transfer(SSL *ssl, int send_cnt)
{
// set non-blocking for socket and ssl
my_ssl_set_non_blocking(ssl);
int sockfd = SSL_get_fd(ssl);
// ssl_read
unsigned int can_read = 0;
// ssl read retry flag
unsigned int read_waiton_read = 0;
unsigned int read_waiton_write = 0;
// ssl_write
unsigned int can_write = 0;
// ssl write retry flag
unsigned int write_waiton_read = 0;
unsigned int write_waiton_write = 0;
// read buffer
int len_rd = 0;
char buf_rd[MAX_BUFF_SIZE];
// write buffer
int len_wr = 0;
char buf_wr[MAX_BUFF_SIZE];
int ret_val = -1;
int ret, sslerrno;
int timeout_cnt = 0;
while (send_cnt >= 0) {
// get socket I/O event flag: can_read or can_write or both
ret = check_availability(sockfd, &can_read, &can_write);
if (ret < 0) {
return -1;
}
else if (ret == 0) {
// bad network condition
timeout_cnt ++;
if (timeout_cnt >= 3) {
goto end;
}
}
else {
timeout_cnt = 0;
}
// ssl read
// try ssl read first if can both read and write
//if (!(write_waiton_read || write_waiton_write)
// && (can_read || (can_write && read_waiton_read))
// && len_rd < MAX_BUFF_SIZE)
if (can_read || (can_write && read_waiton_read))
{
// clear ssl_read retry flag
read_waiton_read = 0;
read_waiton_write = 0;
ret = SSL_read(ssl, buf_rd + len_rd, MAX_BUFF_SIZE - len_rd);
sslerrno = SSL_get_error(ssl, ret);
switch (sslerrno) {
case SSL_ERROR_NONE:
len_rd += ret;
if (len_rd >= RSP_MSG_LEN) {
// parse and consume RSP_MSG_LEN
// ... ...
// get rsp id to reset token queue
// ... ...
len_rd -= RSP_MSG_LEN;
}
goto end;
case SSL_ERROR_WANT_WRITE:
read_waiton_write = 1;
goto end;
case SSL_ERROR_WANT_READ:
read_waiton_read = 1;
goto end;
case SSL_ERROR_ZERO_RETURN:
// connection closed
goto end;
default:
goto end;
}
} // ssl read
// ssl write
if (!(read_waiton_read || read_waiton_write)
&& (can_write || (can_read && write_waiton_write)))
{
// clear ssl_write retry flag
write_waiton_read = 0;
write_waiton_write = 0;
if (len_wr == 0) {
// get next token from token queue
// create push msg, set to buf_wr
// ... ...
}
ret = SSL_write(ssl, buf_wr, len_wr);
sslerrno = SSL_get_error(ssl, ret);
switch (sslerrno) {
/* We wrote something*/
case SSL_ERROR_NONE:
len_wr -= ret;
if (len_wr == 0) {
send_cnt --;
}
else {
memmove(buf_wr, buf_wr + ret, len_wr);
}
break;
case SSL_ERROR_WANT_WRITE:
write_waiton_write = 1;
break;
case SSL_ERROR_WANT_READ:
write_waiton_read = 1;
break;
case SSL_ERROR_ZERO_RETURN:
//rollback token, resend token
// ... ...
goto end;
default:
//rollback token, resend token
// ... ...
goto end;
}
} // ssl write
} // while
end:
SSL_shutdown(ssl);
close(sockfd);
return ret_val;
}
在当下,单连接每秒钟能够处理的推送数量,七八千条左右,与苹果文档所提及的九千条相比,存在着差距。文档之中指出,连接复用以及错误重试逻辑方面,具备很大的可优化空间。接下来,打算将令牌队列转变为批量确认机制,以此来削减应答等待所产生的开销。另外,存在一个槽点,那就是APNS的错误处理极为不透明,一旦出现错误,只能依靠猜测,社区里吐槽这一情况的文章,随便一抓就有很多。要是你正在编写类似的工具,是否遇到过更加奇葩的坑呢?欢迎在评论区分享你的踩坑经历,点个赞,使得更多人能够看到这些实战经验。