ios消息推送的优化
Toc
  1. 最近有空,做了下优化,主要包括:
  2. 值得一提的事项则有如下:
  3. 一些 sample 如下:

去年八月份写过一个 ios 消息批量推送的小程序(ios消息并发推送),使用的是 blocking IO, 效率一般,进程还可能长时间 block 在某次 IO 上。

最近有空,做了下优化,主要包括:

  1. 使用 non-blocking IO,也即 non-blocking SSL 读和写,这次主要也是为了实践 SSL non-block 编程
  2. 尽可能排除无效 token,推送过程能收到应答,知道哪些是无效 token,在推送后清理这些 token,避免下次推送再遇到

值得一提的事项则有如下:

  1. 在我尝试 non-blocking SSL 读写的实践中,建立 socket 连接、建立 ssl 连接,倒还是阻塞的方式,只是在读写推送数据时,才修改为非阻塞模式。因为建立 SSL 连接时会有一个握手过程,非阻塞模式需要中断几次,设置 SSL 让其静默地完成握手,简化逻辑

  2. 在 non-blocking ssl 常见 SSL_WANT_* 的提示,表示应用层要等待(或阻塞),等待 SSL 从底层的 socket IO 中读写数据(可能是进行握手,可能是当前 SSL 缓冲区里的数据还不足够进行加解密)。这时应用层可通过 select 来等待读写事件,且在读写事件中,应用层要继续重试上次阻塞的 SSL_readSSL_write。 所以要记录上次是否遇到 SSL_WANT_* 事件,且在当此进行正确的 SSL 读写。具体可见 《Network Security with OpenSSL》 Chapter 5. SSL/TLS Programming 中的 5.2.2.3 Non-blocking I/O,以下的 non-blocking SSL 读写模式,也是来自这部分章节。

  3. 苹果提供了一个简单粗暴的应答 The Feedback Service。经过我抓包发现,苹果发送应答后立即关闭 socket,且 socket 连接不会进入 TIME_WAIT 阶段(苹果发送的最后一个包里包含有FIN, PSH, ACK, 然后应用层在解析到 FIN 包后会回复一个 ACK 包,但这时收到的却是 RST 包)。
    应用层可能丢失该应答,该应答本来是指向一个无效 token 的,在该无效 token 之后的那些 token 们需要重新发送,而丢失的应答导致无法正确重新发送。 因为应用层在收到应答或知道 socket 被关闭之前,已经发送了 n 个 token, 那个无效的 token 位于 1 ~ n 之间,假设为 m(1<m<n), 第 m+1 个到 n 的这批 token,将会被苹果丢弃(即 m+1 到 n 的这批 token 将不会收到当次推送)。需要尽可能地减少无效 token 的数量来缓解这个情况。

  4. 在上述 2 和 3 的细节下,当 SSL_read 返回 SSL_WANT_* 时,ssl 将可能一直无法得到满足,会陷入一个死循环。因此我放弃当此应答,无法做到 100% 的可靠。

  5. 在发送最后一个 token 后,应检查是否有应答

一些 sample 如下:

  1. ssl 连接

     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;    
     }
    
  2. 设置非阻塞

     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;
     }
    
  3. 通过 select 检查可读写

     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;
         }
     }
    
  4. ssl 读写,消息数据的格式、token 队列的实现,这里就不展开了,替换为... ...

     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;
     }
    

当前的性能上还是不够好,有文档上提到:

If you’re seeing throughput lower than 9,000 notifications per second, your server might benefit from improved error handling logic.


  1. 文档:Technical Note TN2265 Troubleshooting Push Notifications

  2. 一个吐槽:The Problem With Apples Push Notification Service… Solutions and Workarounds…



– EOF –

Categories: c
Tags: c,ios,openssl