簡書賬號停止維護(hù), 提問/討論請移步掘金賬號
目錄
- TCP概述
- 建立通訊連接
- 定義通訊協(xié)議
- 實(shí)現(xiàn)通訊協(xié)議
- 發(fā)起數(shù)據(jù)請求
- 處理請求響應(yīng)
- 處理后臺推送
- 請求超時(shí)和取消
- 心跳
- 文件下載/上傳?
- WebSocket
TCP概述
TCP是一種面向連接的、可靠的、基于字節(jié)流的傳輸層通信協(xié)議,由IETF的RFC793定義. 在因特網(wǎng)協(xié)議族中,TCP屬于傳輸層, 位于網(wǎng)絡(luò)層之上,應(yīng)用層之下.
需要注意的是, TCP只是協(xié)議聲明, 僅對外聲明協(xié)議提供的功能, 但本身并不進(jìn)行任何實(shí)現(xiàn). 因此, 在介紹通信協(xié)議時(shí), 通常我們還會提及另一個(gè)術(shù)語: Socket.
Socket并不是一種協(xié)議, 而是一組接口(即API). 協(xié)議的實(shí)現(xiàn)方通過Socket對外提供具體的功能調(diào)用. TCP協(xié)議的實(shí)現(xiàn)方提供的接口就是TCPSocket, UDP協(xié)議的實(shí)現(xiàn)方提供的接口就是UDPSocket...
通常, 協(xié)議的使用方并不直接面對協(xié)議的實(shí)現(xiàn)方, 而是通過對應(yīng)的Socket使用協(xié)議提供的功能. 因此, 即使以后協(xié)議的底層實(shí)現(xiàn)進(jìn)行了任何改動(dòng), 但由于對外的接口Socket不變, 使用方也不需要做出任何變更.
TCP協(xié)議基于IP協(xié)議, 而IP協(xié)議屬于不可靠協(xié)議, 要在一個(gè)不可靠協(xié)議的的基礎(chǔ)上實(shí)現(xiàn)一個(gè)可靠的數(shù)據(jù)傳輸協(xié)議是困難且復(fù)雜的, TCP的定義者也并不指望所有程序員都能自行實(shí)現(xiàn)一遍TCP協(xié)議. 所以, 與其說本文是在介紹TCP編程, 倒不如說是介紹TCPSocket編程.
建立通訊連接
通過Socket建立TCP連接是非常簡單的, 連接方(客戶端)只需要提供被連接方(服務(wù)端)的IP地址和端口號去調(diào)用連接接口即可, 被連接方接受連接的話, 接口會返回成功, 否則返回失敗, 至于底層的握手細(xì)節(jié), 雙方完全不用關(guān)心. 但考慮到網(wǎng)絡(luò)波動(dòng), 前后臺切換, 服務(wù)器重啟等等可能導(dǎo)致的連接主動(dòng)/被動(dòng)斷開的情況, 客戶端這邊我會加上必要的重連處理. 主要代碼如下:
//HHTCPSocket.h
@class HHTCPSocket;
@protocol HHTCPSocketDelegate <NSObject>
@optional
- (void)socket:(HHTCPSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port; //連接成功
- (void)socketCanNotConnectToService:(HHTCPSocket *)sock; //重連失敗
- (void)socketDidDisconnect:(HHTCPSocket *)sock error:(NSError *)error; //連接失敗并開始重連
@end
@interface HHTCPSocket : NSObject
@property (nonatomic, weak) id<HHTCPSocketDelegate> delegate;
@property (nonatomic, assign) NSUInteger maxRetryTime; //最大重連次數(shù)
- (instancetype)initWithService:(HHTCPSocketService *)service; //service提供ip地址和端口號
- (void)close;
- (void)connect; //連接
- (void)reconnect; //重連
- (BOOL)isConnected;
@end
//HHTCPSocket.m
@implementation HHTCPSocket
- (instancetype)initWithService:(HHTCPSocketService *)service {
if (self = [super init]) {
self.service = service ?: [HHTCPSocketService defaultService];
//1. 初始化Socket
const char *delegateQueueLabel = [[NSString stringWithFormat:@"%p_socketDelegateQueue", self] cStringUsingEncoding:NSUTF8StringEncoding];
self.reconnectTime = self.maxRetryTime;
self.delegateQueue = dispatch_queue_create(delegateQueueLabel, DISPATCH_QUEUE_SERIAL);
self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:self.delegateQueue];
//2. 初始化Socket連接線程
self.machPort = [NSMachPort port];
self.keepRuning = YES;
self.socket.IPv4PreferredOverIPv6 = NO; //支持ipv6
[NSThread detachNewThreadSelector:@selector(configSocketThread) toTarget:self withObject:nil];
//3. 處理網(wǎng)絡(luò)波動(dòng)/前后臺切換
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceivedNetworkChangedNotification:) name:kRealReachabilityChangedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceivedAppBecomeActiveNotification:) name:UIApplicationDidBecomeActiveNotification object:nil];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Interface
- (void)connect {
if (self.isConnecting || !self.isNetworkReachable) { return; }
self.isConnecting = YES;
[self disconnect];
//去Socket連接線程進(jìn)行連接 避免阻塞UI
BOOL isFirstTimeConnect = (self.reconnectTime == self.maxRetryTime);
int64_t delayTime = isFirstTimeConnect ? 0 : (arc4random() % 3) + 1;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime * NSEC_PER_SEC)), dispatch_get_global_queue(2, 0), ^{
[self performSelector:@selector(connectOnSocketThread) onThread:self.socketThread withObject:nil waitUntilDone:YES];
});
}
- (void)reconnect {
self.reconnectTime = self.maxRetryTime;
[self connect];
}
- (void)disconnect {
if (!self.socket.isConnected) { return; }
[self.socket setDelegate:nil delegateQueue:nil];
[self.socket disconnect];
}
- (BOOL)isConnected {
return self.socket.isConnected;
}
#pragma mark - GCDAsyncSocketDelegate
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
//連接成功 通知代理方
if ([self.delegate respondsToSelector:@selector(socket:didConnectToHost:port:)]) {
[self.delegate socket:self didConnectToHost:host port:port];
}
self.reconnectTime = self.maxRetryTime;
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error {
if ([self.delegate respondsToSelector:@selector(socketDidDisconnect:error:)]) {
[self.delegate socketDidDisconnect:self error:error];
}
[self tryToReconnect];//連接失敗 嘗試重連
}
#pragma mark - Action
- (void)configSocketThread {
if (self.socketThread == nil) {
self.socketThread = [NSThread currentThread];
[[NSRunLoop currentRunLoop] addPort:self.machPort forMode:NSDefaultRunLoopMode];
}
while (self.keepRuning) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
[[NSRunLoop currentRunLoop] removePort:self.machPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantPast]];
[self.socketThread cancel];
self.socket = nil;
self.machPort = nil;
self.socketThread = nil;
self.delegateQueue = nil;
}
- (void)connectOnSocketThread {//實(shí)際的調(diào)用連接操作在這里
[self.socket setDelegate:self delegateQueue:self.delegateQueue];
[self.socket connectToHost:self.service.host onPort:self.service.port error:nil];
self.isConnecting = NO;
}
#pragma mark - Notification
- (void)didReceivedNetworkChangedNotification:(NSNotification *)notif {
[self reconnectIfNeed];
}
- (void)didReceivedAppBecomeActiveNotification:(NSNotification *)notif {
[self reconnectIfNeed];
}
#pragma mark - Utils
- (void)tryToReconnect {
if (self.isConnecting || !self.isNetworkReachable) { return; }
self.reconnectTime -= 1;
if (self.reconnectTime >= 0) {
[self connect];
} else if ([self.delegate respondsToSelector:@selector(socketCanNotConnectToService:)]) {
[self.delegate socketCanNotConnectToService:self];
}
}
- (NSUInteger)maxRetryTime {
return _maxRetryTime > 0 ? _maxRetryTime : 5;
}
@end
這邊因?yàn)樾枰砑又剡B操作, 所以我在GCDAsyncSocket的基礎(chǔ)上又封裝了一下, 但總體代碼不多, 應(yīng)該比較好理解. 這里需要注意的是GCDAsyncSocket的連接接口(connectToHost: onPort: error:)是同步調(diào)用的, 慢網(wǎng)情況下可能會阻塞線程一段時(shí)間, 所以這里我單開了一個(gè)線程來做連接操作.
連接建立以后, 就可以讀寫數(shù)據(jù)了, 寫數(shù)據(jù)的接口如下:
- (void)writeData:(NSData *)data {
if (!self.isConnected || data.length == 0) { return; }
[self.socket writeData:data withTimeout:-1 tag:socketTag];
}
至于讀數(shù)據(jù), 這里我們并不走接口, 而是通過回調(diào)方法將讀到的數(shù)據(jù)以參數(shù)的形式將數(shù)據(jù)給到調(diào)用方. 這是因?yàn)檫B接的另一端時(shí)時(shí)刻刻都有可能發(fā)送數(shù)據(jù)過來, 所以通常在連接建立后接收方都會進(jìn)入一個(gè)死循環(huán)反復(fù)讀取數(shù)據(jù), 處理數(shù)據(jù), 讀取數(shù)據(jù)... 偽代碼大概像這樣:
//連接成功...
while (1) {
Error *error;
Data *readData = [socket readToLength:1024 error:&error];//同步 讀不到數(shù)據(jù)就阻塞
if (error) { return; }
[self handleData:readData];//同步異步皆可 多為異步
}
具體到我們的代碼中, 則是這個(gè)樣子:
// HHTCPSocket.h
@protocol HHTCPSocketDelegate <NSObject>
//...其他回調(diào)方法
- (void)socket:(HHTCPSocket *)sock didReadData:(NSData *)data;//讀取到數(shù)據(jù)回調(diào)方法
@end
// HHTCPSocket.m
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
//Socket連接成功 開始讀數(shù)據(jù)
[self.socket readDataWithTimeout:-1 tag:socketTag];
}
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
//Socket寫數(shù)據(jù)成功 繼續(xù)讀取數(shù)據(jù)
[self.socket readDataWithTimeout:-1 tag:socketTag];
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
//從Socket中讀到數(shù)據(jù) 交由調(diào)用方處理
if ([self.delegate respondsToSelector:@selector(socket:didReadData:)]) {
[self.delegate socket:self didReadData:data];
}
[self.socket readDataWithTimeout:-1 tag:socketTag];//繼續(xù)讀取數(shù)據(jù)
}
現(xiàn)在我們已經(jīng)可以通過Socket建立一條會自動(dòng)重連的TCP連接, 然后還可以通過Socket從連接中讀寫數(shù)據(jù), 接下來要做的就是定義一套自己的通訊協(xié)議了.
定義通訊協(xié)議
- 為什么需要定義通訊協(xié)議
TCP協(xié)議定義了連接雙方以字節(jié)流而不是報(bào)文段的方式進(jìn)行數(shù)據(jù)傳輸, 這意味著任何應(yīng)用層報(bào)文(image/text/html...)想要通過TCP進(jìn)行傳輸都必須先轉(zhuǎn)化成二進(jìn)制數(shù)據(jù). 另外, TCP實(shí)現(xiàn)出于傳輸效率考慮, 往往會在連接兩端各自開辟一個(gè)發(fā)送數(shù)據(jù)緩沖區(qū)和一個(gè)接收數(shù)據(jù)緩沖區(qū). 因此, 有時(shí)應(yīng)用層通過Socket向連接中寫入數(shù)據(jù)時(shí), 數(shù)據(jù)其實(shí)并沒有立即被發(fā)送, 而是被放入緩沖區(qū)等待合適的時(shí)機(jī)才會真正的發(fā)送. 理想情況下, TCP進(jìn)行傳輸數(shù)據(jù)的流程可能像這樣:
但實(shí)際情況中, 因?yàn)镹agle算法/網(wǎng)絡(luò)擁堵/擁塞控制/接收方讀取太慢等等各種原因, 數(shù)據(jù)很有可能會在發(fā)送緩沖區(qū)/接收緩沖區(qū)被累積. 所以, 上面的流程更可能是這樣:
或者這樣:
上面的圖都假設(shè)應(yīng)用層報(bào)文不到一個(gè)MSS(一個(gè)MSS一般為1460字節(jié), 這對大部分非文件請求來說都足夠了), 當(dāng)報(bào)文超過一個(gè)MSS時(shí), TCP底層實(shí)現(xiàn)會對報(bào)文進(jìn)行拆分后多次傳輸, 這會稍微復(fù)雜些(不想畫圖了), 但最后導(dǎo)致的問題是一致的, 解決方案也是一致的.
從上面的圖容易看出, 無論數(shù)據(jù)在發(fā)送緩沖區(qū)還是接收緩沖區(qū)被累積, 對于接收方程序來說都是一樣的: 多個(gè)應(yīng)用層報(bào)文不分彼此粘作一串導(dǎo)致數(shù)據(jù)無法還原(粘包).
得益于TCP協(xié)議是可靠的傳輸協(xié)議(可靠意味著TCP實(shí)現(xiàn)會保證數(shù)據(jù)不會丟包, 也不會亂序), 粘包的問題很好處理. 我們只需要在發(fā)送方給每段數(shù)據(jù)都附上一份描述信息(描述信息主要包括數(shù)據(jù)的長度, 解析格式等等), 接收方就可以根據(jù)描述信息從一串?dāng)?shù)據(jù)流中分割出單獨(dú)的每段應(yīng)用層報(bào)文了.
被傳輸數(shù)據(jù)和數(shù)據(jù)的描述一起構(gòu)成了一段應(yīng)用層報(bào)文, 這里我們稱實(shí)際想傳輸?shù)臄?shù)據(jù)為報(bào)文有效載荷, 而數(shù)據(jù)的描述信息為報(bào)文頭部. 此時(shí), 數(shù)據(jù)的傳輸流程就成了這樣:
- 定義一個(gè)簡單的通訊協(xié)議
自定義通訊協(xié)議時(shí), 往往和項(xiàng)目業(yè)務(wù)直接掛鉤, 所以這塊其實(shí)沒什么好寫的. 但為了繼續(xù)接下來的討論, 這里我會給到一個(gè)非常簡單的Demo版協(xié)議, 它長這樣:
因?yàn)榭蛻舳撕头?wù)端都可以發(fā)送和接收數(shù)據(jù), 為了方便描述, 這里我們對客戶端發(fā)出的報(bào)文統(tǒng)一稱為Request, 服務(wù)端發(fā)出的報(bào)文統(tǒng)一稱為Response.
這里需要注意的是, 這里的Request和Response并不總是一一對應(yīng), 比如客戶端單向的心跳請求報(bào)文服務(wù)端是不會響應(yīng)的, 而服務(wù)端主動(dòng)發(fā)出的推送報(bào)文也不是客戶端請求的.
Request由4個(gè)部分組成:
url: 類似HTTP中的統(tǒng)一資源定位符, 32位無符號整數(shù)(4個(gè)字節(jié)). 用于標(biāo)識客戶端請求的服務(wù)端資源或?qū)Y源進(jìn)行的操作. 由服務(wù)端定義, 客戶端使用.
content(可選): 請求攜帶的數(shù)據(jù), 0~N字節(jié)的二進(jìn)制數(shù)據(jù). 用于攜帶請求傳輸?shù)膬?nèi)容, 傳輸?shù)膬?nèi)容目前是請求參數(shù), 也可能什么都沒有. 解析格式固定為JSON.
serNum: 請求序列號, 32位無符號整數(shù)(4個(gè)字節(jié)). 用于標(biāo)示請求本身, 每個(gè)請求對應(yīng)一個(gè)唯一的序列號, 即使兩個(gè)請求的url和content都相同. 由客戶端生成并傳輸, 服務(wù)端解析并回傳. 客戶端通過回傳的序列號和請求序列號之間的對應(yīng)關(guān)系進(jìn)行響應(yīng)數(shù)據(jù)分發(fā).
contentLen: 請求攜帶數(shù)據(jù)長度, 32位無符號整數(shù)(4個(gè)字節(jié)). 用于標(biāo)示請求攜帶的數(shù)據(jù)的長度. 服務(wù)端通過contentLen將粘包的數(shù)據(jù)進(jìn)行切割后一一解析并處理.
Response由5個(gè)部分組成:
url: 同Request.
respCode: 類似HTTP狀態(tài)碼, 32位無符號整數(shù)(4個(gè)字節(jié)).
content(可選): 響應(yīng)攜帶的數(shù)據(jù), 0~N字節(jié)的二進(jìn)制數(shù)據(jù). 攜帶的數(shù)據(jù)可能是某個(gè)Request的響應(yīng)數(shù)據(jù), 也可能是服務(wù)端主動(dòng)發(fā)出的推送數(shù)據(jù), 或者, 什么都沒有. 解析格式固定為JSON.
serNum: 該Response所對應(yīng)的Request序列號, 32位無符號整數(shù)(4個(gè)字節(jié)). 若Response并沒有對應(yīng)的Request(比如推送), Response.serNum==Response.url.
contentLen: Response攜帶的數(shù)據(jù)長度, 32位無符號整數(shù)(4個(gè)字節(jié)). 用于標(biāo)示Response攜帶的數(shù)據(jù)的長度. 客戶端通過contentLen將粘包的數(shù)據(jù)進(jìn)行切割后一一解析并處理.
因?yàn)橹皇荄emo用, 這個(gè)協(xié)議會比較隨意. 但在實(shí)際開發(fā)中, 我們應(yīng)該盡量參考那些成熟的應(yīng)用層協(xié)議(HTTP/FTP...). 比如考慮到后續(xù)的業(yè)務(wù)變更, 應(yīng)該加上Version字段. 加上ContentType字段以傳輸其他類型的數(shù)據(jù), 壓縮字段字節(jié)數(shù)以節(jié)省流量...等等.
實(shí)現(xiàn)通訊協(xié)議
有了協(xié)議以后, 就可以寫代碼進(jìn)行實(shí)現(xiàn)了. Request部分主要代碼如下:
//HHTCPSocketRequest.h
/** URL類型肯定都是后臺定義的 直接copy過來即可 命名用后臺的 方便調(diào)試時(shí)比對 */
typedef enum : NSUInteger {
TCP_heatbeat = 0x00000001,
TCP_notification_xxx = 0x00000002,
TCP_notification_yyy = 0x00000003,
TCP_notification_zzz = 0x00000004,
/* ========== */
TCP_max_notification = 0x00000400,
/* ========== */
TCP_login = 0x00000401,
TCP_weibo_list_public = 0x00000402,
TCP_weibo_list_followed = 0x00000403,
TCP_weibo_like = 0x00000404
} HHTCPSocketRequestURL;
+ (instancetype)requestWithURL:(HHTCPSocketRequestURL)url parameters:(NSDictionary *)parameters header:(NSDictionary *)header;
//HHTCPSocketRequest.m
+ (instancetype)requestWithURL:(HHTCPSocketRequestURL)url parameters:(NSDictionary *)parameters header:(NSDictionary *)header {
NSData *content = [parameters yy_modelToJSONData];
uint32_t requestIdentifier = [self currentRequestIdentifier];
HHTCPSocketRequest *request = [HHTCPSocketRequest new];
request.requestIdentifier = @(requestIdentifier);
[request.formattedData appendData:[HHDataFormatter msgTypeDataFromInteger:url]];/** 請求URL */
[request.formattedData appendData:[HHDataFormatter msgSerialNumberDataFromInteger:requestIdentifier]];/** 請求序列號 */
[request.formattedData appendData:[HHDataFormatter msgContentLengthDataFromInteger:(uint32_t)content.length]];/** 請求內(nèi)容長度 */
if (content != nil) { [request.formattedData appendData:content]; }/** 請求內(nèi)容 */
return request;
}
+ (uint32_t)currentRequestIdentifier {
static uint32_t currentRequestIdentifier;
static dispatch_semaphore_t lock;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
currentRequestIdentifier = TCP_max_notification;
lock = dispatch_semaphore_create(1);
});
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
if (currentRequestIdentifier + 1 == 0xffffffff) {
currentRequestIdentifier = TCP_max_notification;
}
currentRequestIdentifier += 1;
dispatch_semaphore_signal(lock);
return currentRequestIdentifier;
}
HHTCPSocketRequest主要做兩件事: 1.為每個(gè)Request生成唯一序列號; 2. 根據(jù)協(xié)議定義將應(yīng)用層數(shù)據(jù)轉(zhuǎn)化為相應(yīng)的二進(jìn)制數(shù)據(jù).
應(yīng)用層數(shù)據(jù)和二進(jìn)制數(shù)據(jù)間的轉(zhuǎn)化由HHDataFormatter完成, 它負(fù)責(zé)統(tǒng)一數(shù)據(jù)格式化接口和大小端問題 (關(guān)于大小端).
接下來是Response部分的代碼:
//HHTCPSocketResponse.h
@interface HHTCPSocketResponse : NSObject
+ (instancetype)responseWithData:(NSData *)data;
- (HHTCPSocketRequestURL)url;
- (NSData *)content;
- (uint32_t)serNum;
- (uint32_t)statusCode;
@end
//HHTCPSocketResponse.m
+ (instancetype)responseWithData:(NSData *)data {
if (data.length < [HHTCPSocketResponseParser responseHeaderLength]) {
return nil;
}
HHTCPSocketResponse *response = [HHTCPSocketResponse new];
response.data = data;
return response;
}
- (HHTCPSocketRequestURL)url {
if (_url == 0) {
_url = [HHTCPSocketResponseParser responseURLFromData:self.data];
}
return _url;
}
- (uint32_t)serNum {
if (_serNum == 0) {
_serNum = [HHTCPSocketResponseParser responseSerialNumberFromData:self.data];
}
return _serNum;
}
- (uint32_t)statusCode {
if (_statusCode == 0) {
_statusCode = [HHTCPSocketResponseParser responseCodeFromData:self.data];
}
return _statusCode;
}
- (NSData *)content {
return [HHTCPSocketResponseParser responseContentFromData:self.data];
}
@end
HHTCPSocketResponse比較簡單, 它只做一件事: 根據(jù)協(xié)議定義將服務(wù)端返回的二進(jìn)制數(shù)據(jù)解析為應(yīng)用層數(shù)據(jù).
最后, 為了方便管理, 我們再抽象出一個(gè)Task. Task將負(fù)責(zé)請求狀態(tài), 請求超時(shí), 請求回調(diào)等等的管理. 這部分和協(xié)議無關(guān), 但很有必要.
Task部分的代碼如下:
//HHTCPSocketTask.h
typedef enum : NSUInteger {
HHTCPSocketTaskStateSuspended = 0,
HHTCPSocketTaskStateRunning = 1,
HHTCPSocketTaskStateCanceled = 2,
HHTCPSocketTaskStateCompleted = 3
} HHTCPSocketTaskState;
@interface HHTCPSocketTask : NSObject
- (void)cancel;
- (void)resume;
- (HHTCPSocketTaskState)state;
- (NSNumber *)taskIdentifier;
@end
//HHTCPSocketTask.m
//保存Request和completionHandler Request用于將調(diào)用方數(shù)據(jù)寫入Socket completionHandler用于將Response交付給調(diào)用方
+ (instancetype)taskWithRequest:(HHTCPSocketRequest *)request completionHandler:(HHNetworkTaskCompletionHander)completionHandler {
HHTCPSocketTask *task = [HHTCPSocketTask new];
task.request = request;
task.completionHandler = completionHandler;
task.state = HHTCPSocketTaskStateSuspended;
...其他 略
return task;
}
//處理服務(wù)端返回的Response Socket讀取到相應(yīng)的Response報(bào)文數(shù)據(jù)后會調(diào)用此接口
- (void)completeWithResponse:(HHTCPSocketResponse *)response error:(NSError *)error {
if (![self canResponse]) { return; }
NSDictionary *result;
if (error == nil) {
if (response == nil) {
error = [self taskErrorWithResponeCode:HHTCPSocketResponseCodeUnkonwn];
} else {
error = [self taskErrorWithResponeCode:response.statusCode];
result = [NSJSONSerialization JSONObjectWithData:response.content options:0 error:nil];
}
}
[self completeWithResult:result error:error];
}
//將處理后的數(shù)據(jù)交付給調(diào)用方
- (void)completeWithResult:(id)result error:(NSError *)error {
...其他 略
dispatch_async(dispatch_get_main_queue(), ^{
!self.completionHandler ?: self.completionHandler(error, result);
self.completionHandler = nil;
});
}
現(xiàn)在我們已經(jīng)有了TCP連接, Request, Response和Task, 接下來要做的就是把這一切串起來. 具體來說, 我們需要一個(gè)管理方建立并管理TCP連接, 提供接口讓調(diào)用方通過Request向連接中寫入數(shù)據(jù), 監(jiān)聽連接中讀取到的粘包數(shù)據(jù)并將數(shù)據(jù)拆分成單個(gè)Response返回給調(diào)用方.
TCP連接部分比較簡單, 這里我們直接跳過, 從發(fā)起數(shù)據(jù)請求部分開始.
發(fā)起數(shù)據(jù)請求
站在調(diào)用方的角度, 發(fā)起一個(gè)TCP請求與發(fā)起一個(gè)HTTP請求并沒有什么區(qū)別. 調(diào)用方通過Request提供URL和相應(yīng)參數(shù), 然后通過completionHandler回調(diào)處理請求對應(yīng)的響應(yīng)數(shù)據(jù), 就像這樣:
// SomeViewController.m
- (void)fetchData {
HHTCPSocketRequest *request = [HHTCPSocketRequest requestWithURL:aTCPUrl parameters:someParams header:someHeader];
HHTCPSocketTask *task = [[HHTCPSocketClient sharedInstance] dataTaskWithRequest:request completionHandler:^(NSError *error, id result) {
if (error) {
//handle error
} else {
//handle result
}
}
[task resume];
}
站在協(xié)議實(shí)現(xiàn)方的角度, 發(fā)起網(wǎng)絡(luò)請求做的事情會多一些. 我們需要將調(diào)用方提供的Request和completionHandler打包成一個(gè)Task并保存起來, 當(dāng)調(diào)用方調(diào)用Task.resume時(shí), 我們再將Request.data寫入Socket. 這部分的主要代碼如下:
//HHTCPSocketClient.m
@interface HHTCPSocketClient()<HHTCPSocketDelegate>
@property (nonatomic, strong) HHTCPSocket *socket;
//任務(wù)派發(fā)表 以序列號為鍵保存所有已發(fā)出但還未收到響應(yīng)的Request 待收到響應(yīng)后再根據(jù)序列號一一分發(fā)
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, HHTCPSocketTask *> *dispatchTable;
...其他邏輯 略
@end
@implementation HHTCPSocketClient
...其他邏輯 略
#pragma mark - Interface(Public)
//新建數(shù)據(jù)請求任務(wù) 調(diào)用方通過此接口定義Request的收到響應(yīng)后的處理邏輯
- (HHTCPSocketTask *)dataTaskWithRequest:(HHTCPSocketRequest *)request completionHandler:(HHNetworkTaskCompletionHander)completionHandler {
__block NSNumber *taskIdentifier;
//1. 根據(jù)Request新建Task
HHTCPSocketTask *task = [HHTCPSocketTask taskWithRequest:request completionHandler:^(NSError *error, id result) {
//4. Request已收到響應(yīng) 從派發(fā)表中刪除
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
[self.dispatchTable removeObjectForKey:taskIdentifier];
dispatch_semaphore_signal(lock);
!completionHandler ?: completionHandler(error, result);
}];
//2. 設(shè)置Task.client為HHTCPSocketClient 后續(xù)會通過Task.client向Socket中寫入數(shù)據(jù)
task.client = self;
taskIdentifier = task.taskIdentifier;
//3. 將Task保存到派發(fā)表中
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
[self.dispatchTable setObject:task forKey:taskIdentifier];
dispatch_semaphore_signal(lock);
return task;
}
- (NSNumber *)dispatchTask:(HHTCPSocketTask *)task {
if (task == nil) { return @-1; }
[task resume];// 通過task.resume接口發(fā)起請求 task.resume會調(diào)用task.client.resumeTask方法 task.client就是HHTCPSocketClient
return task.taskIdentifier;
}
#pragma mark - Interface(Friend)
//最終向Socket中寫入Request.data的地方 此接口只提供給HHTCPSocketTask使用 對外不可見
- (void)resumeTask:(HHTCPSocketTask *)task {
// 向Socket中寫入Request格式化好的數(shù)據(jù)
if (self.socket.isConnected) {
[self.socket writeData:task.request.requestData];
} else {
NSError *error;
if (self.isNetworkReachable) {
error = HHError(HHNetworkErrorNotice, HHNetworkTaskErrorTimeOut);
} else {
error = HHError(HHNetworkErrorNotice, HHNetworkTaskErrorCannotConnectedToInternet);
}
[task completeWithResponseData:nil error:error];
}
}
@end
//HHTCPSocketTask.m
@interface HHTCPSocketTask ()
- (void)setClient:(id)client;//此接口僅提供給上面的HHTCPSocketClient使用 對外不可見
@end
//對外接口 調(diào)用方通過通過此接口發(fā)起Request
- (void)resume {
...其他邏輯 略
//通知client將task.request的數(shù)據(jù)寫入Socket
[self.client resumeTask:self];
}
簡單描述一下代碼流程:
調(diào)用方提供Request和completionHandler回調(diào)從HHTCPSocketClient獲得一個(gè)打包好的Task(通過dataTaskWithRequest:completionHandler:接口), HHTCPSocketClient內(nèi)部會以(Request.serNum: Task)的形式將其保存在dispatchTable中.
調(diào)用方通過Task.resume發(fā)起TCP請求, 待收到服務(wù)端響應(yīng)后HHTCPSocketClient會根據(jù)Response.serNum從dispatchTable取出Task然后執(zhí)行調(diào)用方提供的completionHandler回調(diào).(這里為了和系統(tǒng)的NSURLSessionTask保持一致的接口, 我給TCPClient和TCPTask加了一些輔助方法, 代碼上繞了一個(gè)圈, 實(shí)際上, Task.resume就是Socket.writeData:Task.Request.Data).
處理請求響應(yīng)
正常情況下, 請求發(fā)出后, 很快就就會收到服務(wù)端的響應(yīng)二進(jìn)制數(shù)據(jù), 我們要做的就是, 從這些二進(jìn)制數(shù)據(jù)中切割出單個(gè)Response報(bào)文, 然后一一進(jìn)行分發(fā). 代碼如下:
//HHTCPSocketClient.m
@interface HHTCPSocketClient()<HHTCPSocketDelegate>
//保存所有收到的服務(wù)端數(shù)據(jù) 等待解析
@property (nonatomic, strong) NSMutableData *buffer;
...其他邏輯 略
@end
#pragma mark - HHTCPSocketDelegate
//從Socket從讀取到數(shù)據(jù)
- (void)socket:(HHTCPSocket *)sock didReadData:(NSData *)data {
[self.buffer appendData:data]; //1. 保存讀取到的二進(jìn)制數(shù)據(jù)
[self readBuffer];//2. 根據(jù)協(xié)議解析二進(jìn)制數(shù)據(jù)
}
#pragma mark - Parse
//遞歸截取Response報(bào)文 因?yàn)樽x取到的數(shù)據(jù)可能已經(jīng)"粘包" 所以需要遞歸
- (void)readBuffer {
if (self.isReading) { return; }
self.isReading = YES;
NSData *responseData = [self getParsedResponseData];//1. 從已讀取到的二進(jìn)制中截取單個(gè)Response報(bào)文數(shù)據(jù)
[self dispatchResponse:responseData];//2. 將Response報(bào)文派發(fā)給對應(yīng)的Task
self.isReading = NO;
if (responseData.length == 0) { return; }
[self readBuffer]; //3. 遞歸解析
}
//根據(jù)定義的協(xié)議從buffer中截取出單個(gè)Response報(bào)文
- (NSData *)getParsedResponseData {
NSData *totalReceivedData = self.buffer;
//1. 每個(gè)Response報(bào)文必有的16個(gè)字節(jié)(url+serNum+respCode+contentLen)
uint32_t responseHeaderLength = [HHTCPSocketResponseParser responseHeaderLength];
if (totalReceivedData.length < responseHeaderLength) { return nil; }
//2. 根據(jù)定義的協(xié)議讀取出Response.content的長度
NSData *responseData;
uint32_t responseContentLength = [HHTCPSocketResponseParser responseContentLengthFromData:totalReceivedData];
//3. Response.content的長度加上必有的16個(gè)字節(jié)即為整個(gè)Response報(bào)文的長度
uint32_t responseLength = responseHeaderLength + responseContentLength;
if (totalReceivedData.length < responseLength) { return nil; }
//4. 根據(jù)上面解析出的responseLength截取出單個(gè)Response報(bào)文
responseData = [totalReceivedData subdataWithRange:NSMakeRange(0, responseLength)];
self.buffer = [[totalReceivedData subdataWithRange:NSMakeRange(responseLength, totalReceivedData.length - responseLength)] mutableCopy];
return responseData;
}
//將Response報(bào)文解析Response 然后交由對應(yīng)的Task進(jìn)行派發(fā)
- (void)dispatchResponse:(NSData *)responseData {
HHTCPSocketResponse *response = [HHTCPSocketResponse responseWithData:responseData];
if (response == nil) { return; }
if (response.url > TCP_max_notification) {/** 請求響應(yīng) */
HHTCPSocketTask *task = self.dispatchTable[@(response.serNum)];
[task completeWithResponse:response error:nil];
} else {/** 推送或心跳 略 */
...
}
}
簡單描述下代碼流程:
TCPClient監(jiān)聽Socket讀取數(shù)據(jù)回調(diào)方法, 將讀取到的服務(wù)端二進(jìn)制數(shù)據(jù)添加到buffer中.
根據(jù)定義的協(xié)議從buffer頭部開始, 不停地截取出單個(gè)Response報(bào)文, 直到buffer數(shù)據(jù)取無可取.
從2中截取到的Response報(bào)文中解析出Response.serNum, 根據(jù)serNum從dispatchTable中取出對應(yīng)的Task(Response.serNum == Request.serNum), 將Response交付給Task. 至此, TCPClient的工作完成.
Task拿到Response后通過completionHandler交付給調(diào)用方. 至此, 一次TCPTask完成.
這里需要注意的是, Socket的回調(diào)方法我這邊默認(rèn)都是在串行隊(duì)列中執(zhí)行的, 所以對buffer的操作并不沒有加鎖, 如果是在并行隊(duì)列中執(zhí)行Socket的回調(diào), 請記得對buffer操作加鎖.
處理后臺推送
除了Request對應(yīng)的Response, 服務(wù)端有時(shí)也會主動(dòng)發(fā)送一些推送數(shù)據(jù)給客戶端, 我們也需要處理一下:
//HHTCPSocketClient.m
- (void)dispatchResponse:(NSData *)responseData {
HHTCPSocketResponse *response = [HHTCPSocketResponse responseWithData:responseData];
if (response == nil) { return; }
if (response.url > TCP_max_notification) {/** 請求響應(yīng) 略*/
//...
} else if (response.url == TCP_heatbeat) {/** 心跳 略 */
//...
} else {/** 推送 */
[self dispatchRemoteNotification:response];
}
}
//各種推送 自行處理
- (void)dispatchRemoteNotification:(HHTCPSocketResponse *)notification {
switch (notification.url) {
case TCP_notification_xxx: ...
case TCP_notification_yyy: ...
case TCP_notification_zzz: ...
default:break;
}
}
請求超時(shí)和取消
TCP協(xié)議的可靠性規(guī)定了數(shù)據(jù)會完整的, 有序的進(jìn)行傳輸, 但并未規(guī)定數(shù)據(jù)傳輸?shù)淖畲髸r(shí)長. 這意味著, 從發(fā)起Request到收到Response的時(shí)間間隔可能比我們能接受的時(shí)間間隔要長. 這里我們也簡單處理一下, 代碼如下:
//HHTCPSocketTask.m
#pragma mark - Interface
- (void)cancel {
if (![self canResponse]) { return; }
self.state = HHTCPSocketTaskStateCanceled;
[self completeWithResult:nil error:[self taskErrorWithResponeCode:HHNetworkTaskErrorCanceled]];
}
- (void)resume {
if (self.state != HHTCPSocketTaskStateSuspended) { 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 = HHTCPSocketTaskStateRunning;
[self.client resumeTask:self];
}
#pragma mark - Action
- (void)requestTimeout {
if (![self canResponse]) { return; }
self.state = HHTCPSocketTaskStateCompleted;
[self completeWithResult:nil error:[self taskErrorWithResponeCode:HHNetworkTaskErrorTimeOut]];
}
#pragma mark - Utils
- (BOOL)canResponse {
return self.state <= HHTCPSocketTaskStateRunning;
}
代碼很簡單, 只是在寫入Task.Request的同時(shí)也開啟一個(gè)timer, timer超時(shí)就直接忽略Response并返回錯(cuò)誤給調(diào)用方而已. 對于類似HTTP的GET請求而言, 忽略和取消幾乎是等價(jià)的. 但對于POST請求而言, 我們需要的可能就是直接斷開連接了, 這部分Demo中并未進(jìn)行實(shí)現(xiàn), 我還沒遇到類似的需求, 也沒想好該不該這樣做.
心跳
目前為止, 我們已經(jīng)有了一個(gè)簡單的TCP客戶端, 它可以發(fā)送數(shù)據(jù)請求, 接收數(shù)據(jù)響應(yīng), 還能處理服務(wù)端推送. 最后, 我們做一下收尾工作: 心跳.(關(guān)于心跳)
單向的心跳就不說了, 這里我們給到一張Ping-Pong的簡易圖:
當(dāng)發(fā)送方為客戶端時(shí), Ping-Pong通常用來驗(yàn)證TCP連接的有效性. 具體來說, 如果Ping-Pong正常, 那么證明連接有效, 數(shù)據(jù)傳輸沒有問題, 反之, 要么連接已斷開, 要么連接還在但服務(wù)器已經(jīng)過載無力進(jìn)行恢復(fù), 此時(shí)客戶端可以選擇斷開重連或者切換服務(wù)器.
當(dāng)發(fā)送方為服務(wù)端時(shí), Ping-Pong通常用來驗(yàn)證數(shù)據(jù)傳輸?shù)募磿r(shí)性. 具體來說, 當(dāng)服務(wù)端向客戶端發(fā)送一條即時(shí)性消息時(shí)通常還會馬上Ping一下客戶端, 如果客戶端即時(shí)進(jìn)行回應(yīng), 那么說明Ping之前的即時(shí)性消息已經(jīng)到達(dá), 反之, 消息不夠即時(shí), 服務(wù)端可能會走APNS再次發(fā)送該消息.
Demo中我簡單實(shí)現(xiàn)了一下Ping-Pong, 代碼如下:
//HHTCPSocketHeartbeat
static NSUInteger maxMissTime = 3;
@implementation HHTCPSocketHeartbeat
+ (instancetype)heartbeatWithClient:(id)client timeoutHandler:(void (^)(void))timeoutHandler {
HHTCPSocketHeartbeat *heartbeat = [HHTCPSocketHeartbeat new];
heartbeat.client = client;
heartbeat.missTime = -1;
heartbeat.timeoutHandler = timeoutHandler;
return heartbeat;
}
- (void)start {
[self stop];
self.timer = [NSTimer timerWithTimeInterval:60 target:self selector:@selector(sendHeatbeat) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)stop {
[self.timer invalidate];
}
- (void)reset {
self.missTime = -1;
[self start];
}
- (void)sendHeatbeat {
self.missTime += 1;
if (self.missTime >= maxMissTime && self.timeoutHandler != nil) {//心跳超時(shí) 執(zhí)行超時(shí)回調(diào)
self.timeoutHandler();
self.missTime = -1;
}
HHTCPSocketRequest *request = [HHTCPSocketRequest requestWithURL:TCP_heatbeat parameters:@{@"ackNum": @(TCP_heatbeat)} header:nil];
[self.client dispatchDataTaskWithRequest:request completionHandler:nil];
}
- (void)handleServerAckNum:(uint32_t)ackNum {
if (ackNum == TCP_heatbeat) {//服務(wù)端返回的心跳回應(yīng)Pong 不用處理
self.missTime = -1;
return;
}
//服務(wù)端發(fā)起的Ping 需要回應(yīng)
HHTCPSocketRequest *request = [HHTCPSocketRequest requestWithURL:TCP_heatbeat parameters:@{@"ackNum": @(ackNum)} header:nil];
[self.client dispatchDataTaskWithRequest:request completionHandler:nil];
}
@end
HHTCPSocketHeartbeat每隔一段時(shí)間就會發(fā)起一個(gè)serNum固定為1的心跳請求Ping一下服務(wù)端, 在超時(shí)時(shí)間間隔內(nèi)當(dāng)收到任何服務(wù)端回應(yīng), 我們認(rèn)為連接有效, 心跳重置, 否則執(zhí)行調(diào)用方設(shè)置的超時(shí)回調(diào). 另外, HHTCPSocketHeartbeat還負(fù)責(zé)回應(yīng)服務(wù)端發(fā)起的serNum為隨機(jī)數(shù)的即時(shí)性Response(這里的隨機(jī)數(shù)我給的是時(shí)間戳).
//HHTCPSocketClient.m
- (void)configuration {
self.heatbeat = [HHTCPSocketHeartbeat heartbeatWithClient:self timeoutHandler:^{//客戶端心跳超時(shí)回調(diào)
// [self reconnect];
SocketLog(@"heartbeat timeout");
}];
}
#pragma mark - HHTCPSocketDelegate
- (void)socket:(HHTCPSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
[self.heatbeat reset];//連接成功 客戶端心跳啟動(dòng)
}
- (void)socketDidDisconnect:(HHTCPSocket *)sock error:(NSError *)error {
[self.heatbeat stop];//連接斷開 客戶端心跳停止
}
- (void)socket:(HHTCPSocket *)sock didReadData:(NSData *)data {
[self.heatbeat reset];//收到服務(wù)端數(shù)據(jù) 說明連接有效 重置心跳
//...其他 略
}
//獲取到服務(wù)端Response
- (void)dispatchResponse:(NSData *)responseData {
HHTCPSocketResponse *response = [HHTCPSocketResponse responseWithData:responseData];
if (response == nil) { return; }
if (response.url == TCP_heatbeat) {/** 心跳 */
[self.heatbeat handleServerAckNum:response.serNum];//回復(fù)服務(wù)端心跳請求 如果有必要的話
}
}
HHTCPSocketHeartbeat由TCPClient調(diào)用, 做的事情很簡單: 1)連接成功時(shí)啟動(dòng)心跳; 2)收到服務(wù)端數(shù)據(jù)時(shí)重置心跳; 3)收到服務(wù)端Ping時(shí)進(jìn)行回復(fù); 4)心跳超時(shí)斷開重連 5)連接斷開時(shí)停止心跳;
文件下載/上傳?
到目前為止, 我們討論的都是類似DataTask的數(shù)據(jù)請求, 并未涉及到文件下載/上傳請求, 事實(shí)上, 我也沒打算在通訊協(xié)議上加上這兩種請求的支持. 這部分我是這樣考慮的:
如果傳輸?shù)奈募容^小, 那么仿照HTTP直接給協(xié)議加上ContentType字段, Content以特殊分隔符進(jìn)行分隔即可.
如果傳輸?shù)奈募容^大, 那么直接在當(dāng)前連接進(jìn)行文件傳輸可能會阻塞其他的數(shù)據(jù)傳輸, 這是我們不希望看到的, 所以一定是另起一條連接專用于大文件傳輸. 考慮到文件傳輸不太可能像普通數(shù)據(jù)傳輸那樣需要即時(shí)性和服務(wù)端推送, 為了節(jié)省服務(wù)端開銷, 文件傳輸完成后連接也沒有必要繼續(xù)保持. 這里的"建立連接-文件傳輸-斷開連接"其實(shí)已經(jīng)由HTTP實(shí)現(xiàn)得很好了, 而且功能還多, 我們沒必要再做重復(fù)工作.
基于以上考慮, 文件傳輸這塊我更趨向于直接使用HTTP而不是自行實(shí)現(xiàn).
至此, TCP部分的討論就結(jié)束了.
WebSocket
就我自己而言, 使用TCP只是看重TCP的全雙工通信和即時(shí)性而已, 雖然TCPSocket已經(jīng)大大降低了TCP的使用門檻, 但門檻依然存在, 使用者仍不可避免的需要對TCP有個(gè)大體了解, 還需要處理諸如"粘包""心跳"之類的細(xì)節(jié)問題. 如果你的需求只是需要全雙工通信和即時(shí)性的數(shù)據(jù)傳輸, 并且對靈活性和流量要求不敏感的話, 那么我更推薦你使用近乎零門檻的WebSocket.
從名字和接口來看, WebSocket有點(diǎn)像TCPSocket, 但它并不屬于Socket. WebSocket和HTTP一樣, 是基于TCP的應(yīng)用層協(xié)議, 它在保留了TCP的全雙工通信的同時(shí)還提供了以應(yīng)用層報(bào)文為傳輸單位和Ping-Pong的功能. 對我們來說, WebSocket用起來就像自帶"粘包處理"和"心跳"功能的TCPSocket, 非常方便.
關(guān)于WebSocket的概念和使用, 這里我不打算浪費(fèi)各位的時(shí)間. 概念總會淡忘, 而使用上大體就和上面的TCPSocket一樣, 只是不用我們自己處理"粘包"和"心跳"了. Demo中我也給出了WebSocket的簡單示例, 供各位參考.