基于CocoaAsyncSocket實現簡單的即時通訊系統(包含心跳檢查,粘包斷包處理,多用戶并發調度)

寫在開始之前

這篇文章的由來是作者以前在看CocoaAsyncSocket一時興起寫的一個即時通訊小demo的介紹,內容包含心跳檢查,粘包斷包處理,多用戶并發調度,用戶間消息傳送等。最近由于在搞一個sockes5的項目。重新整理了一下CocoaAsyncSocket方面的東西,覺得這個demo還是很意思,故寫出來和大家分享一下。項目中斷包處理部分借鑒了涂耀輝的《即時通訊下數據粘包、斷包處理實例(基于CocoaAsyncSocket)》一文。感興趣的同學也可以看看,還是挺簡單的。這個項目只作為作者自己學習使用,不做商業用途,有很多不足和不當之處,歡迎大家探討交流。好吧話不多說,切入正題。

一.關于CocoaAsyncSocket

這一部分是對CocoaAsyncSocket的一些簡述和方法的一些介紹,已經了解的同學可以直接跳過此部分。

1.關于CocoaAsyncSocket

CocoaAsyncSocket是谷歌的開發者,基于BSD-Socket寫的一個IM框架,它給Mac和iOS提供了易于使用的、強大的異步套接字庫,向上封裝出簡單易用OC接口。省去了我們面向Socket以及數據流Stream等繁瑣復雜的編程。

2.結構

CocoaAsyncSocket中主要包含兩個類:

(1).GCDAsyncSocket.

用GCD搭建的基于TCP/IP協議的socket網絡庫
GCDAsyncSocket is a TCP/IP socket networking library built atop Grand Central Dispatch. -- 引自CocoaAsyncSocket.

(2).GCDAsyncUdpSocket.

用GCD搭建的基于UDP/IP協議的socket網絡庫.
GCDAsyncUdpSocket is a UDP/IP socket networking library built atop Grand Central Dispatch..-- 引自CocoaAsyncSocket.

關于GCDAsyncUdpSocket暫時不表,有機會再講。以下主要用到GCDAsyncSocket

3.GCDAsyncSocket下的幾個主要方法的介紹

