Tomcat的生命周期(二)

前言
本文是对Tomcat生命周期内容进行扩展和强化的第一篇文章,在上一篇文章中以StandarServer为例,从宏观上分析了容器的生命周期流转过程,分析了LifeEvent的设计思想,在此基础上,本文着重于分析StandardService下所有Container相关子容器的初始化和启动过程。
文中涉及的很多知识已在前面的文章中做了铺垫,重复的内容就不在累述。建议读者按着Tomcat系列文章的顺序阅读下来,否则可能会造成一定的理解困难,在遇到之前已经提及过的知识点时,本文都会以“在某某文章中说过某某”类似的语句进行提醒

Tomcat架构中各个组件和组件间关系(二)中曾经提过,Tomcat从整体架构上可以分为两大部分:监听请求并生成对应RequestResponseConnector连接器,以及处理请求和控制Tomcat容器运转的Container。再联系上篇生命周期文章中图9中对应的三大部分,我们以container.init()connector.init()两者作为切入点,开始对组件的初始化进行分析(第二部分executor.init(),因为默认是不配置连接池的,所以可以认为该部分无效),分析入口如下所示

图1. StandardService中两处分析入口

前文中提到过Container的顶层容器为StandardEngine,结合模板方法的设计可知,container.init()最终会调用StandardEngine.initInternal()
图2. StandardEngine的initInternal

其中getRealm()主要用于获取在server.xml上配置的<Realm>域对象,而域对象的作用之前也说过,主要用于安全性认证。除此之外就剩下简单调用父类的initInternal()。看到这里有些读者可能会产生疑惑,之前说过容器间的初始化是“父传子”,“子传孙”的责任链模式,怎么刚到StandardEngine就断了呢?其实可以这么理解,责任链开始必定是由外到内的过程,当最内层执行完一定返回上一层,也就是再经历由内到外的逆向过程,我们来看看在逆向的过程中发生了什么(暂且忽略StandardServiceinitInternal()的剩下部分)
图3. LifecycleBase中init()

初始化方法的最初入口在Catalina类中的load()load()会调用getServer().init(),最终对应LifecycleBaseinit(),如上图所示,这里的initInternal()就是责任链的入口,当返回时会设置初始化结束生命周期状态LifecycleState.INITIALIZED,对应的生命周期事件为Lifecycle.AFTER_INIT_EVENT
图4. LifecycleBase的setStateInternal

在设置生命周期状态的同时会发布对应的生命周期状态给对该事件“感兴趣”的监听器,我们看看哪些监听器会对这里的Lifecycle.AFTER_INIT_EVENT做出响应。在Tomcat架构中各个组件及组件间关系(二)中的图20,有关ContextConfig有关介绍时提过,该监听器会对初始化结束事件作出相应,主要工作为初始化解析两种web.xml文件的解析器webDigesterwebFragmentDigester,为接下来的启动事件解析web.xml做准备。至此Container容器的初始化工作结束,回到图1中开始分析Connectorinit()方法
图5. Connector的initInternal()

Connector的初始化过程同样遵循之前所说的“模板方法”设计模式,最终会走到上图中Connector自身实现的initInternal(),方法中CoyoteAdapter可以理解为ConnectorContainer之间的桥梁,也就是说将Connector中接收的请求交给Container一系列容器处理的流程就是该类负责的,这里将当前的Connector实例通过构造器传递给了CoyoteAdapter,又因为ConnectorStandardService存在双向关联关系,那么我们就可以在CoyoteAdapter中得到Connector对应的StandardService,进而得到StandardService下的StandardEngine,并将request交于一系列容器进行处理,具体的代码下文讲对应流程时会看到,由于在整个初始化和启动流程中类与类之间的关系比较复杂,因此,我按照分析的关键功能画了一张大致的类图,有助于下面的理解和分析
图6. 关键流程涉及类UML图

图5中代码将创建好的adapterprotocolHandler进行了关联,在前文中分析过,默认情况protocolHandler就是Http11Protocol的实例,在Tomcat架构中各个组件及组件间关系(二)中分析过,该实例是在Digester解析ConnectorCreateRule时创建Connector对象的同时创建的

图7.Http11Protocol的构造器

