Wifi扫描流程解析

前言

HI,欢迎来到《每周一博》。今天是十二月第一周,我给大家介绍一下安卓系统WiFi的扫描过程。

一. 痛点

为什么要走读Wifi源码,因为定位离不开Wifi,之前在解答问题的时候,总是会有用户报无法获取Wifi的问题,包括发起Wifi扫描,获取Wifi结果和系统缓存的一系列问题,所以我决定带着这些疑问去看一下Wifi的基本流程到底是什么样的,如何发起了扫描,获得结果什么时候用的是缓存,究竟是否有3分钟清缓存的限制。

二. 状态机

Wifi的工作过程使用了状态机,为了了解Wifi工作过程,我特意先学习了一下状态机,否则真不懂,关于状态机我写了一篇文章《状态机工作原理》来介绍。

这里再简单介绍下帮助理解,状态机主要用到了状态模式,不同的状态行为也不同。状态树有很多节点,子节点继承自父亲节点,状态都有enter,exit,processMessage方法,如果当前状态不处理消息,就会由父状态去处理。状态机有deferMessage,transitTo,sendMessage方法,分别是把消息推迟到下一状态执行,切换到某个状态,发送消息方法。状态机内部是通过Handler来实现发送接收消息的。

在WifiStateMachine里面就有几十个状态机,每个状态机处理的消息类型和处理方法都不一样,这就需要搞清楚当前是什么状态,当前要处理什么消息,如何处理,处理完之后变成了什么状态。

三. Wifi上层架构图

我们和Wifi交互的入口就是WifiManager,它通过Binder机制和WifiService进行跨进程通讯,WifiService的具体实现是WifiServiceImpl,它内部有个重要的状态机WifiStateMachine,还有和底层交互的WifiNative类,这里面封装了一些命令,如doCommand(),和监听函数,如wifi_wait_for_event()。

和WifiNative交互的是wpa_supplicant,它是Linux上的一个开源项目,被谷歌修改后加入Android移动平台,用来支持WEP,WPA/WPA2和WAPI无线协议和加密认证,而实际上的工作内容是通过socket(不管是与上层还是与驱动)与驱动交互上报数据给用户,而用户可以通过socket发送命令给wpa_supplicant调动驱动来对WiFi芯片操作。 简单的说,wpa_supplicant就是WiFi驱动和用户的中转站外加对协议和加密认证的支持。

四. Wifi扫描

Wifi扫描的入口是WifiManager的startScan方法,它代理了mService的startScan方法,它们之间是通过Binder来传递消息的。

public boolean startScan(WorkSource workSource) {  
    try {  
        mService.startScan(workSource);  
        return true;  
    } catch (RemoteException e) {  
        return false;  
    }  
}  

先来看一下安卓4.4的实现,mService的实现是WifiService,它和WifiManger通过IWifiManager接口调用。

WifiService里有2个重要的状态机WifiControl,WifiStateMachine,当调用startScan时先回检查权限,然后执行WifiStateMachine的startScan方法。

public void startScan(WorkSource workSource) {
    enforceChangePermission();
    if (workSource != null) {
        enforceWorkSourcePermission();
        workSource.clearNames();
    }
    mWifiStateMachine.startScan(Binder.getCallingUid(), workSource);
}

在WifiStateMachine的构造函数里,初始化了mWifiNative和mWifiMonitor对象,构建了状态树,设置初始状态并开启状态机;

mWifiNative = new WifiNative(mInterfaceName);
mWifiMonitor = new WifiMonitor(this, mWifiNative);

addState(mDefaultState);
    addState(mInitialState, mDefaultState);
    addState(mSupplicantStartingState, mDefaultState);
    addState(mSupplicantStartedState, mDefaultState);
        addState(mDriverStartingState, mSupplicantStartedState);
        addState(mDriverStartedState, mSupplicantStartedState);
            addState(mScanModeState, mDriverStartedState);
            addState(mConnectModeState, mDriverStartedState);
                addState(mL2ConnectedState, mConnectModeState);
                    addState(mObtainingIpState, mL2ConnectedState);
                    addState(mVerifyingLinkState, mL2ConnectedState);
                    addState(mCaptivePortalCheckState, mL2ConnectedState);
                    addState(mConnectedState, mL2ConnectedState);
                addState(mDisconnectingState, mConnectModeState);
                addState(mDisconnectedState, mConnectModeState);
                addState(mWpsRunningState, mConnectModeState);
        addState(mWaitForP2pDisableState, mSupplicantStartedState);
        addState(mDriverStoppingState, mSupplicantStartedState);
        addState(mDriverStoppedState, mSupplicantStartedState);
    addState(mSupplicantStoppingState, mDefaultState);
    addState(mSoftApStartingState, mDefaultState);
    addState(mSoftApStartedState, mDefaultState);
        addState(mTetheringState, mSoftApStartedState);
        addState(mTetheredState, mSoftApStartedState);
        addState(mUntetheringState, mSoftApStartedState);
