如何解析一个自定义协议
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;
}
}
- 在TCP连接回调中[self handleReceivedData:data fromHost:sock.connectedHost];不断被执行,所接到的数据要不断追加到一个NSMutableData变量rsvData中;
- 调parseRsvData: toPropDict: length:来解析协议时,需要在rsvData的头部向尾部依次解析;
- 收到数据,解析...是一个循环不断的过程,如果解析一个数据包成功,则从rsvData中把相应的数据段删掉;
我们解析了一整个数据包,至此,一个简单的协议操作结束了。当然,你还能能设计出一个更复杂的协议,也能优化这个解析协议的过程。