iOS 解析一个自定义协议

级别: ★★☆☆☆
标签:「iOS」「自定义协议」「QSIOP」
作者: dac_1033
审校: QiShare团队


1. 关于协议

在我们学习计算机网络的过程中,涉及到很多协议,比如HTTP/HTTPs、TCP、UDP、IP等。不同的协议可能工作在不同的网络层次上,各个协议之所以称为协议,是因为这是一套规则,信息在端到端(如不同的PC之间)进行传输的过程中,同等的层次之间通过使用这套同样的规则,使两个端都知道这一层的数据因该怎么处理,处理成什么格式。

网络通信的层次
TCP报文格式

比如,上图的TCP协议工作在传输层,那么在两个PC端的传输层,都可以通过TCP协议规定的报文格式来打包/封装上层传下来的数据,并且,也都可以拆包/解析下层传上来的数据。

2. 自定义一个协议

在移动端的开发过程中,有时会要求开发者解析一个自定义协议的情况。通常这个协议是建立在TCP连接基础之上的,下面以一个简单的信息通信协议举个🌰:

通信基于一条持久 TCP 连接,连接由 Client 发起。 连接建立后,客户端与服务端通信为 request/response 模式,Client 发起 request,Server 产生 response,然后 Client 再 request,Server 再 response,如此循环,直到 Client 主动 close。交互采用一致的协议单元,信息通信协议格式如下:

字段 ver op propl prop
字节数 2 2 2 pl

一般维持一个长连接,都要手动的发ping包,收pong包。我们规定:op=0 ping 心跳包 client -> server,任意时刻客户端可以向服务端发送ping包,服务端立刻响应。op=1 pong 心跳包 server -> client。具体发ping包的时间间隔可以由客户端与服务端定义。针对这个数据包中,propl = 0 对应的没有prop,那么真实的数据包内容应该是下面这样的:

字段 ver op propl
字节数 2 2 2

我们在与服务端进行交互的时候,基于这个协议,op可以是任何范围内的值,只要双方协议好,能解析出来就好,甚至协议的格式也可以自己来扩展。比如:我们设定 op=2 为通过长连接上报uerid,client -> server,uerid是字符串。
注意:在这个报文里,datal = 0,那么data是没有内容的,prop中的所存储数据的格式为k1:v1/nk2:v2......,那么真实的数据包内容容如下:

字段 ver op propl prop
字节数 2 2 2 pl

就简单的定义这么几条,总的来说,这个协议是个变长的协议。你也可以定义一个定长的协议,即不论每个报文内容是什么,每个报文长度一致。格式不一,也个有优缺点。
我们给这个简要的协议起个名字:QSIOP(QiShare I/O Protocol)。🤪

3. TCP连接

在iOS中建立TCP连接一般使用第三方库CocoaAsyncSocket,这个库封装了建立TCP连接的整个过程,如果有兴趣可以查看其源码。

/**
 * Connects to the given host and port.
 * 
 * This method invokes connectToHost:onPort:viaInterface:withTimeout:error:
 * and uses the default interface, and no timeout.
**/
- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr;

/**
 * Connects to the given host and port with an optional timeout.
 * 
 * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: and uses the default interface.
**/
- (BOOL)connectToHost:(NSString *)host
               onPort:(uint16_t)port
          withTimeout:(NSTimeInterval)timeout
                error:(NSError **)errPtr;

建立TCP连接很简单,用上述方法只提供host地址、port端口号、timeout超时时间即可连接成功。下面是这个库向TCP连接中发送数据的方法:

/**
 * Writes data to the socket, and calls the delegate when finished.
 * 
 * If you pass in nil or zero-length data, this method does nothing and the delegate will not be called.
 * If the timeout value is negative, the write operation will not use a timeout.
 * 
 * Thread-Safety Note:
 * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while
 * the socket is writing it. In other words, it's not safe to alter the data until after the delegate method
 * socket:didWriteDataWithTag: is invoked signifying that this particular write operation has completed.
 * This is due to the fact that GCDAsyncSocket does NOT copy the data. It simply retains it.
 * This is for performance reasons. Often times, if NSMutableData is passed, it is because
 * a request/response was built up in memory. Copying this data adds an unwanted/unneeded overhead.
 * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket
 * completes writing the bytes (which is NOT immediately after this method returns, but rather at a later time
 * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method.
**/
- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;

下面是这个库中TCP连接的回调方法:

#pragma mark - GCDAsyncSocketDelegate

//! TCP连接成功
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    
    [self sendVersionData];
}

//! TCP写数据成功
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
    
    [sock readDataWithTimeout:-1.0 tag:0];
}

