GCDAsyncSocket

GCDAsyncSocket源碼分析

1.初始化socket 源碼提供了四種初始化方法

- (instancetype)init;
- (instancetype)initWithSocketQueue:(nullable dispatch_queue_t)sq;
- (instancetype)initWithDelegate:(nullable id<GCDAsyncSocketDelegate>)aDelegate delegateQueue:(nullable dispatch_queue_t)dq;
- (instancetype)initWithDelegate:(nullable id<GCDAsyncSocketDelegate>)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq;

最終實現方法:

- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq
{
    if((self = [super init]))
    {
        delegate = aDelegate; // socket的代理
        delegateQueue = sq;  // delegate的線程
          // You MUST set a delegate AND delegate dispatch queue before attempting to use the socket, or you will get an error
          // 初始化socket這里的delegate是必須要有的,否則會出現錯誤
          // socketQueue是可選的,如果socketQueue是空的話會創建一個
        
         //這個宏是在sdk6.0之后才有的,如果是之前的,則OS_OBJECT_USE_OBJC為0,!0即執行if語句
        //對6.0的適配,如果是6.0以下,則去retain release,6.0之后ARC也管理了GCD
        #if !OS_OBJECT_USE_OBJC
        
        if (dq) dispatch_retain(dq);
        #endif
        
        //創建socket,先都置為 -1
        //本機的ipv4
        socket4FD = SOCKET_NULL;
        //ipv6
        socket6FD = SOCKET_NULL;
        //應該是UnixSocket
        socketUN = SOCKET_NULL;
        //url
        socketUrl = nil;
        //狀態
        stateIndex = 0;
        
        if (sq)
        {
            //給定的socketQueue參數不能是一個并發隊列。
            NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0),
                     @"The given socketQueue parameter must not be a concurrent queue.");
            NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),
                     @"The given socketQueue parameter must not be a concurrent queue.");
            NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
                     @"The given socketQueue parameter must not be a concurrent queue.");
            //拿到scoketQueue
            socketQueue = sq;
            //iOS6之下retain
            #if !OS_OBJECT_USE_OBJC
            dispatch_retain(sq);
            #endif
        }
        else
        {
            //沒有的話創建一個,  名字為:GCDAsyncSocket,串行
            socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL);
        }
        
        // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter.
        // From the documentation:
        //
        // > Keys are only compared as pointers and are never dereferenced.
        // > Thus, you can use a pointer to a static variable for a specific subsystem or
        // > any other value that allows you to identify the value uniquely.
        //
        // We're just going to use the memory address of an ivar.
        // Specifically an ivar that is explicitly named for our purpose to make the code more readable.
        //
        // However, it feels tedious (and less readable) to include the "&" all the time:
        // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey)
        //
        // So we're going to make it so it doesn't matter if we use the '&' or not,
        // by assigning the value of the ivar to the address of the ivar.
        // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey;
        
        
        //比如原來為   0X123 -> NULL 變成  0X222->0X123->NULL
        //自己的指針等于自己原來的指針,成二級指針了  看了注釋是為了以后省略&,讓代碼更可讀?
        IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey;
        
        
        void *nonNullUnusedPointer = (__bridge void *)self;
        
        //dispatch_queue_set_specific給當前隊里加一個標識 dispatch_get_specific當前線程取出這個標識,判斷是不是在這個隊列
        //這個key的值其實就是一個一級指針的地址  ,第三個參數把自己傳過去了,上下文對象?第4個參數,為銷毀的時候用的,可以指定一個函數
        dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL);
        //讀的數組 限制為5
        readQueue = [[NSMutableArray alloc] initWithCapacity:5];
        currentRead = nil;
        
        //寫的數組,限制5
        writeQueue = [[NSMutableArray alloc] initWithCapacity:5];
        currentWrite = nil;
        
        //設置大小為 4kb
        preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];
    
#pragma mark alternateAddressDelay??
        //交替地址延時?? wtf
        alternateAddressDelay = 0.3;
    }
    return self;
}

核心建立連接方法