(1)主動方法

 //連接服務器host:服務器地址,port:端口;
 - (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr
 //發送消息 timeout:等待時間設置為-1為一直等待 tag:讀取標示;
 - (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag
//讀消息timeout:等待時間設置為-1為一直等待 tag:讀取標示。與發送消息的方法對應,要求每發一條消息,就要調用一次讀消息,要不然讀取不到。GCDAsyncUdpSocket架構要求;
 - (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;
 //監聽本地端口。port:要監聽的端口。error:返回的錯誤;
 //- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr

(2)代理回調方法

 //socket成功連接到服務器調用
  -(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port;
 //接受到新的socket連接調用
  - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket;
 //讀取數據,有數據就會調用
  - (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;
 //直到讀到這個長度的數據,才會觸發代理
  - (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag; 
 //直到讀到data這個邊界,才會觸發代理 
  - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;
 //有socket斷開連接調用
  - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err;

好了。我們要用到的方法大概就是這么幾個。方法的解釋已經注釋標明,下面開始擼代碼,看下具體實現。
???看到這可以休息下,好久沒打這么多字,有點發昏,接下來就是代碼部分的介紹了??


二.代碼介紹

********代碼在這里??

首先說下demo的大體思路:
看過別人寫過的一些版本,大多是開兩個工程,分別模擬服務器和客戶端。個人覺得這樣比較麻煩,跑起來還得改ip,開兩個設備沒太大必要。

我的思路是:做三個單例分別模擬服務端,客戶端A,客戶端B。然后開三個隊列分別處理服務端,客戶端A,客戶端B的事務。服務器負責接受轉發消息,處理用戶心跳和進程調度。(???當然可以寫一個客戶端的公有類,然后實例化更多的客戶端,給每個客戶端分配隊列和clinetID,這都是OK的。我們這里為了簡明和方便斷點,直接分開寫了兩個客戶端單利的實現,但代碼都是一致的。感興趣的同學可以按這個思路封裝一個客戶端類創建多個客戶端玩玩??)。
好了言歸正傳,開始貼代碼吧。

1.主體部分

屏幕快照 .png

前四個文件為CocoaAsyncSocket的源文件了。還是比較簡潔,源碼的話,感興趣的同學也可以看看,后面如果有時間看看能不能做一篇源碼分析。ViewController文件作界面管理,對應三個UITextView,分別作ClientA,Sever,ClientB的一些消息展示。Sever,ClientA,ClientB,看名字就知道分別對應服務器,客戶端A,客戶端B,分別作對應的事務處理。

2.客戶端:

.h文件

#import <Foundation/Foundation.h>
typedef void(^clientAMSG)(NSString *msg) ;

@interface ClientA : NSObject
@property(nonatomic,copy)clientAMSG clientAmsg;
+(id)sharClineA;
/*連接服務器**/
-(BOOL)connect;
/*給B發消息**/
-(void)sendMSGToB;
-(void)ClientAGetMSG:(clientAMSG)clientAmsg;
@end

h文件里沒什么好說的,大家看注釋就好了

.m文件

我先把代碼貼出來,貼這里顯得有點長,不要被忽悠了,貼在這里也并不是要各位同學在這里看的。代碼完全可以先get下來,在xcode里面看,只有200來行,其實結合注釋還是比較清晰。



#import "ClientA.h"
#import "GCDAsyncSocket.h"
#define HOST @"127.0.0.1"
#define PORT 8088
static dispatch_queue_t CGD_manager_creation_queue() {
    static dispatch_queue_t _CGD_manager_creation_queue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _CGD_manager_creation_queue = dispatch_queue_create("gcd.mine.queue.ClinetAkey", DISPATCH_QUEUE_CONCURRENT);
    });
    return _CGD_manager_creation_queue;
}
@interface ClientA ()<GCDAsyncSocketDelegate>
{
    NSDictionary *currentPacketHead;
}
@property (nonatomic, strong)NSThread *connectThread;
@property (nonatomic,strong)NSTimer * connectTimer;//心跳定時器
@property (nonatomic,strong)GCDAsyncSocket * clinetSocket;//客戶端Socket
@property (nonatomic,assign)BOOL  isAgain;//控制斷線重連
@end
@implementation ClientA
+(id)sharClineA{
    static ClientA * clinet;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        clinet=[[ClientA alloc]init];
    });
    return clinet;
}
-(void)ClientAGetMSG:(clientAMSG)clientAmsg{
    self.clientAmsg=clientAmsg;
}
/*連接服務器**/
-(BOOL)connect{
    self.clinetSocket = [[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:CGD_manager_creation_queue()];
    NSError * error;
    [self.clinetSocket  connectToHost:HOST onPort:PORT error:&error];
    if (!error) {
        return YES;
    }else{
        return NO;
    }
   
}
/*給B發消息**/
-(void)sendMSGToB{
    NSData *data  =  [@"Hello" dataUsingEncoding:NSUTF8StringEncoding];
    NSData *data1  = [@"I" dataUsingEncoding:NSUTF8StringEncoding];
    NSData *data2  = [@"am" dataUsingEncoding:NSUTF8StringEncoding];
    NSData *data3  = [@"A," dataUsingEncoding:NSUTF8StringEncoding];
    NSData *data4  = [@"nice to meet you!" dataUsingEncoding:NSUTF8StringEncoding];
    
    [self sendData:data :@"txt" toClinet:@"CinentB"];
    [self sendData:data1 :@"txt" toClinet:@"CinentB"];
    [self sendData:data2 :@"txt" toClinet:@"CinentB"];
    [self sendData:data3 :@"txt" toClinet:@"CinentB"];
    [self sendData:data4 :@"txt" toClinet:@"CinentB"];
    
    NSString *filePath = [[NSBundle mainBundle]pathForResource:@"7" ofType:@"jpeg"];
    
    NSData *data5 = [NSData dataWithContentsOfFile:filePath];
    
    [self sendData:data5 :@"img" toClinet:@"CinentB"];

}
/*封裝報文**/
- (void)sendData:(NSData *)data :(NSString *)type toClinet:(NSString *)target;
{
    NSUInteger size = data.length;
    
    NSMutableDictionary *headDic = [NSMutableDictionary dictionary];
    [headDic setObject:type forKey:@"type"];
    [headDic setObject:@"CinentA" forKey:@"CinentID"];
    [headDic setObject:target forKey:@"targetID"];
    [headDic setObject:[NSString stringWithFormat:@"%ld",size] forKey:@"size"];
    NSString *jsonStr = [self dictionaryToJson:headDic];
    NSData *lengthData = [jsonStr dataUsingEncoding:NSUTF8StringEncoding];
    NSMutableData *mData = [NSMutableData dataWithData:lengthData];
    //分界
    [mData appendData:[GCDAsyncSocket CRLFData]];
    
    [mData appendData:data];
    
    
    //第二個參數,請求超時時間
    [self.clinetSocket writeData:mData withTimeout:-1 tag:0];
    
}
//字典轉為Json字符串
- (NSString *)dictionaryToJson:(NSDictionary *)dic
{
    NSError *error = nil;
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:&error];
    return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}

#pragma mark 加入心跳
- (NSThread*)connectThread{
    if (!_connectThread) {
        _connectThread = [[NSThread alloc]initWithTarget:self selector:@selector(threadStart) object:nil];
    }
    return _connectThread;
}
- (void)threadStart{
    @autoreleasepool {
        [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(heartBeat) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop]run];
    }
}
#pragma mark 發送心跳包
- (void)heartBeat{

        NSData *data  = [@"A心跳" dataUsingEncoding:NSUTF8StringEncoding];
        [self sendData:data :@"heartA" toClinet:@""];
//    [self.clinetSocket writeData:[@"A心跳" dataUsingEncoding:NSUTF8StringEncoding ] withTimeout:-1 tag:0];
    
}


#pragma mark GCDAsyncSocketDelegate
//讀取到數據調用
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
    //先讀取到當前數據包頭部信息
    if (!currentPacketHead) {
        currentPacketHead = [NSJSONSerialization
                             JSONObjectWithData:data
                             options:NSJSONReadingMutableContainers
                             error:nil];
        
        
        if (!currentPacketHead) {
            NSLog(@"error:當前數據包的頭為空");
            
            //斷開這個socket連接或者丟棄這個包的數據進行下一個包的讀取
            
            //....
            
            return;
        }
        
        NSUInteger packetLength = [currentPacketHead[@"size"] integerValue];
        //讀到數據包的大小
        [sock readDataToLength:packetLength withTimeout:-1 tag:0];
        
        return;
    }
    //正式的包處理
    NSUInteger packetLength = [currentPacketHead[@"size"] integerValue];
    //說明數據有問題
    if (packetLength <= 0 || data.length != packetLength) {
        NSLog(@"error:當前數據包數據大小不正確");
        return;
    }
    
    NSString *type = currentPacketHead[@"type"];
    NSString * sourceClient=currentPacketHead[@"sourceClient"];
    if ([type isEqualToString:@"img"]) {
        NSLog(@"客戶端A成功收到圖片--來自于%@",sourceClient);
        if (self.clientAmsg) {
            self.clientAmsg([NSString stringWithFormat:@"客戶端A成功收到圖片--來自于%@",sourceClient]);
        }
        
    }else{
        
        NSString *msg = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"客戶端A收到消息:%@--來自于%@",msg,sourceClient);
      self.clientAmsg([NSString stringWithFormat:@"客戶端A收到消息:%@--來自于%@",msg,sourceClient]);
    }
    currentPacketHead = nil;
    [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
//連接到服務器調用
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    [self heartBeat];
    NSLog(@"%@",[NSString stringWithFormat:@"%@:連接成功",self.class]);
    if (self.clientAmsg) {
    self.clientAmsg([NSString stringWithFormat:@"%@:連接成功",self.class]);
    }

     [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
    //開啟線程發送心跳
    if (!self.isAgain) {
            [self.connectThread start];
    }

}
//斷開連接調用
-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
    NSLog(@"%@",[NSString stringWithFormat:@"%@:斷開連接(Error:%@)",self.class,err]);
    if (self.clientAmsg) {
         self.clientAmsg([NSString stringWithFormat:@"%@:斷開連接(Error:%@)",self.class,err]);
    }
    
    if (err) {
        //重連
        self.isAgain=YES;
       [self.clinetSocket connectToHost:HOST onPort:PORT error:nil];
    }else{
        self.clinetSocket.delegate=nil;
        self.clinetSocket=nil;
        //斷開
    }
    
}


