大文件分片斷點上傳下載

當(dāng)上傳或者下載的文件比較大的時候损痰,如果中途網(wǎng)絡(luò)斷掉或者后臺前端出現(xiàn)問題福侈,導(dǎo)致下載中斷酒来,作為用戶來說,我肯定希望下次登陸的時候接著上次下載上傳的進(jìn)度繼續(xù)上傳或者下載肪凛,而不是從頭開始堰汉。這就需要前端和后臺支持?jǐn)帱c續(xù)傳。

斷點分片上傳

大體思路是這樣的:

前端:假定我們每個分片的大小是1Mb伟墙,每次前端上傳的時候翘鸭,先讀取一個分片大小的數(shù)據(jù),然后詢問后臺該分片是否已經(jīng)上傳戳葵,如果沒有上傳就乓,則將該分片上傳,如果上傳過了拱烁,那么就seek 一個分片大小的offset生蚁,讀取下一個分片繼續(xù)同樣的操作,直到所有分片上傳完成戏自。

后端:我們將接收到的一個一個的分片文件單獨放到一個以文件md5命名的文件夾下邦投,當(dāng)前端詢問分片是否存在的時候,只要到該文件夾下查找對應(yīng)的臨時文件是否存在即可擅笔。當(dāng)所有的分片文件都上傳后志衣,再合并所有的臨時文件即可屯援。

先看前端的實現(xiàn):

@interface LFUpload : NSObject
-(void)uploadFile:(NSString *)fileName withType:(NSString*)fileType progress:(Progress)progress;
@end

這里,我們將上傳單獨放到一個類里面

static int offset = 1024*1024; // 每片的大小是1Mb

@interface LFUpload()
@property (nonatomic, assign) NSInteger truncks;
@property (nonatomic, copy) NSString *fileMD5;
@property (nonatomic, copy) Progress progress;
@end

@implementation LFUpload
-(void)uploadFile:(NSString *)fileName withType:(NSString *)fileType progress:(Progress)progress {
    self.progress = progress;
    NSString *filePath = [[NSBundle mainBundle] pathForResource:fileName ofType:fileType];
    NSData *fileData = [NSData dataWithContentsOfFile:filePath];
    self.truncks = fileData.length % offset == 0 ? fileData.length/offset : fileData.length/offset + 1; // 計算該文件的總分片數(shù)
    self.fileMD5 = [FileUtil getMD5ofFile:filePath]; // 獲取文件md5值并傳給后臺
    [self checkTrunck:1]; // 檢查第一個分片是否已經(jīng)上傳
}

// 檢查分片是否已經(jīng)上傳
-(void)checkTrunck:(NSInteger)currentTrunck {
    
    if (currentTrunck > self.truncks) {
        self.progress(100, NO); // 標(biāo)示上傳完成
        return;
    }
    
    __weak typeof(self) weakSelf = self;
    
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    [params setValue:self.fileMD5 forKey:@"md5file"];
    [params setValue:@(currentTrunck) forKey:@"chunk"];
    
    [[LFNetManager defaultManager] POST:@"https://192.168.1.57:443/checkChunk" parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSString *code = [responseObject objectForKey:@"code"];
        if ([code isEqualToString:@"0002"]) { //分片未上傳念脯,下面開始上傳該分片數(shù)據(jù)
            [weakSelf uploadTrunck:currentTrunck];
        } else { // 分片已經(jīng)上傳過了狞洋,直接更新上傳進(jìn)度
            CGFloat progressFinished = currentTrunck * 1.0/self.truncks;             self.progress(progressFinished, NO);
            [weakSelf checkTrunck:currentTrunck + 1];
        }
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        self.progress(0, YES);
    }];
}

