典型案例

1. Socket 断开后会收到 SIGPIPE 类型的信号,假如不处理会 crash

同事问了我一个问题,说收到一个 crash 信息,去 mpaas 渠道看到如下的 crash 信息

几个 iOS 端底层网络问题

看了代码,显现在某某文件的313行代码,代码如下

几个 iOS 端底层网络问题

Socket 属于网络最底层的完成,一般咱们开发不需求用到,可是用到了就需求小心翼翼,比方 Hook 网络层、长链接等。检查官方文档会说看到一些说明。

当运用 socket 进行网络衔接时,假如衔接中断,在默许情况下, 进程会收到一个 SIGPIPE 信号。假如你没有处理这个信号,app 会 crash。

Mach 现已通过反常机制提供了底层的陷进处理,而 BSD 则在反常机制之上构建了信号处理机制。硬件产生的信号被 Mach 层捕捉,然后转换为对应的 UNIX 信号,为了保护一个一致的机制,操作体系和用户产生的信号首先被转换为 Mach 反常,然后再转换为信号。

Mach 反常都在 host 层被 ux_exception 转换为相应的 unix 信号,并通过 threadsignal 将信号投递到出错的线程。

几个 iOS 端底层网络问题

有2种解决办法:

  • Ignore the signal globally with the following line of code.(在全局范围内疏忽这个信号 。缺点是所有的 SIGPIPE 信号都将被疏忽)

    signal(SIGPIPE, SIG_IGN);
    
  • Tell the socket not to send the signal in the first place with the following lines of code (substituting the variable containing your socket in place of sock)(告诉 socket 不要发送信号:SO_NOSIGPIPE)

    int value = 1;
    setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, &value, sizeof(value));
    

SO_NOSIGPIPE 是一个宏界说,跳过去看一下完成

#define SO_NOSIGPIPE  0x1022     /* APPLE: No SIGPIPE on EPIPE */

什么意思呢?没有 SIGPIPE 信号在 EPIPE。那啥是 EPIPE

其中:EPIPE 是 socket send 函数可能回来的错误码之一。假如发送数据的话会在 Client 端触发 RST(指Client端的 FIN_WAIT_2 状况超时后衔接现已毁掉的情况),导致send操作回来 EPIPE(errno 32)错误,并触发 SIGPIPE 信号(默许行为是 Terminate)。

What happens if the client ignores the error return from readline and writes more data to the server? This can happen, for example, if the client needs to perform two writes to the server before reading anything back, with the first write eliciting the RST.

The rule that applies is: When a process writes to a socket that has received an RST, the SIGPIPE signal is sent to the process. The default action of this signal is to terminate the process, so the process must catch the signal to avoid being involuntarily terminated.

If the process either catches the signal and returns from the signal handler, or ignores the signal, the write operation returns EPIPE.

UNP(unix network program) 建议应用根据需求处理 SIGPIPE信号,至少不要用体系缺省的处理方式处理这个信号,体系缺省的处理方式是退出进程,这样你的应用就很难查办处理进程为什么退出。对 UNP 感兴趣的能够检查:www.unpbook.com/unpv13e.tar…

下面是2个苹果官方文档,描述了 socket 和 SIGPIPE 信号,以及最佳实践:

Avoiding Common Networking Mistakes

Using Sockets and Socket Streams

可是线上的代码还是存在 Crash。查了下代码,发现奔溃仓库在 PingFoundation 中的 sendPingWithData。也就是虽然在 AppDelegate 中设置疏忽了 SIGPIPE 信号,可是还是会在某些函数下「重置」掉。

- (void)sendPingWithData:(NSData *)data {
    int                     err;
    NSData *                payload;
    NSData *                packet;
    ssize_t                 bytesSent;
    id<PingFoundationDelegate>  strongDelegate;
    // ...
    // Send the packet.
    if (self.socket == NULL) {
        bytesSent = -1;
        err = EBADF;
    } else if (!CFSocketIsValid(self.socket)) {
        //Returns a Boolean value that indicates whether a CFSocket object is valid and able to send or receive messages.
        bytesSent = -1;
        err = EPIPE;
    } else {
        [self ignoreSIGPIPE];
        bytesSent = sendto(
                           CFSocketGetNative(self.socket),
                           packet.bytes,
                           packet.length,
                           SO_NOSIGPIPE,
                           self.hostAddress.bytes,
                           (socklen_t) self.hostAddress.length
                           );
        err = 0;
        if (bytesSent < 0) {
            err = errno;
        }
    }
    // ...
}
- (void)ignoreSIGPIPE {
    int value = 1;
    setsockopt(CFSocketGetNative(self.socket), SOL_SOCKET, SO_NOSIGPIPE, &value, sizeof(value));
}
- (void)dealloc {
    [self stop];
}
- (void)stop {
    [self stopHostResolution];
    [self stopSocket];
    // Junk the host address on stop.  If the client calls -start again, we'll 
    // re-resolve the host name.
    self.hostAddress = NULL;
}

