GCDAsyncSocket 實(shí)現(xiàn) iOS 服務(wù)器

App開(kāi)發(fā)中當(dāng)遇到行情價(jià)格、聊天場(chǎng)景中, 為了讓消息及時(shí)到達(dá), App 端往往采用 Socket 的方式和服務(wù)器實(shí)現(xiàn)長(zhǎng)連接. 服務(wù)器最常見(jiàn)的就是部署在云端, 從根本上來(lái)說(shuō)就是一臺(tái)主機(jī)或者虛擬機(jī). 從互聯(lián)網(wǎng)的組成結(jié)構(gòu)來(lái)看, 任何一個(gè)節(jié)點(diǎn)上的計(jì)算機(jī), 都可以作為服務(wù)器來(lái)使用. iPhone 智能手機(jī)作為連接在網(wǎng)絡(luò)上的硬件, 從理論上來(lái)說(shuō)是可以作為服務(wù)器使用的.

對(duì)于上面所述, 以前只是感性上的調(diào)侃, 這兩天查找資料, 竟然發(fā)現(xiàn)了早就有大神實(shí)現(xiàn)了該功能: CocoaAsyncSocket , 趕快下載下來(lái)體驗(yàn)一番.

1 Socket

Socket 翻譯成中文稱為"套接字", 有人將 Socket 等價(jià)于 TCP/IP, 個(gè)人認(rèn)為這樣不是太準(zhǔn)確, 因?yàn)槭褂?Socket 還可以實(shí)現(xiàn) UDP 協(xié)議的發(fā)送. 可以把 Socket 看做比 TCP/IP 更高級(jí)的接口層, 可以用下圖形象的理解:

TCP/IP

使用Socket進(jìn)行客戶端和服務(wù)端的鏈接過(guò)程如下:

Socket
2 Server

使用 GCDAsyncSocket 創(chuàng)建服務(wù)端, 監(jiān)聽(tīng)端口 5858:

- (GCDAsyncSocket *)serverSocket {
    if (!_serverSocket) {
        _serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
    }
    return _serverSocket;
}

NSError *error;
BOOL result = [self.serverSocket acceptOnPort:port error:&error];

delegateQueue 要求是順序隊(duì)列, 保證Socket中傳輸?shù)臄?shù)據(jù)按順序讀取或者存儲(chǔ).
Socket 鏈接過(guò)程圖中, 能夠看出分為鏈接成功/接收數(shù)據(jù)/斷開(kāi)鏈接 3 個(gè)過(guò)程, GCDAsyncSocket 通過(guò)代理方法來(lái)實(shí)現(xiàn):

#pragma mark - GCDAsyncSocketDelegate
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket {
     NSLog(@"收到鏈接:%@: %d", newSocket.localAddress, newSocket.localPort);   
    [newSocket readDataWithTimeout:-1 tag:0];
}

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    NSString *clientStr = [[NSString alloc] initWithData:data encoding:(NSUTF8StringEncoding)];
    NSLog(@"收到內(nèi)容: %@ 長(zhǎng)度:%zd", clientStr, clientStr.length);
        
    [sock readDataWithTimeout:-1 tag:0];
}

- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err {
    NSLog(@"斷開(kāi)鏈接: %@; error: %@", sock.connectedHost, err);   
}

readDataWithTimeout 中設(shè)置為-1, 表示超時(shí)時(shí)間無(wú)限大, 保證數(shù)據(jù)寫入到Socket中的dataBuffer. tag值為數(shù)據(jù)包的標(biāo)識(shí), 過(guò)程中可以通過(guò)該標(biāo)識(shí)識(shí)別數(shù)據(jù)包, 比如傳輸失敗的代理方法/服務(wù)端接收到數(shù)據(jù)的代理方法中.

3 Client

客戶端通過(guò)GCDAsyncSocket鏈接到主機(jī)端口:

- (GCDAsyncSocket *)clientSocket {
    if (!_clientSocket) {
        _clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:self.sockeQueue];
    return _clientSocket;
}
NSError *error = nil;
[self.clientSocket connectToHost:host onPort:port error:&error];
if (error) {
    return NO;
}else {
    return YES;
}

在鏈接成功后使用 socket 向服務(wù)端口發(fā)送數(shù)據(jù)包:

- (void)socket:(GCDAsyncSocket *)socket didConnectToHost:(NSString *)host port:(uint16_t)port {
   NSData *contentData = [@"client: 123" dataUsingEncoding:(NSUTF8StringEncoding)];
   [socket writeData:data withTimeout:-1 tag:0];
}
4 粘包

手動(dòng)點(diǎn)擊button發(fā)送小數(shù)據(jù), 不會(huì)出現(xiàn)數(shù)據(jù)錯(cuò)亂的問(wèn)題, 當(dāng)大量數(shù)據(jù)通過(guò)Socket同時(shí)發(fā)送時(shí), 在服務(wù)端會(huì)出現(xiàn)數(shù)據(jù)鏈接錯(cuò)亂的情況. 鏈接成功后, 向服務(wù)端發(fā)送 100 個(gè)數(shù)據(jù):

