级别: ★★☆☆☆
标签:「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(一)
奇舞周刊