去年八月份写过一个 ios 消息批量推送的小程序(ios消息并发推送),使用的是 blocking IO, 效率一般,进程还可能长时间 block 在某次 IO 上。
最近有空,做了下优化,主要包括:
- 使用 non-blocking IO,也即 non-blocking SSL 读和写,这次主要也是为了实践 SSL non-block 编程
- 尽可能排除无效 token,推送过程能收到应答,知道哪些是无效 token,在推送后清理这些 token,避免下次推送再遇到
值得一提的事项则有如下:
在我尝试 non-blocking SSL 读写的实践中,建立 socket 连接、建立 ssl 连接,倒还是阻塞的方式,只是在读写推送数据时,才修改为非阻塞模式。因为建立 SSL 连接时会有一个握手过程,非阻塞模式需要中断几次,设置 SSL 让其静默地完成握手,简化逻辑
在 non-blocking ssl 常见
SSL_WANT_*
的提示,表示应用层要等待(或阻塞),等待 SSL 从底层的 socket IO 中读写数据(可能是进行握手,可能是当前 SSL 缓冲区里的数据还不足够进行加解密)。这时应用层可通过select
来等待读写事件,且在读写事件中,应用层要继续重试上次阻塞的SSL_read
或SSL_write
。 所以要记录上次是否遇到SSL_WANT_*
事件,且在当此进行正确的 SSL 读写。具体可见 《Network Security with OpenSSL》Chapter 5. SSL/TLS Programming
中的5.2.2.3 Non-blocking I/O
,以下的 non-blocking SSL 读写模式,也是来自这部分章节。苹果提供了一个简单粗暴的应答 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 的数量来缓解这个情况。在上述 2 和 3 的细节下,当
SSL_read
返回SSL_WANT_*
时,ssl 将可能一直无法得到满足,会陷入一个死循环。因此我放弃当此应答,无法做到 100% 的可靠。在发送最后一个 token 后,应检查是否有应答
一些 sample 如下:
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; }
设置非阻塞
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; }
通过 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; } }
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.
– EOF –