一步一步構建你的網絡層-TCP篇

目錄

  • TCP概述
  • 創建通信鏈接
  • 定義通信協議
  • 實現通信協議
  • 發起數據請求
  • 處理請求響應
  • 處理後臺推送
  • 請求超時和取消
  • 心跳
  • 文件下載/上傳?
  • WebSocket
TCP概述

TCP是一種面向鏈接的、可靠的、基於字節流的傳輸層通訊協議,由IETF的RFC793定義. 在因特網協議族中,TCP屬於傳輸層, 位於網絡層之上,應用層之下.html

須要注意的是, TCP只是協議聲明, 僅對外聲明協議提供的功能, 但自己並不進行任何實現. 所以, 在介紹通訊協議時, 一般咱們還會說起另外一個術語: Socket. Socket並非一種協議, 而是一組接口(即API). 協議的實現方經過Socket對外提供具體的功能調用. TCP協議的實現方提供的接口就是TCPSocket, UDP協議的實現方提供的接口就是UDPSocket...git

一般, 協議的使用方並不直接面對協議的實現方, 而是經過對應的Socket使用協議提供的功能. 所以, 即便之後協議的底層實現進行了任何改動, 但因爲對外的接口Socket不變, 使用方也不須要作出任何變動.程序員

TCP協議基於IP協議, 而IP協議屬於不可靠協議, 要在一個不可靠協議的的基礎上實現一個可靠的數據傳輸協議是困難且複雜的, TCP的定義者也並不期望全部程序員都能自行實現一遍TCP協議. 因此, 與其說本文是在介紹TCP編程, 倒不如說是介紹TCPSocket編程.github

創建通信鏈接

經過Socket創建TCP鏈接是很是簡單的, 鏈接方(客戶端)只須要提供被鏈接方(服務端)的IP地址和端口號去調用鏈接接口便可, 被鏈接方接受鏈接的話, 接口會返回成功, 不然返回失敗, 至於底層的握手細節, 雙方徹底不用關心. 但考慮到網絡波動, 先後臺切換, 服務器重啓等等可能致使的鏈接主動/被動斷開的狀況, 客戶端這邊我會加上必要的重連處理. 主要代碼以下:算法

//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; //最大重連次數

