iOS即时通讯之CocoaAsyncSocket源码解析二
前言
本文承接上文:iOS即时通讯之CocoaAsyncSocket源码解析一
上文我们提到了GCDAsyncSocket
的初始化,以及最终connect
之前的准备工作,包括一些错误检查;本机地址创建以及socket
创建;服务端地址的创建;还有一些本机socket
可选项的配置,例如禁止网络出错导致进程关闭的信号等。
上文讲到了本文方法八--创建Socket,其中有这么一行代码:
//和connectInterface绑定 if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr]) { //绑定失败,直接关闭返回 [self closeSocket:socketFD]; return SOCKET_NULL; }
我们去用之前创建的本机地址去做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] > 0) 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 = 1; 17 18 19 //设置调用close(socket)后,仍可继续重用该socket。调用close(socket)一般不会立即关闭socket,而经历TIME_WAIT的过程。 20 setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); 21 } 22 23 //拿到地址 24 const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes]; 25 //绑定这个地址 26 int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]); 27 28 //绑定出错,返回NO 29 if (result != 0) 30 { 31 if (errPtr) 32 *errPtr = [self errnoErrorWithReason:@"Error in bind() function"]; 33 34 return NO; 35 } 36 } 37 38 //成功 39 return YES; 40 }
这个方法也非常简单,如果没有connectInterface则直接返回YES,当socket进行连接的时候,会自动绑定一个端口,进行连接。
如果有值,则我们开始绑定到我们一开始指定的地址上。
这里调用了两个和scoket相关的函数:
第一个是我们之前提到的配置scoket参数的函数:
setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn));
这里调用这个函数的主要目的是为了调用close
的时候,不立即去关闭socket
连接,而是经历一个TIME_WAIT
过程。在这个过程中,socket
是可以被复用的。我们注意到之前的connect
流程并没有看到复用socket
的代码。注意,我们现在走的连接流程是客户端的流程,等我们讲到服务端accept
进行连接的时候,我们就能看到这个复用的作用了。
第二个是bind函数
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, 0); 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 == 0) 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 }
这个方法主要就是做了一件事,调用下面一个函数进行连接:
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 == -1) 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 }
这个方法很长一大串,其实做的东西也很简单,主要做了下面几件事:
- 把当前状态flags加上已连接,并且关闭掉我们一开始连接开启的,连接超时的定时器。
- 初始化了两个
Block
:SetupStreamsPart1
、SetupStreamsPart2
,这两个Block
做的事都和读写流有关。SetupStreamsPart1
用来创建读写流,并且注册回调。另一个SetupStreamsPart2
用来把流添加到当前线程的runloop
上,并且打开流。 - 判断是否有代理
queue
、host
或者url
这些参数是否为空、是否代理响应didConnectToHost
或didConnectToUrl
代理,这两种分别对应了普通socket
连接和unix domin socket
连接。如果实现了对应的代理,则调用连接成功的代理。 -
在调用代理的同时,调用了我们之前初始化的两个读写流相关的
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,调用如下函数:
int result = fcntl(socketFD, F_SETFL, O_NONBLOCK);
简单来说,这个函数类似我们之前提到的一个函数setsockopt()
,都是给socket
设置一些参数,以实现一些功能。而这个函数,能实现的功能更多。大家可以看看这篇文章参考参考:fcntl函数详解
而在这里,就是为了把socket
的IO模式设置为非阻塞。很多小伙伴又要疑惑什么是非阻塞了,先别急,关于这个我们下文会详细的来谈。
6、我们初始化了读写source(很重要,所有的消息都是由这个source来触发的,我们之后会详细分析这个方法)。
7、我们做完了stream
和source
的初始化处理,则开始做一次读写任务(这两个方法暂时不讲,会放到之后的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 - 绑定Socket和CFStream 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 41 42 //读写stream都设置成不会随着绑定的socket一起close,release。 kCFBooleanFalse不一起,kCFBooleanTrue一起 43 if (readStream) 44 CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); 45 if (writeStream) 46 CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); 47 48 //如果有一个为空 49 if ((readStream == NULL) || (writeStream == NULL)) 50 { 51 LogWarn(@"Unable to create read and write stream..."); 52 53 //关闭对应的stream 54 if (readStream) 55 { 56 CFReadStreamClose(readStream); 57 CFRelease(readStream); 58 readStream = NULL; 59 } 60 if (writeStream) 61 { 62 CFWriteStreamClose(writeStream); 63 CFRelease(writeStream); 64 writeStream = NULL; 65 } 66 //返回创建失败 67 return NO; 68 } 69 //创建成功 70 return YES; 71 }
这个方法基本上很简单,就是关于两个stream函数的调用:
1、创建stream的函数:
-
CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream);
这个函数创建了一对读写stream,并且把stream与这个scoket做了绑定。
2、设置stream属性:
-
CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);
这个函数可以给
stream
设置一个属性,这里是设置stream
不会随着socket
的生命周期(close
,release
)而变化。
接着调用了registerForStreamCallbacksIncludingReadWrite
来给stream
注册读写回调。
Stream相关方法二 -- 读写回调的注册:
//注册Stream的回调 - (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite { LogVerbose(@"%@ %@", THIS_METHOD, (includeReadWrite ? @"YES" : @"NO")); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); //判断读写stream是不是都为空 NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); //客户端stream上下文对象 streamContext.version = 0; streamContext.info = (__bridge void *)(self); streamContext.retain = nil; streamContext.release = nil; streamContext.copyDescription = nil; // The open has completed successfully. // The stream has bytes to be read. // The stream can accept bytes for writing. // An error has occurred on the stream. // The end of the stream has been reached. //设置一个CF的flag 两种,一种是错误发生的时候,一种是stream事件结束 CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered ; //如果包含读写 if (includeReadWrite) //仍然有Bytes要读的时候 The stream has bytes to be read. readStreamEvents |= kCFStreamEventHasBytesAvailable; //给读stream设置客户端,会在之前设置的那些标记下回调函数 CFReadStreamCallback。设置失败的话直接返回NO if (!CFReadStreamSetClient(readStream, readStreamEvents, &CFReadStreamCallback, &streamContext)) { return NO; } //写的flag,也一样 CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; if (includeReadWrite) writeStreamEvents |= kCFStreamEventCanAcceptBytes; if (!CFWriteStreamSetClient(writeStream, writeStreamEvents, &CFWriteStreamCallback, &streamContext)) { return NO; } //走到最后说明读写都设置回调成功,返回YES return YES; }
相信用过CFStream
的朋友,应该会觉得很简单,这个方法就是调用了一些CFStream
相关函数,其中最主要的这个设置读写回调函数:
Boolean CFReadStreamSetClient(CFReadStreamRef stream, CFOptionFlags streamEvents, CFReadStreamClientCallBack clientCB, CFStreamClientContext *clientContext);
Boolean CFWriteStreamSetClient(CFWriteStreamRef stream, CFOptionFlags streamEvents, CFWriteStreamClientCallBack clientCB, CFStreamClientContext *clientContext);
这个函数共4个参数:
第1个为我们需要设置的stream;
第2个为需要监听的事件选项,包括以下事件:
typedef CF_OPTIONS(CFOptionFlags, CFStreamEventType) { kCFStreamEventNone = 0, //没有事件发生 kCFStreamEventOpenCompleted = 1, //成功打开流 kCFStreamEventHasBytesAvailable = 2, //流中有数据可读 kCFStreamEventCanAcceptBytes = 4, //流中可以接受数据去写 kCFStreamEventErrorOccurred = 8, //流发生错误 kCFStreamEventEndEncountered = 16 //到达流的结尾 };
其中具体用法,大家可以自行去试试,这里作者只监听了了两种事件kCFStreamEventErrorOccurred
和kCFStreamEventEndEncountered
,再根据传过来的参数去决定是否监听kCFStreamEventCanAcceptBytes
:
//如果包含读写 if (includeReadWrite) //仍然有Bytes要读的时候 The stream has bytes to be read. readStreamEvents |= kCFStreamEventHasBytesAvailable;
而这里我们传过来的参数为NO,导致它并不监听可读数据。显然,我们正常的连接,当有消息发送过来,并不是由stream
回调来触发的。这个框架中,如果是TLS
传输的socket
是用stream
来触发的,这个我们后续文章会讲到。
那么有数据的时候,到底是什么来触发我们的读写呢,答案就是读写source
,我们接下来就会去创建初始化它。
这里绑定了两个函数,分别对应读和写的回调,分别为:
//读的回调 static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo) //写的回调 static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo)
关于这两个函数,同样这里暂时不做讨论,等后续文章再来分析。
还有一点需要说一下的是streamContext
这个属性,它是一个结构体,包含流的上下文信息,其结构如下:
typedef struct { CFIndex version; void *info; void *(*retain)(void *info); void (*release)(void *info); CFStringRef (*copyDescription)(void *info); } CFStreamClientContext
这个流的上下文中info
指针,其实就是前面所对应的读写回调函数中的pInfo
指针,每次回调都会传过去。其它的version
就是流的版本标识,之外的3个都需要的是一个函数指针,对应我们传递的pInfo
的持有以及释放还有复制的描述信息,这里我们都赋值给nil
。
接着我们来到流处理的第三步:addStreamsToRunLoop
-添加到runloop上。
Stream相关方法三 -- 加到当前线程的runloop
//把stream添加到runloop上 - (BOOL)addStreamsToRunLoop { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); //判断flag里是否包含kAddedStreamsToRunLoop,没添加过则添加。 if (!(flags & kAddedStreamsToRunLoop)) { LogVerbose(@"Adding streams to runloop..."); [[self class] startCFStreamThreadIfNeeded]; //在开启的线程中去执行,阻塞式的 [[self class] performSelector:@selector(scheduleCFStreams:) onThread:cfstreamThread withObject:self waitUntilDone:YES]; //添加标识 flags |= kAddedStreamsToRunLoop; } return YES; }
这里方法做了两件事:
1、开启了一条用于CFStream
读写回调的常驻线程,其中调用了好几个函数:
-
+ (void)startCFStreamThreadIfNeeded; + (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
的注册: -
CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); 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, 0, socketQueue); 6 //_dispatch_source_type_write :监视着 socketFD,直到写数据了 7 writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, 0, 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 > 0) 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 = 2; 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 == 0) 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 == 0) 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 = 0; 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个函数:
//创建source dispatch_source_create(dispatch_source_type_t type, uintptr_t handle, unsigned long mask, dispatch_queue_t _Nullable queue); //为source设置事件句柄 dispatch_source_set_event_handler(dispatch_source_t source, dispatch_block_t _Nullable handler); //为source设置取消句柄 dispatch_source_set_cancel_handler(dispatch_source_t source, dispatch_block_t _Nullable handler);
相信大家用至少用过GCD
定时器,接触过这3个函数,这里创建source的函数,根据参数type的不同,可以处理不同的事件:
这里我们用的是DISPATCH_SOURCE_TYPE_READ
和DISPATCH_SOURCE_TYPE_WRITE
这两个类型。标识如果handle
如果有可读或者可写数据时,会触发我们的事件句柄。
- 而这里初始化的读写事件句柄内容也很简单,就是去读写数据。
- 而取消句柄也就是去关闭
socket
。 - 初始化完成后,我们开启了
readSource
,一旦有数据过来就触发了我们readSource
事件句柄,就可以去监听的socket
所分配的缓冲区中去读取数据了,而wirteSource
初始化完是挂起的。 - 除此之外我们还初始化了当前
source
的状态,用于我们后续的操作。
至此我们客户端的整个Connect
流程结束了,用一张图来概括总结一下吧:
整个客户端连接的流程大致如上图,当然远不及于此,这里我们对地址做了IPV4
和IPV6
的兼容处理,对一些使用socket
而产生的网络错误导致进程退出的容错处理。以及在这个过程中,socketQueue
、代理queue
、全局并发queue
和stream
常驻线程的管理调度等等。
当然其中绝大部分操作都是在socketQueue
中进行的。而在socketQueue
中,我们也分为两种操作dispatch_sync
和dispatch_async
。
因为socketQueue
本身就是一个串行queue
,所以我们所有的操作都在这个queue
中进行保证了线程安全,而需要阻塞后续行为的操作,我们用了sync
的方式。其实这样使用sync
是及其容易死锁的,但是作者每次在调用sync
之前都调用了这么一行判断:
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
判断当前队列是否就是这个socketQueue
队列,如果是则直接调用,否则就用sync
的方式提交到这个queue
中去执行。这种防死锁的方式,你学到了么?
接着我们来讲讲服务端Accept
流程:
整个流程还是相对Connec
t来说还是十分简单的,因为这个方法很长,而且大多数是我们直接连接讲到过得内容,所以我省略了一部分的代码,只把重要的展示出来,大家可以参照着源码看。
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, 0); 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, 1024); 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, 0, socketQueue); 61 62 63 64 //事件句柄 65 dispatch_source_set_event_handler(accept4Source, ^{ @autoreleasepool { 66 67 //拿到数据,连接数 68 unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); 69 70 LogVerbose(@"numPendingConnections: %lu", numPendingConnections); 71 72 //循环去接受这些socket的事件(一次触发可能有多个连接) 73 while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); 74 75 }}); 76 77 //取消句柄 78 dispatch_source_set_cancel_handler(accept4Source, ^{ 79 //... 80 //关闭socket 81 close(socketFD); 82 83 }); 84 85 //开启source 86 dispatch_resume(accept4Source); 87 } 88 89 //ipv6一样 90 ... 91 92 //在scoketQueue中同步做这些初始化。 93 if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) 94 block(); 95 else 96 dispatch_sync(socketQueue, block); 97 98 //...错误判断 99 //返回结果 100 return result; 101 }
这个方法省略完仍然有这么长,它主要做了这两件事(篇幅原因,尽量精简):
- 创建本机地址、创建socket、绑定端口、监听端口。
- 创建了一个
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 = 0; 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 == -1) 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 == 0) 66 acceptedSocket->socket4FD = childSocketFD; 67 else if (socketType == 1) 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
-
childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen);
- 然后调用了
newSocketQueueForConnectionFromAddress:onSocket:
这个代理,可以为新的socket重新设置一个socketQueue
。 - 接着我们用这个
Socket
重新创建了一个GCDAsyncSocket
实例,然后调用我们的代理didAcceptNewSocket
方法,把这个实例给传出去了。 - 这里需要注意的是,我们调用
didAcceptNewSocket
代理方法传出去的实例我们需要自己保留,不然就会被释放掉,那么这个与客户端的连接也就断开了。 - 同时我们还初始化了这个新
socket
的读写source,这一步完全和connect
中一样,调用同一个方法,这样如果有读写数据,就会触发这个新的socket
的source
了。
建立连接之后的无数个新的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] == 0) 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, 0); 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 = 1; 32 //设置可复用 33 setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); 34 35 // Prevent SIGPIPE signals 36 37 int nosigpipe = 1; 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, 0); 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 == 0) 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 }
主要部分基本和客户端连接相同,并且简化了很多,调用了这一行完成了连接:
int result = connect(socketFD, addr, addr->sa_len);
同样也和客户端一样,在连接成功之后去调用下面这个方法完成了一些资源的初始化:
[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 36 //判断是否有这个url路径是否正确 37 ... 38 39 //调用上面的Block创建socket,并且绑定监听。 40 ... 41 42 //创建接受连接的source 43 acceptUNSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketUN, 0, socketQueue); 44 45 int socketFD = socketUN; 46 dispatch_source_t acceptSource = acceptUNSource; 47 //事件句柄,和accpept一样 48 dispatch_source_set_event_handler(acceptUNSource, ^{ @autoreleasepool { 49 //循环去接受所有的每一个连接 50 ... 51 }}); 52 53 //取消句柄 54 dispatch_source_set_cancel_handler(acceptUNSource, ^{ 55 56 //关闭socket 57 close(socketFD); 58 }); 59 60 LogVerbose(@"dispatch_resume(accept4Source)"); 61 dispatch_resume(acceptUNSource); 62 63 flags |= kSocketStarted; 64 65 result = YES; 66 }}; 67 68 if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) 69 block(); 70 else 71 dispatch_sync(socketQueue, block); 72 //填充错误 73 if (result == NO) 74 { 75 LogInfo(@"Error in accept: %@", err); 76 77 if (errPtr) 78 *errPtr = err; 79 } 80 81 return result; 82 }
因为代码基本雷同,所以我们省略了大部分代码,大家可以参照着之前的讲解或者源码去理解。这里和普通服务端socket
唯一的区别就是,这里服务端绑定的地址是unix domin socket
类型的地址,它是一个结构体,里面包含的是我们进行进程通信的纽带-一个本机文件路径。
所以这里服务端简单来说就是绑定的这个文件路径,当这个文件路径有数据可读(即有客户端连接到达)的时候,会触发初始化的source事件句柄,我们会去循环的接受所有的连接,并且新生成一个socket
实例,这里和普通的socket
完全一样。
就这样我们所有的连接方式已经讲完了,后面这两种方式,为了节省篇幅,确实讲的比较粗略,但是核心的部分都有提到。
另外如果你有理解客户端的Connect
流程,那么理解起来应该没有什么问题,这两个流程比前者可简化太多了。