- (BOOL)connectToHost:(NSString *)inHost
               onPort:(uint16_t)port
         viaInterface:(NSString *)inInterface
          withTimeout:(NSTimeInterval)timeout
                error:(NSError **)errPtr
{
    //{} ??有什么意義? -- 跟蹤當前行為 
    LogTrace();
    
    // Just in case immutable objects were passed
    //拿到host ,copy防止值被修改
    NSString *host = [inHost copy];
    //interface?接口?
    NSString *interface = [inInterface copy];
    
    //聲明兩個__block的
    __block BOOL result = NO;
    //error信息
    __block NSError *preConnectErr = nil;
    
    //在socketQueue中執行這個Block
    if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
        block();
    //否則同步的調起這個queue去執行
    else
        dispatch_sync(socketQueue, block);
    
    //如果有錯誤,賦值錯誤
    if (errPtr) *errPtr = preConnectErr;
    //把連接是否成功的result返回
    return result;
}

說明下LogTrace();

#ifndef GCDAsyncSocketLoggingEnabled
#define GCDAsyncSocketLoggingEnabled 0
#endif

#if GCDAsyncSocketLoggingEnabled

#import "DDLog.h"

#define LogTrace()              LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD)

#else
#define LogTrace()              {}
#endif

這個LogTrace根據GCDAsyncSocketLoggingEnabled這個宏定義來是否打開的,會跟蹤到文件名和方法名,但是GCDAsyncSocket這個庫里面沒有使用到。

現在著重說下block()里面干了什么操作:

//gcdBlock ,都包裹在自動釋放池中 :
    // 什么情況下使用自動釋放池
    // 1: 大量臨時變量 connect : 重連
    // 2: 自定義線程管理 : nsoperation
    // 3: 非UI 命令 工具
    dispatch_block_t block = ^{ @autoreleasepool {
        
        // Check for problems with host parameter
        
        if ([host length] == 0)
        {
            NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string.";
            preConnectErr = [self badParamError:msg];
            
            //其實就是return,大牛的代碼真是充滿逼格
            return_from_block;
        }
        
        // Run through standard pre-connect checks
        //一個前置的檢查,如果沒通過返回,這個檢查里,如果interface有值,則會將本機的IPV4 IPV6的 address設置上。
        // 參數 : 指針 操作同一片內存空間
        if (![self preConnectWithInterface:interface error:&preConnectErr])
        {
            return_from_block;
        }
        
        // We've made it past all the checks.
        // It's time to start the connection process.
        //flags 做或等運算。 flags標識為開始Socket連接
        flags |= kSocketStarted;
        
        //又是一個{}? 只是為了標記么?
        LogVerbose(@"Dispatching DNS lookup...");
        
        // It's possible that the given host parameter is actually a NSMutableString.
        //很可能給我們的服務端的參數是一個可變字符串
        // So we want to copy it now, within this block that will be executed synchronously.
        //所以我們需要copy,在Block里同步的執行
        // This way the asynchronous lookup block below doesn't have to worry about it changing.
        //這種基于Block的異步查找,不需要擔心它被改變
        
        //copy,防止改變
        NSString *hostCpy = [host copy];
        
        //拿到狀態
        int aStateIndex = stateIndex;
        __weak GCDAsyncSocket *weakSelf = self;
        
        //全局Queue ---> 服務器
        // client  <---> server
        // 這個globalConcurrentQueue主要是用來查找服務器鏈接        
        dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        //異步執行
        dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool {
            //忽視循環引用
        #pragma clang diagnostic push
        #pragma clang diagnostic warning "-Wimplicit-retain-self"
            
            //查找錯誤
            NSError *lookupErr = nil;
            //server地址數組(包含IPV4 IPV6的地址  sockaddr_in6、sockaddr_in類型)
            NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr];
            
            //strongSelf
            __strong GCDAsyncSocket *strongSelf = weakSelf;
            
            //完整Block安全形態,在加個if
            if (strongSelf == nil) return_from_block;
            
            //如果有錯
            if (lookupErr)
            {
                //用cocketQueue
                dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
                    //一些錯誤處理,清空一些數據等等
                    [strongSelf lookup:aStateIndex didFail:lookupErr];
                }});
            }
            //正常
            else
            {
                
                NSData *address4 = nil;
                NSData *address6 = nil;
                //遍歷地址數組
                for (NSData *address in addresses)
                {
                    //判斷address4為空,且address為IPV4
                    if (!address4 && [[self class] isIPv4Address:address])
                    {
                        address4 = address;
                    }
                    //判斷address6為空,且address為IPV6
                    else if (!address6 && [[self class] isIPv6Address:address])
                    {
                        address6 = address;
                    }
                }
                //異步去發起連接
                dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
                    
                    [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
                }});
            }
            
        #pragma clang diagnostic pop
        }});
        
        
        //開啟連接超時
        [self startConnectTimeout:timeout];
        
        result = YES;
    }};