- (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. 處理網絡波動/先後臺切換
        [[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鏈接線程進行鏈接 避免阻塞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 {//實際的調用鏈接操做在這裏
    
    [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
複製代碼

這邊由於須要添加劇連操做, 因此我在GCDAsyncSocket的基礎上又封裝了一下, 但整體代碼很少, 應該比較好理解. 這裏須要注意的是GCDAsyncSocket的鏈接接口(connectToHost: onPort: error:)是同步調用的, 慢網狀況下可能會阻塞線程一段時間, 因此這裏我單開了一個線程來作鏈接操做.編程

鏈接創建之後, 就能夠讀寫數據了, 寫數據的接口以下:bash

- (void)writeData:(NSData *)data {
    if (!self.isConnected || data.length == 0) { return; }
    
    [self.socket writeData:data withTimeout:-1 tag:socketTag];
}
複製代碼

至於讀數據, 這裏咱們並不走接口, 而是經過回調方法將讀到的數據以參數的形式將數據給到調用方. 這是由於鏈接的另外一端時時刻刻都有可能發送數據過來, 因此一般在鏈接創建後接收方都會進入一個死循環反覆讀取數據, 處理數據, 讀取數據... 僞代碼大概像這樣:服務器

//鏈接成功...
 while (1) {
        
        Error *error;
        Data *readData = [socket readToLength:1024 error:&error];//同步 讀不到數據就阻塞
        if (error) { return; }
        
        [self handleData:readData];//同步異步皆可 多爲異步
}
複製代碼

具體到咱們的代碼中, 則是這個樣子:網絡

// HHTCPSocket.h

@protocol HHTCPSocketDelegate <NSObject>
//...其餘回調方法
- (void)socket:(HHTCPSocket *)sock didReadData:(NSData *)data;//讀取到數據回調方法
@end
複製代碼
// HHTCPSocket.m

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    //Socket鏈接成功 開始讀數據
    [self.socket readDataWithTimeout:-1 tag:socketTag];
}

- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
    //Socket寫數據成功 繼續讀取數據
    [self.socket readDataWithTimeout:-1 tag:socketTag];
}

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    
    //從Socket中讀到數據 交由調用方處理
    if ([self.delegate respondsToSelector:@selector(socket:didReadData:)]) {
        [self.delegate socket:self didReadData:data];
    }
    [self.socket readDataWithTimeout:-1 tag:socketTag];//繼續讀取數據
}
複製代碼

如今咱們已經能夠經過Socket創建一條會自動重連的TCP鏈接, 而後還能夠經過Socket從鏈接中讀寫數據, 接下來要作的就是定義一套本身的通信協議了.app

定義通信協議
  • 爲何須要定義通信協議

TCP協議定義了鏈接雙方以字節流而不是報文段的方式進行數據傳輸, 這意味着任何應用層報文(image/text/html...)想要經過TCP進行傳輸都必須先轉化成二進制數據. 另外, TCP實現出於傳輸效率考慮, 每每會在鏈接兩端各自開闢一個發送數據緩衝區和一個接收數據緩衝區. 所以, 有時應用層經過Socket向鏈接中寫入數據時, 數據其實並無當即被髮送, 而是被放入緩衝區等待合適的時機纔會真正的發送. 理想狀況下, TCP進行傳輸數據的流程可能像這樣:

但實際狀況中, 由於Nagle算法/網絡擁堵/擁塞控制/接收方讀取太慢等等各類緣由, 數據頗有可能會在發送緩衝區/接收緩衝區被累積. 因此, 上面的流程更多是這樣:

或者這樣:

上面的圖都假設應用層報文不到一個MSS(一個MSS通常爲1460字節, 這對大部分非文件請求來講都足夠了), 當報文超過一個MSS時, TCP底層實現會對報文進行拆分後屢次傳輸, 這會稍微複雜些(不想畫圖了), 但最後致使的問題是一致的, 解決方案也是一致的.

從上面的圖容易看出, 不管數據在發送緩衝區仍是接收緩衝區被累積, 對於接收方程序來講都是同樣的: 多個應用層報文不分彼此粘做一串致使數據沒法還原(粘包).

得益於TCP協議是可靠的傳輸協議(可靠意味着TCP實現會保證數據不會丟包, 也不會亂序), 粘包的問題很好處理. 咱們只須要在發送方給每段數據都附上一份描述信息(描述信息主要包括數據的長度, 解析格式等等), 接收方就能夠根據描述信息從一串數據流中分割出單獨的每段應用層報文了.

被傳輸數據和數據的描述一塊兒構成了一段應用層報文, 這裏咱們稱實際想傳輸的數據爲報文有效載荷, 而數據的描述信息爲報文頭部. 此時, 數據的傳輸流程就成了這樣:

  • 定義一個簡單的通信協議

自定義通信協議時, 每每和項目業務直接掛鉤, 因此這塊其實沒什麼好寫的. 但爲了繼續接下來的討論, 這裏我會給到一個很是簡單的Demo版協議, 它長這樣:

由於客戶端和服務端均可以發送和接收數據, 爲了方便描述, 這裏咱們對客戶端發出的報文統一稱爲Request, 服務端發出的報文統一稱爲Response.

這裏須要注意的是, 這裏的Request和Response並不老是一一對應, 好比客戶端單向的心跳請求報文服務端是不會響應的, 而服務端主動發出的推送報文也不是客戶端請求的.

Request由4個部分組成:

  1. url: 相似HTTP中的統一資源定位符, 32位無符號整數(4個字節). 用於標識客戶端請求的服務端資源或對資源進行的操做. 由服務端定義, 客戶端使用.

  2. content(可選): 請求攜帶的數據, 0~N字節的二進制數據. 用於攜帶請求傳輸的內容, 傳輸的內容目前是請求參數, 也可能什麼都沒有. 解析格式固定爲JSON.

  3. serNum: 請求序列號, 32位無符號整數(4個字節). 用於標示請求自己, 每一個請求對應一個惟一的序列號, 即便兩個請求的url和content都相同. 由客戶端生成並傳輸, 服務端解析並回傳. 客戶端經過回傳的序列號和請求序列號之間的對應關係進行響應數據分發.

  4. contentLen: 請求攜帶數據長度, 32位無符號整數(4個字節). 用於標示請求攜帶的數據的長度. 服務端經過contentLen將粘包的數據進行切割後一一解析並處理.

Response由5個部分組成:

  1. url: 同Request.

  2. respCode: 相似HTTP狀態碼, 32位無符號整數(4個字節).

  3. content(可選): 響應攜帶的數據, 0~N字節的二進制數據. 攜帶的數據多是某個Request的響應數據, 也多是服務端主動發出的推送數據, 或者, 什麼都沒有. 解析格式固定爲JSON.

  4. serNum: 該Response所對應的Request序列號, 32位無符號整數(4個字節). 若Response並無對應的Request(好比推送), Response.serNum==Response.url.

  5. contentLen: Response攜帶的數據長度, 32位無符號整數(4個字節). 用於標示Response攜帶的數據的長度. 客戶端經過contentLen將粘包的數據進行切割後一一解析並處理.

由於只是Demo用, 這個協議會比較隨意. 但在實際開發中, 咱們應該儘可能參考那些成熟的應用層協議(HTTP/FTP...). 好比考慮到後續的業務變動, 應該加上Version字段. 加上ContentType字段以傳輸其餘類型的數據, 壓縮字段字節數以節省流量...等等.

實現通信協議

有了協議之後, 就能夠寫代碼進行實現了. Request部分主要代碼以下:

//HHTCPSocketRequest.h

/** URL類型確定都是後臺定義的 直接copy過來便可 命名用後臺的 方便調試時比對 */
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]];/** 請求內容長度 */
    
    if (content != nil) { [request.formattedData appendData:content]; }/** 請求內容 */
    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.爲每一個Request生成惟一序列號; 2. 根據協議定義將應用層數據轉化爲相應的二進制數據.