setInitialState(mInitialState);
start();

WifiStateMachine构建的状态机是这样的,每一个状态要切换到另一状态都需要走过该树上的所有相关节点,而不能直接跨越。比如从mInitialState状态切换到mDriverStartedState需要经历mDefaultState,mSupplicantStartedState,mDriverStartedState三个状态,接下来我们看WifiStateMachine的startScan方法。

 sendMessage(CMD_START_SCAN, callingUid, 0, workSource);

只是发送了一个指令CMD_START_SCAN,那我们看一下不同状态对该指令的行为是什么;
DefaultState:break(不执行)
DriverStartingState:deferMessage(推迟到下一状态)
ObtainingIpState:deferMessage(推迟到下一状态)
在DriverStartedState中处理该消息

noteScanStart(message.arg1, (WorkSource) message.obj);
startScanNative(WifiNative.SCAN_WITH_CONNECTION_SETUP);

在ScanModeState中处理该消息

noteScanStart(message.arg1, (WorkSource) message.obj);      
startScanNative(WifiNative.SCAN_WITHOUT_CONNECTION_SETUP);

都是调用startScanNative方法,只是参数不同而已,noteScanStart用于通知电量统计,startScanNative会调用 mWifiNative.scan(type),向wpa_supplicant发送SCAN的命令,至此一条发起wifi扫描的请求就走完了,那么扫描完后得到结果如何通知的呢?

可以想象这个过程是异步的,不是一发起扫描就会立刻得到结果,所以需要有一个监听器不断的去监听事件。在WifiMonitor里面有一个MonitorThread线程在不断的监听WifiNative上报的事件,这是个无限循环,当接收到事件时会做解析,然后根据不同的类型去调用dispatchEvent(eventStr)来分发事件。

for (;;) {
       String eventStr = mWifiNative.waitForEvent();
}

当事件类型是扫描结果时,会执行handleEvent方法,在这里把消息发出去;

   case SCAN_RESULTS:
        mStateMachine.sendMessage(SCAN_RESULTS_EVENT);
        break;

WifiStateMachine的SupplicantStartedState会处理SCAN_RESULTS_EVENT这个消息,它会做两件事,一是去获取scanResults,二是发送一个广播消息;

setScanResults();
sendScanResultsAvailableBroadcast();

setScanResult主要就是把从WifiNative获取到AP列表信息进行循环解析,然后赋值给系统缓存mScanResultCache和结果列表mScanResults, mScanResultCache使用了LRUCache,它以bssid+ssid做key值。

    private void setScanResults() {
        while (true) {
            tmpResults = mWifiNative.scanResults(sid);
            if (TextUtils.isEmpty(tmpResults)) break;
            scanResultsBuf.append(tmpResults);
            scanResultsBuf.append("\n");
            String[] lines = tmpResults.split("\n");
            sid = -1;
            for (int i=lines.length - 1; i >= 0; i--) {
                if (lines[i].startsWith(END_STR)) {
                    break;
                } else if (lines[i].startsWith(ID_STR)) {
                    try {
                        sid = Integer.parseInt(lines[i].substring(ID_STR.length())) + 1;
                    } catch (NumberFormatException e) {
                        // Nothing to do
                    }
                    break;
               }
           }
            if (sid == -1) break;
        }
        synchronized(mScanResultCache) {
            mScanResults = new ArrayList<ScanResult>();
            String[] lines = scanResults.split("\n");
            final int bssidStrLen = BSSID_STR.length();
            final int flagLen = FLAGS_STR.length();

            for (String line : lines) {
                if (line.startsWith(BSSID_STR)) {
                    bssid = new String(line.getBytes(), bssidStrLen, line.length() - bssidStrLen);
                } else if (line.startsWith(FREQ_STR)) {
                    try {
                        freq = Integer.parseInt(line.substring(FREQ_STR.length()));
                    } catch (NumberFormatException e) {
                        freq = 0;
                    }
                    ……………………………………
                } else if (line.startsWith(SSID_STR)) {
                    wifiSsid = WifiSsid.createFromAsciiEncoded(
                            line.substring(SSID_STR.length()));
                } else if (line.startsWith(DELIMITER_STR) || line.startsWith(END_STR)) {
                    if (bssid != null) {
                        String ssid = (wifiSsid != null) ? wifiSsid.toString() : WifiSsid.NONE;
                        String key = bssid + ssid;
                        ScanResult scanResult = mScanResultCache.get(key);
                        if (scanResult != null) {
                            scanResult.level = level;
                            scanResult.wifiSsid = wifiSsid;
                            // Keep existing API
                            scanResult.SSID = (wifiSsid != null) ? wifiSsid.toString() :
                                    WifiSsid.NONE;
                            scanResult.capabilities = flags;
                            scanResult.frequency = freq;
                            scanResult.timestamp = tsf;
                        } else {
                            scanResult =
                                new ScanResult(
                                        wifiSsid, bssid, flags, level, freq, tsf);
                            mScanResultCache.put(key, scanResult);
                        }
                        mScanResults.add(scanResult);
                    }
                }
            }
        }
    }

