CocoaAsyncSocket 实现时用到的技术

前言

最近在阅读 CocoaAsyncSocket 的源码,整理里一下其中用到的一些技术点。

GCD 相关

目标队列(Target Queue)

概念

目标队列的基本概念是:你创建的所有队列,如果没有指定其目标队列,那么它的目标队列是优先级为 DISPATCH_QUEUE_PRIORITY_DEFAULT 的全局并发队列。每次在你队列中的一个 block 开始执行的时候,GCD 会重新将其放入目标队列执行。详细介绍可以查看引用1。

目标队列可能引起死锁

这里说一下使用目标队列可能导致的死锁问题:

比如说有两个队列:

dispatch_queue_t queueOne;
dispatch_queue_t targetQueueOne;
queueOne.targetQueue = targetQueueOne;

那么所有在 queue 上的操作最终会在 targetQueue 上执行。一切看起来都很好,现在有设想下面这样的函数:

 - (BOOL)doSomething
  {
      __block BOOL result = NO;
      dispatch_block_t block = ^{
          result = [self someInternalMethodToBeRunOnlyOnQueueOne];
      }
      if (is_executing_on_queue(queueOne))
          block();
      else
          dispatch_sync(queueOne, block);
      
      return result;
  }

如果你在 targetQueueOne 上调用这个方法会怎样?答案是死锁。这是因为 GCD 的 API 没有提供一个机制去发现队列的目标队列,所以我们不知道 queueOne 的目标队列。(死锁的原因是在当前串型队列上又同步派发了一个操作,导致两者相互等待)

目标队列死锁问题解决

一句话解释:使用 dispatch_queue_set_specific()dispatch_get_specific()

先给 queueOne 设置 sepcific

IsOnQueueOneOrTargetQueueKey = &IsOnQueueOneOrTargetQueueKey;

void *nonNullUnusedPointer = (__bridge void *)self;
dispatch_queue_set_specific(queue, IsOnQueueOneOrTargetQueueKey, nonNullUnusedPointer, NULL);

每次要判断是否是在当前队列或者目标队列的时候如下判断:

if (dispatch_get_specific(IsOnQueueOneOrTargetQueueKey)) {
    block();
} else {
    dispatch_sync(queueOne, block);
}

GCD Dispatch 源

概念

当要处理系统底层相关任务的时候,我们必须准备好要等待一段时间。当你的应用程序陷入系统内核或者其他系统层面的时候,比起在进程内部的函数调用,我们要面临切换上下文导致的巨大时间开销。结果就是,许多系统库会提供异步接口使你的代码提交一个请求给系统,然后继续自己的工作,当系统完成你的请求,在回调你的请求结果处理 block。GCD 的 dispatch source 就是这样一个基本数据类型。具体介绍可以查看官方文档,引用2。

使用 Dispatch 源来做一个定时器

在 iOS 中想要实现一个定时器可以使用系统原生的类 NSTimerNSTimer 的问题是它的设计模式是 target-action 模式。也就是要在当前类中定义一个方法给定时器使用。比起多定义一个方法我们更喜欢用 block。但是 NSTimer 的 API 在 iOS 11 才支持回调的方式。有很多方式来自定义 NSTimer 使用回调的方式。这里不做介绍,可以查看引用3。

实现一个回调的定时器的一种方式是使用 Dispatch 源,代码如下所示:


dispatch_source_t CreateDispatchTimer(uint64_t interval,
              uint64_t leeway,
              dispatch_queue_t queue,
              dispatch_block_t block)
{
   dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
                                                     0, 0, queue);
   if (timer)
   {
      dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
      dispatch_source_set_event_handler(timer, block);
      dispatch_resume(timer);
   }
   return timer;
}
 
void MyCreateTimer()
{
   dispatch_source_t aTimer = CreateDispatchTimer(30ull * NSEC_PER_SEC,
                               1ull * NSEC_PER_SEC,
                               dispatch_get_main_queue(),
                               ^{ MyPeriodicTask(); });
 
   // Store it somewhere for later use.
    if (aTimer)
    {
        MyStoreTimer(aTimer);
    }
}

使用 Dispatch 源来处理 Socket 相关操作

无论是服务器端还是客户端socket 都需要某种机制使得其能够和对方进行持续的通信。通常的解决办法是进行一个永久循环,遇到某种条件时再终止循环。这种方式缺陷较多,一是不够“优雅”,二是无限循环非常浪费 CPU 时间。使用 Dispatch 源来做处理会更好。

CocoaAsyncSocket 这个框架的核心思想之一就是使用源来处理 Socket 的读和写。下面代码来自 CocoaAsyncSocket:

accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue);
int socketFD = socket4FD;
dispatch_source_t acceptSource = accept4Source;
__weak GCDAsyncSocket *weakSelf = self;
dispatch_source_set_event_handler(accept4Source, ^{ @autoreleasepool {
    __strong GCDAsyncSocket *strongSelf = weakSelf;
    if (strongSelf == nil) return_from_block;
    unsigned long i = 0;
    unsigned long numPendingConnections = dispatch_source_get_data(acceptSource);
    while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections));
}});
dispatch_source_set_cancel_handler(accept4Source, ^{
    close(socketFD);
});

dispatch_resume(accept4Source);

使用 dispatch_source_create 来创建源,参数 DISPATCH_SOURCE_TYPE_READ 指的是这是一个读类型的源,读事件来自与 socket4FD,事件回调发生在 socketQueue 上。

dispatch_source_set_event_handler 指定了每次事件发生时的回调,
dispatch_source_set_cancel_handler 指定了取消一个源的操作,取消一个源可以使用 dispatch_source_cancel 完成。默认情况下源是不会开始执行的,所以要用 dispatch_resume 显示的启动一个源。

dispatch 源还有很多作用,比如监视一个文件夹中的文件变化等。详细介绍可以看官方文档或者引用4

SSL/TLS 握手

我们知道每一个 TCP 连接都会进行3次握手,然后才会开始通信,具体原理可以查看引用5。HTTP 连接是建立在 TCP 连接的基础之上的。HTTPS 则更进一步,其连接是建立在 SSL/TLS 连接之上的。而 SSL/TLS 连接又是建立在 TCP 3次握手之后。这里不描述 SSL/TLS 的握手过程,详细可以查看参考资料6。

如何在 iOS 或者 macOS 系统上实现 SSL/TLS 握手呢?先说一下为什么要自己实现 SSL/TLS 握手,毕竟我们使用 HTTPS 的时候好像并没有处理这个?

了解客户端证书绑定的会知道,要做客户端证书绑定我们需要实现 NSURLSessionDelegate 的 URLSession:didReceiveChallenge:completionHandler 协议。其实在这一步发生的时候,NSURLSession 就已经正在为我们进行 SSL/TLS 握手,只是由于我们实现了这个协议,所以 NSURLSession 需要我们告诉它是否要信任这次握手,默认情况下 NSURLSession 有自己的逻辑来决定是否信任。NSURLSession 为我们隐藏了很多其他握手细节。

当我们想自定义 SSL/TLS 的握手细节的时候就需要自定义了。(SSL/TLS 握手中每一个步骤请看资料6)。

iOS 这边实现 SSL/TLS 握手 有两种方式

  1. 使用苹果提供的 SecureTransport 框架
  2. 使用 CFStream 相关接口

使用 SecureTransport 的优势是可以结合 dispatch 源,并且性能高,可控性强,但是 SecureTransport 没有开源。

使用 CFStream 的优势是使用 dispatch source 苹果没有一种机制告诉我们当前是否要进入后台或者正在后台。CFStream 的 API 就可以,通过设置 steam 的 kCFStreamNetworkServiceType 为 kCFStreamNetworkServiceTypeVoIP 即可。

注意 CocoaAsyncSocket 中貌似无法用 CFSteam 来自定义证书认证。

具体如何使用 CFStream 请看资料 CFSocketStream

具体如何使用 Secure Transport 请看资料 Secure Transport

SOCKET

CocoaAsyncSocket 本来就是对原生的 Socket 的封装,所以要看懂源码需要了解原生的 Socket 的写法。主要分成两个部分:

  1. 服务器端 Socket
  2. 客户端 Socket

同时 Socket 本身还有很多细节内容需要了解。建议在阅读或者使用 CocoaAsyncSocket 之前了解这方面的知识。

英文资料可以看: Introduction to Sockets Programming in C using TCP/IP

中文资料可以看: Socket

引用

  1. 【翻译】GCD Target Queues
  2. Dispatch Sources 官方文档
  3. NSTimer-Blocks
  4. 细说GCD(Grand Central Dispatch)如何用
  5. 通俗大白话来理解TCP协议的三次握手和四次分手
  6. SSL/TLS协议运行机制的概述
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容

  • 1.常见提问: (1)有什么擅长? 这个要好好想想 (2)技术上研究的最深入的是那一块? 这个要好好想想,会问的很...
    树懒啊树懒阅读 999评论 0 11
  • 1. 并行和并发 简单来说,若说两个任务A和B并发执行,则表示任务A和任务B在同一时间段里被执行(更多的可能是二者...
    Z_Han阅读 636评论 0 8
  • 01 前前前同事李不悔,上个月跳槽了。 李不悔新去的,是一家规模为原公司十倍的大型集团,他的月薪,翻了三倍,突破两...
    随性而活阅读 155评论 0 0
  • 首先在viewcontroller 中创建collectionView;自定义一个类继承自UICollection...
    dqk1023阅读 901评论 0 0