Http11Protocol代表了对HTTP1.1协议进行处理的类,初始化时又创建了JIoEndpointHttp11ConnectionHandler的实例,前者用于处理端到端的socket io请求,根据I/O方式的不同又可分为AprEndpointJIoEndpointNioEndpoint;后者主要用于创建对应协议请求的处理器。((JIoEndpoint) endpoint).setHandler(cHandler)建立了两者之间的关系,最后三行代码分别设置了关闭Socket延迟开关、Socket连接超时时间和开启tcpNoDelay选项
回到图5对协议处理类protocolHandler进行初始化,底层调用了所有协议处理类的父类AbstractHandlerinit()
图8. AbstractProtocol的init方法

对于Http的bio请求方式来说,这里endpointNamehttp-bio-8080,协议处理类的初始化主要对相应的endpoint进行初始化
图9. AbstractEndpoint的init()

又来一个模板方法,在父类AbstractEndpoint中抽象了bind(),交由不同类型的端到端类进行实现,本文中必然就是对应JIoEndpointbind(),对应代码清单1

    @Override
    public void bind() throws Exception {

        // Initialize thread count defaults for acceptor
        //      (1)
        if (acceptorThreadCount == 0) {
            acceptorThreadCount = 1;
        }
        // Initialize maxConnections
        //      (2)
        if (getMaxConnections() == 0) {
            // User hasn't set a value - use the default
            setMaxConnections(getMaxThreadsInternal());
        }
        //      (3)
        if (serverSocketFactory == null) {
            if (isSSLEnabled()) {
                serverSocketFactory =
                    handler.getSslImplementation().getServerSocketFactory(this);
            } else {
                serverSocketFactory = new DefaultServerSocketFactory(this);
            }
        }
        //      (4)
        if (serverSocket == null) {
            try {
                if (getAddress() == null) {
                    serverSocket = serverSocketFactory.createSocket(getPort(),
                            getBacklog());
                } else {
                    serverSocket = serverSocketFactory.createSocket(getPort(),
                            getBacklog(), getAddress());
                }
            } catch (BindException orig) {
                String msg;
                if (getAddress() == null)
                    msg = orig.getMessage() + " <null>:" + getPort();
                else
                    msg = orig.getMessage() + " " +
                            getAddress().toString() + ":" + getPort();
                BindException be = new BindException(msg);
                be.initCause(orig);
                throw be;
            }
        }

    }

标注1处涉及一个成员变量acceptorThreadCount,该变量在AbstractEndpoint中,表示等待Socket连接的Acceptor线程个数。Acceptor是一个在AbstractEndpoint中定义的抽象内部类,该类实现了Runnable接口

图10. AbstractEndpoint中的抽象静态内部类Acceptor

该类中仅仅定义了Acceptor的几种状态,并没有实现run(),那必然就是在其子类中实现了,为了流程分析的整体性,我们暂且跳过JIoEndpointAcceptor的具体实现,因为只有在启动时,该线程才会执行,待讲解到启动流程再做分析,我们接着代码清单1往下看
标注2处设置了每个Endpoint允许的最大连接数,需要注意的是,这里不要和Acceptor线程的连接数acceptorThreadCount混淆,当getMaxConnections()返回0时,socket允许最大连接数就由getMaxThreadsInternal()指定
图11. AbstractEndpoint类中getMaxThreadsInternal()

在初始化JIoEndpoint时,已经通过setMaxConnections(int maxCon)将父类maxConnections置为0,那么最大连接数即为AbstractEndpoint类中的成员变量maxThreads = 200
标注3创建了ServerSocket工厂,根据是否在server.xml中开启SSL安全协议serverSocketFactory共有两种创建方式,默认不开启,对应DefaultServerSocket。标注4根据这里的serverSocketFactory创建对应的serverSocket,到这里大家肯定都非常熟悉了,服务端套接字嘛。至此,所有容器和连接器的初始化工作结束,下面我们来看容器的启动过程
Tomcat的生命周期一文中,我们分析到StandardServer的启动,StandardServer启动会调动子容器StandardServicestartInternal()
图12. StandardService的startInternal