//! TCP读数据成功
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    
    [self handleReceivedData:data fromHost:sock.connectedHost];
}

//! TCP断开连接
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
    
  NSLog(@"%s", __func__);
}

可以看到,在这个库的方法里,收/发消息都是以NSData(二进制)的形式进行的。

4. 根据QSIOP,处理要发送的数据

由于GCDAsyncSocket库维持的TCP连接中,传输的数据都是以二进制的形式,8位二进制是一个字节,比如QSIOP中的ver占两个字节,那么就要有一个两个字节的变量去接收它。首先,我们熟悉一下,iOS中关于所占不同字节的整型数据常用类型的定义:

typedef unsigned short                  UInt16;
typedef unsigned int                    UInt32;
typedef unsigned long long              UInt64;

其中,UInt16占两个字节,UInt32占四个字节,UInt64占八个字节。当然,也有其他类型可以用于接受解析出来的数据。

  • ping数据包op = 0
  • 上报uerid的数据包op = 2
+ (NSData *)makeSendMsgPackage:(NSDictionary *)propDict {
    
    NSMutableString *paramsStr = [NSMutableString string];
    for (int i=0; i<propDict.count; i++) {
        NSString *key = [propDict.allKeys objectAtIndex:i];
        NSString *value = (NSString *)[propDict objectForKey:key];
        [paramsStr appendFormat:@"%@:%@\n", key, value];
    }
    NSData *propData = [paramsStr dataUsingEncoding:NSUTF8StringEncoding];
    
    UINT16 iVersion = htons(1.0);
    NSData *verData = [[NSData alloc] initWithBytes:&iVersion length:sizeof(iVersion)];

    UINT16 iOperation = htons(0); //UINT16 iOperation = htons(2);
    NSData *opData = [[NSData alloc] initWithBytes:&iOperation length:sizeof(iOperation)];

    UINT16 iPropLen = htons([paramsStr dataUsingEncoding:NSUTF8StringEncoding].length);
    NSData *propLData = [[NSData alloc] initWithBytes:&iPropLen length:sizeof(iPropLen)];
    

    NSMutableData * msgData = [[NSMutableData alloc] init];
    [msgData appendData:verData];
    [msgData appendData:opData];
    [msgData appendData:propLData];
    [msgData appendData:propData];
    
    return msgData;
}

5. 解析收到的数据

根据QSIOP,解析一个Prop字段有内容的数据包:

  • pong数据包op = 1
  • 其他协议好的数据包...
- (BOOL)parseRsvData:(NSMutableData *)rsvData toPropDict:(NSMutableDictionary *)propDict length:(NSInteger *)length {
    
    UINT16 iVersion;
    UINT16 iOperation;
    UINT16 iPropLen;
    int packageHeaderLength = 2 + 2 + 2;
    
    if (rsvData.length < packageHeaderLength) { return NO; }
    
    [rsvData getBytes:&iVersion range:NSMakeRange(0, 2)];
    [rsvData getBytes:&iOperation range:NSMakeRange(2, 2)];
    [rsvData getBytes:&iPropLen range:NSMakeRange(4, 2)];
    
    UINT16 pl = ntohs(iPropLen);
    
    int propPackageLength = packageHeaderLength+pl;
    if (rsvData.length >= propPackageLength) {
        NSString *propStr = [[NSString alloc] initWithData:[rsvData subdataWithRange:NSMakeRange(packageHeaderLength, pl)] encoding:NSUTF8StringEncoding];
        NSArray *propArr = [propStr componentsSeparatedByString:@"\n"];
        for (NSString *item in propArr) {
            NSArray *arr = [item componentsSeparatedByString:@":"];
            NSString *key = arr.firstObject;
            NSString *value = arr.count>=2 ? arr.lastObject : @"";
            [propDict setObject:value forKey:key];
        }
        if (length) {
            *length = propPackageLength;
        }
        return YES;
    } else {
        return NO;
    }
}
  1. 在TCP连接回调中[self handleReceivedData:data fromHost:sock.connectedHost];不断被执行,所接到的数据要不断追加到一个NSMutableData变量rsvData中;
  2. 调parseRsvData: toPropDict: length:来解析协议时,需要在rsvData的头部向尾部依次解析;
  3. 收到数据,解析...是一个循环不断的过程,如果解析一个数据包成功,则从rsvData中把相应的数据段删掉;

我们解析了一整个数据包,至此,一个简单的协议操作结束了。当然,你还能能设计出一个更复杂的协议,也能优化这个解析协议的过程。


推荐文章:
iOS13 DarkMode适配(二)
iOS13 DarkMode适配(一)
2019苹果秋季新品发布会速览
申请苹果开发者账号的流程
Sign In With Apple(一)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352