Android之無線智能家居實現(xiàn)

起源

晚上在家里洗澡的時候锦担,突然想聽聽歌俭识,自high一把,拿起洗漱柜上的手機放音樂洞渔,不過因為手上的水套媚,導致屏幕按鈕點擊特別煩,結果它掉地上了磁椒。這時候突然有一種想法堤瘤,我用Android能不能實現(xiàn)類似"天貓精靈"這些東西呢?

正文

概述

其實現(xiàn)流程共分3步浆熔,分別為尋址授權本辐,局域網(wǎng)通信,外網(wǎng)通信。其總體架構圖如下所示:


整體架構圖.png

如上所示慎皱,整體架構分2個模塊老虫,分別為家庭局域網(wǎng)模式和互聯(lián)網(wǎng)模式,初始狀態(tài)時茫多,手機通過家庭局域網(wǎng)獲取到Android設備的IP信息以及相應的授權任務祈匙,從而獲得設備操作權限,之后通過局域網(wǎng)通訊的方式進行業(yè)務操作天揖,同時該設備會與服務端進行任務同步夺欲。當該通過授權的手機在互聯(lián)網(wǎng)模式下時,可進行任務下發(fā)今膊,此時家庭局域網(wǎng)中的Android設備會同步到來自服務端下發(fā)的任務些阅,進行相應的業(yè)務操作。

尋址授權

該流程為架構實現(xiàn)的第一步万细,也是實現(xiàn)局域網(wǎng)通訊的前提扑眉。因為設備處于無線模式下,可能會導致IP前后出現(xiàn)變化赖钞,所以每次局域網(wǎng)通訊之前需要先獲取到設備的IP地址腰素,實現(xiàn)該功能的方案有3種,一種是IP輪詢檢索雪营,其次是藍牙配對弓千,最后一種是UDP廣播。

IP輪詢檢索:即從1~255進行一個個的socket連接測試献起,Android設備端進行accept洋访,手機端進行連接嘗試,當手機端獲取到來自設備端的返回時谴餐,說明當前的IP為該設備的IP地址姻政。但是該方法耗時且對性能不足的特點,此案中不引入岂嗓。

藍牙配對:通過手機設備的藍牙進行檢索附近的藍牙設備汁展,然后進行配對授權,因為考慮到該功能的實現(xiàn)需要藍牙服務厌殉,提升了設備成本食绿,其次藍牙服務只能一對一進行交互服務,當存在多部手機設備時公罕,無法滿足該功能器紧。最后因為藍牙服務差不多為10~15m的覆蓋范圍,考慮家庭中存在墻面等情況楼眷,該方案并不合適铲汪。

UDP廣播:使用UDP協(xié)議進行信息的傳輸之前不需要建立連接熊尉。換句話說就是客戶端向服務器發(fā)送信息,客戶端只需要給出服務器的ip地址和端口號桥状,然后將信息封裝到一個待發(fā)送的報文中并且發(fā)送出去帽揪。至于服務器端是否存在硝清,或者能否收到該報文辅斟,客戶端根本不用管。其中廣播UDP與單播UDP的區(qū)別就是IP地址不同芦拿,廣播使用廣播地址255.255.255.255士飒,將消息發(fā)送到在同一廣播網(wǎng)絡上的每個主機。該方案也是本案所采用的方案蔗崎。

ps:androidSDK中在android.net.nsd目錄下存在NsdManager一個類酵幕,該類能夠實現(xiàn)局域網(wǎng)下面的android設備通訊,并且SDK已經(jīng)提供了相應的封裝缓苛,使用起來非常方便芳撒,實現(xiàn)原理是通過網(wǎng)絡服務的發(fā)現(xiàn)服務NsdService,其基于蘋果的Bonjour服務發(fā)現(xiàn)協(xié)議,支持遠程服務的發(fā)現(xiàn)和零配置未桥,相對考慮到IOS的實現(xiàn)笔刹,怕出現(xiàn)兼容性問題,所以該方案暫時不進行考慮冬耿。

實現(xiàn)流程

尋址授權流程圖.png

實現(xiàn)代碼

手機設備請求方

