當Android BLE遇上YModem

背景


最近公司的硬件設備需要升級程序,里面內(nèi)置的是藍牙4.0模塊進行通信逸月。產(chǎn)品已上市栓撞,而且出于成本考慮,以及升級的方便,不用拆機就可以升級瓤湘,決定繼續(xù)使用BLE來進行設備程序升級瓢颅。

協(xié)議


在單片機上進行文件傳輸常用的協(xié)議主要有以下幾個:

Xmodem


Xmodem is one of the most widely used file transfer protocols. The original Xmodem protocol uses 128-byte packets and a simple "checksum" method of error detection. A later enhancement, Xmodem-CRC, uses a more secure Cyclic Redundancy Check (CRC) method for error detection. Xmodem protocol always attempts to use CRC first. If the sender does not acknowledge the requests for CRC, the receiver shifts to the checksum mode and continues its request for transmission.

簡而言之,xmodem就是一個被廣泛使用的文件傳輸協(xié)議弛说,最初的xmodem協(xié)議使用的是128字節(jié)包大小并使用簡單的檢驗和來驗證數(shù)據(jù)是否出錯挽懦。后來就有了加強版的xmodem-crc,它使用的是crc來校驗數(shù)據(jù)正確性木人。只是它默認的還是原本的檢驗和信柿。

Xmodem-1K


Xmodem 1K is essentially Xmodem CRC with 1K (1024 byte) packets. On some systems and bulletin boards it may also be referred to as Ymodem. Some communication software programs, most notably Procomm Plus 1.x, also list Xmodem-1K as Ymodem. Procomm Plus 2.0 no longer refers to Xmodem-1K as Ymodem.

Xmodem-1K就是使用1024字節(jié)的包大小并加上CRC校驗的更高級的Xmodem。在有些系統(tǒng)和軟件里被稱為Ymodem醒第。

Ymodem


Ymodem is essentially Xmodem 1K that allows multiple batch file transfer. On some systems it is listed as Ymodem Batch.

Ymodem就是支持批量文件傳輸?shù)腦modem-1K協(xié)議渔嚷。

以上這些,都類似于TCP協(xié)議淘讥,每次傳輸都會校驗圃伶,但是速度會慢些。

Ymodem-g


Ymodem-g is a variant of Ymodem. It is designed to be used with modems that support error control. This protocol does not provide software error correction or recovery, but expects the modem to provide the service. It is a streaming protocol that sends and receives 1K packets in a continuous stream until instructed to stop. It does not wait for positive acknowledgement after each block is sent, but rather sends blocks in rapid succession. If any block is unsuccessfully transferred, the entire transfer is canceled.

Ymodem-g是Ymodem的一個變體蒲列,它支持錯誤校正窒朋。不過它并不自己處理,而是希望調(diào)制解調(diào)器來提供一個服務去處理蝗岖。這是一個流式的協(xié)議侥猩,數(shù)據(jù)發(fā)送出去并不等待正確響應再發(fā)下一包,而是一個包接一個包地發(fā)送抵赢。只要有一個包發(fā)送出錯欺劳,整個傳輸過程就將被取消。
顯然铅鲤,這個就類似于UDP協(xié)議划提,快,但是不保證穩(wěn)定性邢享。

Zmodem


Zmodem is generally the best protocol to use if the electronic service you are calling supports it. Zmodem has two significant features: it is extremely efficient and it provides crash recovery.
Like Ymodem-g, Zmodem does not wait for positive acknowledgement after each block is sent, but rather sends blocks in rapid succession. If a Zmodem transfer is canceled or interrupted for any reason, the transfer can be resurrected later and the previously transferred information need not be resent.

Zmodem一般被認為是最佳的協(xié)議鹏往。它有兩個很大的特性,就是它非常高效并且支持出錯時的自動恢復骇塘,并且不需要從頭開始伊履,就是它可以斷點續(xù)傳。

以上幾個協(xié)議介紹來自這里款违。

如何選擇


像我們的硬件設備升級唐瀑,比較關注的有幾個方面:

  • 穩(wěn)定性 容錯率很低,一旦出錯插爹,就會比較麻煩哄辣。因為我們的設備在最初選擇硬件的時候就沒有預留多少空間可供備份请梢。
  • 要適合與BLE這樣的設備進行交互。

