iOS 解析一個自定義協議

級別: ★★☆☆☆
標籤:「iOS」「自定義協議」「QSIOP」
做者: dac_1033
審校: QiShare團隊php


1. 關於協議

在咱們學習計算機網絡的過程當中,涉及到不少協議,好比HTTP/HTTPs、TCP、UDP、IP等。不一樣的協議可能工做在不一樣的網絡層次上,各個協議之因此稱爲協議,是由於這是一套規則,信息在端到端(如不一樣的PC之間)進行傳輸的過程當中,同等的層次之間經過使用這套一樣的規則,使兩個端都知道這一層的數據因該怎麼處理,處理成什麼格式。git

網絡通訊的層次

TCP報文格式

好比,上圖的TCP協議工做在傳輸層,那麼在兩個PC端的傳輸層,均可以經過TCP協議規定的報文格式來打包/封裝上層傳下來的數據,而且,也均可以拆包/解析下層傳上來的數據。github

2. 自定義一個協議

在移動端的開發過程當中,有時會要求開發者解析一個自定義協議的狀況。一般這個協議是創建在TCP鏈接基礎之上的,下面以一個簡單的信息通訊協議舉個🌰:objective-c

通訊基於一條持久 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......,那麼真實的數據包內容容以下:app

字段 ver op propl prop
字節數 2 2 2 pl

就簡單的定義這麼幾條,總的來講,這個協議是個變長的協議。你也能夠定義一個定長的協議,即不論每一個報文內容是什麼,每一個報文長度一致。格式不一,也個有優缺點。 咱們給這個簡要的協議起個名字:QSIOP(QiShare I/O Protocol)。🤪socket

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中把相應的數據段刪掉;

咱們解析了一整個數據包,至此,一個簡單的協議操做結束了。固然,你還能能設計出一個更復雜的協議,也能優化這個解析協議的過程。


小編微信:可加並拉入《QiShare技術交流羣》。

關注咱們的途徑有:
QiShare(簡書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公衆號)

推薦文章:
iOS13 DarkMode適配(二)
iOS13 DarkMode適配(一)
2019蘋果秋季新品發佈會速覽
申請蘋果開發者帳號的流程
Sign In With Apple(一)
奇舞週刊

相關文章
相關標籤/搜索