public abstract class DeviceSearchWorker extends Thread {

    private static final String TAG = "DeviceSearchWorker";


    private static final int RECEIVE_TIME_OUT = 15000; // 接收超時時間


    private static final byte PACKET_TYPE_FIND_DEVICE_REQ_13 = 0x13; // 搜索請求
    private static final byte PACKET_TYPE_FIND_DEVICE_RSP_14 = 0x14; // 搜索響應
    private static final byte PACKET_TYPE_FIND_DEVICE_CHK_15 = 0x15; // 搜索確認


    private static final int DEVICE_FIND_PORT = 10000;
    private static final int RESPONSE_DEVICE_MAX = 200; //接收消息的最大次數(shù)


    private static final byte PACKET_DATA_TYPE_DEVICE_NAME_20 = 0x20;
    private static final byte PACKET_DATA_TYPE_DEVICE_ROOM_21 = 0x21;

    private String deviceIP; //發(fā)送廣播之后舌菜,設備返回來的設備ip地址
    DatagramSocket socket = null;

    private Set<DeviceBean> deviceSet;

    public DeviceSearchWorker(){
        deviceSet = new HashSet<>();
    }


    private Handler myHandler = new Handler(Looper.getMainLooper());

    @Override
    public void run() {
        super.run();

        try{
            onPushDeviceSearchStartMsg();
            socket = new DatagramSocket();
            socket.setSoTimeout(RECEIVE_TIME_OUT);

            byte[] sendData = new byte[1024];
            InetAddress broadIp = InetAddress.getByName("255.255.255.255");  //來一個廣播
            DatagramPacket packet = new DatagramPacket(sendData, sendData.length, broadIp, DEVICE_FIND_PORT);

            for (int i = 0; i < 3; i++){
                packet.setData(packetData(i + 1, PACKET_TYPE_FIND_DEVICE_REQ_13));
                //發(fā)送廣播
                socket.send(packet);
                // 監(jiān)聽來信
                byte[] receData = new byte[1024];
                DatagramPacket recePacket = new DatagramPacket(receData, receData.length);

                int rspCount = RESPONSE_DEVICE_MAX;
                while (rspCount-- > 0) {
                    LogUtils.i(TAG, "DatagramPacket >>> " + rspCount);
                    recePacket.setData(receData);
                    socket.receive(recePacket);
                    if (recePacket.getLength() > 0) {
                        deviceIP = recePacket.getAddress().getHostAddress();
                        if (parsePack(recePacket)) {
                            LogUtils.i(TAG, "設備上線:" + deviceIP);
                            // 發(fā)送一對一的確認信息。使用接收報亦镶,因為接收報中有對方的實際IP日月,發(fā)送報時廣播IP
                            recePacket.setData(packetData(rspCount, PACKET_TYPE_FIND_DEVICE_CHK_15)); // 注意:設置數(shù)據(jù)的同時,把recePack.getLength()也改變了
                            socket.send(recePacket);
                            onPushDeviceSearchFinishedMsg();
                        }
                    }
                }
            }

        }catch (SocketException e){
            e.printStackTrace();
            onPushDeviceSearchFailedMsg();
        } catch (UnknownHostException e) {
            e.printStackTrace();
            onPushDeviceSearchFailedMsg();
        } catch (IOException e) {
            e.printStackTrace();
            onPushDeviceSearchFailedMsg();
        } finally {
            if(socket != null){
                socket.close();

            }
        }
    }

    private void onPushDeviceSearchFailedMsg() {
        myHandler.post(new Runnable() {
            @Override
            public void run() {
                onPushDeviceSearchFailedMsg();
            }
        });
    }

    private void onPushDeviceSearchFinishedMsg() {
        myHandler.post(new Runnable() {
            @Override
            public void run() {
                onDeviceSearchFinished(deviceSet);
            }
        });
    }

    private void onPushDeviceSearchStartMsg() {
        myHandler.post(new Runnable() {
            @Override
            public void run() {
                onDeviceSearchStart();
            }
        });
    }


