基于CocoaAsyncSocket實(shí)現(xiàn)簡單的即時通訊系統(tǒng)(包含心跳檢查,粘包斷包處理舅柜,多用戶并發(fā)調(diào)度)

寫在開始之前

這篇文章的由來是作者以前在看CocoaAsyncSocket一時興起寫的一個即時通訊小demo的介紹梭纹,內(nèi)容包含心跳檢查,粘包斷包處理致份,多用戶并發(fā)調(diào)度变抽,用戶間消息傳送等。最近由于在搞一個sockes5的項目。重新整理了一下CocoaAsyncSocket方面的東西绍载,覺得這個demo還是很意思诡宗,故寫出來和大家分享一下。項目中斷包處理部分借鑒了涂耀輝的《即時通訊下數(shù)據(jù)粘包击儡、斷包處理實(shí)例(基于CocoaAsyncSocket)》一文塔沃。感興趣的同學(xué)也可以看看,還是挺簡單的曙痘。這個項目只作為作者自己學(xué)習(xí)使用芳悲,不做商業(yè)用途立肘,有很多不足和不當(dāng)之處边坤,歡迎大家探討交流。好吧話不多說谅年,切入正題茧痒。

一.關(guān)于CocoaAsyncSocket

這一部分是對CocoaAsyncSocket的一些簡述和方法的一些介紹,已經(jīng)了解的同學(xué)可以直接跳過此部分融蹂。

1.關(guān)于CocoaAsyncSocket

CocoaAsyncSocket是谷歌的開發(fā)者旺订,基于BSD-Socket寫的一個IM框架,它給Mac和iOS提供了易于使用的超燃、強(qiáng)大的異步套接字庫区拳,向上封裝出簡單易用OC接口。省去了我們面向Socket以及數(shù)據(jù)流Stream等繁瑣復(fù)雜的編程意乓。

2.結(jié)構(gòu)

CocoaAsyncSocket中主要包含兩個類:

(1).GCDAsyncSocket.

用GCD搭建的基于TCP/IP協(xié)議的socket網(wǎng)絡(luò)庫
GCDAsyncSocket is a TCP/IP socket networking library built atop Grand Central Dispatch. -- 引自CocoaAsyncSocket.

(2).GCDAsyncUdpSocket.

用GCD搭建的基于UDP/IP協(xié)議的socket網(wǎng)絡(luò)庫.
GCDAsyncUdpSocket is a UDP/IP socket networking library built atop Grand Central Dispatch..-- 引自CocoaAsyncSocket.

關(guān)于GCDAsyncUdpSocket暫時不表樱调,有機(jī)會再講。以下主要用到GCDAsyncSocket

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

