原文

前言

本文承接上文:iOS即时通讯之CocoaAsyncSocket源码解析一

上文我们提到了GCDAsyncSocket的初始化,以及最终connect之前的准备工作,包括一些错误检查;本机地址创建以及socket创建;服务端地址的创建;还有一些本机socket可选项的配置,例如禁止网络出错导致进程关闭的信号等。

上文讲到了本文方法八--创建Socket,其中有这么一行代码:

  1. //和connectInterface绑定
  2. if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr])
  3. {
  4. //绑定失败,直接关闭返回
  5. [self closeSocket:socketFD];
  6.  
  7. return SOCKET_NULL;
  8. }

我们去用之前创建的本机地址去做socket绑定,接着会调用到如下方法中:

本文方法九--给Socket绑定本机地址

  1. //绑定一个Socket的本地地址
  2. - (BOOL)bindSocket:(int)socketFD toInterface:(NSData *)connectInterface error:(NSError **)errPtr
  3. {
  4. // Bind the socket to the desired interface (if needed)
  5. //无接口就不绑定,connect会自动绑定到一个不冲突的端口上去。
  6. if (connectInterface)
  7. {
  8. LogVerbose(@"Binding socket...");
  9.  
  10. //判断当前地址的Port是不是大于0
  11. if ([[self class] portFromAddress:connectInterface] > )
  12. {
  13. // Since we're going to be binding to a specific port,
  14. // we should turn on reuseaddr to allow us to override sockets in time_wait.
  15.  
  16. int reuseOn = ;
  17.  
  18. //设置调用close(socket)后,仍可继续重用该socket。调用close(socket)一般不会立即关闭socket,而经历TIME_WAIT的过程。
  19. setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn));
  20. }
  21.  
  22. //拿到地址
  23. const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes];
  24. //绑定这个地址
  25. int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]);
  26.  
  27. //绑定出错,返回NO
  28. if (result != )
  29. {
  30. if (errPtr)
  31. *errPtr = [self errnoErrorWithReason:@"Error in bind() function"];
  32.  
  33. return NO;
  34. }
  35. }
  36.  
  37. //成功
  38. return YES;
  39. }

这个方法也非常简单,如果没有connectInterface则直接返回YES,当socket进行连接的时候,会自动绑定一个端口,进行连接。
如果有值,则我们开始绑定到我们一开始指定的地址上。
这里调用了两个和scoket相关的函数:
第一个是我们之前提到的配置scoket参数的函数:

  1. setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn));

这里调用这个函数的主要目的是为了调用close的时候,不立即去关闭socket连接,而是经历一个TIME_WAIT过程。在这个过程中,socket是可以被复用的。我们注意到之前的connect流程并没有看到复用socket的代码。注意,我们现在走的连接流程是客户端的流程,等我们讲到服务端accept进行连接的时候,我们就能看到这个复用的作用了。

第二个是bind函数

  1. int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]);

这个函数倒是很简单,就3个参数,socket、需要绑定的地址、地址大小。这样就把socket和这个地址(其实就是端口)捆绑在一起了。

这样我们就做完了最终连接前所有准备工作,本机socket有了,服务端的地址也有了。接着我们就可以开始进行最终连接了:

本文方法十 -- 建立连接的最终方法

  1. //连接最终方法 3 finnal。。。
  2. - (void)connectSocket:(int)socketFD address:(NSData *)address stateIndex:(int)aStateIndex
  3. {
  4. // If there already is a socket connected, we close socketFD and return
  5. //已连接,关闭连接返回
  6. if (self.isConnected)
  7. {
  8. [self closeSocket:socketFD];
  9. return;
  10. }
  11.  
  12. // Start the connection process in a background queue
  13. //开始连接过程,在后台queue中
  14. __weak GCDAsyncSocket *weakSelf = self;
  15.  
  16. //获取到全局Queue
  17. dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, );
  18. //新线程
  19. dispatch_async(globalConcurrentQueue, ^{
  20. #pragma clang diagnostic push
  21. #pragma clang diagnostic warning "-Wimplicit-retain-self"
  22. //调用connect方法,该函数阻塞线程,所以要异步新线程
  23. //客户端向特定网络地址的服务器发送连接请求,连接成功返回0,失败返回 -1。
  24. int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]);
  25.  
  26. //老样子,安全判断
  27. __strong GCDAsyncSocket *strongSelf = weakSelf;
  28. if (strongSelf == nil) return_from_block;
  29.  
  30. //在socketQueue中,开辟线程
  31. dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
  32. //如果状态为已经连接,关闭连接返回
  33. if (strongSelf.isConnected)
  34. {
  35. [strongSelf closeSocket:socketFD];
  36. return_from_block;
  37. }
  38.  
  39. //说明连接成功
  40. if (result == )
  41. {
  42. //关闭掉另一个没用的socket
  43. [self closeUnusedSocket:socketFD];
  44. //调用didConnect,生成stream,改变状态等等!
  45. [strongSelf didConnect:aStateIndex];
  46. }
  47. //连接失败
  48. else
  49. {
  50. //关闭当前socket
  51. [strongSelf closeSocket:socketFD];
  52.  
  53. // If there are no more sockets trying to connect, we inform the error to the delegate
  54. //返回连接错误的error
  55. if (strongSelf.socket4FD == SOCKET_NULL && strongSelf.socket6FD == SOCKET_NULL)
  56. {
  57. NSError *error = [strongSelf errnoErrorWithReason:@"Error in connect() function"];
  58. [strongSelf didNotConnect:aStateIndex error:error];
  59. }
  60. }
  61. }});
  62.  
  63. #pragma clang diagnostic pop
  64. });
  65. //输出正在连接中
  66. LogVerbose(@"Connecting...");
  67. }

这个方法主要就是做了一件事,调用下面一个函数进行连接:

  1. int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]);

这里需要注意的是这个函数是阻塞,直到结果返回之前,线程会一直停在这行。所以这里用的是全局并发队列,开辟了一个新的线程进行连接,在得到结果之后,又调回socketQueue中进行后续操作。

如果result为0,说明连接成功,我们会关闭掉另外一个没有用到的socket(如果有的话)。然后调用另外一个方法做一些连接成功的初始化操作。
否则连接失败,我们会关闭socket,填充错误并且返回。

我们接着来看看连接成功后,初始化的方法:

