前言:
CoacoaAsyncSocket
是谷歌的開發者,基於BSD-Socket
寫的一個IM框架,它給Mac和iOS提供了易於使用的、強大的非同步套接字型檔,向上封裝出簡單易用OC介面。省去了我們面向Socket
以及資料流Stream
等繁瑣複雜的程式設計。
本文為一個系列,旨在讓大家瞭解CoacoaAsyncSocket
是如何基於底層進行封裝、工作的。
注:文中涉及程式碼比較多,建議大家結合原始碼一起閱讀比較容易能加深理解。這裡有樓主標註好註釋的原始碼,有需要的可以作為參照:CoacoaAsyncSocket原始碼註釋
如果對該框架用法不熟悉的話,可以參考樓主之前這篇文章:iOS即時通訊,從入門到“放棄”?,或者自行查閱。
正文:
首先我們來看看框架的結構圖:
整個庫就這麼兩個類,一個基於TCP
,一個基於UDP
。其中基於TCP的GCDAsyncSocket
,大概8000多行程式碼。而GCDAsyncUdpSocket
稍微少一點,也有5000多行。
所以單純從程式碼量上來看,這個庫還是做了很多事的。
順便提一下,之前這個框架還有一個runloop版的,不過因為功能重疊和其它種種原因,後續版本便廢棄了,現在僅有GCD
版本。
本系列我們將重點來講GCDAsyncSocket
這個類。
我們先來看看這個類的屬性:
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
@implementation GCDAsyncSocket { //flags,當前正在做操作的識別符號 uint32_t flags; uint16_t config; //代理 __weak id delegate; //代理回撥的queue dispatch_queue_t delegateQueue; //本地IPV4Socket int socket4FD; //本地IPV6Socket int socket6FD; //unix域的套接字 int socketUN; //unix域 服務端 url NSURL *socketUrl; //狀態Index int stateIndex; //本機的IPV4地址 NSData * connectInterface4; //本機的IPV6地址 NSData * connectInterface6; //本機unix域地址 NSData * connectInterfaceUN; //這個類的對Socket的操作都在這個queue中,序列 dispatch_queue_t socketQueue; dispatch_source_t accept4Source; dispatch_source_t accept6Source; dispatch_source_t acceptUNSource; //連線timer,GCD定時器 dispatch_source_t connectTimer; dispatch_source_t readSource; dispatch_source_t writeSource; dispatch_source_t readTimer; dispatch_source_t writeTimer; //讀寫資料包陣列 類似queue,最大限制為5個包 NSMutableArray *readQueue; NSMutableArray *writeQueue; //當前正在讀寫資料包 GCDAsyncReadPacket *currentRead; GCDAsyncWritePacket *currentWrite; //當前socket未獲取完的資料大小 unsigned long socketFDBytesAvailable; //全域性公用的提前緩衝區 GCDAsyncSocketPreBuffer *preBuffer; #if TARGET_OS_IPHONE CFStreamClientContext streamContext; //讀的資料流 CFReadStreamRef readStream; //寫的資料流 CFWriteStreamRef writeStream; #endif //SSL上下文,用來做SSL認證 SSLContextRef sslContext; //全域性公用的SSL的提前緩衝區 GCDAsyncSocketPreBuffer *sslPreBuffer; size_t sslWriteCachedLength; //記錄SSL讀取資料錯誤 OSStatus sslErrCode; //記錄SSL握手的錯誤 OSStatus lastSSLHandshakeError; //socket佇列的標識key void *IsOnSocketQueueOrTargetQueueKey; id userData; //連線備選服務端地址的延時 (另一個IPV4或IPV6) NSTimeInterval alternateAddressDelay; } |
這個裡定義了一些屬性,可以先簡單看看註釋,這裡我們僅僅先暫時列出來,給大家混個眼熟。
在接下來的程式碼中,會大量穿插著這些屬性的使用。所以大家不用覺得困惑,具體作用,我們後面會一一講清楚的。
接著我們來看看本文方法一–初始化方法:
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
//層級呼叫 - (id)init { return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; } - (id)initWithSocketQueue:(dispatch_queue_t)sq { return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; } - (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq { return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; } - (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq { if((self = [super init])) { delegate = aDelegate; delegateQueue = dq; //這個巨集是在sdk6.0之後才有的,如果是之前的,則OS_OBJECT_USE_OBJC為0,!0即執行if語句 //對6.0的適配,如果是6.0以下,則去retain release,6.0之後ARC也管理了GCD #if !OS_OBJECT_USE_OBJC if (dq) dispatch_retain(dq); #endif //建立socket,先都置為 -1 //本機的ipv4 socket4FD = SOCKET_NULL; //ipv6 socket6FD = SOCKET_NULL; //應該是UnixSocket socketUN = SOCKET_NULL; //url socketUrl = nil; //狀態 stateIndex = 0; if (sq) { //如果scoketQueue是global的,則報錯。斷言必須要一個非並行queue。 NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), @"The given socketQueue parameter must not be a concurrent queue."); NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), @"The given socketQueue parameter must not be a concurrent queue."); NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), @"The given socketQueue parameter must not be a concurrent queue."); //拿到scoketQueue socketQueue = sq; //iOS6之下retain #if !OS_OBJECT_USE_OBJC dispatch_retain(sq); #endif } else { //沒有的話建立一個, 名字為:GCDAsyncSocket,序列 socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL); } // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. // From the documentation: // // > Keys are only compared as pointers and are never dereferenced. // > Thus, you can use a pointer to a static variable for a specific subsystem or // > any other value that allows you to identify the value uniquely. // // We're just going to use the memory address of an ivar. // Specifically an ivar that is explicitly named for our purpose to make the code more readable. // // However, it feels tedious (and less readable) to include the "&" all the time: // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) // // So we're going to make it so it doesn't matter if we use the '&' or not, // by assigning the value of the ivar to the address of the ivar. // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; //比如原來為 0X123 -> NULL 變成 0X222->0X123->NULL //自己的指標等於自己原來的指標,成二級指標了 看了註釋是為了以後省略&,讓程式碼更可讀? IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; void *nonNullUnusedPointer = (__bridge void *)self; //dispatch_queue_set_specific給當前隊里加一個標識 dispatch_get_specific當前執行緒取出這個標識,判斷是不是在這個佇列 //這個key的值其實就是一個一級指標的地址 ,第三個引數把自己傳過去了,上下文物件?第4個引數,為銷燬的時候用的,可以指定一個函式 dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); //讀的陣列 限制為5 readQueue = [[NSMutableArray alloc] initWithCapacity:5]; currentRead = nil; //寫的陣列,限制5 writeQueue = [[NSMutableArray alloc] initWithCapacity:5]; currentWrite = nil; //設定大小為 4kb preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; #pragma mark alternateAddressDelay?? //交替地址延時?? wtf alternateAddressDelay = 0.3; } return self; } |
詳細的細節可以看看註釋,這裡初始化了一些屬性:
1.代理、以及代理queue的賦值。
2.本機socket的初始化:包括下面3種
1 2 3 4 5 6 |
//本機的ipv4 socket4FD = SOCKET_NULL; //ipv6 socket6FD = SOCKET_NULL; //UnixSocket socketUN = SOCKET_NULL; |
其中值得一提的是第三種:UnixSocket
,這個是用於Unix Domin Socket
通訊用的。
那麼什麼是Unix Domain Socket
呢?
原來它是在socket的框架上發展出一種IPC(程式間通訊)機制,雖然網路socket也可用於同一臺主機的程式間通訊(通過loopback地址127.0.0.1),但是UNIX Domain Socket用於IPC 更有效率 :
- 不需要經過網路協議棧
- 不需要打包拆包、計算校驗和、維護序號和應答等,只是將應用層資料從一個程式拷貝到另一個程式。這是因為,IPC機制本質上是可靠的通訊,而網路協議是為不可靠的通訊設計的。UNIX Domain Socket也提供面向流和麵向資料包兩種API介面,類似於TCP和UDP,但是面向訊息的UNIX Domain Socket也是可靠的,訊息既不會丟失也不會順序錯亂。
基本上它是當今應用於IPC最主流的方式。至於它到底和普通的socket
通訊實現起來有什麼區別,彆著急,我們接著往下看。
3.生成了一個socketQueue
,這個queue
是序列的,接下來我們看程式碼就會知道它貫穿於這個類的所有地方。所有對socket以及一些內部資料的相關操作,都需要在這個序列queue
中進行。這樣使得整個類沒有加一個鎖,就保證了整個類的執行緒安全。
4.建立了兩個讀寫佇列(本質陣列),接下來我們所有的讀寫任務,都會先追加在這個佇列最後,然後每次取出佇列中最前面的任務,進行處理。
5.建立了一個全域性的資料緩衝區:preBuffer
,我們所操作的資料,大部分都是要先存入這個preBuffer
中,然後再從preBuffer
取出進行處理的。
6.初始化了一個交替延時變數:alternateAddressDelay
,這個變數先簡單的理解下:就是進行另一個服務端地址請求的延時。後面我們一講到,大家就明白了。
初始化方法就到此為止了。
接著我們有socket了,我們如果是客戶端,就需要去connect
伺服器。
又或者我們是服務端的話,就需要去bind
埠,並且accept
,等待客戶端的連線。(基本上也沒有用iOS來做服務端的吧…)
這裡我們先作為客戶端來看看connect
:
其中和connect相關的方法就這麼多,我們一般這麼來連線到服務端:
1 |
[socket connectToHost:Khost onPort:Kport error:nil]; |
也就是我們在截圖中選中的方法,那我們就從這個方法作為起點,開始講起吧。
本文方法二–connect總方法
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
/逐級呼叫 - (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr { return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr]; } - (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr { return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr]; } //多一個inInterface,本機地址 - (BOOL)connectToHost:(NSString *)inHost onPort:(uint16_t)port viaInterface:(NSString *)inInterface withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr { //{} 跟蹤當前行為 LogTrace(); // Just in case immutable objects were passed //拿到host ,copy防止值被修改 NSString *host = [inHost copy]; //interface?介面? NSString *interface = [inInterface copy]; //宣告兩個__block的 __block BOOL result = NO; //error資訊 __block NSError *preConnectErr = nil; //gcdBlock ,都包裹在自動釋放池中 dispatch_block_t block = ^{ @autoreleasepool { // Check for problems with host parameter if ([host length] == 0) { NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; preConnectErr = [self badParamError:msg]; //其實就是return,大牛的程式碼真是充滿逼格 return_from_block; } // Run through standard pre-connect checks //一個前置的檢查,如果沒通過返回,這個檢查裡,如果interface有值,則會將本機的IPV4 IPV6的 address設定上。 if (![self preConnectWithInterface:interface error:&preConnectErr]) { return_from_block; } // We've made it past all the checks. // It's time to start the connection process. //flags 做或等運算。 flags標識為開始Socket連線 flags |= kSocketStarted; //又是一個{}? 只是為了標記麼? LogVerbose(@"Dispatching DNS lookup..."); // It's possible that the given host parameter is actually a NSMutableString. //很可能給我們的服務端的引數是一個可變字串 // So we want to copy it now, within this block that will be executed synchronously. //所以我們需要copy,在Block裡同步的執行 // This way the asynchronous lookup block below doesn't have to worry about it changing. //這種基於Block的非同步查詢,不需要擔心它被改變 //copy,防止改變 NSString *hostCpy = [host copy]; //拿到狀態 int aStateIndex = stateIndex; __weak GCDAsyncSocket *weakSelf = self; //全域性Queue dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //非同步執行 dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { //忽視迴圈引用 #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" //查詢錯誤 NSError *lookupErr = nil; //server地址陣列(包含IPV4 IPV6的地址 sockaddr_in6、sockaddr_in型別) NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr]; //strongSelf __strong GCDAsyncSocket *strongSelf = weakSelf; //完整Block安全形態,在加個if if (strongSelf == nil) return_from_block; //如果有錯 if (lookupErr) { //用cocketQueue dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { //一些錯誤處理,清空一些資料等等 [strongSelf lookup:aStateIndex didFail:lookupErr]; }}); } //正常 else { NSData *address4 = nil; NSData *address6 = nil; //遍歷地址陣列 for (NSData *address in addresses) { //判斷address4為空,且address為IPV4 if (!address4 && [[self class] isIPv4Address:address]) { address4 = address; } //判斷address6為空,且address為IPV6 else if (!address6 && [[self class] isIPv6Address:address]) { address6 = address; } } //非同步去發起連線 dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; }}); } #pragma clang diagnostic pop }}); //開啟連線超時 [self startConnectTimeout:timeout]; result = YES; }}; //在socketQueue中執行這個Block if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); //否則同步的調起這個queue去執行 else dispatch_sync(socketQueue, block); //如果有錯誤,賦值錯誤 if (errPtr) *errPtr = preConnectErr; //把連線是否成功的result返回 return result; } |
這個方法非常長,它主要做了以下幾件事:
- 首先我們需要說一下的是,整個類大量的會出現
LogTrace()
類似這樣的巨集,我們點進去發現它的本質只是一個{},什麼事都沒做。原來這些巨集是為了追蹤當前執行的流程用的,它被定義在一個大的
#if #else
中:123456789101112131415161718192021222324252627282930313233343536373839404142#ifndef GCDAsyncSocketLoggingEnabled#define GCDAsyncSocketLoggingEnabled 0#endif#if GCDAsyncSocketLoggingEnabled// Logging Enabled - See log level below// Logging uses the CocoaLumberjack framework (which is also GCD based).// https://github.com/robbiehanson/CocoaLumberjack//// It allows us to do a lot of logging without significantly slowing down the code.#import "DDLog.h"#define LogAsync YES#define LogContext GCDAsyncSocketLoggingContext#define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__)#define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__)#define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)#define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)#define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)#define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)#define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)#define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)#define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)#define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)#define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD)#define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__)#ifndef GCDAsyncSocketLogLevel#define GCDAsyncSocketLogLevel LOG_LEVEL_VERBOSE#endif// Log levels : off, error, warn, info, verbosestatic const int logLevel = GCDAsyncSocketLogLevel;#else// Logging Disabled#define LogError(frmt, ...) {}#define LogWarn(frmt, ...) {}#define LogInfo(frmt, ...) {}#define LogVerbose(frmt, ...) {}#define LogCError(frmt, ...) {}#define LogCWarn(frmt, ...) {}#define LogCInfo(frmt, ...) {}#define LogCVerbose(frmt, ...) {}#define LogTrace() {}#define LogCTrace(frmt, ...) {}#endif而此時因為
GCDAsyncSocketLoggingEnabled
預設為0,所以僅僅是一個{}。當標記為1時,這些巨集就可以用來輸出我們當前的業務流程,極大的方便了我們的除錯過程。 - 接著我們回到正題上,我們定義了一個
Block
,所有的連線操作都被包裹在這個Block
中。我們做了如下判斷:123456//在socketQueue中執行這個Blockif (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))block();//否則同步的調起這個queue去執行elsedispatch_sync(socketQueue, block);保證這個連線操作一定是在我們的
socketQueue
中,而且還是以序列同步的形式去執行,規避了執行緒安全的問題。 - 接著把Block中連線過程產生的錯誤進行賦值,並且把連線的結果返回出去
1234//如果有錯誤,賦值錯誤if (errPtr) *errPtr = preConnectErr;//把連線是否成功的result返回return result;
接著來看這個方法宣告的Block內部,也就是進行連線的真正主題操作,這個連線過程將會呼叫許多函式,一環扣一環,我會盡可能用最清晰、詳盡的語言來描述…
1.這個Block首先做了一些錯誤的判斷,並呼叫了一些錯誤生成的方法。類似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
if ([host length] == 0) { NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; preConnectErr = [self badParamError:msg]; //其實就是return,大牛的程式碼真是充滿逼格 return_from_block; } //用該字串生成一個錯誤,錯誤的域名,錯誤的引數 - (NSError *)badParamError:(NSString *)errMsg { NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo]; } |
2.接著做了一個前置的錯誤檢查:
1 2 3 4 |
if (![self preConnectWithInterface:interface error:&preConnectErr]) { return_from_block; } |
這個檢查方法,如果沒通過返回NO。並且如果interface有值,則會將本機的IPV4 IPV6的 address設定上。即我們之前提到的這兩個屬性:
1 2 3 4 |
//本機的IPV4地址 NSData * connectInterface4; //本機的IPV6地址 NSData * connectInterface6; |
我們來看看這個前置檢查方法:
本文方法三–前置檢查方法
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 |
//在連線之前的介面檢查,一般我們傳nil interface本機的IP 埠等等 - (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr { //先斷言,如果當前的queue不是初始化quueue,直接報錯 NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); //無代理 if (delegate == nil) // Must have delegate set { if (errPtr) { NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; *errPtr = [self badConfigError:msg]; } return NO; } //沒有代理queue if (delegateQueue == NULL) // Must have delegate queue set { if (errPtr) { NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; *errPtr = [self badConfigError:msg]; } return NO; } //當前不是非連線狀態 if (![self isDisconnected]) // Must be disconnected { if (errPtr) { NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; *errPtr = [self badConfigError:msg]; } return NO; } //判斷是否支援IPV4 IPV6 &位與運算,因為列舉是用 左位移 |
又是非常長的一個方法,但是這個方法還是非常好讀的。
- 主要是對連線前的一個屬性引數的判斷,如果不齊全的話,則填充錯誤指標,並且返回NO。
- 在這裡如果我們interface這個引數不為空話,我們會額外多執行一些操作。
首先來講講這個引數是什麼,簡單來講,這個就是我們設定的本機IP+埠號。照理來說我們是不需要去設定這個引數的,預設的為localhost(127.0.0.1)本機地址。而埠號會在本機中取一個空閒可用的埠。
而我們一旦設定了這個引數,就會強制本地IP和埠為我們指定的。其實這樣設定反而不好,其實大家也能想明白,這裡埠號如果我們寫死,萬一被其他程式給佔用了。那麼肯定是無法連線成功的。
所以就有了我們做IM的時候,一般是不會去指定客戶端bind某一個埠。而是用系統自動去選擇。 - 我們最後清空了當前讀寫queue中,所有的任務。
至於有interface
,我們所做的額外操作是什麼呢,我們接下來看看這個方法:
本文方法四–本地地址繫結方法
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
- (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr address6:(NSMutableData **)interfaceAddr6Ptr fromDescription:(NSString *)interfaceDescription port:(uint16_t)port { NSMutableData *addr4 = nil; NSMutableData *addr6 = nil; NSString *interface = nil; //先用:分割 NSArray *components = [interfaceDescription componentsSeparatedByString:@":"]; if ([components count] > 0) { NSString *temp = [components objectAtIndex:0]; if ([temp length] > 0) { interface = temp; } } if ([components count] > 1 && port == 0) { //拿到port strtol函式,將一個字串,根據base引數轉成長整型,如base值為10則採用10進位制,若base值為16則採用16進位制 long portL = strtol([[components objectAtIndex:1] UTF8String], NULL, 10); //UINT16_MAX,65535最大埠號 if (portL > 0 && portL 1111111 00000000 00000000 00000001->127.0.0.1 sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); //ipv6 struct sockaddr_in6 sockaddr6; memset(&sockaddr6, 0, sizeof(sockaddr6)); sockaddr6.sin6_len = sizeof(sockaddr6); sockaddr6.sin6_family = AF_INET6; sockaddr6.sin6_port = htons(port); sockaddr6.sin6_addr = in6addr_loopback; //賦值 addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; } //非localhost、loopback,去獲取本機IP,看和傳進來Interface是同名或者同IP,相同才給賦埠號,把資料封裝進Data。否則為nil else { //轉成cString const char *iface = [interface UTF8String]; //定義結構體指標,這個指標是本地IP struct ifaddrs *addrs; const struct ifaddrs *cursor; //獲取到本機IP,為0說明成功了 if ((getifaddrs(&addrs) == 0)) { //賦值 cursor = addrs; //如果IP不為空,則迴圈連結串列去設定 while (cursor != NULL) { //如果 addr4 IPV4地址為空,而且地址型別為IPV4 if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) { // IPv4 struct sockaddr_in nativeAddr4; //memcpy記憶體copy函式,把src開始到size的位元組數copy到 dest中 memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4)); //比較兩個字串是否相同,本機的IP名,和介面interface是否相同 if (strcmp(cursor->ifa_name, iface) == 0) { // Name match //相同則賦值 port nativeAddr4.sin_port = htons(port); //用data封號IPV4地址 addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; } //本機IP名和interface不相同 else { //宣告一個IP 16位的陣列 char ip[INET_ADDRSTRLEN]; //這裡是轉成了10進位制。。(因為獲取到的是二進位制IP) const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip)); //如果conversion不為空,說明轉換成功而且 ,比較轉換後的IP,和interface是否相同 if ((conversion != NULL) && (strcmp(ip, iface) == 0)) { // IP match //相同則賦值 port nativeAddr4.sin_port = htons(port); addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; } } } //IPV6 一樣 else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) { // IPv6 struct sockaddr_in6 nativeAddr6; memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6)); if (strcmp(cursor->ifa_name, iface) == 0) { // Name match nativeAddr6.sin6_port = htons(port); addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; } else { char ip[INET6_ADDRSTRLEN]; const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip)); if ((conversion != NULL) && (strcmp(ip, iface) == 0)) { // IP match nativeAddr6.sin6_port = htons(port); addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; } } } //指向連結串列下一個addr cursor = cursor->ifa_next; } //和getifaddrs對應,釋放這部分記憶體 freeifaddrs(addrs); } } //如果這兩個二級指標存在,則取成一級指標,把addr4賦值給它 if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; |
這個方法中,主要是大量的socket相關的函式的呼叫,會顯得比較難讀一點,其實簡單來講就做了這麼一件事:
把interface
變成進行socket操作所需要的地址結構體,然後把地址結構體包裹在NSMutableData
中。
這裡,為了讓大家能更容易理解,我把這個方法涉及到的socket
相關函式以及巨集(按照呼叫順序)都列出來:
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 |
//拿到port strtol函式,將一個字串,根據base引數轉成長整型, //如base值為10則採用10進位制,若base值為16則採用16進位制 long strtol(const char *__str, char **__endptr, int __base); //作用是在一段記憶體塊中填充某個給定的值,它是對較大的結構體或陣列進行清零操作的一種最快方法 //第一個引數為指標地址,第二個為設定值,第三個為連續設定的長度(大小) memset(void *s,int ch,size_t n); //最大埠號 #define UINT16_MAX 65535 //作用是把主機位元組序轉化為網路位元組序 htons() //引數16位 htonl() //引數32位 //獲取佔用記憶體大小 sizeof() //比較兩個指標,是否相同 相同返回0 int strcmp(const char *__s1, const char *__s2) //記憶體copu函式,把src開始到len的位元組數copy到 dest中 memcpy(dest, src, len) //inet_pton和inet_ntop這2個IP地址轉換函式,可以在將IP地址在“點分十進位制”和“二進位制整數”之間轉換 //引數socklen_t cnt,他是所指向快取區dst的大小,避免溢位,如果快取區太小無法儲存地址的值,則返回一個空指標,並將errno置為ENOSPC const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt); //得到本機地址 extern int getifaddrs(struct ifaddrs **); //釋放本機地址 extern void freeifaddrs(struct ifaddrs *); |
還有一些用到的作為引數的結構體:
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 |
//socket通訊用的 IPV4地址結構體 struct sockaddr_in { __uint8_t sin_len; //整個結構體大小 sa_family_t sin_family; //協議族,IPV4?IPV6 in_port_t sin_port; //埠 struct in_addr sin_addr; //IP地址 char sin_zero[8]; //空的佔位符,為了和其他地址結構體保持一致大小,方便轉化 }; //IPV6地址結構體,和上面的類似 struct sockaddr_in6 { __uint8_t sin6_len; /* length of this struct(sa_family_t) */ sa_family_t sin6_family; /* AF_INET6 (sa_family_t) */ in_port_t sin6_port; /* Transport layer port # (in_port_t) */ __uint32_t sin6_flowinfo; /* IP6 flow information */ struct in6_addr sin6_addr; /* IP6 address */ __uint32_t sin6_scope_id; /* scope zone index */ }; //用來獲取本機IP的引數結構體 struct ifaddrs { //指向連結串列的下一個成員 struct ifaddrs *ifa_next; //介面名稱 char *ifa_name; //介面標識位(比如當IFF_BROADCAST或IFF_POINTOPOINT設定到此標識位時,影響聯合體變數ifu_broadaddr儲存廣播地址或ifu_dstaddr記錄點對點地址) unsigned int ifa_flags; //介面地址 struct sockaddr *ifa_addr; //儲存該介面的子網掩碼; struct sockaddr *ifa_netmask; //點對點的地址 struct sockaddr *ifa_dstaddr; //ifa_data儲存了該介面協議族的特殊資訊,它通常是NULL(一般不關注他)。 void *ifa_data; }; |
這一段內容算是比較枯澀了,但是也是瞭解socket
程式設計必經之路。
這裡提到了網路位元組序和主機位元組序。我們建立socket之前,必須把port和host這些引數轉化為網路位元組序。那麼為什麼要這麼做呢?
不同的CPU有不同的位元組序型別 這些位元組序是指整數在記憶體中儲存的順序 這個叫做主機序
最常見的有兩種
1. Little endian:將低序位元組儲存在起始地址
2. Big endian:將高序位元組儲存在起始地址
這樣如果我們到網路中,就無法得知互相的位元組序是什麼了,所以我們就必須統一一套排序,這樣網路位元組序就有它存在的必要了。
網路位元組順序是TCP/IP中規定好的一種資料表示格式,它與具體的CPU型別、作業系統等無關。從而可以保證資料在不同主機之間傳輸時能夠被正確解釋。網路位元組順序採用big endian排序方式。
大家感興趣可以到這篇文章中去看看:網路位元組序與主機位元組序。
除此之外比較重要的就是這幾個地址結構體了。它定義了我們當前socket的地址資訊。包括IP、Port、長度、協議族等等。當然socket中標識為地址的結構體不止這3種,等我們後續程式碼來補充。
大家瞭解了我們上述說的知識點,這個方法也就不難度了。這個方法主要是做了本機IPV4
和IPV6
地址的建立和繫結。當然這裡分了幾種情況:
interface
為空的,我們作為客戶端不會出現這種情況。注意之前我們是這個引數不為空才會調入這個方法的。
而這個一般是用於做服務端監聽用的,這裡的處理是給本機地址繫結0地址(任意地址)。那麼這裡這麼做作用是什麼呢?引用一個應用場景來說明:如果你的伺服器有多個網路卡(每個網路卡上有不同的IP地址),而你的服務(不管是在udp埠上偵聽,還是在tcp埠上偵聽),出於某種原因:可能是你的伺服器作業系統可能隨時增減IP地址,也有可能是為了省去確定伺服器上有什麼網路埠(網路卡)的麻煩 —— 可以要在呼叫bind()的時候,告訴作業系統:“我需要在 yyyy 埠上偵聽,所有傳送到伺服器的這個埠,不管是哪個網路卡/哪個IP地址接收到的資料,都是我處理的。”這時候,伺服器程式則在0.0.0.0這個地址上進行偵聽。
- 如果
interface
為localhost
或者loopback
則把IP設定為127.0.0.1
,這裡localhost
我們大家都知道。那麼什麼是loopback
呢?
loopback地址叫做迴環地址,他不是一個物理介面上的地址,他是一個虛擬的一個地址,只要路由器在工作,這個地址就存在.它是路由器的唯一標識。
更詳細的內容可以看看百科:loopback - 如果是一個其他的地址,我們會去使用getifaddrs()函式得到本機地址。然後去對比本機名或者本機IP。有一個能相同,我們就認為該地址有效,就進行IPV4和IPV6繫結。否則什麼都不做。
至此這個本機地址繫結我們就做完了,我們前面也說過,一般我們作為客戶端,是不需要做這一步的。如果我們不繫結,系統會自己繫結本機IP,並且選擇一個空閒可用的埠。所以這個方法是iOS用來作為服務端呼叫的。
方法三–前置檢查、方法四–本機地址繫結都說完了,我們繼續接著之前的方法二往下看:
之前講到第3點了:
3.這裡把flag標記為kSocketStarted:
1 |
flags |= kSocketStarted; |
原始碼中大量的運用了3個位運算子:分別是或(|)、與(&)、取反(~)、運算子。 運用這個標記的好處也很明顯,可以很簡單的標記當前的狀態,並且因為flags所指向的列舉值是用左位移的方式:
1 2 3 |
enum GCDAsyncSocketFlags { kSocketStarted = 1 |
所以flags
可以通過|
的方式複合橫跨多個狀態,並且運算也非常輕量級,好處很多,所有的狀態標記的意義可以在註釋中清晰的看出,這裡把狀態標記為socket
已經開始連線了。
4.然後我們呼叫了一個全域性queue,非同步的呼叫連線,這裡又做了兩件事:
- 第一步是拿到我們需要連線的服務端
server
的地址陣列:
12//server地址陣列(包含IPV4 IPV6的地址 sockaddr_in6、sockaddr_in型別)NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr]; - 第二步是做一些錯誤判斷,並且把地址資訊賦值到
address4
和address6
中去,然後非同步呼叫回socketQueue
去用另一個方法去發起連線:
12345//非同步去發起連線dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {[strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];}});
在這個方法中我們可以看到作者這裡把建立server地址這些費時的邏輯操作放在了非同步執行緒中併發進行。然後得到資料之後又回到了我們的socketQueue
發起下一步的連線。
然後這裡又是兩個很大塊的分支,首先我們來看看server地址的獲取:
本文方法五–建立服務端server
地址資料:
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
//根據host、port + (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr { LogTrace(); NSMutableArray *addresses = nil; NSError *error = nil; //如果Host是這localhost或者loopback if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) { // Use LOOPBACK address struct sockaddr_in nativeAddr4; nativeAddr4.sin_len = sizeof(struct sockaddr_in); nativeAddr4.sin_family = AF_INET; nativeAddr4.sin_port = htons(port); nativeAddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); //佔位置0 memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); //ipv6 struct sockaddr_in6 nativeAddr6; nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); nativeAddr6.sin6_family = AF_INET6; nativeAddr6.sin6_port = htons(port); nativeAddr6.sin6_flowinfo = 0; nativeAddr6.sin6_addr = in6addr_loopback; nativeAddr6.sin6_scope_id = 0; // Wrap the native address structures NSData *address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; NSData *address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; //兩個新增進陣列 addresses = [NSMutableArray arrayWithCapacity:2]; [addresses addObject:address4]; [addresses addObject:address6]; } else { //拿到port String NSString *portStr = [NSString stringWithFormat:@"%hu", port]; //定義三個addrInfo 是一個sockaddr結構的連結串列而不是一個地址清單 struct addrinfo hints, *res, *res0; //初始化為0 memset(&hints, 0, sizeof(hints)); //相當於 AF_UNSPEC ,返回的是適用於指定主機名和服務名且適合任何協議族的地址。 hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; //根據host port,去獲取地址資訊。 int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); //出錯 if (gai_error) { //獲取到錯誤 error = [self gaiError:gai_error]; } //正確獲取到addrInfo else { // NSUInteger capacity = 0; //遍歷 res0 for (res = res0; res; res = res->ai_next) { //如果有IPV4 IPV6的,capacity+1 if (res->ai_family == AF_INET || res->ai_family == AF_INET6) { capacity++; } } //生成一個地址陣列,陣列為capacity大小 addresses = [NSMutableArray arrayWithCapacity:capacity]; //再去遍歷,為什麼不一次遍歷完,僅僅是為了限制陣列的大小? for (res = res0; res; res = res->ai_next) { //IPV4 if (res->ai_family == AF_INET) { // Found IPv4 address. // Wrap the native address structure, and add to results. //加到陣列中 NSData *address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; [addresses addObject:address4]; } else if (res->ai_family == AF_INET6) { // Fixes connection issues with IPv6 // https://github.com/robbiehanson/CocoaAsyncSocket/issues/429#issuecomment-222477158 // Found IPv6 address. // Wrap the native address structure, and add to results. //強轉 struct sockaddr_in6 *sockaddr = (struct sockaddr_in6 *)res->ai_addr; //拿到port in_port_t *portPtr = &sockaddr->sin6_port; //如果Port為0 if ((portPtr != NULL) && (*portPtr == 0)) { //賦值,用傳進來的port *portPtr = htons(port); } //新增到陣列 NSData *address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; [addresses addObject:address6]; } } //對應getaddrinfo 釋放記憶體 freeaddrinfo(res0); //如果地址裡一個沒有,報錯 EAI_FAIL:名字解析中不可恢復的失敗 if ([addresses count] == 0) { error = [self gaiError:EAI_FAIL]; } } } //賦值錯誤 if (errPtr) *errPtr = error; //返回地址 return addresses; } |
這個方法根據host
進行了劃分:
- 如果
host
為localhost
或者loopback
,則按照我們之前繫結本機地址那一套生成地址的方式,去生成IPV4和IPV6的地址,並且用NSData包裹住這個地址結構體,裝在NSMutableArray中。 - 不是本機地址,那麼我們就需要根據host和port去建立地址了,這裡用到的是這麼一個函式:
1int getaddrinfo( const char *hostname, const char *service, const struct addrinfo *hints, struct addrinfo **result );
這個函式主要的作用是:根據hostname(IP)
,service(port)
,去獲取地址資訊,並且把地址資訊傳遞到result
中。
而hints這個引數可以是一個空指標,也可以是一個指向某個addrinfo
結構體的指標,如果填了,其實它就是一個配置引數,返回的地址資訊會和這個配置引數的內容有關,如下例:舉例來說:指定的服務既可支援
TCP
也可支援UDP
,所以呼叫者可以把hints
結構中的ai_socktype
成員設定成SOCK_DGRAM
使得返回的僅僅是適用於資料包套介面的資訊。這裡我們可以看到result和hints這兩個引數指標指向的都是一個
addrinfo
的結構體,這是我們繼上面以來看到的第4種地址結構體了。它的定義如下:12345678910struct addrinfo {int ai_flags; /* AI_PASSIVE, AI_CANONNAME, AI_NUMERICHOST */int ai_family; /* PF_xxx */int ai_socktype; /* SOCK_xxx */int ai_protocol; /* 0 or IPPROTO_xxx for IPv4 and IPv6 */socklen_t ai_addrlen; /* length of ai_addr */char *ai_canonname; /* canonical name for hostname */struct sockaddr *ai_addr; /* binary address */struct addrinfo *ai_next; /* next structure in linked list */};我們可以看到它其中包括了一個IPV4的結構體地址
ai_addr
,還有一個指向下一個同型別資料節點的指標ai_next
。
其他引數和之前的地址結構體一些引數作用類似,大家可以對著註釋很好理解,或者仍有疑惑可以看看這篇:
socket程式設計之addrinfo結構體與getaddrinfo函式
這裡講講ai_next
這個指標,因為我們是去獲取server
端的地址,所以很可能有不止一個地址,比如IPV4、IPV6,又或者我們之前所說的一個伺服器有多個網路卡,這時候可能就會有多個地址。這些地址就會用ai_next
指標串聯起來,形成一個單連結串列。然後我們拿到這個地址連結串列,去遍歷它,對應取出IPV4、IPV6的地址,封裝成NSData並裝到陣列中去。
- 如果中間有錯誤,賦值錯誤,返回地址陣列,理清楚這幾個結構體與函式,這個方法還是相當容易讀的,具體的細節可以看看註釋。
接著我們回到本文方法二,就要用這個地址陣列去做連線了。
1 2 3 4 5 6 |
//非同步去發起連線 dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; }}); ` |
這裡呼叫了我們本文方法六–開始連線的方法1
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 |
//連線的最終方法 1 - (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6 { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); //至少有一個server地址 NSAssert(address4 || address6, @"Expected at least one valid address"); //如果狀態不一致,說明斷開連線 if (aStateIndex != stateIndex) { LogInfo(@"Ignoring lookupDidSucceed, already disconnected"); // The connect operation has been cancelled. // That is, socket was disconnected, or connection has already timed out. return; } // Check for problems //分開判斷。 BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; if (isIPv4Disabled && (address6 == nil)) { NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address."; [self closeWithError:[self otherError:msg]]; return; } if (isIPv6Disabled && (address4 == nil)) { NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address."; [self closeWithError:[self otherError:msg]]; return; } // Start the normal connection process NSError *err = nil; //呼叫連線方法,如果失敗,則錯誤返回 if (![self connectWithAddress4:address4 address6:address6 error:&err]) { [self closeWithError:err]; } } |
這個方法也比較簡單,基本上就是做了一些錯誤的判斷。比如:
- 判斷在不在這個
socket
佇列。 - 判斷傳過來的
aStateIndex
和屬性stateIndex
是不是同一個值。說到這個值,不得不提的是大神用的框架,在容錯處理上,做的真不是一般的嚴謹。從這個stateIndex
上就能略見一二。
這個aStateIndex
是我們之前呼叫方法,用屬性傳過來的,所以按道理說,是肯定一樣的。但是就怕在呼叫過程中,這個值發生了改變,這時候整個socket配置也就完全不一樣了,有可能我們已經置空地址、銷燬socket、斷開連線等等…等我們後面再來看這個屬性stateIndex
在什麼地方會發生改變。 - 判斷config中是需要哪種配置,它的引數對應了一個列舉:
123enum GCDAsyncSocketConfig{kIPv4Disabled = 1
前3個大家很好理解,無非就是用IPV4還是IPV6。
而第4個官方註釋意思是,我們即使關閉讀的流,也會保持Socket開啟。至於具體是什麼意思,我們先不在這裡討論,等後文再說。
這裡呼叫了我們本文方法七–開始連線的方法2
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
//連線最終方法 2。用兩個Server地址去連線,失敗返回NO,並填充error - (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); //輸出一些東西? LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]); LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]); // Determine socket type //判斷是否傾向於IPV6 BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; // Create and bind the sockets //如果有IPV4地址,建立IPV4 Socket if (address4) { LogVerbose(@"Creating IPv4 socket"); socket4FD = [self createSocket:AF_INET connectInterface:connectInterface4 errPtr:errPtr]; } //如果有IPV6地址,建立IPV6 Socket if (address6) { LogVerbose(@"Creating IPv6 socket"); socket6FD = [self createSocket:AF_INET6 connectInterface:connectInterface6 errPtr:errPtr]; } //如果都為空,直接返回 if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL) { return NO; } //主選socketFD,備選alternateSocketFD int socketFD, alternateSocketFD; //主選地址和備選地址 NSData *address, *alternateAddress; //IPV6 if ((preferIPv6 && socket6FD) || socket4FD == SOCKET_NULL) { socketFD = socket6FD; alternateSocketFD = socket4FD; address = address6; alternateAddress = address4; } //主選IPV4 else { socketFD = socket4FD; alternateSocketFD = socket6FD; address = address4; alternateAddress = address6; } //拿到當前狀態 int aStateIndex = stateIndex; //用socket和address去連線 [self connectSocket:socketFD address:address stateIndex:aStateIndex]; //如果有備選地址 if (alternateAddress) { //延遲去連線備選的地址 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(alternateAddressDelay * NSEC_PER_SEC)), socketQueue, ^{ [self connectSocket:alternateSocketFD address:alternateAddress stateIndex:aStateIndex]; }); } return YES; } |
這個方法也僅僅是連線中過渡的一個方法,做的事也非常簡單:
- 就是拿到IPV4和IPV6地址,先去建立對應的socket,注意這個socket是本機客戶端的,和server端沒有關係。這裡服務端的IPV4和IPV6地址僅僅是用來判斷是否需要去建立對應的本機Socket。這裡去建立socket會帶上我們之前生成的本地地址資訊
connectInterface4
或者connectInterface6
。 - 根據我們的config配置,得到主選連線和備選連線。 然後先去連線主選連線地址,在用我們一開始初始化中設定的屬性
alternateAddressDelay
,就是這個備選連線延時的屬性,去延時連線備選地址(當然如果主選地址在此時已經連線成功,會再次連線導致socket錯誤,並且關閉)。
這兩步分別呼叫了各自的方法去實現,接下來我們先來看建立本機Socket的方法:
本文方法八–建立Socket:
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 |
//建立Socket - (int)createSocket:(int)family connectInterface:(NSData *)connectInterface errPtr:(NSError **)errPtr { //建立socket,用的SOCK_STREAM TCP流 int socketFD = socket(family, SOCK_STREAM, 0); //如果建立失敗 if (socketFD == SOCKET_NULL) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; return socketFD; } //和connectInterface繫結 if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr]) { //繫結失敗,直接關閉返回 [self closeSocket:socketFD]; return SOCKET_NULL; } // Prevent SIGPIPE signals //防止終止程式的訊號? int nosigpipe = 1; //SO_NOSIGPIPE是為了避免網路錯誤,而導致程式退出。用這個來避免系統傳送signal setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); return socketFD; } |
這個方法做了這麼幾件事:
- 建立了一個socket:
12345//建立一個socket,返回值為Int。(注scoket其實就是Int型別)//第一個引數addressFamily IPv4(AF_INET) 或 IPv6(AF_INET6)。//第二個引數 type 表示 socket 的型別,通常是流stream(SOCK_STREAM) 或資料包文datagram(SOCK_DGRAM)//第三個引數 protocol 引數通常設定為0,以便讓系統自動為選擇我們合適的協議,對於 stream socket 來說會是 TCP 協議(IPPROTO_TCP),而對於 datagram來說會是 UDP 協議(IPPROTO_UDP)。int socketFD = socket(family, SOCK_STREAM, 0);
其實這個函式在之前那篇IM文章中也講過了,大家參考參考註釋看看就可以了,這裡如果返回值為-1,說明建立失敗。 - 去繫結我們之前建立的本地地址,它呼叫了另外一個方法來實現。
- 最後我們呼叫瞭如下函式:
1setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
那麼這個函式是做什麼用的呢?簡單來說,它就是給我們的socket加一些額外的設定項,來配置socket
的一些行為。它還有許多的用法,具體可以參考這篇文章:setsockopt函式而這裡的目的是為了來避免網路錯誤而出現的程式退出的情況,呼叫了這行函式,網路錯誤後,系統不再傳送程式退出的訊號。
關於這個程式退出的錯誤可以參考這篇文章:Mac OSX下SO_NOSIGPIPE的怪異表現
未完總結:
connect
篇還沒有完結,奈何篇幅問題,只能斷在這裡。下一個方法將是socket
本地繫結的方法。再下面就是我們最終的連線方法了,歷經九九八十一難,馬上就要取到真經了…(然而這僅僅是一個開始…)
下一篇將會承接這一篇的內容繼續講,包括最終連線、連線完成後的source
和流的處理。
我們還會去講講iOS
作為服務端的accpet
建立連線的流程。
除此之外還有 unix domin socket
(程式間通訊)的連線。
最近總感覺很浮躁,貼一句一直都很喜歡的話:
上善若水。水善利萬物而不爭