GCDAsyncSocket源碼分析
1.初始化socket 源碼提供了四種初始化方法
- (instancetype)init;
- (instancetype)initWithSocketQueue:(nullable dispatch_queue_t)sq;
- (instancetype)initWithDelegate:(nullable id<GCDAsyncSocketDelegate>)aDelegate delegateQueue:(nullable dispatch_queue_t)dq;
- (instancetype)initWithDelegate:(nullable id<GCDAsyncSocketDelegate>)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq;
最終實現方法:
- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq
{
if((self = [super init]))
{
delegate = aDelegate; // socket的代理
delegateQueue = sq; // delegate的線程
// You MUST set a delegate AND delegate dispatch queue before attempting to use the socket, or you will get an error
// 初始化socket這里的delegate是必須要有的,否則會出現錯誤
// socketQueue是可選的,如果socketQueue是空的話會創建一個
//這個宏是在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)
{
//給定的socketQueue參數不能是一個并發隊列。
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;
}
核心建立連接方法
- (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;
//在socketQueue中執行這個Block
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
//否則同步的調起這個queue去執行
else
dispatch_sync(socketQueue, block);
//如果有錯誤,賦值錯誤
if (errPtr) *errPtr = preConnectErr;
//把連接是否成功的result返回
return result;
}
說明下LogTrace();
#ifndef GCDAsyncSocketLoggingEnabled
#define GCDAsyncSocketLoggingEnabled 0
#endif
#if GCDAsyncSocketLoggingEnabled
#import "DDLog.h"
#define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD)
#else
#define LogTrace() {}
#endif
這個
LogTrace
根據GCDAsyncSocketLoggingEnabled
這個宏定義來是否打開的,會跟蹤到文件名和方法名,但是GCDAsyncSocket這個庫里面沒有使用到。
現在著重說下block()里面干了什么操作:
//gcdBlock ,都包裹在自動釋放池中 :
// 什么情況下使用自動釋放池
// 1: 大量臨時變量 connect : 重連
// 2: 自定義線程管理 : nsoperation
// 3: 非UI 命令 工具
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 ---> 服務器
// client <---> server
// 這個globalConcurrentQueue主要是用來查找服務器鏈接
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;
}};
block()connect流程 :
1.
preConnectWithInterface
這個方法先檢查interface是否有值,不過開發者一般都是傳nil,如果interface有值是本機地址。
- 將
flags
的狀態變成kSocketStarted
- 開啟了一個異步全局并行隊列來連接開啟連接任務。
lookupHost:port:error:
這個方法根據host和port獲取地址的信息,是返回一個數組,數組包括(IPV4 IPV6的地址 sockaddr_in6、sockaddr_in類型)lookup: didSucceedWithAddress4: address6:
去創建連接socket。- 連接完后再開辟一條異步線程,會把
flags |= kConnected
,連接成功后會停止超時連接.- 這時會創建讀寫stream后,添加到runloop上,最后openStreams。
接著到讀/寫數據函數
- (void)readDataWithTimeout:(NSTimeInterval)timeout
buffer:(NSMutableData *)buffer
bufferOffset:(NSUInteger)offset
maxLength:(NSUInteger)length
tag:(long)tag
{
if (offset > [buffer length]) {
LogWarn(@"Cannot read: offset > [buffer length]");
return;
}
GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer
startOffset:offset
maxLength:length
timeout:timeout
readLength:0
terminator:nil
tag:tag];
dispatch_async(socketQueue, ^{ @autoreleasepool {
LogTrace();
if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
{
//往讀的隊列添加任務,任務是包的形式
[readQueue addObject:packet];
[self maybeDequeueRead];
}
}});
// Do not rely on the block being run in order to release the packet,
// as the queue might get released without the block completing.
}
//讓讀任務離隊,開始執行這條讀任務
- (void)maybeDequeueRead
{
LogTrace();
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
// If we're not currently processing a read AND we have an available read stream
//如果當前讀的包為空,而且flag為已連接
if ((currentRead == nil) && (flags & kConnected))
{
//如果讀的queue大于0 (里面裝的是我們封裝的GCDAsyncReadPacket數據包)
if ([readQueue count] > 0)
{
// Dequeue the next object in the write queue
//使得下一個對象從寫的queue中離開
//從readQueue中拿到第一個寫的數據
currentRead = [readQueue objectAtIndex:0];
//移除
[readQueue removeObjectAtIndex:0];
//我們的數據包,如果是GCDAsyncSpecialPacket這種類型,這個包里裝了TLS的一些設置
//如果是這種類型的數據,那么我們就進行TLS
if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]])
{
LogVerbose(@"Dequeued GCDAsyncSpecialPacket");
// Attempt to start TLS
//標記flag為正在讀取TLS
flags |= kStartingReadTLS;
// This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set
//只有讀寫都開啟了TLS,才會做TLS認證
[self maybeStartTLS];
}
else
{
LogVerbose(@"Dequeued GCDAsyncReadPacket");
// Setup read timer (if needed)
//設置讀的任務超時,每次延時的時候還會調用 [self doReadData];
[self setupReadTimerWithTimeout:currentRead->timeout];
// Immediately read, if possible
//讀取數據 主要對數據是否有一次性的數據讀取或者粘包的處理
[self doReadData];
}
}
//讀的隊列沒有數據,標記flag為,讀了沒有數據則斷開連接狀態
else if (flags & kDisconnectAfterReads)
{
//如果標記有寫然后斷開連接
if (flags & kDisconnectAfterWrites)
{
//如果寫的隊列為0,而且寫為空
if (([writeQueue count] == 0) && (currentWrite == nil))
{
//斷開連接
[self closeWithError:nil];
}
}
else
{
//斷開連接
[self closeWithError:nil];
}
}
//如果有安全socket。
else if (flags & kSocketSecure)
{
//
[self flushSSLBuffers];
// Edge case:
//
// We just drained all data from the ssl buffers,
// and all known data from the socket (socketFDBytesAvailable).
//
// If we didn't get any data from this process,
// then we may have reached the end of the TCP stream.
//
// Be sure callbacks are enabled so we're notified about a disconnection.
//如果可讀字節數為0
if ([preBuffer availableBytes] == 0)
{
//CFStream形式TLS
if ([self usingCFStreamForTLS]) {
// Callbacks never disabled
}
else {
//重新恢復讀的source。因為每次開始讀數據的時候,都會掛起讀的source
[self resumeReadSource];
}
}
}
}
}
//可能開啟TLS
- (void)maybeStartTLS
{
// We can't start TLS until:
// - All queued reads prior to the user calling startTLS are complete
// - All queued writes prior to the user calling startTLS are complete
//
// We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set
//只有讀和寫TLS都開啟
if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS))
{
//需要安全傳輸
BOOL useSecureTransport = YES;
#if TARGET_OS_IPHONE
{
//拿到當前讀的數據
GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
//得到設置字典
NSDictionary *tlsSettings = tlsPacket->tlsSettings;
//拿到Key為CFStreamTLS的 value
NSNumber *value = [tlsSettings objectForKey:GCDAsyncSocketUseCFStreamForTLS];
if (value && [value boolValue])
//如果是用CFStream的,則安全傳輸為NO
useSecureTransport = NO;
}
#endif
//如果使用安全通道
if (useSecureTransport)
{
//開啟TLS
[self ssl_startTLS];
}
//CFStream形式的Tls
else
{
#if TARGET_OS_IPHONE
[self cf_startTLS];
#endif
}
}
}
- 如果能進入
maybeStartTLS
這個方法后,證明GCDAsyncSpecialPacket
這個包要對TLS認證。- 這里的TLS分了兩種情況:
2.1 ssl_startTLS認證
2.2 cf_startTLS認證- 但是最終都會去到prebuff,再根據代理方法代理出去的
ssl_startTLS認證流程:
- client -- '發送一個信號' --> server
- server先弄個公開密鑰將客戶端發送過來的信號生成主密鑰所需要信息 ---> client
- client將主密鑰加密的信息 ---> server
- server用公開密鑰對主密鑰解密,然后認證
- 認證完后初始化SSL提前緩沖(4kb)
- 然后根據preBuffer可讀大小進行讀寫
- 開始SSL握手過程,調用socket已經開啟安全通道的代理方法
cf_startTLS流程:
這是CF流形式的TLS ---》createReadAndWriteStream ---》addStreamsToRunLoop ---》拿到當前的GCDAsyncSpecialPacket包 ---》再去拿ssl配置 ---》直接設置給讀寫stream ---》openStreams
Close流程:
1.緩沖區的prebuff進行reset
2.相應的事件流關閉、釋放、滯空
3.SSL上下文關閉、釋放
4.針對三種不同類型socket進行關閉釋放
5.取消相關的souce
6.代理回調關閉
GCDAsyncSocketDelegate代理方法:
//已經連接到服務器
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(nonnull NSString *)host port:(uint16_t)port{
NSLog(@"連接成功 : %@---%d",host,port);
//連接成功或者收到消息,必須開始read,否則將無法收到消息,
//不read的話,緩存區將會被關閉
// -1 表示無限時長 ,永久不失效
[self.socket readDataWithTimeout:-1 tag:10086];
}
// 連接斷開
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
NSLog(@"斷開 socket連接 原因:%@",err);
}
//已經接收服務器返回來的數據
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
NSLog(@"接收到tag = %ld : %ld 長度的數據",tag,data.length);
//連接成功或者收到消息,必須開始read,否則將無法收到消息
//不read的話,緩存區將會被關閉
// -1 表示無限時長 , tag
[self.socket readDataWithTimeout:-1 tag:10086];
}
//消息發送成功 代理函數 向服務器 發送消息
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
NSLog(@"%ld 發送數據成功",tag);
}
因為存在粘包和分包的情況,所以接收方需要對接收的數據進行一定的處理,主要解決的問題有兩個:
1.在粘包產生時,要可以在同一個包內獲取出多個包的內容。
2.在分包產生時,要保留上一個包的部分內容,與下一個包的部分內容組合。
處理方式:
#pragma mark - 發送數據格式化
- (void)sendData:(NSData *)data dataType:(unsigned int)dataType{
NSMutableData *mData = [NSMutableData data];
// 1.計算數據總長度 data
unsigned int dataLength = 4+4+(int)data.length;
// 將長度轉成data
NSData *lengthData = [NSData dataWithBytes:&dataLength length:4];
// mData 拼接長度data
[mData appendData:lengthData];
// 數據類型 data
// 2.拼接指令類型(4~7:指令)
NSData *typeData = [NSData dataWithBytes:&dataType length:4];
// mData 拼接數據類型data
[mData appendData:typeData];
// 3.最后拼接真正的數據data
[mData appendData:data];
NSLog(@"發送數據的總字節大小:%ld",mData.length);
// 發數據
[self.socket writeData:mData withTimeout:-1 tag:10086];
}
接收數據
- (void)recvData:(NSData *)data{
//直接就給他緩存起來
[self.cacheData appendData:data];
// 獲取總的數據包大小
// 整段數據長度(不包含長度跟類型)
NSData *totalSizeData = [data subdataWithRange:NSMakeRange(0, 4)];
unsigned int totalSize = 0;
[totalSizeData getBytes:&totalSize length:4];
//包含長度跟類型的數據長度
unsigned int completeSize = totalSize + 8;
//必須要大于8 才會進這個循環
while (self.cacheData.length>8) {
if (self.cacheData.length < completeSize) {
//如果緩存的長度 還不如 我們傳過來的數據長度,就讓socket繼續接收數據
[self.socket readDataWithTimeout:-1 tag:10086];
break;
}
//取出數據
NSData *resultData = [self.cacheData subdataWithRange:NSMakeRange(8, completeSize)];
//處理數據
[self handleRecvData:resultData];
//清空剛剛緩存的data
[self.cacheData replaceBytesInRange:NSMakeRange(0, completeSize) withBytes:nil length:0];
//如果緩存的數據長度還是大于8,再執行一次方法
if (self.cacheData.length > 8) {
[self recvData:nil];
}
}
}
socket的消息如何準確送達
1.當連接的情況下是根據接口來接受消息,如果沒有連接情況是走apns推送。
2.消息有序情況把數據封裝成一個包一個包發送這樣就可以保證消息有序。
websocket
websocket是解決粘包的問題,根據pingpong來保持連接,如果斷了也是會走apns推送,保證消息接受。