綜上柔滔,YModem是Xmodem的升級版溢陪,它保證穩(wěn)定性。Zmodem支持快速傳輸睛廊,這個很好形真。

最終選擇是:Ymodem協(xié)議。

協(xié)議定制


基于我們具體的需求超全,在原有的基礎上加了一下前后的處理咆霜。

 * MY YMODEM IMPLEMTATION
 * *SENDER: ANDROID APP *------------------------------------------* RECEIVER: BLE DEVICE*
 * HELLO BOOTLOADER ---------------------------------------------->*
 * <---------------------------------------------------------------* C
 * SOH 00 FF filename0fileSizeInByte0MD5[90] ZERO[38] CRC CRC----->*
 * <---------------------------------------------------------------* ACK C
 * STX 01 FE data[1024] CRC CRC ---------------------------------->*
 * <---------------------------------------------------------------* ACK
 * STX 02 FF data[1024] CRC CRC ---------------------------------->*
 * <---------------------------------------------------------------* ACK
 * ...
 * ...
 * <p>
 * STX 08 F7 data[1000] CPMEOF[24] CRC CRC ----------------------->*
 * <---------------------------------------------------------------* ACK
 * EOT ----------------------------------------------------------->*
 * <---------------------------------------------------------------* ACK
 * SOH 00 FF ZERO[128] ------------------------------------------->*
 * <---------------------------------------------------------------* ACK
 * <---------------------------------------------------------------* MD5_OK

代碼實現(xiàn)


首先梳理一下它應該具有哪些模塊:

  • 協(xié)議的核心實現(xiàn)
    主要是負責數(shù)據(jù)傳輸過程中有關協(xié)議的部分,如在數(shù)據(jù)包上加入頭嘶朱,CRC蛾坯,驗證返回的正確性以及超時重發(fā)等。
  • 一個協(xié)議工具類疏遏,封裝包數(shù)據(jù)的提供
  • 一個文件數(shù)據(jù)的讀取模塊:它是耗時任務脉课,應該在子線程進行。
  • 各種執(zhí)行狀態(tài)的監(jiān)聽

下面直接上代碼

協(xié)議的核心實現(xiàn)Ymodem:

/**
 * Created by leonxtp on 2017/9/16.
 * Modified by leonxtp on 2017/9/16
 */

public class Ymodem implements FileStreamThread.DataRaderListener {

    private static final int STEP_HELLO = 0x00;
    private static final int STEP_FILE_NAME = 0x01;
    private static final int STEP_FILE_BODY = 0x02;
    private static final int STEP_EOT = 0x03;
    private static final int STEP_END = 0x04;
    private static int CURR_STEP = STEP_HELLO;

    private static final byte ACK = 0x06; /* ACKnowlege */
    private static final byte NAK = 0x15; /* Negative AcKnowlege */
    private static final byte CAN = 0x18; /* CANcel character */
    private static final byte ST_C = 'C';
    private static final String MD5_OK = "MD5_OK";
    private static final String MD5_ERR = "MD5_ERR";

    private Context mContext;
    private String filePath;
    private String fileNameString = "LPK001_Android";
    private String fileMd5String = "63e7bb6eed1de3cece411a7e3e8e763b";
    private YModemListener listener;

    private TimeOutHelper timerHelper = new TimeOutHelper();
    private FileStreamThread streamThread;

    //bytes has been sent of this transmission
    private int bytesSent = 0;
    //package data of current sending, used for int case of fail
    private byte[] currSending = null;
    private int packageErrorTimes = 0;
    private static final int MAX_PACKAGE_SEND_ERROR_TIMES = 5;
    //the timeout interval for a single package
    private static final int PACKAGE_TIME_OUT = 6000;

    /**
     * Construct of the YModemBLE,you may don't need the fileMD5 checking,remove it
     *
     * @param filePath       absolute path of the file
     * @param fileNameString file name for sending to the terminal
     * @param fileMd5String  md5 for terminal checking after transmission finished
     * @param listener
     */
    public Ymodem(Context context, String filePath,
                  String fileNameString, String fileMd5String,
                  YModemListener listener) {
        this.filePath = filePath;
        this.fileNameString = fileNameString;
        this.fileMd5String = fileMd5String;
        this.mContext = context;
        this.listener = listener;
    }

    /**
     * Start the transmission
     */
    public void start() {
        sayHello();
    }