// 上傳分片
-(void)uploadTrunck:(NSInteger)currentTrunck {
    
    __weak typeof(self) weakSelf = self;
    
    [[LFNetManager defaultManager] POST:@"https://192.168.1.57:443/upload"
                             parameters:nil
              constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
                  
                  NSString *filePath = [[NSBundle mainBundle] pathForResource:@"myFilm" ofType:@"mp4"];
                  NSData *data;
                  NSFileHandle *readHandler = [NSFileHandle fileHandleForReadingAtPath:filePath];

                  // 偏移到這個分片開始的位置,并讀取數(shù)據(jù)
                  [readHandler seekToFileOffset:offset * (currentTrunck - 1)];
                  data = [readHandler readDataOfLength:offset];
                  
                  [formData appendPartWithFileData:data name:@"file" fileName:@"myFilm.mp4" mimeType:@"application/mp4"];
                  

                  // md5File和二, 后臺需要的參數(shù)
                  NSData *md5FileData = [self.fileMD5 dataUsingEncoding:NSUTF8StringEncoding];
                  [formData appendPartWithFormData:md5FileData name:@"md5File"];
                  
                  // truncks徘铝, 后臺需要直到分片總數(shù),來判斷上傳是否完成
                  NSData *truncksData = [[NSString stringWithFormat:@"%ld", (long)self.truncks] dataUsingEncoding:NSUTF8StringEncoding];
                  [formData appendPartWithFormData:truncksData name:@"truncks"];
                  
                  // currentTrunck惯吕,告知后臺當(dāng)前上傳的分片索引
                  NSData *trunckData = [[NSString stringWithFormat:@"%ld", (long)currentTrunck] dataUsingEncoding:NSUTF8StringEncoding];
                  [formData appendPartWithFormData:trunckData name:@"currentTrunck"];
                  
              } progress:^(NSProgress * _Nonnull uploadProgress) {
                  CGFloat progressInThisTrunck = (1.0 * uploadProgress.completedUnitCount) / (uploadProgress.totalUnitCount * self.truncks); // 當(dāng)前分片中已上傳數(shù)據(jù)占文件總數(shù)據(jù)的百分比
                  CGFloat progressFinished = (currentTrunck - 1) * 1.0/self.truncks; // 已經(jīng)完成的進(jìn)度
                  self.progress(progressInThisTrunck + progressFinished, NO);
                  
              } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
                  [weakSelf checkTrunck:currentTrunck + 1];
                  
              } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                  self.progress(0, YES);
              }];
}

@end

再來看后臺的實現(xiàn):

  1. 檢查分片是否已經(jīng)上傳
@PostMapping("checkChunk")
    @ResponseBody
    public ResponseCommon checkChunk(@RequestParam(value = "md5file") String md5file, @RequestParam(value = "chunk") Integer chunk) {

        ResponseCommon responseCommon = new ResponseCommon();

        // 這里通過判斷分片對應(yīng)的文件存不存在來判斷分片有沒有上傳
        try {
            File path = new File(ResourceUtils.getURL("classpath:").getPath());
            File fileUpload = new File(path.getAbsolutePath(), "static/uploaded/" + md5file + "/" + chunk + ".tmp");

            if (fileUpload.exists()) {
                responseCommon.setCode("0001");
                responseCommon.setMsg("分片已上傳");
            } else {
                responseCommon.setCode("0002");
                responseCommon.setMsg("分片未上傳");
            }


        } catch (FileNotFoundException e) {
            e.printStackTrace();
            responseCommon.setCode("1111");
            responseCommon.setMsg(e.getLocalizedMessage());
        }

        return responseCommon;
    }
  1. 分片上傳
    @PostMapping("upload")
    @ResponseBody
    public ResponseCommon upload(@RequestParam(value = "file") MultipartFile file,
                          @RequestParam(value = "md5File") String md5File,
                          @RequestParam(value = "truncks") Integer truncks,
                          @RequestParam(value = "currentTrunck") Integer currentTrunck) {

        ResponseCommon responseCommon = new ResponseCommon();

        try {
            File path = new File(ResourceUtils.getURL("classpath:").getPath());
            File fileUpload;
            if (truncks == 1) {
                fileUpload = new File(path.getAbsolutePath(), "static/uploaded/" + md5File + "/1.tmp");
            } else {
                fileUpload = new File(path.getAbsolutePath(), "static/uploaded/" + md5File + "/" + currentTrunck + ".tmp");
            }

            if (!fileUpload.exists()) {
                fileUpload.mkdirs();
            }
            file.transferTo(fileUpload);

            if (currentTrunck == truncks) {
                boolean result = this.merge(truncks, md5File, file.getOriginalFilename());
                if (result) {
                    responseCommon.setCode("0000");
                    responseCommon.setMsg("上傳成功");
                } else {
                    responseCommon.setCode("1111");
                    responseCommon.setMsg("文件合并失敗");
                }

            } else {
                responseCommon.setCode("0000");
                responseCommon.setMsg("上傳成功");
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
            responseCommon.setCode("1111");
            responseCommon.setMsg(e.getLocalizedMessage());

        } catch (IOException e) {
            e.printStackTrace();
            responseCommon.setCode("1111");
            responseCommon.setMsg(e.getLocalizedMessage());
        }

        return responseCommon;
    }
  1. 合并所有分片臨時文件
public boolean merge(@RequestParam(value = "truncks") Integer truncks,
                                @RequestParam(value = "md5File") String md5File,
                                @RequestParam(value = "fileName") String fileName) {

        ResponseCommon responseCommon = new ResponseCommon();

        FileOutputStream outputStream = null;
        try {
            File path = new File(ResourceUtils.getURL("classpath:").getPath());
            File md5FilePath = new File(path.getAbsolutePath(), "static/uploaded/" + md5File);
            File finalFile = new File(path.getAbsolutePath(), "static/uploaded/" + fileName);

            outputStream = new FileOutputStream(finalFile);

            byte[] buffer = new byte[1024];
            for (int i=1; i<=truncks; i++) {
                String chunckFile = i + ".tmp";
                File tmpFile = new File(md5FilePath + "/" + chunckFile);
                InputStream inputStream = new FileInputStream(tmpFile);
                int len = 0;
                while ((len = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, len);
                }
                inputStream.close();
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return false;

        } catch (IOException e) {
            e.printStackTrace();
            return false;

        } finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
                return false;
            }
        }
        return true;
    }

