Android如何实现茄子快传

Android如何实现茄子快传

茄子快传是一款文件传输应用,相信大家都很熟悉这款应用,应该很多人用过用来文件的传输。它有两个核心的功能:

  1. 端到端的文件传输
  2. Web端的文件传输

这两个核心的功能我们具体来分析一下!

端到端的文件传输

所谓的端到端的文件传输是指应用端发送到应用端(这里的应用端指Android应用端),这种文件传输方式是文件发送端和文件接收端必须安装应用。

效果图

文件发送方

文件发送方_1

文件发送方_2

文件发送方_3

文件接收方

文件接收方_1

文件接收方_2

简单的文件传输的话,我们可以用蓝牙,wifi直连,ftp这几种方式来进行文件的传输。但是:

  1. 蓝牙传输的话,速度太慢,而且要配对。相对比较麻烦。
  2. wifi直连差不多跟蓝牙一样,但是速率很快,也要配对。
  3. ftp可以实现文件的批量传输,但是没有文件的缩略图。

最初分析这个项目的时候就想着通过自定义协议的Socket的通信来实现,自定义的协议包括header + body的自定义协议, header部分包括了文件的信息(长度,大小,文件路径,缩略图), body部分就是文件。现在实现这一功能。(后序:后面开发《网页传》功能的时候,可以考虑这两个核心的功能都能用在Android架设微型Http服务器来实现。这是后话了。)

流程图

端到端的流程图

编码实现

两部设备文件传输是需要在一个局域网的条件下的,只有文件发送方连接上文件接收方的热点(搭建了一个局域网),这样文件发送方和文件接收方就在一个局域网里面,我们才可以进行Socket通信。这是一个大前提!

初始化条件 -- Ap(热点)和Wifi的管理, 文件的扫描

对Android的Ap(热点)和Wifi的一些操作都封装在下面两个类:

WifiMgr.java

APMgr.java

关于热点和Wifi的操作都是根据WifiManager来操作的。所以要像操作WifiManeger是必须要一些权限的。必须在AndroidManifest.xml清单文件里面声明权限:


    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

文件接收端打开热点并且配置热点的代码:


        //1.初始化热点
        WifiMgr.getInstance(getContext()).disableWifi();
        if(ApMgr.isApOn(getContext())){
            ApMgr.disableAp(getContext());
        }

        //热点相关的广播
        mWifiAPBroadcastReceiver = new WifiAPBroadcastReceiver() {
            @Override
            public void onWifiApEnabled() {
                Log.i(TAG, "======>>>onWifiApEnabled !!!");
                if(!mIsInitialized){
                    mUdpServerRuannable = createSendMsgToFileSenderRunnable();
                    AppContext.MAIN_EXECUTOR.execute(mUdpServerRuannable);
                    mIsInitialized = true;

                    tv_desc.setText(getResources().getString(R.string.tip_now_init_is_finish));
                    tv_desc.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            tv_desc.setText(getResources().getString(R.string.tip_is_waitting_connect));
                        }
                    }, 2*1000);
                }
            }
        };
        IntentFilter filter = new IntentFilter(WifiAPBroadcastReceiver.ACTION_WIFI_AP_STATE_CHANGED);
        registerReceiver(mWifiAPBroadcastReceiver, filter);

        ApMgr.isApOn(getContext()); // check Ap state :boolean
        String ssid = TextUtils.isNullOrBlank(android.os.Build.DEVICE) ? Constant.DEFAULT_SSID : android.os.Build.DEVICE;
        ApMgr.configApState(getContext(), ssid); // change Ap state :boolean

对于类WifiAPBroadcastReceiver是热点的一个广播类,最后一行代码是配置指定名称的热点,这里是以设备名称作为热点的名称。

文件发送端发送文件,文件发送端首先要选择要发送的文件,然后将要选择的文件存储起来,这里我是用了一个HashMap将发送的文件存储起来,key是文件的路径,value是FileInfo对象。

