socket的封包、粘包、解包,做一个即时通讯项目

这些天项目不是很急,自己研究了一下socket,感觉收获颇丰,真的很高兴。

  • http :
    它是超文本传输协议,对应于应用层,它主要强调的是对数据的封装。它是所谓的“短链接”,一般都是客服端发送请求 到服务器 服务给了客户端应答过后即断开链接,我们平时做的项目中 只要不是客户端和服务器保持长期的链接的情况下 基本都用的是http
  • socket :
    它是我们所谓的“长链接”,它是一个套接字 实际上我觉得它是支持传输协议的的一个基本的单元,它支持tcp/ip 协议 也支持udp协议。
  • tcp/ip :
    我们一般使用的是socket的协议都是tcp/ip协议,这种协议我们认为是绝对安全的为什么呢 因为他是确认建立完整的通道之后才进行传输数据 所以只要链接的通道在数据一定是能从A ->B 或者 B->A的 我们即时通讯的基本上都是这种协议,它建立链接需要“三次握手”,断开链接需要“四次握手”。
    “三次握手”:
    1.客户端发送syn(ack = j)包给服务端,并自己进入syn_send状态,等待服务端确认。
    2.服务端收到客户端的syn包,必须确认客户端的包即(ack = j+1),同时自己也发送一个syn(ack = k)的一个包 ,它发给客户端的是 自己的syn包和确认的客户端的包,并且自己进入syn_recv状态。
    3.客户端收到服务器给的包,并且发送服务端一个确认包及(ack = k+1)的包,这个包发送完毕 双方都进入ESTABLISHED状态,三次握手成功。
    断开的“四次握手”:
    实话说这个我说不很清楚,但是我查了一些资料有些形象的比喻,我基本明白了。
    1. a 告诉b要断开链接
    2.b收到a的消息,并且告诉a等待,等待自己发送未完成的数据包。
    3.b告诉a发送数据完成,a可以关闭了。
    4.a知道b发送完成数据了,并且a等待一下(这个我不知道a为什么不马上关闭),a关闭链接。
    我在网络上找的一个图片形象的描述了这一个过程。


    image.png

    其实我们正常写代码根本就是不知道这些的。因为我们基本不基于苹果原声的api 那个是纯c的我们一般不用 用框架的话框架内部都给我们做好了,我们只需要调用方法就好了,但是我们应该明白原理。

  • udp:
    说实话我工作中根本没用用到这个,但是它是非安全的,举个简单的例子 tcp/ip就是打电话,双方信号通了才能说话,udp是相当于别人给你说话,那你有可能听到也有可能听不到,只是知道说话的人说话了。但是udp的效率还是比tcp相对来说要高。我们一般用到的udp一般是广播等等的吧。
  • 做一个及时通讯的聊天
    我们做一个及时通讯的聊天需要准备什么呢 ?对于我们客户端而言我觉得需要准备数据格式、协议、还有及时通讯的框架。
    1.数据格式:
    我理解的是我们是用json 、protobuf还是xml。
    1.json:我们现在一般的公司有用到的因为它简单,直接将我们的数据转换成json 在将 json转换成data 完成我们数据的封装。
    2.protobuf 相信很多人可能第一次听说,它是谷歌写的一套框架用来我们封装数据的 里面定义的都是模型 ,用起来非常的方便,不好的地方就是安装环境不是很好安装。
    3.xml我没用用过这种方式 ,如果有的公司用的话请查查资料吧 我这个不是很了解。
    上面这三种方式最好的是protobuf 因为它压缩包的大小大概是json的10分之一,大概是xml的20分之一。所以我重点介绍的是protobuf.为什么数据包越小越好,比如我们经常玩的王者荣耀,那里面的几乎每一个操作都是socket通信,如果数据包比较大在网络不是很通畅的情况下,那人家本来不来不该死的是不是就死了,人家该放出技能的情况下 是不是有可能放不出来了。所以数据包小 很重要。
  • protobuf :
    1.安装:这里用的是cocoapod的方式安装,因为这个是最简单的,如果自己手动倒入绝对是缺爹少娘的。
    pod 'Protobuf', '~> 3.1.0'
    2.创建一个proto的文件 ,最简单的办法是我们 用终端命令 ,touch xx.proto.
    里面的内容具体书写:
syntax = "proto2";

message UserInfo {
required string name = 1;
required int64 level = 2;
}

message TextMessage {
required UserInfo user = 1;
required string text = 2;
}

message GiftMessage {
required UserInfo user = 1;
required string giftname = 2;
required string giftURL = 3;
required string giftCount = 4;
}