(1)主動方法

 //連接服務(wù)器host:服務(wù)器地址届良,port:端口笆凌;
 - (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr
 //發(fā)送消息 timeout:等待時間設(shè)置為-1為一直等待 tag:讀取標(biāo)示;
 - (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag
//讀消息timeout:等待時間設(shè)置為-1為一直等待 tag:讀取標(biāo)示士葫。與發(fā)送消息的方法對應(yīng)乞而,要求每發(fā)一條消息,就要調(diào)用一次讀消息慢显,要不然讀取不到爪模。GCDAsyncUdpSocket架構(gòu)要求;
 - (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;
 //監(jiān)聽本地端口荚藻。port:要監(jiān)聽的端口屋灌。error:返回的錯誤;
 //- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr

(2)代理回調(diào)方法

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

好了。我們要用到的方法大概就是這么幾個落塑。方法的解釋已經(jīng)注釋標(biāo)明纽疟,下面開始擼代碼,看下具體實(shí)現(xiàn)憾赁。
???看到這可以休息下污朽,好久沒打這么多字,有點(diǎn)發(fā)昏龙考,接下來就是代碼部分的介紹了??


二.代碼介紹

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

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

我的思路是:做三個單例分別模擬服務(wù)端蛇损,客戶端A,客戶端B坛怪。然后開三個隊列分別處理服務(wù)端淤齐,客戶端A,客戶端B的事務(wù)袜匿。服務(wù)器負(fù)責(zé)接受轉(zhuǎn)發(fā)消息更啄,處理用戶心跳和進(jìn)程調(diào)度。(???當(dāng)然可以寫一個客戶端的公有類居灯,然后實(shí)例化更多的客戶端祭务,給每個客戶端分配隊列和clinetID,這都是OK的穆壕。我們這里為了簡明和方便斷點(diǎn)待牵,直接分開寫了兩個客戶端單利的實(shí)現(xiàn),但代碼都是一致的喇勋。感興趣的同學(xué)可以按這個思路封裝一個客戶端類創(chuàng)建多個客戶端玩玩??)缨该。
好了言歸正傳,開始貼代碼吧川背。

1.主體部分

屏幕快照 .png

前四個文件為CocoaAsyncSocket的源文件了贰拿。還是比較簡潔,源碼的話熄云,感興趣的同學(xué)也可以看看膨更,后面如果有時間看看能不能做一篇源碼分析。ViewController文件作界面管理缴允,對應(yīng)三個UITextView,分別作ClientA荚守,Sever珍德,ClientB的一些消息展示。Sever矗漾,ClientA锈候,ClientB,看名字就知道分別對應(yīng)服務(wù)器敞贡,客戶端A泵琳,客戶端B,分別作對應(yīng)的事務(wù)處理誊役。

2.客戶端:

.h文件

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

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

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

.m文件

我先把代碼貼出來,貼這里顯得有點(diǎn)長蛔垢,不要被忽悠了击孩,貼在這里也并不是要各位同學(xué)在這里看的。代碼完全可以先get下來啦桌,在xcode里面看溯壶,只有200來行,其實(shí)結(jié)合注釋還是比較清晰甫男。



#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;
}
/*連接服務(wù)器**/
-(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發(fā)消息**/
-(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];
    
    
    //第二個參數(shù),請求超時時間
    [self.clinetSocket writeData:mData withTimeout:-1 tag:0];
    
}
//字典轉(zhuǎn)為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 發(fā)送心跳包
- (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
//讀取到數(shù)據(jù)調(diào)用
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
    //先讀取到當(dāng)前數(shù)據(jù)包頭部信息
    if (!currentPacketHead) {
        currentPacketHead = [NSJSONSerialization
                             JSONObjectWithData:data
                             options:NSJSONReadingMutableContainers
                             error:nil];
        
        
        if (!currentPacketHead) {
            NSLog(@"error:當(dāng)前數(shù)據(jù)包的頭為空");
            
            //斷開這個socket連接或者丟棄這個包的數(shù)據(jù)進(jìn)行下一個包的讀取
            
            //....
            
            return;
        }
        
        NSUInteger packetLength = [currentPacketHead[@"size"] integerValue];
        //讀到數(shù)據(jù)包的大小
        [sock readDataToLength:packetLength withTimeout:-1 tag:0];
        
        return;
    }
    //正式的包處理
    NSUInteger packetLength = [currentPacketHead[@"size"] integerValue];
    //說明數(shù)據(jù)有問題
    if (packetLength <= 0 || data.length != packetLength) {
        NSLog(@"error:當(dāng)前數(shù)據(jù)包數(shù)據(jù)大小不正確");
        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];
}
//連接到服務(wù)器調(diào)用
- (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];
    //開啟線程發(fā)送心跳
    if (!self.isAgain) {
            [self.connectThread start];
    }

}
//斷開連接調(diào)用
-(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


這里肯定要講一講验烧,不然肯定有同學(xué)要打我了板驳。
講一講思路:
單例方法就沒必要說了。

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;
    }
   
}

創(chuàng)建一個GCDAsyncSocket若治,指定代理,代理隊列指定為我們自己通過CGD_manager_creation_queue()方法創(chuàng)建的隊列感混。

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;
}

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

