級別: ★★☆☆☆
標籤:「iOS」「自定義協議」「QSIOP」
做者: dac_1033
審校: QiShare團隊php
在咱們學習計算機網絡的過程當中,涉及到不少協議,好比HTTP/HTTPs、TCP、UDP、IP等。不一樣的協議可能工做在不一樣的網絡層次上,各個協議之因此稱爲協議,是由於這是一套規則,信息在端到端(如不一樣的PC之間)進行傳輸的過程當中,同等的層次之間經過使用這套一樣的規則,使兩個端都知道這一層的數據因該怎麼處理,處理成什麼格式。git
好比,上圖的TCP協議工做在傳輸層,那麼在兩個PC端的傳輸層,均可以經過TCP協議規定的報文格式來打包/封裝上層傳下來的數據,而且,也均可以拆包/解析下層傳上來的數據。github
在移動端的開發過程當中,有時會要求開發者解析一個自定義協議的狀況。一般這個協議是創建在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
在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(二進制)的形式進行的。
因爲GCDAsyncSocket庫維持的TCP鏈接中,傳輸的數據都是以二進制的形式,8位二進制是一個字節,好比QSIOP中的ver佔兩個字節,那麼就要有一個兩個字節的變量去接收它。首先,咱們熟悉一下,iOS中關於所佔不一樣字節的整型數據經常使用類型的定義:
typedef unsigned short UInt16;
typedef unsigned int UInt32;
typedef unsigned long long UInt64;
複製代碼
其中,UInt16佔兩個字節,UInt32佔四個字節,UInt64佔八個字節。固然,也有其餘類型能夠用於接受解析出來的數據。
+ (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;
}
複製代碼
根據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;
}
}
複製代碼
- 在TCP鏈接回調中[self handleReceivedData:data fromHost:sock.connectedHost];不斷被執行,所接到的數據要不斷追加到一個NSMutableData變量rsvData中;
- 調parseRsvData: toPropDict: length:來解析協議時,須要在rsvData的頭部向尾部依次解析;
- 收到數據,解析...是一個循環不斷的過程,若是解析一個數據包成功,則從rsvData中把相應的數據段刪掉;
咱們解析了一整個數據包,至此,一個簡單的協議操做結束了。固然,你還能能設計出一個更復雜的協議,也能優化這個解析協議的過程。
小編微信:可加並拉入《QiShare技術交流羣》。
關注咱們的途徑有:
QiShare(簡書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公衆號)
推薦文章:
iOS13 DarkMode適配(二)
iOS13 DarkMode適配(一)
2019蘋果秋季新品發佈會速覽
申請蘋果開發者帳號的流程
Sign In With Apple(一)
奇舞週刊