syntax = "proto2";好像说的是包名,这个我们不用纠结等下下面会说。
required 是必须要传的参数,如果这个不传 会导致protobuf 没有数据。
我们现在建立的protobuf的proto文件写好了 下面将我们写的proto文件倒入到我们的工程。顺便说一下 我们不能这样写

message GiftMessage {
required UserInfo user = 1;
required string giftname = 1;
}

因为后面的那个数字是它的一个标示,标示不能重复。我给大家看一下我的proto文件的位置


image.png

到这一步是没有这两个文件的


image.png

下面我们用命令生成这两个文件
首先我们用命令切换到我们proto所在的文件的位置 ,比如我的文件的位置是:
image.png

这时候我们执行这个命令:
protoc --objc_out=. *.proto
我们回到工程点击我们的proto文件然后show in finder 会看到这时候生成两个文件
将这两个文件拖到我们的工程中 变成这个样子:


image.png

我们编译 发现会报错,这是非arc的原因 我们只需要这样操作 我用图片来进行演示:
image.png

只时候进行编译 发现成功了 我们这时候应该默默的高兴。
指的说明的是protobuf 安装网上怎么说的都有 我也尝试了 但是我不知道什么原因基本上都不能成功,我现在这个是没有问题的,大家可以试试。
下面我们就可以使用了,具体怎么使用:
这是一个protobuf付值并且转换成data的例子。
GiftMessage *giftM = [[GiftMessage alloc] init];
    giftM.user = self.userInfo;
    giftM.giftname = giftName;
    giftM.giftURL = imageUrl;
    giftM.giftCount = giftCount;
    NSMutableData *needSendData = [self calculatorData:MessageTypeGift bodyData:giftM.data maxM:2];

将protobuf转换成我们的模型

 UserInfo *userI = [UserInfo parseFromData:bodyData error:nil];

我们平时用到的基本就两个方法,还有其他的使用就到github上看下文档就可以了。注意服务端和我们的客户端都用一套protobuf也就是proto文件是一样的,而且proto支持多言语 什么java 、php、 oc、swift、android等等的都支持。

  • 协议:
    其实我们做一个聊天的功能最重要的就是协议的定义和我们的封包和解包。
    协议我们一般都是自定义的协议,这个协议是我们和我们的后端商量好的
    我们正规公司做的协议大概是这样的
version:版本(一般4个字节)
type:类型(有的公司用一个也有的公司用 maintype 和 subtype共同决定,一般4个字节)
lengthData: 消息体的长度(一般4个字节)
messageData:消息体
salt:加密的盐,需要加密才用,不加密的可能不用 具体根据公司来定。(一般4个字节)

具体说明:
1.version的作用 一般来说 比如 我们的qq 假如新用户升级了一个版本,但是老用户还没有升级到最新的版本,那么我们新用户给老用户发送消息的时候老用户可能就解析不了(比如你公司的加密升级)所以需要version。
2.type:顾名思义就是 比如我们的文本 图片 音频 视频等等的 用来做区分的.
3.消息体的长度:这个需要客户端提前计算好,因为你丢过去一个数据包 服务器怎么解呢 它怎么知道那块是消息体的长度呢 有的人说去掉前面的就是消息体 那么这个包是不完整的呢 比如 完整的包是1024个字节 实际山服务端收到的是800字节 服务端怎么知道这个是不是完整的呢。所以消息体的长度很重要.
4.salt 顾名思义 这个不在多说 具体有你公司而定.

  1. 我们按照顺序将我们这些每一个组成一个data 然后进行拼接 最后将一个整个的data发送出去。注意顺序不能错 公司怎么定义你需要怎么拼接 否则服务端解析会报错。
  • socket用什么框架 oc的话 我用的是CocoaAsyncSocket,swift也有很多 我具体不说了,我用的oc 所以大家查一下就可以了。
    说实话这个框架很简单 具体怎么使用大家具体百度一下 我觉得太简单了 所以我就不说了。
  • socket的封包:
    我觉得从现在以后的都很重要 ,何谓封包就是我们按照我们的协议将我们的数据封装起来,将一个完整的包发给服务端。我下面说一下我自己简单定的协议
lengthData:消息体的长度 占4个字节
type:类型 占2个字节
data :消息体

因为服务器是我自己写的(网上查了一点资料) 我以我的电脑当作服务器
下面是我具体的封包的代码,我拿文本消息进行举例 代码写了很详细的注释

 GiftMessage *giftM = [[GiftMessage alloc] init];
    giftM.user = self.userInfo;
    giftM.giftname = giftName;
    giftM.giftURL = imageUrl;
    giftM.giftCount = giftCount;
    NSMutableData *needSendData = [self calculatorData:MessageTypeGift bodyData:giftM.data maxM:2];
    [self.socket writeData:needSendData withTimeout:TIME_OUT tag:MessageTypeGift];
