Socket学习总结系列(二) -- CocoaAsyncSocket

 

前言


     

      这是这个系列文章的第二篇,要是没有看第一篇的还是建议看看第一篇,以为这个是接着第一篇梳理的先大概的总结一下在上篇的文章中说的些内容:

      1、 整理了一下做IM我们有那些途径,以及我们怎样选择最适合自己的

      2、在做IM的时候协议你又该怎样选择,以及这些协议之间一些的对比等等

      3、接下来梳理了一下Socket的我们该怎样理解,它的心跳,pingpong,重连机制等等

      4、利用demo整理出来了原生Socket的简单的连接以及接收/发送消息。

 

内容


  

      1、 对CocoaAsyncSocke这个三方的理解以及一些自己的看法 

      2、分析CocoaAsyncSocket的集成,源码的一些解析

      3、利用CocoaAsyncSocket实现Socket的连接,接收/发送 消息,以及总结一下这整个过程

 

CocoaAsyncSocke


 

      这里我们先认识一下CocoaAsyncSocke:

      CocoaAsyncSocke是谷歌的开发者,基于BSD-Socket写的一个IM框架,它给Mac和iOS提供了易于使用的、强大的异步套接字库,向上封装出简单易用OC接口。省去了我们面向Socket以及数据流Stream等繁琐复杂的编程,下面是我们导入的整个框架的

 

 
       这里大概说一下:     GCDAsyncSocket         是基于TCP协议写的

                                  GCDAsyncUdpSocket    是基于UDP协议写的

       以前框架还有一个runloop版,不过因为功能相似等其他的原因,后续版本就废弃了,现在仅有这个GCD版本。
 
 

 
 

 

 

 

 

CocoaAsyncSocket源码 (建议先文章最后下载Demo)


     
     下面我们开始整理分析CocoaAsyncSocket的GCDAsyncSocket部分的源码,这部分代码量在九千多行,我们这一次按照它.h的可以给外面调用的方法开始认识它的源码,具体的每一行的注释你可以下载Demo去看Demo中GCDAsyncSocket部分的源码,上面给了很详细的注释,然后我们这里就是按照.h的方法去总结,Demo中的很多的注释我也是看着大神学的,大家要有什么疑问或者不明白的地方可以找我QQ联系我:
 
 
一:初始化
      我把初始化这一部分的源码又划分成了三部分,这三部分我们分开来说:
      
 
 
      第一部分: 第一部部分的三个方法你在.h文件当中也能看得到,三个逐层调用的初始化方法,注意这种逐层调用写法的好处就是灵活的进行初始化,这个你要看过AFNetworking的源码的话你也可以看到同样的写法,我们重点不放在这一块,我相信这一块的东西你要仔细点看起来是没有什么问题的。

      第二部分:这一部分的内容就是代理和线程的设置,说实话也没什么好说的,重点还是下面的连接部分,这个你也在Demo中配合注释去理解理解。

      第三部分:这一部分比起前面的两小部分稍微就需要我们注意点了,这部分的内容在下面的重点的连接部分用的比较多,你仔细看这部分方法的名字也可以理解,都是一些设置、判断IPV4和IPV6是否可用的方法。至于最下面userData的复制方法,这点我觉得似乎可以暂时忽略。

  

二:Accept  Socket      
      这一部分的代码主要是在服务端用得到:
  

   

      但按照我自己的理解,很少用OC来写服务端的代码吧!当然这也许也只是我自己见的少而已吧,我是真的不怎么知道用OC来写服务端,不过这部分的代码能能帮助我们理解在整个过程中服务端的Accept到底是怎么一个流程:

 

