原文:https://github.com/robbiehanson/CocoaAsyncSocket/wiki/Intro_GCDAsyncSocket
GCDAsyncSocket 是一個 TCP 庫。它建立在 Grand Central Dispatch 之上。
本頁提供了該庫的介紹。
初始化
最常見的初始化實例的方法簡單來說就是這樣:
socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
為了讓 GCDAsyncSocket 調(diào)用你(遵守)的委托方法,需要 delegate 和 delegate_queue。上面的代碼中指定 self
為遵守委托的對象,并指示庫在主線程上調(diào)用所有的委托方法。
設(shè)置一個 delegate 可能是一個熟悉的操作。然而,提供一個 delegateQueue 可能是一個新概念。大多數(shù)典型的庫都是單線程的。當(dāng)需要調(diào)用 delegate 方法時,他們只是調(diào)用它。這些庫假設(shè)你的 delegate 代碼也是單線程的?;蛘?,庫的內(nèi)部可能是多線程的,但它們假設(shè)你的 delegate 代碼是單線程的,并且設(shè)計成只在主線程上運(yùn)行。所以它們只是總是在主線程上調(diào)用所有的委托方法。
而 GCDAsyncSocket 則是為性能而設(shè)計的。它允許你在你選擇的專用 GCD 隊列上接收委托回調(diào)。這使得它可以在高性能服務(wù)器中使用,并且可以支持成千上萬的并發(fā)連接。但在典型的應(yīng)用中它也有幫助。想讓你的 UI 更敏捷一點嗎?有沒有考慮過將網(wǎng)絡(luò)處理代碼從 UI 線程上移開?即使是今天的移動設(shè)備也有多個 CPU 內(nèi)核......也許是時候開始利用它們了。
配置
大多數(shù)時候你不需要配置。有各種配置選項(如頭文件中所述),但它們主要是針對高級用例。
注意:安全(TLS/SSL)是你以后設(shè)置的東西。這些協(xié)議實際上運(yùn)行在TCP之上(它們不是TCP本身的一部分)。
連接
最常見的連接方式是:
NSError *err = nil;
if (![socket connectToHost:@"deusty.com" onPort:80 error:&err]) // 異步的!
{
// 如果有錯誤,很可能是 "已經(jīng)連接" 或 "沒有設(shè)置委托" 之類的問題。
NSLog(@"I goofed: %@", err);
}
連接方法是異步的。這意味著什么?這意味著當(dāng)你調(diào)用 connect 方法時,它們會開啟一個后臺操作來連接到所需的主機(jī)/端口,然后立即返回。這個異步的后臺操作最終要么成功,要么失敗。無論哪種方式,相關(guān)的委托方法都會被調(diào)用。
- (void)socket:(GCDAsyncSocket *)sender didConnectToHost:(NSString *)host port:(UInt16)port
{
NSLog(@"Cool, I'm connected! That was easy.");
}
那么如果 connect 方法是異步的,為什么會返回一個布爾值和錯誤呢?只有當(dāng)一些明顯的事情阻止它開始連接操作時,這個方法才會返回NO。例如,如果 socket 已經(jīng)被連接,或者從未設(shè)置過 delegate。
實際上有幾種不同的連接方法供你使用。它們?yōu)槟闾峁┝瞬煌倪x項,例如
- 可選擇指定一個連接超時。
例如:如果5秒內(nèi)沒有連接,則失敗。 - 可選擇指定要連接的接口(interface)
例如,使用藍(lán)牙連接,或使用 WiFi 連接,無論是否有有線連接。 - 提供一個原始的 socket 地址,而不是名稱/端口對。
如:我用NSNetService
解析了一個地址,而我只想連接到這個地址。
讀和寫
該庫最大的特點之一是 "隊列讀/寫操作"。什么叫 "隊列讀寫 "呢?一個簡單的代碼例子可能是最好的解釋:
NSError *err = nil;
if (![socket connectToHost:@"deusty.com" onPort:80 error:&err]) // 異步的!
{
// 如果有錯誤,很可能是 "已經(jīng)連接" 或 "沒有設(shè)置委托" 之類的問題。
NSLog(@"I goofed: %@", err);
return;
}
// 此時 socket 未連接。
// 但你還是可以開始向它寫入東西!
// 該庫會將你所有的寫入操作排成隊列。
// 然后在 socket 連接后,它會自動開始執(zhí)行你的寫入方法!
[socket writeData:request1 withTimeout:-1 tag:1];
// 事實上,我知道我有兩個請求。
// 為什么不現(xiàn)在就把它們都解決掉呢?
[socket writeData:request2 withTimeout:-1 tag:2];
// 哼,趁著現(xiàn)在,我還不如排隊閱讀,爭取第一時間回復(fù)。
[socket readDataToLength:responseHeaderLength withTimeout:-1 tag:TAG_RESPONSE_HEADER];
你可能已經(jīng)注意到了 tag
參數(shù)。那是什么意思?嗯,這都是為了方便你。你指定的 tag
參數(shù)不會通過 socket 發(fā)送,也不會從 socket 中讀取。tag
參數(shù)只是通過各種委托方法回傳給你。它的設(shè)計是為了幫助您簡化委托方法中的代碼。
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
if (tag == 1)
NSLog(@"發(fā)出第一個請求");
else if (tag == 2)
NSLog(@"發(fā)出第二個請求");
}
Tag 對讀取操作的幫助最大:
#define TAG_WELCOME 10
#define TAG_CAPABILITIES 11
#define TAG_MSG 12
- (void)socket:(GCDAsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag
{
if (tag == TAG_WELCOME)
{
// 忽略歡迎消息
}
else if (tag == TAG_CAPABILITIES)
{
[self processCapabilities:data];
}
else if (tag == TAG_MSG)
{
[self processMessage:data];
}
}
你看,TCP 協(xié)議是以無限長的單一連續(xù)流的概念為模型的。理解這一點至關(guān)重要--事實上,這也是我們所看到的造成混亂的首要原因。
想象一下,你正試圖通過套接字發(fā)送一些消息。所以你做了這樣的事情(在偽代碼中):
socket.write("Hi Sandy.");
socket.write("Are you busy tonight?");
數(shù)據(jù)如何在另一端顯示出來?如果你認(rèn)為另一端會在兩個獨(dú)立的讀取操作中收到兩個獨(dú)立的句子,那么你剛剛中了一個常見的陷阱! 驚呼! 但不要害怕! 你的狀況并沒有波及生命危險,只是普通的感冒而已。通過閱讀 "常見陷阱 "頁面,可以找到治療方法。
既然說到這里,你可能想知道哪些讀取方法。下面就給大家介紹幾種。
- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag;
- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;
第一個方法,readDataToLength
,讀取并返回給定長度的數(shù)據(jù)。讓我們來看一個例子。
你正在編寫一個協(xié)議的客戶端側(cè),服務(wù)器發(fā)送帶有固定長度頭的響應(yīng)。所有響應(yīng)的頭正好是 8 個字節(jié)。前 4 個字節(jié)包含各種標(biāo)志等。而后 4 個字節(jié)包含響應(yīng)數(shù)據(jù)的長度,是可變的。所以你的代碼可能是這樣的。
- (void)socket:(GCDAsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag
{
if (tag == TAG_FIXED_LENGTH_HEADER)
{
int bodyLength = [self parseHeader:data];
[socket readDataToLength:bodyLength withTimeout:-1 tag:TAG_RESPONSE_BODY];
}
else if (tag == TAG_RESPONSE_BODY)
{
// Process the response
[self handleResponseBody:data];
// Start reading the next response
[socket readDataToLength:headerLength withTimeout:-1 tag:TAG_FIXED_LENGTH_HEADER];
}
}
讓我們看看另一個例子。畢竟,不是所有的協(xié)議都使用固定長度的頭。HTTP 就是這樣一個協(xié)議。
一個典型的 HTTP 響應(yīng)看起來像這樣。
HTTP/1.1 200 OK
Date: Thu, 24 Nov 2011 02:18:50 GMT
Server: Apache/2.2.3 (CentOS)
X-Powered-By: PHP/5.1.6
Content-Length: 5233
Content-Type: text/html; charset=UTF-8
這只是一個例子。可以有任何數(shù)量的頭字段。換句話說,HTTP 頭的長度是可變的。我們?nèi)绾巫x取它?
好吧,HTTP 協(xié)議解釋了如何實現(xiàn)的方式。頭部的每一行都以 CRLF(回車,換行:"\r\n")結(jié)束。此外,頭的結(jié)尾用2個背對背的CRLF標(biāo)記。而正文的長度則是通過 "Content-Length "頭域來指定的。所以我們可以這樣做。
- (void)socket:(GCDAsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag
{
if (tag == HTTP_HEADER)
{
int bodyLength = [self parseHttpHeader:data];
[socket readDataToLength:bodyLength withTimeout:-1 tag:HTTP_BODY];
}
else if (tag == HTTP_BODY)
{
// Process response
[self processHttpBody:data];
// 讀取下一個響應(yīng)頭
NSData *term = [@"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding];
[socket readDataToData:term withTimeout:-1 tag:HTTP_HEADER];
}
}
我已經(jīng)列出了 2 種可用的讀取方法。有近 10 種不同的讀取方法。它們提供了更多的高級選項,如指定最大長度,或提供你自己的讀取緩沖區(qū)。
編寫服務(wù)器
GCDAsyncSocket
還允許你創(chuàng)建一個服務(wù)器,并接受傳入的連接。它看起來像這樣:
listenSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
NSError *error = nil;
if (![listenSocket acceptOnPort:port error:&error])
{
NSLog(@"I goofed: %@", error);
}
- (void)socket:(GCDAsyncSocket *)sender didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
// The "sender" parameter is the listenSocket we created.
// The "newSocket" is a new instance of GCDAsyncSocket.
// It represents the accepted incoming client connection.
// Do server stuff with newSocket...
}
就這么簡單! 更具體的例子,請查看存儲庫中的 "EchoServer" 示例項目。