同的初始化流程一样,StandardService的启动流程也分为三部分,我们重点依然是Container容器和ConnectorContainer.start()对应StandardEngine.startInternal()
图13. StandardEngine的startInternal

StandardServerStandardService启动不同的是Container的子容器除了StandardContext外都会调用公共父类ContainerBasestartInternal
图14. ContainerBase的startInternal

findChildren()拿到成员变量HashMap<String, Container> children的所有value,该集合是在Digester解析容器对应规则的时候通过addChild(Container)放入值的,比如对于这里的StandardEngine来说,StandardHost是他的childrenStartChild实现了Callable接口,其call()调用了child.start(),对应StandardHoststartInternal()

图15. StandardHost的startInternal

方法中主要做了两件事:1. 给StandardHostStandardPipeline又添加了一个阀门ErrorReportValve;2.继续调用ContainerBasestartInternal。但需要注意的是对于StandardHost来说children肯定是StandardContext,此时通过findChildren()得到的StandardContext实际上是通过解析server.xml中的<Context>转变而来,该StandardContext和通常意义上的webapps/xxx.war并没有对应关系,况且很多时候我们并不会在server.xml中添加<Context>标签,而对于真正的StandardContext的解析并不是通过StandardHost.findChildren()得到。这时候我们需要看图14的第二个红框处,setState(LifecycleState.STARTING)StandardHost的生命周期状态设为STARTING(注意此时流程已经进入StandardHost,虽然图14对应的是StandardEngine,但是两者都会调用ContainerBase.startInternal(),代码是一样的,下面的说明也需要注意这一点),并发送对应的事件START_EVENT给对应的监听器HostConfig
图16. HostConfig的start()

重点在于最后一个判断,默认情况下host.getDeployOnStartup()返回成员变量deployOnStartup为true,表示一启动就加载web应用
图17. deployApps()

共有web应用部署方式:1.XML描述符;2.WAR;3.扩展文件夹,我们选择WAR方式进行分析。appBase()得到${catalina.base}/webapps下对应的文件,filterAppPaths(appBase.list())得到/webapps下所有的war文件,并滤除一些排除项,代码清单2 展示了deployWARs的具体逻辑

protected void deployWARs(File appBase, String[] files) {

    if (files == null)
        return;

    ExecutorService es = host.getStartStopExecutor();
    List<Future<?>> results = new ArrayList<Future<?>>();

    for (int i = 0; i < files.length; i++) {

        if (files[i].equalsIgnoreCase("META-INF"))
            continue;
        if (files[i].equalsIgnoreCase("WEB-INF"))
            continue;
        File war = new File(appBase, files[i]);
        if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".war") &&
                war.isFile() && !invalidWars.contains(files[i]) ) {

            ContextName cn = new ContextName(files[i], true);

            if (isServiced(cn.getName())) {
                continue;
            }
            if (deploymentExists(cn.getName())) {
                DeployedApplication app = deployed.get(cn.getName());
                boolean unpackWAR = unpackWARs;
                if (unpackWAR && host.findChild(cn.getName()) instanceof StandardContext) {
                    unpackWAR = ((StandardContext) host.findChild(cn.getName())).getUnpackWAR();
                }
                if (!unpackWAR && app != null) {
                    // Need to check for a directory that should not be
                    // there
                    File dir = new File(appBase, cn.getBaseName());
                    if (dir.exists()) {
                        if (!app.loggedDirWarning) {
                            log.warn(sm.getString(
                                    "hostConfig.deployWar.hiddenDir",
                                    dir.getAbsoluteFile(),
                                    war.getAbsoluteFile()));
                            app.loggedDirWarning = true;
                        }
                    } else {
                        app.loggedDirWarning = false;
                    }
                }
                continue;
            }

            // Check for WARs with /../ /./ or similar sequences in the name
            if (!validateContextPath(appBase, cn.getBaseName())) {
                log.error(sm.getString(
                        "hostConfig.illegalWarName", files[i]));
                invalidWars.add(files[i]);
                continue;
            }

            results.add(es.submit(new DeployWar(this, cn, war)));
        }
    }

    for (Future<?> result : results) {
        try {
            result.get();
        } catch (Exception e) {
            log.error(sm.getString(
                    "hostConfig.deployWar.threaded.error"), e);
        }
    }
}