2.連接到服務(wù)器會觸發(fā)這個代理方法

//連接到服務(wù)器調(diào)用
- (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];
    //開啟線程發(fā)送心跳
    if (!self.isAgain) {
            [self.connectThread start];
    }

}

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

創(chuàng)建心跳和發(fā)送心跳包的方法:

#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 發(fā)送心跳包
- (void)heartBeat{

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

寫數(shù)據(jù)是用
[self.clinetSocket writeData:[@"A心跳" dataUsingEncoding:NSUTF8StringEncoding ] withTimeout:-1 tag:0]方法
這里的-1為等待時間庭呜,如果寫為-1意思為無限等待滑进,tag指定會話標(biāo)示。
---注意
(**) [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0] 這一句募谎;
這一句的意思是接收到[GCDAsyncSocket CRLFData] 這個邊界扶关,觸發(fā)代理,至于[GCDAsyncSocket CRLFData]是什么下面會介紹数冬。
上文說過节槐,沒發(fā)送一次消息,就對應(yīng)寫一個read消息,這樣才能觸發(fā)- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag方法來接收回調(diào)铜异。

3.重點(diǎn)來了:這一塊牽扯到數(shù)據(jù)包的處理地来。我們看下發(fā)送消息和發(fā)送心跳包的方法:

/*給B發(fā)消息**/
-(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];
    
    
    //第二個參數(shù),請求超時時間
    [self.clinetSocket writeData:mData withTimeout:-1 tag:0];
    
}
//字典轉(zhuǎn)為Json字符串
- (NSString *)dictionaryToJson:(NSDictionary *)dic
{
    NSError *error = nil;
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:&error];
    return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}

這里就是做封包處理了:

在這里主要是發(fā)了幾條消息和一個圖片給ClientB熙掺。
我們定義了一個headDic未斑,這個是我們數(shù)據(jù)包的頭部,里面裝了這個數(shù)據(jù)包的大小和類型信息币绩,自身客戶端ID蜡秽,和目標(biāo)客戶端ID(當(dāng)然,你可以裝更多的其他標(biāo)識信息缆镣。)然后我們把它轉(zhuǎn)成了json芽突,最后轉(zhuǎn)成data。
然后我們把這個head拼在最前面董瞻,接著拼了一個:

[GCDAsyncSocket CRLFData]

這個是什么呢寞蚌?其實(shí)它就是一個\r\n。我們用它來做頭部的邊界钠糊。(又或者我們可以規(guī)定一個固定的頭部長度挟秤,來作為邊界)。
最后我們把真正的數(shù)據(jù)包給拼接上抄伍。

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

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

//斷開連接調(diào)用
-(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;
        //斷開
    }
    
}

客戶端基本就是這些東西了秋度,至于對應(yīng)的解包處理我想放到服務(wù)端來講,原理都是一樣的钱床。
???休息下荚斯,不知道有幾個同學(xué)能看到這里,如果你看到這里說明你是一個很有耐心的程序員了诞丽,不知道各位能看到這里的同學(xué)有沒有一些混亂鲸拥,個人語音能力有限,實(shí)在抱歉僧免。還是看代碼清晰刑赶,其實(shí)客戶端這一部分就做三件事:1.連接服務(wù)器,維持心跳懂衩。2.封包撞叨,通過服務(wù)器給指定客戶端發(fā)送消息金踪。3接收服務(wù)器消息。下面我們將開始服務(wù)端的部分??


3.服務(wù)端:

老規(guī)矩牵敷,貼代碼

.h

#import <Foundation/Foundation.h>


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

