網絡(五):socket

目錄
一艘绍、socket是什么,socket和HTTP的區(qū)別
二秫筏、如何建立一個socket連接
三鞍盗、使用CocoaAsyncSocket實現(xiàn)socket編程
?1、基本實現(xiàn)
?2跳昼、數(shù)據(jù)粘包
?3般甲、心跳保活
?4鹅颊、斷線重連


一敷存、socket是什么,socket和HTTP的區(qū)別


  • HTTP是應用層的一個協(xié)議堪伍,而socket不是協(xié)議锚烦、它只是操作系統(tǒng)提供給我們程序員用來操作傳輸層TCP協(xié)議和UDP協(xié)議的一套接口,使用socket其實就是在面向傳輸層的TCP協(xié)議和UDP協(xié)議編程帝雇,說白了使用socket編程我們就是在直接發(fā)送一個TCP請求或UDP請求涮俄;(如果直接使用TCP協(xié)議和UDP協(xié)議編程的話,我們程序員就需要按照這兩個協(xié)議的標準去嚴格組織數(shù)據(jù)格式尸闸,并且還有很多其它的事情要做彻亲,這會非常復雜孕锄,而socket這套接口就方便了我們程序員開發(fā),我們只需要調用簡單的接口即可苞尝,它內部幫我們做了組織數(shù)據(jù)格式等復雜的事情畸肆。這就好比我們發(fā)送一個HTTP請求,如果我們直接使用HTTP協(xié)議編程宙址,那它的數(shù)據(jù)格式也是很復雜的轴脐,包括請求行、請求頭抡砂、請求體等大咱,但是好在我們各個系統(tǒng)都有系統(tǒng)級別的網絡框架,我們只需要使用它們的接口注益,它們內部會做好數(shù)據(jù)格式轉化等復雜的事情碴巾,這樣看起來socket跟網絡框架倒更像是同一級別的概念)
  • HTTP協(xié)議在傳輸層對應的是TCP協(xié)議,所以我們常說的HTTP連接其實就是一個TCP連接聊浅,HTTP連接可以是短連接也可以是長連接餐抢,HTTP/1.0采用的是短連接现使,HTTP/1.1采用的是長連接低匙;而socket是TCP協(xié)議和UDP協(xié)議的一套接口,所以我們常說socket連接其實也是一個TCP連接碳锈,同樣socket連接可以是短連接也可以是長連接顽冶,這取決于我們怎么使用,當然socket基于UDP時甚至都不存在連接售碳,所以不要把socket連接和長連接劃等號强重;(順便說一下長連接和短連接,長連接和短連接是應用層或者傳輸層應用的一個概念贸人,而不是傳輸層的概念间景,傳輸層只有連接這個概念,它本身沒有長短之分艺智,只是因為應用層或者傳輸層應用才導致了這個連接有長短之分倘要。比如HTTP/1.0采用的是短連接,也就是說TCP三次握手之后十拣,建立了一個連接封拧,客戶端發(fā)送一個請求,服務器給一個響應夭问,此時TCP就要四次揮手了泽西,連接就斷開了,想再傳數(shù)據(jù)的話就得再三次握手建立連接四次揮手斷開連接缰趋,也就是說短連接一次只處理一個輸入流和輸出流捧杉;而HTTP/1.1采用的是長連接陕见,TCP三次握手之后,建立了一個連接糠溜,客戶端發(fā)送一個請求淳玩,服務器給一個響應,TCP并不揮手非竿,連接也沒斷開蜕着,雙方還可以繼續(xù)傳遞下一次數(shù)據(jù)、下一次數(shù)據(jù)數(shù)據(jù)......红柱,直到雙方等了某個時間段發(fā)現(xiàn)雙方都不需要傳遞數(shù)據(jù)了承匣,才會四次揮手斷開連接,也就是說長連接一次可以處理若干個輸入流和輸出流锤悄。又比如socket可以建立一個長連接韧骗,這就意味著TCP的那個連接要處理若干個輸入流和輸出流,socket也可以建立短連接零聚,這就意味著TCP的那個連接只處理一個輸入流和輸出流袍暴。長連接和短連接不是靠時間長短來界定的,而是靠一次處理的輸入流和輸出流個數(shù)來界定的)
  • HTTP主要用來做客戶端發(fā)一個請求隶症、服務端就給一個響應這樣的場景政模,而socket主要用來做客戶端和服務端都能主動給對方發(fā)數(shù)據(jù)的場景,通常情況下蚂会,單臺服務器可以支持十萬個socket連接的并發(fā)淋样。


二、如何建立一個socket連接


服務端socket要做五件事胁住,客戶端socket要做三件事:

  • 服務端socket初始化趁猴、客戶端socket初始化
  • 服務端socket調用bind()函數(shù)蜗字,綁定IP地址和端口
  • 服務端socket調用listen()函數(shù)牺丙,設置監(jiān)聽隊列有多長(比如說我們設置監(jiān)聽隊列的長度為10匪凡,這就意味著最多可以有10個客戶端可以嘗試連接服務器齐苛,而第11個客戶端會被告知服務器太忙了)
  • 服務端socket調用accept()函數(shù)鉴嗤,等待來自客戶端socket的連接請求
  • 客戶端socket調用connect()函數(shù)毁菱,向指定IP地址和端口的服務器發(fā)起連接請求
  • 服務端socket的accept()函數(shù)返回用于傳輸?shù)膕ocket的文件描述符周拐,連接建立成功

