当Android BLE遇上YModem

背景


最近公司的硬件设备需要升级程序,里面内置的是蓝牙4.0模块进行通信。产品已上市,而且出于成本考虑,以及升级的方便,不用拆机就可以升级,决定继续使用BLE来进行设备程序升级。

协议


在单片机上进行文件传输常用的协议主要有以下几个:

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就是一个被广泛使用的文件传输协议,最初的xmodem协议使用的是128字节包大小并使用简单的检验和来验证数据是否出错。后来就有了加强版的xmodem-crc,它使用的是crc来校验数据正确性。只是它默认的还是原本的检验和。

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字节的包大小并加上CRC校验的更高级的Xmodem。在有些系统和软件里被称为Ymodem

Ymodem


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

Ymodem就是支持批量文件传输的Xmodem-1K协议。

以上这些,都类似于TCP协议,每次传输都会校验,但是速度会慢些。

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的一个变体,它支持错误校正。不过它并不自己处理,而是希望调制解调器来提供一个服务去处理。这是一个流式的协议,数据发送出去并不等待正确响应再发下一包,而是一个包接一个包地发送。只要有一个包发送出错,整个传输过程就将被取消。
显然,这个就类似于UDP协议,快,但是不保证稳定性。

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一般被认为是最佳的协议。它有两个很大的特性,就是它非常高效并且支持出错时的自动恢复,并且不需要从头开始,就是它可以断点续传。

以上几个协议介绍来自这里

如何选择


像我们的硬件设备升级,比较关注的有几个方面:

  • 稳定性 容错率很低,一旦出错,就会比较麻烦。因为我们的设备在最初选择硬件的时候就没有预留多少空间可供备份。
  • 要适合与BLE这样的设备进行交互。

综上,YModem是Xmodem的升级版,它保证稳定性。Zmodem支持快速传输,这个很好。

最终选择是:Ymodem协议。

协议定制


基于我们具体的需求,在原有的基础上加了一下前后的处理。

 * 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

代码实现


首先梳理一下它应该具有哪些模块:

  • 协议的核心实现
    主要是负责数据传输过程中有关协议的部分,如在数据包上加入头,CRC,验证返回的正确性以及超时重发等。
  • 一个协议工具类,封装包数据的提供
  • 一个文件数据的读取模块:它是耗时任务,应该在子线程进行。
  • 各种执行状态的监听

下面直接上代码

协议的核心实现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);
        }

    }

}

协议包工具类

/**
 * 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;
    }
}

文件数据读取类

/**
 * 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();
    }

}

各种状态监听接口

/**
 * 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容

  • 本来想着昨天熬到很晚给老师发了data就算暂时能喘口气,结果早上收到邮件最好再深入做一做。 我想去度假。 暂定查尔斯顿。
    ritaxqzhang阅读 126评论 0 0
  • 艾米丽.狄金森 如果你秋天要来 我会如主妇对待苍蝇般 以浅浅笑容和...
    近者悦远者来阅读 597评论 0 0
  • 姓名:魏浩~公司:杭州龙居门业有限公司 【日精进打卡第36天】 【知~学习】 《六项精进》1遍共1遍 《大学》1遍...
    A0魏浩富贵龙别墅门阅读 87评论 0 0