三:Connect  

      连接这部分的代码可以说是这整个三方的核心内容,先看看我们划分的它的方法架构:

 

      

      接下来把这五部分我们说说:

      第一部分: 前置判断,这一部分的内容是在调用了连接方法之后在连接的方法里面调用的,我们在这里先不说它的调用时具体在连接方法哪里调用,怎么调用的我们先看看这个前置检查到底检查了什么,看看里面的内容,等到我们看到调用它的地方的时候我们再谈。

      注意:下面的代码不是完整的,完整版本看Demo,具体判断之后返回YES还是NO看具体的情况而定,我们这里是为了不让无用代码占篇幅,我们的注意点放在它是通过哪些条件作了前置的判断,可以看代码中的注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
{
        // 先断言,如果当前的queue不是初始化quueue,直接报错
        // dispatch_get_specific 这个和dispatch_set_specific的用法具体的可以百度
    NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
        //无代理
    if (delegate == nil) // Must have delegate set{
    }
    //没有代理queue
    if (delegateQueue == NULL) // Must have delegate queue set{
    }
        //当前不是非连接状态
    if (![self isDisconnected]) // Must be disconnected{
    }
        // 判断是否支持IPV4 IPV6  &按位“与”运算,因为枚举是用  左位移<<运算定义的,所以可以用来判断 config包不包含某个枚举。因为一个值可能包含好几个枚举值,所以这时候不能用==来判断,只能用&来判断
        // 注意这个解释:kIPv4DisabledIf set, IPv4 is disabled,要是包含就说明IPV4是不能使用的,也就是要是返回YES,说明IPV4不能使用
         
    BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
    BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
     
        //是否都不支持
    if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled{
    }
        //  如果有interface,本机地址
        //  interface这个参数这个就是我们设置的本机IP+端口号
        /*
            一般情况不需要去设置这个参数,默认的为localhost(127.0.0.1)本机地址。而端口号会在本机中取一个空闲可用的端口。
            而我们一旦设置了这个参数,就会强制本地IP和端口为我们指定的
            这里端口号如果我们写死,万一被其他进程给占用,讲导致无法连接成功
         */
    if (interface)
    {
        NSMutableData * interface4 = nil;
        NSMutableData * interface6 = nil;
         
                //得到本机的IPV4 IPV6地址
        [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0];
         
                //如果两者都为nil
        if ((interface4 == nil) && (interface6 == nil)){
        }
        //IPV4不能正常使用且本机的IPV6为nil
        if (isIPv4Disabled && (interface6 == nil)){
        }
                //IPV6不能正常使用且本机的IPV4为nil
        if (isIPv6Disabled && (interface4 == nil)){
        }
        //如果都没问题,则赋值
        connectInterface4 = interface4;
        connectInterface6 = interface6;
    }
     
    // Clear queues (spurious read/write requests post disconnect)
        // 读写Queue清除
        // 走到这里则前面的全没有返回值,在这里就返回YES,
    [readQueue  removeAllObjects];
    [writeQueue removeAllObjects];
     
        //能走到这里的条件  有delegate   delegateQueue 包含IPV4或者IPV6
    return YES;
}

       注意:还有一个前置检测方法,我们在这里就不粘贴代码了。你看了第一个你也能看的懂第二个的啊判断条件,至于为什么会有两个前置检测的方法,怎么调用这个我们接着看。

       第二部分:逐层调用连接方法   你在这三个逐层调用的连接方法里面可以看到下面这段代码,在这 block 中你可以看到在这里调用了我们上面说的第一个前置检测方法:

    

             

      在这里做了前置判断通过之后,再往下面就是异步执行获取得到IPV4的地址:address4 和IPV6的地址:address6 ,获取的具体方法你可以在Demo中看看,在这里获取到之后就进入我们需要理解的三部曲连接终极方法了,这三个方法先知道有三个,我们在说完下面的地址调用连接方法之后会说这三个方法,就在这个block的最后,发起了调用终极连接三部曲:

1
2
3
4
5
//异步去发起连接
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
                     
    [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
}});

      这个block的调用就在这个block的下面。

      第三部分: 直接连接一个addr的data 三个逐层连接方法,这一部分的内容在我们日常的使用中使用的也不是很多,具体的在注释中也有,你也可以按照前面我们说的去理解这部分的逻辑。

      第四部分: 我们前面说的终极连接三方法都是在这一部分里面的,在这部分我们说说这三个方法,还有我们前面需要补充的问题,就是为什么有两个前置检测方法,哪里用到了呢?

      下面这三个方法是终极的连接方法,这三个也是逐层的调用连接,返回值以及里面具体的调用还有方法里面的内容注释里面都写得比较清楚,大家看Demo。