本文方法十一 -- 连接成功后的初始化

  1. //连接成功后调用,设置一些连接成功的状态
  2. - (void)didConnect:(int)aStateIndex
  3. {
  4. LogTrace();
  5.  
  6. NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
  7.  
  8. //状态不同
  9. if (aStateIndex != stateIndex)
  10. {
  11. LogInfo(@"Ignoring didConnect, already disconnected");
  12.  
  13. // The connect operation has been cancelled.
  14. // That is, socket was disconnected, or connection has already timed out.
  15. return;
  16. }
  17.  
  18. //kConnected合并到当前flag中
  19. flags |= kConnected;
  20. //停止连接超时
  21. [self endConnectTimeout];
  22.  
  23. #if TARGET_OS_IPHONE
  24. // The endConnectTimeout method executed above incremented the stateIndex.
  25. //上面的endConnectTimeout,会导致stateIndex增加,所以需要重新赋值
  26. aStateIndex = stateIndex;
  27. #endif
  28.  
  29. // Setup read/write streams (as workaround for specific shortcomings in the iOS platform)
  30. //
  31. // Note:
  32. // There may be configuration options that must be set by the delegate before opening the streams.
  33. //打开stream之前必须用相关配置设置代理
  34. // The primary example is the kCFStreamNetworkServiceTypeVoIP flag, which only works on an unopened stream.
  35. //主要的例子是kCFStreamNetworkServiceTypeVoIP标记,只能工作在未打开的stream中?
  36. //
  37. // Thus we wait until after the socket:didConnectToHost:port: delegate method has completed.
  38. //所以我们要等待,连接完成的代理调用完
  39. // This gives the delegate time to properly configure the streams if needed.
  40. //这些给了代理时间,去正确的配置Stream,如果是必要的话
  41.  
  42. //创建个Block来初始化Stream
  43. dispatch_block_t SetupStreamsPart1 = ^{
  44.  
  45. NSLog(@"hello~");
  46. #if TARGET_OS_IPHONE
  47. //创建读写stream失败,则关闭并报对应错误
  48. if (![self createReadAndWriteStream])
  49. {
  50. [self closeWithError:[self otherError:@"Error creating CFStreams"]];
  51. return;
  52. }
  53.  
  54. //参数是给NO的,就是有可读bytes的时候,不会调用回调函数
  55. if (![self registerForStreamCallbacksIncludingReadWrite:NO])
  56. {
  57. [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]];
  58. return;
  59. }
  60.  
  61. #endif
  62. };
  63. //part2设置stream
  64. dispatch_block_t SetupStreamsPart2 = ^{
  65. #if TARGET_OS_IPHONE
  66. //状态不一样直接返回
  67. if (aStateIndex != stateIndex)
  68. {
  69. // The socket has been disconnected.
  70. return;
  71. }
  72. //如果加到runloop上失败
  73. if (![self addStreamsToRunLoop])
  74. {
  75. //错误返回
  76. [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]];
  77. return;
  78. }
  79.  
  80. //读写stream open
  81. if (![self openStreams])
  82. {
  83. //开启错误返回
  84. [self closeWithError:[self otherError:@"Error creating CFStreams"]];
  85. return;
  86. }
  87.  
  88. #endif
  89. };
  90.  
  91. // Notify delegate
  92. //通知代理
  93. //拿到server端的host port
  94. NSString *host = [self connectedHost];
  95. uint16_t port = [self connectedPort];
  96. //拿到unix域的 url
  97. NSURL *url = [self connectedUrl];
  98. //拿到代理
  99. __strong id theDelegate = delegate;
  100.  
  101. //代理队列 和 Host不为nil 且响应didConnectToHost代理方法
  102. if (delegateQueue && host != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToHost:port:)])
  103. {
  104. //调用初始化stream1
  105. SetupStreamsPart1();
  106.  
  107. dispatch_async(delegateQueue, ^{ @autoreleasepool {
  108.  
  109. //到代理队列调用连接成功的代理方法
  110. [theDelegate socket:self didConnectToHost:host port:port];
  111.  
  112. //然后回到socketQueue中去执行初始化stream2
  113. dispatch_async(socketQueue, ^{ @autoreleasepool {
  114.  
  115. SetupStreamsPart2();
  116. }});
  117. }});
  118. }
  119. //这个是unix domain 请求回调
  120. else if (delegateQueue && url != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToUrl:)])
  121. {
  122. SetupStreamsPart1();
  123.  
  124. dispatch_async(delegateQueue, ^{ @autoreleasepool {
  125.  
  126. [theDelegate socket:self didConnectToUrl:url];
  127.  
  128. dispatch_async(socketQueue, ^{ @autoreleasepool {
  129.  
  130. SetupStreamsPart2();
  131. }});
  132. }});
  133. }
  134. //否则只初始化stream
  135. else
  136. {
  137. SetupStreamsPart1();
  138. SetupStreamsPart2();
  139. }
  140.  
  141. // Get the connected socket
  142.  
  143. int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN;
  144.  
  145. //fcntl,功能描述:根据文件描述词来操作文件的特性。http://blog.csdn.net/pbymw8iwm/article/details/7974789
  146. // Enable non-blocking IO on the socket
  147. //使socket支持非阻塞IO
  148. int result = fcntl(socketFD, F_SETFL, O_NONBLOCK);
  149. if (result == -)
  150. {
  151. //失败 ,报错
  152. NSString *errMsg = @"Error enabling non-blocking IO on socket (fcntl)";
  153. [self closeWithError:[self otherError:errMsg]];
  154.  
  155. return;
  156. }
  157.  
  158. // Setup our read/write sources
  159. //初始化读写source
  160. [self setupReadAndWriteSourcesForNewlyConnectedSocket:socketFD];
  161.  
  162. // Dequeue any pending read/write requests
  163. //开始下一个任务
  164. [self maybeDequeueRead];
  165. [self maybeDequeueWrite];
  166. }

这个方法很长一大串,其实做的东西也很简单,主要做了下面几件事:

  1. 把当前状态flags加上已连接,并且关闭掉我们一开始连接开启的,连接超时的定时器。
  2. 初始化了两个BlockSetupStreamsPart1SetupStreamsPart2,这两个Block做的事都和读写流有关。SetupStreamsPart1用来创建读写流,并且注册回调。另一个SetupStreamsPart2用来把流添加到当前线程的runloop上,并且打开流。
  3. 判断是否有代理queuehost或者url这些参数是否为空、是否代理响应didConnectToHostdidConnectToUrl代理,这两种分别对应了普通socket连接和unix domin socket连接。如果实现了对应的代理,则调用连接成功的代理。
  4. 在调用代理的同时,调用了我们之前初始化的两个读写流相关的Block。这里值得说下的是这两个Block和代理之间的调用顺序:

    • 先执行SetupStreamsPart1后执行SetupStreamsPart2,没什么好说的,问题是代理的执行时间,想想如果我们放在SetupStreamsPart2后面是不是会导致个问题,就是用户收到消息了,但是连接成功的代理还没有被调用,这显然是不合理的。所以我们的调用顺序是SetupStreamsPart1->代理->SetupStreamsPart2

      所以出现了如下代码:

      1. //调用初始化stream1
      2. SetupStreamsPart1();
      3.  
      4. dispatch_async(delegateQueue, ^{ @autoreleasepool {
      5.  
      6. //到代理队列调用连接成功的代理方法
      7. [theDelegate socket:self didConnectToHost:host port:port];
      8.  
      9. //然后回到socketQueue中去执行初始化stream2
      10. dispatch_async(socketQueue, ^{ @autoreleasepool {
      11.  
      12. SetupStreamsPart2();
      13. }});
      14. }});

原因是为了线程安全和socket相关的操作必须在socketQueue中进行。而代理必须在我们设置的代理queue中被回调。

5、拿到当前的本机socket,调用如下函数:

  1. int result = fcntl(socketFD, F_SETFL, O_NONBLOCK);

