iOS-從零開始自建TCP通道

目錄

  • 前言
  • TCP通道的建立
  • 自定義應(yīng)用層協(xié)議
  • 請求體
  • 響應(yīng)體
  • 請求和響應(yīng)的序列化
    • 序列化器
    • 請求的序列化
    • 響應(yīng)的序列化
  • 任務(wù)機(jī)制
    • KTTCPSocketTask
    • 任務(wù)超時(shí)
  • 管理器
    • KTTCPSocketManager
    • 請求的發(fā)送
    • 響應(yīng)的接收
    • 將響應(yīng)派發(fā)給對(duì)應(yīng)任務(wù)
  • Demo
  • 參考資料

前言

以前用的Websocket、簡單粗暴。如果你只想要一個(gè)全雙工的TCP長連接、Websocket作為和HTTP一樣的應(yīng)用層協(xié)議完全夠用。

但本文主要是嘗試自己用socket(雖然并不是完全原生)構(gòu)建一個(gè)能夠像HTTP請求一樣使用的TCP通道。并且最終、將HTTP請求放在自建的TCP加密通道上傳輸。

關(guān)于網(wǎng)絡(luò)層一些基礎(chǔ)知識(shí)、或許《當(dāng)被尬聊網(wǎng)絡(luò)協(xié)議、我們可以侃點(diǎn)什么?》可以幫到你。

自己對(duì)Socket通道的建設(shè)一開始也不太懂、所以有很多地方借鑒了《一步一步構(gòu)建你的iOS網(wǎng)絡(luò)層 - TCP篇》的思路。十分感謝


TCP通道的建立

首先、我們需要一個(gè)類似websocket的應(yīng)用層協(xié)議。
參照SRWebSocket來看、除了全雙工通信之外。我們還需要處理心跳、重連、粘包這三個(gè)特殊的概念(SSL在CocoaAsyncSocket下已經(jīng)封裝了實(shí)現(xiàn))。

此外。由于原生socket比較麻煩、所以借助了一個(gè)開源框架CocoaAsyncSocket來操作scoket(類似NSLayoutConstraintMasonry的關(guān)系)。具體使用的是基于GCD的GCDAsyncSocket(似乎以前還有個(gè)基于Runloop的AsyncSocket、但是我用的時(shí)候已經(jīng)沒有了。大概和NSURLCollection被NSURLSession淘汰了一樣)。

CocoaAsyncSocket初始狀態(tài)下就具備連接、斷開、發(fā)送以及讀取等基本功能。
這里主要對(duì)CocoaAsyncSocket添加了重連、專屬線程等易用性的封裝、并且將scoket事件通過代理進(jìn)行回調(diào)。

頭文件

@class KTTCPSocket;
@protocol KTTCPSocketDelegate <NSObject>

@optional

/**
 鏈接成功

 @param sock KTTCPSocket
 @param host 主機(jī)
 @param port 端口
 */
