上篇已經(jīng)準(zhǔn)備好了基本的條件叠纷,接下來就是如何與服務(wù)器之間建立
一條長(zhǎng)連接刻帚,以及如何封包
、解包
涩嚣;
新建LXSocketManager
類崇众,用于對(duì)CocoaAsyncSocket
進(jìn)行封裝,這樣以后如果更換另外的socket庫(kù)航厚,只需要修改該文件即可顷歌。pod下來我們發(fā)現(xiàn)CocoaAsyncSocket
有兩個(gè)文件GCDAsyncSocket.h
、GCDAsyncUdpSocket.h
幔睬,前者基于TCP
而后者基于UDP
眯漩,這里選用前者。
// LXSocketManager.h
typedef NS_ENUM(NSInteger, LXSocketStatus) {
LXSocketStatusUnknown = -1,
LXSocketStatusUnconnect,
LXSocketStatusConnect,
};
@class LXSocketManager;
@protocol LXSocketManagerDelegate <NSObject>
@optional
- (void)socketWillSendHeartBeat;
- (void)socket:(LXSocketManager *)socket didConnect:(NSString *)server;
- (void)socket:(LXSocketManager *)socket didReceive:(Message *)message;
@end
@interface LXSocketManager : NSObject
@property (nonatomic, assign, readonly) LXSocketStatus connectStatus;
@property (nonatomic, weak) id<LXSocketManagerDelegate> delegate;
// 連接
- (void)connectTo:(NSString *)host onPort:(uint16_t)port;
// 斷開連接
- (void)disconnect;
// 重連
- (void)forceReconnect;
// 發(fā)送數(shù)據(jù)
- (void)sendData:(NSData *)data;
// 開始發(fā)送心跳
- (void)startHeartBeat;
@end
點(diǎn)開GCDAsyncSocket.h
文件溪窒,可以看到以下方法
// 初始化
- (instancetype)init;
- (instancetype)initWithSocketQueue:(nullable dispatch_queue_t)sq;
- (instancetype)initWithDelegate:(nullable id<GCDAsyncSocketDelegate>)aDelegate delegateQueue:(nullable dispatch_queue_t)dq;
- (instancetype)initWithDelegate:(nullable id<GCDAsyncSocketDelegate>)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq;
// 連接
- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr;
- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr;
// 斷開連接
- (void)disconnect;
首先創(chuàng)建客戶端socket
socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
接下來連接服務(wù)端的socket
[socket connectToHost:host onPort:port error:&error];
怎么知道是否連接成功坤塞,如何接收數(shù)據(jù)冯勉,socket中斷等消息,查看GCDAsyncSocketDelegate
代理摹芙,會(huì)看到以下方法
// 已連接
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
// 斷開連接
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
// 接收數(shù)據(jù)
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
// LXSocketManager.m
#pragma mark - GCDAsyncSocketDelegate
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
LXLog(@"==============socket did connect host: %@, port: %hu==============", host, port);
//
[self pullMesasge];
//
_connectStatus = LXSocketStatusConnect;
if ([self.delegate respondsToSelector:@selector(socket:didConnect:)]) {
NSString *server = [NSString stringWithFormat:@"%@:%d", host, port];
[self.delegate socket:self didConnect:server]; // 開啟心跳等操作
}
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
LXLog(@"==============socket did disconnect==============");
// 停止心跳
[self stopHeartBeat];
_connectStatus = LXSocketStatusUnconnect;
// 重連
[self reconnectIfNeed];
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
[receiveData appendData:data];
// 讀取包內(nèi)容長(zhǎng)度
int32_t headLength = 0;
int32_t contentLength = [self getContentLength:receiveData withHeadLength:&headLength];
if (contentLength <= 0) {
[self pullMesasge];
return;
}
// 還未接收到一個(gè)完整的數(shù)據(jù)
if (headLength + contentLength > [receiveData length]) {
// 繼續(xù)接收下一條消息
[self pullMesasge];
return;
}
// 解析
[self parseContentDataWithHeadLength:headLength withContentLength:contentLength];
[self pullMesasge];
}
- (void)pullMesasge {
[socket readDataWithTimeout:-1 tag:110];
}
心跳
客戶端每隔一段時(shí)間發(fā)送一個(gè)數(shù)據(jù)包給服務(wù)端告知服務(wù)端我還活著灼狰,這就是心跳。心跳的數(shù)據(jù)需要與服務(wù)端約定浮禾;當(dāng)服務(wù)端在一定時(shí)間內(nèi)沒有收到心跳包交胚,就會(huì)斷開連接,客戶端會(huì)收到斷開連接的回調(diào)盈电,然后進(jìn)入重連機(jī)制蝴簇。
重連機(jī)制
當(dāng)斷開連接后,每過一段時(shí)間T
重連匆帚。在這里時(shí)間采用的是指數(shù)增長(zhǎng)的熬词,并且最大次數(shù)是4次。
封包
先將Message.proto
文件編譯成objc文件吸重,然后直接調(diào)用對(duì)象delimitedData
方法,接著就可以用socket
發(fā)送我們的數(shù)據(jù)包了
NSData *data = [message delimitedData];
解包
當(dāng)我們讀取數(shù)據(jù)的時(shí)候嚎幸,正常
的情況是收到一個(gè)個(gè)完整的數(shù)據(jù)包颜矿,然后再反序列化成我們的ProtoBuf對(duì)象
,但有時(shí)候會(huì)出現(xiàn)粘包
替废、斷包
的情況教沾。如何處理堪唐?一個(gè)數(shù)據(jù)包
由包頭
和包體
組成枢赔,包頭
有這個(gè)數(shù)據(jù)包的長(zhǎng)度信息
,因此先獲取該數(shù)據(jù)包的長(zhǎng)度
,然后根據(jù)長(zhǎng)度去截取
即可。具體代碼如下副瀑。
/** 關(guān)鍵代碼:獲取data數(shù)據(jù)的內(nèi)容長(zhǎng)度和頭部長(zhǎng)度: index --> 頭部占用長(zhǎng)度 (頭部占用長(zhǎng)度1-4個(gè)字節(jié)) */
- (int32_t)getContentLength:(NSData *)data withHeadLength:(int32_t *)index {
int8_t tmp = [self readRawByte:data headIndex:index];
if (tmp >= 0) return tmp;
int32_t result = tmp & 0x7f;
if ((tmp = [self readRawByte:data headIndex:index]) >= 0) {
result |= tmp << 7;
} else {
result |= (tmp & 0x7f) << 7;
if ((tmp = [self readRawByte:data headIndex:index]) >= 0) {
result |= tmp << 14;
} else {
result |= (tmp & 0x7f) << 14;
if ((tmp = [self readRawByte:data headIndex:index]) >= 0) {
result |= tmp << 21;
} else {
result |= (tmp & 0x7f) << 21;
result |= (tmp = [self readRawByte:data headIndex:index]) << 28;
if (tmp < 0) {
for (int i = 0; i < 5; i++) {
if ([self readRawByte:data headIndex:index] >= 0) {
return result;
}
}
result = -1;
}
}
}
}
return result;
}
/** 讀取字節(jié) */
- (int8_t)readRawByte:(NSData *)data headIndex:(int32_t *)index{
if (*index >= data.length) return -1;
*index = *index + 1;
return ((int8_t *)data.bytes)[*index - 1];
}
/** 解析二進(jìn)制數(shù)據(jù):NSData --> 自定義模型對(duì)象 */
- (void)parseContentDataWithHeadLength:(int32_t)headL withContentLength:(int32_t)contentL{
NSRange range = NSMakeRange(0, headL + contentL); //本次解析data的范圍
NSData *data = [receiveData subdataWithRange:range]; //本次解析的data
GPBCodedInputStream *inputStream = [GPBCodedInputStream streamWithData:data];
NSError *error;
Message *obj = [Message parseDelimitedFromCodedInputStream:inputStream extensionRegistry:nil error:&error];
if (!error){
if (obj) {
//保存解析正確的模型對(duì)象
if ([self.delegate respondsToSelector:@selector(socket:didReceive:)]) {
[self.delegate socket:self didReceive:obj];
}
}
[receiveData replaceBytesInRange:range withBytes:NULL length:0]; //移除已經(jīng)解析過的data
}
if (receiveData.length < 1) return;
//對(duì)于粘包情況下被合并的多條消息,循環(huán)遞歸直至解析完所有消息
headL = 0;
contentL = [self getContentLength:receiveData withHeadLength:&headL];
if (headL + contentL > receiveData.length) return; //實(shí)際包不足解析惋鹅,繼續(xù)接收下一個(gè)包
[self parseContentDataWithHeadLength:headL withContentLength:contentL]; //繼續(xù)解析下一條
}
監(jiān)控網(wǎng)絡(luò)狀態(tài)
因?yàn)槭且苿?dòng)設(shè)備则酝,網(wǎng)絡(luò)狀態(tài)的改變是非常頻繁的;所以需要監(jiān)控網(wǎng)絡(luò)狀態(tài)來做出相應(yīng)的操作负饲。這里選擇的是RealReachability
第三方庫(kù)
// 開啟監(jiān)聽
[GLobalRealReachability startNotifier];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkChange:) name:kRealReachabilityChangedNotification object:nil];
#pragma mark - network reachability
//
- (void)networkChange:(NSNotification *)notif {
RealReachability *reachability = (RealReachability *)notif.object;
ReachabilityStatus status = [reachability currentReachabilityStatus];
switch (status) {
case RealStatusNotReachable:
case RealStatusUnknown: {
LXLog(@"network unknown or no reachable");
if (self.socket.connectStatus == LXSocketStatusConnect) {
[self.socket disconnect];
}
break;
}
case RealStatusViaWiFi:
case RealStatusViaWWAN: {
LXLog(@"wifi or wwan");
if (self.socket.connectStatus != LXSocketStatusConnect) {
// 重連
[self.socket forceReconnect];
}
break;
}
}
}
參考文章
1堤魁、ProtoBuf
粘包、斷包處理 https://www.cnblogs.com/tandaxia/archive/2017/04/16/6718695.html