
目錄
- 前言
- 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(類似
NSLayoutConstraint與Masonry的關(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ì)象。
在這里我依舊參考AFNNetworking的AFURLResponseSerialization采用了協(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)存在紕漏、萬望留言斧正。如果不吝賜教小弟更加感謝。