SpringBoot中Tomcat的源码分析

前言

Spring Boot 版本 2.1.7.RELEASE
本文大致跟踪了一遍Tomcat的启动流程和http请求处理流程。
请边debug源代码边看本文,因为很多变量细节在debug时才能清晰的观察到,而本文是无法全部涵盖所有细节的。
文中如有错误遗漏,请留言指正。

相关文章

SpringBoot启动流程的源码分析

正文

1 第一部分的调用栈如下

createWebServer:180, ServletWebServerApplicationContext (org.springframework.boot.web.servlet.context)
onRefresh:153, ServletWebServerApplicationContext (org.springframework.boot.web.servlet.context)
refresh:543, AbstractApplicationContext (org.springframework.context.support)
refresh:141, ServletWebServerApplicationContext (org.springframework.boot.web.servlet.context)
refresh:743, SpringApplication (org.springframework.boot)
refreshContext:390, SpringApplication (org.springframework.boot)
run:312, SpringApplication (org.springframework.boot)
run:1214, SpringApplication (org.springframework.boot)
run:1203, SpringApplication (org.springframework.boot)
main:13, XXXXXApplication (com.xxx.xxx)  //自定义的函数入口

这一部分主要工作是:

  • 实例化org.apache.catalina.startup.Tomcat
  • Tomcat实例作为构造函数的入参,实例化org.springframework.boot.web.embedded.tomcat.TomcatWebServer
  • TomcatWebServer的构造函数执行的末尾处,调用Tomcat.start()方法启动Tomcat

大致流程如下:
1.1
首先在org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer()方法中,
ServletWebServerFactory factory = getWebServerFactory();语句通过beanFactory构造出TomcatWebServer的factory实例。

1.2
然后this.webServer = factory.getWebServer(getSelfInitializer());语句通过TomcatWebServer的factory构造TomcatWebServer实例。Tomcat的实例化及启动流程就在此函数中完成。
代码实现在org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory类的getWebServer()方法中。

1.2.1
getWebServer()方法的前半部分都是在实例化Tomcat类。其中包括实例化Connector,StandardServer,StandardService,StandardEngine等类,最终组合成Tomcat类。

1.2.2
getWebServer()方法的最后一条语句getTomcatWebServer(tomcat);,将Tomcat实例作为构造函数的参数,构造了TomcatWebServer。而在TomcatWebServer的构造函数的末尾处,initialize();方法会启动Tomcat
initialize();方法主要做了两件事

  • this.tomcat.start(); 启动tomcat
  • startDaemonAwaitThread();
    启动一条非daemon线程来执行TomcatWebServer.this.tomcat.getServer().await();
    用途如源代码中的注释所写。
    // Unlike Jetty, all Tomcat threads are daemon threads. We create a
    // blocking non-daemon to stop immediate shutdown
    

先来看一下this.tomcat.start();
tomcat.start();内部会初始化并启动Server,Service,EngineConnectorHttp11NioProtocolNioEndpoint
这个过程中会多次发布Lifecycle中的事件,但默认只有StandardContextlifecycleListeners中有事件监听者。
StandardContext.startInternal()方法中会调用initializersonStartup()方法,其中包括了ServletWebServerApplicationContext.selfInitialize()方法。
1.2.3 selfInitialize()方法
1.2.3.1
selfInitialize()方法首先将ServletWebServerApplicationContext以attribute的方式注入到当前的ServletContext中。
ServletWebServerApplicationContext功能齐全,可以获取bean,可以获取environment,进而获取property。
所以一种通过静态方法获取ApplicationContext的方式是

RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
    ServletContext servletContext = servletRequestAttributes.getRequest().getServletContext();
    WebApplicationContext webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
}


有时候会需要在自定义的Filter中注入Spring维护的bean。如果是使用当前这种通过springboot的方式启动服务,直接通过注解注入是没有问题的。
但如果是通过tomcat方式启动服务,Filter初始化的时候是没有在Spring的上下文中的。这时候是无法通过注解注入Spring bean依赖的,可以尝试使用DelegatingFilterProxy这个类来实现。