應用層數據和二進制數據間的轉化由HHDataFormatter完成, 它負責統一數據格式化接口和大小端問題 (關於大小端).

接下來是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比較簡單, 它只作一件事: 根據協議定義將服務端返回的二進制數據解析爲應用層數據.

最後, 爲了方便管理, 咱們再抽象出一個Task. Task將負責請求狀態, 請求超時, 請求回調等等的管理. 這部分和協議無關, 但頗有必要. 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用於將調用方數據寫入Socket completionHandler用於將Response交付給調用方
+ (instancetype)taskWithRequest:(HHTCPSocketRequest *)request completionHandler:(HHNetworkTaskCompletionHander)completionHandler {
    
    HHTCPSocketTask *task = [HHTCPSocketTask new];
    task.request = request;
    task.completionHandler = completionHandler;
    task.state = HHTCPSocketTaskStateSuspended;
    ...其餘 略
    return task;
}

//處理服務端返回的Response Socket讀取到相應的Response報文數據後會調用此接口
- (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];
}

//將處理後的數據交付給調用方
- (void)completeWithResult:(id)result error:(NSError *)error {
    
    ...其餘 略
    dispatch_async(dispatch_get_main_queue(), ^{
        
        !self.completionHandler ?: self.completionHandler(error, result);
        self.completionHandler = nil;
    });
}
複製代碼

如今咱們已經有了TCP鏈接, Request, Response和Task, 接下來要作的就是把這一切串起來. 具體來講, 咱們須要一個管理方創建並管理TCP鏈接, 提供接口讓調用方經過Request向鏈接中寫入數據, 監聽鏈接中讀取到的粘包數據並將數據拆分紅單個Response返回給調用方.

TCP鏈接部分比較簡單, 這裏咱們直接跳過, 從發起數據請求部分開始.

發起數據請求

站在調用方的角度, 發起一個TCP請求與發起一個HTTP請求並無什麼區別. 調用方經過Request提供URL和相應參數, 而後經過completionHandler回調處理請求對應的響應數據, 就像這樣:

// 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];
}
複製代碼

站在協議實現方的角度, 發起網絡請求作的事情會多一些. 咱們須要將調用方提供的Request和completionHandler打包成一個Task並保存起來, 當調用方調用Task.resume時, 咱們再將Request.data寫入Socket. 這部分的主要代碼以下:

//HHTCPSocketClient.m

@interface HHTCPSocketClient()<HHTCPSocketDelegate>

@property (nonatomic, strong) HHTCPSocket *socket;

//任務派發表 以序列號爲鍵保存全部已發出但還未收到響應的Request 待收到響應後再根據序列號一一分發
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, HHTCPSocketTask *> *dispatchTable;

...其餘邏輯 略
@end

@implementation HHTCPSocketClient

...其餘邏輯 略

#pragma mark - Interface(Public)

