一步一步構(gòu)建你的iOS網(wǎng)絡(luò)層 - TCP篇


簡書賬號停止維護(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ù)的流程可能像這樣:

image

但實(shí)際情況中, 因?yàn)镹agle算法/網(wǎng)絡(luò)擁堵/擁塞控制/接收方讀取太慢等等各種原因, 數(shù)據(jù)很有可能會在發(fā)送緩沖區(qū)/接收緩沖區(qū)被累積. 所以, 上面的流程更可能是這樣:

image

或者這樣:

image

上面的圖都假設(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ù)的傳輸流程就成了這樣:

image
  • 定義一個(gè)簡單的通訊協(xié)議

自定義通訊協(xié)議時(shí), 往往和項(xiàng)目業(yè)務(wù)直接掛鉤, 所以這塊其實(shí)沒什么好寫的. 但為了繼續(xù)接下來的討論, 這里我會給到一個(gè)非常簡單的Demo版協(xié)議, 它長這樣:

image

因?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è)部分組成:

  1. url: 類似HTTP中的統(tǒng)一資源定位符, 32位無符號整數(shù)(4個(gè)字節(jié)). 用于標(biāo)識客戶端請求的服務(wù)端資源或?qū)Y源進(jìn)行的操作. 由服務(wù)端定義, 客戶端使用.

  2. content(可選): 請求攜帶的數(shù)據(jù), 0~N字節(jié)的二進(jìn)制數(shù)據(jù). 用于攜帶請求傳輸?shù)膬?nèi)容, 傳輸?shù)膬?nèi)容目前是請求參數(shù), 也可能什么都沒有. 解析格式固定為JSON.

  3. 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ā).

  4. contentLen: 請求攜帶數(shù)據(jù)長度, 32位無符號整數(shù)(4個(gè)字節(jié)). 用于標(biāo)示請求攜帶的數(shù)據(jù)的長度. 服務(wù)端通過contentLen將粘包的數(shù)據(jù)進(jìn)行切割后一一解析并處理.

Response由5個(gè)部分組成:

  1. url: 同Request.

  2. respCode: 類似HTTP狀態(tài)碼, 32位無符號整數(shù)(4個(gè)字節(jié)).

  3. 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.

  4. serNum: 該Response所對應(yīng)的Request序列號, 32位無符號整數(shù)(4個(gè)字節(jié)). 若Response并沒有對應(yīng)的Request(比如推送), Response.serNum==Response.url.

  5. 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];
}

簡單描述一下代碼流程:

  1. 調(diào)用方提供Request和completionHandler回調(diào)從HHTCPSocketClient獲得一個(gè)打包好的Task(通過dataTaskWithRequest:completionHandler:接口), HHTCPSocketClient內(nèi)部會以(Request.serNum: Task)的形式將其保存在dispatchTable中.

  2. 調(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 {/** 推送或心跳 略 */
        ...
    }
}

簡單描述下代碼流程:

  1. TCPClient監(jiān)聽Socket讀取數(shù)據(jù)回調(diào)方法, 將讀取到的服務(wù)端二進(jìn)制數(shù)據(jù)添加到buffer中.

  2. 根據(jù)定義的協(xié)議從buffer頭部開始, 不停地截取出單個(gè)Response報(bào)文, 直到buffer數(shù)據(jù)取無可取.

  3. 從2中截取到的Response報(bào)文中解析出Response.serNum, 根據(jù)serNum從dispatchTable中取出對應(yīng)的Task(Response.serNum == Request.serNum), 將Response交付給Task. 至此, TCPClient的工作完成.

  4. 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的簡易圖:


image

當(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的簡單示例, 供各位參考.

本文附帶的Demo地址

一步一步構(gòu)建你的網(wǎng)絡(luò)層-HTTP篇

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,427評論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,380評論 2 379

推薦閱讀更多精彩內(nèi)容