通过CocoaAsyncSocket实现Socket长链接

通过GCDAsyncSocket实现socket链接,直接上代码:

服务端代码

#import "ServerViewController.h"
#import 

// 定义消息类型
#define kcTextDataType 0x00000000
#define kcImageDataType 0x00000001
#define kcVideoDataType 0x00000002

@interface ServerViewController ()
@property (nonatomic, strong) GCDAsyncSocket *socket;
@property (nonatomic, strong) NSMutableArray *clientSockets;
@property (weak, nonatomic) IBOutlet UIButton *linkButton;
@property (weak, nonatomic) IBOutlet UIButton *senderFileButton;
@property (weak, nonatomic) IBOutlet UIButton *breakButton;
@property (weak, nonatomic) IBOutlet UIButton *senderButton;
@property (weak, nonatomic) IBOutlet UITextView *msgTextView;
@property (weak, nonatomic) IBOutlet UITextField *inputTextField;
@property (nonatomic, copy) NSString * chackedMessage;
@property (nonatomic, strong) NSMutableData  *dataM;
@property (nonatomic, assign) unsigned int totalSize;
@property (nonatomic, assign) unsigned int currentCommandId;

@end

@implementation ServerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.chackedMessage = @"";
}

- (IBAction)linkAction:(id)sender
{
    // 创建socket
    if (self.socket == nil)
        self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
    // 绑定socket
    if (!self.socket.isConnected){
        NSError *error;
        [self.socket acceptOnPort:8090 error:&error];
        if (error) {
            NSLog(@"%@",error);
        }else {
            NSLog(@"服务器socket 开启成功");
            [self.linkButton setTitle:@"建立成功" forState:UIControlStateNormal];
        }
    }
}

- (IBAction)senderFileAction:(UIButton *)sender
{
    UIImage * img = [UIImage imageNamed:@"test.jpg"];
    NSData  *imageData  = UIImagePNGRepresentation(img);
    unsigned int command = kcImageDataType;
    NSMutableData * mData = [self dataHander:imageData type:command];
    for (GCDAsyncSocket *clientSocket in self.clientSockets) {
        [clientSocket writeData:mData withTimeout:-1 tag:10010];
    }
}

- (IBAction)breakLink:(id)sender
{
    [self.socket disconnect];
    self.socket = nil;
}
- (IBAction)senderAction:(id)sender {
    if (self.inputTextField.text.length == 0) {
           return;
    }
    
    NSData *data = [[self.inputTextField.text stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding];
    NSMutableData *mData = [self dataHander:data type:kcTextDataType];
    self.chackedMessage = [self.chackedMessage stringByAppendingFormat:@"发送消息:%@\n", self.inputTextField.text];
        dispatch_async(dispatch_get_main_queue(), ^{
         self.msgTextView.text = self.chackedMessage;
         self.inputTextField.text = @"";
     });
    
    for (GCDAsyncSocket *clientSocket in self.clientSockets) {
        [clientSocket writeData:mData withTimeout:-1 tag:10010];
    }
}
// 将数据类型及数据本身进行拼接处理
- (NSMutableData *)dataHander:(NSData *)data type:(unsigned int)type
{
    NSMutableData *mData = [NSMutableData data];
    unsigned int dataLength = 4+4+(int)data.length;
    NSData *lengthData = [NSData dataWithBytes:&dataLength length:4];
    [mData appendData:lengthData];
    
    // 拼接指令类型(4~7:指令)
    NSData *typeData = [NSData dataWithBytes:&type length:4];
    [mData appendData:typeData];
    [mData appendData:data];
    
    return mData;
}

#pragma mark - GCDAsyncSocketDelegate
// 连接状态回调
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket{
    NSLog(@"当前客户端的IP:%@ 端口号%d",newSocket.connectedHost,newSocket.connectedPort);
    // 1.存储客户端面的Socket对象
     [self.clientSockets addObject:newSocket];
     NSLog(@"当前有%ld个客户端连接",self.clientSockets.count);
    // 2.客户连接建立后,设置可以读取数据
    [newSocket readDataWithTimeout:-1 tag:10010];
    [self.socket readDataWithTimeout:-1 tag:10010];
}

// 连接断开
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
    NSLog(@"断开 socket连接 原因:%@",err);
    [self.clientSockets removeObject:sock];
}

