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

来源:互联网 发布:mysql base64 解码 编辑:程序博客网 时间:2024/06/16 21:53

概要:有关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 和 整个数据的长度        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的联合使用。