/**
 这个方法是封包    组装数据的格式是。type + length + bodyData
 
 @param type 类型
 @param bodyData 要发送的数据
 @param maxM 最大的发送数据M数量
 @return 整理好的data
 */
- (NSMutableData *)calculatorData:(MessageType)type bodyData:(NSData *)bodyData maxM:(int)maxM{
    // 需要返回的data
    NSMutableData *needSendData = [[NSMutableData alloc] init];
    // 类型的data 固定2个字节
    int typeInt = (int)type;
    NSMutableData *typeData = [NSMutableData dataWithBytes:&typeInt length:2];
    // 发送数据的长度data 也是固定4个字节
    int lenth = (int)bodyData.length;
    NSMutableData *legthData = [NSMutableData dataWithBytes:&lenth length:4];
    // 进行拼接,注意顺序不能错
    [needSendData appendData:legthData];
    [needSendData appendData:typeData];
    [needSendData appendData:bodyData];
    return needSendData;
}

再次强调顺序不能错
顺便说一下 :我们封装的包是完整的 但是我们服务器收到的包不一定就是完整的为什么因为tcp/ip 协议是有优化的算法的它可能会分批发送也有可能是发送一个整个数据包,这样的话就会导致粘包 服务器发送给我们的也有可能是这种情况,下面我举一下导致沾包的原因


image.png

假设有两个数据包
1.发送a是完整的b不完整
2.假设发送a是不完整的,b是完整的
3.假设a、b都是完整的
等等还有其他的情况 我大致就是据这个三个例子
其实第三个情况是没有问题的,1、2 等的不完整的数据包就会导致沾包问题

  • socket的解包,下面是我的解包(很重要 ,解包不了导致直接数据解析失败)
/**
 拆除包  防止沾包

 @param data data
 @param sock sock
 */
- (void)parseData:(NSData *)data socket:(GCDAsyncSocket *)sock{
    // 首先付给要处理的data
    [self.cacheParseData appendData:data];
    // 找到我们当初存储的长度
    NSData *lengthData = [self.cacheParseData subdataWithRange:NSMakeRange(0, 4)];
    int shouldLength = 0;
    [lengthData getBytes:&shouldLength length:4];
    shouldLength += 6;
    while (self.cacheParseData.length > 6) {
        if (shouldLength > self.cacheParseData.length) { // 说明这个包是不完整的
            [sock readDataWithTimeout:TIME_OUT tag:0];
            break;
        }else{  // 说明这个包至少是大于等于一个完整包的长度
            NSData *needParseData = [self.cacheParseData subdataWithRange:NSMakeRange(0, shouldLength)];
            // 在这里开始正式解决这个包
            [self parseData:needParseData dataLength:shouldLength - 6];
            [self.cacheParseData replaceBytesInRange:NSMakeRange(0, shouldLength) withBytes:nil length:0];
            [sock readDataWithTimeout:TIME_OUT tag:0];
        }
    }
}
/**
 进入到这个方法说明是一个完整的包,并且是能够解析的

 @param data data
 @param shouldHaveLength 消息的长度
 */
- (void)parseData:(NSData *)data dataLength:(int)shouldHaveLength{
    
    // 类型
    NSData *typeData = [data subdataWithRange:NSMakeRange(4, 2)];
    int type = 0;
    [typeData getBytes:&type length:2];
    // 消息体
    NSData *bodyData = [data subdataWithRange:NSMakeRange(6, shouldHaveLength)];
    switch (type) {
        case MessageTypeHeart:
        {
            LDGLog(@"心跳包的消息");
        }
            break;
        case MessageTypeJoinRoom:
        {
            UserInfo *userI = [UserInfo parseFromData:bodyData error:nil];
            if ([self.toolDelegate respondsToSelector:@selector(haveJoinRoom:)]) {
                [self.toolDelegate haveJoinRoom:userI];
            }
        }
            break;
        case MessageTypeLeaveRoom:
        {
            UserInfo *userI = [UserInfo parseFromData:bodyData error:nil];
            if ([self.toolDelegate respondsToSelector:@selector(haveLeaveRoom:)]) {
                [self.toolDelegate haveLeaveRoom:userI];
            }
        }
            break;
        case MessageTypeText:
        {
            TextMessage *textM = [TextMessage parseFromData:bodyData error:nil];
            if ([self.toolDelegate respondsToSelector:@selector(haveAcceptTextMessage:)]) {
                [self.toolDelegate haveAcceptTextMessage:textM];
            }
        }
            break;
        case MessageTypeGift:
        {
            GiftMessage *giftM = [GiftMessage parseFromData:bodyData error:nil];
            if ([self.toolDelegate respondsToSelector:@selector(haveAcceptGiftMessage:)]) {
                [self.toolDelegate haveAcceptGiftMessage:giftM];
            }
        }
            break;
        default:
        {
            LDGLog(@"不知道是啥子消息");
        }
            break;
    }
}
  • 至此 我们socket的简单的封包和解包就做完了,我们基本有一点基础了,但是我们现在做的远远不够,因为有的公司这个需求 就是我要求你客户端每一次发送的包不能大于2M?我们该怎么办?还有我们做socket 听过心跳包 那个是怎么回事?好 下面我会针对不同的问题说几点注意点。我觉得很重要。
  • 个人觉得非常重要的几点说明:
    1.假如公司要求你每次发送的数据包不能大于2M,我觉得我们在封装包的情况下应该这样写,因为我属于自己写的服务器 ,没有那么复杂,我下面的代码我没有验证,但是我觉得大致思路是对的。
