commons-fileupload框架源碼解析(五)--MultipartStream

  1. commons-fileupload框架源碼解析(一)--實(shí)例
  2. commons-fileupload框架源碼解析(二)--HTTP
  3. commons-fileupload框架源碼解析(三)--ParseRequest
  4. commons-fileupload框架源碼解析(四)--FileItemIterator
  5. commons-fileupload框架源碼解析(五)--MultipartStream
  6. commons-fileupload框架源碼解析(六)--ParameterParser
  7. commons-fileupload框架源碼解析(七)--FileCleaningTracker
  8. commons-fileupload框架源碼解析(八)--DeferredFileOutputStream

前言

在FileItemIteratorImpl上,其實(shí)在讀取解析主體文本這一塊,是交給了MultipartStream進(jìn)行處理,F(xiàn)ileItemIteratorImpl更準(zhǔn)確的說,是將MultipartStream讀取到的內(nèi)容油猫,封裝成FileItemStream.所以要想知道commons-Fileupload是如何讀取主體文本的,就需要深入了解MultipartStream的代碼,而MultipartStream插爹,也是這個(gè)框架的中最難深入,最容易混亂,我當(dāng)時(shí)花最長的解讀時(shí)間也是在MultipartStream,深有體會(huì)赠尾,尤其是在調(diào)試一步一步的每行代碼時(shí)力穗,因?yàn)镸ultipartStream由于直接操作的是字節(jié),所以很難理解每一步在干什么气嫁,為什么要這樣做当窗。
另外,我是根據(jù)程序一步一步執(zhí)行下去的方式寸宵,遇到那個(gè)方法就解釋那個(gè)方法崖面,這樣可能會(huì)導(dǎo)致讀者的混亂,所以梯影,我建議下載這個(gè)框架源碼巫员,一邊看我的博客,一步一步的調(diào)試甲棍,遇到不懂的方法简识,在本文中搜索一下位置,我已經(jīng)盡力在代碼塊上寫上備注救军,以便理解财异,如果單純看我這篇文章,肯定是看得想吐唱遭。

HttpServletRequest.getInputStream

在解析源碼之前戳寸,我們想看看HttpServletRequest.getInputStream里面有什么內(nèi)容,因?yàn)镸ultipartStream的讀取解析工作都是對HttpServletRequest.getInputStream的內(nèi)容進(jìn)行開展的拷泽,如果不了解HttpServletRequest.getInputStream里面的內(nèi)容疫鹊,光看源碼,調(diào)試司致,是非常困難的事情拆吆。內(nèi)容我以截圖的方式弄出來

圖1.png:HttpServletRequest.getInputStream

說明一下:
藍(lán)色框就是主體內(nèi)容
紅色框的就是參數(shù)內(nèi)容
------WebKitFormBoundaryuCJWrl4DtkP7RoK5就是分割線,主體文本的最后一條分割線后面會(huì)多兩個(gè)'-'

源碼

MultipartStream構(gòu)造方法

   public MultipartStream(InputStream input,
            byte[] boundary,
            int bufSize,//默認(rèn)是4096字節(jié)
            ProgressNotifier pNotifier) {

        if (boundary == null) {
            throw new IllegalArgumentException("boundary may not be null");
        }
        // We prepend CR/LF to the boundary to chop trailing CR/LF from
        // body-data tokens.
        this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length;//各個(gè)數(shù)據(jù)內(nèi)容之間的分隔界線的組成: CR+LF+兩個(gè)'-'+從消息頭Content-Type找到boundary的值
        //因?yàn)樵诓檎曳指罱泳€的操作是在緩存區(qū)buffer中進(jìn)行查找到脂矫,所以bufSize必須起碼能裝得分割線的所有字節(jié)
        if (bufSize < this.boundaryLength + 1) {
            throw new IllegalArgumentException(
                    "The buffer size specified for the MultipartStream is too small");
        }

        this.input = input;//HttpServletRequest的流枣耀,流的數(shù)據(jù)就是主體文本
        this.bufSize = Math.max(bufSize, boundaryLength * 2);//框架認(rèn)為bufSize越大越好,畢竟這個(gè)讀取主體內(nèi)容的速度庭再,和解析捞奕、查詢效率
        this.buffer = new byte[this.bufSize];
        this.notifier = pNotifier;

        this.boundary = new byte[this.boundaryLength];
        this.boundaryTable = new int[this.boundaryLength + 1];//KMP算法的前綴表
        this.keepRegion = this.boundary.length;//在buffer中預(yù)留的分割線字節(jié)數(shù)

        //將成員變量boundary賦值,首先將BOUNDARY_PREFIX的元素復(fù)制到成員變量boundary的開頭拄轻,再將參數(shù)boundary的元素復(fù)制成員變量boundary剩下的元素
        //即\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW
        System.arraycopy(BOUNDARY_PREFIX, 0, this.boundary, 0,
                BOUNDARY_PREFIX.length);
        System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length,
                boundary.length);
        computeBoundaryTable();//計(jì)算出分割線的KMP算法前綴表

        head = 0;//讀取buffer的開始位置颅围,0<=head<=buffer.length
        tail = 0;//讀取buffer的結(jié)束位置,0<=head<=buffer.length
    }