遍历每一个war包,对文件名称进行校验和特殊字符的处理,判断文件名对应的war包是否已经运行是否已经部署成功,最后将文件及其对应信息包装成DeployWar放入线程池中进行部署,由于DeployWar实现了Runable,所以这里的es.submit使用的是ExectuorService.submit(Runnable)这个重载方法,得到的Future中并没有返回值,下面result.get()的目的只是为了阻塞让所有的DeployWar任务执行完毕,我们来看DeployWar做了什么

图18. DeployWar

DeployWar中只是将当前需要加载的ContextName和对应的war文件传递给deployWAR(String, File),如代码清单3

    protected void deployWAR(ContextName cn, File war) {

        // Checking for a nested /META-INF/context.xml
        JarFile jar = null;
        InputStream istream = null;
        FileOutputStream fos = null;
        BufferedOutputStream ostream = null;

        File xml = new File(appBase(),
                cn.getBaseName() + "/META-INF/context.xml");

        boolean xmlInWar = false;
        try {
            jar = new JarFile(war);
            JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml);
            if (entry != null) {
                xmlInWar = true;
            }
        } catch (IOException e) {
            /* Ignore */
        } finally {
            if (jar != null) {
                try {
                    jar.close();
                } catch (IOException ioe) {
                    // Ignore;
                }
                jar = null;
            }
        }

        //
        Context context = null;
        try {
            if (deployXML && xml.exists() && unpackWARs && !copyXML) {
                synchronized (digesterLock) {
                    try {
                        context = (Context) digester.parse(xml);
                    } catch (Exception e) {
                        log.error(sm.getString(
                                "hostConfig.deployDescriptor.error",
                                war.getAbsolutePath()), e);
                    } finally {
                        digester.reset();
                        if (context == null) {
                            context = new FailedContext();
                        }
                    }
                }
                context.setConfigFile(xml.toURI().toURL());
            } else if (deployXML && xmlInWar) {
                synchronized (digesterLock) {
                    try {
                        jar = new JarFile(war);
                        JarEntry entry =
                            jar.getJarEntry(Constants.ApplicationContextXml);
                        istream = jar.getInputStream(entry);
                        context = (Context) digester.parse(istream);
                    } catch (Exception e) {
                        log.error(sm.getString(
                                "hostConfig.deployDescriptor.error",
                                war.getAbsolutePath()), e);
                    } finally {
                        digester.reset();
                        if (istream != null) {
                            try {
                                istream.close();
                            } catch (IOException e) {
                                /* Ignore */
                            }
                            istream = null;
                        }
                        if (jar != null) {
                            try {
                                jar.close();
                            } catch (IOException e) {
                                /* Ignore */
                            }
                            jar = null;
                        }
                        if (context == null) {
                            context = new FailedContext();
                        }
                        context.setConfigFile(
                                UriUtil.buildJarUrl(war, Constants.ApplicationContextXml));
                    }
                }
            } else if (!deployXML && xmlInWar) {
                // Block deployment as META-INF/context.xml may contain security
                // configuration necessary for a secure deployment.
                log.error(sm.getString("hostConfig.deployDescriptor.blocked",
                        cn.getPath(), Constants.ApplicationContextXml,
                        new File(configBase(), cn.getBaseName() + ".xml")));
            } else {
                context = (Context) Class.forName(contextClass).newInstance();
            }
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString("hostConfig.deployWar.error",
                    war.getAbsolutePath()), t);
        } finally {
            if (context == null) {
                context = new FailedContext();
            }
        }

        boolean copyThisXml = false;
        if (deployXML) {
            if (host instanceof StandardHost) {
                copyThisXml = ((StandardHost) host).isCopyXML();
            }

            // If Host is using default value Context can override it.
            if (!copyThisXml && context instanceof StandardContext) {
                copyThisXml = ((StandardContext) context).getCopyXML();
            }

            if (xmlInWar && copyThisXml) {
                // Change location of XML file to config base
                xml = new File(configBase(), cn.getBaseName() + ".xml");
                try {
                    jar = new JarFile(war);
                    JarEntry entry =
                        jar.getJarEntry(Constants.ApplicationContextXml);
                    istream = jar.getInputStream(entry);

                    fos = new FileOutputStream(xml);
                    ostream = new BufferedOutputStream(fos, 1024);
                    byte buffer[] = new byte[1024];
                    while (true) {
                        int n = istream.read(buffer);
                        if (n < 0) {
                            break;
                        }
                        ostream.write(buffer, 0, n);
                    }
                    ostream.flush();
                } catch (IOException e) {
                    /* Ignore */
                } finally {
                    if (ostream != null) {
                        try {
                            ostream.close();
                        } catch (IOException ioe) {
                            // Ignore
                        }
                        ostream = null;
                    }
                    if (fos != null) {
                        try {
                            fos.close();
                        } catch (IOException ioe) {
                            // Ignore
                        }
                        fos = null;
                    }
                    if (istream != null) {
                        try {
                            istream.close();
                        } catch (IOException ioe) {
                            // Ignore
                        }
                        istream = null;
                    }
                    if (jar != null) {
                        try {
                            jar.close();
                        } catch (IOException ioe) {
                            // Ignore;
                        }
                        jar = null;
                    }
                }
            }
        }

        DeployedApplication deployedApp = new DeployedApplication(cn.getName(),
                xml.exists() && deployXML && copyThisXml);

        long startTime = 0;
        // Deploy the application in this WAR file
        if(log.isInfoEnabled()) {
            startTime = System.currentTimeMillis();
            log.info(sm.getString("hostConfig.deployWar",
                    war.getAbsolutePath()));
        }

        try {
            // Populate redeploy resources with the WAR file
            deployedApp.redeployResources.put
                (war.getAbsolutePath(), Long.valueOf(war.lastModified()));

            if (deployXML && xml.exists() && copyThisXml) {
                deployedApp.redeployResources.put(xml.getAbsolutePath(),
                        Long.valueOf(xml.lastModified()));
            } else {
                // In case an XML file is added to the config base later
                deployedApp.redeployResources.put(
                        (new File(configBase(),
                                cn.getBaseName() + ".xml")).getAbsolutePath(),
                        Long.valueOf(0));
            }
            //        (1)
            Class<?> clazz = Class.forName(host.getConfigClass());
            LifecycleListener listener =
                (LifecycleListener) clazz.newInstance();
            context.addLifecycleListener(listener);

            context.setName(cn.getName());
            context.setPath(cn.getPath());
            context.setWebappVersion(cn.getVersion());
            context.setDocBase(cn.getBaseName() + ".war");

            host.addChild(context);
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString("hostConfig.deployWar.error",
                    war.getAbsolutePath()), t);
        } finally {
            // If we're unpacking WARs, the docBase will be mutated after
            // starting the context
            boolean unpackWAR = unpackWARs;
            if (unpackWAR && context instanceof StandardContext) {
                unpackWAR = ((StandardContext) context).getUnpackWAR();
            }
            if (unpackWAR && context.getDocBase() != null) {
                File docBase = new File(appBase(), cn.getBaseName());
                deployedApp.redeployResources.put(docBase.getAbsolutePath(),
                        Long.valueOf(docBase.lastModified()));
                addWatchedResources(deployedApp, docBase.getAbsolutePath(),
                        context);
                if (deployXML && !copyThisXml && (xmlInWar || xml.exists())) {
                    deployedApp.redeployResources.put(xml.getAbsolutePath(),
                            Long.valueOf(xml.lastModified()));
                }
            } else {
                // Passing null for docBase means that no resources will be
                // watched. This will be logged at debug level.
                addWatchedResources(deployedApp, null, context);
            }
            // Add the global redeploy resources (which are never deleted) at
            // the end so they don't interfere with the deletion process
            addGlobalRedeployResources(deployedApp);
        }

        deployed.put(cn.getName(), deployedApp);

        if (log.isInfoEnabled()) {
            log.info(sm.getString("hostConfig.deployWar.finished",
                war.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime)));
        }
    }