1.2.3.2
接下来会调动ServletContextInitializerBeans类的构造函数。
在这个构造函数中加载了ServletFilter
加载的方式有两种
a) addServletContextInitializerBeans方法
从beanFactory中加载ServletContextInitializer接口的类。FilterRegistrationBean等类就实现了这一接口,这种filter的声明方式会在这个阶段被加载进来。
b) addAdaptableBeans()方法
从beanFactory中加载ServletFilter接口的类。
代码如下

addAsRegistrationBean(beanFactory, Servlet.class, new ServletRegistrationBeanAdapter(multipartConfig));
addAsRegistrationBean(beanFactory, Filter.class, new FilterRegistrationBeanAdapter());

代码中的adapter会将ServletFilter封装成RegistrationBean,这样就与a方式相同了。

2 第二部分的调用栈如下
这一部分主要工作是:调用TomcatWebServer.start()方法启动TomcatWebServer

start:195, TomcatWebServer (org.springframework.boot.web.embedded.tomcat)
startWebServer:297, ServletWebServerApplicationContext (org.springframework.boot.web.servlet.context)
finishRefresh:163, ServletWebServerApplicationContext (org.springframework.boot.web.servlet.context)
refresh:552, AbstractApplicationContext (org.springframework.context.support)
refresh:141, ServletWebServerApplicationContext (org.springframework.boot.web.servlet.context)
refresh:743, SpringApplication (org.springframework.boot)
refreshContext:390, SpringApplication (org.springframework.boot)
run:312, SpringApplication (org.springframework.boot)
run:1214, SpringApplication (org.springframework.boot)
run:1203, SpringApplication (org.springframework.boot)
main:13, XXXXXApplication (com.xxx.xxx)  //自定义的函数入口

大致流程如下:

2.1 addPreviouslyRemovedConnectors();
1.2.2TomcatWebServer.initialize()方法中有这样一段代码:

Context context = findContext();
context.addLifecycleListener((event) -> {
        if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
                // Remove service connectors so that protocol binding doesn't
                // happen when the service is started.
                removeServiceConnectors();
        }
});

这段代码的意思是在StandardContext中添加Lifecycle事件监听器,监听Lifecycle.START_EVENT事件。当接收到此事件时,执行removeServiceConnectors();,执行的目的如注释所示。
因此,本节的addPreviouslyRemovedConnectors();的目的就是把之前移除的connecters再添加回来。

2.1.1 addPreviouslyRemovedConnectors();方法中使用service.addConnector(connector);方法添加connector。
这一方法曾经在 1.2.1TomcatServletWebServerFactory.getWebServer()方法中被调用过一次,调用处的语句为tomcat.getService().addConnector(connector);
不过在TomcatServletWebServerFactory.getWebServer()方法中被调用时,由于getState().isAvailable() == false,没能继续执行后半部分,此时StandardService.state== LifecycleState.NEW;而在本节的addPreviouslyRemovedConnectors();方法中调用时,StandardService.state== LifecycleState.STARTED,可以继续执行connector.start();

2.1.2 connector.start();
方法中会连续调用,Connector.startInternal()Http11NioProtocol.start()NioEndpoint.start()。而主要工作在NioEndpoint.start()中执行,这里面包含了两个重要语句
bindWithCleanup();
startInternal();

2.1.2.1 NioEndpoint.bindWithCleanup()

  • 创建并配置ServerSocketChannel
  • Initialize SSL if needed
  • selectorPool.open(getName());
    • NioSelectorPool.sharedSelector = Selector.open();
    • NioSelectorPool.blockingSelector = new NioBlockingSelector();
    • NioSelectorPool.blockingSelector.poller = new BlockPoller();
    • 启动"BlockPoller"线程,执行
      org.apache.tomcat.util.net.NioBlockingSelector.BlockPoller.run()

2.1.2.2 NioEndpoint.startInternal()
创建executor
启动"ClientPoller"线程,执行org.apache.tomcat.util.net.NioEndpoint.Poller.run()
启动"Acceptor"线程,执行org.apache.tomcat.util.net.Acceptor.run()

2.1.2 续
至此,启动了一些线程总结如下

线程名 执行方法
http-nio-8080-Acceptor org.apache.tomcat.util.net.Acceptor.run()
http-nio-8080-BlockPoller org.apache.tomcat.util.net.NioBlockingSelector.BlockPoller.run()
http-nio-8080-ClientPoller org.apache.tomcat.util.net.NioEndpoint.Poller.run()
http-nio-8080-exec-1 org.apache.tomcat.util.threads.ThreadPoolExecutor (这是executor的worker线程,没有具体的执行任务)
http-nio-8080-exec-2 同上,共有10条线程