這里惕它,我們只是大體實現(xiàn)一下,不必在意細(xì)節(jié)和規(guī)范废登。

斷點分片下載

這里的邏輯和上傳的邏輯正好是反過來的

前端先到后臺獲取總分片數(shù)淹魄,然后循環(huán)遍歷,檢查本地對應(yīng)文件夾下是否有對應(yīng)的分片臨時文件堡距,如果有甲锡,表示之前下載過,如果沒有羽戒,就到后臺請求下載對應(yīng)的分片文件缤沦,當(dāng)所有分片下載完成后,再合并所有的分片臨時文件即可

后臺易稠,這里要提供文件的總分片數(shù)缸废,以及和之前前端一樣的可以seek到某個分片,單獨提供給前端下載驶社,另外企量,考慮到gzip的問題,最好提供一個單獨的接口告知前端整個文件的大小亡电,當(dāng)然届巩,這里,因為我們知道每個分片的大小是1Mb份乒,以及所有的分片數(shù)恕汇,為了簡單起見,就簡單粗糙的計算下下載進(jìn)度或辖。

先看后端實現(xiàn):

  1. 獲取該文件的所有分片數(shù)
@GetMapping("getTruncks")
    @ResponseBody
    public ResponseCommon getTruncks() {

        ResponseCommon responseCommon = new ResponseCommon();
        responseCommon.setCode("0000");


        try {
            File file = new File("/Users/archerlj/Desktop/Untitled/myFilm.mp4");
            Long fileLength = file.length();
            Long trunck = fileLength/(1024 * 1024);

            if (fileLength%(1024*1024) == 0) {
                responseCommon.setTruncks(trunck.intValue());
            } else {
                responseCommon.setTruncks(trunck.intValue() + 1);
            }

            String fileMD5 = DigestUtils.md5DigestAsHex(new FileInputStream(file));
            responseCommon.setFileMD5(fileMD5);
            responseCommon.setCode("0000");

        } catch (IOException e) {
            e.printStackTrace();
            responseCommon.setCode("1111");
            responseCommon.setMsg(e.getLocalizedMessage());
        }

        return responseCommon;
    }
  1. 分片下載接口