    /**
     * 解析報文
     * 協(xié)議:$ + packType(1) + data(n)
     *  data: 由n組數(shù)據(jù)缤骨,每組的組成結構type(1) + length(4) + data(length)
     *  type類型中包含name爱咬、room類型,但name必須在最前面
     */
    private boolean parsePack(DatagramPacket pack) {
        if (pack == null || pack.getAddress() == null) {
            return false;
        }


        String ip = pack.getAddress().getHostAddress();
        int port = pack.getPort();
        for (DeviceBean d : deviceSet) {
            if (d.getIp().equals(ip)) {
                return false;
            }
        }
        int dataLen = pack.getLength();
        int offset = 0;
        byte packType;
        byte type;
        int len;
        DeviceBean device = null;

        if (dataLen < 2) {
            return false;
        }
        byte[] data = new byte[dataLen];
        System.arraycopy(pack.getData(), pack.getOffset(), data, 0, dataLen);

        if (data[offset++] != '$') {
            return false;
        }

        packType = data[offset++];
        if (packType != PACKET_TYPE_FIND_DEVICE_RSP_14) {
            return false;
        }

        while (offset + 5 < dataLen) {
            type = data[offset++];
            len = data[offset++] & 0xFF;
            len |= (data[offset++] << 8);
            len |= (data[offset++] << 16);
            len |= (data[offset++] << 24);

            if (offset + len > dataLen) {
                break;
            }
            switch (type) {
                case PACKET_DATA_TYPE_DEVICE_NAME_20:
                    String name = new String(data, offset, len, Charset.forName("UTF-8"));
                    device = new DeviceBean();
                    device.setName(name);
                    device.setIp(ip);
                    device.setPort(port);
                    break;
                case PACKET_DATA_TYPE_DEVICE_ROOM_21:
                    String room = new String(data, offset, len, Charset.forName("UTF-8"));
                    if (device != null) {
                        device.setRoom(room);
                    }
                    break;
                default: break;
            }
            offset += len;
        }
        if (device != null) {
            deviceSet.add(device);
            return true;
        }
        return false;
    }

    /**
     * 協(xié)議:$ + packType(1) + sendSeq(4) + [deviceIP(n<=15)]
     * @param seq 發(fā)送序列
     * @param packetType 報文類型
     * @return
     */
    private byte[] packetData(int seq, byte packetType) {
        byte[] data = new byte[1024];
        int offset = 0;

        data[offset++] = '$';

        data[offset++] = packetType;

        seq = seq == 3 ? 1 : ++seq; // can't use findSeq++
        data[offset++] = (byte) seq;
        data[offset++] = (byte) (seq >> 8 );
        data[offset++] = (byte) (seq >> 16);
        data[offset++] = (byte) (seq >> 24);

        if (packetType == PACKET_TYPE_FIND_DEVICE_CHK_15) {
            byte[] ips = deviceIP.getBytes(Charset.forName("UTF-8"));
            System.arraycopy(ips, 0, data, offset, ips.length);
            offset += ips.length;
        }

        byte[] result = new byte[offset];
        System.arraycopy(data, 0, result, 0, offset);
        return result;



    }


    public abstract void onDeviceSearchStart();


    public abstract void onDeviceSearchFinished(Set<DeviceBean> deviceSet);


    public abstract void onDeviceSearchFailed();


    public void close(){
        try{
            if(socket != null){
                socket.close();
            }
        }catch (Exception e){
            e.printStackTrace();
        }

        this.interrupt();
    }
}

Android設備接收方

public abstract class DeviceClientWorker extends Thread {

    /**
     * 設備對應的port
     */
    private static final int DEVICE_FIND_PORT = 10000;

    private static final String TAG = "DeviceClientWorker";

    private static final int RECEIVE_TIME_OUT = 10000; // 接收超時時間绊起,應小于等于主機的超時時間10000

    private static final byte PACKET_TYPE_FIND_DEVICE_REQ_13 = 0x13; // 搜索請求
    private static final byte PACKET_TYPE_FIND_DEVICE_RSP_14 = 0x14; // 搜索響應
    private static final byte PACKET_TYPE_FIND_DEVICE_CHK_15 = 0x15; // 搜索確認