接下來雙方就可以通過read()write()函數(shù)通信了涡戳,雙方也都可以通過close()函數(shù)主動斷開連接浪规。


三或听、使用CocoaAsyncSocket實現(xiàn)socket編程


1、基本實現(xiàn)

  • 服務端socket
#import "ViewController.h"
#import "GCDAsyncSocket.h"

#define kServerIPAddress @"192.168.148.63"
#define kServerPort 8080

@interface ViewController () <GCDAsyncSocketDelegate>

/// 服務端socket
@property (strong, nonatomic) GCDAsyncSocket *serverSocket;

/// 所有的客戶端socket
@property (strong, nonatomic) NSMutableArray *clientSockets;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}


#pragma mark - GCDAsyncSocketDelegate

/// 連接客戶端成功的回調
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(nonnull GCDAsyncSocket *)newSocket {
    NSLog(@"===========>連接客戶端成功");

    // 保存客戶端socket
    [self.clientSockets addObject:newSocket];
    
    // 連接成功后笋婿,立馬開始讀取客戶端的數(shù)據(jù)誉裆,調用這個讀取方法后,當客戶端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取客戶端數(shù)據(jù)成功的回調”
    [newSocket readDataWithTimeout:-1 tag:0];
}

/// 讀取客戶端數(shù)據(jù)成功的回調
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    // 此處我們不對數(shù)據(jù)做過多的處理缸濒,只是把它轉換成JSON字符串打印出來看看
    NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"%@", [NSString stringWithFormat:@"===========>讀取客戶端數(shù)據(jù)成功:%@", jsonString]);
    
    // 注意:讀取客戶端數(shù)據(jù)成功后足丢,在這里需要再調用一次讀取客戶端數(shù)據(jù)的方法粱腻,框架本身就是這么設計的,否則我們就只能接收一次數(shù)據(jù)斩跌,之后再也接收不到數(shù)據(jù)
    [sock readDataWithTimeout:-1 tag:0];
}

/// 與客戶端斷開連接的回調
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
    NSLog(@"%@", [NSString stringWithFormat:@"===========>與客戶端斷開連接:%@", err]);
    
    if (err == nil) { // 代表是服務端主動斷開連接绍些,我們不做處理,客戶端那邊收到回調后會走斷線重連的邏輯
        NSLog(@"===========>服務端主動斷開連接");
    } else { // 代表是客戶端斷開連接耀鸦,移除斷開的客戶端
        [self.clientSockets removeObject:sock];
    }
}


#pragma mark - action

/// 服務端開始監(jiān)聽來自客戶端的連接請求柬批,收到請求后就建立連接,連接成功后會觸發(fā)“連接客戶端成功的回調”
- (IBAction)listen:(id)sender {
    NSError *error = nil;
    BOOL success = [self.serverSocket acceptOnPort:kServerPort error:&error];
    
    if (error == nil && success) {
        NSLog(@"===========>服務端開始監(jiān)聽成功");
    } else {
        NSLog(@"%@", [NSString stringWithFormat:@"==========>服務端開始監(jiān)聽失斝涠:%@", error]);
    }
}

/// 向客戶端發(fā)送數(shù)據(jù)(以JSON字符串的格式傳輸)
- (IBAction)send:(id)sender {
    if (self.clientSockets == nil) {
        return;
    }

    // 數(shù)據(jù)1
    NSDictionary *dictionary1 = @{
        @"data": @"江南憶氮帐,最憶是杭州。山寺月中尋桂子洛姑,郡亭枕上看潮頭上沐。何日更重游?",
    };
    NSData *jsonData1 = [NSJSONSerialization dataWithJSONObject:dictionary1 options:NSJSONWritingPrettyPrinted error:nil];
    NSString *jsonString1 = [[NSString alloc] initWithData:jsonData1 encoding:NSUTF8StringEncoding];
    NSData *data1 = [[jsonString1 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
    
    // 數(shù)據(jù)2
    NSDictionary *dictionary2 = @{
        @"data": @"江南憶楞艾,其次憶吳宮参咙。吳酒一杯春竹葉,吳娃雙舞醉芙蓉硫眯。早晚復相逢蕴侧?",
    };
    NSData *jsonData2 = [NSJSONSerialization dataWithJSONObject:dictionary2 options:NSJSONWritingPrettyPrinted error:nil];
    NSString *jsonString2 = [[NSString alloc] initWithData:jsonData2 encoding:NSUTF8StringEncoding];
    NSData *data2 = [[jsonString2 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
    
    [self.clientSockets enumerateObjectsUsingBlock:^(id  _Nonnull clientSocket, NSUInteger idx, BOOL * _Nonnull stop) {
        // 給客戶端發(fā)送數(shù)據(jù)1(tag:消息標記)
        [clientSocket writeData:data1 withTimeout:-1 tag:0];
        // 給客戶端發(fā)送數(shù)據(jù)2(tag:消息標記)
        [clientSocket writeData:data2 withTimeout:-1 tag:0];
    }];
}

/// 斷開與客戶端的連接
- (IBAction)disconnect:(id)sender {
    [self.serverSocket disconnect];
    self.serverSocket.delegate = nil;
    self.serverSocket = nil;
}


#pragma mark - setter, getter

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

- (NSMutableArray *)clientSockets {
    if (_clientSockets == nil) {
        _clientSockets = [NSMutableArray array];
    }
    return _clientSockets;
}

@end
  • 客戶端socket
#import "ViewController.h"
#import "GCDAsyncSocket.h"

#define kServerIPAddress @"192.168.148.63"
#define kServerPort 8080

@interface ViewController () <GCDAsyncSocketDelegate>

/// 客戶端socket
@property (strong, nonatomic) GCDAsyncSocket *clientSocket;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}


#pragma mark - GCDAsyncSocketDelegate

/// 連接服務端成功的回調
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    NSLog(@"===========>連接服務端成功");
    
    // 連接成功后,立馬開始讀取服務端的數(shù)據(jù)舟铜,調用這個讀取方法后戈盈,當服務端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取服務端數(shù)據(jù)成功的回調”
    [sock readDataWithTimeout:-1 tag:0];
}

/// 讀取服務端數(shù)據(jù)成功的回調
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    // 此處我們不對數(shù)據(jù)做過多的處理奠衔,只是把它轉換成JSON字符串打印出來看看
    NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"%@", [NSString stringWithFormat:@"===========>讀取服務端數(shù)據(jù)成功:%@", jsonString]);
    
    // 注意:讀取服務端數(shù)據(jù)成功后谆刨,在這里需要再調用一次讀取服務端數(shù)據(jù)的方法,框架本身就是這么設計的归斤,否則我們就只能接收一次數(shù)據(jù)痊夭,之后再也接收不到數(shù)據(jù)
    [sock readDataWithTimeout:-1 tag:0];
}