代码逻辑比较长,我们挑重点的讲。标注1为每一个StandardContext创建一个监听器ContextConfig,该监听器的值是写死的,并通过host.getConfigClass()获得,之后为StandardContext设置名称、路劲、版本等信息,最后调用host.addChild(Container)建立StandardHost与所有StandardContext的关联。流程又走入生命周期的一个大循环内,调用LifecycleBase.start(),由于此时StandardContext刚刚创建出来,其生命周期状态为NEW,并不会进入启动流程而是先进行init()

图19. 当前对象为StandardContext流程debug截图

由于在StandardHost.initInternal()没做什么关键操作,这里就不做分析了,之后在LifecycleBase中会向ContextConfig发送AFTER_INIT_EVENT事件,此时ContextConfig会对该事件做出响应,调用init()进行web.xml文件的解析规则设置,具体的分析过程已经在Tomcat架构中各个组件及组件间关系(二)讲过
图14中的最后一句threadStart()用于启动ContainerBase中的ContainerBackgroundProcessor线程,同样在Tomcat架构中各个组件及组件间关系(二)提过,该线程的启动有一个先决条件,就是backgroundProcessorDelay > 0,而Container子容器中只有StandardEngine对该值进行了覆盖,满足大于0的条件,因此可以说该线程启动入口只在StandardEngine启动时调用父类的startInternal()中。线程中会调用processChildren()
图20. ContainerBackgroundProcessor中processChildren()