這里就是對需要用到的成員變量進(jìn)行初始化,值得注意的是恨搓,MultipartStream在buffer中查詢分割線boundary是采用KMP算法院促,不算KMP算法的讀者筏养,請先去了解一下KMP算法。computeBoundaryTable()就是用于計(jì)算KMP算法所需的boundary前綴表

SkipPreamble

在開始讀取主體文本的第一步是調(diào)用skipPreamble方法常拓,參照UploadFileBase.parseRequest(RequestContext)

   public boolean skipPreamble() throws IOException {
        // First delimiter may be not preceeded with a CRLF.
        //去掉前面的‘\n’和‘\r’這兩個(gè)字節(jié)渐溶,因?yàn)榈谝粋€(gè)分割線前面沒有回車換行符,無法與成員變量boundary中的數(shù)據(jù)相匹配
        System.arraycopy(boundary, 2, boundary, 0, boundary.length - 2);
        boundaryLength = boundary.length - 2;
        computeBoundaryTable();//計(jì)算出分割線的KMP算法前綴表
        try {
            // Discard all data up to the delimiter.
            //丟掉第一個(gè)分隔界線符前數(shù)據(jù)弄抬,一般是沒有的掌猛,如果有就是一些注釋數(shù)據(jù)的數(shù)據(jù)
            // 按照MIME規(guī)范,消息頭和消息體之間的分隔界線前面可以有一些作為注釋信息的內(nèi)容
            discardBodyData();

            // Read boundary - if succeeded, the stream contains an
            // encapsulation.
            return readBoundary();
        } catch (MalformedStreamException e) {
            return false;
        } finally {
            // Restore delimiter.
            //成員變量boundary恢復(fù)回原來的樣子眉睹,并重新計(jì)算成員變量boundary的KMP算法前綴表
            System.arraycopy(boundary, 0, boundary, 2, boundary.length - 2);
            boundaryLength = boundary.length;
            boundary[0] = CR;
            boundary[1] = LF;
            computeBoundaryTable();//計(jì)算出分割線的KMP算法前綴表
        }
    }

在這里荔茬,我先不急著去說明skipPreamble的作用,而是先進(jìn)入discardBodyData()看看

discardBodyData

   public int discardBodyData() throws MalformedStreamException, IOException {
        return readBodyData(null);//
    }

從方法名中竹海,可以看出該方法用于丟棄沒有的數(shù)據(jù)慕蔚,如,按 照MIME規(guī)范斋配,消息頭和消息體之間的分隔界線前面可以有一些作為注釋信息的內(nèi)容孔飒。其實(shí)現(xiàn)的方式是調(diào)用readBodyData(OutputStream)。