- (void)socket:(KTTCPSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port;


/**
 最終鏈接失敗
 連接失敗 + N次重連失敗

 @param sock KTTCPSocket
 */
- (void)socketCanNotConnectToService:(KTTCPSocket *)sock;


/**
 鏈接失敗并重連

 @param sock KTTCPSocket
 @param error error
 */
- (void)socketDidDisconnect:(KTTCPSocket *)sock error:(NSError *)error;


/**
 接收到了數(shù)據(jù)

 @param sock KTTCPSocket
 @param data 二進(jìn)制數(shù)據(jù)
 */
- (void)socket:(KTTCPSocket *)sock didReadData:(NSData *)data;

@end


/**
 對(duì)GCDAsyncSocket進(jìn)行封裝的工具類。
 具備自動(dòng)重連、讀寫數(shù)據(jù)等基礎(chǔ)操作
 */
@interface KTTCPSocket : NSObject

@property (nonatomic,readonly) NSString *host;//主機(jī)
@property (nonatomic,readonly) uint16_t port;//端口
@property (nonatomic) NSUInteger maxRetryCount;//重連次數(shù)
@property (nonatomic, weak) id<KTTCPSocketDelegate> delegate;


- (instancetype)init NS_UNAVAILABLE;

/**
 構(gòu)造方法

 @param host 主機(jī)號(hào)
 @param port 端口號(hào)
 @return KTTCPSocket實(shí)例
 */
- (instancetype)initSocketWithHost:(NSString *)host port:(uint16_t)port NS_DESIGNATED_INITIALIZER;


/**
    關(guān)閉連接--注意關(guān)閉之后就沒辦法再次開啟了。不然沒辦法判斷socke對(duì)象該何時(shí)銷毀
 */
- (void)close;


/**
    連接
 */
- (void)connect;

/**
    重連并且重置次數(shù)
 */
- (void)reconnect;


/**
    鏈接狀態(tài)

 @return 是否已經(jīng)鏈接
 */
- (BOOL)isConnected;


/**
 寫入數(shù)據(jù)

 @param data 二進(jìn)制數(shù)據(jù)
 */
- (void)writeData:(NSData *)data;
@end

業(yè)務(wù)代碼

  • 寫入數(shù)據(jù)
- (void)writeData:(NSData *)data {
    if (data.length == 0) { return; }
    
    [self.socket writeData:data withTimeout:-1 tag:socketTag];
}

由于TCP面向字節(jié)流、所以并不需要我們調(diào)用發(fā)送之類的方法、他會(huì)按照順序一個(gè)字節(jié)一個(gè)字節(jié)的把數(shù)據(jù)進(jìn)行傳輸。

  • 讀取數(shù)據(jù)
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    
    if ([self.delegate respondsToSelector:@selector(socket:didReadData:)]) {
        [self.delegate socket:self didReadData:data];
    }
    
    [self.socket readDataWithTimeout:-1 tag:socketTag];
}

readDataWithTimeout方法會(huì)持續(xù)監(jiān)聽一次緩存區(qū)、當(dāng)接收到數(shù)據(jù)立刻通過代理交付。這里也就相當(dāng)于遞歸調(diào)用了。

  • 重連
鏈接失敗的重連:
//鏈接失敗
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error {
//    NSLog(@"TCPSocket--連接已斷開.error:%@", error);
    
    if ([self.delegate respondsToSelector:@selector(socketDidDisconnect:error:)]) {
        [self.delegate socketDidDisconnect:self error:error];
    }
    [self tryToReconnect];
}

//嘗試自動(dòng)重連
- (void)tryToReconnect {
    if (self.isConnecting || !self.isNetworkReachable) {
        return;
    }
    
    self.currentRetryCount -= 1;
    //如果還有嘗試次數(shù)就自動(dòng)重連
    if (self.currentRetryCount >= 0) {
        NSLog(@"嘗試重連");
        [self connect];
    } else if ([self.delegate respondsToSelector:@selector(socketCanNotConnectToService:)]) {
        //自動(dòng)重連失敗
        NSLog(@"重連失敗");
        [self.delegate socketCanNotConnectToService:self];
    }
}

連接失敗會(huì)監(jiān)聽重連次數(shù)、超過次數(shù)則宣告失敗

網(wǎng)絡(luò)波動(dòng)的重連:
//網(wǎng)絡(luò)波動(dòng)
- (void)didReceivedNetworkChangedNotification:(NSNotification *)notif {
    [self reconnectIfNeed];
}

//切換到后臺(tái)
- (void)didReceivedAppBecomeActiveNotification:(NSNotification *)notif {
    [self reconnectIfNeed];
}

- (void)reconnectIfNeed {

    if (self.isConnecting || self.isConnected) { return; }
  
    [self reconnect];
}

網(wǎng)絡(luò)波動(dòng)會(huì)重置連接次數(shù)并重連

  • 線程的常駐
- (void)socketWillBeConnect {
    if (self.socketThread == nil) {
        //保存異步線程
        self.socketThread = [NSThread currentThread];
        [[NSRunLoop currentRunLoop] addPort:self.machPort forMode:NSDefaultRunLoopMode];
        while (self.machPort) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
    }
}

