在IBM Bluemix 云平台上开发并部署您的下一个应用。
现在就开始免费试用
本文以Tomcat 5为基础,也兼顾最新的Tomcat 6和Tomcat 4。Tomcat的基本设计思路和架构是具有一定连续性的。
Tomcat 总体结构
Tomcat的结构很复杂,但是Tomcat也非常的模块化,找到了Tomcat最核心的模块,您就抓住了Tomcat的“七寸”。下面是Tomcat的总体结构图:
图1.Tomcat 的总体结构
从上图中可以看出Tomcat的心脏是两个组件:Connector和Container,关于这两个组件将在后面详细介绍。Connector组件是可以被替换,这样可以提供给服务器设计者更多的选择,因为这个组件是如此重要,不仅跟服务器的设计的本身,而且和不同的应用场景也十分相关,所以一个Container可以选择对应多个Connector。多个Connector和一个Container就形成了一个Service,Service的概念大家都很熟悉了,有了Service就可以对外提供服务了,但是Service还要一个生存的环境,必须要有人能够给她生命、掌握其生死大权,那就非Server莫属了。所以整个Tomcat的生命周期由Server控制。
以Service 作为“婚姻”
我们将Tomcat中Connector、Container作为一个整体比作一对情侣的话,Connector主要负责对外交流,可以比作为Boy,Container主要处理Connector接受的请求,主要是处理内部事务,可以比作为Girl。那么这个Service就是连接这对男女的结婚证了。是Service将它们连接在一起,共同组成一个家庭。当然要组成一个家庭还要很多其它的元素。
说白了,Service只是在Connector和Container外面多包一层,把它们组装在一起,向外面提供服务,一个Service可以设置多个Connector,但是只能有一个Container容器。这个Service接口的方法列表如下:
图2. Service 接口
从Service接口中定义的方法中可以看出,它主要是为了关联Connector和Container,同时会初始化它下面的其它组件,注意接口中它并没有规定一定要控制它下面的组件的生命周期。所有组件的生命周期在一个Lifecycle的接口中控制,这里用到了一个重要的设计模式,关于这个接口将在后面介绍。
Tomcat中Service接口的标准实现类是StandardService它不仅实现了Service借口同时还实现了Lifecycle接口,这样它就可以控制它下面的组件的生命周期了。StandardService类 结构图如下:
图3. StandardService 的类结构图
从上图中可以看出除了Service 接口的方法的实现以及控制组件生命周期的Lifecycle 接口的实现,还有几个方法是用于在事件监听的方法的实现,不仅是这个Service 组件,Tomcat 中其它组件也同样有这几个方法,这也是一个典型的设计模式,将在后面介绍。
下面看一下StandardService 中主要的几个方法实现的代码,下面是setContainer 和addConnector 方法的源码:
清单1. StandardService. SetContainer
public void setContainer(Container container) { Container oldContainer = this.container; if ((oldContainer != null) && (oldContainer instanceof Engine)) ((Engine) oldContainer).setService(null); this.container = container; if ((this.container != null) && (this.container instanceof Engine)) ((Engine) this.container).setService(this); if (started && (this.container != null) && (this.container instanceof Lifecycle)) { try { ((Lifecycle) this.container).start(); } catch (LifecycleException e) { ; } } synchronized (connectors) { for (int i = 0; i < connectors.length; i++) connectors[i].setContainer(this.container); } if (started && (oldContainer != null) && (oldContainer instanceof Lifecycle)) { try { ((Lifecycle) oldContainer).stop(); } catch (LifecycleException e) { ; } } support.firePropertyChange("container", oldContainer, this.container); }
这段代码很简单,其实就是先判断当前的这个Service有没有已经关联了Container,如果已经关联了,那么去掉这个关联关系—— oldContainer.setService(null)。如果这个oldContainer已经被启动了,结束它的生命周期。然后再替换新的关联、再初始化并开始这个新的Container的生命周期。最后将这个过程通知感兴趣的事件监听程序。这里值得注意的地方就是,修改Container时要将新的Container关联到每个Connector,还好Container和Connector没有双向关联,不然这个关联关系将会很难维护。
清单2. StandardService. addConnector
public void addConnector(Connector connector) { synchronized (connectors) { connector.setContainer(this.container); connector.setService(this); Connector results[] = new Connector[connectors.length + 1]; System.arraycopy(connectors, 0, results, 0, connectors.length); results[connectors.length] = connector; connectors = results; if (initialized) { try { connector.initialize(); } catch (LifecycleException e) { e.printStackTrace(System.err); } } if (started && (connector instanceof Lifecycle)) { try { ((Lifecycle) connector).start(); } catch (LifecycleException e) { ; } } support.firePropertyChange("connector", null, connector); } }
上面是addConnector方法,这个方法也很简单,首先是设置关联关系,然后是初始化工作,开始新的生命周期。这里值得一提的是,注意Connector用的是数组而不是List集合,这个从性能角度考虑可以理解,有趣的是这里用了数组但是并没有向我们平常那样,一开始就分配一个固定大小的数组,它这里的实现机制是:重新创建一个当前大小的数组对象,然后将原来的数组对象copy到新的数组中,这种方式实现了类似的动态数组的功能,这种实现方式,值得我们以后拿来借鉴。
最新的Tomcat6 中StandardService 也基本没有变化,但是从Tomcat5 开始Service、Server 和容器类都继承了MBeanRegistration 接口,Mbeans 的管理更加合理。
以Server 为“居”
前面说一对情侣因为Service 而成为一对夫妻,有了能够组成一个家庭的基本条件,但是它们还要有个实体的家,这是它们在社会上生存之本,有了家它们就可以安心的为人民服务了,一起为社会创造财富。
Server要完成的任务很简单,就是要能够提供一个接口让其它程序能够访问到这个Service集合、同时要维护它所包含的所有Service的生命周期,包括如何初始化、如何结束服务、如何找到别人要访问的Service。还有其它的一些次要的任务,如您住在这个地方要向当地政府去登记啊、可能还有要配合当地公安机关日常的安全检查什么的。
Server 的类结构图如下:
图4. Server 的类结构图
它的标准实现类StandardServer 实现了上面这些方法,同时也实现了Lifecycle、MbeanRegistration 两个接口的所有方法,下面主要看一下StandardServer 重要的一个方法addService 的实现:
清单3. StandardServer.addService
public void addService(Service service) { service.setServer(this); synchronized (services) { Service results[] = new Service[services.length + 1]; System.arraycopy(services, 0, results, 0, services.length); results[services.length] = service; services = results; if (initialized) { try { service.initialize(); } catch (LifecycleException e) { e.printStackTrace(System.err); } } if (started && (service instanceof Lifecycle)) { try { ((Lifecycle) service).start(); } catch (LifecycleException e) { ; } } support.firePropertyChange("service", null, service); } }
从上面第一句就知道了Service和Server是相互关联的,Server也是和Service管理Connector一样管理它,也是将Service放在一个数组中,后面部分的代码也是管理这个新加进来的Service的生命周期。Tomcat6中也是没有什么变化的。
组件的生命线“Lifecycle”
前面一直在说Service 和Server 管理它下面组件的生命周期,那它们是如何管理的呢?
Tomcat 中组件的生命周期是通过Lifecycle 接口来控制的,组件只要继承这个接口并实现其中的方法就可以统一被拥有它的组件控制了,这样一层一层的直到一个最高级的组件就可以控制Tomcat 中所有组件的生命周期,这个最高的组件就是Server,而控制Server 的是Startup,也就是您启动和关闭Tomcat。
下面是Lifecycle 接口的类结构图:
图5. Lifecycle 类结构图
除了 控制生命周期的Start和Stop方法外还有一个监听机制,在生命周期开始和结束的时候做一些额外的操作。这个机制在其它的框架中也被使用,如在Spring中。关于这个设计模式会在后面介绍。
Lifecycle接口的方法的实现都在其它组件中,就像前面中说的,组件的生命周期由包含它的父组件控制,所以它的Start方法自然就是调用它下面的组件的Start方法,Stop方法也是一样。如在Server中Start方法就会调用Service组件的Start方法,Server的Start方法代码如下:
清单4. StandardServer.Start
public void start() throws LifecycleException { if (started) { log.debug(sm.getString("standardServer.start.started")); return; } lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null); lifecycle.fireLifecycleEvent(START_EVENT, null); started = true; synchronized (services) { for (int i = 0; i < services.length; i++) { if (services[i] instanceof Lifecycle) ((Lifecycle) services[i]).start(); } } lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null); }
监听的代码会包围Service 组件的启动过程,就是简单的循环启动所有Service 组件的Start 方法,但是所有Service 必须要实现Lifecycle 接口,这样做会更加灵活。
Server 的Stop 方法代码如下:
清单5. StandardServer.Stop
public void stop() throws LifecycleException { if (!started) return; lifecycle.fireLifecycleEvent(BEFORE_STOP_EVENT, null); lifecycle.fireLifecycleEvent(STOP_EVENT, null); started = false; for (int i = 0; i < services.length; i++) { if (services[i] instanceof Lifecycle) ((Lifecycle) services[i]).stop(); } lifecycle.fireLifecycleEvent(AFTER_STOP_EVENT, null); }
它所要做的事情也和Start 方法差不多。
回页首
Connector 组件
Connector 组件是Tomcat 中两个核心组件之一,它的主要任务是负责接收浏览器的发过来的tcp 连接请求,创建一个Request 和Response 对象分别用于和请求端交换数据,然后会产生一个线程来处理这个请求并把产生的Request 和Response 对象传给处理这个请求的线程,处理这个请求的线程就是Container 组件要做的事了。
由于这个过程比较复杂,大体的流程可以用下面的顺序图来解释:
图6. Connector 处理一次请求顺序图
(查看清晰大图)
Tomcat5中默认的Connector是Coyote,这个Connector是可以选择替换的。Connector最重要的功能就是接收连接请求然后分配线程让Container来处理这个请求,所以这必然是多线程的,多线程的处理是Connector设计的核心。Tomcat5将这个过程更加细化,它将Connector划分成Connector、Processor、Protocol,另外Coyote也定义自己的Request和Response对象。
下面主要看一下Tomcat 中如何处理多线程的连接请求,先看一下Connector 的主要类图:
图7. Connector 的主要类图
(查看清晰大图)
看一下HttpConnector 的Start 方法:
清单6. HttpConnector.Start
public void start() throws LifecycleException { if (started) throw new LifecycleException (sm.getString("httpConnector.alreadyStarted")); threadName = "HttpConnector[" + port + "]"; lifecycle.fireLifecycleEvent(START_EVENT, null); started = true; threadStart(); while (curProcessors < minProcessors) { if ((maxProcessors > 0) && (curProcessors >= maxProcessors)) break; HttpProcessor processor = newProcessor(); recycle(processor); } }
threadStart()执行就会进入等待请求的状态,直到一个新的请求到来才会激活它继续执行,这个激活是在HttpProcessor的assign方法中,这个方法是代码如下:
清单7. HttpProcessor.assign
synchronized void assign(Socket socket) { while (available) { try { wait(); } catch (InterruptedException e) { } } this.socket = socket; available = true; notifyAll(); if ((debug >= 1) && (socket != null)) log(" An incoming request is being assigned"); }
创建HttpProcessor 对象是会把available 设为false,所以当请求到来时不会进入while 循环,将请求的socket 赋给当期处理的socket,并将available 设为true,当available 设为true 是HttpProcessor 的run 方法将被激活,接下去将会处理这次请求。
Run 方法代码如下:
清单8. HttpProcessor.Run
public void run() { while (!stopped) { Socket socket = await(); if (socket == null) continue; try { process(socket); } catch (Throwable t) { log("process.invoke", t); } connector.recycle(this); } synchronized (threadSync) { threadSync.notifyAll(); } }
解析socket 的过程在process 方法中,process 方法的代码片段如下:
清单9. HttpProcessor.process
private void process(Socket socket) { boolean ok = true; boolean finishResponse = true; SocketInputStream input = null; OutputStream output = null; try { input = new SocketInputStream(socket.getInputStream(),connector.getBufferSize()); } catch (Exception e) { log("process.create", e); ok = false; } keepAlive = true; while (!stopped && ok && keepAlive) { finishResponse = true; try { request.setStream(input); request.setResponse(response); output = socket.getOutputStream(); response.setStream(output); response.setRequest(request); ((HttpServletResponse) response.getResponse()) .setHeader("Server", SERVER_INFO); } catch (Exception e) { log("process.create", e); ok = false; } try { if (ok) { parseConnection(socket); parseRequest(input, output); if (!request.getRequest().getProtocol().startsWith("HTTP/0")) parseHeaders(input); if (http11) { ackRequest(output); if (connector.isChunkingAllowed()) response.setAllowChunking(true); } } 。。。。。。 try { ((HttpServletResponse) response).setHeader ("Date", FastHttpDateFormat.getCurrentDate()); if (ok) { connector.getContainer().invoke(request, response); } 。。。。。。 } try { shutdownInput(input); socket.close(); } catch (IOException e) { ; } catch (Throwable e) { log("process.invoke", e); } socket = null; }
当Connector 将socket 连接封装成request 和response 对象后接下来的事情就交给Container 来处理了。
回页首
Servlet 容器“Container”
Container是容器的父接口,所有子容器都必须实现这个接口,Container容器的设计用的是典型的责任链的设计模式,它有四个子容器组件构成,分别是:Engine、Host、Context、Wrapper,这四个组件不是平行的,而是父子关系,Engine包含Host,Host包含Context,Context包含Wrapper。通常一个Servlet class对应一个Wrapper,如果有多个Servlet就可以定义多个Wrapper,如果有多个Wrapper就要定义一个更高的Container了,如Context,Context通常就是对应下面这个配置:
清单10. Server.xml
path="/library"
docBase="D:\projects\library\deploy\target\library.war"
reloadable="true"
/>
容器的总体设计
Context还可以定义在父容器Host中,Host不是必须的,但是要运行war程序,就必须要Host,因为war中必有web.xml文件,这个文件的解析就需要Host了,如果要有多个Host就要定义一个top容器Engine了。而Engine没有父容器了,一个Engine代表一个完整的Servlet引擎。
那么这些容器是如何协同工作的呢?先看一下它们之间的关系图:
图8. 四个容器的关系图
(查看清晰大图)
当Connector接受到一个连接请求时,将请求交给Container,Container是如何处理这个请求的?这四个组件是怎么分工的,怎么把请求传给特定的子容器的呢?又是如何将最终的请求交给Servlet处理。下面是这个过程的时序图:
图9. Engine 和Host 处理请求的时序图
(查看清晰大图)
这里看到了Valve是不是很熟悉,没错Valve的设计在其他框架中也有用的,同样Pipeline的原理也基本是相似的,它是一个管道,Engine和Host都会执行这个Pipeline,您可以在这个管道上增加任意的Valve,Tomcat会挨个执行这些Valve,而且四个组件都会有自己的一套Valve集合。您怎么才能定义自己的Valve呢?在server.xml文件中可以添加,如给Engine和Host增加一个Valve如下:
清单11. Server.xml
……… xmlNamespaceAware="false" xmlValidation="false"> directory="logs" prefix="localhost_access_log." suffix=".txt" pattern="common" resolveHosts="false"/> …………
StandardEngineValve 和StandardHostValve 是Engine 和Host 的默认的Valve,它们是最后一个Valve 负责将请求传给它们的子容器,以继续往下执行。
前面是Engine和Host容器的请求过程,下面看Context和Wrapper容器时如何处理请求的。下面是处理请求的时序图:
图10. Context 和wrapper 的处理请求时序图
(查看清晰大图)
从Tomcat5 开始,子容器的路由放在了request 中,request 中保存了当前请求正在处理的Host、Context 和wrapper。
Engine 容器
Engine 容器比较简单,它只定义了一些基本的关联关系,接口类图如下:
图11. Engine 接口的类结构
它的标准实现类是StandardEngine,这个类注意一点就是Engine没有父容器了,如果调用setParent方法时将会报错。添加子容器也只能是Host类型的,代码如下:
清单12. StandardEngine. addChild
public void addChild(Container child) { if (!(child instanceof Host)) throw new IllegalArgumentException (sm.getString("standardEngine.notHost")); super.addChild(child); } public void setParent(Container container) { throw new IllegalArgumentException (sm.getString("standardEngine.notParent")); }
它的初始化方法也就是初始化和它相关联的组件,以及一些事件的监听。
Host 容器
Host是Engine的字容器,一个Host在Engine中代表一个虚拟主机,这个虚拟主机的作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是Context,它除了关联子容器外,还有就是保存一个主机应该有的信息。
下面是和Host 相关的类关联图:
图12. Host 相关的类图
(查看清晰大图)
从上图中可以看出除了所有容器都继承的ContainerBase 外,StandardHost 还实现了Deployer 接口,上图清楚的列出了这个接口的主要方法,这些方法都是安装、展开、启动和结束每个web application。
Deployer 接口的实现是StandardHostDeployer,这个类实现了的最要的几个方法,Host 可以调用这些方法完成应用的部署等。
Context 容器
Context代表Servlet的Context,它具备了Servlet运行的基本环境,理论上只要有Context就能运行Servlet了。简单的Tomcat可以没有Engine和Host。
Context最重要的功能就是管理它里面的Servlet实例,Servlet实例在Context中是以Wrapper出现的,还有一点就是Context如何才能找到正确的Servlet来执行它呢?Tomcat5以前是通过一个Mapper类来管理的,Tomcat5以后这个功能被移到了request中,在前面的时序图中就可以发现获取子容器都是通过request来分配的。
Context 准备Servlet 的运行环境是在Start 方法开始的,这个方法的代码片段如下:
清单13. StandardContext.start
public synchronized void start() throws LifecycleException { ……… if( !initialized ) { try { init(); } catch( Exception ex ) { throw new LifecycleException("Error initializaing ", ex); } } ……… lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null); setAvailable(false); setConfigured(false); boolean ok = true; File configBase = getConfigBase(); if (configBase != null) { if (getConfigFile() == null) { File file = new File(configBase, getDefaultConfigFile()); setConfigFile(file.getPath()); try { File appBaseFile = new File(getAppBase()); if (!appBaseFile.isAbsolute()) { appBaseFile = new File(engineBase(), getAppBase()); } String appBase = appBaseFile.getCanonicalPath(); String basePath = (new File(getBasePath())).getCanonicalPath(); if (!basePath.startsWith(appBase)) { Server server = ServerFactory.getServer(); ((StandardServer) server).storeContext(this); } } catch (Exception e) { log.warn("Error storing config file", e); } } else { try { String canConfigFile = (new File(getConfigFile())).getCanonicalPath(); if (!canConfigFile.startsWith (configBase.getCanonicalPath())) { File file = new File(configBase, getDefaultConfigFile()); if (copy(new File(canConfigFile), file)) { setConfigFile(file.getPath()); } } } catch (Exception e) { log.warn("Error setting config file", e); } } } ……… Container children[] = findChildren(); for (int i = 0; i < children.length; i++) { if (children[i] instanceof Lifecycle) ((Lifecycle) children[i]).start(); } if (pipeline instanceof Lifecycle) ((Lifecycle) pipeline).start(); ……… }
它主要是设置各种资源属性和管理组件,还有非常重要的就是启动子容器和Pipeline。
我们知道Context 的配置文件中有个reloadable 属性,如下面配置:
清单14. Server.xml
path="/library"
docBase="D:\projects\library\deploy\target\library.war"
reloadable="true"
/>
当这个reloadable设为true时,war被修改后Tomcat会自动的重新加载这个应用。如何做到这点的呢?这个功能是在StandardContext的backgroundProcess方法中实现的,这个方法的代码如下:
清单15. StandardContext. backgroundProcess
public void backgroundProcess() { if (!started) return; count = (count + 1) % managerChecksFrequency; if ((getManager() != null) && (count == 0)) { try { getManager().backgroundProcess(); } catch ( Exception x ) { log.warn("Unable to perform background process on manager",x); } } if (getLoader() != null) { if (reloadable && (getLoader().modified())) { try { Thread.currentThread().setContextClassLoader (StandardContext.class.getClassLoader()); reload(); } finally { if (getLoader() != null) { Thread.currentThread().setContextClassLoader (getLoader().getClassLoader()); } } } if (getLoader() instanceof WebappLoader) { ((WebappLoader) getLoader()).closeJARs(false); } } }
它会调用reload方法,而reload方法会先调用stop方法然后再调用Start方法,完成Context的一次重新加载。可以看出执行reload方法的条件是reloadable为true和应用被修改,那么这个backgroundProcess方法是怎么被调用的呢?
这个方法是在ContainerBase 类中定义的内部类ContainerBackgroundProcessor 被周期调用的,这个类是运行在一个后台线程中,它会周期的执行run 方法,它的run 方法会周期调用所有容器的backgroundProcess 方法,因为所有容器都会继承ContainerBase 类,所以所有容器都能够在backgroundProcess 方法中定义周期执行的事件。
Wrapper 容器
Wrapper代表一个Servlet,它负责管理一个Servlet,包括的Servlet的装载、初始化、执行以及资源回收。Wrapper是最底层的容器,它没有子容器了,所以调用它的addChild将会报错。
Wrapper 的实现类是StandardWrapper,StandardWrapper 还实现了拥有一个Servlet 初始化信息的ServletConfig,由此看出StandardWrapper 将直接和Servlet 的各种信息打交道。
下面看一下非常重要的一个方法loadServlet,代码片段如下:
清单16. StandardWrapper.loadServlet
public synchronized Servlet loadServlet() throws ServletException { ……… Servlet servlet; try { ……… ClassLoader classLoader = loader.getClassLoader(); ……… Class classClass = null; ……… servlet = (Servlet) classClass.newInstance(); if ((servlet instanceof ContainerServlet) && (isContainerProvidedServlet(actualClass) || ((Context)getParent()).getPrivileged() )) { ((ContainerServlet) servlet).setWrapper(this); } classLoadTime=(int) (System.currentTimeMillis() -t1); try { instanceSupport.fireInstanceEvent(InstanceEvent.BEFORE_INIT_EVENT,servlet); if( System.getSecurityManager() != null) { Class[] classType = new Class[]{ServletConfig.class}; Object[] args = new Object[]{((ServletConfig)facade)}; SecurityUtil.doAsPrivilege("init",servlet,classType,args); } else { servlet.init(facade); } if ((loadOnStartup >= 0) && (jspFile != null)) { ……… if( System.getSecurityManager() != null) { Class[] classType = new Class[]{ServletRequest.class, ServletResponse.class}; Object[] args = new Object[]{req, res}; SecurityUtil.doAsPrivilege("service",servlet,classType,args); } else { servlet.service(req, res); } } instanceSupport.fireInstanceEvent(InstanceEvent.AFTER_INIT_EVENT,servlet); ……… return servlet; }
它基本上描述了对Servlet 的操作,当装载了Servlet 后就会调用Servlet 的init 方法,同时会传一个StandardWrapperFacade 对象给Servlet,这个对象包装了StandardWrapper,ServletConfig 与它们的关系图如下:
图13. ServletConfig 与StandardWrapperFacade、StandardWrapper 的关系
Servlet可以获得的信息都在StandardWrapperFacade封 装,这些信息又是在StandardWrapper对象中拿到的。所以Servlet可以通过ServletConfig拿到有限的容器的信息。
当Servlet 被初始化完成后,就等着StandardWrapperValve 去调用它的service 方法了,调用service 方法之前要调用Servlet 所有的filter。
回页首
Tomcat 中其它组件
Tomcat还有其它重要的组件,如安全组件security、logger日志组件、session、mbeans、naming等其它组件。这些组件共同为Connector和Container提供必要的服务。
参考资料
学习
- 查看本系列的第2部分:“ Tomcat设计模式分析 ”。
- “ 面向初级Web开发人员的Tomcat ”(developerWorks,2005年10月):Apache Tomcat应用服务器不再是高级Web系统开发人员的专用领域。在本教程中,Sing Li将向初级Web开发人员展示如何利用他们当前的Java开发技能,使用Tomcat编写服务器端JSP、servlet和Web服务。
- 技术书店:浏览关于这些和其他技术主题的图书。
- developerWorks Java技术专区:数百篇关于Java编程各个方面的文章。
讨论
- 加入developerWorks社区。
- 查看developerWorks博客的最新信息。
条评论
共有评论( 35 )显示: 最新评论 所有评论
谢谢。
由dyp 于2014年10月25日发布
报告滥用
thanks
由江边草木 于2014年10月24日发布
报告滥用
thanks a lot
由蓝风970655147 于2014年09月29日发布
报告滥用
a nice paper
由zq-miao 于2014年07月25日发布
报告滥用
thanks a lot
由jeremy_lincoln 于2014年06月22日发布
报告滥用
回页首
请登录或注册 后发表评论。
添加评论:
注意:评论中不支持HTML 语法