readBodyData(OutputStream)

 public int readBodyData(OutputStream output)
            throws MalformedStreamException, IOException {
        //Streams.copy(InputStrea,OutputStream,closeOuputStream):將輸入流數(shù)據(jù)傳到輸出流中艰争,如果輸入流為null坏瞄,
        //                              則只進(jìn)行對輸入流的讀取,而不會(huì)講讀取的數(shù)據(jù)傳給輸出流,closeOuptStream表示是否關(guān)閉輸出流
        // 這返回已經(jīng)寫入的字節(jié)數(shù)甩卓,如果傳入的是輸出流為null的話鸠匀,則這里表示已經(jīng)讀取的字節(jié)數(shù)
        return (int) Streams.copy(newInputStream(), output, false); // N.B. Streams.copy closes the input stream
    }

這里要注意一下,readBodyData返回的是已經(jīng)寫入的字節(jié)數(shù)逾柿,但是如果傳入的輸出流為null缀棍,則返回已經(jīng)讀取的字節(jié)數(shù)。事實(shí)上机错,從源碼可以看到爬范,Streams.copy計(jì)算數(shù)量是在對每次輸入流讀取的字節(jié)數(shù)累加的,而不是對每次輸輸出流輸出的字節(jié)數(shù)累加弱匪,所以青瀑,Streams.copy更準(zhǔn)確的說返回的是已經(jīng)讀取的字節(jié)數(shù)。
在readBoyData中萧诫,我們看到調(diào)用了newInputStream()

newInputStream

ItemInputStream newInputStream() {
        return new ItemInputStream();
    }

又是內(nèi)部類斥难,交給ItemInputStream,ItemInputStream是繼承了InputStream的MultipartStream內(nèi)部了财搁,實(shí)現(xiàn)讀取主體文本中的參數(shù)內(nèi)容的相關(guān)操作蘸炸,這里我需要先說明一下ItemInputStream是如何讀取主體文本中的參數(shù)內(nèi)容躬络。

ItemInputStream讀取參數(shù)內(nèi)容的操作解析

從圖1可以看出尖奔,主體文本的組成就是分割線+參數(shù)內(nèi)容,所以我們要準(zhǔn)確地讀取到參數(shù)內(nèi)容,需要確定好分割線的起始位置提茁,分割線的長度淹禾。但是在讀取過程,因?yàn)橹黧w文本一般都比較大茴扁,只能通過一個(gè)緩存區(qū)buffer來分段讀取铃岔,也就造成了一個(gè)問題,無法確定那里是分割線峭火,因?yàn)閎uffer有可能完全將分割線的字節(jié)讀取了毁习,也有可能只讀取了分割線的部分字節(jié),也有可能完全沒有讀取到分割線的字節(jié)卖丸,所以MultipartStream定義了三個(gè)成員變量:buffer讀取的開始位置head,buffer讀取的最后一個(gè)位置tail,還有保留分割線字節(jié)數(shù)pad纺且,在讀取buffer的數(shù)據(jù)時(shí),以head作為開始讀取的游標(biāo)稍浆,以tail做為讀取buffer的結(jié)束位置载碌,而pad,會(huì)在buffer中沒有找到分割線時(shí)候衅枫,保留buffer的最后pad個(gè)的字節(jié)嫁艇,在下一次的從主體內(nèi)容的流中讀取到buffer的時(shí)候,會(huì)先將原buffer中最后pad個(gè)字節(jié)放到buffer的前面弦撩,再填充剩下的buffer字節(jié)步咪,從而精確的讀到參數(shù)內(nèi)容。
具體實(shí)現(xiàn)益楼,來看源碼:

ItemInputStream構(gòu)造方法

         ItemInputStream() {
            findSeparator();
        }

調(diào)用了查詢分割線的方法歧斟,我們再進(jìn)去看看

ItemInputStream.findSeparator()

  private void findSeparator() {
            pos = MultipartStream.this.findSeparator();
            if (pos == -1) {//緩沖區(qū)buffer中沒有包含分隔界線
                //讀取的數(shù)據(jù)量是否大于保留區(qū)的大小,來決定保留到下一次buffer緩沖區(qū)中的字節(jié)個(gè)數(shù)
                if (tail - head > keepRegion) {
                    pad = keepRegion;//大于保留區(qū)偏形,取保留區(qū)大小
                } else {
                    pad = tail - head;//小于保留區(qū)静袖,取所有數(shù)據(jù)量
                }
            }
        }