从图中可以看出虽然线程的启动入口只在StandardEngine启动时,但代码采用了递归开启的方式,使得StandardEngine下所有的子容器都能执行backgroundProcess()的具体实现,如代码清单4

    @Override
    public void backgroundProcess() {
        
        if (!getState().isAvailable())
            return;

        if (cluster != null) {
            try {
                cluster.backgroundProcess();
            } catch (Exception e) {
                log.warn(sm.getString("containerBase.backgroundProcess.cluster", cluster), e);                
            }
        }
        if (loader != null) {
            try {
                loader.backgroundProcess();
            } catch (Exception e) {
                log.warn(sm.getString("containerBase.backgroundProcess.loader", loader), e);                
            }
        }
        if (manager != null) {
            try {
                manager.backgroundProcess();
            } catch (Exception e) {
                log.warn(sm.getString("containerBase.backgroundProcess.manager", manager), e);                
            }
        }
        Realm realm = getRealmInternal();
        if (realm != null) {
            try {
                realm.backgroundProcess();
            } catch (Exception e) {
                log.warn(sm.getString("containerBase.backgroundProcess.realm", realm), e);                
            }
        }
        Valve current = pipeline.getFirst();
        while (current != null) {
            try {
                current.backgroundProcess();
            } catch (Exception e) {
                log.warn(sm.getString("containerBase.backgroundProcess.valve", current), e);                
            }
            current = current.getNext();
        }
        fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null);
    }

clusterrealm调用backgroundProcess()非本文重点,这里不做分析。我们来看看loader.backgroundProcess(),在违反ClassLoader双亲委派机制三部曲第二部——Tomcat类加载机制中曾今阐述过war包加载需要依赖WebappLoader,而StandardContext又是个war一一对应的,那很明显这里就是调用WebappLoaderbackgroundProcess()

图21. WebappLoader的backgroundProcess

reloadable<Context>的一个属性,用于标明是否运行war在文件改动后自动重新加载,modified()用于检测war中是否有class或者resource文件存在改动,如果两者都为true则会调用StandardContext.reload(),该方法的逻辑其实也非常简单,就是先pause容器,在stop容器,最后start。我们回到代码清单4中最后一行,代码向Container的所有子容器发送了PERIODIC_EVENT,其中HostConfig会对该事件做出响应,调用该类的check()
图22. HostConfig的check()

图中的代码其实很大一部分就是上面说如果发布war包流程的重复,if中判断<Host>autoDeploy自动部署属性是否打开,再从已部署应用的Map<String, DeployedApplication> deployed集合中得到所有war,进行部署前的校验,最后再次调用deployApps()对三种形式的文件进行发布
至此,Container下所有容器的初始化和启动流程基本分析完毕,Connector分析了初始化流程,还顺带解释了Tomcat自动加载的原理,由于篇幅不宜过长,因此将Connector的启动过程放在下一篇文章中再做分析

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

推荐阅读更多精彩内容