1灾前、AsyncSocket介紹
?如果需要在項目中像QQ微信一樣做到即時通訊寻咒,必須使用socket通訊烤低。
iOS中Socket編程的方式:
? ? BSD ?Socket :
BSD Socket 是?UNIX 系統(tǒng)中通用的網(wǎng)絡(luò)接口刑顺,它不僅支持各種不同的網(wǎng)絡(luò)類型柔纵,而且也是一種內(nèi)部進(jìn)程之間的通信機制缔杉。而iOS系統(tǒng)其實本質(zhì)就是?UNIX ,所以可以用搁料,但是比較復(fù)雜或详。
? ??CFSocket ?:
CFSocket是蘋果提供給我們的使用?Socket 的方式,但是用起來還是會不太順手郭计。當(dāng)然想使用的話霸琴,可以細(xì)細(xì)研究一下。
??AsyncSocket :
第三方開源庫昭伸,首選方式梧乘,也是在開發(fā)項目中經(jīng)常會用的。
? ? 選擇AsyncSocket的原因:
iphone 的 ?CFNetwork編程比較復(fù)雜庐杨。使用?AsyncSocket 開源庫來開發(fā)相對較簡單选调,幫助我們封裝了很多東西。
環(huán)境:
??下載AsyncSocket:
https://github.com/robbiehanson/CocoaAsyncSocket 類庫灵份,將?RunLoop 文件夾下的?AsyncSocket.h仁堪、AsyncSocket.m、??AsyncUdpSocket.h填渠、?AsyncUdpSocket.m 文件拷貝到自己的項目中弦聂,添加?CFNetwork.framework, ?再使用?socket 的文件頭
? ? #import <sys/socket.h>
? ??#import <netinet/in.h>
? ? #import <arpa/inet.h>
? ? #import <unistd.h>
2鸟辅、AsyncSocket詳解
在實際開發(fā)中,主要的任務(wù)是開發(fā)客戶端莺葫。所以下面主要詳解客戶端的整個連接建立過程匪凉,以及在說明時候回調(diào)哪些函數(shù)。
常用方法:
1捺檬、建立連接
- (int)connectServer:(NSString *)hostIP port:(int)hostPort
2再层、連接成功后,會回調(diào)的函數(shù)
- (void)onSocket:(AsyncSocket *)sockdidConnectToHost:(NSString *)host port:(UInt16)port
3欺冀、發(fā)送數(shù)據(jù)
- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;
4树绩、接受數(shù)據(jù)
-(void)onSocket:(AsyncSocket *)sockdidReadData:(NSData *)data withTag:(long)tag
5萨脑、斷開連接
- (void)onSocket:(AsyncSocket *)sockwillDisconnectWithError:(NSError *)err
- (void)onSocketDidDisconnect:(AsyncSocket *)sock
主要就是上述的幾個方法隐轩,只是說在真正開發(fā)當(dāng)中,很可能我們在收發(fā)數(shù)據(jù)的時候渤早,我們收發(fā)的數(shù)據(jù)并不僅僅是一個字符串包裝成?NSData 即可职车,我們很可能會發(fā)送結(jié)構(gòu)體等類型,這個時候我們就需要和服務(wù)器端的人員協(xié)作來開發(fā):定義怎樣的結(jié)構(gòu)體
3鹊杖、使用方法詳解
即時通訊最大的特點就是實時性悴灵,基本感覺不到延時或是掉線,所以必須對?socket 的連接進(jìn)行監(jiān)視與檢測骂蓖,在斷線時進(jìn)行重新連接积瞒,如果用戶退出登錄,要將?socket 手動關(guān)閉登下,否則對服務(wù)器會造成一定的負(fù)荷茫孔。
一般來說,一個用戶(對于iOS來說也就是我們的項目中)只能有一個正在連接的?socket被芳,所以這個?socket 變量必須是全局的缰贝,這里可以考慮使用單例或是?AppDelegate 進(jìn)行數(shù)據(jù)共享,首選使用單例畔濒。如果對一個已經(jīng)連接的?socket 對象再次進(jìn)行連接操作剩晴,會拋出異常(不可對已經(jīng)連接的socket進(jìn)行連接)程序崩潰,所以在連接?socket 之前要對?socket 對象的連接狀態(tài)進(jìn)行判斷侵状。
使用?socket 進(jìn)行即時通訊還有一個必須的操作赞弥,即時服務(wù)器發(fā)送心跳包,每隔一段時間對服務(wù)器發(fā)送長連接指令(指令不唯一趣兄,由服務(wù)器指定嗤攻,包括使用?socket 發(fā)送消息,發(fā)送的數(shù)據(jù)和格式都是由服務(wù)器指定)诽俯,如果沒有收到服務(wù)器的返回消息妇菱,?AsyncSocket 會得到失去連接的消息承粤,我們可以在失去連接的回調(diào)方法里進(jìn)行重新連接。
聲明socket變量:
@property (nonatomic, strong)?AsyncSocket?*socket; // socket
@property (nonatomic, copy ) NSString *socketHost; // socket的Host
@property (nonatomic, assign) UInt16 socketPort; // socket的prot
? ?連接(長連接)
-(void)socketConnectHost;// socket連接闯团,
連接時host與port都是由服務(wù)器指定辛臊。
?// socket連接
-(void)socketConnectHost{
self.socket = [[AsyncSocket?alloc] initWithDelegate:self];
NSError *error = nil;
[self.socket connectToHost:self.socketHost onPort:self.socketPort withTimeout:3 error:&error];
}
心跳
心跳通過計時器來實現(xiàn)?
@property (nonatomic, retain) NSTimer *connectTimer; // 計時器
實現(xiàn)連接成功回調(diào)的方法,并在此方法中初始化定時器房交,定時向服務(wù)器發(fā)送一次請求彻舰,保持連接
#pragma mark - 連接成功回調(diào)
-(void)onSocket:(AsyncSocket?*)sock didConnectToHost:(NSString *)host port:(UInt16)port {
NSLog(@"socket連接成功"); // 每隔30s像服務(wù)器發(fā)送心跳包
self.connectTimer = [NSTimer scheduledTimerWithTimeInterval:30 target:self selector:@selector(longConnectToSocket) userInfo:nil repeats:YES];
// 在longConnectToSocket方法中進(jìn)行長連接需要向服務(wù)器發(fā)送的訊息
[self.connectTimer fire];
?}
斷開連接:
失去連接由幾種情況,服務(wù)器斷開候味,用戶主動cut刃唤,還可能有如QQ其他設(shè)備登錄被掉線的情況,不管那種情況白群,我們都能收到?socket 回調(diào)方法返回給我們的訊息尚胞,如果是用戶退出登錄或是程序退出而需要手動cut,我們在cut前對?socket 的?userData 賦予一個值來標(biāo)記為用戶退出帜慢,這樣我們可以在收到斷開信息時判斷究竟是什么原因?qū)е碌牡艟€
在.h文件中聲明一個枚舉類型
enum{
SocketOfflineByServer,//服務(wù)器掉線笼裳,默認(rèn)為0
SocketOfflineByUser, //用戶主動cut
};
定義并實現(xiàn)斷開方法
-(void)cutOffSocket; // 斷開socket連接
// 切斷socket
-(void)cutOffSocket{
self.socket.userData = SocketOfflineByUser;// 聲明是由用戶主動切斷
[self.connectTimer invalidate];
[self.socket disconnect];
}
? ? 重連
實現(xiàn)代理方法
-(void)onSocketDidDisconnect:(AsyncSocket?*)sock {
? ? NSLog(@"sorry the connect is failure %ld",sock.userData);
? ? ? ? ? if (sock.userData == SocketOfflineByServer) {
? ? ? ? ? ? ? ?// 服務(wù)器掉線,重連
? ? ? ? ? ? ? ?[self socketConnectHost];
? ? ? ? ? ?} else if (sock.userData == SocketOfflineByUser) {
? ? ? ? ? ? ? // 如果由用戶斷開粱玲,不進(jìn)行重連
? ? ? ? ? ? ? ? return;
????? }
}
?發(fā)送數(shù)據(jù):
我們補充上文心跳連接未完成的方法
// 心跳連接
-(void)longConnectToSocket{
// 根據(jù)服務(wù)器要求發(fā)送固定格式的數(shù)據(jù)躬柬,假設(shè)為指令@"longConnect",但是一般不會是這么簡單的指令
NSString *longConnect = @"longConnect";
NSData *dataStream = [longConnect dataUsingEncoding:
NSUTF8StringEncoding];
[self.socket writeData:dataStream withTimeout:1 tag:1];
}
socket發(fā)送數(shù)據(jù)是以棧的形式存放抽减,所有數(shù)據(jù)放在一個棧中允青,存取時會出現(xiàn)粘包的現(xiàn)象,所以很多時候服務(wù)器在收發(fā)數(shù)據(jù)時是以先發(fā)送內(nèi)容字節(jié)長度卵沉,再發(fā)送內(nèi)容的形式颠锉,得到數(shù)據(jù)時也是先得到一個長度,再根據(jù)這個長度在棧中讀取這個長度的字節(jié)流偎箫,如果是這種情況木柬,發(fā)送數(shù)據(jù)時只需在發(fā)送內(nèi)容前發(fā)送一個長度,發(fā)送方法與發(fā)送內(nèi)容一樣淹办,假設(shè)長度是8
NSData *dataStream = [@8 dataUsingEncoding:NSUTF8StringEncoding];
[self.socket writeData:dataStream withTimeout:1 tag:1];
接收數(shù)據(jù):
為了能時刻接收?socket 的消息眉枕,我們在長連接方法中進(jìn)行讀取數(shù)據(jù)
[self.socket readDataWithTimeout:30 tag:0];
如果得到數(shù)據(jù),會調(diào)用回調(diào)方法
-(void)onSocket:(AsyncSocket?*)sock didReadData:(NSData *)data withTag:(long)tag {
// 對得到的data值進(jìn)行解析與轉(zhuǎn)換即可
[self.socket readDataWithTimeout:30 tag:0];
}
【備注】關(guān)于NSData對象
無論?SOCKET 收發(fā)都采用?NSData 對象怜森。
NSData 主要是帶一個?(id)data 指向的數(shù)據(jù)空間和長度?length速挑。?
NSString?轉(zhuǎn)換成NSData?對象
NSData* xmlData = [@"testdata" dataUsingEncoding:
NSUTF8StringEncoding];
NSData?轉(zhuǎn)換成NSString對象
NSData * data;
NSString *result = [[NSString alloc] initWithData:data? encoding:
NSUTF8StringEncoding];
出現(xiàn)粘包,半包 代碼處理辦法
- (void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
??while(_readBuf.length >= 10)//因為頭部固定10個字節(jié)副硅,數(shù)據(jù)長度至少要大于10個字節(jié)姥宝,我們才能得到完整的消息描述信息
??{
????NSData *head = [_readBuf subdataWithRange:NSMakeRange(0, 10)];//取得頭部數(shù)據(jù)
????NSData *lengthData = [head subdataWithRange:NSMakeRange(6, 4)];//取得長度數(shù)據(jù)
????NSInteger length = [[[NSString alloc] initWithData:lengthData encoding:NSUTF8StringEncoding] integerValue];//得出內(nèi)容長度
????NSInteger complateDataLength = length + 10;//算出一個包完整的長度(內(nèi)容長度+頭長度)
????if(_readBuf.length >= complateDataLength)//如果緩存中數(shù)據(jù)夠一個整包的長度
????{
??????NSData *data = [_readBuf subdataWithRange:NSMakeRange(0, complateDataLength)];//截取一個包的長度(處理粘包)
??????[self handleTcpResponseData:data];//處理包數(shù)據(jù)
??????//從緩存中截掉處理完的數(shù)據(jù),繼續(xù)循環(huán)
??????_readBuf = [NSMutableData dataWithData:[_readBuf subdataWithRange:NSMakeRange(complateDataLength, _readBuf.length - complateDataLength)]];
????}
????else//如果緩存中的數(shù)據(jù)長度不夠一個包的長度,則包不完整(處理半包恐疲,繼續(xù)讀取)
????{
??????[_socket readDataWithTimeout:-1 buffer:_readBuf bufferOffset:_readBuf.length tag:0];//繼續(xù)讀取數(shù)據(jù)
??????return;
????}
??}
??//緩存中數(shù)據(jù)都處理完了腊满,繼續(xù)讀取新數(shù)據(jù)
??[_socket readDataWithTimeout:-1 buffer:_readBuf bufferOffset:_readBuf.length tag:0];//繼續(xù)讀取數(shù)據(jù)
}