以下是扫描手机存储盘上面的文件列表的代码:


    /**
     * 存储卡获取 指定后缀名文件 
     * @param context
     * @param extension 
     * @return
     */
    public static List<FileInfo> getSpecificTypeFiles(Context context, String[] extension){
        List<FileInfo> fileInfoList = new ArrayList<FileInfo>();

        //内存卡文件的Uri
        Uri fileUri= MediaStore.Files.getContentUri("external");
        //筛选列,这里只筛选了:文件路径和含后缀的文件名
        String[] projection=new String[]{
                MediaStore.Files.FileColumns.DATA, MediaStore.Files.FileColumns.TITLE
        };

        //构造筛选条件语句
        String selection="";
        for(int i=0;i<extension.length;i++)
        {
            if(i!=0)
            {
                selection=selection+" OR ";
            }
            selection=selection+ MediaStore.Files.FileColumns.DATA+" LIKE '%"+extension[i]+"'";
        }
        //按时间降序条件
        String sortOrder = MediaStore.Files.FileColumns.DATE_MODIFIED;

        Cursor cursor = context.getContentResolver().query(fileUri, projection, selection, null, sortOrder);
        if(cursor != null){
            while (cursor.moveToNext()){
                try{
                    String data = cursor.getString(0);
                    FileInfo fileInfo = new FileInfo();
                    fileInfo.setFilePath(data);

                    long size = 0;
                    try{
                        File file = new File(data);
                        size = file.length();
                        fileInfo.setSize(size);
                    }catch(Exception e){

                    }
                    fileInfoList.add(fileInfo);
                }catch (Exception e){
                    Log.i("FileUtils", "------>>>" + e.getMessage());
                }

            }
        }
        Log.i(TAG, "getSize ===>>> " + fileInfoList.size());
        return fileInfoList;
    }

注意**:这里扫描的FileInfo对象只是扫描了文件路径filePath, 还有文件的大小size。
FileInfo的其他属性到文件传输的时候再二次获取,获取FileInfo的其他属性都在FileUtils这个工具类里面了。

文件发送端打开wifi扫描热点并且连接热点的代码:


        if(!WifiMgr.getInstance(getContext()).isWifiEnable()) {//wifi未打开的情况,打开wifi
            WifiMgr.getInstance(getContext()).openWifi();
        }

        //开始扫描
        WifiMgr.getInstance(getContext()).startScan();
        mScanResultList = WifiMgr.getInstance(getContext()).getScanResultList();
        mScanResultList = ListUtils.filterWithNoPassword(mScanResultList);

        if(mScanResultList != null){
            mWifiScanResultAdapter = new WifiScanResultAdapter(getContext(),mScanResultList);
            lv_result.setAdapter(mWifiScanResultAdapter);
            lv_result.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                    //单击选中指定的网络
                    ScanResult scanResult = mScanResultList.get(position);
                    Log.i(TAG, "###select the wifi info ======>>>" + scanResult.toString());

                    //1.连接网络
                    String ssid = Constant.DEFAULT_SSID;
                    ssid = scanResult.SSID;
                    WifiMgr.getInstance(getContext()).openWifi();
                    WifiMgr.getInstance(getContext()).addNetwork(WifiMgr.createWifiCfg(ssid, null, WifiMgr.WIFICIPHER_NOPASS));

                    //2.发送UDP通知信息到 文件接收方 开启ServerSocketRunnable
                    mUdpServerRuannable = createSendMsgToServerRunnable(WifiMgr.getInstance(getContext()).getIpAddressFromHotspot());
                    AppContext.MAIN_EXECUTOR.execute(mUdpServerRuannable);
                }
            });
        }

对于ListUtils.filterWithNoPassword是将扫描的结果进行过滤,过滤掉有密码的扫描结果。

lv_result.setOnItemClickListener回调的方法是连接指定的热点来形成一个局域网。文件传输的大前提条件就已经形成了。

到这里文件发送端和文件接收端的初始化环境也就搭建起来了。

文件传输模块

文件传输模块的核心代码就只有4个类,Transferable, BaseTransfer, FileSender, FileReceiver

Transferable是接口。

BaseTransfer, FileSender, FileReceiver是类。

对于文件发送端,每一个文件发送对应一个FileSender,而对于文件接收端,每一个文件的接收对应一个FileReceiver。
而FileSender,FileReceiver是继承自 抽象类BaseTransfer的。 BaseTransfer是实现了Transferable接口。

下面是4个类图的关系:

这里写图片描述

在Transferable接口中定义了4个方法,分别是初始化解析头部解析主体结束。解析头部和解析主体分别对应上面说的自定义协议的headerbody。初始化是为每一次文件传输做初始化工作,而结束是为每一次文件传输做结束工作,比如关闭一些资源流,Socket等等。

