您的位置:首页 > 理论基础 > 计算机网络

iOS TCP的使用及粘包断包处理

2017-07-25 10:09 274 查看

概要:有关TCP的与服务器的三次握手此处就不介绍了,网上有很多基础知识,此篇主要是介绍使用TCP与服务器通信的实战项目。

一、TCP的基本使用  

使用TCP与服务器通讯,我是使用GCDAsyncSocket三方库,首先在github中下载类库,加到项目工程,或者直接使用cocopods导入
封装自己在项目中使用的TCP类库,在OGTcpClient.h中定义常用属性,socket连接和发送消息的方法,如下

#import "GCDAsyncSocket.h"
#import <Foundation/Foundation.h>

@class OGTcpClient;

enum{
SocketOfflineByServer,// 服务器掉线,默认为0
SocketOfflineByUser,  // 用户主动cut
};
typedef enum {
OGResultTypeUserLoginSuccess,
OGResultTypeUserLoginFailed,
OGResultTypeActorLoginSuccess,
OGResultTypeActorLoginFailure
}OGResultType;
typedef void(^OGResultBlock)(OGResultType type);

@interface OGTcpClient : NSObject<GCDAsyncSocketDelegate, UIAlertViewDelegate>
singleton_interface(OGTcpClient)
@property (nonatomic, strong) GCDAsyncSocket *clientSocket;
@property (nonatomic, assign) NSTimer *connectTimer;// 计时器
@property (nonatomic, assign) BOOL hasHeatBeat;
@property (nonatomic, assign) OGResultType _block;
@property (nonatomic, strong) NSMutableData *_readBuf;// 缓冲区
- (void)socketConnectHost;// socket连接
- (void)cutOffSocket; // 断开socket连接
- (void)writeData:(NSData *)data; // 发送消息
- (void)hasReadData:(NSData *)data; // 接收数据
/**向服务器发送登录确认消息,消息格式使用Protobuf,此篇未实现此方法,有关protobuf消息的序列化见下一篇*/
- (void)sendUserLoginConfirmMsgWithUserId:(int)userid session:(NSString *)session andCompletion:(OGResultBlock)completion;
@end

连接到服务器

- (void)socketConnectHost {
// 连接之前需要手动断开
// 确保断开后再连,如果对一个正处于连接状态的socket进行连接,会出现崩溃
[self cutOffSocket];
self.hasHeatBeat = NO;
self.clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_queue_create(kCLIENT_QUEUE, NULL)];
NSError *error = nil;
[self.clientSocket connectToHost:kSERVER_ADDRESS onPort:kSERVER_PORT error:&error];

}
// 主动断开
- (void)cutOffSocket {
id userData = @(SocketOfflineByUser);
self.clientSocket.userData = userData;
[self.clientSocket disconnect];

}

如果连接服务器成功会调用GCDAsyncSocket中的didConnectToHost代理方法,如下
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"连接到服务器: [%@:%d]", host, port);
// 存储接收数据的缓存区,处理数据的粘包和断包
_readBuf = [[NSMutableData alloc] init];
// 连接成功之后可以增加定时器检查心跳包,检测心跳包的时间可以根据自己项目实际情况来定,一般z
[self addConnctTimer];
[self.clientSocket readDataWithTimeout:kREAD_TIMEOUT tag:0];
}
// 登录成功之后在检测心跳包
- (void)addConnctTimer {
//把定时器放在子线程中 每隔30秒检测是否接收过心跳包
if (self.connectTimer != nil) {
[self.connectTimer invalidate];
self.connectTimer = nil;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_MSEC),  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{
self.connectTimer = [NSTimer scheduledTimerWithTimeInterval:30 target:self selector:@selector(checkForHeartBeat) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});
}

检测心跳包方法
-(void)checkForHeartBeat {
NSLog(@"======检测过心跳包");
if (self.hasHeatBeat == NO) {
[self cutOffSocket];
AppDelegate *app = (AppDelegate *)[UIApplication sharedApplication].delegate;
[app loginAndSelectRole];
}
self.hasHeatBeat = NO;

}

如果未成功连接或者又断开连接,会调用socketDidDisconnect代理方法
/**断开连接:1、服务器断开2、用户主动断开(用户退出或程序退出)*/
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
if (self.connectTimer != nil) {
[self.connectTimer invalidate];
self.connectTimer = nil;
}
NSLog(@"失去连接:[%@:%d]==%@", [sock connectedHost], [sock connectedPort], err);
if (sock.userData == SocketOfflineByServer) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"isSocketDisconnect" object:nil];
[[NSUserDefaults standardUserDefaults]setBool:YES forKey:@"isDisconnect"];
[[NSUserDefaults standardUserDefaults] synchronize];
// 服务器掉线,重连
// 这里可以给用户以提示,写一个重连发送,并调用
AppDelegate *app = (AppDelegate *)[UIApplication sharedApplication].delegate;
[app loginAndSelectRole];

}
}

连接服务器成功后会调用GCDAscySocket提供的代理方法接收服务器数据,或者向服务器发送数据

// 接收到服务器数据时调用
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
// 此方法是处理数据的粘包或者断包,见后面
[self disposeBufferData:data];
[sock readDataWithTimeout:kREAD_TIMEOUT tag:0];

}
// 已经向服务器发送数据后调用此代理方法
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
NSString *ip = [sock connectedHost];
uint16_t port = [sock connectedPort];
NSLog(@" 客户端已发送数据: [%@:%d]", ip, port);
[sock readDataWithTimeout:kREAD_TIMEOUT tag:0];
}

- (void)writeData:(NSData *)data {

[self.clientSocket writeData:data withTimeout:kWRITE_TIMEOUT tag:0];

}


客户端与服务器的消息格式一般由服务器定义好,按照规则读取数据,下面举个例子

 


根据上面定义的数据协议进行消息的断包和粘包处理

- (void)disposeBufferData:(NSData *)data {
// 粘包处理方式 保存缓存数据
[_readBuf appendData:data];
while (_readBuf.length >= 5) {// 头数据为5个字节
// int16FromBytes是自己写的一个方法,将截取的data数据转换成int
// 得到数据的ID 和 整个数据的长度

4000
NSInteger ID = [OGSocketUtils int16FromBytes:[_readBuf subdataWithRange:NSMakeRange(2, 2)]];
NSInteger length = [OGSocketUtils int16FromBytes:[_readBuf subdataWithRange:NSMakeRange(0, 2)]];
NSLog(@"已读取数据:缓冲区长度:%ld, 接收长度:%lu iD:%ld 数据长度:%ld ", (long)_readBuf.length, (unsigned long)[data length], (long)ID, (long)length);
NSInteger dataLength = length + 2;
if (_readBuf.length >= dataLength) {//如果缓存中的数据 够  一个整包的长度
NSData *msgData = [_readBuf subdataWithRange:NSMakeRange(0, dataLength)];
// 处理消息数据
[self hasReadData:msgData];
// 从缓存中截掉处理完的数据,继续循环
if (_readBuf.length > 0) {
_readBuf = [NSMutableData dataWithData:[_readBuf subdataWithRange:NSMakeRange(completeLength, _readBuf.length - completeLength)]];
}
} else { // 断包情况,继续读取
[self.clientSocket readDataWithTimeout:kREAD_TIMEOUT buffer:_readBuf bufferOffset:_readBuf.length tag:0];
return;
}
}
}
- (void)hasReadData:(NSData *)data{
// 处理接收到服务器的数据data
}
以上是我在使用TCP与服务器通信时写的类,可以完成TCP的一个基本开发使用,下一篇会分享一下如何使用Protobuf的消息格式与TCP的联合使用。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息