/**
 这个方法是封包    组装数据的格式是。type + length + bodyData
 
 @param type 类型
 @param bodyData 要发送的数据
 @param maxM 最大的发送数据M数量
 @return 整理好的data
 */
- (NSMutableData *)calculatorData:(MessageType)type bodyData:(NSData *)bodyData maxM:(int)maxM{
    // 需要返回的data
    NSMutableData *needSendData = [[NSMutableData alloc] init];
    // 类型的data 固定2个字节
    int typeInt = (int)type;
    NSMutableData *typeData = [NSMutableData dataWithBytes:&typeInt length:2];
    // 发送数据的长度data 也是固定4个字节
    int lenth = (int)bodyData.length;
    NSMutableData *legthData = [NSMutableData dataWithBytes:&lenth length:4];
    // 进行拼接,注意顺序不能错
    [needSendData appendData:legthData];
    [needSendData appendData:typeData];
    [needSendData appendData:bodyData];
    
    if (needSendData.length > 2 * 1024 *1024) { // 说明数据包大于2M
        // 计算countNumber是一个小算法,就是计算我们的数据包有(2*1024*1024),最后一个不足也加1
        NSInteger countNumber = (needSendData.length - 1)/ (2*1024*1024) + 1;
        for (NSInteger index = 0; index < countNumber; index ++) {
            NSData *perData = [[NSData alloc] init];
            if (index == countNumber -1 ) {
                perData = [needSendData subdataWithRange:NSMakeRange(index *(2*1024*1024) , needSendData.length - index *(2*1024*1024))];
            }else{
                perData = [needSendData subdataWithRange:NSMakeRange(index *(2*1024*1024) , 2*1024*1024)];
            }
            [self.sectionArray addObject:perData];
        }
    }
    return needSendData;
}
/**
 发送礼物的消息
 
 @param imageUrl 图片的url
 @param giftName 图片的名字
 @param giftCount 礼物的数量
 */
- (void)sendGiftMessage:(NSString *)imageUrl giftName:(NSString *)giftName giftCount:(NSString *)giftCount{
    
    GiftMessage *giftM = [[GiftMessage alloc] init];
    giftM.user = self.userInfo;
    giftM.giftname = giftName;
    giftM.giftURL = imageUrl;
    giftM.giftCount = giftCount;
    NSMutableData *needSendData = [self calculatorData:MessageTypeGift bodyData:giftM.data maxM:2];
    if (self.sectionArray.count) {
        for (NSData *data in self.sectionArray) {
           [self.socket writeData:data withTimeout:TIME_OUT tag:MessageTypeGift];
        }
        [self.sectionArray removeAllObjects];
    }else{
     [self.socket writeData:needSendData withTimeout:TIME_OUT tag:MessageTypeGift];
    }
}
  • 解释疑问:我开始可能会产生这样的一个疑问,我现在是把一个完整的包分开了 那会不会出现这样的一个问题呢 会不会导致我把一个汉字或者一个字母分成两半了呢,后来我想了一下不会 因为我们在解包的情况下 我判断只有完整的包才会解析,所以在界面显示的时候不会出现。
    2.什么心跳包,心跳包有什么作用?
    心跳包就是在间隔相同的时间内,客户端像服务端发送你们规定好的一个数据包,用来判断客户端与服务端一直保持链接的。下面是我写的心跳包,我在子线程开辟了一个定时器。
/**
 * Called when a socket connects and is ready for reading and writing.
 * The host parameter will be an IP address, not a DNS name.
 **/
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
    LDGLog(@"说明已经链接成功了");
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        if (!self.heartTimer) {
            self.heartTimer = [NSTimer timerWithTimeInterval:HEART_TIME target:self selector:@selector(heartAction) userInfo:nil repeats:YES];
            [[NSRunLoop currentRunLoop] addTimer:self.heartTimer forMode:NSDefaultRunLoopMode];
            [[NSRunLoop currentRunLoop] run];
        }
    });
    [sock readDataWithTimeout:TIME_OUT tag:0];
}
/**
 心跳包的事件  为了保持客户端和服务端长期的链接
 */