@end


這里肯定要講一講,不然肯定有同學要打我了。
講一講思路:
單例方法就沒必要說了。

1.程序入口為-(BOOL)connect方法,

-(BOOL)connect{
    self.clinetSocket = [[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:CGD_manager_creation_queue()];
    NSError * error;
    [self.clinetSocket  connectToHost:HOST onPort:PORT error:&error];
    if (!error) {
        return YES;
    }else{
        return NO;
    }
   
}

創建一個GCDAsyncSocket,指定代理,代理隊列指定為我們自己通過CGD_manager_creation_queue()方法創建的隊列。

static dispatch_queue_t CGD_manager_creation_queue() {
    static dispatch_queue_t _CGD_manager_creation_queue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _CGD_manager_creation_queue = dispatch_queue_create("gcd.mine.queue.ClinetAkey", DISPATCH_QUEUE_CONCURRENT);
    });
    return _CGD_manager_creation_queue;
}

然后調用 - (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr方法,指明服務器地址端口連接到服務器。

2.連接到服務器會觸發這個代理方法

//連接到服務器調用
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    [self heartBeat];
    NSLog(@"%@",[NSString stringWithFormat:@"%@:連接成功",self.class]);
    self.clientAmsg([NSString stringWithFormat:@"%@:連接成功",self.class]);
     [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
    //開啟線程發送心跳
    if (!self.isAgain) {
            [self.connectThread start];
    }

}

在這個方法里加入心跳,連接上第一時間發送一個心跳包,目的是為了更新服務端里的socket的ClientID識別用戶這個后面解釋。

創建心跳和發送心跳包的方法:

#pragma mark 加入心跳
- (NSThread*)connectThread{
    if (!_connectThread) {
        _connectThread = [[NSThread alloc]initWithTarget:self selector:@selector(threadStart) object:nil];
    }
    return _connectThread;
}
- (void)threadStart{
    @autoreleasepool {
        [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(heartBeat) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop]run];
    }
}
#pragma mark 發送心跳包
- (void)heartBeat{

        NSData *data  = [@"A心跳" dataUsingEncoding:NSUTF8StringEncoding];
        [self sendData:data :@"heartA" toClinet:@""];
//    [self.clinetSocket writeData:[@"A心跳" dataUsingEncoding:NSUTF8StringEncoding ] withTimeout:-1 tag:0];
    
}