简单来说,这个函数类似我们之前提到的一个函数setsockopt(),都是给socket设置一些参数,以实现一些功能。而这个函数,能实现的功能更多。大家可以看看这篇文章参考参考:fcntl函数详解

而在这里,就是为了把socket的IO模式设置为非阻塞。很多小伙伴又要疑惑什么是非阻塞了,先别急,关于这个我们下文会详细的来谈。

6、我们初始化了读写source(很重要,所有的消息都是由这个source来触发的,我们之后会详细分析这个方法)。

7、我们做完了streamsource的初始化处理,则开始做一次读写任务(这两个方法暂时不讲,会放到之后的Read和Write篇中去讲)。

我们接着来讲讲这个方法中对其他方法的调用,按照顺序来,先从第2条,两个Block中对stream的处理开始。和stream相关的函数一共有6个:

Stream相关方法一 -- 创建读写stream

  1. //创建读写stream
  2. - (BOOL)createReadAndWriteStream
  3. {
  4. LogTrace();
  5.  
  6. NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
  7.  
  8. //如果有一个有值,就返回
  9. if (readStream || writeStream)
  10. {
  11. // Streams already created
  12. return YES;
  13. }
  14. //拿到socket,首选是socket4FD,其次socket6FD,都没有才是socketUN,socketUN应该是Unix的socket结构体
  15. int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN;
  16.  
  17. //如果都为空,返回NO
  18. if (socketFD == SOCKET_NULL)
  19. {
  20. // Cannot create streams without a file descriptor
  21. return NO;
  22. }
  23.  
  24. //如果非连接,返回NO
  25. if (![self isConnected])
  26. {
  27. // Cannot create streams until file descriptor is connected
  28. return NO;
  29. }
  30.  
  31. LogVerbose(@"Creating read and write stream...");
  32.  
  33. #pragma mark - 绑定SocketCFStream
  34. //下面的接口用于创建一对 socket stream,一个用于读取,一个用于写入:
  35. CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream);
  36.  
  37. // The kCFStreamPropertyShouldCloseNativeSocket property should be false by default (for our case).
  38. // But let's not take any chances.
  39.  
  40. //读写stream都设置成不会随着绑定的socket一起close,release。 kCFBooleanFalse不一起,kCFBooleanTrue一起
  41. if (readStream)
  42. CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);
  43. if (writeStream)
  44. CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);
  45.  
  46. //如果有一个为空
  47. if ((readStream == NULL) || (writeStream == NULL))
  48. {
  49. LogWarn(@"Unable to create read and write stream...");
  50.  
  51. //关闭对应的stream
  52. if (readStream)
  53. {
  54. CFReadStreamClose(readStream);
  55. CFRelease(readStream);
  56. readStream = NULL;
  57. }
  58. if (writeStream)
  59. {
  60. CFWriteStreamClose(writeStream);
  61. CFRelease(writeStream);
  62. writeStream = NULL;
  63. }
  64. //返回创建失败
  65. return NO;
  66. }
  67. //创建成功
  68. return YES;
  69. }

这个方法基本上很简单,就是关于两个stream函数的调用:

1、创建stream的函数:

    1. CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream);

这个函数创建了一对读写stream,并且把stream与这个scoket做了绑定。

2、设置stream属性:

    1. CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);
    2. CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);

    这个函数可以给stream设置一个属性,这里是设置stream不会随着socket的生命周期(close,release)而变化。

接着调用了registerForStreamCallbacksIncludingReadWrite来给stream注册读写回调。

Stream相关方法二 -- 读写回调的注册:

  1. //注册Stream的回调
  2. - (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite
  3. {
  4. LogVerbose(@"%@ %@", THIS_METHOD, (includeReadWrite ? @"YES" : @"NO"));
  5.  
  6. NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
  7. //判断读写stream是不是都为空
  8. NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null");
  9.  
  10. //客户端stream上下文对象
  11. streamContext.version = ;
  12. streamContext.info = (__bridge void *)(self);
  13. streamContext.retain = nil;
  14. streamContext.release = nil;
  15. streamContext.copyDescription = nil;
  16.  
  17. // The open has completed successfully.
  18. // The stream has bytes to be read.
  19. // The stream can accept bytes for writing.
  20. // An error has occurred on the stream.
  21. // The end of the stream has been reached.
  22.  
  23. //设置一个CF的flag 两种,一种是错误发生的时候,一种是stream事件结束
  24. CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered ;
  25. //如果包含读写
  26. if (includeReadWrite)
  27. //仍然有Bytes要读的时候 The stream has bytes to be read.
  28. readStreamEvents |= kCFStreamEventHasBytesAvailable;
  29.  
  30. //给读stream设置客户端,会在之前设置的那些标记下回调函数 CFReadStreamCallback。设置失败的话直接返回NO
  31. if (!CFReadStreamSetClient(readStream, readStreamEvents, &CFReadStreamCallback, &streamContext))
  32. {
  33. return NO;
  34. }
  35.  
  36. //写的flag,也一样
  37. CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered;
  38. if (includeReadWrite)
  39. writeStreamEvents |= kCFStreamEventCanAcceptBytes;
  40.  
  41. if (!CFWriteStreamSetClient(writeStream, writeStreamEvents, &CFWriteStreamCallback, &streamContext))
  42. {
  43. return NO;
  44. }
  45. //走到最后说明读写都设置回调成功,返回YES
  46. return YES;
  47. }

相信用过CFStream的朋友,应该会觉得很简单,这个方法就是调用了一些CFStream相关函数,其中最主要的这个设置读写回调函数:

  1. Boolean CFReadStreamSetClient(CFReadStreamRef stream, CFOptionFlags streamEvents, CFReadStreamClientCallBack clientCB, CFStreamClientContext *clientContext);
  2. Boolean CFWriteStreamSetClient(CFWriteStreamRef stream, CFOptionFlags streamEvents, CFWriteStreamClientCallBack clientCB, CFStreamClientContext *clientContext);

这个函数共4个参数:
第1个为我们需要设置的stream;
第2个为需要监听的事件选项,包括以下事件:

  1. typedef CF_OPTIONS(CFOptionFlags, CFStreamEventType) {
  2. kCFStreamEventNone = , //没有事件发生
  3. kCFStreamEventOpenCompleted = , //成功打开流
  4. kCFStreamEventHasBytesAvailable = , //流中有数据可读
  5. kCFStreamEventCanAcceptBytes = , //流中可以接受数据去写
  6. kCFStreamEventErrorOccurred = , //流发生错误
  7. kCFStreamEventEndEncountered = //到达流的结尾
  8. };

其中具体用法,大家可以自行去试试,这里作者只监听了了两种事件kCFStreamEventErrorOccurredkCFStreamEventEndEncountered,再根据传过来的参数去决定是否监听kCFStreamEventCanAcceptBytes

  1. //如果包含读写
  2. if (includeReadWrite)
  3. //仍然有Bytes要读的时候 The stream has bytes to be read.
  4. readStreamEvents |= kCFStreamEventHasBytesAvailable;

而这里我们传过来的参数为NO,导致它并不监听可读数据。显然,我们正常的连接,当有消息发送过来,并不是由stream回调来触发的。这个框架中,如果是TLS传输的socket是用stream来触发的,这个我们后续文章会讲到。

那么有数据的时候,到底是什么来触发我们的读写呢,答案就是读写source,我们接下来就会去创建初始化它。

这里绑定了两个函数,分别对应读和写的回调,分别为:

  1. //读的回调
  2. static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo)
  3. //写的回调
  4. static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo)