-(void)heartAction{
    NSData *heartData = [@"my heart is very bad" dataUsingEncoding:NSUTF8StringEncoding];
    NSMutableData *needSendData = [self calculatorData:MessageTypeHeart bodyData:heartData maxM:2];
    [self.socket writeData:needSendData withTimeout:TIME_OUT tag:MessageTypeHeart];
}

解释:其中为什么在[[NSRunLoop currentRunLoop] run];而没有用[NSTimer scheduledTimerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>]这是runloop相关的知识,我们不细说因为 runloop在子线程默认是不开启的 在主线程默认是开启的。顺便说一句 每个心跳包的时间是不一样的,一般根据公司规定, 一般来说10秒、20秒 、30秒。心跳包能接收到消息说明是链接着的。反之说明断开链接。
3.说明一下 一般而言 根本不会写服务器的代码,我也是在网上找的 自己修改了一丢丢,那我们如果不会写服务器代码,我们自己做socket项目怎么演示呢,我想到一个不算太好的办法,可以验证我们解析包的时候是否解析出来


image.png
  1. CocoaAsyncSocket大致说一下:
    4.1)#import "GCDAsyncSocket.h"
    4.2)链接主机和端口号,端口号不能小于等于1024 因为这些端口都被系统或者什么的占领了,为什么还需要端口号 因为一个ip地址下可以有多个服务器,怎样找到我们的那台服务器就是需要端口号。端口号和ip地址确定唯一的服务器。
 self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
 [self.socket connectToHost:@"192.168.100.193" onPort:7878 error:nil];

4.3)已经链接成功的delegate回调

/**
 * Called when a socket connects and is ready for reading and writing.
 * The host parameter will be an IP address, not a DNS name.
 **/
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port

4.3)已经读取成功的回调

/**
 * Called when a socket has completed reading the requested data into memory.
 * Not called if there is an error.
 **/
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag

4.4)断开链接的回调 被动断开

/**
 已经断开链接了 被动断开链接

 @param sock socket
 @param err 错误信息
 */
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err

4.5) 客户端主动断开链接

[sock disconnect]

4.6)链接主机成功了

/**
 * Called when a socket connects and is ready for reading and writing.
 * The host parameter will be an IP address, not a DNS name.
 **/
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port

大致就是这些常用的 还有需要的 我们看一下文档就可以了说的很清楚,大家肯定也会。

  1. 如果公司用的json 封装数据的怎么办,我在网上找了一段很好的代码,我直接粘贴了,大家看一下就很明白了 很简单
NSMutableDictionary *dictTemp = [NSMutableDictionary dictionary];
    dictTemp[@"username"]         = @"LD";
    
    //先创建模型 --> 转Json -->转字符串
    TestModel *model = [TestModel new];
    model.type       = 1;
    model.userName   = @"LD";
    model.age        = @"18";
    model.message    = @"Hellow";
    model.Content    = dictTemp;
    
    //先将模型转换成Json格式的数据这里根据自己项目情况来看是否需要转成Json格式  使用到了MJExtension,
    NSString * strJson  = [[NSString alloc] initWithData :model.mj_JSONData encoding :NSUTF8StringEncoding];
    Cs_Connect *connect = [Cs_Connect new];
    connect.serverID    = 1;
    connect.message     = strJson;
    connect.length      = (int)connect.message.length;

    //将数据传换成二进制数据,转换之后的数据和协议顺序是一致的(为什么不需要调整顺序我也不知道,有兴趣的的同学自己去研究下这个方法)
    NSMutableData *dataModel =  [socket RequestSpliceAttribute:connect];
    
    // 通过Socket发出去
    [socket sendMessage:dataModel];