1
2
3
4
5
6
// 下面三个是终极的连接方法
- (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6{}
 
- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr{}
 
- (void)connectSocket:(int)socketFD address:(NSData *)address stateIndex:(int)aStateIndex{}

     

      再说说我们遗留下来的那个问题,另一个前置方法在哪里用?看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//连接本机的url上,IP8C,进程间通信
- (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr;
{
    LogTrace();
     
    __block BOOL result = NO;
    __block NSError *err = nil;
     
    dispatch_block_t block = ^{ @autoreleasepool {
         
        //判断长度
        if ([url.path length] == 0)
        {
            NSString *msg = @"Invalid unix domain socket url.";
            err = [self badParamError:msg];
             
            return_from_block;
        }
         
        // Run through standard pre-connect checks
        //前置的检查
        if (![self preConnectWithUrl:url error:&err])
        {
            return_from_block;
        }
         
        // We've made it past all the checks.
        // It's time to start the connection process.
         
        flags |= kSocketStarted;
         
        // Start the normal connection process
         
        NSError *connectError = nil;
                //调用另一个方法去连接,连接Unix域服务器
        if (![self connectWithAddressUN:connectInterfaceUN error:&connectError])
        {
            [self closeWithError:connectError];
            return_from_block;
        }
 
        [self startConnectTimeout:timeout];
         
        result = YES;
    }};
     
        //在socketQueue中同步执行
    if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
        block();
    else
        dispatch_sync(socketQueue, block);
     
    if (result == NO)
    {
        if (errPtr)
            *errPtr = err;
    }
     
    return result;
}

 

      首先这个方法是在didConnect 方法里面去调用的,这个didConnect就是已经连接成功的方法,在这个放里面调用我们上面的方法,然后再上面给的方法里面你就可以看到前置检查方法和连接Unix域服务器的方法,这里我想大家就明白了,在连接Unix域服务器的时候用到了前置检查,这个也是在服务端用到的,大家要是感兴趣可以去看看里面的具体的代码注释。

      下面Diagnostics部分的代码结构如下,这里全都是在配合我们上面连接部分的代码所用:

   

      上面的第五部分你看方法名称也就知道,这里我们就不在多说这部分的内容。

      接下来我们说说剩下的主要的两部分,读和写这两部分:

 

三:Writing

            

 

       这部分是写的内容,通过这几个方法就完成了一个写数据的操作,当然这写方法里面肯定还是会涉及到其他的一些辅助的方法,这里我们不一一的列举了,大家在Demo里面去看,再说一点,这部分的代码你根据demo看注释之前,还是先把上篇我们说的那个Socket原生的发送和接收过程理解了,这样有助于你更好的看完写部分的代码,发送完了之后接下来我们就是要看接收的代码了。我们看接收部分的代码。

 

四:Reading

    

      上面最重要的就是这个方法:   doReadData  

      上面这个方法后面我们添加的几个标签(开始读取数据 CFStream , 开始读取数据 SSLRead, 开始读取数据普通的形式 等等)都是对这个方法的解释。

      当到下面的  completecurrentread  完成当前的读操作,到下面这里的时候:

    

 

      在这里就调用了我们GCDAsyncSocket中接收消息的代理方法:

 

 

      这里我们的读的操作你也就理解了,当然我说的不是看看这样一个过程你就理解了,重点还是我们Demo里面CocoaAsyncSocket的注释!!     

      剩下的方法几乎也全都是在辅助我们这几个重要的模块,也都有注释,还是那句看Demo!        

 
 
总结整个过程

      
      上面就大概的把CocoaAsyncSocket的一个组织框架分析的一下,里面的具体的内容还是得看Demo,Demo的地址我也会在最下面给出来,这里我们把我看的简书文章中作者整理的这个连接过程图给大家,然后剩下的心跳或者是pingpong机制的写法我在下一篇的总结中我们具体的写一写。
 

 

 
      下面是这个简单的Demo的地址,服务端的代码以及运行看这个系列第一篇文章: Socket学习总结系列(一) -- IM & Socket
 
       Demo下载
 
 
posted @   MrRisingSun  阅读(3998)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示