公司項(xiàng)目棄用了第三方的IM埠忘,使用socket來實(shí)現(xiàn)。我在網(wǎng)上看了一些資料,決定使用CocoaAsyncSocket來實(shí)現(xiàn)莹妒∶總體來說,這個框架很輕量級旨怠,而且非常的好用渠驼、簡單易懂。
cocoaAsyncSocket github 地址
下面說一下它的用法:
初始化socket
//創(chuàng)建一個socket對象
if(!_socket) {
dispatch_queue_t queue = dispatch_queue_create("SocketQueue", NULL);
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:queue];
}else{
[_socket setDelegate:self];
}
//連接
NSError *error = nil;
[self.socket connectToHost:addr onPort:port withTimeout:TIME_OUT error:&error]
這里創(chuàng)建socket的時傳入的隊(duì)列我自己創(chuàng)建了一個串行鉴腻,你也可以加入主隊(duì)列迷扇,但是不能是并發(fā)隊(duì)列。TIME_OUT 為超時時間爽哎,-1表示不設(shè)置超時蜓席,你也可以根據(jù)實(shí)際情況設(shè)置超時限制。
發(fā)送socket消息
- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag
關(guān)閉socket
[self.socket disconnect];
[self.socket setDelegate:nil];
代理方法
#pragma mark -socket的代理
#pragma mark 連接成功
-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"Socket連接成功");
}
#pragma mark 斷開連接
-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
NSLog(@"Socket連接斷開");
}
#pragma mark 數(shù)據(jù)發(fā)送成功
-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
NSLog(@"Socket發(fā)送成功");
[socket readDataWithTimeout:WRITE_TIME_OUT tag:tag];//手動調(diào)用讀取剛剛發(fā)送的數(shù)據(jù)
}
#pragma mark 讀取數(shù)據(jù)
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
//在這里處理接收到的socket數(shù)據(jù)
[self.socket readDataWithTimeout:READ_TIME_OUT buffer:nil bufferOffset:0 maxLength:MAX_BUFFER tag:0];
}
以上就是CocoaAsyncSocket的基本用法课锌,是不是很簡單明了厨内。實(shí)際開發(fā)中,我們會在這個的基礎(chǔ)上添加一些其他的邏輯比如:連接成功后與服務(wù)器做權(quán)限校驗(yàn)渺贤,發(fā)送數(shù)據(jù)加密雏胃,接收數(shù)據(jù)解碼、心跳等癣亚,這些和服務(wù)端協(xié)商好就可以了丑掺。本以為到此結(jié)束的時候,發(fā)現(xiàn)了一個問題:粘包述雾。
為什么會出現(xiàn)粘包 街州?
TCP(transport control protocol,傳輸控制協(xié)議)是面向連接的玻孟,面向流的唆缴,提供高可靠性服務(wù)。收發(fā)兩端(客戶端和服務(wù)器端)都要有一一成對的socket黍翎,因此面徽,發(fā)送端為了將多個發(fā)往接收端的包,更有效的發(fā)到對方匣掸,使用了優(yōu)化方法(Nagle算法)趟紊,將多次間隔較小且數(shù)據(jù)量小的數(shù)據(jù),合并成一個大的數(shù)據(jù)塊碰酝,然后進(jìn)行封包霎匈。這樣,接收端送爸,就難于分辨出來了铛嘱,必須提供科學(xué)的拆包機(jī)制暖释。 即面向流的通信是無消息保護(hù)邊界的。
UDP(user datagram protocol墨吓,用戶數(shù)據(jù)報(bào)協(xié)議)是無連接的球匕,面向消息的,提供高效率服務(wù)帖烘。不會使用塊的合并優(yōu)化算法亮曹,, 由于UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區(qū))采用了鏈?zhǔn)浇Y(jié)構(gòu)來記錄每一個到達(dá)的UDP包蚓让,在每個UDP包中就有了消息頭(消息來源地址乾忱,端口等信息),這樣历极,對于接收端來說窄瘟,就容易進(jìn)行區(qū)分處理了。 即面向消息的通信是有消息保護(hù)邊界的趟卸。
拆包
思路:預(yù)先設(shè)置一個全局的變量NSMutableData *cacheData蹄葱,每收到一個data數(shù)據(jù)先不馬上去解析它,而是把它拼接到cacheData中去锄列。這樣的話图云,如果包不完整,就可以保留數(shù)據(jù)不做處理等下一次拼接邻邮,然后處理;如果多個包粘一起竣况,可以循環(huán)處理。
區(qū)分每個包:每個包分為 head 和 body 兩部分筒严,當(dāng)多個包粘在一起時丹泉,我們從包的head 中取出這個包的長度。然后從cacheData中截取對應(yīng)長度鸭蛙,然后再用同樣的方法取下一個摹恨,就這樣一個一個的拆分,如果不夠讀取數(shù)據(jù)等下一次拼接娶视。
head 中的信息是和服務(wù)端協(xié)議的晒哄,按照具體情況定
代碼:
#pragma mark 讀取數(shù)據(jù)
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
//由于獲取data數(shù)據(jù)可能為多個數(shù)據(jù)的拼接或是不完整 先加入cachedata中再解析
[self analyticalData:data];
[self.socket readDataWithTimeout:READ_TIME_OUT buffer:nil bufferOffset:0 maxLength:MAX_BUFFER tag:0];
}
#pragma mark 解析數(shù)據(jù)
- (void)analyticalData:(NSData *)data {
if (data.length > 0) {
[_cacheData appendData:data];
}
//我們這邊定義的head的長度為16個字節(jié),按具體情況肪获。如果大于16個字節(jié)就循環(huán)取包
while (_cacheData.length > 16) {
NSData *head = [_cacheData subdataWithRange:NSMakeRange(0, 16)];//取得頭部數(shù)據(jù)
//head中定義前4個字節(jié)為包的長度
NSData *lengthData = [head subdataWithRange:NSMakeRange(0, 4)];
NSInteger packageLength = [[[NSString alloc] initWithData:lengthData encoding:NSUTF8StringEncoding] integerValue];
//從head中取出和服務(wù)器協(xié)商的消息協(xié)議
NSData *operation = [head subdataWithRange:NSMakeRange(8, 4)];;
//如果包的長度沒有實(shí)際包的長度寝凌,讀取數(shù)據(jù)等下次拼接
if (packageLength > _cacheData.length) {
[self.socket readDataWithTimeout:-1 tag:0];
return;
}
取出boday
NSData *bodyData = [_cacheData subdataWithRange:NSMakeRange(16, packageLength)];
//從cacheData中去掉已讀取的數(shù)據(jù)
_cacheData = [NSMutableData dataWithData:[_cacheData subdataWithRange:NSMakeRange(packageLength, _cacheData.length - packageLength)]];
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//異步處理數(shù)據(jù),根據(jù)不同數(shù)據(jù)類型處理不同的事件
[weakSelf functionWithOperation:operation jsonData:bodyData];
});
}
}
一些問題:
讀取數(shù)據(jù)
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
在這個方法中要手動調(diào)用readDataWithTimeout方法孝赫,不然會出現(xiàn)只有第一次讀到數(shù)據(jù)硫兰。讀取消息的TIME_OUT 要設(shè)置為-1,不然會頻繁的斷開寒锚。
因?yàn)槲覀兊捻?xiàng)目中解析數(shù)據(jù)要先將NSData 轉(zhuǎn)成byte數(shù)組劫映,然后進(jìn)去解碼。會用到Byte *byte = malloc() 方法刹前,malloc 方法會在堆中分配內(nèi)存泳赋,每次收到消息都要調(diào)用,內(nèi)存就會一直漲需要使用完byte后要調(diào)用free(byte)釋放喇喉。