2.2 performDeferredLoadOnStartup();
这里会调用javax.servlet.GenericServlet.init()方法,对org.apache.catalina.servlets.DefaultServlet进行初始化。
org.springframework.web.servlet.DispatcherServlet是延迟初始化的,它会在将来有网络请求到来时才调用init()

3 tomcat处理网络请求
3.1 首先总结一下基本信息
3.1.1 线程信息如 2.1.2 续 所述
3.1.2 类信息摘要

class-dig.png

其中
NioEndpoint.poller != NioBlockingSelector.poller
NioSelectorPool.sharedSelector == NioBlockingSelector.sharedSelector == BlockPoller.selector (图中蓝色框的Selector为同一个对象)
NioEndpoint$Poller.selector != NioSelectorPool.sharedSelector (图中红色框的Selector与蓝色框的Selector不是同一个对象)

3.2 Accept线程
3.2.1
"Acceptor"线程中的org.apache.tomcat.util.net.Acceptor.run()方法在执行的时候,会阻塞在socket = endpoint.serverSocketAccept();语句处。
当接受到新的网络请求时,此语句解除阻塞,socket得到赋值,继续执行。

3.2.2
socket获得赋值后,会来到如下代码段

// setSocketOptions() will hand the socket off to
// an appropriate processor if successful
if (!endpoint.setSocketOptions(socket)) {
    endpoint.closeSocket(socket);
}

其中endpoint.setSocketOptions(socket)语句内内部,会将socket封装成NioChannel类,NioChannel类又被进一步封装成NioSocketWrapper类。
不过NioChannel类和NioSocketWrapper类二者内部都有着对方的引用。代码摘要如下

NioChannel channel = new NioChannel(socket, bufhandler);
NioSocketWrapper socketWrapper = new NioSocketWrapper(channel, this);
channel.setSocketWrapper(socketWrapper);

接下来org.apache.tomcat.util.net.NioEndpoint.Poller.register()方法将其注册到Poller中。
Poller.register()方法中又将NioChannel进一步封装成PollerEvent,然后添加到Poller.events队列中,队列的类型是SynchronizedQueue<PollerEvent> events

3.3 ClientPoller线程
此线程主体是一个while(true)轮询逻辑。
3.3.1
首先调用events();函数处理events队列中的PollerEvent,当 3.2.2 的末尾处有新PollerEvent加入队列后,events();方法内部会对其处理,过程如下
3.3.1.1
PollerEvent.run()方法内部,3.2.2 末尾处注册时的赋值,致使interestOps == OP_REGISTER,进而执行SocketChannel.register()方法,将这个socket注册到Poller.selector上,监听SelectionKey.OP_READ,attachment是NioSocketWrapper
3.3.1.2
PollerEvent.reset()方法重置PollerEvent对象。
eventCache.push(pe);将重置后的PollerEvent对象放入cache,方便以后重用。

3.3.2
events();调用完成后,会调用selector.selectedKeys().iterator()3.3.1.1 中注册到selector中的socket会被select出来。processKey(sk, socketWrapper);方法会对这个socket做进一步处理,其中socketWrapper是当时注册时的attachment。
processKey(sk, socketWrapper);方法内部会将socketWrapper封装成NioEndpoint.SocketProcessor类,交给NioEndpointEndpoint.executor去执行,也就是交给worker线程了。

3.4 worker线程
3.4.1
3.3.2 的末尾,worker线程接到新的任务后,执行的是SocketProcessor.doRun()方法。
其中关键语句是state = getHandler().process(socketWrapper, event);,即ConnectionHandler.process()方法,这个方法超长。
这个方法内部处理语句是state = processor.process(wrapper, status);,即Http11Processor.process()。然后是层层调用如下,直到执行filter。

doFilter:166, ApplicationFilterChain (org.apache.catalina.core) [1]
invoke:202, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:490, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:139, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:408, Http11Processor (org.apache.coyote.http11)
process:66, AbstractProcessorLight (org.apache.coyote)
process:853, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1587, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1130, ThreadPoolExecutor (java.util.concurrent)
run:630, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:832, Thread (java.lang)