/// 與服務端斷開連接的回調
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
    NSLog(@"%@", [NSString stringWithFormat:@"===========>與服務端斷開連接:%@", err]);
    
    if (err == nil) { // 代表是客戶端主動斷開連接,我們不做處理脏里,服務端那邊收到回調后她我,會移除跟指定客戶端的連接
        NSLog(@"===========>客戶端主動斷開連接");
    } else { // 其它情況下斷開連接,暫時不做處理
        
    }
}


#pragma mark - action

/// 向服務端發(fā)起連接請求
- (IBAction)connect:(id)sender {
    NSError *error = nil;
    BOOL success = [self.clientSocket connectToHost:kServerIPAddress onPort:kServerPort viaInterface:nil withTimeout:-1 error:&error];
    
    if (error == nil && success) {
        // 連接服務端成功后會觸發(fā)“連接服務端成功的回調”
    } else {
        NSLog(@"%@", [NSString stringWithFormat:@"===========>連接服務端失斊群帷:%@", error]);
    }
}

/// 向服務端發(fā)送數(shù)據(jù)(以JSON字符串的格式傳輸)
- (IBAction)send:(id)sender {
    if (self.clientSocket == nil) {
        return;
    }

    // 數(shù)據(jù)1
    NSDictionary *dictionary1 = @{
        @"data": @"水光瀲滟晴方好番舆,山色空蒙雨亦奇。 ",
    };
    NSData *jsonData1 = [NSJSONSerialization dataWithJSONObject:dictionary1 options:NSJSONWritingPrettyPrinted error:nil];
    NSString *jsonString1 = [[NSString alloc] initWithData:jsonData1 encoding:NSUTF8StringEncoding];
    NSData *data1 = [[jsonString1 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];

    // 數(shù)據(jù)2
    NSDictionary *dictionary2 = @{
        @"data": @"欲把西湖比西子矾踱,淡妝濃抹總相宜恨狈。",
    };
    NSData *jsonData2 = [NSJSONSerialization dataWithJSONObject:dictionary2 options:NSJSONWritingPrettyPrinted error:nil];
    NSString *jsonString2 = [[NSString alloc] initWithData:jsonData2 encoding:NSUTF8StringEncoding];
    NSData *data2 = [[jsonString2 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];

    // 給服務端發(fā)送數(shù)據(jù)1(tag:消息標記)
    [self.clientSocket writeData:data1 withTimeout:-1 tag:0];
    // 給服務端發(fā)送數(shù)據(jù)2(tag:消息標記)
    [self.clientSocket writeData:data2 withTimeout:-1 tag:0];
}

/// 斷開與服務端的連接
- (IBAction)disconnect:(id)sender {
    [self.clientSocket disconnect];
    self.clientSocket.delegate = nil;
    self.clientSocket = nil;
}


#pragma mark - setter, getter

- (GCDAsyncSocket *)clientSocket {
    if (_clientSocket == nil) {
        _clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
    }
    return _clientSocket;
}

@end

以上我們就基本實現(xiàn)了socket編程,客戶端和服務端之間就可以相互通信了呛讲。但是在實際開發(fā)中禾怠,我們還需要考慮三個問題:數(shù)據(jù)粘包返奉、心跳保活和斷線重連吗氏。

2芽偏、數(shù)據(jù)粘包

2.1 數(shù)據(jù)粘包是什么

上面的例子中,我們預期的效果是客戶端點擊一次發(fā)送弦讽,給服務端發(fā)送兩條數(shù)據(jù)污尉,服務端觸發(fā)兩次“收到客戶端數(shù)據(jù)的回調”,然后分別打油:

===========>讀取客戶端數(shù)據(jù)成功:{
  "data" : "水光瀲滟晴方好十厢,山色空蒙雨亦奇。 "
}
===========>讀取客戶端數(shù)據(jù)成功:{
  "data" : "欲把西湖比西子捂齐,淡妝濃抹總相宜蛮放。"
}

但實際上兩條數(shù)據(jù)被合并成一條數(shù)據(jù)發(fā)送給服務端了,服務端只觸發(fā)了一次“收到客戶端數(shù)據(jù)的回調”奠宜,也只打印了一次:

===========>讀取客戶端數(shù)據(jù)成功:{
  "data" : "水光瀲滟晴方好包颁,山色空蒙雨亦奇。 "
}{
  "data" : "欲把西湖比西子压真,淡妝濃抹總相宜娩嚼。"
}

這就是數(shù)據(jù)粘包——多條數(shù)據(jù)被合并成了一條數(shù)據(jù)傳輸。

2.2 為什么會出現(xiàn)數(shù)據(jù)粘包

我們知道TCP有個發(fā)送緩存滴肿,有些情況下TCP并不是有一條數(shù)據(jù)就發(fā)一條數(shù)據(jù)岳悟,而是等發(fā)送緩存滿了,再把發(fā)送緩存里的多條數(shù)據(jù)一起發(fā)送出去泼差,這就會導致數(shù)據(jù)粘包贵少。

此外TCP還采用了Nagle優(yōu)化算法來打包數(shù)據(jù),它會將多次間隔較小且數(shù)據(jù)量較小的數(shù)據(jù)自動合并成一個比較大的數(shù)據(jù)一塊兒傳輸堆缘,這也會導致數(shù)據(jù)粘包滔灶。

2.3 怎么處理數(shù)據(jù)粘包

處理數(shù)據(jù)粘包也很簡單,核心思路就是:發(fā)送方在發(fā)送數(shù)據(jù)的時候先給每條數(shù)據(jù)都添加一個包頭吼肥,包頭里存放的關鍵信息就是真實數(shù)據(jù)的長度录平,當然也可以存放更多的業(yè)務信息,此外包頭的尾部還需要拼接一個包頭結束標識——回車換行符缀皱,以便將來接收方讀取數(shù)據(jù)時可以根據(jù)這個包頭結束標識優(yōu)先讀取到包頭數(shù)據(jù)斗这。接收方調用指定的讀取方法優(yōu)先讀取到包頭數(shù)據(jù),然后根據(jù)包頭里的長度信息再去精準讀取指定長度的真實數(shù)據(jù)啤斗,這樣就可以讀取到一條完整的數(shù)據(jù)了表箭,然后再讀取下一條數(shù)據(jù)就不會粘包了。

  • 服務端socket(相對于上面的例子争占,變化的代碼)
// 讀取處理

/// 包頭
@property (strong, nonatomic) NSDictionary *headerDictionary;

#pragma mark - GCDAsyncSocketDelegate

/// 連接客戶端成功的回調
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(nonnull GCDAsyncSocket *)newSocket {
    NSLog(@"===========>連接客戶端成功");

    // 保存客戶端socket
    [self.clientSockets addObject:newSocket];
    
    // 連接成功后燃逻,立馬開始讀取客戶端的數(shù)據(jù)序目,調用這個讀取方法后,當客戶端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取客戶端數(shù)據(jù)成功的回調”
//    [newSocket readDataWithTimeout:-1 tag:0];
    // 連接成功后伯襟,立馬開始讀取客戶端的數(shù)據(jù)猿涨,調用這個讀取方法后,當客戶端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取客戶端數(shù)據(jù)成功的回調”姆怪,但跟上面方法不同的是這個方法只會從數(shù)據(jù)的開頭起讀取到包頭結束標識為止叛赚,也就是說“讀取客戶端數(shù)據(jù)成功的回調”的data只會讀取到包頭數(shù)據(jù)
    [newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}

/// 讀取客戶端數(shù)據(jù)成功的回調
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    if (self.headerDictionary == nil) { // 如果包頭為空,就代表之前沒讀取到過包頭稽揭,那本次回調的觸發(fā)肯定就是讀取到包頭了俺附,因為上面采用的是readDataToData讀取方法讀取到包頭結束標識為止
        self.headerDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
        if (self.headerDictionary == nil) {
            NSLog(@"===========>數(shù)據(jù)格式出錯了:包頭數(shù)據(jù)為空");
            return;
        }
        
        // 獲取包頭的內容1——真實數(shù)據(jù)的長度
        NSInteger size = [self.headerDictionary[@"size"] integerValue];
        // 此時再調用這個讀取方法去讀取指定長度的真實數(shù)據(jù),讀取到真實數(shù)據(jù)后還是會觸發(fā)當前這個回調
        [sock readDataToLength:size withTimeout:-1 tag:0];

        return;
    }
    
    // 如果走到這里溪掀,代表是讀取到了真實數(shù)據(jù)
    NSInteger size = [self.headerDictionary[@"size"] integerValue];
    // 說明數(shù)據(jù)有問題
    if (size <= 0 || data.length != size) {
        NSLog(@"===========>數(shù)據(jù)格式出錯了:真實數(shù)據(jù)大小不正確");
        return;
    }
    // 此處我們不對數(shù)據(jù)做過多的處理事镣,只是把它轉換成JSON字符串打印出來看看
    NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"%@", [NSString stringWithFormat:@"===========>讀取客戶端數(shù)據(jù)成功:%@", jsonString]);

    // 置位包頭
    self.headerDictionary = nil;
    // 注意:讀取客戶端數(shù)據(jù)成功后,在這里需要再調用一次讀取客戶端數(shù)據(jù)的方法揪胃,框架本身就是這么設計的璃哟,否則我們就只能接收一次數(shù)據(jù),之后再也接收不到數(shù)據(jù)
//    [sock readDataWithTimeout:-1 tag:0];
    // 繼續(xù)讀取下一條數(shù)據(jù)
    [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
// 發(fā)送處理

#pragma mark - action

/// 向客戶端發(fā)送數(shù)據(jù)(以JSON字符串的格式傳輸)
- (IBAction)send:(id)sender {
    if (self.clientSockets == nil) {
        return;
    }

    // 數(shù)據(jù)1
    NSDictionary *dictionary1 = @{
        @"data": @"江南憶喊递,最憶是杭州随闪。山寺月中尋桂子,郡亭枕上看潮頭骚勘。何日更重游铐伴?",
    };
    NSData *jsonData1 = [NSJSONSerialization dataWithJSONObject:dictionary1 options:NSJSONWritingPrettyPrinted error:nil];
    NSString *jsonString1 = [[NSString alloc] initWithData:jsonData1 encoding:NSUTF8StringEncoding];
    NSData *data1 = [[jsonString1 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
    NSData *finalData1 = [self dataWithHeader:data1];
    
    // 數(shù)據(jù)2
    NSDictionary *dictionary2 = @{
        @"data": @"江南憶,其次憶吳宮俏讹。吳酒一杯春竹葉当宴,吳娃雙舞醉芙蓉。早晚復相逢藐石?",
    };
    NSData *jsonData2 = [NSJSONSerialization dataWithJSONObject:dictionary2 options:NSJSONWritingPrettyPrinted error:nil];
    NSString *jsonString2 = [[NSString alloc] initWithData:jsonData2 encoding:NSUTF8StringEncoding];
    NSData *data2 = [[jsonString2 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
    NSData *finalData2 = [self dataWithHeader:data2];
    
    [self.clientSockets enumerateObjectsUsingBlock:^(id  _Nonnull clientSocket, NSUInteger idx, BOOL * _Nonnull stop) {
        // 給客戶端發(fā)送數(shù)據(jù)1(tag:消息標記)
        [clientSocket writeData:finalData1 withTimeout:-1 tag:0];
        // 給客戶端發(fā)送數(shù)據(jù)2(tag:消息標記)
        [clientSocket writeData:finalData2 withTimeout:-1 tag:0];
    }];
}


#pragma mark - private method

/// 獲取拼接了包頭后的數(shù)據(jù)
- (NSData *)dataWithHeader:(NSData *)data {
    // 包頭
    NSMutableDictionary *headerDictionary = [NSMutableDictionary dictionary];
    
    // 包頭里的內容1——真實數(shù)據(jù)的長度
    [headerDictionary setObject:[NSString stringWithFormat:@"%ld", data.length] forKey:@"size"];

    // 把字典格式的包頭轉化成JSON字符串格式的包頭
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:headerDictionary options:NSJSONWritingPrettyPrinted error:nil];
    NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];

    // 得到JSON字符串格式的包頭數(shù)據(jù)
    NSMutableData *headerData = [[jsonString dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
    
    // 包頭里的內容2——包頭結束標識即供,回車換行符
    [headerData appendData:[GCDAsyncSocket CRLFData]];

    NSMutableData *finalData = headerData;
    // 在包頭數(shù)據(jù)后面拼接上真實數(shù)據(jù)
    [finalData appendData:data];
    
    return finalData;
}
  • 客戶端socket(相對于上面的例子定拟,變化的代碼)
// 讀取處理

/// 包頭
@property (strong, nonatomic) NSDictionary *headerDictionary;

#pragma mark - GCDAsyncSocketDelegate

/// 連接服務端成功的回調
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    NSLog(@"===========>連接服務端成功");
        
    // 連接成功后于微,立馬開始讀取服務端的數(shù)據(jù),調用這個讀取方法后青自,當服務端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取服務端數(shù)據(jù)成功的回調”
//    [sock readDataWithTimeout:-1 tag:0];
    // 連接成功后株依,立馬開始讀取服務端的數(shù)據(jù),調用這個讀取方法后延窜,當服務端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取服務端數(shù)據(jù)成功的回調”恋腕,但跟上面方法不同的是這個方法只會從數(shù)據(jù)的開頭起讀取到包頭結束標識為止,也就是說“讀取服務端數(shù)據(jù)成功的回調”的data只會讀取到包頭數(shù)據(jù)
    [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}

/// 讀取服務端數(shù)據(jù)成功的回調
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    if (self.headerDictionary == nil) { // 如果包頭為空逆瑞,就代表之前沒讀取到過包頭荠藤,那本次回調的觸發(fā)肯定就是讀取到包頭了伙单,因為上面采用的是readDataToData讀取方法讀取到包頭結束標識為止
        self.headerDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
        if (self.headerDictionary == nil) {
            NSLog(@"===========>數(shù)據(jù)格式出錯了:包頭數(shù)據(jù)為空");
            return;
        }
        
        // 獲取包頭的內容1——真實數(shù)據(jù)的長度
        NSInteger size = [self.headerDictionary[@"size"] integerValue];
        // 此時再調用這個讀取方法去讀取指定長度的真實數(shù)據(jù),讀取到真實數(shù)據(jù)后還是會觸發(fā)當前這個回調
        [sock readDataToLength:size withTimeout:-1 tag:0];

        return;
    }
    
    // 如果走到這里哈肖,代表是讀取到了真實數(shù)據(jù)
    NSInteger size = [self.headerDictionary[@"size"] integerValue];
    // 說明數(shù)據(jù)有問題
    if (size <= 0 || data.length != size) {
        NSLog(@"===========>數(shù)據(jù)格式出錯了:真實數(shù)據(jù)大小不正確");
        return;
    }
    // 此處我們不對數(shù)據(jù)做過多的處理吻育,只是把它轉換成JSON字符串打印出來看看
    NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"%@", [NSString stringWithFormat:@"===========>讀取服務端數(shù)據(jù)成功:%@", jsonString]);

    // 置位包頭
    self.headerDictionary = nil;
    // 注意:讀取服務端數(shù)據(jù)成功后,在這里需要再調用一次讀取服務端數(shù)據(jù)的方法淤井,框架本身就是這么設計的布疼,否則我們就只能接收一次數(shù)據(jù),之后再也接收不到數(shù)據(jù)
//    [sock readDataWithTimeout:-1 tag:0];
    // 繼續(xù)讀取下一條數(shù)據(jù)
    [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
// 發(fā)送處理

#pragma mark - action

/// 向服務端發(fā)送數(shù)據(jù)(以JSON字符串的格式傳輸)
- (IBAction)send:(id)sender {
    if (self.clientSocket == nil) {
        return;
    }

    // 數(shù)據(jù)1
    NSDictionary *dictionary1 = @{
        @"data": @"水光瀲滟晴方好币狠,山色空蒙雨亦奇游两。 ",
    };
    NSData *jsonData1 = [NSJSONSerialization dataWithJSONObject:dictionary1 options:NSJSONWritingPrettyPrinted error:nil];
    NSString *jsonString1 = [[NSString alloc] initWithData:jsonData1 encoding:NSUTF8StringEncoding];
    NSData *data1 = [[jsonString1 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
    NSData *finalData1 = [self dataWithHeader:data1];

    // 數(shù)據(jù)2
    NSDictionary *dictionary2 = @{
        @"data": @"欲把西湖比西子,淡妝濃抹總相宜漩绵。",
    };
    NSData *jsonData2 = [NSJSONSerialization dataWithJSONObject:dictionary2 options:NSJSONWritingPrettyPrinted error:nil];
    NSString *jsonString2 = [[NSString alloc] initWithData:jsonData2 encoding:NSUTF8StringEncoding];
    NSData *data2 = [[jsonString2 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
    NSData *finalData2 = [self dataWithHeader:data2];

    // 給服務端發(fā)送數(shù)據(jù)1(tag:消息標記)
    [self.clientSocket writeData:finalData1 withTimeout:-1 tag:0];
    // 給服務端發(fā)送數(shù)據(jù)2(tag:消息標記)
    [self.clientSocket writeData:finalData2 withTimeout:-1 tag:0];
}


#pragma mark - private method

/// 獲取拼接了包頭后的數(shù)據(jù)
- (NSData *)dataWithHeader:(NSData *)data {
    // 包頭
    NSMutableDictionary *headerDictionary = [NSMutableDictionary dictionary];
    
    // 包頭里的內容1——真實數(shù)據(jù)的長度
    [headerDictionary setObject:[NSString stringWithFormat:@"%ld", data.length] forKey:@"size"];

    // 把字典格式的包頭轉化成JSON字符串格式的包頭
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:headerDictionary options:NSJSONWritingPrettyPrinted error:nil];
    NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];

    // 得到JSON字符串格式的包頭數(shù)據(jù)
    NSMutableData *headerData = [[jsonString dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
    
    // 包頭里的內容2——包頭結束標識贱案,回車換行符
    [headerData appendData:[GCDAsyncSocket CRLFData]];

    NSMutableData *finalData = headerData;
    // 在包頭數(shù)據(jù)后面拼接上真實數(shù)據(jù)
    [finalData appendData:data];
    
    return finalData;
}

3、心跳敝雇拢活

正常來說轰坊,socket連接一旦建立之后就會一直掛在那里,直到某一端主動斷開連接祟印。但實際上肴沫,運營商在檢測到鏈路上有一段時間無數(shù)據(jù)傳輸時,就會自動斷開這種處于非活躍狀態(tài)的連接蕴忆,這就是所謂的運營商NAT超時颤芬,超時時間為5分鐘。因此我們就需要做心跳碧锥欤活——即客戶端每隔一定的時間間隔就向服務端發(fā)送一個心跳數(shù)據(jù)包站蝠,用來保證當前socket連接處于活躍狀態(tài),避免運營商把我們的連接中斷卓鹿,這個時間間隔我們取的是3分鐘菱魔,服務器在收到心跳包時不當做真實數(shù)據(jù)處理即可。

  • 服務端socket(相對于上面的例子吟孙,變化的代碼)
#pragma mark - GCDAsyncSocketDelegate

/// 讀取客戶端數(shù)據(jù)成功的回調
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    if (self.headerDictionary == nil) { // 如果包頭為空澜倦,就代表之前沒讀取到過包頭,那本次回調的觸發(fā)肯定就是讀取到包頭了杰妓,因為上面采用的是readDataToData讀取方法讀取到包頭結束標識為止
        self.headerDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
        if (self.headerDictionary == nil) {
            NSLog(@"===========>數(shù)據(jù)格式出錯了:包頭數(shù)據(jù)為空");
            return;
        }
        
        // 獲取包頭的內容1——真實數(shù)據(jù)的長度
        NSInteger size = [self.headerDictionary[@"size"] integerValue];
        // 此時再調用這個讀取方法去讀取指定長度的真實數(shù)據(jù)藻治,讀取到真實數(shù)據(jù)后還是會觸發(fā)當前這個回調
        [sock readDataToLength:size withTimeout:-1 tag:0];

        return;
    }
    
    // 如果走到這里,代表是讀取到了真實數(shù)據(jù)
    NSInteger size = [self.headerDictionary[@"size"] integerValue];
    // 說明數(shù)據(jù)有問題
    if (size <= 0 || data.length != size) {
        NSLog(@"===========>數(shù)據(jù)格式出錯了:真實數(shù)據(jù)大小不正確");
        return;
    }
    
    NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
    if ([dictionary[@"data"] isEqual:@"com.zhangshuo.alive"]) { // 如果是心跳包巷挥,我們不做處理
        NSLog(@"===========>我是心跳包");
    } else { // 如果不是心跳包桩卵,我們再做處理
        // 此處我們不對數(shù)據(jù)做過多的處理,只是把它轉換成JSON字符串打印出來看看
        NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"%@", [NSString stringWithFormat:@"===========>讀取客戶端數(shù)據(jù)成功:%@", jsonString]);
    }
    
    // 置位包頭
    self.headerDictionary = nil;
    // 注意:讀取客戶端數(shù)據(jù)成功后,在這里需要再調用一次讀取客戶端數(shù)據(jù)的方法雏节,框架本身就是這么設計的胜嗓,否則我們就只能接收一次數(shù)據(jù),之后再也接收不到數(shù)據(jù)
//    [sock readDataWithTimeout:-1 tag:0];
    // 繼續(xù)讀取下一條數(shù)據(jù)
    [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
  • 客戶端socket(相對于上面的例子钩乍,變化的代碼)
#define kHeartBeatTimeInterval 60 * 3 // 心跳奔嫒铮活時間間隔:3分鐘

/// 心跳保活定時器
@property (nonatomic, strong) NSTimer *heartBeatTimer;


#pragma mark - GCDAsyncSocketDelegate

/// 連接服務端成功的回調
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    NSLog(@"===========>連接服務端成功");
        
    // 連接成功后件蚕,立馬開始讀取服務端的數(shù)據(jù)孙技,調用這個讀取方法后,當服務端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取服務端數(shù)據(jù)成功的回調”
//    [sock readDataWithTimeout:-1 tag:0];
    // 連接成功后排作,立馬開始讀取服務端的數(shù)據(jù)牵啦,調用這個讀取方法后,當服務端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取服務端數(shù)據(jù)成功的回調”妄痪,但跟上面方法不同的是這個方法只會從數(shù)據(jù)的開頭起讀取到包頭結束標識為止哈雏,也就是說“讀取服務端數(shù)據(jù)成功的回調”的data只會讀取到包頭數(shù)據(jù)
    [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
    
    // 添加心跳保活
    [self addHeartBeat];
}

/// 與服務端斷開連接的回調
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
    NSLog(@"%@", [NSString stringWithFormat:@"===========>與服務端斷開連接:%@", err]);
    
    // 移除心跳鄙郎活
    [self removeHeartBeat];
}


#pragma mark - 心跳鄙驯瘢活

// 添加心跳保活
- (void)addHeartBeat {
    [self removeHeartBeat];
    
    // 心跳時間設置為3分鐘罪针,NAT超時一般為3~5分鐘
    self.heartBeatTimer = [NSTimer scheduledTimerWithTimeInterval:kHeartBeatTimeInterval repeats:YES block:^(NSTimer * _Nonnull timer) {
        // 和服務端約定好心跳迸砀活數(shù)據(jù)包的內容,以便它們讀取泪酱,盡可能減小心跳迸梢螅活數(shù)據(jù)包的大小
        NSDictionary *dictionary = @{
            @"data": @"com.zhangshuo.alive",
        };
        NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary options:NSJSONWritingPrettyPrinted error:nil];
        NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
        NSData *data = [[jsonString dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
        NSData *finalData = [self dataWithHeader:data];

        // 給服務端發(fā)送數(shù)據(jù)1(tag:消息標記)
        [self.clientSocket writeData:finalData withTimeout:-1 tag:0];
    }];
    [[NSRunLoop currentRunLoop]addTimer:self.heartBeatTimer forMode:NSRunLoopCommonModes];
}

// 移除心跳保活
- (void)removeHeartBeat {
    [self.heartBeatTimer invalidate];
    self.heartBeatTimer = nil;
}

4墓阀、斷線重連

客戶端主動斷開連接時(如App退出登錄或者App進入后臺等場景)毡惜,我們不需要做斷線重連;其它情況下如果連接斷開了(如服務器出了問題或者網斷了等場景)斯撮,我們就需要做斷線重連经伙,來盡量使連接處于正常連接的狀態(tài),這樣才能保證業(yè)務的正常運行勿锅。具體做法就是帕膜,當客戶端檢測到跟服務端斷開連接時就啟動第一次斷線重連,2秒后啟動第二次斷線重連粱甫,再隔4秒后啟動第三次斷線重連泳叠,如果三次斷線重連還沒成功,就認為是服務器出了問題茶宵,不再重連。

/// 斷線重連總時長
@property (assign, nonatomic) NSTimeInterval reconnectDuration;


#pragma mark - GCDAsyncSocketDelegate

/// 連接服務端成功的回調
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    NSLog(@"===========>連接服務端成功");
        
    // 連接成功后宗挥,立馬開始讀取服務端的數(shù)據(jù)乌庶,調用這個讀取方法后种蝶,當服務端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取服務端數(shù)據(jù)成功的回調”
//    [sock readDataWithTimeout:-1 tag:0];
    // 連接成功后,立馬開始讀取服務端的數(shù)據(jù)瞒大,調用這個讀取方法后螃征,當服務端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取服務端數(shù)據(jù)成功的回調”,但跟上面方法不同的是這個方法只會從數(shù)據(jù)的開頭起讀取到包頭結束標識為止透敌,也就是說“讀取服務端數(shù)據(jù)成功的回調”的data只會讀取到包頭數(shù)據(jù)
    [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
    
    // 添加心跳倍⒐觯活
    [self addHeartBeat];
    
    // 斷線重連成功后,置位斷線重連總時長
    self.reconnectDuration = 0;
}

/// 與服務端斷開連接的回調
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
    NSLog(@"%@", [NSString stringWithFormat:@"===========>與服務端斷開連接:%@", err]);
    
    // 移除心跳毙锏纾活
    [self removeHeartBeat];
    
    if (err == nil) { // 代表是客戶端主動斷開連接魄藕,我們不做處理,服務端那邊收到回調后撵术,會移除跟指定客戶端的連接
        NSLog(@"===========>客戶端主動斷開連接");
    } else { // 其它情況下斷開連接背率,啟動斷線重連
        [self reconnect];
    }
}


#pragma mark - 斷線重連

// 斷線重連
- (void)reconnect {
    if (self.clientSocket.isConnected) {
        [self.clientSocket disconnect];
    }
    
    // 第一次斷線重連:0
    // 第二次斷線重連:2
    // 第三次斷線重連:4
    if (self.reconnectDuration > 6) { // 已經三次斷線重連了,還沒成功
        NSLog(@"===========>服務器出小差了嫩与,請稍后重試");
        return;
    }
    
    NSLog(@"===========>斷線重連總時長:%f", self.reconnectDuration);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.reconnectDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if (self.clientSocket.isDisconnected) {
            NSError *error = nil;
            // 如果沒重連成寝姿,還會觸發(fā)“與服務端斷開連接的回調”
            BOOL success = [self.clientSocket connectToHost:kServerIPAddress onPort:kServerPort viaInterface:nil withTimeout:-1 error:&error];
            
            if (error == nil && success) {
                // 連接服務端成功后會觸發(fā)“連接服務端成功的回調”
            } else {
                NSLog(@"%@", [NSString stringWithFormat:@"===========>連接服務端失敗:%@", error]);
            }
        }
    });
    
    // 斷線重連時間以2指數(shù)級增長
    if (self.reconnectDuration == 0) {
        self.reconnectDuration += 2;
    } else if (self.reconnectDuration == 2) {
        self.reconnectDuration += 4;
    } else if (self.reconnectDuration == 6) {
        self.reconnectDuration += 8;
    }
}

參考
1划滋、iOS即時通訊饵筑,從入門到“放棄”?
2处坪、即時通訊下數(shù)據(jù)粘包翻翩、斷包處理實例(基于CocoaAsyncSocket)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市稻薇,隨后出現(xiàn)的幾起案子嫂冻,更是在濱河造成了極大的恐慌,老刑警劉巖塞椎,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件桨仿,死亡現(xiàn)場離奇詭異,居然都是意外死亡案狠,警方通過查閱死者的電腦和手機服傍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來骂铁,“玉大人吹零,你說我怎么就攤上這事±郑” “怎么了灿椅?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我茫蛹,道長操刀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任婴洼,我火速辦了婚禮骨坑,結果婚禮上,老公的妹妹穿的比我還像新娘柬采。我一直安慰自己欢唾,他們只是感情好,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布粉捻。 她就那樣靜靜地躺著礁遣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪杀迹。 梳的紋絲不亂的頭發(fā)上亡脸,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天,我揣著相機與錄音树酪,去河邊找鬼浅碾。 笑死,一個胖子當著我的面吹牛续语,可吹牛的內容都是我干的垂谢。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼疮茄,長吁一口氣:“原來是場噩夢啊……” “哼滥朱!你這毒婦竟也來了?” 一聲冷哼從身側響起力试,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤徙邻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后畸裳,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缰犁,經...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年怖糊,在試婚紗的時候發(fā)現(xiàn)自己被綠了帅容。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡伍伤,死狀恐怖并徘,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情扰魂,我是刑警寧澤麦乞,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布蕴茴,位于F島的核電站,受9級特大地震影響路幸,放射性物質發(fā)生泄漏荐开。R本人自食惡果不足惜付翁,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一简肴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧百侧,春花似錦砰识、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至辛润,卻和暖如春膨处,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背砂竖。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工真椿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人乎澄。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓突硝,卻偏偏與公主長得像,于是被迫代替她去往敵國和親置济。 傳聞我的和親對象是個殘疾皇子解恰,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內容

  • socket這個詞可以表示很多概念: 在TCP/IP協(xié)議中护盈,“IP地址+TCP或UDP端口號”唯一標識網絡通訊中的...
    木魚_cc閱讀 325評論 0 0
  • 一腐宋、什么是Socket? 我們寫的應用程序是通過Socket向網絡發(fā)出請求的整慎,或者你向我發(fā)出請求脏款,我通過Socke...
    lifeline張閱讀 375評論 0 0
  • 1 述 即套接字,是一個對 TCP / IP體系機構的運輸層(可基于TCP或者UDP協(xié)議)進行封裝 的編程調用接口...
    凱玲之戀閱讀 1,083評論 0 2
  • 地址:http://www.cnblogs.com/taoxu/p/7064103.html 寫在準備動手的時候:...
    wvqusrtg閱讀 2,839評論 1 19
  • 一裤园、Socket Socket 作為一種通用的技術規(guī)范撤师,首次是由 Berkeley 大學在 1983 為 4.2B...
    秀花123閱讀 30,968評論 3 26