美文网首页
如何解析一个自定义协议

如何解析一个自定义协议

作者: 大成小栈 | 来源:发表于2019-09-25 14:39 被阅读0次

    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];
        }
        [paramsStr appendString:[NSString stringWithFormat:@"ts:%@", [self getTsStr]]];
        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字段有内容的数据包:

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

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

    相关文章

      网友评论

          本文标题:如何解析一个自定义协议

          本文链接:https://www.haomeiwen.com/subject/cyziuctx.html