前言:
CocoaAsyncSocket
是谷歌的開發者,基于BSD-Socket
寫的一個IM框架,它給Mac和iOS提供了易于使用的、強大的異步套接字庫,向上封裝出簡單易用OC接口。省去了我們面向Socket
以及數據流Stream
等繁瑣復雜的編程。
本文為一個系列,旨在讓大家了解CocoaAsyncSocket
是如何基于底層進行封裝、工作的。
注:文中涉及代碼比較多,建議大家結合源碼一起閱讀比較容易能加深理解。這里有樓主標注好注釋的源碼,有需要的可以作為參照:CocoaAsyncSocket源碼注釋
如果對該框架用法不熟悉的話,可以參考樓主之前這篇文章:iOS即時通訊,從入門到“放棄”?,或者自行查閱。
正文:
首先我們來看看框架的結構圖:
整個庫就這么兩個類,一個基于TCP
,一個基于UDP
。其中基于TCP的GCDAsyncSocket
,大概8000多行代碼。而GCDAsyncUdpSocket
稍微少一點,也有5000多行。
所以單純從代碼量上來看,這個庫還是做了很多事的。
順便提一下,之前這個框架還有一個runloop版的,不過因為功能重疊和其它種種原因,后續版本便廢棄了,現在僅有GCD
版本。
本系列我們將重點來講GCDAsyncSocket
這個類。
我們先來看看這個類的屬性:
@implementation GCDAsyncSocket
{
//flags,當前正在做操作的標識符
uint32_t flags;
uint16_t config;
//代理
__weak id<GCDAsyncSocketDelegate> 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;
}
這個里定義了一些屬性,可以先簡單看看注釋,這里我們僅僅先暫時列出來,給大家混個眼熟。
在接下來的代碼中,會大量穿插著這些屬性的使用。所以大家不用覺得困惑,具體作用,我們后面會一一講清楚的。
接著我們來看看本文方法一--初始化方法:
//層級調用
- (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種
//本機的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.png
其中和connect相關的方法就這么多,我們一般這么來連接到服務端:
[socket connectToHost:Khost onPort:Kport error:nil];
也就是我們在截圖中選中的方法,那我們就從這個方法作為起點,開始講起吧。
本文方法二--connect總方法
/逐級調用
- (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
中:
#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, verbose
static 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
中。我們做了如下判斷:
//在socketQueue中執行這個Block
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
//否則同步的調起這個queue去執行
else
dispatch_sync(socketQueue, block);
保證這個連接操作一定是在我們的socketQueue
中,而且還是以串行同步的形式去執行,規避了線程安全的問題。
- 接著把Block中連接過程產生的錯誤進行賦值,并且把連接的結果返回出去
//如果有錯誤,賦值錯誤
if (errPtr) *errPtr = preConnectErr;
//把連接是否成功的result返回
return result;
接著來看這個方法聲明的Block內部,也就是進行連接的真正主題操作,這個連接過程將會調用許多函數,一環扣一環,我會盡可能用最清晰、詳盡的語言來描述...
1.這個Block首先做了一些錯誤的判斷,并調用了一些錯誤生成的方法。類似:
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.接著做了一個前置的錯誤檢查:
if (![self preConnectWithInterface:interface error:&preConnectErr])
{
return_from_block;
}
這個檢查方法,如果沒通過返回NO。并且如果interface有值,則會將本機的IPV4 IPV6的 address設置上。即我們之前提到的這兩個屬性:
//本機的IPV4地址
NSData * connectInterface4;
//本機的IPV6地址
NSData * connectInterface6;
我們來看看這個前置檢查方法:
本文方法三--前置檢查方法
//在連接之前的接口檢查,一般我們傳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 &位與運算,因為枚舉是用 左位移<<運算定義的,所以可以用來判斷 config包不包含某個枚舉。因為一個值可能包含好幾個枚舉值,所以這時候不能用==來判斷,只能用&來判斷
BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
//是否都不支持
if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled
{
if (errPtr)
{
NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first.";
*errPtr = [self badConfigError:msg];
}
return NO;
}
//如果有interface,本機地址
if (interface)
{
NSMutableData *interface4 = nil;
NSMutableData *interface6 = nil;
//得到本機的IPV4 IPV6地址
[self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0];
//如果兩者都為nil
if ((interface4 == nil) && (interface6 == nil))
{
if (errPtr)
{
NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address.";
*errPtr = [self badParamError:msg];
}
return NO;
}
if (isIPv4Disabled && (interface6 == nil))
{
if (errPtr)
{
NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6.";
*errPtr = [self badParamError:msg];
}
return NO;
}
if (isIPv6Disabled && (interface4 == nil))
{
if (errPtr)
{
NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4.";
*errPtr = [self badParamError:msg];
}
return NO;
}
//如果都沒問題,則賦值
connectInterface4 = interface4;
connectInterface6 = interface6;
}
// Clear queues (spurious read/write requests post disconnect)
//清除queue(假的讀寫請求 ,提交斷開連接)
//讀寫Queue清除
[readQueue removeAllObjects];
[writeQueue removeAllObjects];
return YES;
}
又是非常長的一個方法,但是這個方法還是非常好讀的。
主要是對連接前的一個屬性參數的判斷,如果不齊全的話,則填充錯誤指針,并且返回NO。
在這里如果我們interface這個參數不為空話,我們會額外多執行一些操作。
首先來講講這個參數是什么,簡單來講,這個就是我們設置的本機IP+端口號。照理來說我們是不需要去設置這個參數的,默認的為localhost(127.0.0.1)本機地址。而端口號會在本機中取一個空閑可用的端口。
而我們一旦設置了這個參數,就會強制本地IP和端口為我們指定的。其實這樣設置反而不好,其實大家也能想明白,這里端口號如果我們寫死,萬一被其他進程給占用了。那么肯定是無法連接成功的。
所以就有了我們做IM的時候,一般是不會去指定客戶端bind某一個端口。而是用系統自動去選擇。我們最后清空了當前讀寫queue中,所有的任務。
至于有interface
,我們所做的額外操作是什么呢,我們接下來看看這個方法:
本文方法四--本地地址綁定方法
- (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 <= UINT16_MAX)
{
port = (uint16_t)portL;
}
}
//為空則自己創建一個 0x00000000 ,全是0 ,為線路地址
//如果端口為0 通常用于分析操作系統。這一方法能夠工作是因為在一些系統中“0”是無效端口,當你試圖使用通常的閉合端口連接它時將產生不同的結果。一種典型的掃描,使用IP地址為0.0.0.0,設置ACK位并在以太網層廣播。
if (interface == nil)
{
struct sockaddr_in sockaddr4;
//memset作用是在一段內存塊中填充某個給定的值,它是對較大的結構體或數組進行清零操作的一種最快方法
//memset(void *s,int ch,size_t n);函數,第一個參數為指針地址,第二個為設置值,第三個為連續設置的長度(大小)
memset(&sockaddr4, 0, sizeof(sockaddr4));
//結構體長度
sockaddr4.sin_len = sizeof(sockaddr4);
//addressFamily IPv4(AF_INET) 或 IPv6(AF_INET6)。
sockaddr4.sin_family = AF_INET;
//端口號 htons將主機字節順序轉換成網絡字節順序 16位
sockaddr4.sin_port = htons(port);
//htonl ,將INADDR_ANY:0.0.0.0,不確定地址,或者任意地址 htonl 32位。 也是轉為網絡字節序
//ipv4 32位 4個字節 INADDR_ANY,0x00000000 (16進制,一個0代表4位,8個0就是32位) = 4個字節的
sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY);
struct sockaddr_in6 sockaddr6;
memset(&sockaddr6, 0, sizeof(sockaddr6));
sockaddr6.sin6_len = sizeof(sockaddr6);
//ipv6
sockaddr6.sin6_family = AF_INET6;
//port
sockaddr6.sin6_port = htons(port);
//共128位
sockaddr6.sin6_addr = in6addr_any;
//把這兩個結構體轉成data
addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)];
addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)];
}
//如果localhost、loopback 回環地址,虛擬地址,路由器工作它就存在。一般用來標識路由器
//這兩種的話就賦值為127.0.0.1,端口為port
else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"])
{
// LOOPBACK address
//ipv4
struct sockaddr_in sockaddr4;
memset(&sockaddr4, 0, sizeof(sockaddr4));
sockaddr4.sin_len = sizeof(sockaddr4);
sockaddr4.sin_family = AF_INET;
sockaddr4.sin_port = htons(port);
//#define INADDR_LOOPBACK (u_int32_t)0x7f000001
//7f000001->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
相關函數以及宏(按照調用順序)都列出來:
//拿到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 *);
還有一些用到的作為參數的結構體:
//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:
flags |= kSocketStarted;
源碼中大量的運用了3個位運算符:分別是或(|)、與(&)、取反(~)、運算符。 運用這個標記的好處也很明顯,可以很簡單的標記當前的狀態,并且因為flags所指向的枚舉值是用左位移的方式:
enum GCDAsyncSocketFlags
{
kSocketStarted = 1 << 0, // If set, socket has been started (accepting/connecting)
kConnected = 1 << 1, // If set, the socket is connected
kForbidReadsWrites = 1 << 2, // If set, no new reads or writes are allowed
kReadsPaused = 1 << 3, // If set, reads are paused due to possible timeout
kWritesPaused = 1 << 4, // If set, writes are paused due to possible timeout
kDisconnectAfterReads = 1 << 5, // If set, disconnect after no more reads are queued
kDisconnectAfterWrites = 1 << 6, // If set, disconnect after no more writes are queued
kSocketCanAcceptBytes = 1 << 7, // If set, we know socket can accept bytes. If unset, it's unknown.
kReadSourceSuspended = 1 << 8, // If set, the read source is suspended
kWriteSourceSuspended = 1 << 9, // If set, the write source is suspended
kQueuedTLS = 1 << 10, // If set, we've queued an upgrade to TLS
kStartingReadTLS = 1 << 11, // If set, we're waiting for TLS negotiation to complete
kStartingWriteTLS = 1 << 12, // If set, we're waiting for TLS negotiation to complete
kSocketSecure = 1 << 13, // If set, socket is using secure communication via SSL/TLS
kSocketHasReadEOF = 1 << 14, // If set, we have read EOF from socket
kReadStreamClosed = 1 << 15, // If set, we've read EOF plus prebuffer has been drained
kDealloc = 1 << 16, // If set, the socket is being deallocated
#if TARGET_OS_IPHONE
kAddedStreamsToRunLoop = 1 << 17, // If set, CFStreams have been added to listener thread
kUsingCFStreamForTLS = 1 << 18, // If set, we're forced to use CFStream instead of SecureTransport
kSecureSocketHasBytesAvailable = 1 << 19, // If set, CFReadStream has notified us of bytes available
#endif
};
所以flags
可以通過|
的方式復合橫跨多個狀態,并且運算也非常輕量級,好處很多,所有的狀態標記的意義可以在注釋中清晰的看出,這里把狀態標記為socket
已經開始連接了。
4.然后我們調用了一個全局queue,異步的調用連接,這里又做了兩件事:
- 第一步是拿到我們需要連接的服務端
server
的地址數組:
//server地址數組(包含IPV4 IPV6的地址 sockaddr_in6、sockaddr_in類型)
NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr];
- 第二步是做一些錯誤判斷,并且把地址信息賦值到
address4
和address6
中去,然后異步調用回socketQueue
去用另一個方法去發起連接:
//異步去發起連接
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
[strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
}});
在這個方法中我們可以看到作者這里把創建server地址這些費時的邏輯操作放在了異步線程中并發進行。然后得到數據之后又回到了我們的socketQueue
發起下一步的連接。
然后這里又是兩個很大塊的分支,首先我們來看看server地址的獲取:
本文方法五--創建服務端server
地址數據:
//根據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去創建地址了,這里用到的是這么一個函數:
int 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種地址結構體了。它的定義如下:
struct 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并裝到數組中去。
- 如果中間有錯誤,賦值錯誤,返回地址數組,理清楚這幾個結構體與函數,這個方法還是相當容易讀的,具體的細節可以看看注釋。
接著我們回到本文方法二,就要用這個地址數組去做連接了。
//異步去發起連接
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
[strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
}});
這里調用了我們本文方法六--開始連接的方法1
//連接的最終方法 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中是需要哪種配置,它的參數對應了一個枚舉:
enum GCDAsyncSocketConfig
{
kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled
kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled
kPreferIPv6 = 1 << 2, // If set, IPv6 is preferred over IPv4
kAllowHalfDuplexConnection = 1 << 3, // If set, the socket will stay open even if the read stream closes
};
前3個大家很好理解,無非就是用IPV4還是IPV6。
而第4個官方注釋意思是,我們即使關閉讀的流,也會保持Socket開啟。至于具體是什么意思,我們先不在這里討論,等后文再說。
這里調用了我們本文方法七--開始連接的方法2
//連接最終方法 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:
//創建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:
//創建一個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,說明創建失敗。
- 去綁定我們之前創建的本地地址,它調用了另外一個方法來實現。
- 最后我們調用了如下函數?:
setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
那么這個函數是做什么用的呢?簡單來說,它就是給我們的socket加一些額外的設置項,來配置socket
的一些行為。它還有許多的用法,具體可以參考這篇文章:setsockopt函數
而這里的目的是為了來避免網絡錯誤而出現的進程退出的情況,調用了這行函數,網絡錯誤后,系統不再發送進程退出的信號。
關于這個進程退出的錯誤可以參考這篇文章:Mac OSX下SO_NOSIGPIPE的怪異表現
未完總結:
connect
篇還沒有完結,奈何篇幅問題,只能斷在這里。下一個方法將是socket
本地綁定的方法。再下面就是我們最終的連接方法了,歷經九九八十一難,馬上就要取到真經了...(然而這僅僅是一個開始...)
下一篇將會承接這一篇的內容繼續講,包括最終連接、連接完成后的source
和流的處理。
我們還會去講講iOS
作為服務端的accpet
建立連接的流程。
除此之外還有 unix domin socket
(進程間通信)的連接。
最近總感覺很浮躁,貼一句一直都很喜歡的話:
上善若水。水善利萬物而不爭