WifiNative.scanResut的返回结果格式如下,每个AP之间用"===="分割,末尾以“####”来表示结束。

id=1
bssid=68:7f:76:d7:1a:6e
freq=2412
level=-44
tsf=1344626243700342
flags=[WPA2-PSK-CCMP][WPS][ESS]
ssid=zfdy
====
id=2
bssid=68:5f:74:d7:1a:6f
req=5180
level=-73
tsf=1344626243700373
flags=[WPA2-PSK-CCMP][WPS][ESS]
ssid=zuby
####

这里对结果ScanResult做个说明,它描述了AP的信息,具体包含以下字段;
A. String SSID:网络名称
B. String BSSID:WiFi的mac地址,也是唯一id
C. String capabilities:描述认证、密钥管理以及加密方式
D. int level:信号等级,单位是dBm,也被称作RSSI,一般是个负数,越大信号越强,比如-50强过-100
E. int frequency:客户端与WiFi通信的频率,单位MHz,如果频率值在2400-2500之间是2.4GHz,如果频率值在4900-5900之间是5GHz
F. long timestamp:从启动开始到该扫描记录最后一次被发现经过的微秒数。

接下来就是去发广播了,广播的Action是WifiManager.SCAN_RESULTS_AVAILABLE_ACTION,然后通过getScanResults去取mScanResults信息就可以了,需要注意的是这里的广播设置了Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT这个属性,所以只有动态注册的broadcastReceive才会收到广播。

    private void sendScanResultsAvailableBroadcast() {
        noteScanEnd();
        Intent intent = new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
        mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
    }

至此一个发起wifi扫描的流程就算是走完了,从6.0开始发送结果的广播增加了EXTRA_RESULTS_UPDATED新字段,如果是true表示结果可用。

private void sendScanResultBroadcast(boolean scanSucceeded) {
    Intent intent = new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
    intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
    intent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, scanSucceeded);
    mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
}  

所以我们在监听广播的时候先判断下系统版本,如果高于M,取出EXTRA_RESULTS_UPDATED字段,如果为true,再取结果。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            boolean hasResult=intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false);
            if (hasResult){
                List<ScanResult> results = wifiManager.getScanResults();
            }
        }

一个安卓4.4发起wifi扫描的流程如下图;