當(dāng)在buffer中,沒有找到分割線俊扭,pad將會(huì)被賦值队橙,keepRegion就是分割線的字節(jié)數(shù)。來看看MutlitpartStream.findSeparator的實(shí)現(xiàn)

 protected int findSeparator() {

        int bufferPos = this.head;
        int tablePos = 0;

        //KMP算法
        while (bufferPos < this.tail) {
            while (tablePos >= 0 && buffer[bufferPos] != boundary[tablePos]) {
                tablePos = boundaryTable[tablePos];
            }
            bufferPos++;
            tablePos++;
            if (tablePos == boundaryLength) {
                return bufferPos - boundaryLength;//通過減去分割線的長度就能得到分割線在buffer的起始位置
            }
        }
        return -1;
    }

可以看到萨惑,是buffer中進(jìn)行查找分割線捐康,而查詢的起始位置是head,使用的查詢方法是KMP算法庸蔼,而返回的就是分割線的在buffer中的起始位置

ItemInputStream.read(Byte[],int,int)

 @Override
        public int read(byte[] b, int off, int len) throws IOException {
            if (closed) {//判斷流是否已經(jīng)關(guān)閉
                throw new FileItemStream.ItemSkippedException();
            }
            if (len == 0) {
                return 0;
            }
            int res = available();//返回有效字節(jié)數(shù)
            if (res == 0) {
                res = makeAvailable();//讀取更多字節(jié)到buffer中解总,并返回有效字節(jié)數(shù)
                if (res == 0) {
                    return -1;
                }
            }
            res = Math.min(res, len);
            System.arraycopy(buffer, head, b, off, res);
            head += res;
            total += res;
            return res;
        }

先用available來查看buffer有沒有數(shù)據(jù)可以讀取,如果沒有通過makeAvailable從主體文本inputStream中讀取進(jìn)buffer中姐仅。我們先進(jìn)去看看available

    public int available() throws IOException {
            if (pos == -1) {//未找到分割線
                //buffer中的有效字節(jié)數(shù)=buffer中可讀的最后一個(gè)字節(jié)位置-buffer中已讀的字節(jié)數(shù)head-減去去保留區(qū)后的數(shù)據(jù)量pad
                return tail - head - pad;
            }
            //找到分割線的情況下:buffer中的有效字節(jié)數(shù)=buffer中分割線的開始位置-buffer中已讀的字節(jié)數(shù)head
            return pos - head;//返回分割線前面的數(shù)據(jù)量
        }

該方法就是返回在buffer中還可以讀的字節(jié)數(shù)花枫,即buffer中的有效字節(jié)數(shù)刻盐。

ItemInputStream.makeAvailable

      private int makeAvailable() throws IOException {
            if (pos != -1) {
                return 0;//在buffer中找到了分割線,就意味著已經(jīng)讀取完一個(gè)參數(shù)內(nèi)容劳翰,所以沒必要讀取下去敦锌。
            }

            // Move the data to the beginning of the buffer.
            //total 是統(tǒng)計(jì)已處理的字節(jié)數(shù)
            // tail-head-pad一般情況下都會(huì)是得出的結(jié)果都是0,因?yàn)閔ead在調(diào)用read的相關(guān)方法的時(shí)候會(huì)累加已讀去的字節(jié)數(shù)佳簸,使得head越來越逼近tail
            //              當(dāng)時(shí)又不會(huì)超過tail-pad的范圍
            total += tail - head - pad;
            //將上一次buffer緩沖區(qū)中的未處理的數(shù)據(jù)轉(zhuǎn)移到下一次buffer緩沖區(qū)的開始位置
            System.arraycopy(buffer, tail - pad, buffer, 0, pad);

            // Refill buffer with new data.
            head = 0;
            tail = pad;//因?yàn)檫@個(gè)時(shí)候的buffer已經(jīng)將上一次pad個(gè)字節(jié)放到buffer的開頭乙墙,所以tail要從pad開始,再通過下面的循環(huán)累加下去生均。

            for (;;) {
                int bytesRead = input.read(buffer, tail, bufSize - tail);//再讀取buffer緩沖區(qū)剩下的數(shù)據(jù)量
                if (bytesRead == -1) {
                    // The last pad amount is left in the buffer.
                    // Boundary can't be in there so signal an error
                    // condition.
                    final String msg = "Stream ended unexpectedly";
                    throw new MalformedStreamException(msg);
                }
                if (notifier != null) {
                    notifier.noteBytesRead(bytesRead);
                }
                tail += bytesRead;

                findSeparator();//再次查找看看有沒有分割線在buffer中,并確定保留的字節(jié)數(shù)pad的值
                int av = available();

                if (av > 0 || pos != -1) {
                    return av;//返回有效的字節(jié)數(shù)
                }
            }
        }