关于这两个函数,同样这里暂时不做讨论,等后续文章再来分析。

还有一点需要说一下的是streamContext这个属性,它是一个结构体,包含流的上下文信息,其结构如下:

  1. typedef struct {
  2. CFIndex version;
  3. void *info;
  4. void *(*retain)(void *info);
  5. void (*release)(void *info);
  6. CFStringRef (*copyDescription)(void *info);
  7. } CFStreamClientContext

这个流的上下文中info指针,其实就是前面所对应的读写回调函数中的pInfo指针,每次回调都会传过去。其它的version就是流的版本标识,之外的3个都需要的是一个函数指针,对应我们传递的pInfo的持有以及释放还有复制的描述信息,这里我们都赋值给nil

接着我们来到流处理的第三步:addStreamsToRunLoop-添加到runloop上。

Stream相关方法三 -- 加到当前线程的runloop

  1. //把stream添加到runloop上
  2. - (BOOL)addStreamsToRunLoop
  3. {
  4. LogTrace();
  5.  
  6. NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
  7. NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null");
  8.  
  9. //判断flag里是否包含kAddedStreamsToRunLoop,没添加过则添加。
  10. if (!(flags & kAddedStreamsToRunLoop))
  11. {
  12. LogVerbose(@"Adding streams to runloop...");
  13.  
  14. [[self class] startCFStreamThreadIfNeeded];
  15. //在开启的线程中去执行,阻塞式的
  16. [[self class] performSelector:@selector(scheduleCFStreams:)
  17. onThread:cfstreamThread
  18. withObject:self
  19. waitUntilDone:YES];
  20.  
  21. //添加标识
  22. flags |= kAddedStreamsToRunLoop;
  23. }
  24.  
  25. return YES;
  26. }

这里方法做了两件事:

1、开启了一条用于CFStream读写回调的常驻线程,其中调用了好几个函数:

    1. + (void)startCFStreamThreadIfNeeded
    2. + (void)cfstreamThread

    在这两个函数中,添加了一个runloop,并且绑定了一个定时器事件,让它run起来,使得线程常驻。大家可以结合着github中demo的注释,自行查看这几个方法。如果有任何疑问可以看看楼主这篇文章:基于runloop的线程保活、销毁与通信,或者本文下评论,会一一解答。

2、在这个常驻线程中去调用注册方法:

    1. //注册CFStream
    2. + (void)scheduleCFStreams:(GCDAsyncSocket *)asyncSocket
    3. {
    4. LogTrace();
    5.  
    6. //断言当前线程是cfstreamThread,不是则报错
    7. NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread");
    8.  
    9. //获取到runloop
    10. CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    11. //如果有readStream
    12. if (asyncSocket->readStream)
    13. //注册readStream在runloop的kCFRunLoopDefaultMode上
    14. CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode);
    15.  
    16. //一样
    17. if (asyncSocket->writeStream)
    18. CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode);
    19. }

    这里可以看到,我们流的回调都是在这条流的常驻线程中,至于为什么要这么做,相信大家楼主看过AFNetworking系列文章的会明白。我们之后文章也会就这个框架线程的问题详细讨论的,这里就暂时不详细说明了。
    这里主要用了CFReadStreamScheduleWithRunLoop函数完成了runloop的注册:

    1. CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode);
    2. CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode);

    这样,如果stream中有我们监听的事件发生了,就会在这个runloop中触发我们之前设置的读写回调函数。

我们完成了注册,接下来我们就需要打开stream了:

Stream相关方法四 -- 打开stream:

  1. //打开stream
  2. - (BOOL)openStreams
  3. {
  4. LogTrace();
  5.  
  6. NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
  7. //断言读写stream都不会空
  8. NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null");
  9.  
  10. //返回stream的状态
  11.  
  12. CFStreamStatus readStatus = CFReadStreamGetStatus(readStream);
  13. CFStreamStatus writeStatus = CFWriteStreamGetStatus(writeStream);
  14.  
  15. //如果有任意一个没有开启
  16. if ((readStatus == kCFStreamStatusNotOpen) || (writeStatus == kCFStreamStatusNotOpen))
  17. {
  18. LogVerbose(@"Opening read and write stream...");
  19.  
  20. //开启
  21. BOOL r1 = CFReadStreamOpen(readStream);
  22. BOOL r2 = CFWriteStreamOpen(writeStream);
  23.  
  24. //有一个开启失败
  25. if (!r1 || !r2)
  26. {
  27. LogError(@"Error in CFStreamOpen");
  28. return NO;
  29. }
  30. }
  31.  
  32. return YES;
  33. }

方法也很简单,通过CFReadStreamGetStatus函数,获取到当前stream的状态,判断没开启则调用CFReadStreamOpen函数去开启,如果开启失败,错误返回。

到这里stream初始化相关的工作就做完了,接着我们还是回到本文方法十一 -- 连接成功后的初始化中:

其中第5条,我们谈到了设置socket的I/O模式为非阻塞,相信很多朋友对socket的I/O:同步、异步、阻塞、非阻塞。这四个概念有所混淆。
简单的来说,同步、异步是对于客户端而言的。比如我发起一个调用一个函数,我如果直接去调用,那么就是同步的,否则新开辟一个线程去做,那么对于当前线程而言就是异步的。
而阻塞和非阻塞是对于服务端而言。当服务端被客户端调用后,我如果立刻返回调用的结果(无论数据是否处理完)那么就是非阻塞的,又或者等待数据拿到并且处理完(总之一系列逻辑)再返回,那么这种情况就是阻塞的。

好了,有了这个概念,我们接下来看看Linux下的5种I/O模型:
1)阻塞I/O(blocking I/O)
2)非阻塞I/O (nonblocking I/O)
3) I/O复用(select 和poll) (I/O multiplexing)
4)信号驱动I/O (signal driven I/O (SIGIO))
5)异步I/O (asynchronous I/O (the POSIX aio_functions))

