几个 iOS 端底层网络问题

典型案例

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

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

几个 iOS 端底层网络问题_第1张图片

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

几个 iOS 端底层网络问题_第2张图片

Socket 属于网络最底层的实现,一般我们开发不需要用到,但是用到了就需要小心翼翼,比如 Hook 网络层、长链接等。查看官方文档会说看到一些说明。

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

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

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

几个 iOS 端底层网络问题_第3张图片

有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 感兴趣的可以查看:http://www.unpbook.com/unpv13...

下面是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  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)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];
}

你可能感兴趣的:(几个 iOS 端底层网络问题)