寫數據是用
[self.clinetSocket writeData:[@"A心跳" dataUsingEncoding:NSUTF8StringEncoding ] withTimeout:-1 tag:0]方法
這里的-1為等待時間,如果寫為-1意思為無限等待,tag指定會話標示。
---注意
(**) [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0] 這一句;
這一句的意思是接收到[GCDAsyncSocket CRLFData] 這個邊界,觸發代理,至于[GCDAsyncSocket CRLFData]是什么下面會介紹。
上文說過,沒發送一次消息,就對應寫一個read消息,這樣才能觸發- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag方法來接收回調。

3.重點來了:這一塊牽扯到數據包的處理。我們看下發送消息和發送心跳包的方法:

/*給B發消息**/
-(void)sendMSGToB{
    NSData *data  =  [@"Hello" dataUsingEncoding:NSUTF8StringEncoding];
    NSData *data1  = [@"I" dataUsingEncoding:NSUTF8StringEncoding];
    NSData *data2  = [@"am" dataUsingEncoding:NSUTF8StringEncoding];
    NSData *data3  = [@"A," dataUsingEncoding:NSUTF8StringEncoding];
    NSData *data4  = [@"nice to meet you!" dataUsingEncoding:NSUTF8StringEncoding];
    
    [self sendData:data :@"txt" toClinet:@"CinentB"];
    [self sendData:data1 :@"txt" toClinet:@"CinentB"];
    [self sendData:data2 :@"txt" toClinet:@"CinentB"];
    [self sendData:data3 :@"txt" toClinet:@"CinentB"];
    [self sendData:data4 :@"txt" toClinet:@"CinentB"];
    
    NSString *filePath = [[NSBundle mainBundle]pathForResource:@"7" ofType:@"jpeg"];
    
    NSData *data5 = [NSData dataWithContentsOfFile:filePath];
    
    [self sendData:data5 :@"img" toClinet:@"CinentB"];

}
/*封裝報文**/
- (void)sendData:(NSData *)data :(NSString *)type toClinet:(NSString *)target;
{
    NSUInteger size = data.length;
    
    NSMutableDictionary *headDic = [NSMutableDictionary dictionary];
    [headDic setObject:type forKey:@"type"];
    [headDic setObject:@"CinentA" forKey:@"CinentID"];
    [headDic setObject:target forKey:@"targetID"];
    [headDic setObject:[NSString stringWithFormat:@"%ld",size] forKey:@"size"];
    NSString *jsonStr = [self dictionaryToJson:headDic];
    NSData *lengthData = [jsonStr dataUsingEncoding:NSUTF8StringEncoding];
    NSMutableData *mData = [NSMutableData dataWithData:lengthData];
    //分界
    [mData appendData:[GCDAsyncSocket CRLFData]];
    
    [mData appendData:data];
    
    
    //第二個參數,請求超時時間
    [self.clinetSocket writeData:mData withTimeout:-1 tag:0];
    
}
//字典轉為Json字符串
- (NSString *)dictionaryToJson:(NSDictionary *)dic
{
    NSError *error = nil;
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:&error];
    return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}

這里就是做封包處理了:

在這里主要是發了幾條消息和一個圖片給ClientB。
我們定義了一個headDic,這個是我們數據包的頭部,里面裝了這個數據包的大小和類型信息,自身客戶端ID,和目標客戶端ID(當然,你可以裝更多的其他標識信息。)然后我們把它轉成了json,最后轉成data。
然后我們把這個head拼在最前面,接著拼了一個:

[GCDAsyncSocket CRLFData]

這個是什么呢?其實它就是一個\r\n。我們用它來做頭部的邊界。(又或者我們可以規定一個固定的頭部長度,來作為邊界)。
最后我們把真正的數據包給拼接上。

這一塊借鑒了涂耀輝的《即時通訊下數據粘包、斷包處理實例(基于CocoaAsyncSocket)》一文。也看過別的幾種處理方式,但是感覺這種處理方式比較容易理解也好用一些。

4.最后我們還做了一個客戶端的斷開重連的例子(只是舉個例子,大家還是不要這樣干)。

//斷開連接調用
-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
    NSLog(@"%@",[NSString stringWithFormat:@"%@:斷開連接(Error:%@)",self.class,err]);
      self.clientAmsg([NSString stringWithFormat:@"%@:斷開連接(Error:%@)",self.class,err]);
    if (err) {
        //重連
        self.isAgain=YES;
       [self.clinetSocket connectToHost:HOST onPort:PORT error:nil];
    }else{
        self.clinetSocket.delegate=nil;
        self.clinetSocket=nil;
        //斷開
    }
    
}