我们来简单谈谈这5种模型:
1)阻塞I/O:
简单举个例子,比如我们调用read()去读取消息,如果是在阻塞模式下,我们会一直等待,直到有消息到来为止。
很多小伙伴可能又要说了,这有什么不可以,我们新开辟一条线程,让它等着不就行了,看起来确实没什么不可以。
那是因为你仅仅是站在客户端的角度上来看。试想如果我们服务端也这么做,那岂不是有多少个socket连接,我们得开辟多少个线程去做阻塞IO?
2)非阻塞I/O
于是就有了非阻塞的概念,当我们去read()的时候,直接返回结果,这样在很大概率下,是并没有消息给我们读的。这时候函数就会错误返回-1,并将errno设置为 EWOULDBLOCK,意为IO并没有数据。
这时候就需要我们自己有一个机制,能知道什么时候有数据,在去调用read()。有一个很傻的方式就是不停的循环去调用这个函数,这样有数据来,我们第一时间就读到了。
3)I/O复用模式
I/O复用模式阻塞I/O的改进版,它在read之前,会先去调用select去遍历所有的socket,看哪一个有消息。当然这个过程是阻塞的,直到有消息返回为止。然后在去调用read,阻塞的方式去读取从系统内核中去读取这条消息到进程中来。
4)信号驱动I/O
信号驱动I/O是一个半异步的I/O模式,它首先会调用一个系统sginal相关的函数,把socket和信号绑定起来,然后不管有没有消息直接返回(这一步非阻塞)。这时候系统内核会去检查socket是否有可用数据。有的话则发送该信号给进程,然后进程在去调用read阻塞式的从系统内核读取数据到进程中来(这一步阻塞)。
5)可能聪明的你已经想到了更好的解决方式,这就对了,这就是我们第5种IO模式:异步I/O ,它和第4步一样,也是调用sginal相关函数,把socket和信号绑定起来,同时绑定起来的还有一块数据缓冲区buffer。然后无论有没有数据直接返回(非阻塞)。而系统内核会去检查是否有可用数据,一旦有可用数据,则触发信号,并且把数据填充到我们之前提供的数据缓冲区buffer中。这样我们进程被信号触发,并且直接能从buffer中读取到数据,整个过程没有任何阻塞。
很显然,我们CocoaAyncSocket框架用的就是第5种I/O模式。

如果大家对I/O模式仍然感到疑惑,可以看看这篇文章:
socket阻塞与非阻塞,同步与异步、I/O模型

接着我们继续看本文方法十一 -- 连接成功后的初始化中第6条,读写source的初始化方法:

本文方法十二 -- 初始化读写source:

  1. //初始化读写source
  2. - (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD
  3. {
  4. //GCD source DISPATCH_SOURCE_TYPE_READ 会一直监视着 socketFD,直到有数据可读
  5. readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, , socketQueue);
  6. //_dispatch_source_type_write :监视着 socketFD,直到写数据了
  7. writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, , socketQueue);
  8.  
  9. // Setup event handlers
  10.  
  11. __weak GCDAsyncSocket *weakSelf = self;
  12.  
  13. #pragma mark readSource的回调
  14.  
  15. //GCD事件句柄 读,当socket中有数据流出现,就会触发这个句柄,全自动,不需要手动触发
  16. dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool {
  17. #pragma clang diagnostic push
  18. #pragma clang diagnostic warning "-Wimplicit-retain-self"
  19.  
  20. __strong GCDAsyncSocket *strongSelf = weakSelf;
  21. if (strongSelf == nil) return_from_block;
  22.  
  23. LogVerbose(@"readEventBlock");
  24. //从readSource中,获取到数据长度,
  25. strongSelf->socketFDBytesAvailable = dispatch_source_get_data(strongSelf->readSource);
  26. LogVerbose(@"socketFDBytesAvailable: %lu", strongSelf->socketFDBytesAvailable);
  27.  
  28. //如果长度大于0,开始读数据
  29. if (strongSelf->socketFDBytesAvailable > )
  30. [strongSelf doReadData];
  31. else
  32. //因为触发了,但是却没有可读数据,说明读到当前包边界了。做边界处理
  33. [strongSelf doReadEOF];
  34.  
  35. #pragma clang diagnostic pop
  36. }});
  37.  
  38. //写事件句柄
  39. dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool {
  40. #pragma clang diagnostic push
  41. #pragma clang diagnostic warning "-Wimplicit-retain-self"
  42.  
  43. __strong GCDAsyncSocket *strongSelf = weakSelf;
  44. if (strongSelf == nil) return_from_block;
  45.  
  46. LogVerbose(@"writeEventBlock");
  47. //标记为接受数据
  48. strongSelf->flags |= kSocketCanAcceptBytes;
  49. //开始写
  50. [strongSelf doWriteData];
  51.  
  52. #pragma clang diagnostic pop
  53. }});
  54.  
  55. // Setup cancel handlers
  56.  
  57. __block int socketFDRefCount = ;
  58.  
  59. #if !OS_OBJECT_USE_OBJC
  60. dispatch_source_t theReadSource = readSource;
  61. dispatch_source_t theWriteSource = writeSource;
  62. #endif
  63.  
  64. //读写取消的句柄
  65. dispatch_source_set_cancel_handler(readSource, ^{
  66. #pragma clang diagnostic push
  67. #pragma clang diagnostic warning "-Wimplicit-retain-self"
  68.  
  69. LogVerbose(@"readCancelBlock");
  70.  
  71. #if !OS_OBJECT_USE_OBJC
  72. LogVerbose(@"dispatch_release(readSource)");
  73. dispatch_release(theReadSource);
  74. #endif
  75.  
  76. if (--socketFDRefCount == )
  77. {
  78. LogVerbose(@"close(socketFD)");
  79. //关闭socket
  80. close(socketFD);
  81. }
  82.  
  83. #pragma clang diagnostic pop
  84. });
  85.  
  86. dispatch_source_set_cancel_handler(writeSource, ^{
  87. #pragma clang diagnostic push
  88. #pragma clang diagnostic warning "-Wimplicit-retain-self"
  89.  
  90. LogVerbose(@"writeCancelBlock");
  91.  
  92. #if !OS_OBJECT_USE_OBJC
  93. LogVerbose(@"dispatch_release(writeSource)");
  94. dispatch_release(theWriteSource);
  95. #endif
  96.  
  97. if (--socketFDRefCount == )
  98. {
  99. LogVerbose(@"close(socketFD)");
  100. //关闭socket
  101. close(socketFD);
  102. }
  103.  
  104. #pragma clang diagnostic pop
  105. });
  106.  
  107. // We will not be able to read until data arrives.
  108. // But we should be able to write immediately.
  109.  
  110. //设置未读数量为0
  111. socketFDBytesAvailable = ;
  112. //把读挂起的状态移除
  113. flags &= ~kReadSourceSuspended;
  114.  
  115. LogVerbose(@"dispatch_resume(readSource)");
  116. //开启读source
  117. dispatch_resume(readSource);
  118.  
  119. //标记为当前可接受数据
  120. flags |= kSocketCanAcceptBytes;
  121. //先把写source标记为挂起
  122. flags |= kWriteSourceSuspended;
  123. }

这个方法初始化了读写source,这个方法主要是GCD source运用,如果有对这部分知识有所疑问,可以看看宜龙大神这篇:GCD高级用法
这里GCD Source相关的主要是下面这3个函数:

  1. //创建source
  2. dispatch_source_create(dispatch_source_type_t type,
  3. uintptr_t handle,
  4. unsigned long mask,
  5. dispatch_queue_t _Nullable queue);
  6. //为source设置事件句柄
  7. dispatch_source_set_event_handler(dispatch_source_t source,
  8. dispatch_block_t _Nullable handler);
  9. //为source设置取消句柄
  10. dispatch_source_set_cancel_handler(dispatch_source_t source,
  11. dispatch_block_t _Nullable handler);