//新建數據請求任務 調用方經過此接口定義Request的收到響應後的處理邏輯
- (HHTCPSocketTask *)dataTaskWithRequest:(HHTCPSocketRequest *)request completionHandler:(HHNetworkTaskCompletionHander)completionHandler {
    
    __block NSNumber *taskIdentifier;
    //1. 根據Request新建Task
    HHTCPSocketTask *task = [HHTCPSocketTask taskWithRequest:request completionHandler:^(NSError *error, id result) {
        
        //4. Request已收到響應 從派發表中刪除
        dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
        [self.dispatchTable removeObjectForKey:taskIdentifier];
        dispatch_semaphore_signal(lock);
        
        !completionHandler ?: completionHandler(error, result);
    }];
    //2. 設置Task.client爲HHTCPSocketClient 後續會經過Task.client向Socket中寫入數據
    task.client = self;
    taskIdentifier = task.taskIdentifier;
    
    //3. 將Task保存到派發表中
    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接口發起請求 task.resume會調用task.client.resumeTask方法 task.client就是HHTCPSocketClient
    return task.taskIdentifier;
}

#pragma mark - Interface(Friend)

//最終向Socket中寫入Request.data的地方 此接口只提供給HHTCPSocketTask使用 對外不可見
- (void)resumeTask:(HHTCPSocketTask *)task {
 
    // 向Socket中寫入Request格式化好的數據
    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

//對外接口 調用方經過經過此接口發起Request
- (void)resume {
    ...其餘邏輯 略
    
    //通知client將task.request的數據寫入Socket
    [self.client resumeTask:self];
}
複製代碼

簡單描述一下代碼流程:

  1. 調用方提供Request和completionHandler回調從HHTCPSocketClient得到一個打包好的Task(經過dataTaskWithRequest:completionHandler:接口), HHTCPSocketClient內部會以(Request.serNum: Task)的形式將其保存在dispatchTable中.

  2. 調用方經過Task.resume發起TCP請求, 待收到服務端響應後HHTCPSocketClient會根據Response.serNum從dispatchTable取出Task而後執行調用方提供的completionHandler回調.(這裏爲了和系統的NSURLSessionTask保持一致的接口, 我給TCPClient和TCPTask加了一些輔助方法, 代碼上繞了一個圈, 實際上, Task.resume就是Socket.writeData:Task.Request.Data).

處理請求響應

正常狀況下, 請求發出後, 很快就就會收到服務端的響應二進制數據, 咱們要作的就是, 從這些二進制數據中切割出單個Response報文, 而後一一進行分發. 代碼以下:

//HHTCPSocketClient.m

@interface HHTCPSocketClient()<HHTCPSocketDelegate>

//保存全部收到的服務端數據 等待解析
@property (nonatomic, strong) NSMutableData *buffer;
...其餘邏輯 略
@end

#pragma mark - HHTCPSocketDelegate

//從Socket從讀取到數據
- (void)socket:(HHTCPSocket *)sock didReadData:(NSData *)data {
    [self.buffer appendData:data]; //1. 保存讀取到的二進制數據
    
    [self readBuffer];//2. 根據協議解析二進制數據
}

#pragma mark - Parse

//遞歸截取Response報文 由於讀取到的數據可能已經"粘包" 因此須要遞歸
- (void)readBuffer {
    if (self.isReading) { return; }
    
    self.isReading = YES;
    NSData *responseData = [self getParsedResponseData];//1. 從已讀取到的二進制中截取單個Response報文數據
    [self dispatchResponse:responseData];//2. 將Response報文派發給對應的Task
    self.isReading = NO;
    
    if (responseData.length == 0) { return; }
    [self readBuffer]; //3. 遞歸解析
}

//根據定義的協議從buffer中截取出單個Response報文
- (NSData *)getParsedResponseData {
    
    NSData *totalReceivedData = self.buffer;
    //1. 每一個Response報文必有的16個字節(url+serNum+respCode+contentLen)
    uint32_t responseHeaderLength = [HHTCPSocketResponseParser responseHeaderLength];
    if (totalReceivedData.length < responseHeaderLength) { return nil; }
    
    //2. 根據定義的協議讀取出Response.content的長度
    NSData *responseData;
    uint32_t responseContentLength = [HHTCPSocketResponseParser responseContentLengthFromData:totalReceivedData];
    //3. Response.content的長度加上必有的16個字節即爲整個Response報文的長度
    uint32_t responseLength = responseHeaderLength + responseContentLength;
    if (totalReceivedData.length < responseLength) { return nil; }
    
    //4. 根據上面解析出的responseLength截取出單個Response報文
    responseData = [totalReceivedData subdataWithRange:NSMakeRange(0, responseLength)];
    self.buffer = [[totalReceivedData subdataWithRange:NSMakeRange(responseLength, totalReceivedData.length - responseLength)] mutableCopy];
    return responseData;
}

//將Response報文解析Response 而後交由對應的Task進行派發
- (void)dispatchResponse:(NSData *)responseData {
    HHTCPSocketResponse *response = [HHTCPSocketResponse responseWithData:responseData];
    if (response == nil) { return; }
    
    if (response.url > TCP_max_notification) {/** 請求響應 */
        
        HHTCPSocketTask *task = self.dispatchTable[@(response.serNum)];
        [task completeWithResponse:response error:nil];
    } else {/** 推送或心跳 略 */
        ...
    }
}

複製代碼

簡單描述下代碼流程:

  1. TCPClient監聽Socket讀取數據回調方法, 將讀取到的服務端二進制數據添加到buffer中.

  2. 根據定義的協議從buffer頭部開始, 不停地截取出單個Response報文, 直到buffer數據取無可取.

  3. 從2中截取到的Response報文中解析出Response.serNum, 根據serNum從dispatchTable中取出對應的Task(Response.serNum == Request.serNum), 將Response交付給Task. 至此, TCPClient的工做完成.

  4. Task拿到Response後經過completionHandler交付給調用方. 至此, 一次TCPTask完成.

這裏須要注意的是, Socket的回調方法我這邊默認都是在串行隊列中執行的, 因此對buffer的操做並不沒有加鎖, 若是是在並行隊列中執行Socket的回調, 請記得對buffer操做加鎖.

處理後臺推送

除了Request對應的Response, 服務端有時也會主動發送一些推送數據給客戶端, 咱們也須要處理一下:

//HHTCPSocketClient.m

- (void)dispatchResponse:(NSData *)responseData {
    HHTCPSocketResponse *response = [HHTCPSocketResponse responseWithData:responseData];
    if (response == nil) { return; }
    
    if (response.url > TCP_max_notification) {/** 請求響應 略*/
        //...
    } 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;
    }
}
複製代碼
請求超時和取消