    private static final byte PACKET_DATA_TYPE_DEVICE_NAME_20 = 0x20; //設備名稱
    private static final byte PACKET_DATA_TYPE_DEVICE_ROOM_21 = 0x21; //設備所處的房間名

    private static final int RESPONSE_DEVICE_MAX = 200; // 響應設備的最大個數(shù)精拟,防止UDP廣播攻擊


    private static Handler workHandler = new Handler(Looper.getMainLooper());


    /**
     * 設備名稱
     */
    private String deviceName;
    /**
     * 房間名稱
     */
    private String room;

    private boolean isRunning;

    DatagramSocket socket = null;


    public DeviceClientWorker(String deviceName, String room){
        this.deviceName = deviceName;
        this.room = room;
        isRunning = true;
    }



    @Override
    public void run() {
        super.run();

        try {
            socket = new DatagramSocket(DEVICE_FIND_PORT);
            byte[] data = new byte[1024];
            DatagramPacket packet = new DatagramPacket(data, data.length);
            while (isRunning){
                LogUtils.i(TAG, "waitting receive data");
                socket.receive(packet);  //等待接收數(shù)據(jù)
                LogUtils.i(TAG, "data received");
                if(verifySearchData(packet)){
                    byte[] backData = packData();
                    LogUtils.i(TAG, "back device info");
                    DatagramPacket sendPacket = new DatagramPacket(backData, backData.length, packet.getAddress(), packet.getPort());
                    socket.send(sendPacket);
                    socket.setSoTimeout(RECEIVE_TIME_OUT);
                    LogUtils.i(TAG, "waitting for server veritify again");
                    socket.receive(packet);
                    if(verifyCheckData(packet)){ //驗證確認信息
                        pushDeviceClientSearchedMsg((InetSocketAddress)packet.getSocketAddress());
                    }
                }

                socket.setSoTimeout(0); // 連接超時還原成無窮大,阻塞式接收
            }
        } catch (SocketException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(socket != null){
                socket.close();
            }
        }
    }

    private void pushDeviceClientSearchedMsg(final InetSocketAddress socketAddress) {
        workHandler.post(new Runnable() {
            @Override
            public void run() {
                onDeviceSearchedCallBack(socketAddress);
            }
        });
    }

    /**
     * 當設備被發(fā)現(xiàn)時執(zhí)行
     */
    public abstract void onDeviceSearchedCallBack(InetSocketAddress socketAddr);

    /**
     * 驗證再次確認信息
     * 協(xié)議:$ + packType(1) + sendSeq(4) + deviceIP(n<=15)
     *  packType - 報文類型
     *  sendSeq - 發(fā)送序列
     *  deviceIP - 設備IP勒庄,僅確認時攜帶
     * @param packet
     * @return
     */
    private boolean verifyCheckData(DatagramPacket packet) {
        if(packet.getLength() < 6){
            return false; //前面的$ + packType(1) + sendSeq(4)   共占6位
        }

        byte[] data = packet.getData();
        int offset = packet.getOffset();
        int sendSeq;
        if (data[offset++] != '$' || data[offset++] != PACKET_TYPE_FIND_DEVICE_CHK_15) {
            return false;
        }

        sendSeq = data[offset++] & 0xFF;
        sendSeq |= (data[offset++] << 8 );
        sendSeq |= (data[offset++] << 16);
        sendSeq |= (data[offset++] << 24);
        if (sendSeq < 1 || sendSeq > RESPONSE_DEVICE_MAX) {
            return false;
        }

        String ip = new String(data, offset, packet.getLength() - offset, Charset.forName("UTF-8"));
        LogUtils.i(TAG, "ip from host : " + ip);
        return ip.equals(DeviceUtils.getOwnWifiIP());
    }

