Protobuf(Protocol Buffer)是一種數(shù)據(jù)通信協(xié)議腾它,相比JSON报亩,它的傳輸數(shù)據(jù)量更小,而且沒有對應的proto文件根本無法看懂傳輸?shù)亩M制格式數(shù)據(jù),所以傳輸安全性也更高滚局。
Protobuf的安裝就不多做介紹了,這是之前寫的Protobuf安裝踩坑記錄
放一個批量編譯.proto文件的命令:
批量編譯Module目錄下的所有proto文件并輸出protoc -I=./Module --objc_out=./Module ./Module/*.proto
這里主要列出使用其通信時需要處理的情況及常規(guī)寫法顽频,根據(jù)不同公司架構自行調整,下圖是與后臺協(xié)商后的數(shù)據(jù)包裝格式太闺,來源于游戲架構糯景。
HTTP短連接
Request格式
字段 | 長度 | 類型 | 數(shù)值 |
---|---|---|---|
0xff | 4 |
int (int32_t ) |
大端255,小端-1677216 |
playerId | 8 |
long (int64_t ) |
用戶id |
sessionId | 8 |
long (int64_t ) |
會話id |
size | 4 |
int (int32_t ) |
data.length+4 |
commandId | 4 |
int (int32_t ) |
協(xié)議號id |
data | data.length | NSData |
數(shù)據(jù) |
Response格式
字段 | 長度 | 類型 | 數(shù)值 |
---|---|---|---|
0xff | 4 |
int (int32_t ) |
大端255省骂,小端-1677216 |
size | 4 |
int (int32_t ) |
data.length+4 |
commandId | 4 |
int (int32_t ) |
協(xié)議號 |
data | data.length | NSData |
數(shù)據(jù) |
AFNetworking 3.0+Protobuf
這里封裝了一個網(wǎng)絡訪問的方法蟀淮,要注意的是大小端處理,即蘋果系統(tǒng)在傳輸前要先將整數(shù)類型數(shù)據(jù)轉為小端序
- (void)requestProtobufCommandId:(int)commandId
params:(NSData *)params
playerId:(uint64_t)playerID
sessionId:(uint64_t)sessionID
completionHandler:(void(^)(NSData *data))dataBlock
errorHandler:(void(^)(int32_t errorCode))errorBlock {
// 蘋果 整數(shù)字節(jié)使用小端序傳輸钞澳,而其他都是網(wǎng)絡端序大端序傳輸
// HTONS 轉換端序怠惶,從小端序轉為大端序,HTONL 轉化4字節(jié)端序轧粟,HTONLL轉化8字節(jié)端序策治。
// int htonl ->short 類型的主機存儲->網(wǎng)絡的網(wǎng)絡存儲,并寫入內存塊
// char,string 類型不需要轉換
NSMutableData *protobufData = [[NSMutableData alloc] init];
// 0XFF
int str = 0xff;
str = htonl(str);
[protobufData appendBytes:&str length:sizeof(str)];
// playerId
uint64_t playerId = 0;
playerId = htonll(playerId);
NSData *playerIdData = [NSData dataWithBytes: &playerId length: sizeof(playerId)];
[protobufData appendData:playerIdData];
// sessionId
uint64_t sessionId = 0;
sessionId = htonll(sessionId);
NSData *sessionIdData = [NSData dataWithBytes: &sessionId length: sizeof(sessionId)];
[protobufData appendData:sessionIdData];
// size
u_long size = params.length+4;
size = htonl(size);
[protobufData appendBytes:&size length:4];
// commandId
commandId = htonl(commandId);
NSData *commandIdData = [NSData dataWithBytes: &commandId length: sizeof(commandId)];
[protobufData appendData:commandIdData];
// data
[protobufData appendData:params];
Byte *byte = (Byte *)[protobufData bytes];
NSString *byteString = @"";
for (int i=0 ; i<[protobufData length]; i++) {
byteString = [byteString stringByAppendingString:[NSString stringWithFormat:@"%d ",byte[i]]];
}
NSLog(@"byteString: %@",byteString);
//第一步兰吟,創(chuàng)建url
NSURL *url = [NSURL URLWithString:@"192.168.1.1:8080"];
//第二步通惫,創(chuàng)建請求
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:protobufData];
//第三步,連接服務器
AFHTTPSessionManager *_manager = [AFHTTPSessionManager manager];
//不可使用JSON序列化方式
_manager.responseSerializer = [AFHTTPResponseSerializer serializer];
NSURLSessionDataTask *task = [_manager dataTaskWithRequest:request completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
if (error){
NSLog(@"error = %@",error);
dataBlock(nil);
}else{
NSLog(@"protobuf data = %@", responseObject);
NSData *data = responseObject;
if (data.length > 12) {
// normal data
dataBlock([data subdataWithRange:NSMakeRange(12, data.length-12)]);
} else {
dataBlock(nil);
}
}
}];
[task resume];
}
使用方法如下
Test_Req *req = [Test_Req new];
req.param1 = @"0";
req.param2 = 1;
[self requestProtobufCommandId:CommandEnum_CmdTest params:[req data] playerId:0 sessionId:0 completionHandler:^(NSData *data) {
if (data) {
Test_Rsp *rsp = [Test_Rsp parseFromData:data error:nil];
NSLog(@"rsp: %@, result1:%@, result2: %lld", rsp,rsp.result1,rsp.result2);
}
} errorHandler:^(int32_t errorCode) {
NSLog(@"errorCode: %d",errorCode);
}];
這里要注意必須使用Req對應的Rsp去解析返回的數(shù)據(jù)混蔼,如果協(xié)議號不相同則會解析失敗履腋,得到的只會是一段亂碼
Socket長連接
約定的Request與Response格式是一樣的
字段 | 長度 | 類型 | 數(shù)值 |
---|---|---|---|
0xff | 4 |
int (int32_t ) |
大端255,小端-1677216 |
size | 4 |
int (int32_t ) |
data.length+4 |
commandId | 4 |
int (int32_t ) |
協(xié)議號 |
data | data.length | NSData |
數(shù)據(jù) |
CocoaAsyncSocket+TCP+Protobuf
這里使用的是CocoaAsyncSocket惭嚣,并提供了二次封裝類的寫法遵湖。
//初始化socket并連接到服務端
- (void)initSocket {
GCDAsyncSocket *gcdSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
}
#pragma mark - 對外的一些接口
//建立連接
- (BOOL)connect {
return [gcdSocket connectToHost:@"192.168.1.1" onPort:8080 error:nil];
}
//斷開連接
- (void)disConnect {
[gcdSocket disconnect];
}
#pragma mark - GCDAsyncSocketDelegate
//連接成功調用
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"連接成功,host:%@,port:%d",host,port);
//監(jiān)聽讀數(shù)據(jù)的代理 -1永遠監(jiān)聽,不超時晚吞,但是只收一次消息延旧,
//所以每次接受到消息還得調用一次
[gcdSocket readDataWithTimeout:-1 tag:0];
//心跳寫在這...
}
//斷開連接的時候調用
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err {
NSLog(@"斷開連接,host:%@,port:%d",sock.localHost,sock.localPort);
//斷線重連寫在這...
if (err) {
//再次重連
} else {
//正常斷開
}
}
CocoaAsyncSocket提供了讀數(shù)據(jù)和寫數(shù)據(jù)的方法
-
[gcdSocket readDataWithTimeout:-1 tag:0];
用來收取消息,-1表示永遠不超時槽地, -
[gcdSocket writeData:protobufData withTimeout:-1 tag:0];
用來寫入數(shù)據(jù)垄潮,將數(shù)據(jù)按要求轉化為protobuf data即可
發(fā)送協(xié)議的方法如下
//發(fā)送數(shù)據(jù)流和協(xié)議號
- (void)sendProbufData:(NSData *)data
CommandId:(int)commandId {
NSMutableData *protobufData = [[NSMutableData alloc] init];
// 0XFF
int str = 0xff;
str = htonl(str);
[protobufData appendBytes:&str length:sizeof(str)];
// size
u_long size = data.length+4;
size = htonl(size);
[protobufData appendBytes:&size length:4];
// commandId
commandId = htonl(commandId);
NSData *commandIdData = [NSData dataWithBytes: &commandId length: sizeof(commandId)];
[protobufData appendData:commandIdData];
// data
[protobufData appendData:data];
Byte *byte = (Byte *)[protobufData bytes];
NSString *byteString = @"";
for (int i=0 ; i<[protobufData length]; i++) {
byteString = [byteString stringByAppendingString:[NSString stringWithFormat:@"%d ",byte[i]]];
}
NSLog(@"byteString: %@",byteString);
// 寫入數(shù)據(jù)
[gcdSocket writeData:protobufData withTimeout:-1 tag:0];
}
嘗試向服務端發(fā)送一個心跳包,寫法如下
- (void)heartBeat {
//生成一個空的請求占位
Empty_Req *req = [Empty_Req new];
[self sendProbufData:[req data] CommandId:CommandEnum_CmdHeartBeat];
}
收到消息處理數(shù)據(jù)
//收到消息的回調
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
NSLog(@"收到消息:%@",data);
if (data.length > 12) {
NSData *cmdIdData = [data subdataWithRange:NSMakeRange(8, 4)];
int j; // j為協(xié)議號的數(shù)值
[cmdIdData getBytes: &j length: sizeof(j)];
j = htonl(j);
NSData *sizeData = [data subdataWithRange:NSMakeRange(4, 4)];
int i; //i為協(xié)議號與數(shù)據(jù)流字節(jié)的長度之和闷盔,在有拼接數(shù)據(jù)時使用
[sizeData getBytes: &i length: sizeof(i)];
i = htonl(i);
if (j == CommandEnum_CmdHeartBeat) {
// 心跳包不做處理
NSLog(@"收到了心跳包!");
} else {
// 這里解析正常數(shù)據(jù)...
}
}
//繼續(xù)接收數(shù)據(jù)
[gcdSocket readDataWithTimeout:-1 tag:0];
}
地址
@黑花白花 的博文Runtime在實際開發(fā)中的應用第一節(jié)提供了一個Protobuf解析器弯洗,幫助PB編譯出Model與普通Model之間相互轉換:PBPaser