TCP協議的可靠性規定了數據會完整的, 有序的進行傳輸, 但並未規定數據傳輸的最大時長. 這意味着, 從發起Request到收到Response的時間間隔可能比咱們能接受的時間間隔要長. 這裏咱們也簡單處理一下, 代碼以下:

//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; }
    
    //發起Request的同時也啓動一個timer timer超時直接返回錯誤並忽略後續的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的同時也開啓一個timer, timer超時就直接忽略Response並返回錯誤給調用方而已. 對於相似HTTP的GET請求而言, 忽略和取消幾乎是等價的. 但對於POST請求而言, 咱們須要的可能就是直接斷開鏈接了, 這部分Demo中並未進行實現, 我還沒遇到相似的需求, 也沒想好該不應這樣作.

心跳

目前爲止, 咱們已經有了一個簡單的TCP客戶端, 它能夠發送數據請求, 接收數據響應, 還能處理服務端推送. 最後, 咱們作一下收尾工做: 心跳.(關於心跳)

單向的心跳就不說了, 這裏咱們給到一張Ping-Pong的簡易圖:

當發送方爲客戶端時, Ping-Pong一般用來驗證TCP鏈接的有效性. 具體來講, 若是Ping-Pong正常, 那麼證實鏈接有效, 數據傳輸沒有問題, 反之, 要麼鏈接已斷開, 要麼鏈接還在但服務器已通過載無力進行恢復, 此時客戶端能夠選擇斷開重連或者切換服務器.

當發送方爲服務端時, Ping-Pong一般用來驗證數據傳輸的即時性. 具體來講, 當服務端向客戶端發送一條即時性消息時一般還會立刻Ping一下客戶端, 若是客戶端即時進行迴應, 那麼說明Ping以前的即時性消息已經到達, 反之, 消息不夠即時, 服務端可能會走APNS再次發送該消息.

Demo中我簡單實現了一下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) {//心跳超時 執行超時回調
        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) {//服務端返回的心跳回應Pong 不用處理
        self.missTime = -1;
        return;
    }
    
    //服務端發起的Ping 須要迴應
    HHTCPSocketRequest *request = [HHTCPSocketRequest requestWithURL:TCP_heatbeat parameters:@{@"ackNum": @(ackNum)} header:nil];
    [self.client dispatchDataTaskWithRequest:request completionHandler:nil];
}

@end
複製代碼