org.apache.catalina.core.StandardWrapperValve.invoke()方法中,
servlet = wrapper.allocate();获取DispatcherServlet
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);获取filterChain。
org.apache.catalina.core.StandardContext.filterMaps中记录了所有filter。
filterChain.doFilter(request.getRequest(), response.getResponse());执行filter。

3.4.2 filter的执行
ApplicationFilterChain.doFilter() -> ApplicationFilterChain.internalDoFilter() -> if (pos < n) -> Filter.doFilter() -> ApplicationFilterChain.doFilter()
上述是链式的执行所有filter。
当filter全部执行完毕后,if (pos < n)将为false,继续执行ApplicationFilterChain.internalDoFilter()的后半部分。
最终执行到servlet.service(request, response);,servlet
DispatcherServlet

3.4.3 DispatcherServlet.doDispatch()
DispatcherServlet.getHandler()会根据请求url寻找对应controller。
AbstractHandlerMapping.getHandlerExecutionChain()会添加intercepter。
最终返回HandlerExecutionChain
HandlerExecutionChain的执行顺序如下:

  • mappedHandler.applyPreHandle()调用HandlerInterceptor.preHandle()
  • ha.handle()调用controller内的执行方法,这个方法是被@Aspect修饰过的。
  • mappedHandler.applyPostHandle()调用HandlerInterceptor.postHandle()
  • mappedHandler.triggerAfterCompletion()调用HandlerInterceptor.afterCompletion()

3.4.3 ha.handle()中会调用ServletInvocableHandlerMethod.invokeAndHandle()方法。
该方法中的Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);会最终调用controller的方法,并获得返回值。
然后this.returnValueHandlers.handleReturnValue( returnValue, getReturnValueType(returnValue), mavContainer, webRequest);会将controller的返回值转化成response body,并分片发送response。
分片发送依次调用
CoyoteOutputStream.flush()
org.apache.coyote.Response.action() actionCode == ActionCode.CLIENT_FLUSH
Http11Processor.action()
NioEndpoint.NioSocketWrapper.doWrite()
NioSelectorPool.write()
NioBlockingSelector.write()

然后再经过层层filter以及其他操作,最终回到了org.apache.catalina.connector.CoyoteAdapter.service()方法,该方法中会执行以下语句

request.finishRequest();
response.finishResponse();

org.apache.catalina.connector.Response.finishResponse()中会调用coyoteResponse.action(ActionCode.CLOSE, null); -> org.apache.coyote.http11.Http11Processor.finishResponse()发送结束分片。http response至此结束。

4 Tomcat 线程池
Tomcat基于jdk中提供的线程池,实现了自己的线程池。
4.1
首先梳理一下jdk中的线程池运行方式。
java.util.concurrent.ThreadPoolExecutor的构造方式中有3个重要参数 corePoolSize, maximumPoolSize, workQueue
我们以java.util.concurrent.LinkedBlockingQueue类作为workQueue举例说明。
此线程池构造完毕后,线程池内初始有 0 条线程。然后开始提交任务。
如果 当前线程数 < corePoolSize,建立一条新线程执行任务。
如果 当前线程数 == corePoolSize,将任务放入workQueue队列等待执行。
如果 当前线程数 == corePoolSizeworkQueue满了,建立一条新线程执行任务。
如果 workQueue满了 且 当前线程数 == maximumPoolSize,执行reject()方法,拒绝新任务,拒绝新任务的处理方式默认是抛出RejectedExecutionException异常。

4.2
Tomcat的线程池是在org.apache.tomcat.util.net.AbstractEndpoint.createExecutor()方法中执行创建的。
org.apache.tomcat.util.threads.ThreadPoolExecutor类继承了java.util.concurrent.ThreadPoolExecutor
Tomcat 使用的队列类为 org.apache.tomcat.util.threads.TaskQueue,该类继承了java.util.concurrent.LinkedBlockingQueue<Runnable>,且capacity == Integer.MAX_VALUE,即队列承载力极大。
他们组合使用构成的线程池的运行方式如下:
此线程池构造完毕后,线程池内初始有 corePoolSize 条线程。然后开始提交任务。
由于初始化时就建立了一些线程,所以最初提交的几个任务可以直接被执行。
如果 当前线程数 == corePoolSize 且 没有空闲线程,建立一条新线程执行任务。
如果 当前线程数 == maximumPoolSize,将任务放入workQueue队列等待执行。
如果 workQueue满了 且 当前线程数 == maximumPoolSize,执行reject()方法,拒绝新任务,拒绝新任务的处理方式默认是抛出RejectedExecutionException异常。