客戶端基本就是這些東西了,至于對應的解包處理我想放到服務端來講,原理都是一樣的。
???休息下,不知道有幾個同學能看到這里,如果你看到這里說明你是一個很有耐心的程序員了,不知道各位能看到這里的同學有沒有一些混亂,個人語音能力有限,實在抱歉。還是看代碼清晰,其實客戶端這一部分就做三件事:1.連接服務器,維持心跳。2.封包,通過服務器給指定客戶端發送消息。3接收服務器消息。下面我們將開始服務端的部分??


3.服務端:

老規矩,貼代碼

.h

#import <Foundation/Foundation.h>


typedef void(^clientAMSG)(NSString *msg) ;

@interface ClientA : NSObject
@property(nonatomic,copy)clientAMSG clientAmsg;
+(id)sharClineA;
/*連接服務器**/
-(BOOL)connect;
/*給B發消息**/
-(void)sendMSGToB;
-(void)ClientAGetMSG:(clientAMSG)clientAmsg;
@end

.h文件沒什么好說的。

.m

代碼比較簡單,先貼出來,后面做解釋,拿到項目的各位可以直接不看這一塊在xcode里打開。


#import "Sever.h"
#import "GCDAsyncSocket.h"

static dispatch_queue_t CGD_manager_SEVER_queue() {
    static dispatch_queue_t _CGD_manager_SEVER_queue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _CGD_manager_SEVER_queue = dispatch_queue_create("gcd.mine.queue.SeverAkey", DISPATCH_QUEUE_CONCURRENT);
    });
    return _CGD_manager_SEVER_queue;
}
//儲存在本地的客戶端類型
@interface Client : NSObject
@property(nonatomic, strong)GCDAsyncSocket *scocket;//客戶端scocket
@property(nonatomic, strong)NSDate *timeOfSocket;  //更新通訊時間
@property(nonatomic,strong) NSDictionary *currentPacketHead;//客戶端報文字典
@property(nonatomic,copy)NSString * clientID;//客戶端ID
@end
@implementation Client
@end



@interface Sever () <GCDAsyncSocketDelegate>
@property(nonatomic, strong)GCDAsyncSocket *serve;
@property(nonatomic, strong)NSMutableArray *clientsArray;// 儲存客戶端
@property(nonatomic, strong)NSThread *checkThread;// 檢測心跳
@end

@implementation Sever
+(instancetype)sharSever{
    static Sever * sever;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sever=[[Sever alloc]init];
    });
    return sever;
}
-(instancetype)init{
    if (self = [super init]) {
        self.serve = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:CGD_manager_SEVER_queue()];
        self.checkThread = [[NSThread alloc]initWithTarget:self selector:@selector(checkClient) object:nil];
        [self.checkThread start];
    }
    
    return self;
}
-(NSMutableArray *)clientsArray{
    if (!_clientsArray) {
        _clientsArray = [NSMutableArray array];
    }
    
    return _clientsArray;
}
-(void)SeverGetMSG:(SeverMSG)severAmsg{
    self.severAmsg =severAmsg;
}
//監控端口
-(void)openSerVice{
    
    NSError *error;
    BOOL sucess = [self.serve acceptOnPort:8088 error:&error];
    if (sucess) {
        NSLog(@"%@",[NSString stringWithFormat:@"%@---監聽端口成功,等待客戶端請求連接...",self.class]);
        if (self.severAmsg) {
            self.severAmsg([NSString stringWithFormat:@"%@---監聽端口成功,等待客戶端請求連接...",self.class]);
        }
        
    }else {
        NSLog(@"%@",[NSString stringWithFormat:@"%@---端口開啟失敗...",self.class]);
        if (self.severAmsg) {
            self.severAmsg([NSString stringWithFormat:@"%@---端口開啟失敗...",self.class]);
        }
    }
}