@GetMapping("downloadFile")
    @ResponseBody
    public void downloadFile(Integer trunck, HttpServletRequest request, HttpServletResponse response) {

        try {
            RandomAccessFile file = new RandomAccessFile("/Users/archerlj/Desktop/Untitled/myFilm.mp4", "r");
            long offset = (trunck - 1) * 1024 * 1024;
            file.seek(offset);

            byte[] buffer = new byte[1024];
            int len = 0;
            int allLen = 0;
            for (int i=0; i<1024; i++) {
                len = file.read(buffer);

                if (len == -1) {
                    break;
                }

                allLen += len;
                response.getOutputStream().write(buffer, 0, len);
                file.seek(offset + (1024 * (i + 1)));
            }

            file.close();
            response.setContentLength(allLen);
            response.flushBuffer();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

前端實現(xiàn):

同樣瘾英,這里我們單獨提供一個類:

typedef void(^DownloadProgress)(CGFloat progress, Boolean error);

@interface LFDownload : NSObject
-(void)downloadWithCallBack:(DownloadProgress)callback;
@end
@interface LFDownload()
@property (nonatomic, copy) DownloadProgress progress;
@property (nonatomic, assign) NSInteger allTruncks;
@property (nonatomic, assign) NSInteger currentTrunck;
@property (nonatomic, copy) NSString *fileMD5;
@end

@implementation LFDownload
-(void)downloadWithCallBack:(DownloadProgress)callback {
    self.progress = callback;
    [self getTruncks];
    [self getProgress]; // 這里我們單獨來計算下載進(jìn)度
}

-(void)getTruncks {
    
    __weak typeof(self) weakSelf = self;
    
    [[LFNetManager defaultManager] GET:@"https://192.168.1.57:443/getTruncks" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        
        if ([[responseObject valueForKey:@"code"] isEqualToString:@"0000"]) {
            
            weakSelf.allTruncks = [[responseObject valueForKey:@"truncks"] integerValue];
            weakSelf.fileMD5 = [responseObject valueForKey:@"fileMD5"];
            
            if ([self checkMD5FilePath]) {
                [self downloadWithTrunck:1];
            } else {
                weakSelf.progress(0, YES);
            }
            
        } else {
            weakSelf.progress(0, YES);
        }
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        weakSelf.progress(0, YES);
    }];
}

//  檢查存放temp文件的文件夾是否存在
-(BOOL)checkMD5FilePath {
    
    NSString *path = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:self.fileMD5];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    BOOL isDir = YES;
    BOOL fileMD5PathExists = [fileManager fileExistsAtPath:path isDirectory:&isDir];
    if (!fileMD5PathExists) {
        // 如果不存在,就創(chuàng)建文件夾
        NSError *error;
        [fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error];
        if (error) {
            NSLog(@"下載失敗");
            return NO;
        }
    }
    
    return YES;
}


// 下載完成孝凌,合并所有temp文件
-(void)mergeTempFiles {
    
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *finalFilePath = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:@"myFilm.mp4"];
    BOOL isDir = NO;
    
    // 檢查合并的最終文件是否存在方咆,不存在就新建一個文件
    if (![fileManager fileExistsAtPath:finalFilePath isDirectory:&isDir]) {
        BOOL result = [fileManager createFileAtPath:finalFilePath contents:nil attributes:nil];
        if (!result) {
            self.progress(0, YES);
            NSLog(@"文件合并失敗");
            return;
        }
    }
    
    NSFileHandle *outFileHandler = [NSFileHandle fileHandleForUpdatingAtPath:finalFilePath];
    NSString *path = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:self.fileMD5];
    
    // 開始循環(huán)讀取每個分片臨時文件
    for (int i=1; i<= self.allTruncks; i++) {
        
        NSString *fileName = [NSString stringWithFormat:@"%d.temp", i];
        NSString *tempFilePath = [path stringByAppendingPathComponent:fileName];
        NSFileHandle *inFileHandler = [NSFileHandle fileHandleForReadingAtPath:tempFilePath];
        
        int offsetIndex = 0;
        BOOL end = NO;
        
        // 這里每個分片文件,我們每次讀取 1024 byte蟀架, 循環(huán)讀取瓣赂,直到讀取的data長度為0
        while (!end) {
            [inFileHandler seekToFileOffset:1024 * offsetIndex];
            NSData *data = [inFileHandler readDataOfLength:1024];
            if (data.length == 0) {
                end = YES;
            } else {
                // 將最終文件seek到最后榆骚,并將數(shù)據(jù)寫入
                [outFileHandler seekToEndOfFile];
                [outFileHandler writeData:data];
            }
            offsetIndex += 1;
        }

        [inFileHandler closeFile];
    }
    [outFileHandler closeFile];
    NSLog(@"文件合并成功");
}