//接收客户端发送来的数据并进行粘包处理
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
      if (data.length == 0) {
          return;
      }
      
      if(self.dataM.length == 0){
          NSData *totalSizeData = [data subdataWithRange:NSMakeRange(0, 4)];
          unsigned int totalSize = 0;
          
          [totalSizeData getBytes:&totalSize length:4];
          NSLog(@"接收总数据的大小 %u",totalSize);
          self.totalSize = totalSize;
          
          NSData *commandIdData = [data subdataWithRange:NSMakeRange(4, 4)];
          unsigned int commandId = 0;
          [commandIdData getBytes:&commandId length:4];
          self.currentCommandId = commandId;
      }
      
      [self.dataM appendData:data];

      if (self.dataM.length == self.totalSize) {
          if (self.currentCommandId == kcTextDataType) {
              [self showMsg:[self.dataM subdataWithRange:NSMakeRange(8, self.dataM.length - 8)]];
              self.dataM = nil;
          }else if  (self.currentCommandId == kcVideoDataType){//图片

          }else if (self.currentCommandId == kcVideoDataType){
              
          }
      }
      [sock readDataWithTimeout:-1 tag:10010];
      [self.socket readDataWithTimeout:-1 tag:10010];
}

- (void)showMsg:(NSData *)data
{
    NSString * str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    if ([str isEqualToString:@"\n"]) {
           return;
    }
    self.chackedMessage = [self.chackedMessage stringByAppendingFormat:@"接收消息:%@", str];
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"历史消息:%@", self.chackedMessage);
        self.msgTextView.text = self.chackedMessage;
    });
}

//消息发送成功 代理函数 向服务器 发送消息
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
    NSLog(@"服务器发送 laile ");
    NSLog(@"%ld 发送数据成功",tag);
}
#pragma mark -- lazy
- (NSMutableArray *)clientSockets{
    if (!_clientSockets) {
        _clientSockets = [NSMutableArray arrayWithCapacity:10];
    }
    return _clientSockets;
}

- (NSMutableData *)dataM
{
    if (!_dataM) {
        _dataM = [NSMutableData data];
    }
    return _dataM;
}

@end

客户端代码

#import "ClientViewController.h"
#import 

// 定义消息类型
#define kcTextDataType 0x00000000
#define kcImageDataType 0x00000001
#define kcVideoDataType 0x00000002

#define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}

@interface ClientViewController ()

@property (weak, nonatomic) IBOutlet UIButton *linkButton;
@property (weak, nonatomic) IBOutlet UIButton *reLinkButton;
@property (weak, nonatomic) IBOutlet UIButton *breakButton;
@property (weak, nonatomic) IBOutlet UIButton *senderButton;
@property (weak, nonatomic) IBOutlet UITextView *msgTextView;
@property (weak, nonatomic) IBOutlet UITextField *inputTextField;

@property (nonatomic, strong) GCDAsyncSocket *socket;
@property (nonatomic, copy) NSString * chackedMessage;
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, strong) NSMutableData  *dataM;
@property (nonatomic, strong) dispatch_source_t timer;
@property (nonatomic, assign) unsigned int totalSize;
@property (nonatomic, assign) unsigned int currentCommandId;
@property (weak, nonatomic) IBOutlet UIImageView * backImageView;
// 心跳timer
@property (nonatomic, strong) NSTimer *heartTimer;
// 重连等待时间
@property (nonatomic, assign) NSInteger reconnectTime;
@end

@implementation ClientViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.chackedMessage = @"";
    self.queue = dispatch_get_global_queue(0, 0);
    UIBarButtonItem * item = [[UIBarButtonItem alloc] initWithTitle:@"发送" style:UIBarButtonItemStyleDone target:self action:@selector(senderAction:)];
    self.navigationItem.rightBarButtonItem = item;
    
}