    /**
     * Stop the transmission when you don't need it or shut it down in accident
     */
    public void stop() {
        bytesSent = 0;
        currSending = null;
        packageErrorTimes = 0;
        if (streamThread != null) {
            streamThread.release();
        }
        timerHelper.stopTimer();
    }

    /**
     * Method for the outer caller when received data from the terminal
     */
    public void onReceiveData(byte[] respData) {
        //Stop the package timer
        timerHelper.stopTimer();
        if (respData != null && respData.length > 0) {
            switch (CURR_STEP) {
                case STEP_HELLO:
                    handleHello(respData);
                    break;
                case STEP_FILE_NAME:
                    handleFileName(respData);
                    break;
                case STEP_FILE_BODY:
                    handleFileBody(respData[0]);
                    break;
                case STEP_EOT:
                    handleEOT(respData);
                    break;
                case STEP_END:
                    handleEnd(respData);
                    break;
                default:
                    break;
            }
        } else {
            L.f("The terminal do responsed something, but received nothing??");
        }
    }

    /**
     * ==============================================================================
     * Methods for sending data begin
     * ==============================================================================
     */
    private void sayHello() {
        streamThread = new FileStreamThread(mContext, filePath, this);
        CURR_STEP = STEP_HELLO;
        L.f("sayHello!!!");
        byte[] hello = YModemUtil.getYModelHello();
        if (listener != null) {
            listener.onDataReady(hello);
        }
    }