也就是说在调用 sendto() 的时候需求判别下,调用 CFSocketIsValid 判别当前通道的质量。该函数回来当前 Socket 目标是否有用且能够发送或者接纳音讯。之 前的判别是,当 self.socket 目标不为 NULL,则直接发送音讯。可是有种情况就是 Socket 目标不为空,可是通道不可用,这时候会 Crash。

Returns a Boolean value that indicates whether a CFSocket object is valid and able to send or receive messages.

if (self.socket == NULL) {
    bytesSent = -1;
    err = EBADF;
} else {
    [self ignoreSIGPIPE];
    bytesSent = sendto(
                        CFSocketGetNative(self.socket),
                        packet.bytes,
                        packet.length,
                        SO_NOSIGPIPE,
                        self.hostAddress.bytes,
                        (socklen_t) self.hostAddress.length
                        );
    err = 0;
    if (bytesSent < 0) {
        err = errno;
    }
}   

2. 设备无可用空间问题

最早遇到这个问题,直观的判别是某个接口地点的服务器机器,呈现了存储问题(由于查了代码是网络回调存在 Error 的时候会调用咱们公司基础),由于不是安稳必现,所以也就没怎么注重。直到后来发现线上有商家反应这个问题最近经常呈现。通过排查该问题该问题 Error Domain=NSPOSIXErrorDomain Code=28 "No space left on device" 是体系报出来的,开启 Instrucments Network 面板后看到显现 Session 过多。为了将问题复现,定时器去触发“切店”逻辑,切店则会触发首页所需的各个网络恳求,则能够复现问题。工程中查找 NSURLSession 创立的代码,将问题定位到某几个底层库,HOOK 网络监控的能力上。一个是 APM 网络监控,确定 APMM 网路监控 Session 创立是收敛的,另一个库是动态域名替换的库,之前呈现过线上故障。所以思考之下,暂时将这个库发布热修代码。之前是采用“失望战略”,99%的概率不会呈现故障,然后献身线上每个网络的功能,添加一道流程,而且该流程的完成还存在问题。思考之下,采用达观战略,假设线上大概率不会呈现故障,保留2个办法。线上呈现故障,马上发布热修,调用下面的办法。

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    return NO;
}
//下面代码保留着,以防热修复运用
+ (BOOL)open_canInitWithRequest:(NSURLRequest *)request {
    // 代理网络恳求
} 

问题暂时解决后,后续动态域名替换的库能够参考 WeexSDK 的完成。见 WXResourceRequestHandlerDefaultImpl.m。WeexSDK 这个代码完成考虑到了多个网络监听目标的问题、且考虑到了 Session 创立多个的问题,是一个合理解法。

- (void)sendRequest:(WXResourceRequest *)request withDelegate:(id<WXResourceRequestDelegate>)delegate
{
    if (!_session) {
        NSURLSessionConfiguration *urlSessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
        if ([WXAppConfiguration customizeProtocolClasses].count > 0) {
            NSArray *defaultProtocols = urlSessionConfig.protocolClasses;
            urlSessionConfig.protocolClasses = [[WXAppConfiguration customizeProtocolClasses] arrayByAddingObjectsFromArray:defaultProtocols];
        }
        _session = [NSURLSession sessionWithConfiguration:urlSessionConfig
                                                 delegate:self
                                            delegateQueue:[NSOperationQueue mainQueue]];
        _delegates = [WXThreadSafeMutableDictionary new];
    }
    NSURLSessionDataTask *task = [_session dataTaskWithRequest:request];
    request.taskIdentifier = task;
    [_delegates setObject:delegate forKey:task];
    [task resume];
}