由于為長連接新開辟了一個(gè)線程、所以需要使用Runloop來維持線程的生存。


自定義通訊協(xié)議報(bào)文

這里需要解釋一下TCP的兩個(gè)概念

面向字節(jié)流傳輸

TCP協(xié)議將數(shù)據(jù)看做有序排列的二進(jìn)制位、并按照8位分割成有序的字節(jié)流。

就像之前在談到寫入數(shù)據(jù)的時(shí)候說的一樣、你并不需要主動(dòng)調(diào)用發(fā)送函數(shù)。Socket在接收到數(shù)據(jù)的時(shí)候就會(huì)直接按照流的模式發(fā)送以數(shù)據(jù)段

TCP緩沖區(qū)

應(yīng)用層提供給TCP協(xié)議的數(shù)據(jù)會(huì)被先放入緩沖區(qū)中、并沒有真正的發(fā)送。只有在合適的時(shí)候或者應(yīng)用程序顯示地要求將數(shù)據(jù)發(fā)送時(shí)、TCP才會(huì)將數(shù)據(jù)組織成合適的數(shù)據(jù)段發(fā)送出去。
對(duì)于接收方、在正式交付給上層應(yīng)用之前、接收到的數(shù)據(jù)也會(huì)被放在緩沖區(qū)備用。

上圖中、"未發(fā)送"部分的數(shù)據(jù)、就是存放在緩沖區(qū)的

總之、接收方的socket永遠(yuǎn)不可能知道“發(fā)送端發(fā)送的數(shù)據(jù)包長”

如果發(fā)送方這樣發(fā)送:

while (1) {
        [self writeData:@"123"];
    }

假設(shè)接收方緩沖區(qū)為10個(gè)長度
那么他將接收到1231231231、2312312312、3123123123
這也就是我們所說的粘包

什么是報(bào)文

我們可以先來看看TCP數(shù)據(jù)段的報(bào)文格式


簡而言之。報(bào)文0000040200000401000000287b226d736d73223這樣的16進(jìn)制字符串、而報(bào)文格式也就是關(guān)于報(bào)文該如何解釋的一套規(guī)定。

自定義通訊協(xié)議的報(bào)文格式

以本文的Demo舉個(gè)例子:

#define ReqTypeLengthForDemo (4)/** 消息類型的長度 */
#define IdentifierLengthForDemo (4)/** 消息序號(hào)的長度 */
#define ContentLengthForDemo (4)/** 消息有效載荷的長度 */
#define HeaderLengthForDemo (ReqTypeLengthForDemo + IdentifierLengthForDemo + ContentLengthForDemo)/** Demo消息響應(yīng)的頭部長度 */

當(dāng)然、你也可以設(shè)計(jì)的再復(fù)雜一些。包括協(xié)議版本、內(nèi)容類型、校驗(yàn)和等等元素:

#define ReqTypeLength (4)/** 消息類型的長度 */
#define VersionLength (4)/** 協(xié)議版本號(hào)的長度 */
#define IdentifierLength (4)/** 消息序號(hào)的長度 */
#define ContentTypeLength (4)/** 內(nèi)容類型的長度 */
#define VerifyLength (32)/** 校驗(yàn)和的長度 */
#define ContentLength (4)/** 消息有效載荷的長度 */
#define HeaderLength (ReqTypeLength + VersionLength + IdentifierLength + ContentTypeLength + VerifyLength + ContentLength)/** 消息響應(yīng)的頭部長度 */

請求體

這里我仿造了NSURLRequest進(jìn)行設(shè)計(jì)、希望通過KTTCPSocketRequest可以直接進(jìn)行TCP通信。在極簡狀態(tài)下他應(yīng)該長這樣:


//通訊類型標(biāo)識(shí)符
typedef enum : NSUInteger {
    //    心跳
    KTTCP_type_heatbeat = 0x00000001,
    KTTCP_type_notification_xxx = 0x00000002,
    KTTCP_type_notification_yyy = 0x00000003,
    KTTCP_type_notification_zzz = 0x00000004,
    
    //    通知類型最多到400
    KTTCP_type_max_notification = 0x00000400,
    
    KTTCP_type_dictionary = 0x00000402,//內(nèi)容為字典類型
    
    KTTCP_type_http_get = 0x00000403//內(nèi)容為字典類型
    
} KTTCPSocketRequestType;


/**
 將單次TCP需要發(fā)送的資源進(jìn)行整合、類似NSURLRequest的作用
 */
@interface KTTCPSocketRequest : NSObject

@property (nonatomic, assign) NSUInteger timeoutInterval;//超時(shí)

/**
 請求構(gòu)造方法

 @param type 請求類型
 @param parameters 內(nèi)容數(shù)據(jù)
 @return 請求實(shí)例
 */
+(instancetype)requestWithType:(KTTCPSocketRequestType)type parameters:(NSDictionary *)parameters;


@end

一個(gè)超時(shí)時(shí)間屬性、一個(gè)根據(jù)參數(shù)以及請求類型實(shí)例化的構(gòu)造方法。


響應(yīng)體

為了適應(yīng)不同的通訊協(xié)議類型、我使用了基類和繼承的方式:

/**
    響應(yīng)體基類、不提供使用
 */
@interface KTTCPSocketResponse : NSObject


@property (nonatomic,readonly) KTTCPSocketRequestType type;//響應(yīng)類型
@property (nonatomic,readonly) NSNumber *requestIdentifier;//序列號(hào)
@property (nonatomic,readonly) NSData *content;//內(nèi)容


@end

/**
    某一應(yīng)用協(xié)議的響應(yīng)體
 */
@interface KTTCPSocketResponseForXXX : KTTCPSocketResponse

@property (nonatomic,readonly) KTTCPSocketContentType contentType;//內(nèi)容類型
@property (nonatomic,readonly) BOOL verify;//校驗(yàn)和情況
@property (nonatomic,readonly) KTTCPSocketVersion version;//協(xié)議版本號(hào)


/**
 對(duì)響應(yīng)體進(jìn)行初始化
 
 @param data 數(shù)據(jù)包
 @param ipAddress 數(shù)據(jù)包源地址
 @return 響應(yīng)體
 */
+ (instancetype)responseWithData:(NSData *)data ipAddress:(NSString *)ipAddress;
@end




/****************<# Demo #>********************/
/**
 某一應(yīng)用協(xié)議的響應(yīng)體
 */
@interface KTTCPSocketResponseForDemo : KTTCPSocketResponse



/**
 對(duì)響應(yīng)體進(jìn)行初始化
 
 @param data 數(shù)據(jù)包
 @return 響應(yīng)體
 */
+ (instancetype)responseWithData:(NSData *)data;
@end

針對(duì)不同的通訊協(xié)議結(jié)構(gòu)、使用不同的響應(yīng)體進(jìn)行解析。


請求和響應(yīng)的序列化

通俗來講、就是將請求體對(duì)象轉(zhuǎn)化成需要發(fā)送的數(shù)據(jù)包、以及將接收到的數(shù)據(jù)包解析成的響應(yīng)體對(duì)象。

在這里我依舊參考AFNNetworkingAFURLResponseSerialization采用了協(xié)議+繼承的方式進(jìn)行設(shè)計(jì)。

  • 序列化器

首先、我們需要一個(gè)協(xié)議、讓所有序列化器各自實(shí)現(xiàn)請求和響應(yīng)的序列化動(dòng)作。

@protocol KTTCPSocketSerializerDelegate <NSObject>

/**
 根據(jù)不同的策略將請求體格式化成數(shù)據(jù)包
 
 @param req 請求體
 
 */
- (void)configRequestDataWithSerializerWithRequest:(KTTCPSocketRequest *)req;


/**
 嘗試根據(jù)不同的策略將響應(yīng)數(shù)據(jù)包格式化成響應(yīng)體
 
 @return 響應(yīng)體
 */