該方法就是從主體文本中讀取剩下的字節(jié)給buffer重新填充听想,填充之前,會(huì)先將上一次buffer的保留字節(jié)數(shù)挪到這次buffer的前面马胧,然后將讀取主體文本的字節(jié)填充到buffer剩下的元素哗魂。該方法還會(huì)調(diào)用findSeparator確定好分割線的開始位置,并返回調(diào)用available返回有效的字節(jié)數(shù)漓雅。

readBoundary

回到MultipartStream.skipPreamble方法上录别,除了調(diào)用discardBodayDat之外,還調(diào)用了readBoundary

  public boolean readBoundary()
            throws FileUploadIOException, MalformedStreamException {
        byte[] marker = new byte[2];
        boolean nextChunk = false;

        head += boundaryLength;//通過對head游標(biāo)加上分割線長度邻吞,使得head游標(biāo)跳過分割線的數(shù)據(jù)组题,
        try {
            //雖然已經(jīng)跳過了分割線的數(shù)據(jù)字節(jié),但是分割線后面還跟著一個(gè)回車換行抱冷,通過readByte逐個(gè)讀取處理
            //然后判斷是否真的是回車換行還是兩個(gè)'-'崔列,
            // 如果是回車換行,表示分隔界線是下一個(gè)分區(qū)的開始標(biāo)記旺遮,返回true
            // 如果是兩個(gè)'-',返回false赵讯,表示已經(jīng)到了文本體的末尾
            marker[0] = readByte();
            if (marker[0] == LF) {
                // Work around IE5 Mac bug with input type=image.
                // Because the boundary delimiter, not including the trailing
                // CRLF, must not appear within any file (RFC 2046, section
                // 5.1.1), we know the missing CR is due to a buggy browser
                // rather than a file containing something similar to a
                // boundary.
                //IE5 Mac 的bug。
                return true;
            }

            marker[1] = readByte();
            if (arrayequals(marker, STREAM_TERMINATOR, 2)) {
                nextChunk = false;
            } else if (arrayequals(marker, FIELD_SEPARATOR, 2)) {
                nextChunk = true;
            } else {
                throw new MalformedStreamException(
                "Unexpected characters follow a boundary");
            }
        } catch (FileUploadIOException e) {
            // wraps a SizeException, re-throw as it will be unwrapped later
            throw e;
        } catch (IOException e) {
            throw new MalformedStreamException("Stream ended unexpectedly");
        }
        return nextChunk;
    }

通過head游標(biāo)跳過分割線的數(shù)據(jù)和分割線與下個(gè)參數(shù)內(nèi)容的數(shù)據(jù)開始位置的回車換行耿眉,還檢驗(yàn)是否存在下一個(gè)分區(qū)數(shù)據(jù)边翼,和是否已經(jīng)到文本體結(jié)尾
并返回是否存在下一個(gè)參數(shù)內(nèi)容,返回true存在下一個(gè)分區(qū)數(shù)據(jù)鸣剪,返回false组底,說明已經(jīng)到文本體結(jié)尾

public byte readByte() throws IOException {
        // Buffer depleted ?//
        if (head == tail) {//這個(gè)情況一般會(huì)出現(xiàn)在一開始讀取數(shù)據(jù)的時(shí)候,head==0,tail==0
            head = 0;
            // Refill.
            tail = input.read(buffer, head, bufSize);//標(biāo)記1
            if (tail == -1) {
                // No more data available.
                throw new IOException("No more data is available");
            }
            if (notifier != null) {
                notifier.noteBytesRead(tail);
            }
        }
        return buffer[head++];
    }