    /**
     * 搜索響應
     * 組裝搜索反饋信息
     * 協(xié)議:$ + packType(1) + data(n)
     * data: 由n組數(shù)據(jù)串前,每組的組成結構type(1) + length(4) + data(length)
     * type類型中包含name、room類型实蔽,但name必須在最前面
     * @return
     */
    private byte[] packData() {
        byte[] data = new byte[1024];
        int offset = 0;
        data[offset++] = '$';
        data[offset++] = PACKET_TYPE_FIND_DEVICE_RSP_14;
        //追加設備名稱信息
        byte[] temp = getBytesFromType(PACKET_DATA_TYPE_DEVICE_NAME_20, deviceName);
        System.arraycopy(temp, 0, data, offset, temp.length);
        offset += temp.length;

        temp = getBytesFromType(PACKET_DATA_TYPE_DEVICE_ROOM_21, room);
        System.arraycopy(temp, 0, data, offset, temp.length);
        offset += temp.length;

        byte[] retVal = new byte[offset];
        System.arraycopy(data, 0, retVal, 0, offset);

        return retVal;
    }

    /**
     * 根據(jù)類型 追加數(shù)據(jù)
     * @param dataType
     * @param data
     * @return
     */
    private byte[] getBytesFromType(byte dataType, String data) {
        byte[] retVal = new byte[0];
        if(data != null){
            byte[] tmpData = data.getBytes(Charset.forName("utf-8"));
            retVal = new byte[5 + tmpData.length]; //5來源于  type(1) + length(4)
            retVal[0] = dataType;
            retVal[1] = (byte) tmpData.length;
            retVal[2] = (byte) (tmpData.length << 8 );
            retVal[3] = (byte) (tmpData.length << 16);
            retVal[4] = (byte) (tmpData.length << 24);
            System.arraycopy(tmpData, 0, retVal, 5, tmpData.length);
        }
        return retVal;
    }

    /**
     * 驗證接收到的數(shù)據(jù)是否為約定的合法搜索數(shù)據(jù)
     * 協(xié)議:$ + packType(1) + sendSeq(4)
     * @param packet
     * @return
     */
    private boolean verifySearchData(DatagramPacket packet) {
        if(packet.getLength() != 6){
            return false;
        }

        byte[] data = packet.getData();
        int offset = packet.getOffset();
        int sendReq = 0;
        //校驗格式是否正確
        if (data[offset++] != '$' || data[offset++] != PACKET_TYPE_FIND_DEVICE_REQ_13) {
            return false;
        }

        sendReq = data[offset++] & 0xFF;
        sendReq |= (data[offset++] << 8 );
        sendReq |= (data[offset++] << 16);
        sendReq |= (data[offset++] << 24);
        return sendReq >= 1 && sendReq <= 3;
    }

    public void close() {
        try{
            if(socket != null){
                socket.close();
            }
        }catch (Exception e){
            e.printStackTrace();
        }

        isRunning = false;
        this.interrupt();
    }
}

考慮到安全性交互荡碾,可進行多個不同的協(xié)議進行設備認證,以上代碼并不包含token驗證機制局装,可后續(xù)進行追加

局域網(wǎng)通信

該環(huán)節(jié)需要將Android設備當做服務器坛吁,來處理接收客戶端的業(yè)務請求劳殖。因為在尋址授權那一環(huán)節(jié)時,使用UDP來實現(xiàn)功能拨脉。當時想到的是后續(xù)的索性全部使用UDP來實現(xiàn)好了哆姻,全部使用自定義協(xié)議。但是這樣一來玫膀,發(fā)現(xiàn)后續(xù)所有的業(yè)務需求的實現(xiàn)都無法借鑒我們平時通用的方法了矛缨,此時就想,是不是能模擬Http請求帖旨,使用Android設備實現(xiàn)后臺服務箕昭?

Android設備服務端實現(xiàn)
通常我們搭建后臺Server所使用的是Tomcat容器,翻閱了一下資料解阅,發(fā)現(xiàn)Apache官方提供了一個叫HttpCore這個包落竹,可以用它來建立客戶端、代理货抄、服務端Http服務同時支持同步異步服務述召,相關鏈接地址,除此之外蟹地,還需要模擬HttpServlet积暖,并對其進行Controller、Service锈津、Dao三層模塊劃分呀酸,這里介紹一個Android的開源框架:AndServer

其實現(xiàn)的系統(tǒng)流程圖如下所示:


system_flow_chat.gif