- (void)sendData:(GCDAsyncSocket *)socket {
    for (int i = 0; i < 100; i++) {
        NSString *str = [NSString stringWithFormat:@"%d", i];
        NSData *contentData = [str dataUsingEncoding:(NSUTF8StringEncoding)];
        [socket writeData:data withTimeout:-1 tag:0];
    }
}

在服務(wù)端接收的數(shù)據(jù)如下:

SocketServer[70565:1560844] 收到內(nèi)容: 0 長(zhǎng)度:1
SocketServer[70565:1560844] 收到內(nèi)容: 1234 長(zhǎng)度:4
SocketServer[70565:1560844] 收到內(nèi)容: 5678910111213141516171819202122 長(zhǎng)度:31
SocketServer[70565:1560844] 收到內(nèi)容: 2324252627282930313233343536 長(zhǎng)度:28
SocketServer[70565:1560844] 收到內(nèi)容: 373839404142434445464748495051 長(zhǎng)度:30
SocketServer[70565:1560844] 收到內(nèi)容: 5253545556575859606162636465666768697071727374 長(zhǎng)
SocketServer[70565:1560844] 收到內(nèi)容: 7576777879808182838485868788899091929394959697 長(zhǎng)
SocketServer[70565:1560844] 收到內(nèi)容: 9899 長(zhǎng)度:4

能夠看出接收的數(shù)據(jù), 保持了客戶端發(fā)送123...的順序, 但是并沒(méi)有按照 Socket 發(fā)送的 1, 2, 3...相互分割的方式去讀取, 這就是Socket粘包現(xiàn)象. 粘包導(dǎo)致服務(wù)端接收數(shù)據(jù)后, 無(wú)法分辨出客戶端發(fā)送數(shù)據(jù)的起始和終止位置, 會(huì)導(dǎo)致解析出錯(cuò)誤數(shù)據(jù).

粘包的原因
TCP是面向連接的,面向流的,提供高可靠性服務(wù)。收發(fā)兩端(客戶端和服務(wù)器端)都要有一一成對(duì)的socket,因此,發(fā)送端為了將多個(gè)發(fā)往接收端的包,更有效的發(fā)到對(duì)方,使用了優(yōu)化方法(Nagle算法),將多次間隔較小且數(shù)據(jù)量小的數(shù)據(jù),合并成一個(gè)大的數(shù)據(jù)塊,然后進(jìn)行封包。

粘包的解決方式:

  1. 禁用Nagle算法;
  2. 當(dāng)填入數(shù)據(jù)后調(diào)用push操作指令強(qiáng)制數(shù)據(jù)立即傳送,而不必等待發(fā)送緩沖區(qū)填充;
  3. 數(shù)據(jù)包中加頭,頭部信息為整個(gè)數(shù)據(jù)的長(zhǎng)度(最廣泛最常用);

發(fā)送數(shù)據(jù)

- (void)sendData:(NSData *)contentData {
    NSInteger dataLength = contentData.length;
    NSData *lengthData = [NSData dataWithBytes:&dataLength length:sizeof(dataLength)];
    NSData *headData = [lengthData subdataWithRange:NSMakeRange(0, 4)];
    NSMutableData *data = [NSMutableData dataWithData:headData];
    [data appendData:contentData];
    
    [self.clientSocket writeData:data withTimeout:-1 tag:0];
}

接收數(shù)據(jù)

- (NSMutableData *)dataBuffer {
    if (!_dataBuffer) {
        _dataBuffer = [NSMutableData data];
    }
    return _dataBuffer;
}

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    [self.dataBuffer appendData:data];
    while (self.dataBuffer.length >= 4) {
        NSInteger dataLength = 0;
        //獲取數(shù)據(jù)長(zhǎng)度
        [[self.dataBuffer subdataWithRange:(NSMakeRange(0, 4))] getBytes:&dataLength length:sizeof(dataLength)];
        if (self.dataBuffer.length >= (dataLength+4)) {
            NSData *realData = [self.dataBuffer subdataWithRange:NSMakeRange(4, dataLength)];
            NSString *clientStr = [[NSString alloc] initWithData:realData encoding:(NSUTF8StringEncoding)];
    NSLog(@"收到內(nèi)容: %@ 長(zhǎng)度:%zd", clientStr, clientStr.length);
            self.dataBuffer = [[self.dataBuffer subdataWithRange:NSMakeRange(4+dataLength, self.dataBuffer.length-4-dataLength)] mutableCopy];            
        } else {
            break;
        }
    }
    [sock readDataWithTimeout:-1 tag:0];
}

注:
datagetBytes: length:方法, 將datalength部分拷貝到getBytes的容器中, 當(dāng)length大于data長(zhǎng)度時(shí), 拷貝data的全部. 初始化NSInteger dataLength = 0, 將data頭部的4個(gè)長(zhǎng)度數(shù)據(jù)的字節(jié), 存儲(chǔ)到長(zhǎng)度為8dataLength 空間.

5 Demo

實(shí)現(xiàn)了ClientServer的工程: Demo

喜歡和關(guān)注都是對(duì)我的鼓勵(lì)和支持~

參考:
1 CocoaAsyncSocket使用
2 CocoaAsyncSocket介紹與使用
3 CocoaAsyncSocket 讀/寫操作以及粘包處理

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容