@interface ClientA : NSObject
@property(nonatomic,copy)clientAMSG clientAmsg;
+(id)sharClineA;
/*連接服務(wù)器**/
-(BOOL)connect;
/*給B發(fā)消息**/
-(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;
}
//監(jiān)控端口
-(void)openSerVice{
    
    NSError *error;
    BOOL sucess = [self.serve acceptOnPort:8088 error:&error];
    if (sucess) {
        NSLog(@"%@",[NSString stringWithFormat:@"%@---監(jiān)聽端口成功,等待客戶端請求連接...",self.class]);
        if (self.severAmsg) {
            self.severAmsg([NSString stringWithFormat:@"%@---監(jiān)聽端口成功,等待客戶端請求連接...",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;
    }
    //先讀取到當(dāng)前數(shù)據(jù)包頭部信息
    if (!client.currentPacketHead) {
        client.currentPacketHead = [NSJSONSerialization
                                    JSONObjectWithData:data
                                    options:NSJSONReadingMutableContainers
                                    error:nil];
        if (!client.currentPacketHead) {
            NSLog(@"error:當(dāng)前數(shù)據(jù)包的頭為空");
            if (self.severAmsg) {
                self.severAmsg(@"error:當(dāng)前數(shù)據(jù)包的頭為空");
            }
            //斷開這個socket連接或者丟棄這個包的數(shù)據(jù)進(jìn)行下一個包的讀取
            //....
            return;
        }
        NSUInteger packetLength = [client.currentPacketHead[@"size"] integerValue];
        //讀到數(shù)據(jù)包的大小
        [sock readDataToLength:packetLength withTimeout:-1 tag:0];
        return;
    }
    //正式的包處理
    NSUInteger packetLength = [client.currentPacketHead[@"size"] integerValue];
    //說明數(shù)據(jù)有問題
    if (packetLength <= 0 || data.length != packetLength) {
        NSString *msg = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"error:當(dāng)前數(shù)據(jù)包數(shù)據(jù)大小不正確(%@)",msg);
        if (self.severAmsg) {
            self.severAmsg([NSString stringWithFormat:@"error:當(dāng)前數(shù)據(jù)包數(shù)據(jù)大小不正確(%@)",msg]);
        }
        return;
    }
    //分配ID
    NSString *clientID=client.currentPacketHead[@"CinentID"];
    client.clientID=clientID;
    NSString *targetID=client.currentPacketHead[@"targetID"];
    NSString *type = client.currentPacketHead[@"type"];
    
    
    
    
    /*
     *服務(wù)端可以不解析內(nèi)容毛肋,直接轉(zhuǎn)發(fā)出去怨咪,這里只是想看看打印消息
     **/
    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) {
        //這里找不到目標(biāo)客戶端,可以把數(shù)據(jù)保存起來润匙,等待目標(biāo)客戶端上線诗眨,再轉(zhuǎn)發(fā)出去,這里就不做了孕讳,感興趣的同學(xué)自己可以試一試
        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(@"當(dāng)前在線用戶個數(shù):%ld",self.arrayClient.count);
}

- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
    if (self.severAmsg) {
        self.severAmsg([NSString stringWithFormat:@"%@---數(shù)據(jù)發(fā)送成功.....",self.class]);
    }
    NSLog(@"%@",[NSString stringWithFormat:@"%@---數(shù)據(jù)發(fā)送成功.....",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];
    //第二個參數(shù)匠楚,請求超時時間
    [clientSocket writeData:mData withTimeout:-1 tag:0];
    
}
//字典轉(zhuǎn)為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 循環(huán)檢測客戶端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


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

1.跟客戶端一樣芋簿,先做單例,構(gòu)造隊列初始化服務(wù)端Socket蟀苛,設(shè)置代理益咬。

+(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.監(jiān)聽本地端口

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

3.接收到新的socket連接到本地端口,會觸發(fā)代理調(diào)用

- (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連接后,我們創(chuàng)建一個Client 類型的對象梅鹦,將當(dāng)前socket交給這個對象裆甩,再將這個Client用一個可變數(shù)組self.clientsArray保存起來。(這個數(shù)組用來保存所有連接到服務(wù)器的socket對應(yīng)創(chuàng)建的Client對象齐唆,后面會利用它來處理心跳嗤栓,轉(zhuǎn)發(fā),和用戶調(diào)度)箍邮。然后讓當(dāng)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屬性: 對應(yīng)每個客戶端連接過來的scocket锭弊;
timeOfSocket屬性: 對應(yīng)每個客戶端最后和服務(wù)器交互時間堪澎;
currentPacketHead屬性: 用來儲存用戶數(shù)據(jù)包報頭;
clientID屬性: 對應(yīng)每個客戶端分配到的ID味滞;

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

- (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;
    }
    //先讀取到當(dāng)前數(shù)據(jù)包頭部信息
    if (!client.currentPacketHead) {
        client.currentPacketHead = [NSJSONSerialization
                                    JSONObjectWithData:data
                                    options:NSJSONReadingMutableContainers
                                    error:nil];
        if (!client.currentPacketHead) {
            NSLog(@"error:當(dāng)前數(shù)據(jù)包的頭為空");
            if (self.severAmsg) {
                self.severAmsg(@"error:當(dāng)前數(shù)據(jù)包的頭為空");
            }
            //斷開這個socket連接或者丟棄這個包的數(shù)據(jù)進(jìn)行下一個包的讀取
            //....
            return;
        }
        NSUInteger packetLength = [client.currentPacketHead[@"size"] integerValue];
        //讀到數(shù)據(jù)包的大小
        [sock readDataToLength:packetLength withTimeout:-1 tag:0];
        return;
    }
    //正式的包處理
    NSUInteger packetLength = [client.currentPacketHead[@"size"] integerValue];
    //說明數(shù)據(jù)有問題
    if (packetLength <= 0 || data.length != packetLength) {
        NSString *msg = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"error:當(dāng)前數(shù)據(jù)包數(shù)據(jù)大小不正確(%@)",msg);
        if (self.severAmsg) {
            self.severAmsg([NSString stringWithFormat:@"error:當(dāng)前數(shù)據(jù)包數(shù)據(jù)大小不正確(%@)",msg]);
        }
        return;
    }
    //分配ID
    NSString *clientID=client.currentPacketHead[@"CinentID"];
    client.clientID=clientID;
    NSString *targetID=client.currentPacketHead[@"targetID"];
    NSString *type = client.currentPacketHead[@"type"];
    
    
    
    
    /*
     *服務(wù)端可以不解析內(nèi)容樱蛤,直接轉(zhuǎn)發(fā)出去钮呀,這里只是想看看打印消息
     **/
    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) {
        //這里找不到目標(biāo)客戶端,可以把數(shù)據(jù)保存起來昨凡,等待目標(biāo)客戶端上線爽醋,再轉(zhuǎn)發(fā)出去,這里就不做了便脊,感興趣的同學(xué)自己可以試一試
        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];
}

這一塊算是服務(wù)端核心的部分了蚂四,數(shù)據(jù)包的拆解,分發(fā)哪痰,用戶調(diào)度遂赠,心跳刷新都在這里處理。
大體思路:監(jiān)聽到有[GCDAsyncSocket CRLFData] 邊界的數(shù)據(jù)包妒御,調(diào)用didReadData方法解愤,在這個方法里。先根據(jù)當(dāng)前sock找到self.clientsArray(存儲所有client和相關(guān)信息)里找到對應(yīng)的Client乎莉,在找的過程中送讲,將找到的對應(yīng)的Client的timeOfSocket(可以理解為時間戳)刷新。
判斷對應(yīng)Client的currentPacketHead是否為nil惋啃。如果為空哼鬓,將收到的data轉(zhuǎn)化為字典賦值給currentPacketHead。此時currentPacketHead的內(nèi)容應(yīng)該為

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

然后异希,通知當(dāng)前socket來接收size對應(yīng)長度的數(shù)據(jù)包。
理想狀態(tài)下(這里會有并發(fā)過程绒瘦,這里先提一下称簿,后面解釋)該sockt會去讀到size長度的內(nèi)容包,檢測下內(nèi)容包的合理性惰帽,如果合理憨降。我們就取到正確的內(nèi)容了,然后该酗,根據(jù)根據(jù)收到的內(nèi)容給此Client分配clientID授药,根據(jù)報文目的客戶端id,在self.clientsArray中找到對應(yīng)Client所對應(yīng)的socket呜魄,再將內(nèi)容封裝轉(zhuǎn)發(fā)悔叽。客戶端的解析過程爵嗅,和這里大體相似娇澎,不表。

上面說到用戶并發(fā)操骡,因?yàn)樗械目蛻舳硕紩瑫r發(fā)送心跳包或用戶消息九火,都會調(diào)用didReadData方法赚窃,比如說用戶A對應(yīng)的socket讀取到報文頭部,要去讀報文內(nèi)容的時候岔激,用戶B對應(yīng)的socket也同時調(diào)用didReadData方法勒极,那么會照成我們接收到數(shù)據(jù)處理混亂。所以虑鼎,我們封裝一個Client來對應(yīng)處理每個客戶端的socket事務(wù)辱匿,通過定位標(biāo)記,讓他們并發(fā)工作炫彩,各自維持自己處理數(shù)據(jù)的邏輯匾七,互不干擾。