另外8.0的扫描流程有了很大的变化,主要功能都是由WifiScanner来完成的,它的具体实现是WifiScanningServiceImpl,当调用WifiStateMachine的startScanNative时会走到WifiScanner的startScan方法。

   private boolean startScanNative(final Set<Integer> freqs,
        List<WifiScanner.ScanSettings.HiddenNetwork> hiddenNetworkList,
        WorkSource workSource) {
    WifiScanner.ScanSettings settings = new WifiScanner.ScanSettings();
    if (freqs == null) {
        settings.band = WifiScanner.WIFI_BAND_BOTH_WITH_DFS;
    } else {
        settings.band = WifiScanner.WIFI_BAND_UNSPECIFIED;
        int index = 0;
        settings.channels = new WifiScanner.ChannelSpec[freqs.size()];
        for (Integer freq : freqs) {
            settings.channels[index++] = new WifiScanner.ChannelSpec(freq);
        }
    }
    settings.reportEvents = WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN
            | WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT;

    settings.hiddenNetworks =
            hiddenNetworkList.toArray(
                    new WifiScanner.ScanSettings.HiddenNetwork[hiddenNetworkList.size()]);

    WifiScanner.ScanListener nativeScanListener = new WifiScanner.ScanListener() {
            // ignore all events since WifiStateMachine is registered for the supplicant events
            @Override
            public void onSuccess() {
            }
            @Override
            public void onFailure(int reason, String description) {
                mIsScanOngoing = false;
                mIsFullScanOngoing = false;
            }
            @Override
            public void onResults(WifiScanner.ScanData[] results) {
            }
            @Override
            public void onFullResult(ScanResult fullScanResult) {
            }
            @Override
            public void onPeriodChanged(int periodInMs) {
            }
        };
    mWifiScanner.startScan(settings, nativeScanListener, workSource);
    mIsScanOngoing = true;
    mIsFullScanOngoing = (freqs == null);
    lastScanFreqs = freqs;
    return true;
}

WifiScanner的startScan方法里会通过mAsyncChannel发送了一个CMD_START_SINGLE_SCAN的消息;

public void startScan(ScanSettings settings, ScanListener listener, WorkSource workSource) {
    Preconditions.checkNotNull(listener, "listener cannot be null");
    int key = addListener(listener);
    if (key == INVALID_KEY) return;
    validateChannel();
    Bundle scanParams = new Bundle();
    scanParams.putParcelable(SCAN_PARAMS_SCAN_SETTINGS_KEY, settings);
    scanParams.putParcelable(SCAN_PARAMS_WORK_SOURCE_KEY, workSource);
    mAsyncChannel.sendMessage(CMD_START_SINGLE_SCAN, 0, key, scanParams);
}

WifiScanningServiceImpl的ClientHandler接受到该消息后会调用状态机发消息的方法。

mSingleScanStateMachine.sendMessage(Message.obtain(msg));

WifiSingleScanStateMachine状态接收到消息后,会执行关键的方法tryToStartNewScan;

case WifiScanner.CMD_START_SINGLE_SCAN:
    mWifiMetrics.incrementOneshotScanCount();
    int handler = msg.arg2;
    Bundle scanParams = (Bundle) msg.obj;
    if (scanParams == null) {
        logCallback("singleScanInvalidRequest",  ci, handler, "null params");
        replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "params null");
        return HANDLED;
    }
    scanParams.setDefusable(true);
    ScanSettings scanSettings =
            scanParams.getParcelable(WifiScanner.SCAN_PARAMS_SCAN_SETTINGS_KEY);
    WorkSource workSource =
            scanParams.getParcelable(WifiScanner.SCAN_PARAMS_WORK_SOURCE_KEY);
    if (validateScanRequest(ci, handler, scanSettings, workSource)) {
        logScanRequest("addSingleScanRequest", ci, handler, workSource,
                scanSettings, null);
        replySucceeded(msg);

        // If there is an active scan that will fulfill the scan request then
        // mark this request as an active scan, otherwise mark it pending.
        // If were not currently scanning then try to start a scan. Otherwise
        // this scan will be scheduled when transitioning back to IdleState
        // after finishing the current scan.
        if (getCurrentState() == mScanningState) {
            if (activeScanSatisfies(scanSettings)) {
                mActiveScans.addRequest(ci, handler, workSource, scanSettings);
            } else {
                mPendingScans.addRequest(ci, handler, workSource, scanSettings);
            }
        } else {
            mPendingScans.addRequest(ci, handler, workSource, scanSettings);
            tryToStartNewScan();
        }
    } else {
        logCallback("singleScanInvalidRequest",  ci, handler, "bad request");
        replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "bad request");
        mWifiMetrics.incrementScanReturnEntry(
                WifiMetricsProto.WifiLog.SCAN_FAILURE_INVALID_CONFIGURATION, 1);
    }
    return HANDLED;

而它又会调用startSingleScan,最终调到了WifiNative的scan方法,一个安卓8.0的wifi扫描流程图如下。

五. 获取Wifi结果

获取WiFi结果需要调用WifiManager的getScanResults方法,这个实现在8.0上也有所不同,我们先来看4.4的实现。