應用層運行時流程圖如下所示:


framework_flow_chat.gif

該框架模擬了SpringMVC的注解方式來實現(xiàn),最后關于Dao層數(shù)據(jù)庫的實現(xiàn)琼梆,使用LitePal數(shù)據(jù)庫框架進行實現(xiàn)性誉。

手機局域網(wǎng)客戶端實現(xiàn)

相對來說,客戶端的實現(xiàn)非常簡單茎杂,在通過UDP授權之后错览,將會獲取到來自Android設備的token,以及IP地址煌往,后續(xù)的業(yè)務請求只需要通過Http請求服務器的方式請求局域網(wǎng)中的Android設備倾哺,這里不再進行詳細介紹

外網(wǎng)通信

該環(huán)節(jié)產(chǎn)生的場景來自于當我們身處在非局域網(wǎng)覆蓋范圍時,但又想要進行Android設備操作刽脖,如:迅雷下載


外網(wǎng)同步.png

手機實現(xiàn)

判斷當前設備是否進行過授權操作羞海,如存在多個,可進行Android設備的選擇曲管,同時進行業(yè)務請求操作給服務端

服務端實現(xiàn)

接收來自手機設備的業(yè)務請求却邓,每個請求中會包含需要操作的Android設備ID信息以及業(yè)務行為,并對該操作進行SyncKey的自增院水。

接收來自Android設備的每隔10s的輪詢同步請求腊徙,返回該ID設備對應的業(yè)務行為简十。

Android設備實現(xiàn)

當同步到新的SyncKey信息時,更新本地Job數(shù)據(jù)庫撬腾,并對新的業(yè)務進行流程處理

擴展

  1. 將家庭電視機集成Android設備的應用螟蝙,實現(xiàn)媒體資源下載,迅雷下載民傻,室內視頻遙控控制
  2. 在樹莓派3中燒入AndroidThings胰默,并在應用中集成科大訊飛語音功能,實現(xiàn)室內無線投屏饰潜,窗簾初坠、燈泡開關控制、音樂播放等等
  3. Android設備集成相機功能彭雾,實現(xiàn)視頻流RTSP實時傳輸遠程查看家庭監(jiān)控
  4. Android設備集成Face++,實現(xiàn)門鎖開關報警
  5. AndroidThings集成一氧化碳傳感器锁保,實現(xiàn)家庭燃氣檢測報警
  6. 等等等等
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末薯酝,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子爽柒,更是在濱河造成了極大的恐慌吴菠,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浩村,死亡現(xiàn)場離奇詭異做葵,居然都是意外死亡,警方通過查閱死者的電腦和手機心墅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門酿矢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人怎燥,你說我怎么就攤上這事瘫筐。” “怎么了铐姚?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵策肝,是天一觀的道長。 經(jīng)常有香客問我隐绵,道長之众,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任依许,我火速辦了婚禮棺禾,結果婚禮上,老公的妹妹穿的比我還像新娘悍手。我一直安慰自己帘睦,他們只是感情好袍患,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著竣付,像睡著了一般诡延。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上古胆,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天肆良,我揣著相機與錄音,去河邊找鬼逸绎。 笑死惹恃,一個胖子當著我的面吹牛,可吹牛的內容都是我干的棺牧。 我是一名探鬼主播巫糙,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼颊乘!你這毒婦竟也來了参淹?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤乏悄,失蹤者是張志新(化名)和其女友劉穎浙值,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體檩小,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡开呐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了规求。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片筐付。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖颓哮,靈堂內的尸體忽然破棺而出家妆,到底是詐尸還是另有隱情,我是刑警寧澤冕茅,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布伤极,位于F島的核電站,受9級特大地震影響姨伤,放射性物質發(fā)生泄漏哨坪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一乍楚、第九天 我趴在偏房一處隱蔽的房頂上張望当编。 院中可真熱鬧,春花似錦徒溪、人聲如沸忿偷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鲤桥。三九已至揍拆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間茶凳,已是汗流浹背嫂拴。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留贮喧,地道東北人筒狠。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像箱沦,于是被迫代替她去往敵國和親辩恼。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內容