block()connect流程 :

1.preConnectWithInterface這個方法先檢查interface是否有值,不過開發者一般都是傳nil,如果interface有值是本機地址。

  1. flags的狀態變成kSocketStarted
  2. 開啟了一個異步全局并行隊列來連接開啟連接任務。
  3. lookupHost:port:error:這個方法根據host和port獲取地址的信息,是返回一個數組,數組包括(IPV4 IPV6的地址 sockaddr_in6、sockaddr_in類型)
  4. lookup: didSucceedWithAddress4: address6:去創建連接socket。
  5. 連接完后再開辟一條異步線程,會把flags |= kConnected,連接成功后會停止超時連接.
  6. 這時會創建讀寫stream后,添加到runloop上,最后openStreams。

接著到讀/寫數據函數

- (void)readDataWithTimeout:(NSTimeInterval)timeout
                     buffer:(NSMutableData *)buffer
               bufferOffset:(NSUInteger)offset
                  maxLength:(NSUInteger)length
                        tag:(long)tag
{
    if (offset > [buffer length]) {
        LogWarn(@"Cannot read: offset > [buffer length]");
        return;
    }
    
    GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer
                                                              startOffset:offset
                                                                maxLength:length
                                                                  timeout:timeout
                                                               readLength:0
                                                               terminator:nil
                                                                      tag:tag];
    
    dispatch_async(socketQueue, ^{ @autoreleasepool {
        
        LogTrace();
        
        if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
        {
            //往讀的隊列添加任務,任務是包的形式
            [readQueue addObject:packet];
            [self maybeDequeueRead];
        }
    }});
    
    // Do not rely on the block being run in order to release the packet,
    // as the queue might get released without the block completing.
}