//  将模型数据转换成二进制数据
-(NSMutableData *)RequestSpliceAttribute:(id)obj{

    _data = nil;//记得清空不然数据包会越来越大
    if (obj == nil) {
        self.object = self.data;
        
        NSLog(@"传入需转二进制的数据为空");
        return nil;
     }
    unsigned int numIvars; //成员变量个数
    objc_property_t *propertys = class_copyPropertyList(NSClassFromString([NSString stringWithUTF8String:object_getClassName(obj)]), &numIvars);
    NSString *type = nil;
    NSString *name = nil;
    
    for (int i = 0; i < numIvars; i++) {
        objc_property_t thisProperty = propertys[i];
        
        name = [NSString stringWithUTF8String:property_getName(thisProperty)];
//                NSLog(@"%d.name:%@",i,name);
        type = [[[NSString stringWithUTF8String:property_getAttributes(thisProperty)] componentsSeparatedByString:@","] objectAtIndex:0]; //获取成员变量的数据类型
//                NSLog(@"%d.type:%@",i,type);
        id propertyValue = [obj valueForKey:[(NSString *)name substringFromIndex:0]];
//                NSLog(@"%d.propertyValue:%@",i,propertyValue);
        
        if ([type isEqualToString:TYPE_UINT8]) {
            uint8_t i = [propertyValue charValue];// 8位
            [self.data appendData:[DLSocketDataUtils byteFromUInt8:i]];
        }else if([type isEqualToString:TYPE_UINT16]){
            uint16_t i = [propertyValue shortValue];// 16位
            [self.data appendData:[DLSocketDataUtils bytesFromUInt16:i]];
        }else if([type isEqualToString:TYPE_UINT32]){
            uint32_t i = [propertyValue intValue];// 32位
            [self.data appendData:[DLSocketDataUtils bytesFromUInt32:i]];
        }else if([type isEqualToString:TYPE_UINT64]){
            uint64_t i = [propertyValue longLongValue];// 64位
            [self.data appendData:[DLSocketDataUtils bytesFromUInt64:i]];
        }else if([type isEqualToString:TYPE_STRING]){
            NSData *data = [(NSString*)propertyValue \
                            dataUsingEncoding:NSUTF8StringEncoding];// 通过utf-8转为data
            [self.data appendData:data];
            
        }else {
            NSLog(@"RequestSpliceAttribute:未知类型");
            NSAssert(YES, @"RequestSpliceAttribute:未知类型");
        }
    }
    
    // hy: 记得释放C语言的结构体指针
    free(propertys);
    self.object = _data;
    return _data;
}
  • 解释:其中作者有一段 为什么不需要调整顺序我也不知道,有兴趣的的同学自己去研究下这个方法这个写的那个人不明白 ,其实很简单,是不是运行时Cs_Connect这个模型的顺序是一定的 所以顺序不会错对吧 很简单的。
    6 .这是我封装消息转发的工具类
LDGMessageTransformTool.h 文件
//
//  LDGMessageTransformTool.h
//  ZhiBo
//
//  Created by apple on 2018/5/23.
//  Copyright © 2018年 apple. All rights reserved.
//

#import 
#import "GCDAsyncSocket.h"

typedef NS_ENUM(NSUInteger,MessageType){
    MessageTypeJoinRoom   = 0,
    MessageTypeLeaveRoom  = 1,
    MessageTypeText       = 2,
    MessageTypeGift       = 3,
    MessageTypeHeart      = 100
};

#define TIME_OUT 20
#define HEART_TIME 10
@protocol LDGMessageTransformToolDelegate


/**
  已经接收到进入到 房间消息了

 @param userI userI
 */
- (void)haveJoinRoom:(UserInfo *)userI;

/**
 已经接收到推出到 房间消息了

 @param userI userI
 */
- (void)haveLeaveRoom:(UserInfo *)userI;
/**
 已经接收到收到礼物的消息了

 @param giftM giftM
 */
- (void)haveAcceptGiftMessage:(GiftMessage *)giftM;

/**
 已经接受到文本消息了。大师兄

 @param textM 文本消息的model
 */
- (void)haveAcceptTextMessage:(TextMessage *)textM;
@end

@interface LDGMessageTransformTool : NSObject

@property (weak, nonatomic) id toolDelegate;
/**
 进入房间的消息是  : 0
 离开房间的消息是  : 1
 发送文本消息是   : 2
 发送礼物消息是   : 3
 */
/**
 链接上服务器
 */
- (instancetype)initWithConnectServer;
/**
 进入房间
 */
- (void)joinRoom;
/**
 离开房间
 */
- (void)leaveRoom;
/**
 发送文本消息

 @param text text
 */
- (void)sendTextMessage:(NSString *)text;

/**
 发送礼物的消息

 @param imageUrl 图片的url
 @param giftName 图片的名字
 @param giftCount 礼物的数量
 */
- (void)sendGiftMessage:(NSString *)imageUrl giftName:(NSString *)giftName giftCount:(NSString *)giftCount;

@end
LDGMessageTransformTool.m 文件
//
//  LDGMessageTransformTool.m
//  ZhiBo
//
//  Created by apple on 2018/5/23.
//  Copyright © 2018年 apple. All rights reserved.
//

#import "LDGMessageTransformTool.h"

@interface LDGMessageTransformTool ()

@property (strong, nonatomic) GCDAsyncSocket *socket;
@property (strong, nonatomic) UserInfo *userInfo;
@property (strong, nonatomic) NSTimer *heartTimer;
@property (strong, nonatomic) NSMutableData *cacheParseData;