- (void)finish
{
    [self.inputTextField resignFirstResponder];
}
- (IBAction)linkAction:(id)sender {
    if (self.socket == nil) {
        self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:self.queue];
    }
    
    // 连接socket
    if (!self.socket.isConnected){
        NSError *error;
        [self.socket connectToHost:@"ip地址" onPort:8090 withTimeout:-1 error:&error];
        if (error) {
            NSLog(@"%@",error);
            return;
        }else {
            [self.linkButton setTitle:@"已连接" forState:UIControlStateNormal];
            [self setupHeartBeat];
        }
    }
}
// 重新链接
- (IBAction)reLinkAction:(id)sender {
    // 创建socket
    if (self.socket == nil) {
        self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:self.queue];
    }
    
    // 连接socket
    if (!self.socket.isConnected){
        NSError *error;
        [self.socket connectToHost:@"ip地址" onPort:8090 withTimeout:-1 error:&error];
        if (error) NSLog(@"%@",error);
    }
}
// 断开链接
- (IBAction)breakAction:(id)sender {
    [self.socket disconnect];
    self.socket = nil;
    [self.linkButton setTitle:@"连接" forState:UIControlStateNormal];
}

- (IBAction)senderAction:(id)sender {
    if (self.inputTextField.text.length == 0) {
        return;
    }
    NSData *data = [[self.inputTextField.text stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding];
    NSMutableData *mData = [NSMutableData data];
    // 计算数据总长度 data
    unsigned int dataLength = 4+4+(int)data.length;
    NSData *lengthData = [NSData dataWithBytes:&dataLength length:4];
    [mData appendData:lengthData];
        
    // 数据类型 data
    // 2.拼接指令类型(4~7:指令)
    unsigned int command = kcTextDataType;
    // 将类型标记拼接到数据上
    NSData *typeData = [NSData dataWithBytes:&command length:4];
    [mData appendData:typeData];

    // 最后拼接数据
    [mData appendData:data];
    NSLog(@"发送数据的总字节大小:%ld",mData.length);
    [self.socket writeData:mData withTimeout:-1 tag:10086];
    
    self.chackedMessage = [self.chackedMessage stringByAppendingFormat:@"发送消息:%@\n", self.inputTextField.text];
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"历史消息:%@", self.chackedMessage);
        self.msgTextView.text = self.chackedMessage;
        self.inputTextField.text = @"";
    });
}

// 关闭socket
- (void)disconnectSocket{
    if (self.socket) {
        [self.socket disconnect];
        self.socket.delegate = nil;
        self.socket = nil;
        [self destoryHeartBeat];
    }
}

// 心跳处理
//在TCP的机制里面,本身是存在有心跳包的机制的,也就是TCP的选项:SO_KEEPALIVE。系统默认是设置的2小时的心跳频率。但是它检查不到机器断电、网线拔出、防火墙这些断线。而且逻辑层处理断线可能也不是那么好处理。一般,如果只是用于保活还是可以的。
//其实,要判定掉线,只需要send或者recv一下,如果结果为零,则为掉线。但是,在长连接下,有可能很长一段时间都没有数据往来。理论上说,这个连接是一直保持连接的,但是实际情况中,如果中间节点出现什么故障是难以知道的。更要命的是,有的节点(防火墙)会自动把一定时间之内没有数据交互的连接给断掉。在这个时候,就需要我们的心跳包了,用于维持长连接,保活。
// 在获知了断线之后,服务器逻辑可能需要做一些事情,比如断线后的数据清理呀,重新连接呀……当然,这个自然是要由逻辑层根据需求去做了。
- (void)setupHeartBeat{
    dispatch_async(dispatch_get_main_queue(), ^{
        [self destoryHeartBeat];
        __weak typeof(self) weakSelf = self;
        self.heartTimer = [NSTimer scheduledTimerWithTimeInterval:15 repeats:YES block:^(NSTimer * _Nonnull timer) {
            __weak typeof(self) strongSelf = weakSelf;
            NSData *data = [[self.inputTextField.text stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding];
            NSMutableData *mData = [NSMutableData data];
            // 计算数据总长度 data
            unsigned int dataLength = 4+4+(int)data.length;
            NSData *lengthData = [NSData dataWithBytes:&dataLength length:4];
            [mData appendData:lengthData];
            
            // 数据类型 data
            // 2.拼接指令类型(4~7:指令)
            unsigned int command = kcTextDataType;
            NSData *typeData = [NSData dataWithBytes:&command length:4];
            [mData appendData:typeData];

            // 最后拼接数据
            [mData appendData:data];
            NSLog(@"发送数据的总字节大小:%ld",mData.length);
            [strongSelf.socket writeData:mData withTimeout:-1 tag:10086];
        }];
    });
}

- (void)destoryHeartBeat{
    //当然这里还以更加严谨一点: 因为已经在主线程了
    dispatch_main_async_safe(^{
        if (self.heartTimer && [self.heartTimer respondsToSelector:@selector(isValid)] && [self.heartTimer isValid]) {
            [self.heartTimer invalidate];
            self.heartTimer = nil;
        }
    });
}


#pragma mark - GCDAsyncSocketDelegate
//已经连接到服务器
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(nonnull NSString *)host port:(uint16_t)port{
    NSLog(@"连接成功 : %@---%d",host,port);
    [self.socket readDataWithTimeout:-1 tag:10086];
}

// 连接断开
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
    NSLog(@"断开 socket连接 原因:%@",err);
}