相信大家用至少用过GCD定时器,接触过这3个函数,这里创建source的函数,根据参数type的不同,可以处理不同的事件:

这里我们用的是DISPATCH_SOURCE_TYPE_READDISPATCH_SOURCE_TYPE_WRITE这两个类型。标识如果handle如果有可读或者可写数据时,会触发我们的事件句柄。

  • 而这里初始化的读写事件句柄内容也很简单,就是去读写数据。
  • 而取消句柄也就是去关闭socket
  • 初始化完成后,我们开启了readSource,一旦有数据过来就触发了我们readSource事件句柄,就可以去监听的socket所分配的缓冲区中去读取数据了,而wirteSource初始化完是挂起的。
  • 除此之外我们还初始化了当前source的状态,用于我们后续的操作。

至此我们客户端的整个Connect流程结束了,用一张图来概括总结一下吧:

整个客户端连接的流程大致如上图,当然远不及于此,这里我们对地址做了IPV4IPV6的兼容处理,对一些使用socket而产生的网络错误导致进程退出的容错处理。以及在这个过程中,socketQueue、代理queue、全局并发queuestream常驻线程的管理调度等等。

当然其中绝大部分操作都是在socketQueue中进行的。而在socketQueue中,我们也分为两种操作dispatch_syncdispatch_async
因为socketQueue本身就是一个串行queue,所以我们所有的操作都在这个queue中进行保证了线程安全,而需要阻塞后续行为的操作,我们用了sync的方式。其实这样使用sync是及其容易死锁的,但是作者每次在调用sync之前都调用了这么一行判断:

  1. if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))

判断当前队列是否就是这个socketQueue队列,如果是则直接调用,否则就用sync的方式提交到这个queue中去执行。这种防死锁的方式,你学到了么?

接着我们来讲讲服务端Accept流程:

整个流程还是相对Connect来说还是十分简单的,因为这个方法很长,而且大多数是我们直接连接讲到过得内容,所以我省略了一部分的代码,只把重要的展示出来,大家可以参照着源码看。

  1. //监听端口起点
  2. - (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr
  3. {
  4. return [self acceptOnInterface:nil port:port error:errPtr];
  5. }
  6.  
  7. - (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr
  8. {
  9. LogTrace();
  10.  
  11. // Just in-case interface parameter is immutable.
  12. //防止参数被修改
  13. NSString *interface = [inInterface copy];
  14.  
  15. __block BOOL result = NO;
  16. __block NSError *err = nil;
  17.  
  18. // CreateSocket Block
  19. // This block will be invoked within the dispatch block below.
  20. //创建socket的Block
  21. int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) {
  22.  
  23. //创建TCP的socket
  24. int socketFD = socket(domain, SOCK_STREAM, );
  25.  
  26. //一系列错误判断
  27. ...
  28. // Bind socket
  29. //用本地地址去绑定
  30. status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]);
  31.  
  32. //监听这个socket
  33. //第二个参数是这个端口下维护的socket请求队列,最多容纳的用户请求数。
  34. status = listen(socketFD, );
  35. return socketFD;
  36. };
  37.  
  38. // Create dispatch block and run on socketQueue
  39.  
  40. dispatch_block_t block = ^{ @autoreleasepool {
  41.  
  42. //一系列错误判断
  43. ...
  44.  
  45. //判断ipv4 ipv6是否支持
  46. ...
  47.  
  48. //得到本机的IPV4 IPV6的地址
  49. [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port];
  50. ...
  51.  
  52. //判断可以用IPV4还是6进行请求
  53. ...
  54.  
  55. // Create accept sources
  56. //创建接受连接被触发的source
  57. if (enableIPv4)
  58. {
  59. //接受连接的source
  60. accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, , socketQueue);
  61.  
  62. //事件句柄
  63. dispatch_source_set_event_handler(accept4Source, ^{ @autoreleasepool {
  64.  
  65. //拿到数据,连接数
  66. unsigned long numPendingConnections = dispatch_source_get_data(acceptSource);
  67.  
  68. LogVerbose(@"numPendingConnections: %lu", numPendingConnections);
  69.  
  70. //循环去接受这些socket的事件(一次触发可能有多个连接)
  71. while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections));
  72.  
  73. }});
  74.  
  75. //取消句柄
  76. dispatch_source_set_cancel_handler(accept4Source, ^{
  77. //...
  78. //关闭socket
  79. close(socketFD);
  80.  
  81. });
  82.  
  83. //开启source
  84. dispatch_resume(accept4Source);
  85. }
  86.  
  87. //ipv6一样
  88. ...
  89.  
  90. //在scoketQueue中同步做这些初始化。
  91. if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
  92. block();
  93. else
  94. dispatch_sync(socketQueue, block);
  95.  
  96. //...错误判断
  97. //返回结果
  98. return result;
  99. }

这个方法省略完仍然有这么长,它主要做了这两件事(篇幅原因,尽量精简):

  1. 创建本机地址、创建socket、绑定端口、监听端口。
  2. 创建了一个GCD Source,来监听这个socket读source,这样连接事件一发生,就会触发我们的事件句柄。接着我们调用了doAccept:方法循环去接受所有的连接。

