1. 程式人生 > IOS開發 >iOS GCDAsyncSocket原始碼分析(一)

iOS GCDAsyncSocket原始碼分析(一)

序言

上一篇文章文章中,簡單介紹了GCDAsyncSocket的使用,socket建立、連線、傳送訊息、接收訊息、關閉socket、粘包分包、以及心跳包機制。並且立下了一個flag,所以在這篇文章,將帶來GCDAsyncSocket的原始碼分析,看看在GCDAsyncSocket中是如何運用原生程式碼並封裝起來的,在簡單實現的原生程式碼基礎上,他又做了什麼樣的操作。

我們還是按照建立socket、連線socket、傳送訊息、接收訊息、關閉socket的順序,一步一步深入瞭解GCDAsyncSocket。

1. GCDAsyncSocket初始化

在開始之前,GCDAsyncSocket.m中聲明瞭許許多多的成員變數,先看看都是啥。

@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域的套接字 // 程序通訊  locahost VS 127.0.0.1
	int socketUN;
    //unix域 服務端 url
	NSURL *socketUrl;
    //狀態Index
	int stateIndex;
    
    //本機的IPV4地址  --- 地址host interface
	NSData * connectInterface4;
    //本機的IPV6地址
	NSData * connectInterface6;
    //本機unix域地址
	NSData * connectInterfaceUN;
	
    //這個類的對Socket的操作都在這個queue中,序列
	dispatch_queue_t socketQueue;
	
    // 源 ---> mergdata  get_data buffer tls ssl CFStream
    // data
	dispatch_source_t accept4Source;
	dispatch_source_t accept6Source;
	dispatch_source_t acceptUNSource;
    
    //連線timer,GCD定時器 重連
	dispatch_source_t connectTimer;
	dispatch_source_t read
Source; dispatch_source_t writeSource; dispatch_source_t readTimer; dispatch_source_t writeTimer; //讀寫資料包陣列 類似queue,最大限制為5個包 - FIFO NSMutableArray *readQueue; NSMutableArray *writeQueue; //當前正在讀寫資料包 GCDAsyncReadPacket *currentRead; GCDAsyncWritePacket *currentWrite; //當前socket未獲取完的資料大小 unsigned long socketFDBytesAvailable; //全域性公用的提前緩衝區 GCDAsyncSocketPreBuffer *preBuffer; #if TARGET_OS_IPHONE
CFStreamClientContext streamContext; //讀的資料流 ---- c CFReadStreamRef readStream; //寫的資料流 CFWriteStreamRef writeStream; #endif //SSL上下文,用來做SSL認證 SSLContextRef sslContext; //全域性公用的SSL的提前緩衝區 GCDAsyncSocketPreBuffer *sslPreBuffer; size_t sslWriteCachedLength; //記錄SSL讀取資料錯誤 OSStatus sslErrCode; //記錄SSL握手的錯誤 OSStatus lastSSLHandshakeError; //socket佇列的標識key -- key - queue void *IsOnSocketQueueOrTargetQueueKey; id userData; //連線備選服務端地址的延時 (另一個IPV4或IPV6) NSTimeInterval alternateAddressDelay; } 複製程式碼

建立函式

self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0,0)];
複製程式碼

這個init方法最終將會來到,在這個方法裡,socketQueue傳值為NULL,所以後面如果有sq的部分可以先行跳過,等梳理完了,再去看看這個sq具體都幹了啥。

- (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,代表socket預設建立失敗
        //本機的ipv4
		socket4FD = SOCKET_NULL;
        //ipv6
		socket6FD = SOCKET_NULL;
        //應該是UnixSocket
		socketUN = SOCKET_NULL;
        //url
		socketUrl = nil;
        //狀態
		stateIndex = 0;
        //這裡並沒有sq,可以選擇跳過
		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,@"The given socketQueue parameter must not be a concurrent queue.");
			NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,@"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
		{
            //沒有的話建立一個socketQueue,  名字為:GCDAsyncSocket,NULL = 序列
			socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String],NULL);
		}
        
        //比如原來為   0X123 -> NULL 變成  0X222->0X123->NULL
        //自己的指標等於自己原來的指標,成二級指標了  看了註釋是為了以後省略&,讓程式碼更可讀?
        //這裡不懂作者的用意,繼續往下看
		IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey;
		
        
		void *nonNullUnusedPointer = (__bridge void *)self;
        
        //dispatch_queue_set_specific給當前隊里加一個標識 dispatch_get_specific當前執行緒取出這個標識,判斷是不是在這個佇列
     	dispatch_queue_set_specific(socketQueue,IsOnSocketQueueOrTargetQueueKey,nonNullUnusedPointer,NULL);
		//讀的陣列 
		readQueue = [[NSMutableArray alloc] initWithCapacity:5];
		currentRead = nil;
		
        //寫的陣列
		writeQueue = [[NSMutableArray alloc] initWithCapacity:5];
		currentWrite = nil;
		
        //緩衝區 設定大小為 4kb
		preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];
    
