寫在開始之前
這篇文章的由來是作者以前在看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.主體部分
前四個文件為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ā)。??