接着我们来看这个接受连接的方法(同样省略了一部分不那么重要的代码):

  1. //连接接受的方法
  2. - (BOOL)doAccept:(int)parentSocketFD
  3. {
  4. LogTrace();
  5.  
  6. int socketType;
  7. int childSocketFD;
  8. NSData *childSocketAddress;
  9.  
  10. //IPV4
  11. if (parentSocketFD == socket4FD)
  12. {
  13. socketType = ;
  14.  
  15. struct sockaddr_in addr;
  16. socklen_t addrLen = sizeof(addr);
  17. //调用接受,得到接受的子socket
  18. childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen);
  19. //NO说明没有连接
  20. if (childSocketFD == -)
  21. {
  22. LogWarn(@"Accept failed with error: %@", [self errnoError]);
  23. return NO;
  24. }
  25. //子socket的地址数据
  26. childSocketAddress = [NSData dataWithBytes:&addr length:addrLen];
  27. }
  28. //一样
  29. else if (parentSocketFD == socket6FD)
  30. {
  31. ...
  32. }
  33. //unix domin socket 一样
  34. else // if (parentSocketFD == socketUN)
  35. {
  36. ...
  37. }
  38.  
  39. //socket 配置项的设置... 和connect一样
  40.  
  41. //响应代理
  42. if (delegateQueue)
  43. {
  44. __strong id theDelegate = delegate;
  45. //代理队列中调用
  46. dispatch_async(delegateQueue, ^{ @autoreleasepool {
  47.  
  48. // Query delegate for custom socket queue
  49.  
  50. dispatch_queue_t childSocketQueue = NULL;
  51.  
  52. //判断是否实现了为socket 生成一个新的SocketQueue,是的话拿到新queue
  53. if ([theDelegate respondsToSelector:@selector(newSocketQueueForConnectionFromAddress:onSocket:)])
  54. {
  55. childSocketQueue = [theDelegate newSocketQueueForConnectionFromAddress:childSocketAddress
  56. onSocket:self];
  57. }
  58.  
  59. // Create GCDAsyncSocket instance for accepted socket
  60. //新创建一个本类实例,给接受的socket
  61. GCDAsyncSocket *acceptedSocket = [[[self class] alloc] initWithDelegate:theDelegate
  62. delegateQueue:delegateQueue
  63. socketQueue:childSocketQueue];
  64. //IPV4 6 un
  65. if (socketType == )
  66. acceptedSocket->socket4FD = childSocketFD;
  67. else if (socketType == )
  68. acceptedSocket->socket6FD = childSocketFD;
  69. else
  70. acceptedSocket->socketUN = childSocketFD;
  71. //标记开始 并且已经连接
  72. acceptedSocket->flags = (kSocketStarted | kConnected);
  73.  
  74. // Setup read and write sources for accepted socket
  75. //初始化读写source
  76. dispatch_async(acceptedSocket->socketQueue, ^{ @autoreleasepool {
  77.  
  78. [acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD];
  79. }});
  80.  
  81. //判断代理是否实现了didAcceptNewSocket方法,把我们新创建的socket返回出去
  82. if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)])
  83. {
  84. [theDelegate socket:self didAcceptNewSocket:acceptedSocket];
  85. }
  86.  
  87. }});
  88. }
  89. return YES;
  90. }
  • 这个方法很简单,核心就是调用下面这个函数,去接受连接,并且拿到一个新的socket
    1. childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen);
  • 然后调用了newSocketQueueForConnectionFromAddress:onSocket:这个代理,可以为新的socket重新设置一个socketQueue
  • 接着我们用这个Socket重新创建了一个GCDAsyncSocket实例,然后调用我们的代理didAcceptNewSocket方法,把这个实例给传出去了。
  • 这里需要注意的是,我们调用didAcceptNewSocket代理方法传出去的实例我们需要自己保留,不然就会被释放掉,那么这个与客户端的连接也就断开了。
  • 同时我们还初始化了这个新socket的读写source,这一步完全和connect中一样,调用同一个方法,这样如果有读写数据,就会触发这个新的socketsource了。

建立连接之后的无数个新的socket,都是独立的,它们处理读写连接断开的逻辑就和客户端socket完全一样了。
而我们监听本机端口的那个socket始终只有一个,这个用来监听触发socket连接,并返回创建我们这无数个新的socket实例。

作为服务端的Accept流程就这么结束了,因为篇幅原因,所以尽量精简了一些细节的处理,不过这些处理在Connect中也是反复出现的,所以基本无伤大雅。如果大家会感到困惑,建议下载github中的源码注释,对照着再看一遍,相信会有帮助的。

接着我们来讲讲Unix Domin Socket建立本地进程通信流程:

基本上这个流程,比上述任何流程还要简单,简单的到即使不简化代码,也没多少行(当然这是建立在客户端Connect流程已经实现了很多公用方法的基础上)。

接着进入正题,我们来看看它发起连接的方法:

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

连接方法非常简单,就只是做了一些错误的处理,然后调用了其他的方法,包括一个前置检查,这检查中会去判断各种参数是否正常,如果正常会返回YES,并且把url转换成Uinix domin socket地址的结构体,赋值给我们的属性connectInterfaceUN
接着调用了connectWithAddressUN方法去发起连接。

我们接着来看看这个方法:

  1. //连接Unix域服务器
  2. - (BOOL)connectWithAddressUN:(NSData *)address error:(NSError **)errPtr
  3. {
  4. LogTrace();
  5.  
  6. NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
  7.  
  8. // Create the socket
  9.  
  10. int socketFD;
  11.  
  12. LogVerbose(@"Creating unix domain socket");
  13.  
  14. //创建本机socket
  15. socketUN = socket(AF_UNIX, SOCK_STREAM, );
  16.  
  17. socketFD = socketUN;
  18.  
  19. if (socketFD == SOCKET_NULL)
  20. {
  21. if (errPtr)
  22. *errPtr = [self errnoErrorWithReason:@"Error in socket() function"];
  23.  
  24. return NO;
  25. }
  26.  
  27. // Bind the socket to the desired interface (if needed)
  28.  
  29. LogVerbose(@"Binding socket...");
  30.  
  31. int reuseOn = ;
  32. //设置可复用
  33. setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn));
  34.  
  35. // Prevent SIGPIPE signals
  36.  
  37. int nosigpipe = ;
  38. //进程终止错误信号禁止
  39. setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
  40.  
  41. // Start the connection process in a background queue
  42.  
  43. int aStateIndex = stateIndex;
  44.  
  45. dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, );
  46. dispatch_async(globalConcurrentQueue, ^{
  47.  
  48. const struct sockaddr *addr = (const struct sockaddr *)[address bytes];
  49. //并行队列调用连接
  50. int result = connect(socketFD, addr, addr->sa_len);
  51. if (result == )
  52. {
  53. dispatch_async(socketQueue, ^{ @autoreleasepool {
  54. //连接成功的一些状态初始化
  55. [self didConnect:aStateIndex];
  56. }});
  57. }
  58. else
  59. {
  60. // 失败的处理
  61. perror("connect");
  62. NSError *error = [self errnoErrorWithReason:@"Error in connect() function"];
  63.  
  64. dispatch_async(socketQueue, ^{ @autoreleasepool {
  65.  
  66. [self didNotConnect:aStateIndex error:error];
  67. }});
  68. }
  69. });
  70.  
  71. LogVerbose(@"Connecting...");
  72.  
  73. return YES;
  74. }

主要部分基本和客户端连接相同,并且简化了很多,调用了这一行完成了连接:

  1. int result = connect(socketFD, addr, addr->sa_len);

同样也和客户端一样,在连接成功之后去调用下面这个方法完成了一些资源的初始化:

  1. [self didConnect:aStateIndex];

基本上连接就这么两个方法了(当然我们省略了一些细节),看完客户端的连接之后,到这就变得非常简单了。

接着我们来看看uinix domin socket作为服务端Accept。

这个Accpet,基本和我们普通Socket服务端的Accept相同。

  1. //接受一个Url,uniex domin socket 做为服务端
  2. - (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr;
  3. {
  4. LogTrace();
  5.  
  6. __block BOOL result = NO;
  7. __block NSError *err = nil;
  8.  
  9. //基本和正常的socket accept一模一样
  10. // CreateSocket Block
  11. // This block will be invoked within the dispatch block below.
  12. //生成一个创建socket的block,创建、绑定、监听
  13. int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) {
  14.  
  15. //creat socket
  16. ...
  17. // Set socket options
  18.  
  19. ...
  20. // Bind socket
  21.  
  22. ...
  23.  
  24. // Listen
  25. ...
  26. };
  27.  
  28. // Create dispatch block and run on socketQueue
  29. //错误判断
  30. dispatch_block_t block = ^{ @autoreleasepool {
  31.  
  32. //错误判断
  33. ...
  34.  
  35. //判断是否有这个url路径是否正确
  36. ...
  37.  
  38. //调用上面的Block创建socket,并且绑定监听。
  39. ...
  40.  
  41. //创建接受连接的source
  42. acceptUNSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketUN, , socketQueue);
  43.  
  44. int socketFD = socketUN;
  45. dispatch_source_t acceptSource = acceptUNSource;
  46. //事件句柄,和accpept一样
  47. dispatch_source_set_event_handler(acceptUNSource, ^{ @autoreleasepool {
  48. //循环去接受所有的每一个连接
  49. ...
  50. }});
  51.  
  52. //取消句柄
  53. dispatch_source_set_cancel_handler(acceptUNSource, ^{
  54.  
  55. //关闭socket
  56. close(socketFD);
  57. });
  58.  
  59. LogVerbose(@"dispatch_resume(accept4Source)");
  60. dispatch_resume(acceptUNSource);
  61.  
  62. flags |= kSocketStarted;
  63.  
  64. result = YES;
  65. }};
  66.  
  67. if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
  68. block();
  69. else
  70. dispatch_sync(socketQueue, block);
  71. //填充错误
  72. if (result == NO)
  73. {
  74. LogInfo(@"Error in accept: %@", err);
  75.  
  76. if (errPtr)
  77. *errPtr = err;
  78. }
  79.  
  80. return result;
  81. }