而BaseTransfer就只是实现了Transferable, 里面封装了一些常量。没有实现具体的方法,具体的实现是FileSender,FileReceiver。

代码详情:

Transferable
BaseTransfer
FileSender
FileReceiver

总结

端到端的文件传输就分析到这里,主要是Ap热点的操作,Wifi的操作,Socket通信来实现文件的传输。但是这里的Socket用到的不是异步IO,是同步IO。所以会引起阻塞。比如在FileSender中的暂停文件传输pause方法调用之后,会引起FileReceiver中文件传输的阻塞。如果你对异步IO有兴趣,你也可以去实现一下。

对于端对端的核心代码都是在 io.github.mayubao.kuaichuan.core 包下面。
这是我在github上面的项目链接 https://github.com/mayubao/KuaiChuan

web端的文件传输

所谓的Web端的文件传输是指文件发送端作为一个Http服务器,提供文件接收端来下载。这种文件传输方式是文件发送端必须安装应用,而文件接收端只需要有浏览器即可

效果图

文件发送端

文件选择
开启Http服务器

文件接收端

文件接收端浏览器访问

在android应用端架设微型Http服务器来实现文件的传输。这里可以用ftp来实现,为什么不用ftp呢?因为没有缩略图,这是重点!

web端的文件传输的核心重点:

  1. 文件发送端热点的开启(参考端对端的热点操作类 APMgr.java
  2. 文件发送端架设Http服务器。

Android端的Http服务器

Android上微型Http服务器(Socket实现),结合上面的效果图分析。主要解决三种Http url的请求形式就行了,由上面的文件接收端的效果图可以看出来(文件接收端是去访问文件发送端的Http服务器),大致可以分为三种链接:

  1. Index主页链接 http://hostname:port
  2. Image链接 http://hostname:port/image/xxx.xxx
  3. Download链接 http://hostname:port/download/xxx.xxx
这里写图片描述

下面用Socket来实现在Android上面的微型Http服务器的。

关于Http协议,我简单的描述一下Http协议。对于Http协议,就是"请求-回复(响应)"的这种通信模式。客户端发出请求,服务器根据请求,返回一个回复(响应)给客户端。

Http请求的大致分为四个部分:

  1. 请求行
  2. 请求头
  3. 空行
  4. 请求实体

Http响应的大致分为四个部分:

  1. 状态行
  2. 响应头
  3. 空行
  4. 响应实体

Http请求(POST请求)的示例


POST /image/index.html HTTP/1.1
Host: 127.0.0.1:7878
Connection: keep-alive
Content-Length: 247
Cache-Control: no-cache
Origin: chrome-extension://fdmmgilgnpjigdojojpjoooidkmcomcm
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryLIr5t1rdtuD8Ztuw
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6

------WebKitFormBoundaryLIr5t1rdtuD8Ztuw
Content-Disposition: form-data; name="username"

mayubao
------WebKitFormBoundaryLIr5t1rdtuD8Ztuw
Content-Disposition: form-data; name="username"

123456
------WebKitFormBoundaryLIr5t1rdtuD8Ztuw--

1.请求行(请求方式 + uri + http版本)


POST /image/index.html HTTP/1.1

2.请求头


Host: 127.0.0.1:7878
Connection: keep-alive
Content-Length: 247
Cache-Control: no-cache
Origin: chrome-extension://fdmmgilgnpjigdojojpjoooidkmcomcm
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryLIr5t1rdtuD8Ztuw
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6

3.空行

4.请求实体(对于GET请求一般没有请求实体)


------WebKitFormBoundaryLIr5t1rdtuD8Ztuw
Content-Disposition: form-data; name="username"

mayubao
------WebKitFormBoundaryLIr5t1rdtuD8Ztuw
Content-Disposition: form-data; name="username"

123456
------WebKitFormBoundaryLIr5t1rdtuD8Ztuw--

Http响应示例


HTTP/1.0 200 OK 
Cache-Control:public, max-age=86400
Content-Length:235
Content-Type:image/png
Date:Wed, 21 Dec 2016 08:20:54 GMT

请求实体

1.状态行(Http版本 + 状态 + 描述)


HTTP/1.0 200 OK 

2.响应头


HTTP/1.0 200 OK 
Cache-Control:public, max-age=86400
Content-Length:235
Content-Type:image/png
Date:Wed, 21 Dec 2016 08:20:54 GMT

3.空行

4.响应实体

上面只是简单的叙述了一下Http一般的请求-响应流程,还有对应请求,响应的结构。如果你想进一步了解http协议,请私下自行了解。

回到我们的重点 AndroidMicroServer
AndroidMicroServer是Http服务器的核心类,还有关联到其他的类,有IndexUriResHandler,ImageUriResHandler, DowloadUriResHandler。是AndroidMicroServer根据不同的Uri格式分配给指定的Handler去处理的。

UML的分析图如下:

AndroidMicroServer分析

下面是AndroidMicroServer的源码:


/**
 * The micro server in Android
 * Created by mayubao on 2016/12/14.
 * Contact me 345269374@qq.com
 */
public class AndroidMicroServer {

    private static final String TAG = AndroidMicroServer.class.getSimpleName();

    /**
     * the server port
     */
    private int mPort;

    /**
     * the server socket
     */
    private ServerSocket mServerSocket;

    /**
     *  the thread pool which handle the incoming request
     */
    private ExecutorService mThreadPool = Executors.newCachedThreadPool();

    /**
     * uri router handler
     */
    private List<ResUriHandler> mResUriHandlerList = new ArrayList<ResUriHandler>();

    /**
     * the flag which the micro server enable
     */
    private boolean mIsEnable = true;

    public AndroidMicroServer(int port){
        this.mPort = port;
    }

    /**
     * register the resource uri handler
     * @param resUriHandler
     */
    public void resgisterResUriHandler(ResUriHandler resUriHandler){
        this.mResUriHandlerList.add(resUriHandler);
    }

    /**
     * unresigter all the resource uri hanlders
     */
    public void unresgisterResUriHandlerList(){
        for(ResUriHandler resUriHandler : mResUriHandlerList){
            resUriHandler.destroy();
            resUriHandler = null;
        }
    }

    /**
     * start the android micro server
     */
    public void start(){
        mThreadPool.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    mServerSocket = new ServerSocket(mPort);

                    while(mIsEnable){
                        Socket socket = mServerSocket.accept();
                        hanlderSocketAsyn(socket);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * stop the android micro server
     */
    public void stop(){
        if(mIsEnable){
            mIsEnable = false;
        }

        //release resource
        unresgisterResUriHandlerList();

        if(mServerSocket != null){
            try {
//                mServerSocket.accept(); //fuck ! fix the problem, block the main thread
                mServerSocket.close();
                mServerSocket = null;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * handle the incoming socket
     * @param socket
     */
    private void hanlderSocketAsyn(final Socket socket) {
        mThreadPool.submit(new Runnable() {
            @Override
            public void run() {
                //1. auto create request object by the parameter socket
                Request request = createRequest(socket);

                //2. loop the mResUriHandlerList, and assign the task to the specify ResUriHandler
                for(ResUriHandler resUriHandler : mResUriHandlerList){
                    if(!resUriHandler.matches(request.getUri())){
                        continue;
                    }

                    resUriHandler.handler(request);
                }
            }
        });

    }

    /**
     * create the requset object by the specify socket
     *
     * @param socket
     * @return
     */
    private Request createRequest(Socket socket) {
        Request request = new Request();
        request.setUnderlySocket(socket);
        try {
            //Get the reqeust line
            SocketAddress socketAddress = socket.getRemoteSocketAddress();
            InputStream is = socket.getInputStream();
            String requestLine = IOStreamUtils.readLine(is);
            SLog.i(TAG, socketAddress + "requestLine------>>>" + requestLine);
            String requestType = requestLine.split(" ")[0];
            String requestUri = requestLine.split(" ")[1];

//            requestUri = URLDecoder.decode(requestUri, "UTF-8");

            request.setUri(requestUri);

            //Get the header line
            String header = "";
            while((header = IOStreamUtils.readLine(is)) != null){
                SLog.i(TAG, socketAddress + "header------>>>" + requestLine);
                String headerKey = header.split(":")[0];
                String headerVal = header.split(":")[1];
                request.addHeader(headerKey, headerVal);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return request;
    }

}

AndroidMicroServer主要有两个方法:

  1. start (Http服务器的开启)
  2. stop (Http服务器的关闭,主要用来关闭ServerSocket和反注册UriResHandler)

start方法 是Http服务器的入口

对于start方法:


    /**
     * start the android micro server
     */
    public void start(){
        mThreadPool.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    mServerSocket = new ServerSocket(mPort);

                    while(mIsEnable){
                        Socket socket = mServerSocket.accept();
                        hanlderSocketAsyn(socket);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }

开启一个线程去执行ServerSocket, while循环去接收每一个进来的Socket。 而hanlderSocketAsyn(socket)是异步处理每一个进来的socket。


    /**
     * handle the incoming socket
     * @param socket
     */
    private void hanlderSocketAsyn(final Socket socket) {
        mThreadPool.submit(new Runnable() {
            @Override
            public void run() {
                //1. auto create request object by the parameter socket
                Request request = createRequest(socket);

                //2. loop the mResUriHandlerList, and assign the task to the specify ResUriHandler
                for(ResUriHandler resUriHandler : mResUriHandlerList){
                    if(!resUriHandler.matches(request.getUri())){
                        continue;
                    }

                    resUriHandler.handler(request);
                }
            }
        });
    }

    /**
     * create the requset object by the specify socket
     *
     * @param socket
     * @return
     */
    private Request createRequest(Socket socket) {
        Request request = new Request();
        request.setUnderlySocket(socket);
        try {
            //Get the reqeust line
            SocketAddress socketAddress = socket.getRemoteSocketAddress();
            InputStream is = socket.getInputStream();
            String requestLine = IOStreamUtils.readLine(is);
            SLog.i(TAG, socketAddress + "requestLine------>>>" + requestLine);
            String requestType = requestLine.split(" ")[0];
            String requestUri = requestLine.split(" ")[1];

//            //解决URL中文乱码的问题
//            requestUri = URLDecoder.decode(requestUri, "UTF-8");

            request.setUri(requestUri);

            //Get the header line
            String header = "";
            while((header = IOStreamUtils.readLine(is)) != null){
                SLog.i(TAG, socketAddress + "header------>>>" + requestLine);
                String headerKey = header.split(":")[0];
                String headerVal = header.split(":")[1];
                request.addHeader(headerKey, headerVal);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return request;
    }

对于每一个进来的Socket:

  1. 通过createRequest(socket)来创建一个Request对象,对应一个Http Request对象。在createRequest(socket)中如何去从socket中去读取每一行呢?对于每一个Http请求的每一行都是以'\r\n'字节结尾的。只要判断读取字节流的时候判断连续的两个字节是以'\r\n'结尾的就是一行结尾的标识。详情请查看IOStreamUtils.java

  2. 根据请求行的path,分配给对应的Uri处理对象去处理,而所对应uri如何获取,是从Socket的Inputsream读取Http Request的请求行中读取出来的。对于ResUriHandler,是一个接口。主要根据请求行的uri 分配给对应的ResUriHandler去处理。 ResUriHandler的实现类是对应给出响应的处理类。

注意:可参考上面的UML的类图分析

ResUriHandler有三个实现类分别对应上面分析的三种Uri格式:

  1. IndexResUriHandler 处理发送文件列表的显示
  2. ImageResUriHandler 处理文件图片
  3. DownloadResUriHandler 处理文件下载

总结

AndroidMicroServer是架设在Android平台上面的一个微型HttpServer, 是根据快传项目的具体需求来实现的。巧妙的利用ResUriHandler来处理不同的uri。注意这不是一般通用的HttpServer, 之前有想过在Github上面去找一些Server端的代码来进行开发,发现代码关联太多,而且不容易定制,所以才会萌生自己用ServerSocket来实现符合自己需求的HttpServer。

对于HttpServer的核心代码都是在 io.github.mayubao.kuaichuan.micro_server包下面。
这是我在github上面的项目链接 https://github.com/mayubao/KuaiChuan

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,848评论 25 707
  • 能边哭边吃饭的人,是可以活下去的。 听朋友说,他们学校一个女孩去世了,自杀。 我深感惋惜,更多的却是困惑。 著名影...
    拾年流转阅读 505评论 0 2
  • 自去年起,我一直在问自己,我可以做什么?我更适合做什么?我的天赋是什么?我好想做我擅长的事情,并且把它做到极致.....
    潼潼物语阅读 343评论 1 1
  • 中午起来都十二点了,吃了中午饭,让下滩给别人拉牛粪呢,一车30块。这是把我们家的牛粪,装好给拉到别人地里面。感觉钱...
    暴走的老虎阅读 131评论 0 1