    private void sendFileName() {
        CURR_STEP = STEP_FILE_NAME;
        L.f("sendFileName");
        try {
            int fileByteSize = streamThread.getFileByteSize();
            byte[] hello = YModemUtil.getFileNamePackage(fileNameString, fileByteSize
                    , fileMd5String);
            if (listener != null) {
                listener.onDataReady(hello);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void startSendFileData() {
        CURR_STEP = STEP_FILE_BODY;
        L.f("startSendFileData");
        streamThread.start();
    }

    //Callback from the data reading thread when a data package is ready
    @Override
    public void onDataReady(byte[] data) {
        if (listener != null) {
            currSending = data;
            //Start the timer, it will be cancelled when reponse received,
            // or trigger the timeout and resend the current package data
            timerHelper.startTimer(timeoutListener, PACKAGE_TIME_OUT);
            listener.onDataReady(data);
        }
    }

    private void sendEOT() {
        CURR_STEP = STEP_EOT;
        L.f("sendEOT");
        if (listener != null) {
            listener.onDataReady(YModemUtil.getEOT());
        }
    }

    private void sendEND() {
        CURR_STEP = STEP_END;
        L.f("sendEND");
        if (listener != null) {
            try {
                listener.onDataReady(YModemUtil.getEnd());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * ==============================================================================
     * Method for handling the response of a package
     * ==============================================================================
     */
    private void handleHello(byte[] value) {
        int character = value[0];
        if (character == ST_C) {//Receive "C" for "HELLO"
            packageErrorTimes = 0;
            sendFileName();
        } else {
            handleOthers(character);
        }
    }

    //The file name package was responsed
    private void handleFileName(byte[] value) {
        if (value.length == 2 && value[0] == ACK && value[1] == ST_C) {//Receive 'ACK C' for file name
            packageErrorTimes = 0;
            startSendFileData();
        } else if (value[0] == ST_C) {//Receive 'C' for file name, this package should be resent
            handlePackageFail();
        } else {
            handleOthers(value[0]);
        }
    }

    private void handleFileBody(int character) {
        if (character == ACK) {//Receive ACK for file data
            packageErrorTimes = 0;
            bytesSent += currSending.length;
            try {
                if (listener != null) {
                    listener.onProgress(bytesSent, streamThread.getFileByteSize());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            streamThread.keepReading();

        } else if (character == ST_C) {
            //Receive C for file data, the ymodem cannot handle this circumstance, transmission failed...
            if (listener != null) {
                listener.onFailed();
            }
        } else {
            handleOthers(character);
        }
    }

    private void handleEOT(byte[] value) {
        if (value[0] == ACK) {
            packageErrorTimes = 0;
            sendEND();
        } else if (value[0] == ST_C) {//As we haven't received ACK, we should resend EOT
            handlePackageFail();
        } else {
            handleOthers(value[0]);
        }
    }

    private void handleEnd(byte[] character) {
        if (character[0] == ACK) {//The last ACK represents that the transmission has been finished, but we should validate the file
            packageErrorTimes = 0;
        } else if ((new String(character)).equals(MD5_OK)) {//The file data has been checked,Well Done!
            stop();
            if (listener != null) {
                listener.onSuccess();
            }
        } else if ((new String(character)).equals(MD5_ERR)) {//Oops...Transmission Failed...
            stop();
            if (listener != null) {
                listener.onFailed();
            }
        } else {
            handleOthers(character[0]);
        }
    }

    private void handleOthers(int character) {
        if (character == NAK) {//We need to resend this package as the terminal failed when checking the crc
            handlePackageFail();
        } else if (character == CAN) {//Some big problem occurred, transmission failed...
            stop();
        }
    }

    //Handle a failed package data ,resend it up to MAX_PACKAGE_SEND_ERROR_TIMES times.
    //If still failed, then the transmission failed.
    private void handlePackageFail() {
        packageErrorTimes++;
        if (packageErrorTimes < MAX_PACKAGE_SEND_ERROR_TIMES) {
            if (listener != null) {
                listener.onDataReady(currSending);
            }
        } else {
            //Still, we stop the transmission, release the resources
            stop();
            if (listener != null) {
                listener.onFailed();
            }
        }
    }

    /* The InputStream data reading thread was done */
    @Override
    public void onFinish() {
        sendEOT();
    }

    //The timeout listener
    private TimeOutHelper.ITimeOut timeoutListener = new TimeOutHelper.ITimeOut() {
        @Override
        public void onTimeOut() {
            if (currSending != null) {
                handlePackageFail();
            }
        }
    };

    public static class Builder {
        private Context context;
        private String filePath;
        private String fileNameString;
        private String fileMd5String;
        private YModemListener listener;

        public Builder with(Context context) {
            this.context = context;
            return this;
        }

        public Builder filePath(String filePath) {
            this.filePath = filePath;
            return this;
        }

        public Builder fileName(String fileName) {
            this.fileNameString = fileName;
            return this;
        }

        public Builder checkMd5(String fileMd5String) {
            this.fileMd5String = fileMd5String;
            return this;
        }

        public Builder callback(YModemListener listener) {
            this.listener = listener;
            return this;
        }

        public Ymodem build() {
            return new Ymodem(context, filePath, fileNameString, fileMd5String, listener);
        }

    }

}

協(xié)議包工具類

/**
 * Util for encapsulating data package of ymodem protocol
 * <p>
 * Created by leonxtp on 2017/9/16.
 * Modified by leonxtp on 2017/9/16
 */

public class YModemUtil {

    /*This is my concrete ymodem start signal, customise it to your needs*/
    private static final String HELLO = "HELLO BOOTLOADER";

    private static final byte SOH = 0x01; /* Start Of Header with data size :128*/
    private static final byte STX = 0x02; /* Start Of Header with data size : 1024*/
    private static final byte EOT = 0x04; /* End Of Transmission */
    private static final byte CPMEOF = 0x1A;/* Fill the last package if not long enough */

    private static CRC16 crc16 = new CRC16();

    /**
     * Get the first package data for hello with a terminal
     */
    public static byte[] getYModelHello() {
        return HELLO.getBytes();
    }

    /**
     * Get the file name package data
     *
     * @param fileNameString file name in String
     * @param fileByteSize   file byte size of int value
     * @param fileMd5String  the md5 of the file in String
     */
    public static byte[] getFileNamePackage(String fileNameString,
                                            int fileByteSize,
                                            String fileMd5String) throws IOException {

        byte seperator = 0x0;
        String fileSize = fileByteSize + "";
        byte[] byteFileSize = fileSize.getBytes();

        byte[] fileNameBytes1 = concat(fileNameString.getBytes(),
                new byte[]{seperator},
                byteFileSize);

        byte[] fileNameBytes2 = Arrays.copyOf(concat(fileNameBytes1,
                new byte[]{seperator},
                fileMd5String.getBytes()), 128);

        byte seq = 0x00;
        return getDataPackage(fileNameBytes2, 128, seq);
    }

    /**
     * Get a encapsulated package data block
     *
     * @param block      byte data array
     * @param dataLength the actual content length in the block without 0 filled in it.
     * @param sequence   the package serial number
     * @return a encapsulated package data block
     */
    public static byte[] getDataPackage(byte[] block, int dataLength, byte sequence) throws IOException {

        byte[] header = getDataHeader(sequence, block.length == 1024 ? STX : SOH);

        //The last package, fill CPMEOF if the dataLength is not sufficient
        if (dataLength < block.length) {
            int startFil = dataLength;
            while (startFil < block.length) {
                block[startFil] = CPMEOF;
                startFil++;
            }
        }

        //We should use short size when writing into the data package as it only needs 2 bytes
        short crc = (short) crc16.calcCRC(block);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);
        dos.writeShort(crc);
        dos.close();

        byte[] crcBytes = baos.toByteArray();

        return concat(header, block, crcBytes);
    }

    /**
     * Get the EOT package
     */
    public static byte[] getEOT() {
        return new byte[]{EOT};
    }

    /**
     * Get the Last package
     */
    public static byte[] getEnd() throws IOException {
        byte seq = 0x00;
        return getDataPackage(new byte[128], 128, seq);
    }

    /**
     * Get InputStream from Assets, you can customize it from the other sources
     *
     * @param fileAbsolutePath absolute path of the file in asstes
     */
    public static InputStream getInputStream(Context context, String fileAbsolutePath) throws IOException {
        return new InputStreamSource().getStream(context, fileAbsolutePath);
    }

    private static byte[] getDataHeader(byte sequence, byte start) {
        //The serial number of the package increases Cyclically up to 256
        byte modSequence = (byte) (sequence % 0x256);
        byte complementSeq = (byte) ~modSequence;

        return concat(new byte[]{start},
                new byte[]{modSequence},
                new byte[]{complementSeq});
    }

    private static byte[] concat(byte[] a, byte[] b, byte[] c) {
        int aLen = a.length;
        int bLen = b.length;
        int cLen = c.length;
        byte[] concated = new byte[aLen + bLen + cLen];
        System.arraycopy(a, 0, concated, 0, aLen);
        System.arraycopy(b, 0, concated, aLen, bLen);
        System.arraycopy(c, 0, concated, aLen + bLen, cLen);
        return concated;
    }
}

文件數(shù)據(jù)讀取類

/**
 * Thread for reading input Stream and encapsulating into a ymodem package
 * <p>
 * Created by leonxtp on 2017/9/16.
 * Modified by leonxtp on 2017/9/16
 */

public class FileStreamThread extends Thread {

    private Context mContext;
    private InputStream inputStream = null;
    private DataRaderListener listener;
    private String filePath;
    private AtomicBoolean isDataAcknowledged = new AtomicBoolean(false);
    private boolean isKeepRunning = false;
    private int fileByteSize = 0;

    public FileStreamThread(Context mContext, String filePath, DataRaderListener listener) {
        this.mContext = mContext;
        this.filePath = filePath;
        this.listener = listener;
    }

    public int getFileByteSize() throws IOException {
        if (fileByteSize == 0 || inputStream == null) {
            initStream();
        }
        return fileByteSize;
    }

    @Override
    public void run() {
        try {
            prepareData();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void prepareData() throws IOException {
        initStream();
        byte[] block = new byte[1024];
        int dataLength;
        byte blockSequence = 1;//The data package of a file is actually started from 1
        isDataAcknowledged.set(true);
        isKeepRunning = true;
        while (isKeepRunning) {

            if (!isDataAcknowledged.get()) {
                try {
                    //We need to sleep for a while as the sending 1024 bytes data from ble would take several seconds
                    //In my circumstances, this can be up to 3 seconds.
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                continue;
            }

            if ((dataLength = inputStream.read(block)) == -1) {
                L.f("The file data has all been read...");
                if (listener != null) {
                    onStop();
                    listener.onFinish();
                }
                break;
            }

            byte[] packige = YModemUtil.getDataPackage(block, dataLength, blockSequence);

            if (listener != null) {
                listener.onDataReady(packige);
            }

            blockSequence++;
            isDataAcknowledged.set(false);
        }

    }

    /**
     * When received response from the terminal ,we should keep the thread keep going
     */
    public void keepReading() {
        isDataAcknowledged.set(true);
    }

    public void release() {
        onStop();
        listener = null;
    }

    private void onStop() {
        isKeepRunning = false;
        isDataAcknowledged.set(false);
        fileByteSize = 0;
        onReadFinished();
    }

    private void initStream() {
        if (inputStream == null) {
            try {
                inputStream = YModemUtil.getInputStream(mContext, filePath);
                fileByteSize = inputStream.available();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void onReadFinished() {
        if (inputStream != null) {
            try {
                inputStream.close();
                inputStream = null;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public interface DataRaderListener {
        void onDataReady(byte[] data);

        void onFinish();
    }

}

各種狀態(tài)監(jiān)聽接口

/**
 * Listener of the transmission process
 */
public interface YModemListener {

    /* the data package has been encapsulated */
    void onDataReady(byte[] data);

    /*just the file data progress*/
    void onProgress(int currentSent, int total);

    /* the file has been correctly sent to the terminal */
    void onSuccess();

    /* the task has failed with several remedial measures like retrying some times*/
    void onFailed();

}

具體使用


初始化

        ymodem = new Ymodem.Builder()
                .with(this)
                .filePath("assets://demo.bin")
                .fileName("demo.bin")
                .checkMd5("lsfjlhoiiw121241l241lgljaf")
                .callback(new YModemListener() {
                    @Override
                    public void onDataReady(byte[] data) {
                        //send this data[] to your ble component here...
                    }

                    @Override
                    public void onProgress(int currentSent, int total) {
                        //the progress of the file data has transmitted
                    }

                    @Override
                    public void onSuccess() {
                        //we are well done with md5 checked
                    }

                    @Override
                    public void onFailed() {
                        //the task has failed for several times of trying
                    }
                }).build();

        ymodem.start();

開始傳輸

ymodem.start();

當接收到設備響應

ymodem.onReceiveData(data);

停止

ymodem.stop();

完整代碼在Github上财异。
如果對您有幫助倘零,歡迎star、fork戳寸!

參考


Wikipedia YMODEM
xmodem呈驶、ymodem、zmodem
aesirot ymodem on github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末疫鹊,一起剝皮案震驚了整個濱河市袖瞻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拆吆,老刑警劉巖聋迎,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異枣耀,居然都是意外死亡霉晕,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門奕枢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來娄昆,“玉大人佩微,你說我怎么就攤上這事缝彬。” “怎么了哺眯?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵谷浅,是天一觀的道長。 經(jīng)常有香客問我,道長一疯,這世上最難降的妖魔是什么撼玄? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮墩邀,結(jié)果婚禮上掌猛,老公的妹妹穿的比我還像新娘。我一直安慰自己眉睹,他們只是感情好荔茬,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著竹海,像睡著了一般慕蔚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上斋配,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天孔飒,我揣著相機與錄音,去河邊找鬼艰争。 笑死坏瞄,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的园细。 我是一名探鬼主播惦积,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼猛频!你這毒婦竟也來了狮崩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤鹿寻,失蹤者是張志新(化名)和其女友劉穎睦柴,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體毡熏,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡坦敌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了痢法。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狱窘。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖财搁,靈堂內(nèi)的尸體忽然破棺而出蘸炸,到底是詐尸還是另有隱情,我是刑警寧澤尖奔,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布搭儒,位于F島的核電站穷当,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏淹禾。R本人自食惡果不足惜馁菜,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望铃岔。 院中可真熱鬧汪疮,春花似錦、人聲如沸毁习。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蜓洪。三九已至纤勒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間隆檀,已是汗流浹背摇天。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留恐仑,地道東北人泉坐。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像裳仆,于是被迫代替她去往敵國和親腕让。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

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

  • 本來想著昨天熬到很晚給老師發(fā)了data就算暫時能喘口氣歧斟,結(jié)果早上收到郵件最好再深入做一做纯丸。 我想去度假。 暫定查爾斯頓静袖。
    ritaxqzhang閱讀 134評論 0 0
  • 艾米麗.狄金森 如果你秋天要來 我會如主婦對待蒼蠅般 以淺淺笑容和...
    近者悅遠者來閱讀 612評論 0 0
  • 姓名:魏浩~公司:杭州龍居門業(yè)有限公司 【日精進打卡第36天】 【知~學習】 《六項精進》1遍共1遍 《大學》1遍...
    A0魏浩富貴龍別墅門閱讀 96評論 0 0