因为代码基本雷同,所以我们省略了大部分代码,大家可以参照着之前的讲解或者源码去理解。这里和普通服务端socket唯一的区别就是,这里服务端绑定的地址是unix domin socket类型的地址,它是一个结构体,里面包含的是我们进行进程通信的纽带-一个本机文件路径。
所以这里服务端简单来说就是绑定的这个文件路径,当这个文件路径有数据可读(即有客户端连接到达)的时候,会触发初始化的source事件句柄,我们会去循环的接受所有的连接,并且新生成一个socket实例,这里和普通的socket完全一样。

就这样我们所有的连接方式已经讲完了,后面这两种方式,为了节省篇幅,确实讲的比较粗略,但是核心的部分都有提到。
另外如果你有理解客户端的Connect流程,那么理解起来应该没有什么问题,这两个流程比前者可简化太多了。

iOS即时通讯之CocoaAsyncSocket源码解析二的更多相关文章

  1. iOS即时通讯之CocoaAsyncSocket源码解析一

    申明:本文内容属于转载整理,原文连接 前言: CocoaAsyncSocket是谷歌的开发者,基于BSD-Socket写的一个IM框架,它给Mac和iOS提供了易于使用的.强大的异步套接字库,向上封装 ...

  2. iOS即时通讯之CocoaAsyncSocket源码解析五

    接上篇:iOS即时通讯之CocoaAsyncSocket源码解析四         原文 前言: 本文为CocoaAsyncSocket Read篇终,将重点涉及该框架是如何利用缓冲区对数据进行读取. ...

  3. iOS即时通讯之CocoaAsyncSocket源码解析四

    原文 前言: 本文为CocoaAsyncSocket源码系列中第二篇:Read篇,将重点涉及该框架是如何利用缓冲区对数据进行读取.以及各种情况下的数据包处理,其中还包括普通的.和基于TLS的不同读取操 ...

  4. iOS即时通讯之CocoaAsyncSocket源码解析三

    原文 前言 本文实例Github地址:即时通讯的数据粘包.断包处理实例. 本文旨以实例的方式,使用CocoaAsyncSocket这个框架进行数据封包和拆包.来解决频繁的数据发送下,导致的数据粘包.以 ...

  5. Mybatis源码解析(二) —— 加载 Configuration

    Mybatis源码解析(二) -- 加载 Configuration    正如上文所看到的 Configuration 对象保存了所有Mybatis的配置信息,也就是说mybatis-config. ...

  6. RxJava2源码解析(二)

    title: RxJava2源码解析(二) categories: 源码解析 tags: 源码解析 rxJava2 前言 本篇主要解析RxJava的线程切换的原理实现 subscribeOn 首先, ...

  7. Sentinel源码解析二(Slot总览)

    写在前面 本文继续来分析Sentinel的源码,上篇文章对Sentinel的调用过程做了深入分析,主要涉及到了两个概念:插槽链和Node节点.那么接下来我们就根据插槽链的调用关系来依次分析每个插槽(s ...

  8. iOS开发之Masonry框架源码解析

    Masonry是iOS在控件布局中经常使用的一个轻量级框架,Masonry让NSLayoutConstraint使用起来更为简洁.Masonry简化了NSLayoutConstraint的使用方式,让 ...

  9. jQuery 源码解析二:jQuery.fn.extend=jQuery.extend 方法探究

    终于动笔开始 jQuery 源码解析第二篇,写文章还真是有难度,要把自已懂的表述清楚,要让别人听懂真的不是一见易事. 在 jQuery 源码解析一:jQuery 类库整体架构设计解析 一文,大致描述了 ...

随机推荐

  1. 为何单片机程序不用加载到RAM

    一. STM32程序构成 1.1. STM32的存储空间 1.1.1. RAM 1.1.1.1 单片机的RAM是SRAM. 1.1.1.2. SRAM特点 a.优点,速度快,不必配合内存刷新电路,可提 ...

  2. OpenGL字体绘制

    /* glfont.hpp sdragonx 2019-08-15 00:03:33 opengl字体类,提供初学者参考学习 opengl初始化之后,创建字体 font.init(L"微软雅 ...

  3. IDEA使用指北教程

    来自官网的指导手册: https://www.jetbrains.com/help/idea/2019.1/run-for-the-first-time.html?section=Windows 记得 ...

  4. HTML中button标签点击实现页面跳转

    方法1:使用onclick事件 <input type="button" value="按钮" onclick="javascrtpt:wind ...

  5. 分布式事务——幂等设计(rocketmq案例)

    幂等指的就是执行多次和执行一次的效果相同,主要是为了防止数据重复消费.MQ中为了保证消息的可靠性,生产者发送消息失败(例如网络超时)会触发 "重试机制",它不是生产者重试而是MQ自 ...

  6. c# 杀死占用某个文件的进程

    原文:c# 杀死占用某个文件的进程 需要使用微软提供的工具Handle.exe string fileName = @"H:\abc.dll";//要检查被那个进程占用的文件 Pr ...

  7. go & log

    更多日志库 https://github.com/golang/glog github.com/astaxie/beego/logs ... 这里有一个讨论 Golang的log包哪个好用? 参考 G ...

  8. luogu P1399 [NOI2013]快餐店

    传送门 注意到答案为这个基环树直径\(/2\) 因为是基环树,所以考虑把环拎出来.如果直径不过环上的边,那么可以在环上每个点下挂的子树内\(dfs\)求得.然后如果过环上的边,那么环上的部分也是一条链 ...

  9. vue : 无法加载文件 C:\Users\XXX\AppData\Roaming\npm\vue.ps1,因为在此系统上禁止运行脚本

    问题: 使用命令行安装完成vue/cli后,使用vue ui无法创建demo vue : 无法加载文件 C:\Users\yangx\AppData\Roaming\npm\vue.ps1,因为在此系 ...

  10. 前端Unicode字符图标

    前端Unicode字符图标 原文链接地址:http://www.htmleaf.com/ziliaoku/qianduanjiaocheng/20141225979.html