#pragma mark  GCDAsyncSocketDelegate
- (void)socket:(GCDAsyncSocket *)serveSock didAcceptNewSocket:(GCDAsyncSocket *)newSocket{
    if (self.severAmsg) {
        self.severAmsg([NSString stringWithFormat:@"%@---%@ IP: %@: %zd 客戶端請求連接...",self.class,newSocket,newSocket.connectedHost,newSocket.connectedPort]);
    }
    NSLog(@"%@---%@ IP: %@: %zd 客戶端請求連接...",self.class,newSocket,newSocket.connectedHost,newSocket.connectedPort);
    // 1.將客戶端socket保存起來
    Client *client = [[Client alloc]init];
    client.scocket = newSocket;
    client.timeOfSocket = [NSDate date];
    [self.clientsArray addObject:client];
    [newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
    
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag  {
    Client * client=[self getClientBysocket:sock];
    if (!client) {
        [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
        return;
    }
    //先讀取到當前數據包頭部信息
    if (!client.currentPacketHead) {
        client.currentPacketHead = [NSJSONSerialization
                                    JSONObjectWithData:data
                                    options:NSJSONReadingMutableContainers
                                    error:nil];
        if (!client.currentPacketHead) {
            NSLog(@"error:當前數據包的頭為空");
            if (self.severAmsg) {
                self.severAmsg(@"error:當前數據包的頭為空");
            }
            //斷開這個socket連接或者丟棄這個包的數據進行下一個包的讀取
            //....
            return;
        }
        NSUInteger packetLength = [client.currentPacketHead[@"size"] integerValue];
        //讀到數據包的大小
        [sock readDataToLength:packetLength withTimeout:-1 tag:0];
        return;
    }
    //正式的包處理
    NSUInteger packetLength = [client.currentPacketHead[@"size"] integerValue];
    //說明數據有問題
    if (packetLength <= 0 || data.length != packetLength) {
        NSString *msg = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"error:當前數據包數據大小不正確(%@)",msg);
        if (self.severAmsg) {
            self.severAmsg([NSString stringWithFormat:@"error:當前數據包數據大小不正確(%@)",msg]);
        }
        return;
    }
    //分配ID
    NSString *clientID=client.currentPacketHead[@"CinentID"];
    client.clientID=clientID;
    NSString *targetID=client.currentPacketHead[@"targetID"];
    NSString *type = client.currentPacketHead[@"type"];
    
    
    
    
    /*
     *服務端可以不解析內容,直接轉發出去,這里只是想看看打印消息
     **/
    if ([type isEqualToString:@"img"]) {
        NSLog(@"收到圖片");
        if (self.severAmsg) {
            self.severAmsg(@"收到圖片");
        }
    }else{
        NSString *msg = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
        if (self.severAmsg) {
            self.severAmsg([NSString stringWithFormat:@"收到消息:%@",msg]);
        }
        NSLog(@"收到消息:%@",msg);
    }
    
    
    
    
    for (Client *socket in self.clientsArray) {
        //這里找不到目標客戶端,可以把數據保存起來,等待目標客戶端上線,再轉發出去,這里就不做了,感興趣的同學自己可以試一試
        if ([socket.clientID isEqualToString:targetID]) {
            [self writeDataWithSocket:socket.scocket data:data type:type sourceClient:clientID];
        }
    }
    client.currentPacketHead = nil;
    [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
-(Client *)getClientBysocket:(GCDAsyncSocket *)sock{
    for (Client *socket in self.clientsArray) {
        if ([sock isEqual:socket.scocket]) {
            ///更新最新時間
            socket.timeOfSocket = [NSDate date];
            return socket;
        }
    }
    return nil;
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
    if (self.severAmsg) {
        self.severAmsg([NSString stringWithFormat:@"%@---有用戶下線...",self.class]);
    }
    NSLog(@"%@",[NSString stringWithFormat:@"%@---有用戶下線...",self.class]);
    NSMutableArray *arrayNew = [NSMutableArray array];
    for (Client *socket in self.clientsArray ) {
        if ([socket.scocket isEqual:sock]) {
            continue;
        }
        [arrayNew addObject:socket   ];
    }
    self.clientsArray = arrayNew;
}

-(void)exitWithSocket:(GCDAsyncSocket *)clientSocket{
    //    [self writeDataWithSocket:clientSocket str:@"成功退出\n"];
    //    [self.arrayClient removeObject:clientSocket];
    //
    //    NSLog(@"當前在線用戶個數:%ld",self.arrayClient.count);
}

- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
    if (self.severAmsg) {
        self.severAmsg([NSString stringWithFormat:@"%@---數據發送成功.....",self.class]);
    }
    NSLog(@"%@",[NSString stringWithFormat:@"%@---數據發送成功.....",self.class]);
}

- (void)writeDataWithSocket:(GCDAsyncSocket*)clientSocket data:(NSData *)data type:(NSString *)type sourceClient:(NSString *)sourceClient {
    NSUInteger size = data.length;
    NSMutableDictionary *headDic = [NSMutableDictionary dictionary];
    [headDic setObject:type forKey:@"type"];
    [headDic setObject:sourceClient forKey:@"sourceClient"];
    [headDic setObject:[NSString stringWithFormat:@"%ld",size] forKey:@"size"];
    NSString *jsonStr = [self dictionaryToJson:headDic];
    NSData *lengthData = [jsonStr dataUsingEncoding:NSUTF8StringEncoding];
    NSMutableData *mData = [NSMutableData dataWithData:lengthData];
    //分界
    [mData appendData:[GCDAsyncSocket CRLFData]];
    [mData appendData:data];
    //第二個參數,請求超時時間
    [clientSocket writeData:mData withTimeout:-1 tag:0];
    
}
//字典轉為Json字符串
- (NSString *)dictionaryToJson:(NSDictionary *)dic
{
    NSError *error = nil;
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:&error];
    return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}

#pragma checkTimeThread

//開啟線程 啟動runloop 循環檢測客戶端socket最新time
- (void)checkClient{
    @autoreleasepool {
        [NSTimer scheduledTimerWithTimeInterval:30 target:self selector:@selector(repeatCheckClinet) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop]run];
    }
}

//移除 超過心跳的 client
- (void)repeatCheckClinet{
    if (self.clientsArray.count == 0) {
        return;
    }
    NSDate *date = [NSDate date];
    NSMutableArray *arrayNew = [NSMutableArray array];
    for (Client *socket in self.clientsArray ) {
        if ([date timeIntervalSinceDate:socket.timeOfSocket]>20||!socket) {
            if (socket) {
                [socket.scocket disconnect];
            }
            
            continue;
        }
        [arrayNew addObject:socket];
    }
    self.clientsArray = arrayNew;
}
@end


看過客戶端的流程,再來看服務端就簡單很多。說下思路:

1.跟客戶端一樣,先做單例,構造隊列初始化服務端Socket,設置代理。

+(instancetype)sharSever{
    static Sever * sever;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sever=[[Sever alloc]init];
    });
    return sever;
}
-(instancetype)init{
    if (self = [super init]) {
        self.serve = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:CGD_manager_SEVER_queue()];
        self.checkThread = [[NSThread alloc]initWithTarget:self selector:@selector(checkClient) object:nil];
        [self.checkThread start];
    }
    
    return self;
}
-(NSMutableArray *)clientsArray{
    if (!_clientsArray) {
        _clientsArray = [NSMutableArray array];
    }
    
    return _clientsArray;
}

