標籤:編碼 socket 架構 ios tcp
CocoaAsyncSocket網路通訊使用之資料編碼和解碼(二)
在上一篇CocoaAsyncSocket網路通訊使用之tcp串連(一)中,我們已經利用CocoaAsyncSocket封裝了自己的socket connection。
本篇主要是通過引入編碼器和解碼器,將可以共用的內容模組化。
簡述:
在tcp的應用中,都是以二機制位元組的形式來對資料做傳輸。
一般會針對業務協議構造對應的資料結構/資料對象,然後在使用的時候針對協議轉換成位元據發送給服務端。
但是我們在不同的app中,不同的業務情境使用不同的tcp協議,這樣每次socket模組的重用性就特別差,即使是完全一樣的底層內容,也因為實現的時候耦合性太高,而導致需要全部重新開發。為了實現模組化的重用,我仿照mina和netty,引入編碼器和解碼器。
介面架構設計:
為了後續擴充和自訂實現自己的編碼器/解碼器,有了以下的設計介面。
資料包
資料包基本介面定義( RHSocketPacket.h):
#import <Foundation/Foundation.h>@protocol RHSocketPacket <NSObject>@property (nonatomic, assign, readonly) NSInteger tag;@property (nonatomic, strong, readonly) NSData *data;- (instancetype)initWithData:(NSData *)data;@optional- (void)setTag:(NSInteger)tag;- (void)setData:(NSData *)data;@end
資料包內容介面定義(RHSocketPacketContent.h):(增加timeout逾時欄位,主要是針對發送的資料包)
#import <Foundation/Foundation.h>#import "RHSocketPacket.h"@protocol RHSocketPacketContent <RHSocketPacket>@property (nonatomic, readonly) NSTimeInterval timeout;@optional- (void)setTimeout:(NSTimeInterval)timeout;@end
tcp編碼器
編碼器介面定義( RHSocketEncoderProtocol.h):
#import <Foundation/Foundation.h>#import "RHSocketPacketContent.h"@protocol RHSocketEncoderOutputDelegate <NSObject>@required- (void)didEncode:(NSData *)data timeout:(NSTimeInterval)timeout tag:(NSInteger)tag;@end@protocol RHSocketEncoderProtocol <NSObject>@required- (void)encodePacket:(id<RHSocketPacketContent>)packet encoderOutput:(id<RHSocketEncoderOutputDelegate>)output;@end
tcp解碼器
解碼器介面定義( RHSocketDecoderProtocol.h):
#import <Foundation/Foundation.h>#import "RHSocketPacketContent.h"@protocol RHSocketDecoderOutputDelegate <NSObject>@required- (void)didDecode:(id<RHSocketPacketContent>)packet tag:(NSInteger)tag;@end@protocol RHSocketDecoderProtocol <NSObject>@required- (NSUInteger)decodeData:(NSData *)data decoderOutput:(id<RHSocketDecoderOutputDelegate>)output tag:(long)tag;//這裡有傳回值,是了為了處理資料包拼包@end
ok,經過幾次調整,程式員內心無數次糾結後,介面定義終於完成了,接下來我們看看怎麼組合使用。
前面的socket connection在使用時,還是需要實現delegate的委託方法的,
在不同的app間使用還是需要copy,再實現資料[編碼]、[解碼]、[分發],
然後才到對應的情境。其實在[編碼]、[分發]之前都是可以模組化獨立的,我們就從這裡入手。
架構整合調用
首先引入一個service,來幫我們做[串連]、[編碼]、[解碼]、[分發]的事情。
廢話不多說,直接貼代碼。
RHSocketService.h檔案:
#import <Foundation/Foundation.h>#import "RHSocketEncoderProtocol.h"#import "RHSocketDecoderProtocol.h"extern NSString *const kNotificationSocketServiceState;extern NSString *const kNotificationSocketPacketRequest;extern NSString *const kNotificationSocketPacketResponse;@interface RHSocketService : NSObject <RHSocketEncoderOutputDelegate, RHSocketDecoderOutputDelegate>@property (nonatomic, copy) NSString *serverHost;@property (nonatomic, assign) int serverPort;@property (nonatomic, strong) id<RHSocketEncoderProtocol> encoder;@property (nonatomic, strong) id<RHSocketDecoderProtocol> decoder;@property (assign, readonly) BOOL isRunning;+ (instancetype)sharedInstance;- (void)startServiceWithHost:(NSString *)host port:(int)port;- (void)stopService;- (void)asyncSendPacket:(id<RHSocketPacketContent>)packet;@end
RHSocketService.m檔案:
#import "RHSocketService.h"#import "RHSocketConnection.h"#import "RHSocketDelimiterEncoder.h"#import "RHSocketDelimiterDecoder.h"NSString *const kNotificationSocketServiceState = @"kNotificationSocketServiceState";NSString *const kNotificationSocketPacketRequest = @"kNotificationSocketPacketRequest";NSString *const kNotificationSocketPacketResponse = @"kNotificationSocketPacketResponse";@interface RHSocketService () <RHSocketConnectionDelegate>{ RHSocketConnection *_connection;}@end@implementation RHSocketService+ (instancetype)sharedInstance{ static id sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance;}- (instancetype)init{ if (self = [super init]) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(detectSocketPacketRequest:) name:kNotificationSocketPacketRequest object:nil]; _encoder = [[RHSocketDelimiterEncoder alloc] init]; _decoder = [[RHSocketDelimiterDecoder alloc] init]; } return self;}- (void)dealloc{ [[NSNotificationCenter defaultCenter] removeObserver:self];}- (void)startServiceWithHost:(NSString *)host port:(int)port{ NSAssert(_encoder, @"error, _encoder is nil..."); NSAssert(_decoder, @"error, _decoder is nil..."); NSAssert(host.length > 0, @"error, host is nil..."); if (_isRunning) { return; } _serverHost = host; _serverPort = port; [self openConnection];}- (void)stopService{ _isRunning = NO; [self closeConnection];}- (void)asyncSendPacket:(id<RHSocketPacketContent>)packet{ if (!_isRunning) { NSDictionary *userInfo = @{@"msg":@"Send packet error. Service is stop!"}; NSError *error = [NSError errorWithDomain:@"RHSocketService" code:1 userInfo:userInfo]; [self didDisconnectWithError:error]; return; } [_encoder encodePacket:packet encoderOutput:self];}#pragma mar -#pragma mark recevie response data- (void)detectSocketPacketRequest:(NSNotification *)notif{ id object = notif.object; [self asyncSendPacket:object];}#pragma mark -#pragma mark RHSocketConnection method- (void)openConnection{ [self closeConnection]; _connection = [[RHSocketConnection alloc] init]; _connection.delegate = self; [_connection connectWithHost:_serverHost port:_serverPort];}- (void)closeConnection{ if (_connection) { _connection.delegate = nil; [_connection disconnect]; _connection = nil; }}#pragma mark -#pragma mark RHSocketConnectionDelegate method- (void)didDisconnectWithError:(NSError *)error{ RHSocketLog(@"didDisconnectWithError: %@", error); _isRunning = NO; NSDictionary *userInfo = @{@"isRunning":@(_isRunning), @"error":error}; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationSocketServiceState object:@(_isRunning) userInfo:userInfo];}- (void)didConnectToHost:(NSString *)host port:(UInt16)port{ _isRunning = YES; NSDictionary *userInfo = @{@"host":host, @"port":@(port), @"isRunning":@(_isRunning)}; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationSocketServiceState object:@(_isRunning) userInfo:userInfo];}- (void)didReceiveData:(NSData *)data tag:(long)tag{ NSUInteger remainDataLen = [_decoder decodeData:data decoderOutput:self tag:tag]; if (remainDataLen > 0) { [_connection readDataWithTimeout:-1 tag:tag]; } else { [_connection readDataWithTimeout:-1 tag:0]; }}#pragma mark -#pragma mark RHSocketEncoderOutputDelegate method- (void)didEncode:(NSData *)data timeout:(NSTimeInterval)timeout tag:(NSInteger)tag{ [_connection writeData:data timeout:timeout tag:tag];}#pragma mark -#pragma mark RHSocketDecoderOutputDelegate method- (void)didDecode:(id<RHSocketPacketContent>)packet tag:(NSInteger)tag{ NSDictionary *userInfo = @{@"RHSocketPacketBody":packet, @"tag":@(tag)}; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationSocketPacketResponse object:nil userInfo:userInfo];}@end
測試代碼如下:
NSString *host = @"www.baidu.com";int port = 80;[[RHSocketService sharedInstance] startServiceWithHost:host port:port];
[RHSocketHttpService sharedInstance].encoder = [[RHSocketHttpEncoder alloc] init];[RHSocketHttpService sharedInstance].decoder = [[RHSocketHttpDecoder alloc] init];[[RHSocketHttpService sharedInstance] startServiceWithHost:host port:port];
代碼調用方法和過程說明:
1-通過startServiceWithHost方法,可實現對伺服器的串連。
2-用戶端給服務端發送資料,在串連成功後,可以調用asyncSendPacket方法發送資料包。也可以通過notification發送kNotificationSocketPacketRequest通知,發送資料包。在發送資料的過程中,會通過編碼器,編碼完成後通過connection發送給服務端。
3-服務端向用戶端推送資料,會觸發didReceiveData方法的回調,通過解碼器,解碼完成後發出通知給對應的分發器(分發器針對業務調整實現)
代碼中是預設的解碼器和編碼器,針對資料中的分隔字元處理,也可以自訂分隔字元和資料幀的最大值,代碼如下:
分隔字元編碼器
RHSocketDelimiterEncoder.h檔案:
#import <Foundation/Foundation.h>#import "RHSocketEncoderProtocol.h"/** * 針對資料包分隔字元編碼器 * 預設資料包中每幀最大值為8192(maxFrameSize == 8192) * 預設資料包每幀分隔字元為0xff(delimiter == 0xff) */@interface RHSocketDelimiterEncoder : NSObject <RHSocketEncoderProtocol>@property (nonatomic, assign) NSUInteger maxFrameSize;@property (nonatomic, assign) uint8_t delimiter;@end
RHSocketDelimiterEncoder.m檔案:
#import "RHSocketDelimiterEncoder.h"#import "RHSocketConfig.h"@implementation RHSocketDelimiterEncoder- (instancetype)init{ if (self = [super init]) { _maxFrameSize = 8192; _delimiter = 0xff; } return self;}- (void)encodePacket:(id<RHSocketPacketContent>)packet encoderOutput:(id<RHSocketEncoderOutputDelegate>)output{ NSData *data = [packet data]; NSMutableData *sendData = [NSMutableData dataWithData:data]; [sendData appendBytes:&_delimiter length:1]; NSAssert(sendData.length < _maxFrameSize, @"Encode frame is too long..."); NSTimeInterval timeout = [packet timeout]; NSInteger tag = [packet tag]; RHSocketLog(@"tag:%ld, timeout: %f, data: %@", (long)tag, timeout, data); [output didEncode:data timeout:timeout tag:tag];}@end
分隔字元解碼器
RHSocketDelimiterDecoder.h檔案:
#import <Foundation/Foundation.h>#import "RHSocketDecoderProtocol.h"/** * 針對資料包分隔字元解碼器 * 預設資料包中每幀最大值為8192(maxFrameSize == 8192) * 預設資料包每幀分隔字元為0xff(delimiter == 0xff) */@interface RHSocketDelimiterDecoder : NSObject <RHSocketDecoderProtocol>@property (nonatomic, assign) NSUInteger maxFrameSize;@property (nonatomic, assign) uint8_t delimiter;@end
RHSocketDelimiterDecoder.m檔案:
#import "RHSocketDelimiterDecoder.h"#import "RHPacketBody.h"@interface RHSocketDelimiterDecoder (){ NSMutableData *_receiveData;}@end@implementation RHSocketDelimiterDecoder- (instancetype)init{ if (self = [super init]) { _maxFrameSize = 8192; _delimiter = 0xff; } return self;}- (NSUInteger)decodeData:(NSData *)data decoderOutput:(id<RHSocketDecoderOutputDelegate>)output tag:(long)tag{ @synchronized(self) { if (_receiveData) { [_receiveData appendData:data]; } else { _receiveData = [NSMutableData dataWithData:data]; } NSUInteger dataLen = _receiveData.length; NSInteger headIndex = 0; for (NSInteger i=0; i<dataLen; i++) { NSAssert(i < _maxFrameSize, @"Decode frame is too long..."); uint8_t byte; [_receiveData getBytes:&byte range:NSMakeRange(i, 1)]; if (byte == _delimiter) { NSInteger packetLen = i - headIndex; NSData *packetData = [_receiveData subdataWithRange:NSMakeRange(headIndex, packetLen)]; RHPacketBody *body = [[RHPacketBody alloc] initWithData:packetData]; [output didDecode:body tag:0]; headIndex = i + 1; } } NSData *remainData = [_receiveData subdataWithRange:NSMakeRange(headIndex, dataLen-headIndex)]; [_receiveData setData:remainData]; return _receiveData.length; }//@synchronized}@end
總結:
目前沒來得及提供伺服器代碼,不過socket架構已經基本完整,可以根據不同的協議擴充實現自訂的編碼器和解碼器。
測試代碼中訪問的是百度的首頁,為http協議,需要額外實現http協議的編碼器和解碼器。
下一篇,我們來實現針對http的簡單編碼器和解碼器。
--------------------
轉載請註明出處,謝謝
email: [email protected]
qq: 410289616
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
CocoaAsyncSocket網路通訊使用之資料編碼和解碼(二)