-(void)downloadWithTrunck:(NSInteger)currentTrunck {
    
    if (currentTrunck > self.allTruncks) {
        NSLog(@"下載完成");
        self.progress(1.0, NO);
        [self mergeTempFiles];
        return;
    }
    
    __weak typeof(self) weakSelf = self;
    
    NSString *urlStr = [NSString stringWithFormat:@"https://192.168.1.57:443/downloadFile?trunck=%ld", (long)currentTrunck];
    NSURL *url = [NSURL URLWithString:urlStr];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    
    NSString *fileName = [NSString stringWithFormat:@"%ld.temp", (long)currentTrunck];
    NSString *path = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:self.fileMD5];
    NSString *tempFilePath = [path stringByAppendingPathComponent:fileName];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    BOOL isDir = NO;
    BOOL tempFileExists = [fileManager fileExistsAtPath:tempFilePath isDirectory:&isDir];
    
    if (tempFileExists) { // 文件已經(jīng)下載了
        NSLog(@"%ld.temp 已經(jīng)下載過了", currentTrunck);
        self.progress(currentTrunck * 1.9 /self.allTruncks, NO);
        [weakSelf downloadWithTrunck:currentTrunck + 1];
        
    } else { // 開始下載文件
        
        self.currentTrunck = currentTrunck;
        NSURLSessionDownloadTask *task = [[LFNetManager defaultSessionManager] downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
            /* 由于后臺使用gzip壓縮的原因,導(dǎo)致http header中沒有Content-Length字段煌集,所以這里不能獲取到下載進(jìn)度 */
        } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
            NSLog(@"%@", tempFilePath);
            return [NSURL fileURLWithPath:tempFilePath];
        } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
            if (error) {
                weakSelf.progress(0, YES);
            } else {
                [weakSelf downloadWithTrunck:currentTrunck + 1];
            }
        }];
        
        [task resume];
    }
}

// 我們自己在回調(diào)中計算進(jìn)度
-(void)getProgress {
    
    __weak typeof(self) weakSelf = self;
    
    [[LFNetManager defaultSessionManager] setDownloadTaskDidWriteDataBlock:^(NSURLSession * _Nonnull session, NSURLSessionDownloadTask * _Nonnull downloadTask, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite) {
        
        // 這里總長度應(yīng)該從后臺單獨獲取妓肢,這里取一個大致的值,假設(shè)所有的trunk都有1Mb苫纤,其實碉钠,只有最后一個沒有1Mb
        long allBytes = 1024 * 1024 * self.allTruncks;
        long downloadedBytes = bytesWritten + 1024 * 1024 * (self.currentTrunck - 1); // 當(dāng)前trunk已經(jīng)下載的大小 + 前面已經(jīng)完成的trunk的大小
        weakSelf.progress(downloadedBytes * 1.0 / allBytes, NO);
    }];
}
@end

文件下載完成以后,我們可以在XCode中卷拘,到Window->Devices and Simulators中喊废,將app的container文件下載到電腦上,查看下載的文件:


屏幕快照 2019-08-16 上午10.44.38.png
屏幕快照 2019-08-16 上午10.55.35.png

這里只考慮了分片斷點續(xù)傳栗弟,其實文件比較大的時候污筷,考慮多線程上傳應(yīng)該會更快一些,可以用NSOperationQueue來簡單的將文件分片分成多個任務(wù)上傳下載乍赫,這里就要控制下多線程下臨界區(qū)的問題了瓣蛀,這里主要是分片索引和進(jìn)度的多線程訪問問題。當(dāng)然雷厂,如果是多個文件上傳下載惋增,將每個文件單獨分配任務(wù)的話就簡單多了。


詳細(xì)的代碼請參考下面鏈接:

iOS端Demo
后臺Demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末改鲫,一起剝皮案震驚了整個濱河市诈皿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌钩杰,老刑警劉巖纫塌,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件诊县,死亡現(xiàn)場離奇詭異讲弄,居然都是意外死亡,警方通過查閱死者的電腦和手機依痊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進(jìn)店門避除,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人胸嘁,你說我怎么就攤上這事瓶摆。” “怎么了性宏?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵群井,是天一觀的道長。 經(jīng)常有香客問我毫胜,道長书斜,這世上最難降的妖魔是什么诬辈? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮荐吉,結(jié)果婚禮上焙糟,老公的妹妹穿的比我還像新娘。我一直安慰自己样屠,他們只是感情好穿撮,可當(dāng)我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著痪欲,像睡著了一般悦穿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上业踢,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天咧党,我揣著相機與錄音,去河邊找鬼陨亡。 笑死傍衡,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的负蠕。 我是一名探鬼主播蛙埂,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼遮糖!你這毒婦竟也來了绣的?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤欲账,失蹤者是張志新(化名)和其女友劉穎屡江,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赛不,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡惩嘉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了踢故。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片文黎。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖殿较,靈堂內(nèi)的尸體忽然破棺而出耸峭,到底是詐尸還是另有隱情,我是刑警寧澤淋纲,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布劳闹,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏本涕。R本人自食惡果不足惜儡首,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望偏友。 院中可真熱鬧蔬胯,春花似錦、人聲如沸位他。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鹅髓。三九已至舞竿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間窿冯,已是汗流浹背骗奖。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留醒串,地道東北人执桌。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像芜赌,于是被迫代替她去往敵國和親仰挣。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,933評論 2 355

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