2.監聽本地端口

//監控端口
-(void)openSerVice{
    
    NSError *error;
    BOOL sucess = [self.serve acceptOnPort:8088 error:&error];
    if (sucess) {
        NSLog(@"%@",[NSString stringWithFormat:@"%@---監聽端口成功,等待客戶端請求連接...",self.class]);
        if (self.severAmsg) {
            self.severAmsg([NSString stringWithFormat:@"%@---監聽端口成功,等待客戶端請求連接...",self.class]);
        }
        
    }else {
        NSLog(@"%@",[NSString stringWithFormat:@"%@---端口開啟失敗...",self.class]);
        if (self.severAmsg) {
            self.severAmsg([NSString stringWithFormat:@"%@---端口開啟失敗...",self.class]);
        }
    }
}

3.接收到新的socket連接到本地端口,會觸發代理調用

- (void)socket:(GCDAsyncSocket *)serveSock didAcceptNewSocket:(GCDAsyncSocket *)newSocket{
    if (self.severAmsg) {
        self.severAmsg([NSString stringWithFormat:@"%@---%@ IP: %@: %zd 客戶端請求連接...",self.class,newSocket,newSocket.connectedHost,newSocket.connectedPort]);
    }
    NSLog(@"%@---%@ IP: %@: %zd 客戶端請求連接...",self.class,newSocket,newSocket.connectedHost,newSocket.connectedPort);
    // 1.將客戶端socket保存起來
    Client *client = [[Client alloc]init];
    client.scocket = newSocket;
    client.timeOfSocket = [NSDate date];
    [self.clientsArray addObject:client];
    [newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
    
}

這里還是蠻重要的,解釋下:在接到新的socket連接后,我們創建一個Client 類型的對象,將當前socket交給這個對象,再將這個Client用一個可變數組self.clientsArray保存起來。(這個數組用來保存所有連接到服務器的socket對應創建的Client對象,后面會利用它來處理心跳,轉發,和用戶調度)。然后讓當socket前讀取有[GCDAsyncSocket CRLFData]邊界的報文。

可以看下Client對象的聲明:

//儲存在本地的客戶端類型
@interface Client : NSObject
@property(nonatomic, strong)GCDAsyncSocket *scocket;//客戶端scocket
@property(nonatomic, strong)NSDate *timeOfSocket;  //更新通訊時間
@property(nonatomic,strong) NSDictionary *currentPacketHead;//客戶端報文字典
@property(nonatomic,copy)NSString * clientID;//客戶端ID
@end
@implementation Client
@end

繼承自NSObject類,里面包含4個屬性:

scocket屬性: 對應每個客戶端連接過來的scocket;
timeOfSocket屬性: 對應每個客戶端最后和服務器交互時間;
currentPacketHead屬性: 用來儲存用戶數據包報頭;
clientID屬性: 對應每個客戶端分配到的ID;

4.接收數據

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag  {
    Client * client=[self getClientBysocket:sock];
    if (!client) {
        [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
        return;
    }
    //先讀取到當前數據包頭部信息
    if (!client.currentPacketHead) {
        client.currentPacketHead = [NSJSONSerialization
                                    JSONObjectWithData:data
                                    options:NSJSONReadingMutableContainers
                                    error:nil];
        if (!client.currentPacketHead) {
            NSLog(@"error:當前數據包的頭為空");
            if (self.severAmsg) {
                self.severAmsg(@"error:當前數據包的頭為空");
            }
            //斷開這個socket連接或者丟棄這個包的數據進行下一個包的讀取
            //....
            return;
        }
        NSUInteger packetLength = [client.currentPacketHead[@"size"] integerValue];
        //讀到數據包的大小
        [sock readDataToLength:packetLength withTimeout:-1 tag:0];
        return;
    }
    //正式的包處理
    NSUInteger packetLength = [client.currentPacketHead[@"size"] integerValue];
    //說明數據有問題
    if (packetLength <= 0 || data.length != packetLength) {
        NSString *msg = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"error:當前數據包數據大小不正確(%@)",msg);
        if (self.severAmsg) {
            self.severAmsg([NSString stringWithFormat:@"error:當前數據包數據大小不正確(%@)",msg]);
        }
        return;
    }
    //分配ID
    NSString *clientID=client.currentPacketHead[@"CinentID"];
    client.clientID=clientID;
    NSString *targetID=client.currentPacketHead[@"targetID"];
    NSString *type = client.currentPacketHead[@"type"];
    
    
    
    
    /*
     *服務端可以不解析內容,直接轉發出去,這里只是想看看打印消息
     **/
    if ([type isEqualToString:@"img"]) {
        NSLog(@"收到圖片");
        if (self.severAmsg) {
            self.severAmsg(@"收到圖片");
        }
    }else{
        NSString *msg = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
        if (self.severAmsg) {
            self.severAmsg([NSString stringWithFormat:@"收到消息:%@",msg]);
        }
        NSLog(@"收到消息:%@",msg);
    }
    
    
    
    
    for (Client *socket in self.clientsArray) {
        //這里找不到目標客戶端,可以把數據保存起來,等待目標客戶端上線,再轉發出去,這里就不做了,感興趣的同學自己可以試一試
        if ([socket.clientID isEqualToString:targetID]) {
            [self writeDataWithSocket:socket.scocket data:data type:type sourceClient:clientID];
        }
    }
    client.currentPacketHead = nil;
    [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}

這一塊算是服務端核心的部分了,數據包的拆解,分發,用戶調度,心跳刷新都在這里處理。
大體思路:監聽到有[GCDAsyncSocket CRLFData] 邊界的數據包,調用didReadData方法,在這個方法里。先根據當前sock找到self.clientsArray(存儲所有client和相關信息)里找到對應的Client,在找的過程中,將找到的對應的Client的timeOfSocket(可以理解為時間戳)刷新。
判斷對應Client的currentPacketHead是否為nil。如果為空,將收到的data轉化為字典賦值給currentPacketHead。此時currentPacketHead的內容應該為

{
@"size":@"****",//攜帶內容的大小
@"CinentID":@"****",//源客戶端id
@"type":@"****",//攜帶數據格式
@"targetID":@"****"http://目的客戶端id
....當然我們還可以封裝一些別的信息,我們這里就設計這幾個我們需要的
}

然后,通知當前socket來接收size對應長度的數據包。
理想狀態下(這里會有并發過程,這里先提一下,后面解釋)該sockt會去讀到size長度的內容包,檢測下內容包的合理性,如果合理。我們就取到正確的內容了,然后,根據根據收到的內容給此Client分配clientID,根據報文目的客戶端id,在self.clientsArray中找到對應Client所對應的socket,再將內容封裝轉發。客戶端的解析過程,和這里大體相似,不表。

上面說到用戶并發,因為所有的客戶端都會同時發送心跳包或用戶消息,都會調用didReadData方法,比如說用戶A對應的socket讀取到報文頭部,要去讀報文內容的時候,用戶B對應的socket也同時調用didReadData方法,那么會照成我們接收到數據處理混亂。所以,我們封裝一個Client來對應處理每個客戶端的socket事務,通過定位標記,讓他們并發工作,各自維持自己處理數據的邏輯,互不干擾。

5.心跳檢測

//開啟線程 啟動runloop 循環檢測客戶端socket最新time
- (void)checkClient{
    @autoreleasepool {
        [NSTimer scheduledTimerWithTimeInterval:30 target:self selector:@selector(repeatCheckClinet) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop]run];
    }
}

//移除 超過心跳的 client
- (void)repeatCheckClinet{
    if (self.clientsArray.count == 0) {
        return;
    }
    NSDate *date = [NSDate date];
    NSMutableArray *arrayNew = [NSMutableArray array];
    for (Client *socket in self.clientsArray ) {
        if ([date timeIntervalSinceDate:socket.timeOfSocket]>20||!socket) {
            if (socket) {
                [socket.scocket disconnect];
            }
            
            continue;
        }
        [arrayNew addObject:socket];
    }
    self.clientsArray = arrayNew;
}

這一塊很簡單,做法是,沒收到客戶端發過來的報文,就更新下,客戶端最后交互時間(timeOfSocket),然后,每隔一段時間檢測self.clientsArray每個Client對應的timeOfSocket,和目前時間對比,如果超出預先設定的失活時間,就斷開此Client對應的scocket,殺死客戶端。


寫在最后

???到這里,就全部講完了,文章篇幅比較長,但大多是代碼部分,對CocoaAsyncSocket有了解的同學,可以直接看代碼,比較簡單,可能很多同學看到這種又臭又長的文章,會選擇直接略過。嘴拙,總想用更多的文字來解釋,還是怕自己表達的不夠清晰,水平有限,文章和代碼中多有漏洞,歡迎指出,內心忐忑,只愿不要誤人子弟就好。同時也希望能拋磚引玉,給有需要的同學一些思路和啟發。??

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容