- (KTTCPSocketResponse *)tryGetResponseDataWithSerializer;

@end
  • 請求的序列化

調(diào)用通過上面的代理進(jìn)行

- (void)configRequestDataWithSerializerWithRequest:(KTTCPSocketRequest *)req {
    
    if (req.type == KTTCP_type_heatbeat) {
        
        [req setKTRequestIdentifier:@(KTTCP_identifier_heatbeat)];
        req.formattedData = configFormattedDataForDemo(KTTCP_type_heatbeat, KTTCP_identifier_heatbeat, req.parameters);
        
        return;
        
    }
    uint32_t requestIdentifier = [self.manager.socket currentRequestIdentifier];//獲取唯一序列號(hào)
    
    [req setKTRequestIdentifier:@(requestIdentifier)];//設(shè)置標(biāo)識(shí)符
    req.formattedData = configFormattedDataForDemo(req.type, requestIdentifier, req.parameters);//根據(jù)協(xié)議配置數(shù)據(jù)包
}

最終需要發(fā)送的數(shù)據(jù)包formattedData通過configFormattedDataForDemo方法進(jìn)行生成

/**
 生成二進(jìn)制請求包
 
 @param type 通訊類型
 @param requestIdentifier 序列號(hào)
 @param parameters 內(nèi)容
 @return 請求包
 */
NSMutableData * configFormattedDataForDemo(KTTCPSocketRequestType type,uint32_t requestIdentifier,NSDictionary *parameters) {
    NSMutableData * formattedData = [NSMutableData new];
    
    //內(nèi)容轉(zhuǎn)data
    NSData * encodingContent = [ConvertToJsonStr(parameters) dataUsingEncoding:NSUTF8StringEncoding];
    //協(xié)議拼接--類型標(biāo)識(shí)符
    [formattedData appendData:DataFromInteger(type)];
    //協(xié)議拼接--序列號(hào)
    [formattedData appendData:DataFromInteger(requestIdentifier)];
    //協(xié)議拼接--請求體長度
    uint32_t contengtLength = (uint32_t)encodingContent.length;
    [formattedData appendData:DataFromInteger(contengtLength)];
    //協(xié)議拼接--請求體
    if (encodingContent != nil) { [formattedData appendData:encodingContent]; }
    
    return formattedData;
}

這里、就是按照我們剛才制定的通訊協(xié)議格式進(jìn)行拼接。

  • 響應(yīng)的序列化

在接收到TCP協(xié)議呈遞上來的數(shù)據(jù)之后調(diào)用代理由序列化器處理

KTTCPSocketResponse *response = [self.serializer tryGetResponseDataWithSerializer];

序列化器內(nèi)部對(duì)數(shù)據(jù)包進(jìn)行拆分

- (KTTCPSocketResponse *)tryGetResponseDataWithSerializer {
    
    NSData *totalReceivedData = self.manager.buffer;
    
    //1.頭部 -- 每個(gè)Response報(bào)文必有的16個(gè)字節(jié)(url+serNum+respCode+contentLen)
    if (totalReceivedData.length < HeaderLengthForDemo) { return nil; }
    
    //2.內(nèi)容
    NSData *responseData;
    //根據(jù)定義的協(xié)議讀取出Response.content的長度
    uint32_t responseContentLength = IntegerFromData([self.manager.buffer subdataWithRange:NSMakeRange(HeaderLengthForDemo - ContentLengthForDemo, ContentLengthForDemo)]);
    
    //3.單個(gè)響應(yīng)包長度  Response.content的長度加上必有的16個(gè)字節(jié)即為整個(gè)Response報(bào)文的長度
    uint32_t responseLength = HeaderLengthForDemo + responseContentLength;
    if (totalReceivedData.length < responseLength) { return nil; }
    
    //4. 根據(jù)上面解析出的responseLength截取出單個(gè)Response報(bào)文
    if (self.manager.buffer.length < responseLength) { return nil; }//如果緩存池的長度不足一個(gè)數(shù)據(jù)包則不讀取
    responseData = [totalReceivedData subdataWithRange:NSMakeRange(0, responseLength)];
    
    //更新緩存池  源緩存池-已經(jīng)獲取的長度
    self.manager.buffer = [[totalReceivedData subdataWithRange:NSMakeRange(responseLength, totalReceivedData.length - responseLength)] mutableCopy];
    
    KTTCPSocketResponseForDemo * response = [KTTCPSocketResponseForDemo responseWithData:responseData];
    
    return response;//校驗(yàn)和通過則返回、否則部分返回
}