4.3
对比上述二者的执行方式,可知其间的差异。
jdk自带的线程池,初始化时不会启动新线程,当有任务提交时才会启动线程。当活跃线程达到corePoolSize时,会优先将任务放入队列等待,当等待队列满时,才会再次新增线程数量,直到达到maximumPoolSize
而Tomcat使用的线程池,初始化时就会启动corePoolSize条线程。如果线程都在忙于处理任务时,再提交新任务,会继续创建新线程,直到达到maximumPoolSize条线程。如果线程都在忙于处理任务,且线程数量达到了上限时,再提交新任务,才会将其放入等待队列。
可见,二者的显著区分处有三点。

  • jdk线程池初始化时不会启动任何线程,第一个任务提交时才会创建线程。
    而Tomcat线程池初始化就会启动corePoolSize条线程。
  • 在活跃线程达到corePoolSize,且所有线程都在忙碌中(即都在执行任务)时,再次提交新任务时,二者对新任务的处理方式的优先级不同。
    jdk线程池会优先将新任务放入任务队列中等待执行,当任务队列满了才会再次新建线程。
    而Tomcat线程池会优先创建线程执行任务,但线程数量达到上限且都在忙碌执行时,才会将任务放入等待队列中。
  • jdk线程池中的任务队列可自由使用任意类。
    而Tomcat线程池需要搭配使用TaskQueue类,该类的capacity == Integer.MAX_VALUE

所以Tomcat的线程池的策略是尽可能用线程立即执行到来的新任务,达到线程数量上限才会让任务排队等待。Tomcat的任务队列的承载能力又是极大的,capacity == Integer.MAX_VALUE

4.4
tomcat线程池创建的调用栈

<init>:77, ThreadPoolExecutor (org.apache.tomcat.util.threads)
createExecutor:987, AbstractEndpoint (org.apache.tomcat.util.net)
startInternal:331, NioEndpoint (org.apache.tomcat.util.net)
start:1293, AbstractEndpoint (org.apache.tomcat.util.net)
start:614, AbstractProtocol (org.apache.coyote)
startInternal:1072, Connector (org.apache.catalina.connector)
start:183, LifecycleBase (org.apache.catalina.util)
addConnector:239, StandardService (org.apache.catalina.core)
addPreviouslyRemovedConnectors:282, TomcatWebServer (org.springframework.boot.web.embedded.tomcat)
start:213, TomcatWebServer (org.springframework.boot.web.embedded.tomcat)
start:43, WebServerStartStopLifecycle (org.springframework.boot.web.servlet.context)
doStart:182, DefaultLifecycleProcessor (org.springframework.context.support)
access$200:53, DefaultLifecycleProcessor (org.springframework.context.support)
start:360, DefaultLifecycleProcessor$LifecycleGroup (org.springframework.context.support)
startBeans:158, DefaultLifecycleProcessor (org.springframework.context.support)
onRefresh:122, DefaultLifecycleProcessor (org.springframework.context.support)
finishRefresh:895, AbstractApplicationContext (org.springframework.context.support)
refresh:554, AbstractApplicationContext (org.springframework.context.support)
refresh:143, ServletWebServerApplicationContext (org.springframework.boot.web.servlet.context)
refresh:755, SpringApplication (org.springframework.boot)
refresh:747, SpringApplication (org.springframework.boot)
refreshContext:402, SpringApplication (org.springframework.boot)
run:312, SpringApplication (org.springframework.boot)
run:1247, SpringApplication (org.springframework.boot)
run:1236, SpringApplication (org.springframework.boot)
main:18, Application (com.jindi.search.platform)

5 其他
5.1 http status code
HttpServletResponse类中,定义了多个http状态码的常量,可以用来反向查找,查看在哪些情况下会返回哪种状态码。

推荐阅读

Tomcat剖析之架构篇(一)
Tomcat剖析之源码篇(二)

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