//讓讀任務離隊,開始執行這條讀任務
- (void)maybeDequeueRead
{
    LogTrace();
    NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
    
    // If we're not currently processing a read AND we have an available read stream
    
    //如果當前讀的包為空,而且flag為已連接
    if ((currentRead == nil) && (flags & kConnected))
    {
        //如果讀的queue大于0 (里面裝的是我們封裝的GCDAsyncReadPacket數據包)
        if ([readQueue count] > 0)
        {
            // Dequeue the next object in the write queue
            //使得下一個對象從寫的queue中離開
            
            //從readQueue中拿到第一個寫的數據
            currentRead = [readQueue objectAtIndex:0];
            //移除
            [readQueue removeObjectAtIndex:0];
            
            //我們的數據包,如果是GCDAsyncSpecialPacket這種類型,這個包里裝了TLS的一些設置
            //如果是這種類型的數據,那么我們就進行TLS
            if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]])
            {
                LogVerbose(@"Dequeued GCDAsyncSpecialPacket");
                
                // Attempt to start TLS
                //標記flag為正在讀取TLS
                flags |= kStartingReadTLS;
                
                // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set
                //只有讀寫都開啟了TLS,才會做TLS認證
                [self maybeStartTLS];
            }
            else
            {
                LogVerbose(@"Dequeued GCDAsyncReadPacket");
                
                // Setup read timer (if needed)
                //設置讀的任務超時,每次延時的時候還會調用 [self doReadData];
                [self setupReadTimerWithTimeout:currentRead->timeout];
                
                // Immediately read, if possible
                //讀取數據 主要對數據是否有一次性的數據讀取或者粘包的處理
                [self doReadData];
            }
        }
        
        //讀的隊列沒有數據,標記flag為,讀了沒有數據則斷開連接狀態
        else if (flags & kDisconnectAfterReads)
        {
            //如果標記有寫然后斷開連接
            if (flags & kDisconnectAfterWrites)
            {
                //如果寫的隊列為0,而且寫為空
                if (([writeQueue count] == 0) && (currentWrite == nil))
                {
                    //斷開連接
                    [self closeWithError:nil];
                }
            }
            else
            {
                //斷開連接
                [self closeWithError:nil];
            }
        }
        //如果有安全socket。
        else if (flags & kSocketSecure)
        {
            //
            [self flushSSLBuffers];
            
            // Edge case:
            // 
            // We just drained all data from the ssl buffers,
            // and all known data from the socket (socketFDBytesAvailable).
            // 
            // If we didn't get any data from this process,
            // then we may have reached the end of the TCP stream.
            // 
            // Be sure callbacks are enabled so we're notified about a disconnection.
            
            //如果可讀字節數為0
            if ([preBuffer availableBytes] == 0)
            {
                //CFStream形式TLS
                if ([self usingCFStreamForTLS]) {
                    // Callbacks never disabled
                }
                else {
                    //重新恢復讀的source。因為每次開始讀數據的時候,都會掛起讀的source
                    [self resumeReadSource];
                }
            }
        }
    }
}

//可能開啟TLS
- (void)maybeStartTLS
{
    // We can't start TLS until:
    // - All queued reads prior to the user calling startTLS are complete
    // - All queued writes prior to the user calling startTLS are complete
    // 
    // We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set
    
    //只有讀和寫TLS都開啟
    if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS))
    {
        //需要安全傳輸
        BOOL useSecureTransport = YES;
        
        #if TARGET_OS_IPHONE
        {
            //拿到當前讀的數據
            GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
            //得到設置字典
            NSDictionary *tlsSettings = tlsPacket->tlsSettings;
            
            //拿到Key為CFStreamTLS的 value
            NSNumber *value = [tlsSettings objectForKey:GCDAsyncSocketUseCFStreamForTLS];
            
            if (value && [value boolValue])
                //如果是用CFStream的,則安全傳輸為NO
                useSecureTransport = NO;
        }
        #endif
        //如果使用安全通道
        if (useSecureTransport)
        {
            //開啟TLS
            [self ssl_startTLS];
        }
        //CFStream形式的Tls
        else
        {
        #if TARGET_OS_IPHONE
            [self cf_startTLS];
        #endif
        }
    }
}
  1. 如果能進入maybeStartTLS這個方法后,證明GCDAsyncSpecialPacket這個包要對TLS認證。
  2. 這里的TLS分了兩種情況:
    2.1 ssl_startTLS認證
    2.2 cf_startTLS認證
  3. 但是最終都會去到prebuff,再根據代理方法代理出去的

ssl_startTLS認證流程:

  1. client -- '發送一個信號' --> server
  2. server先弄個公開密鑰將客戶端發送過來的信號生成主密鑰所需要信息 ---> client
  3. client將主密鑰加密的信息 ---> server
  4. server用公開密鑰對主密鑰解密,然后認證
  5. 認證完后初始化SSL提前緩沖(4kb)
  6. 然后根據preBuffer可讀大小進行讀寫
  7. 開始SSL握手過程,調用socket已經開啟安全通道的代理方法

cf_startTLS流程:

這是CF流形式的TLS ---》createReadAndWriteStream ---》addStreamsToRunLoop ---》拿到當前的GCDAsyncSpecialPacket包 ---》再去拿ssl配置 ---》直接設置給讀寫stream ---》openStreams

Close流程:

1.緩沖區的prebuff進行reset
2.相應的事件流關閉、釋放、滯空
3.SSL上下文關閉、釋放
4.針對三種不同類型socket進行關閉釋放
5.取消相關的souce
6.代理回調關閉