@end

@implementation LDGMessageTransformTool
-(NSMutableData *)cacheParseData {
    if (!_cacheParseData) {
        _cacheParseData = [[NSMutableData alloc] init];
    }
    return _cacheParseData;
}

/**
 链接上服务器,创建
 */
- (instancetype)initWithConnectServer{
    if (self = [super init]) {
        UserInfo *userInfo = [[UserInfo alloc] init];
        userInfo.name = @"liudiange";
        userInfo.level = 1;
        self.userInfo = userInfo;
        self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
        [self.socket connectToHost:@"192.168.100.193" onPort:7878 error:nil];
    }
    return self;
}

#pragma mark - 发送消息的方法
/**
 进入房间的消息是 : 0
 离开房间的消息是 : 1
 发送文本消息是 : 2
 发送礼物消息是 : 3
 */
/**
 进入房间
 */
- (void)joinRoom{
    
    NSMutableData *needSendData = [self calculatorData:MessageTypeJoinRoom bodyData:self.userInfo.data maxM:2];
    // 没有超时时间 -1 代表没有超时时间
    [self.socket writeData:needSendData withTimeout:TIME_OUT tag:MessageTypeJoinRoom];
}
/**
 离开房间
 */
- (void)leaveRoom {
    
    NSMutableData *needSendData = [self calculatorData:MessageTypeLeaveRoom bodyData:self.userInfo.data maxM:2];
    
    // 没有超时时间 -1 代表没有超时时间
    [self.socket writeData:needSendData withTimeout:TIME_OUT tag:MessageTypeLeaveRoom];
    
}
/**
 发送文本消息
 
 @param text text
 */
- (void)sendTextMessage:(NSString *)text{
    
    TextMessage *textM = [[TextMessage alloc] init];
    textM.user = self.userInfo;
    textM.text = text;
    NSMutableData *needSendData = [self calculatorData:MessageTypeText bodyData:textM.data maxM:2];
    [self.socket writeData:needSendData withTimeout:TIME_OUT tag:MessageTypeText];
    
//    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//        [self parseData:needSendData socket:self.socket];
//    });
}

/**
 发送礼物的消息
 
 @param imageUrl 图片的url
 @param giftName 图片的名字
 @param giftCount 礼物的数量
 */
- (void)sendGiftMessage:(NSString *)imageUrl giftName:(NSString *)giftName giftCount:(NSString *)giftCount{
    
    GiftMessage *giftM = [[GiftMessage alloc] init];
    giftM.user = self.userInfo;
    giftM.giftname = giftName;
    giftM.giftURL = imageUrl;
    giftM.giftCount = giftCount;
    NSMutableData *needSendData = [self calculatorData:MessageTypeGift bodyData:giftM.data maxM:2];
    [self.socket writeData:needSendData withTimeout:TIME_OUT tag:MessageTypeGift];

}

/**
 这个方法是封包    组装数据的格式是。type + length + bodyData
 
 @param type 类型
 @param bodyData 要发送的数据
 @param maxM 最大的发送数据M数量
 @return 整理好的data
 */
- (NSMutableData *)calculatorData:(MessageType)type bodyData:(NSData *)bodyData maxM:(int)maxM{
    // 需要返回的data
    NSMutableData *needSendData = [[NSMutableData alloc] init];
    // 类型的data 固定2个字节
    int typeInt = (int)type;
    NSMutableData *typeData = [NSMutableData dataWithBytes:&typeInt length:2];
    // 发送数据的长度data 也是固定4个字节
    int lenth = (int)bodyData.length;
    NSMutableData *legthData = [NSMutableData dataWithBytes:&lenth length:4];
    // 进行拼接,注意顺序不能错
    [needSendData appendData:legthData];
    [needSendData appendData:typeData];
    [needSendData appendData:bodyData];
    return needSendData;
}
/**
 心跳包的事件  为了保持客户端和服务端长期的链接
 */
-(void)heartAction{
    NSData *heartData = [@"my heart is very bad" dataUsingEncoding:NSUTF8StringEncoding];
    NSMutableData *needSendData = [self calculatorData:MessageTypeHeart bodyData:heartData maxM:2];
    [self.socket writeData:needSendData withTimeout:TIME_OUT tag:MessageTypeHeart];
}
#pragma mark - 接受到消息的方法
/**
 * Called when a socket has completed writing the requested data. Not called if there is an error.
 **/
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
    
    [sock readDataWithTimeout:TIME_OUT tag:tag];
}
/**
 * Called when a socket connects and is ready for reading and writing.
 * The host parameter will be an IP address, not a DNS name.
 **/
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
    LDGLog(@"说明已经链接成功了");
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        if (!self.heartTimer) {
            self.heartTimer = [NSTimer timerWithTimeInterval:HEART_TIME target:self selector:@selector(heartAction) userInfo:nil repeats:YES];
            [[NSRunLoop currentRunLoop] addTimer:self.heartTimer forMode:NSDefaultRunLoopMode];
            [[NSRunLoop currentRunLoop] run];
        }
    });
    [sock readDataWithTimeout:TIME_OUT tag:0];
}
/**
 * Called when a socket has completed reading the requested data into memory.
 * Not called if there is an error.
 **/
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
    
    [self parseData:data socket:sock];
}