#pragma mark alternateAddressDelay??
        //交替地址延時?? wtf 應該是用來給備用地址的
        alternateAddressDelay = 0.3;
	}
	return self;
}
複製程式碼

看完這段程式碼...懵逼。只是一些初始化操作。本來還以為create()會在這裡面呢,很無奈啊,哎,先不管了,繼續往下看吧。

2. GCDAsyncSocket Connect

外層呼叫

[self.socket connectToHost:@"127.0.0.1" onPort:8090 withTimeout:-1 error:&error];
複製程式碼

底層最終會來到這裡,每個方法都好長啊 - - 。這裡的inInterface傳入的是nil,所以,跟上面那個方法的sq一樣,如果有遇到可以選擇跳過。

- (BOOL)connectToHost:(NSString *)inHost
               onPort:(uint16_t)port
         viaInterface:(NSString *)inInterface
          withTimeout:(NSTimeInterval)timeout
                error:(NSError **)errPtr
{
    //LogObjc(LOG_FLAG_VERBOSE,@"%@: %@",THIS_FILE,THIS_METHOD) -- 跟蹤當前行為
	LogTrace();
    
    //拿到host,copy防止值被修改
	NSString *host = [inHost copy];
    //interface?介面?先不管 反正是nil
	NSString *interface = [inInterface copy];
	
    //宣告兩個__block的臨時變數
	__block BOOL result = NO;
    //error資訊
	__block NSError *preConnectErr = nil;
	
    //gcdBlock,都包裹在自動釋放池中 :
    // 1: 大量臨時變數 connect : 重連
    // 2: 自定義執行緒管理 : nsoperation
    // 3: 非UI 命令 工具
	dispatch_block_t block = ^{ @autoreleasepool {
		
		// Check for problems with host parameter
        // 翻譯:檢查host引數 是否存在問題
		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,大牛的程式碼真是充滿逼格 - ret
            // 裡面有註釋,有想法的可以自己去看看,大概意思就是
            // 可以讓這個return能更快的被read,後面還有很多地方被呼叫到
			return_from_block;
		}
		
		//一個前置的檢查,如果沒通過返回,這q個檢查裡,如果interface有值,則會將本機的IPV4 IPV6的 address設定上。
        // 引數 : 指標 操作同一片記憶體空間
        // 因為interface 是nil,所以不會執行return
		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...");
		
        //很可能給我們的服務端的引數是一個可變字串
        //所以我們需要copy,在Block裡同步的執行
		//這種基於Block的非同步查詢,不需要擔心它被改變
        //copy,防止改變
		NSString *hostCpy = [host copy];
		
        //拿到狀態 初始化的時候 stateIndex = 0
		int aStateIndex = stateIndex;
		__weak GCDAsyncSocket *weakSelf = self;
		
        //獲取全域性併發Queue 
		dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
        //非同步執行,這裡的autoreleasepool 跟上面的一樣,可以往上翻
		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 {
					// 方法名大概是說,address4 address6 兩個地址都成功獲取到了。
					[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;
}
複製程式碼

這個connect跟想的也不太一樣,並沒有熟悉的connect(),有毒。但是!還知道這個方法裡都幹了啥呢。

[strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
複製程式碼

一探究竟!

- (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];
	}
}
複製程式碼

咦,好像有點苗頭,看作者悄咪咪的都幹了些啥。

	if (![self connectWithAddress4:address4 address6:address6 error:&err])
複製程式碼

繼續點進去看看

- (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]);
	
    //判斷是否傾向於IPV6
	BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO;
	
	// Create and bind the sockets
    
    //如果有IPV4地址
    if (address4)
    {
        LogVerbose(@"Creating IPv4 socket");
        // 咦?這不是建立嗎,瞧瞧我發現了啥。
        socket4FD = [self createSocket:AF_INET connectInterface:connectInterface4 errPtr:errPtr];
    }
    //如果有IPV6地址,同上
    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;

    // 我去,這不是連線嗎?都悄咪咪的把建立跟連線放在這個方法裡了,糟老頭子壞得很。
    [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;
}
複製程式碼

作者是真的皮啊,把這麼重要的方法,放在一個if裡面?騷還是你騷啊。 總算是找到建立跟連線了,說什麼也要點進去看看吧。 先看建立

//建立Socket
- (int)createSocket:(int)family connectInterface:(NSData *)connectInterface errPtr:(NSError **)errPtr
{
    // 注意
    // 這個connectInterface 建立socketFD4跟6時,分別是傳入了connectInterface4與connectInterface6
    // 這兩個值,在preConnectWithInterface時,如果interface不為空,就會賦值,但是interface一直是nil,所以
    // connectInterface4與connectInterface6 都是nil
    
    
    // 建立socket,用的SOCK_STREAM TCP流
    // 總算是看到了熟悉的東西
    int socketFD = socket(family,SOCK_STREAM,0);
    //如果建立失敗 SOCKET_NULL = -1
    if (socketFD == SOCKET_NULL)
    {
        if (errPtr)
            *errPtr = [self errnoErrorWithReason:@"Error in socket() function"];
        
        return socketFD;
    }
    
    //和connectInterface繫結,由於connectInterface 是nil 所以這個方法會放回YES,//所以不會走進去
    if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr])
    {
        //繫結失敗,直接關閉返回
        [self closeSocket:socketFD];
        
        return SOCKET_NULL;
    }
    
    // Prevent SIGPIPE signals
    //防止終止程序的訊號?
    int nosigpipe = 1;
    //SO_NOSIGPIPE是為了避免網路錯誤,而導致程序退出。用這個來避免系統傳送signal
    //setsockopt()函式,用於任意型別、任意狀態套介面的設定選項值。百度百科有詳解
    setsockopt(socketFD,SOL_SOCKET,SO_NOSIGPIPE,&nosigpipe,sizeof(nosigpipe));
    
    return socketFD;
}
複製程式碼