可以看到、通過對(duì)協(xié)議每個(gè)字段的解析、進(jìn)而確定單個(gè)數(shù)據(jù)包應(yīng)有的長度并進(jìn)行截取。這也是粘包問題的解決辦法。

單個(gè)數(shù)據(jù)包的解析、由響應(yīng)體根據(jù)自身的數(shù)據(jù)包自行解析

- (KTTCPSocketRequestType)type {
    if (!_type) {
        _type = IntegerFromData([self.data subdataWithRange:NSMakeRange(0, ReqTypeLengthForDemo)]);
    }
    return _type;
}


- (NSNumber *)requestIdentifier {
    if (!_requestIdentifier) {
        _requestIdentifier = @(IntegerFromData([self.data subdataWithRange:NSMakeRange(ReqTypeLengthForDemo , IdentifierLengthForDemo)]));
    }
    return _requestIdentifier;
}



- (uint32_t)contentLength {
    if (!_contentLength) {
        _contentLength = IntegerFromData([self.data subdataWithRange:NSMakeRange(ReqTypeLengthForDemo + IdentifierLengthForDemo, ContentLengthForDemo)]);
    }
    return _contentLength;
}

- (NSData *)content {
    if (!_content) {
        _content = [self.data subdataWithRange:NSMakeRange(HeaderLengthForDemo, self.contentLength)];
    }
    return _content;
}

任務(wù)機(jī)制

你可以參考NSURLSessionTask的作用來理解。

  • KTTCPSocketTask

@interface KTTCPSocketTask : NSObject

@property (nonatomic,readonly) KTTCPSocketTaskState state;//任務(wù)狀態(tài)
@property (nonatomic,readonly) NSNumber *taskIdentifier;//任務(wù)ID

- (void)cancel;
- (void)resume;


@end

其中taskIdentifier與請求時(shí)的序列號(hào)進(jìn)行綁定、并且在收到服務(wù)器消息時(shí)通過序列號(hào)匹配是否有對(duì)應(yīng)的task需要被處理。

  • 任務(wù)超時(shí)

- (void)resume {
    if (self.state != KTTCPSocketTaskState_Suspended) { return; }
    //發(fā)起Request的同時(shí)也啟動(dòng)一個(gè)timer timer超時(shí)直接返回錯(cuò)誤并忽略后續(xù)的Response
    self.timer = [NSTimer scheduledTimerWithTimeInterval:self.request.timeoutInterval target:self selector:@selector(requestTimeout) userInfo:nil repeats:NO];
    [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
    
    self.state = KTTCPSocketTaskState_Running;
    [self.manager resumeTask:self];//通知manager將task.request的數(shù)據(jù)寫入Socket
}

#pragma mark - Private method
- (void)requestTimeout {
    if (![self canResponse]) { return; }
    
    self.state = KTTCPSocketTaskState_Completed;
    [self completeWithResult:nil error:taskError(KTNetworkTaskError_TimeOut)];
}

任務(wù)開始時(shí)會(huì)啟動(dòng)一個(gè)定時(shí)器、當(dāng)?shù)竭_(dá)超時(shí)時(shí)間則將超時(shí)錯(cuò)誤加入回調(diào)執(zhí)行。


管理器

同樣、可以參照AFURLSessionManager來理解

  • KTTCPSocketManager

負(fù)責(zé)將請求(KTTCPSocketRequest)發(fā)送、以及當(dāng)收到響應(yīng)時(shí)將數(shù)據(jù)派發(fā)給對(duì)應(yīng)的task。