一般情況下調(diào)用skipPremble的時(shí)候筐骇,也只有一開始的讀取主體文本的時(shí)候债鸡,head==tail才會(huì)成立,也就是說铛纬,readByte才是真正開始讀取主體文本數(shù)據(jù)的方法厌均。該方法一般情況下,都是直接通過head++直接取出buffer的時(shí)候告唆,還有一點(diǎn)需要注意的是棺弊,標(biāo)記1的位置晶密,input是HttpServletRequest的主體文本的字節(jié)流InputStream,而不是內(nèi)部ItemInputStream镊屎。

readHeaders

public String readHeaders() throws FileUploadIOException, MalformedStreamException {
        int i = 0;
        byte b;
        // to support multi-byte characters
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int size = 0;
        //該循環(huán),會(huì)讀取完該分區(qū)的所有消息頭數(shù)據(jù)字節(jié)直到讀取完兩個(gè)回車換行結(jié)束
        while (i < HEADER_SEPARATOR.length) {
            try {
                b = readByte();
            } catch (FileUploadIOException e) {
                // wraps a SizeException, re-throw as it will be unwrapped later
                throw e;
            } catch (IOException e) {
                throw new MalformedStreamException("Stream ended unexpectedly");
            }
            if (++size > HEADER_PART_SIZE_MAX) {
                throw new MalformedStreamException(
                        format("Header section has more than %s bytes (maybe it is not properly terminated)",
                               Integer.valueOf(HEADER_PART_SIZE_MAX)));
            }
            if (b == HEADER_SEPARATOR[i]) {
                i++;
            } else {
                i = 0;
            }
            baos.write(b);
        }

        //將獲取到的字節(jié)根據(jù)字符串headerEncoding轉(zhuǎn)換成字符串
        String headers = null;
        if (headerEncoding != null) {
            try {
                headers = baos.toString(headerEncoding);
            } catch (UnsupportedEncodingException e) {
                // Fall back to platform default if specified encoding is not
                // supported.
                headers = baos.toString();
            }
        } else {
            headers = baos.toString();
        }
        return headers;
    }

讀取分區(qū)里的消息頭部分,返回消息頭以及消息頭與值之間的兩個(gè)回車換行茄螃,即:Content-Disposition: form-data; name="test"\r\n\r\n

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末缝驳,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子归苍,更是在濱河造成了極大的恐慌用狱,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拼弃,死亡現(xiàn)場離奇詭異夏伊,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)吻氧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進(jìn)店門溺忧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人盯孙,你說我怎么就攤上這事鲁森。” “怎么了振惰?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵歌溉,是天一觀的道長。 經(jīng)常有香客問我骑晶,道長痛垛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任桶蛔,我火速辦了婚禮匙头,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘仔雷。我一直安慰自己乾胶,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布朽寞。 她就那樣靜靜地躺著识窿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪脑融。 梳的紋絲不亂的頭發(fā)上喻频,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天,我揣著相機(jī)與錄音肘迎,去河邊找鬼甥温。 笑死锻煌,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的姻蚓。 我是一名探鬼主播宋梧,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼狰挡!你這毒婦竟也來了捂龄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤加叁,失蹤者是張志新(化名)和其女友劉穎倦沧,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體它匕,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡展融,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了豫柬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片告希。...
    茶點(diǎn)故事閱讀 39,932評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖烧给,靈堂內(nèi)的尸體忽然破棺而出暂雹,到底是詐尸還是另有隱情,我是刑警寧澤创夜,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布杭跪,位于F島的核電站,受9級特大地震影響驰吓,放射性物質(zhì)發(fā)生泄漏涧尿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一檬贰、第九天 我趴在偏房一處隱蔽的房頂上張望姑廉。 院中可真熱鬧,春花似錦翁涤、人聲如沸桥言。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽号阿。三九已至,卻和暖如春鸳粉,著一層夾襖步出監(jiān)牢的瞬間扔涧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留枯夜,地道東北人弯汰。 一個(gè)月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像湖雹,于是被迫代替她去往敵國和親咏闪。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評論 2 354