/**
 粘包中容易出现的问题:
 1.要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去;
 2.接收数据端的应用层没有及时读取接收缓冲区中的数据;
 3.数据发送过快,数据包堆积导致缓冲区积压多个数据后才一次性发送出去(如果客户端每发送一条数据就睡眠一段时间就不会发生粘包);
 */

//已经接收服务器返回来的数据,由于拆包、粘包的问题数据不能发送过快
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
    if (data.length == 0) {
        NSLog(@"传输数据长度为: 0");
        return;
    }
    
    // 1.第一次接收数据
    if(self.dataM.length == 0){
        // 获取总的数据包大小
        NSData *totalSizeData = [data subdataWithRange:NSMakeRange(0, 4)];
        unsigned int totalSize = 0;
        //读取前四个字节
        [totalSizeData getBytes:&totalSize length:4];
        NSLog(@"接收总数据的大小 %u",totalSize);
        self.totalSize = totalSize;
        
        // 获取指令类型
        NSData *commandIdData = [data subdataWithRange:NSMakeRange(4, 4)];
        unsigned int commandId = 0;
        [commandIdData getBytes:&commandId length:4];
        self.currentCommandId = commandId;
    }
    
    // 2.拼接二进制数据
    [self.dataM appendData:data];
    
    if (self.dataM.length == self.totalSize) {
        //丢包的现象
        NSLog(@"数据已经接收完成");
        if (self.currentCommandId == kcTextDataType) {
            [self showMsg:[self.dataM subdataWithRange:NSMakeRange(8, self.dataM.length - 8)]];
            dispatch_async(dispatch_get_main_queue(), ^{
                self.msgTextView.hidden = NO;
                self.backImageView.hidden = YES;
            });
            self.dataM = nil;
        }else if  (self.currentCommandId == kcImageDataType){//图片
            UIImage * img = [UIImage imageWithData:[self.dataM subdataWithRange:NSMakeRange(8, self.dataM.length - 8)]];
            dispatch_async(dispatch_get_main_queue(), ^{
                self.msgTextView.hidden = YES;
                self.backImageView.hidden = NO;
                self.backImageView.image = img;
            });
            self.dataM = nil;
        }else if (self.currentCommandId == kcVideoDataType){
            self.dataM = nil;
        }
        self.totalSize = 0;
    }
    
    [sock readDataWithTimeout:-1 tag:10010];
    [self.socket readDataWithTimeout:-1 tag:10010];
}

//消息发送成功 代理函数 向服务器 发送消息
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
    NSLog(@"%ld 发送数据成功",tag);
}

- (void)showMsg:(NSData *)data
{
    NSString * str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    if ([str isEqualToString:@"\n"]) {
           return;
    }
    self.chackedMessage = [self.chackedMessage stringByAppendingFormat:@"接收消息:%@", str];
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"历史消息:%@", self.chackedMessage);
        self.msgTextView.hidden = NO;
        self.msgTextView.text = self.chackedMessage;
    });
}

#pragma mark -- lazyLoad
- (NSMutableData *)dataM
{
    if (!_dataM) {
        _dataM = [NSMutableData data];
    }
    return _dataM;
}
@end

参考文章:拆包粘包

你可能感兴趣的:(通过CocoaAsyncSocket实现Socket长链接)