GCDAsyncSocketDelegate代理方法:

//已經連接到服務器
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(nonnull NSString *)host port:(uint16_t)port{
    NSLog(@"連接成功 : %@---%d",host,port);
    //連接成功或者收到消息,必須開始read,否則將無法收到消息,
    //不read的話,緩存區將會被關閉
    // -1 表示無限時長 ,永久不失效
    [self.socket readDataWithTimeout:-1 tag:10086];
}

// 連接斷開
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
    NSLog(@"斷開 socket連接 原因:%@",err);
}

//已經接收服務器返回來的數據
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
    NSLog(@"接收到tag = %ld : %ld 長度的數據",tag,data.length);
    //連接成功或者收到消息,必須開始read,否則將無法收到消息
    //不read的話,緩存區將會被關閉
    // -1 表示無限時長 , tag
    [self.socket readDataWithTimeout:-1 tag:10086];
}

//消息發送成功 代理函數 向服務器 發送消息
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
    NSLog(@"%ld 發送數據成功",tag);
}

因為存在粘包和分包的情況,所以接收方需要對接收的數據進行一定的處理,主要解決的問題有兩個:

1.在粘包產生時,要可以在同一個包內獲取出多個包的內容。
2.在分包產生時,要保留上一個包的部分內容,與下一個包的部分內容組合。

處理方式:

#pragma mark - 發送數據格式化
- (void)sendData:(NSData *)data dataType:(unsigned int)dataType{
    NSMutableData *mData = [NSMutableData data];
    // 1.計算數據總長度 data
    unsigned int dataLength = 4+4+(int)data.length;
    // 將長度轉成data
    NSData *lengthData = [NSData dataWithBytes:&dataLength length:4];
    // mData 拼接長度data
    [mData appendData:lengthData];
    
    // 數據類型 data
    // 2.拼接指令類型(4~7:指令)
    NSData *typeData = [NSData dataWithBytes:&dataType length:4];
    // mData 拼接數據類型data
    [mData appendData:typeData];
    
    // 3.最后拼接真正的數據data
    [mData appendData:data];
    NSLog(@"發送數據的總字節大小:%ld",mData.length);
    
    // 發數據
    [self.socket writeData:mData withTimeout:-1 tag:10086];
}

接收數據

- (void)recvData:(NSData *)data{
    //直接就給他緩存起來
    [self.cacheData appendData:data];
    // 獲取總的數據包大小
    // 整段數據長度(不包含長度跟類型)
    NSData *totalSizeData = [data subdataWithRange:NSMakeRange(0, 4)];
    unsigned int totalSize = 0;
    [totalSizeData getBytes:&totalSize length:4];
    //包含長度跟類型的數據長度
    unsigned int completeSize = totalSize  + 8;
    //必須要大于8 才會進這個循環
    while (self.cacheData.length>8) {
        if (self.cacheData.length < completeSize) {
            //如果緩存的長度 還不如 我們傳過來的數據長度,就讓socket繼續接收數據
            [self.socket readDataWithTimeout:-1 tag:10086];
            break;
        }
        //取出數據
        NSData *resultData = [self.cacheData subdataWithRange:NSMakeRange(8, completeSize)];
        //處理數據
        [self handleRecvData:resultData];
        //清空剛剛緩存的data
        [self.cacheData replaceBytesInRange:NSMakeRange(0, completeSize) withBytes:nil length:0];
        //如果緩存的數據長度還是大于8,再執行一次方法
        if (self.cacheData.length > 8) {
            [self recvData:nil];
        }
    }
}

socket的消息如何準確送達

1.當連接的情況下是根據接口來接受消息,如果沒有連接情況是走apns推送。
2.消息有序情況把數據封裝成一個包一個包發送這樣就可以保證消息有序。

websocket

websocket是解決粘包的問題,根據pingpong來保持連接,如果斷了也是會走apns推送,保證消息接受。

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

推薦閱讀更多精彩內容