/**
 已经断开链接了 被动断开链接

 @param sock socket
 @param err 错误信息
 */
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err{
    if (self.heartTimer) {
        [self.heartTimer invalidate];
        self.heartTimer = nil;
    }
}
/**
 拆除包  防止沾包

 @param data data
 @param sock sock
 */
- (void)parseData:(NSData *)data socket:(GCDAsyncSocket *)sock{
    // 首先付给要处理的data
    [self.cacheParseData appendData:data];
    // 找到我们当初存储的长度
    NSData *lengthData = [self.cacheParseData subdataWithRange:NSMakeRange(0, 4)];
    int shouldLength = 0;
    [lengthData getBytes:&shouldLength length:4];
    shouldLength += 6;
    while (self.cacheParseData.length > 6) {
        if (shouldLength > self.cacheParseData.length) { // 说明这个包是不完整的
            [sock readDataWithTimeout:TIME_OUT tag:0];
            break;
        }else{  // 说明这个包至少是大于等于一个完整包的长度
            NSData *needParseData = [self.cacheParseData subdataWithRange:NSMakeRange(0, shouldLength)];
            // 在这里开始正式解决这个包
            [self parseData:needParseData dataLength:shouldLength - 6];
            [self.cacheParseData replaceBytesInRange:NSMakeRange(0, shouldLength) withBytes:nil length:0];
            [sock readDataWithTimeout:TIME_OUT tag:0];
        }
    }
}

/**
 进入到这个方法说明是一个完整的包,并且是能够解析的

 @param data data
 @param shouldHaveLength 消息的长度
 */
- (void)parseData:(NSData *)data dataLength:(int)shouldHaveLength{
    
    // 类型
    NSData *typeData = [data subdataWithRange:NSMakeRange(4, 2)];
    int type = 0;
    [typeData getBytes:&type length:2];
    // 消息体
    NSData *bodyData = [data subdataWithRange:NSMakeRange(6, shouldHaveLength)];
    switch (type) {
        case MessageTypeHeart:
        {
            LDGLog(@"心跳包的消息");
        }
            break;
        case MessageTypeJoinRoom:
        {
            UserInfo *userI = [UserInfo parseFromData:bodyData error:nil];
            if ([self.toolDelegate respondsToSelector:@selector(haveJoinRoom:)]) {
                [self.toolDelegate haveJoinRoom:userI];
            }
        }
            break;
        case MessageTypeLeaveRoom:
        {
            UserInfo *userI = [UserInfo parseFromData:bodyData error:nil];
            if ([self.toolDelegate respondsToSelector:@selector(haveLeaveRoom:)]) {
                [self.toolDelegate haveLeaveRoom:userI];
            }
        }
            break;
        case MessageTypeText:
        {
            TextMessage *textM = [TextMessage parseFromData:bodyData error:nil];
            if ([self.toolDelegate respondsToSelector:@selector(haveAcceptTextMessage:)]) {
                [self.toolDelegate haveAcceptTextMessage:textM];
            }
        }
            break;
        case MessageTypeGift:
        {
            GiftMessage *giftM = [GiftMessage parseFromData:bodyData error:nil];
            if ([self.toolDelegate respondsToSelector:@selector(haveAcceptGiftMessage:)]) {
                [self.toolDelegate haveAcceptGiftMessage:giftM];
            }
        }
            break;
        default:
        {
            LDGLog(@"不知道是啥子消息");
        }
            break;
    }
}
@end
  • 最后我目前总结大概就是这么多,其实到公司用到应该比这还多,那时候我们只能现场发挥了见招拆招了,我写到这的时候真的很兴奋,因为研究出来有成果我们正常人都会感到高兴,还有我以后会更加深入的研究socket ,我们作为一个程序员不能满足现状,因为知识是无止境的。
  • 最后为我写的一个下载的框架做一个小小的宣传,大家觉得我写的还行的话给个星。
    https://github.com/liudiange/DGDownloadManager

你可能感兴趣的:(socket的封包、粘包、解包,做一个即时通讯项目)