@interface KTTCPSocketManager : NSObject

@property (nonatomic) NSUInteger timeoutInterval;//超時(shí)
@property (nonatomic,readonly) KTTCPSocket *socket;
@property (nonatomic,readonly) NSArray<KTTCPSocketTask *> *tasks;//當(dāng)前在執(zhí)行的任務(wù)

/**
 通過指定協(xié)議的序列化方案進(jìn)行初始化
 
 @param serializer 指定協(xié)議
 @return manager
 */
- (instancetype)initWithTCPSocketSerializer:(id<KTTCPSocketSerializerDelegate>)serializer;

/**
 用指定地址去連接

 @param host 主機(jī)
 @param port 端口
 @param block 回調(diào)
 */
- (void)contentWithHost:(NSString *)host port:(uint16_t)port blcok:(KTTCPSocketManagerContentBlock)block;

/**
 發(fā)送信息
 任務(wù)會(huì)自動(dòng)開始
 @param request 請求體
 @param completionHandler 回調(diào)
 @return 任務(wù)
 */
- (KTTCPSocketTask *)sendMsgWithRequest:(KTTCPSocketRequest *)request completionHandler:(KTNetworkTaskCompletionHander)completionHandler;


/**
 創(chuàng)建任務(wù)
 任務(wù)不會(huì)自動(dòng)開始 需要自己[task resume];
 @param request 請求體
 @param completionHandler 回調(diào)
 @return 任務(wù)
 */
- (KTTCPSocketTask *)TaskWithRequest:(KTTCPSocketRequest *)request completionHandler:(KTNetworkTaskCompletionHander)completionHandler;

@end
  • 請求的發(fā)送

- (KTTCPSocketTask *)sendMsgWithRequest:(KTTCPSocketRequest *)request completionHandler:(KTNetworkTaskCompletionHander)completionHandler {
    if (!request.timeoutInterval) { request.timeoutInterval = self.timeoutInterval; }
    [self.serializer configRequestDataWithSerializerWithRequest:request];
    
    KTTCPSocketTask *task = [self dataTaskWithRequest:request completionHandler:completionHandler];
    [task resume];
    return task;
}

//新建數(shù)據(jù)請求任務(wù) 調(diào)用方通過此接口定義Request的收到響應(yīng)后的處理邏輯
- (KTTCPSocketTask *)dataTaskWithRequest:(KTTCPSocketRequest *)request completionHandler:(KTNetworkTaskCompletionHander)completionHandler {
    
    __block NSNumber *taskIdentifier;
    //1. 根據(jù)Request新建Task
    KTTCPSocketTask *task = [KTTCPSocketTask taskWithRequest:request completionHandler:^(NSError *error, id result) {
        //4. Request已收到響應(yīng) 從派發(fā)表中刪除
        [self.tableLock lock];
        [self.mutableTaskByTaskIdentifier removeObjectForKey:taskIdentifier];
        [self.tableLock unlock];
        
        !completionHandler ?: completionHandler(error, result);
    }];
    //2. 設(shè)置Task.manager 后續(xù)會(huì)通過Task.manager向Socket中寫入數(shù)據(jù)
    task.manager = self;
    taskIdentifier = task.taskIdentifier;
    //3. 將Task保存到派發(fā)表中
    [self.tableLock lock];
    [self.mutableTaskByTaskIdentifier setObject:task forKey:taskIdentifier];
    [self.tableLock unlock];
    
    return task;
}

//用socket發(fā)送數(shù)據(jù)包
- (void)resumeTask:(KTTCPSocketTask *)task {
    if (self.socket.isConnected) {
        [self.socket writeData:task.request.requestData];
    }else {
        KTError(@"TCP通道不通", KTNetworkTaskError_SocketNotConnect);
    }
}

這里通過[self.mutableTaskByTaskIdentifier setObject:task forKey:taskIdentifier];將任務(wù)與對(duì)應(yīng)序列號(hào)綁定備用。

  • 響應(yīng)的接收