HHTCPSocketHeartbeat每隔一段時間就會發起一個serNum固定爲1的心跳請求Ping一下服務端, 在超時時間間隔內當收到任何服務端迴應, 咱們認爲鏈接有效, 心跳重置, 不然執行調用方設置的超時回調. 另外, HHTCPSocketHeartbeat還負責迴應服務端發起的serNum爲隨機數的即時性Response(這裏的隨機數我給的是時間戳).

//HHTCPSocketClient.m

- (void)configuration {
    
    self.heatbeat = [HHTCPSocketHeartbeat heartbeatWithClient:self timeoutHandler:^{//客戶端心跳超時回調 
        //  [self reconnect];
        SocketLog(@"heartbeat timeout");
    }];
}

#pragma mark - HHTCPSocketDelegate

- (void)socket:(HHTCPSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    [self.heatbeat reset];//鏈接成功 客戶端心跳啓動
}

- (void)socketDidDisconnect:(HHTCPSocket *)sock error:(NSError *)error {
    [self.heatbeat stop];//鏈接斷開 客戶端心跳中止
}

- (void)socket:(HHTCPSocket *)sock didReadData:(NSData *)data {
    [self.heatbeat reset];//收到服務端數據 說明鏈接有效 重置心跳
    //...其餘 略
}

//獲取到服務端Response
- (void)dispatchResponse:(NSData *)responseData {
    HHTCPSocketResponse *response = [HHTCPSocketResponse responseWithData:responseData];
    if (response == nil) { return; }
    
    if (response.url == TCP_heatbeat) {/** 心跳 */
        [self.heatbeat handleServerAckNum:response.serNum];//回覆服務端心跳請求 若是有必要的話
    } 
}

複製代碼

HHTCPSocketHeartbeat由TCPClient調用, 作的事情很簡單: 1)鏈接成功時啓動心跳; 2)收到服務端數據時重置心跳; 3)收到服務端Ping時進行回覆; 4)心跳超時斷開重連 5)鏈接斷開時中止心跳;

文件下載/上傳?

到目前爲止, 咱們討論的都是相似DataTask的數據請求, 並未涉及到文件下載/上傳請求, 事實上, 我也沒打算在通信協議上加上這兩種請求的支持. 這部分我是這樣考慮的:

若是傳輸的文件比較小, 那麼仿照HTTP直接給協議加上ContentType字段, Content以特殊分隔符進行分隔便可.

若是傳輸的文件比較大, 那麼直接在當前鏈接進行文件傳輸可能會阻塞其餘的數據傳輸, 這是咱們不但願看到的, 因此必定是另起一條鏈接專用於大文件傳輸. 考慮到文件傳輸不太可能像普通數據傳輸那樣須要即時性和服務端推送, 爲了節省服務端開銷, 文件傳輸完成後鏈接也沒有必要繼續保持. 這裏的"創建鏈接-文件傳輸-斷開鏈接"其實已經由HTTP實現得很好了, 並且功能還多, 咱們不必再作重複工做.

基於以上考慮, 文件傳輸這塊我更趨向於直接使用HTTP而不是自行實現.

至此, TCP部分的討論就結束了.

WebSocket

就我本身而言, 使用TCP只是看重TCP的全雙工通訊和即時性而已, 雖然TCPSocket已經大大下降了TCP的使用門檻, 但門檻依然存在, 使用者仍不可避免的須要對TCP有個大致瞭解, 還須要處理諸如"粘包""心跳"之類的細節問題. 若是你的需求只是須要全雙工通訊和即時性的數據傳輸, 而且對靈活性和流量要求不敏感的話, 那麼我更推薦你使用近乎零門檻的WebSocket.

從名字和接口來看, WebSocket有點像TCPSocket, 但它並不屬於Socket. WebSocket和HTTP同樣, 是基於TCP的應用層協議, 它在保留了TCP的全雙工通訊的同時還提供了以應用層報文爲傳輸單位和Ping-Pong的功能. 對咱們來講, WebSocket用起來就像自帶"粘包處理"和"心跳"功能的TCPSocket, 很是方便.

關於WebSocket的概念和使用, 這裏我不打算浪費各位的時間. 概念總會淡忘, 而使用上大致就和上面的TCPSocket同樣, 只是不用咱們本身處理"粘包"和"心跳"了. Demo中我也給出了WebSocket的簡單示例, 供各位參考.

本文附帶的Demo地址

一步一步構建你的網絡層-HTTP篇

相關文章
相關標籤/搜索