5.心跳檢測

//開啟線程 啟動runloop 循環(huán)檢測客戶端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;
}

這一塊很簡單江兢,做法是昨忆,沒收到客戶端發(fā)過來的報文,就更新下杉允,客戶端最后交互時間(timeOfSocket)邑贴,然后,每隔一段時間檢測self.clientsArray每個Client對應(yīng)的timeOfSocket叔磷,和目前時間對比拢驾,如果超出預(yù)先設(shè)定的失活時間,就斷開此Client對應(yīng)的scocket改基,殺死客戶端繁疤。


寫在最后

???到這里,就全部講完了秕狰,文章篇幅比較長稠腊,但大多是代碼部分,對CocoaAsyncSocket有了解的同學(xué)鸣哀,可以直接看代碼麻养,比較簡單,可能很多同學(xué)看到這種又臭又長的文章诺舔,會選擇直接略過。嘴拙备畦,總想用更多的文字來解釋低飒,還是怕自己表達(dá)的不夠清晰,水平有限懂盐,文章和代碼中多有漏洞褥赊,歡迎指出,內(nèi)心忐忑莉恼,只愿不要誤人子弟就好拌喉。同時也希望能拋磚引玉速那,給有需要的同學(xué)一些思路和啟發(fā)。??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末尿背,一起剝皮案震驚了整個濱河市端仰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌田藐,老刑警劉巖荔烧,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異汽久,居然都是意外死亡鹤竭,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門景醇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來臀稚,“玉大人,你說我怎么就攤上這事三痰“伤拢” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵酒觅,是天一觀的道長撮执。 經(jīng)常有香客問我,道長舷丹,這世上最難降的妖魔是什么抒钱? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮颜凯,結(jié)果婚禮上谋币,老公的妹妹穿的比我還像新娘。我一直安慰自己症概,他們只是感情好蕾额,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著彼城,像睡著了一般诅蝶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上募壕,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天调炬,我揣著相機(jī)與錄音,去河邊找鬼舱馅。 笑死缰泡,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的代嗤。 我是一名探鬼主播棘钞,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼缠借,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了宜猜?” 一聲冷哼從身側(cè)響起泼返,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎宝恶,沒想到半個月后符隙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡垫毙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年霹疫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片综芥。...
    茶點(diǎn)故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡丽蝎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出膀藐,到底是詐尸還是另有隱情屠阻,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布额各,位于F島的核電站国觉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏虾啦。R本人自食惡果不足惜麻诀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望傲醉。 院中可真熱鬧蝇闭,春花似錦、人聲如沸硬毕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吐咳。三九已至逻悠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間韭脊,已是汗流浹背蹂风。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留乾蓬,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓慎恒,卻偏偏與公主長得像任内,于是被迫代替她去往敵國和親撵渡。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容