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中把相应的数据段删掉;

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


了解更多iOS及相关新技术,请关注我们的公众号:

小编微信:可加并拉入《QiShare技术交流群》。

关注我们的途径有:
QiShare(简书)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公众号)

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