//接收到數(shù)據(jù)--放入緩存池并解析數(shù)據(jù)
- (void)socket:(KTTCPSocket *)sock didReadData:(NSData *)data {
    [self.lock lock];
    [self.buffer appendData:data];//加入緩存池
    [self.lock unlock];
//    [self.heatbeat reset];
    
    [self readBuffer];//解析數(shù)據(jù)
}

//遞歸截取Response報(bào)文 因?yàn)樽x取到的數(shù)據(jù)可能已經(jīng)"粘包" 所以需要遞歸
- (void)readBuffer {
    if (self.isReading) { return; }
    
    self.isReading = YES;
    [self.lock lock];
    KTTCPSocketResponse *response = [self.serializer tryGetResponseDataWithSerializer];//截取單個(gè)響應(yīng)報(bào)文
    [self.lock unlock];
    [self dispatchResponse:response];//將報(bào)文派發(fā)給對(duì)應(yīng)的task
    self.isReading = NO;
    
    if (!response) { return; }
    [self readBuffer];//繼續(xù)解析
}

這里通過協(xié)議方法tryGetResponseDataWithSerializer讓代理器生成對(duì)應(yīng)的響應(yīng)體、具體過程上文已經(jīng)說過了。

  • 將響應(yīng)派發(fā)給對(duì)應(yīng)任務(wù)

//將Response報(bào)文解析Response 然后交由對(duì)應(yīng)的Task進(jìn)行派發(fā)
- (void)dispatchResponse:(KTTCPSocketResponse *)response {
    
    if (response == nil) { return; }
    
    //根據(jù)報(bào)文類型標(biāo)識(shí)符進(jìn)行分發(fā)
    if (response.type > KTTCP_type_max_notification) {/** 請求響應(yīng) */
        //根據(jù)序列號(hào)取出指定的task
        KTTCPSocketTask *task = self.mutableTaskByTaskIdentifier[response.requestIdentifier];
        //通過task將響應(yīng)報(bào)文回調(diào)
        [task completeWithResponse:response error:nil];
    } else if (response.type == KTTCP_type_heatbeat) {/** 心跳 */
        NSLog(@"接收到心跳");
        [self.heatbeat handleServerAckNum:response.requestIdentifier.intValue];
    } else {/** 推送 */
        //自行處理
    }
}

通過不同的請求類型決定不同的動(dòng)作、如果是響應(yīng)報(bào)文則派發(fā)給對(duì)應(yīng)序列號(hào)的任務(wù)。


Demo

這里我用的Node.js搭建的服務(wù)器、并且支持通過TCP讓Node代替我們進(jìn)行HTTP請求(雖然只寫了Get)。
這樣我們就可以大概實(shí)現(xiàn)美團(tuán)這種客戶端向長連接服務(wù)器發(fā)送TCP請求、長連接服務(wù)器向業(yè)務(wù)服務(wù)器發(fā)送HTTP請求的基本操作。
這樣做除了提高請求的成功率以及速度之外。還有一個(gè)很重要的作用就是可以很大程度上免去被抓包以及篡改的擔(dān)心(自定義通訊協(xié)議)。


不過、加密通道以及UDP/HTTP降級(jí)策略Demo里并沒有寫。因?yàn)椴浑y么難了~(其中加密通道可以借鑒HTTPS的方案、用公鑰來協(xié)商秘鑰就好)。

Demo用起來也沒啥問題、親切可用
客戶端

服務(wù)器

Deme可以《自取》


參考資料

一步一步構(gòu)建你的iOS網(wǎng)絡(luò)層 - TCP篇
iOS使用AsyncSocket循環(huán)接收消息的問題
iOS使用GCDAsyncSocket實(shí)現(xiàn)消息推送
AsyncSocket中tag參數(shù)的用處


最后

本文主要是自己的學(xué)習(xí)與總結(jié)。如果文內(nèi)存在紕漏、萬望留言斧正。如果不吝賜教小弟更加感謝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容