再來就是連線socket

- (void)connectSocket:(int)socketFD address:(NSData *)address stateIndex:(int)aStateIndex
{
    //已連線,關閉連線返回
    if (self.isConnected)
    {
        [self closeSocket:socketFD];
        return;
    }
    
    // Start the connection process in a background queue
    //開始連線過程,在後臺queue中
    __weak GCDAsyncSocket *weakSelf = self;
    
    //獲取到全域性Queue
    dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
    //新執行緒
    dispatch_async(globalConcurrentQueue,^{
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
        //呼叫connect方法,該函式阻塞執行緒,所以要非同步新執行緒
        //客戶端向特定網路地址的伺服器傳送連線請求,連線成功返回0,失敗返回 -1。
        int result = connect(socketFD,(const struct sockaddr *)[address bytes],(socklen_t)[address length]);
        
        //老樣子,安全判斷
        __strong GCDAsyncSocket *strongSelf = weakSelf;
        if (strongSelf == nil) return_from_block;
        
        //在socketQueue中,開闢執行緒
        dispatch_async(strongSelf->socketQueue,^{ @autoreleasepool {
            //如果狀態為已經連線,關閉連線返回
            if (strongSelf.isConnected)
            {
                [strongSelf closeSocket:socketFD];
                // 又是這個裝逼寫法
                return_from_block;
            }
            
            //說明連線成功
            if (result == 0)
            {
                //關閉掉另一個沒用的socket
                [self closeUnusedSocket:socketFD];
                //呼叫didConnect,生成stream,改變狀態等等!
                [strongSelf didConnect:aStateIndex];
            }
            //連線失敗
            else
            {
                //關閉當前socket
                [strongSelf closeSocket:socketFD];
                
                // If there are no more sockets trying to connect,we inform the error to the delegate
                //返回連線錯誤的error
                if (strongSelf.socket4FD == SOCKET_NULL && strongSelf.socket6FD == SOCKET_NULL)
                {
                    NSError *error = [strongSelf errnoErrorWithReason:@"Error in connect() function"];
                    [strongSelf didNotConnect:aStateIndex error:error];
                }
            }
        }});
        
#pragma clang diagnostic pop
    });
    //輸出正在連線中
    LogVerbose(@"Connecting...");
}

複製程式碼

至此,我們就看到了socket的建立跟連線的實現原理,接下來說讀寫操作。 由於篇幅問題這裡另起一篇文章看這裡