乐趣区

关于ios:几个-iOS-端底层网络问题

典型案例

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

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

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

Socket 属于网络最底层的实现,个别咱们开发不须要用到,然而用到了就须要小心翼翼,比方 Hook 网络层、长链接等。查看官网文档会说看到一些阐明。

当应用 socket 进行网络连接时,如果连贯中断,在默认状况下,过程会收到一个 SIGPIPE 信号。如果你没有解决这个信号,app 会 crash。

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

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

有 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<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];
}
退出移动版