头图

几个 iOS 端底层网络问题

典型案例

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

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

2021-04-06-NetworkFatlError.png

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

2021-04-06-NetworkFatlError.png

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

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

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

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

Mach 异常处理以及转换为 Unix 信号的流程

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

无线前端技术
关于无线前端开发的各种技术。包括多端融合能力、动态化、工程化、SDK 输出。iOS、Hybrid、H5、JS、Node...

95 - iOS - 养了4只布偶猫

1.1k 声望
5.1k 粉丝
0 条评论
推荐阅读
深度剖析 Runtime
做很多需求或者是技术细节验证的时候会用到 Runtime 技术,用了挺久的了,本文就写一些场景和源码分析相关的文章。先问几个小问题:class_rw_t的结构是数组,数组里面的元素是数组,那它是二维数组吗?为什么16字...

杭城小刘4阅读 1.2k

封面图
网易云音乐开源全链路埋点方案-曙光埋点(dawn)
网易云音乐开源了曙光埋点 dawn,一个跨多端的全链路埋点解决方案,旨在构造一个完美的数据理想国。曙光埋点创造性的提出了埋点虚拟树(VTree),并在此基础上实现了诸多能力,解决大前端侧埋点困难、精度差、不...

云音乐技术团队3阅读 2.5k

封面图
深度剖析 Runtime
做很多需求或者是技术细节验证的时候会用到 Runtime 技术,用了挺久的了,本文就写一些场景和源码分析相关的文章。先问几个小问题:class_rw_t的结构是数组,数组里面的元素是数组,那它是二维数组吗?为什么16字...

杭城小刘4阅读 1.2k

封面图
“老默我想吃鱼了”与五层网络模型
最近看狂飙有点上头了😂,还专门把几百人的群昵称改成了“摸鱼强盛集团”,群友们也很积极,昵称都改成了狂飙中的人名,聊着聊着嘴里蹦出几句狂飙中的台词,一时间感觉还蛮有意思的,群里充满了欢心笑语,给疲惫了一...

法医3阅读 1k

封面图
Mac下编译WebRTC(Mac和iOS版本)
随着新冠疫情的影响,这两年音视频的需求呈爆发式增长。在音视频领域中,WebRTC可以说是一个绕不开宝库,包括了音视频采集、编解码、传输、渲染的全过程。本文主要记录下在Mac平台上编译WebRTC Mac和iOS版本的全...

吴尼玛阅读 3.8k

抢鲜解读:Flutter 3.7更新啦
新年伊始,由 Flutter 3.7 正式版来「打头阵」!我们与整个 Flutter 社区们继续在 Flutter 3.7 中优化了框架,包括创建自定义菜单栏和层叠式菜单、更好的国际化工具支持、新的调试工具以及其他功能和特性等。

慕课网阅读 2.5k

封面图
OpenTranslator:一款基于ChatGPT API的翻译神器
这是一款使用 ChatGPT API 进行划词翻译和文本润色的浏览器插件。借助了 ChatGPT 强大的翻译能力,它将帮助您更流畅地阅读外语和编辑外语。

听蝉阅读 2.5k

95 - iOS - 养了4只布偶猫

1.1k 声望
5.1k 粉丝
宣传栏