它最终会走到WifiStateMachine的syncGetScanResultsList方法,它的实现如下;

public List<ScanResult> syncGetScanResultsList() {
    synchronized (mScanResultCache) {
        List<ScanResult> scanList = new ArrayList<ScanResult>();
        for(ScanResult result: mScanResults) {
            scanList.add(new ScanResult(result));
        }
        return scanList;
    }
}

上面说到扫描到wifi结果后会把结果存到全局变mScanResults中,所以这里直接把该变量里的值返回去,比较简单。

在8.0上调用getScanResults时,它调用的是WifiScanner的getSingleScanResults方法,它是用mAsyncChannel发送了一个CMD_GET_SINGLE_SCAN_RESULTS的消息;

public List<ScanResult> getSingleScanResults() {
    validateChannel();
    Message reply = mAsyncChannel.sendMessageSynchronously(CMD_GET_SINGLE_SCAN_RESULTS, 0);
    if (reply.what == WifiScanner.CMD_OP_SUCCEEDED) {
        return Arrays.asList(((ParcelableScanResults) reply.obj).getResults());
    }
    OperationResult result = (OperationResult) reply.obj;
    Log.e(TAG, "Error retrieving SingleScan results reason: " + result.reason
            + " description: " + result.description);
    return new ArrayList<ScanResult>();
}

WifiScanningServiceImpl的ClientHandler接受到该消息后会用状态机发消息,mSingleScanStateMachine.sendMessage(Message.obtain(msg)),接着WifiSingleScanStateMachine状态接收到消息后,执行filterCachedScanResultsByAge这个方法来填充数据;

case WifiScanner.CMD_GET_SINGLE_SCAN_RESULTS:
    msg.obj = new WifiScanner.ParcelableScanResults(
        filterCachedScanResultsByAge());
    replySucceeded(msg);
    return HANDLED;

我们来看一下filterCachedScanResultsByAge这个方法;

private ScanResult[] filterCachedScanResultsByAge() {
    // Using ScanResult.timestamp here to ensure that we use the same fields 
    // as WificondScannerImpl for filtering stale results.
    long currentTimeInMillis = mClock.getElapsedSinceBootMillis();
    return mCachedScanResults.stream()
            .filter(scanResult
                    -> ((currentTimeInMillis - (scanResult.timestamp / 1000))
                            < CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS))
            .toArray(ScanResult[]::new);
}

mCachedScanResults存了每一次获得到扫描结果的数据,在每次发广播前进行赋值。CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS是180秒,也就是3分钟,所以3分钟后不发起wifi扫描直接取结果的话得到的是空。

if (results.isAllChannelsScanned()) {
    mCachedScanResults.clear();
    mCachedScanResults.addAll(Arrays.asList(results.getResults()));
    sendScanResultBroadcast(true);
}

接下来就是返回结果了

void replySucceeded(Message msg) {
    if (msg.replyTo != null) {
        Message reply = Message.obtain();
        reply.what = WifiScanner.CMD_OP_SUCCEEDED;
        reply.arg2 = msg.arg2;
        if (msg.obj != null) {
            reply.obj = msg.obj;
        }
        try {
            msg.replyTo.send(reply);
            mLog.trace("replySucceeded recvdMessage=%").c(msg.what).flush();
        } catch (RemoteException e) {
            // There's not much we can do if reply can't be sent!
        }
    } else {
        // locally generated message; doesn't need a reply!
    }
}

会发送一个CMD_OP_SUCCEEDED的消息,WifiScanner接收到该消息后会继续上抛,直到返给WifiManger。

所以8.0新加了一个3分钟的时间限制,即只返回3分钟内的缓存结果,这点在定制Wifi策略的时候需要考虑一下。

六. 总结

本文介绍了wifi扫描和获得结果的流程,我觉得wifi源码要比网络定位复杂一些,网络定位主要是两层client-server调用,而wifi主要是先得搞懂状态模式,状态机这些东西,这样才能知道wifi各种状态是如何切换的,发送了一个消息该由哪个状态去执行。走读源码是深入学习的必经之路,阅读安卓源码的过程,也是学习设计模式的过程,比如谷歌为什么这么设计,为什么需要这么多的类,它是如何做到面向抽象,保持类的功能单一的,这些都值得我们去掌握。感谢大家的阅读,我们下周再见。

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

推荐阅读更多精彩内容