分享Tomcat源码系列三部曲

开发 后端
本文主要和大家分享一下Tomcat的源代码,希望对大家有所帮助。

最近在看Tomcat的源码,下面用博客记下看源码的一些心得。

Tomcat是从org.apache.catalina.startup.Bootstrap#main()开始启动. 大致分为三个步骤,即init、load和start。代码如下:

Java代码

  1. public static void main(String args[]) {      
  2.         try {      
  3.             // Attempt to load JMX class      
  4.             new ObjectName("test:foo=bar");      
  5.         } catch (Throwable t) {      
  6.             System.out.println(JMX_ERROR_MESSAGE);      
  7.             try {      
  8.                 // Give users some time to read the message before exiting      
  9.                 Thread.sleep(5000);      
  10.             } catch (Exception ex) {      
  11.             }      
  12.             return;      
  13.         }      
  14.         if (daemon == null) {      
  15.             daemon = new Bootstrap();      
  16.             try {      
  17.                 daemon.init();   ★1      
  18.             } catch (Throwable t) {      
  19.                 t.printStackTrace();      
  20.                 return;      
  21.             }      
  22.         }      
  23.         try {      
  24.             String command = "start";      
  25.             if (args.length > 0) {      
  26.                 command = args[args.length - 1];      
  27.             }      
  28.             if (command.equals("startd")) {      
  29.                 args[0] = "start";      
  30.                 daemon.load(args);      
  31.                 daemon.start();      
  32.             } else if (command.equals("stopd")) {      
  33.                 args[0] = "stop";      
  34.                 daemon.stop();      
  35.             } else if (command.equals("start")) {      
  36.                 daemon.setAwait(true);      
  37.                 daemon.load(args);   ★2      
  38.     
  39.              // 反射调用Catalina的start方法      
  40.                 daemon.start();        ★3      
  41.             } else if (command.equals("stop")) {      
  42.                 daemon.stopServer(args);      
  43.             }      
  44.         } catch (Throwable t) {      
  45.             t.printStackTrace();      
  46.         }      
  47.     }      

 

从以上可以很清楚的看出tomcat是通过参数的不同进行相应的命令调用。

★1 启动、初始化(加载类)

启动之前要进行相应的init()初始化,进行相应的环境设置以及包的加,以下是init()方法。(org.apache.catalina.startup.Bootstrap.init())

Java代码

 

  1. public void init()      
  2.         throws Exception      
  3.     {      
  4.         setCatalinaHome();//设置Catalina安装目录      
  5.         setCatalinaBase();//设置Catalina工作目录      
  6.         initClassLoaders();//加载jar包      
  7.     
  8.        // 将classload设置进线程,以便我们使用时进行调用            
  9.         Thread.currentThread().      
  10.                       setContextClassLoader(catalinaLoader);      
  11.         SecurityClassLoad.securityClassLoad(catalinaLoader);      
  12.     
  13.         // 加载启动类和调用它的process方法      
  14.         if (log.isDebugEnabled())      
  15.             log.debug("Loading startup class");      
  16.         Class startupClass =      
  17.             catalinaLoader.loadClass      
  18.             ("org.apache.catalina.startup.Catalina");      
  19.         Object startupInstance = startupClass.newInstance();      
  20.     
  21.         // 设置共享扩张类加载器      
  22.         if (log.isDebugEnabled())      
  23.             log.debug("Setting startup class properties");      
  24.         String methodName = "setParentClassLoader";      
  25.         Class paramTypes[] = new Class[1];      
  26.         paramTypes[0] = Class.forName("java.lang.ClassLoader");      
  27.         Object paramValues[] = new Object[1];      
  28.         paramValues[0] = sharedLoader;      
  29.         Method method =      
  30.         startupInstance.getClass().getMethod(methodName,      
  31.                                                           paramTypes);      
  32.         method.invoke(startupInstance, paramValues);      
  33.         catalinaDaemon = startupInstance;      
  34.     }  

 

在加载jar的时候,需要初始化classloader,代码如下:(org.apache.catalina.startup.Bootstrap)

Java代码

 

  1. private void initClassLoaders() {      
  2.         try {      
  3.             commonLoader = createClassLoader("common"null);      
  4.             catalinaLoader= createClassLoader("server", commonLoader);      
  5.             sharedLoader = createClassLoader("shared", commonLoader);      
  6.         } catch (Throwable t) {      
  7.             log.error("Class loader creation threw exception", t);      
  8.             System.exit(1);      
  9.         }      
  10.     }   

 

tomcat中的加载方式是:

|-------commonLoader (common)-> System Loader

|-------sharedLoader (shared)-> commonLoader -> System Loader

|-------catalinaLoader(server) -> commonLoader -> System Loader

Common是公共类加载器,负责加载tomcat内部和web应用程序可以看到的类(%CATALINA_HOME%/bin/common下的jar文件),Catalina负责加载的是tomcat内部使用的类(%CATALINA_HOME%/server下的jar文件),这些类对web应用程序不可见。Shared负责加载的是web应用程序之间共享的类(%CATALINA_BASE%/shared下的jar文件),这些类对于tomcat内部是不可见的。如果%CATALINA_HOME%/conf/catalina.Properties中没有指定Common的搜索路径,则用当前的类的类加载器即系统类加载器作为Common。

★2 装载相应的资源

下面主要讲解tomcat的load()方法。下图是Catalina.load方法的时序图。

 

 

(1) 从上面的时序图可以看出首先调用Catalina类的load()方法,具体代码如下:

(org.apache.catalina.startup.Catalina)。

Java代码

 

  1. public void load() {      
  2.         initDirs();      
  3.     
  4.         // Before digester - it may be needed      
  5.         initNaming();      
  6.     
  7.         // Create and execute our Digester      
  8.         Digester digester = createStartDigester();      
  9.             
  10.         try {      
  11.             inputSource.setByteStream(inputStream);      
  12.             digester.push(this);      
  13.             digester.parse(inputSource); //对server.xml进行解析      
  14.             inputStream.close();      
  15.         }      
  16.        ......      
  17.         // Start the new server      
  18.         if (server instanceof Lifecycle) {      
  19.             try {      
  20.                 server.initialize();  //server初始化工作      
  21.             } catch (LifecycleException e) {      
  22.                 log.error("Catalina.start", e);      
  23.             }      
  24.         }      
  25.         long t2 = System.currentTimeMillis();      
  26.         log.info("Initialization processed in " + (t2 - t1) + " ms");      
  27.     
  28.     }     

 

(2) 在上面的load()方法中需要进行server的初始化工作,下图为Catalina.initialize的时序图,从图中可以看出server初始化所完成的工作。

 

 

至此,load方法结束,初期化的工作结束,下面开始进入start方法。

★3 容器启动

容器启动时,会调用Catalina.start(),下图为它的时序图。从图中可以看出StandardService的start方法被调用后会分别对Container和Connector进行start方法的调用。

 

 

1. Bootstrap调用Catalina的start方法

Catalina.start()方法(org.apache.catalina.startup.Catalina.start())

Java代码

 

  1. public void start() {      
  2.     // 启动server      
  3.     if (server instanceof Lifecycle) {      
  4.         try {      
  5.             ((Lifecycle) server).start();      
  6.                     ......      
  7.    }     

 

2. Catalina调用StandardServer的start方法

StandardServer.start() (org.apache.catalina.core.StandardServer.start() )

Java代码

 

  1. public void start() throws LifecycleException {            
  2.         synchronized (services) {      
  3.             for (int i = 0; i < services.length; i++) {      
  4.                 if (services[i] instanceof Lifecycle)      
  5.                     ((Lifecycle) services[i]).start();      
  6.             }       
  7. }   

 

3. StandardServer调用StandardService的start方法

Java代码

 

  1. org.apache.catalina.core.StandardService.start() )      
  2.         public void start() throws LifecycleException {      
  3.                   if (container != null) {      
  4.             synchronized (container) {      
  5.                 if (container instanceof Lifecycle) {      
  6.               //  standardEngine的启动      
  7.                     ((Lifecycle) container).start();      
  8.                 }      
  9.             }      
  10.        //两个connector的启动,8080和8009        
  11.        synchronized (connectors) {        
  12.            for (int i = 0; i < connectors.length; i++) {        
  13.                if (connectors[i] instanceof Lifecycle)        
  14.                    ((Lifecycle) connectors[i]).start();        
  15.                   }        
  16.        }        
  17. }  

 

以上StandardService.start()方法主要实现了两个功能,standardEngine的启动和connector的启动,下面分别来介绍。

#p#

下面是standardEngine的启动和connector的启动

● standardEngine的启动

(1) 首先是StandardEngine.start()被调用

Java代码

 

 

  1. public void start() throws LifecycleException {      
  2.        // Standard container startup        
  3.       //进行logger,manager,cluster,realm,resource的启动        
  4.        super.start();      
  5. }  

 

(2) super.start()--->org.apache.catalina.core.ContainerBase#start()

Java代码

 

 

  1. public synchronized void start() throws LifecycleException {      
  2. //(省略)  server.xml中配置应用组件的启动         
  3. //StandardHost容器的启动,        
  4.         Container children[] = findChildren();        
  5.         for (int i = 0; i < children.length; i++) {        
  6.             if (children[i] instanceof Lifecycle)        
  7.                 ((Lifecycle) children[i]).start();        
  8.         }          
  9.     
  10.     //StandardPipeline的启动(容器与容器间的管道)        
  11.         if (pipeline instanceof Lifecycle)        
  12.             ((Lifecycle) pipeline).start();       
  13. }   

 

(3) StandardHost.start()被调用

Java代码

 

 

  1. public synchronized void start() throws LifecycleException {      
  2. //返回到以上的containerBase#start执行pipeline        
  3.       super.start();       
  4. }  

 

(4) StandardPipeline#start

Java代码

 

 

  1. public synchronized void start() throws LifecycleException {      
  2.        // 将会调用HostConfig#start方法        
  3.        lifecycle.fireLifecycleEvent(START_EVENT, null);        
  4.        
  5.        // Notify our interested LifecycleListeners        
  6.        lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);        
  7. }    

 

(5) HostConfig#start

Java代码

 

 

  1. public void start() {       
  2.           //部暑webapps        
  3.           deployApps();                 
  4.   }     

 

(6) HostConfig#deployApps

Java代码

 

 

  1. protected void deployApps() {        
  2.     File appBase = appBase();        
  3.     File configBase = configBase();        
  4.     // Deploy XML descriptors from configBase        
  5.     deployDescriptors(configBase, configBase.list());        
  6.     // Deploy WARs, and loop if additional descriptors are found        
  7.     deployWARs(appBase, appBase.list());        
  8.     // Deploy expanded folders        
  9.     deployDirectories(appBase, appBase.list());                  
  10. }    

 

(7) deployWARs

Java代码

 

 

  1. protected void deployWARs(File appBase, String[] files) {      
  2. ……      
  3. deployWAR(contextPath, dir, file);               
  4.   }  

 

(8) deployWAR

Java代码

 

 

  1. protected void deployWAR(String contextPath, File war, String file) {      
  2. if (context instanceof Lifecycle) {        
  3.   // (省略)      
  4.             Class clazz = Class.forName(host.getConfigClass());        
  5.             LifecycleListener listener =        
  6.                 (LifecycleListener) clazz.newInstance();        
  7.             ((Lifecycle) context).addLifecycleListener(listener);        
  8.         }        
  9.         context.setPath(contextPath);        
  10.         context.setDocBase(file);        
  11.         //以下这一步跟进去,,StandardContext的启动        
  12.         host.addChild(context);              
  13.   }   

 

(9) StandardContext#start

在Context的启动过程中,主要完成了以下任务。

----------------------------------------------------------------------------------------------------------------------

a) 设置web app的具体目录webappResources。

b) postWorkDirectory (),创建临时文件目录。Tomcat下面有一个work目录,用来存放临时文件。

c) 触发START_EVENT事件监听,在这个事件监听里面会启动ContextConfig的start()事件,ContextConfig是用来配置web.xml的。

d) 为context创建welcome files,通常是这三个启动文件:index.html、index.htm、index.jsp

e) 配置filter

f) 启动带有的Servlet。

g) 注册JMX。

----------------------------------------------------------------------------------------------------------------------

至此,Container启动完毕,下面是connector的启动。

● connector的启动

(1) org.apache.catalina.connector.Connector.start()

Java代码

 

 

  1. public void start() throws LifecycleException {      
  2.            // Http11Protocol的启动      
  3.             protocolHandler.start();      
  4. }   

 

(2) Http11Protocol#start

Java代码

 

 

  1. public void start() throws Exception {      
  2. try {        
  3.             //到了终点的启动        
  4.             endpoint.start();        
  5.         } catch (Exception ex) {        
  6.             log.error(sm.getString("http11protocol.endpoint.starterror"), ex);        
  7.             throw ex;        
  8.         }    

 

(3) JIoEndPoint#start

Java代码

 

 

  1. public void start()        
  2.         throws Exception {                 
  3.                    
  4.             for (int i = 0; i < acceptorThreadCount; i++) {        
  5.         //这里的acceptor是一个线程,里面是一个serversocket的启动        
  6.                 Thread acceptorThread = new Thread(new Acceptor(), getName() + "-Acceptor-" + i);        
  7.                 acceptorThread.setPriority(threadPriority);        
  8.                 acceptorThread.setDaemon(daemon);        
  9.                 acceptorThread.start();        
  10.             }        
  11.         }  

 

(4) Acceptor#run

Java代码

 

 

  1. public void run() {                    
  2. // Accept the next incoming connection from the server socket        
  3.                try {        
  4.           //这里进行了accept(),等待客户端消息,进行接收        
  5.                    Socket socket = serverSocketFactory.acceptSocket(serverSocket);        
  6.                    serverSocketFactory.initSocket(socket);        
  7.                    // Hand this socket off to an appropriate processor        
  8.                    if (!processSocket(socket)) {        
  9.                        // Close socket right away        
  10.                        try {        
  11.                            socket.close();        
  12.                        } catch (IOException e) {        
  13.                            // Ignore        
  14.                        }        
  15.                    }        
  16.                }catch ( IOException x ) {        
  17.                    if ( running ) log.error(sm.getString("endpoint.accept.fail"), x);        
  18.                } catch (Throwable t) {        
  19.                    log.error(sm.getString("endpoint.accept.fail"), t);        
  20.                }        
  21. }    

 

至此Connector.start方法调用完毕。整个server启动完毕。

#p#

本次讲解一下Tomcat请求处理的流程,不当之处还请comment。

一. Tomcat 总体结构

Tomcat采用模块化管理,下面是 Tomcat 的总体结构图:

 

 

从上图中可以看出 Tomcat 的核心是两个组件:Connector 和 Container。下面是一些概念的介绍。

① Server

一个server代表了整个catalina servlet容器,在Tomcat里面的Server的用处是启动和监听服务端事件(诸如重启、关闭等命令)。

② Service

Service是由一个或多个Connector与一个Engine的组合。

③ Connector

Connector将在某个指定的端口上监听客户的请求,把从socket传递过来的数据,封装成Request,传递给Engine来处理,并从Engine处获得响应并返回给客户。

Tomcat通常会用到两种Connector:

a) Http Connector 在端口8080处侦听来自客户browser的http请求。

b) AJP Connector 在端口8009处侦听来自其它WebServer(Apache)的servlet/jsp代理请求。

二、请求处理过程解析

1. Connector处理请求

Connector处理请求的流程大致如下:

 

 

Connector组件启动后,会侦听相关的端口的客户端请求。

(1) 接受一个新的连接请求(org.apache.tomcat.util.net.TcpWorkerThread)

Java代码

 

  1. void runIt(Object[] perThrData){      
  2.        Socket s = null;      
  3.             try {      
  4.                 s = endpoint.acceptSocket();  //获取一个请求      
  5.             } finally {      
  6.                 if (endpoint.isRunning()) {      
  7.                     endpoint.tp.runIt(this);      
  8.   // 此处启动另一个TcpWorkerTread去接受其他请求,此线程处理已接受的请求      
  9.                 }      
  10.             }                      
  11.       TcpConnection con = null;      
  12.       con = (TcpConnection) perThrData[0];      
  13.       con.setEndpoint(endpoint);      
  14.       con.setSocket(s);endpoint.getConnectionHandler().processConnection(con,(Object[]) perThrData[1]);          
  15. }    

 

(2) 新接收的请求被传到Http11ConnectionHandler中处理。(org.apache.coyote.http11.Http11Protocol.Http11ConnectionHandler)

Java代码

 

  1. void processConnection(TcpConnection connection, Object[] thData){          
  2.     Http11Processor  processor=null;      
  3.     processor=(Http11Processor)thData[Http11Protocol.THREAD_DATA_PROCESSOR];        
  4.     socket=connection.getSocket();                           
  5.     InputStream in = socket.getInputStream();        
  6.     OutputStream out = socket.getOutputStream();      
  7.     processor.setSocket(socket );      
  8.     processor.process(in, out);        
  9. //processor是org.apache.coyote.http11.Http11Processor 的 一个实例      
  10. }    

 

(3) 在 Http11Processor 中处理 http11 协议相关的信息(org.apache.coyote.http11.Http11Processor)

Java代码

 

  1. void process(InputStream input, OutputStream output) throws IOException{      
  2.         ~~略~~      
  3.         inputBuffer.setInputStream(input);      
  4.         outputBuffer.setOutputStream(output);      
  5.         inputBuffer.parseHeaders();      
  6.       //http11 协议头在此方法中被取出      
  7.         adapter.service(request, response);         
  8.       //adapter 是org.apache.catalina.connector.CoyoteAdapter 的 一个实例      
  9. }   

 

接下来的流程交由容器进行处理。

2. 容器处理请求

容器交由Pipeline处理,这个Pipeline里面会放置一些vavle,请求沿着pipeline传递下去并且vavle对其进行相关的处理。比如说日志等,valve还可以自定义,具体需要查看server.xml配置文件。相关类图如下:

 

 

Tomcat的主要处理组件Engine、Host、Context和Wrapper的实现都会实现Pipeline接口,实际对请求的处理是一个Adpater,Tomcat中Adapter的实现是CoyoteAdapter,因此容器请求处理的入口是CoyoteAdapter的service方法。

1. CoyoteAdapter.service

--组装好请求处理链

--StandardEngine. getPipeline().getFirst().invoke(request, response);

--StandardEngineValve.invoke

2. StandardEngineValve.invoke

--Host.getPipeline().getFirst().invoke(request, response);

--StandardHostValve.invoke

3. StandardHostValve.invoke

--Context. getPipeline().getFirst().invoke(request, response);

--StandardContextValve.invoke

4. StandardContextValve.invoke

--ServletRequestListener.requestInitialized

--Wrapper.getPipeline().getFirst().invoke(request, response);

--StandardWrapperValve.invoke

-- ServletRequestListener.requestDestroyed

5. StandardWrapperValve.invoke

--组装Filter+Servlet

--处理请求

(1) Connector传来的请求调用CoyoteAdapter.service()方法。(org.apache.catalina.connector.CoyoteAdapter)

Java代码

 

  1. public void service(org.apache.coyote.Request req,         
  2.                     org.apache.coyote.Response res)        
  3.     throws Exception {        
  4.          ~~略~~       
  5.    if (request == null) {       
  6.         request = (Request) connector.createRequest();      
  7.         request.setCoyoteRequest(req);      
  8.         response = (Response) connector.createResponse();      
  9.      response.setCoyoteResponse(res);      
  10.      //创建request、response对象        
  11.          ~~略~~        
  12.     }              
  13.     try {         
  14.         if (postParseRequest(req, request, res, response)) {        
  15. connector.getContainer().getPipeline().getFirst().invoke(request, response);      
  16. //此处的Container是StandardEngine对象       
  17.            ~~略~~         
  18.     }        
  19. }    

 

(2) 默认StandardEngine的Pipeline会有StandardEngineValve处理单元(参照StandardEngine构造函数)。(org.apache.catalina.core.StandardEngineValve)

Java代码

 

  1. public final void invoke(Request request, Response response)        
  2.     throws IOException, ServletException {          
  3.      // Select the Host to be used for this Request        
  4.   Host host = request.getHost();        
  5.     if (host == null) {        
  6.          response.sendError        
  7.              (HttpServletResponse.SC_BAD_REQUEST,        
  8.              sm.getString("standardEngine.noHost",        
  9.                            request.getServerName()));        
  10.          return;        
  11.      }          
  12.      // Ask this Host to process this request        
  13.      host.getPipeline().getFirst().invoke(request, response);        
  14.    }    

 

(3) 同样的,StandardHost的Pipeline会有StandardHostValve处理单元。StandardHostValve如何处理请求跟StandardEngineValve类似,接下来请求进入到StandardContextValve.invoke

(4) 同样的,StandardContext的Pipeline会有StandardContextValve处理单元。

Java代码

 

  1. public final void invoke(Request request, Response response)        
  2.         throws IOException, ServletException {         
  3.         // Disallow any direct access to resources under WEB-INF or META-INF         MessageBytes requestPathMB = request.getRequestPathMB();        
  4.         if ((requestPathMB.startsWithIgnoreCase("/META-INF/"0))        
  5.             || (requestPathMB.equalsIgnoreCase("/META-INF"))        
  6.             || (requestPathMB.startsWithIgnoreCase("/WEB-INF/"0))        
  7.            || (requestPathMB.equalsIgnoreCase("/WEB-INF"))) {        
  8.             String requestURI = request.getDecodedRequestURI();        
  9.             notFound(requestURI, response);        
  10.             return;        
  11.         }          
  12.         // Wait if we are reloading        
  13.         while (context.getPaused()) {        
  14.             try {        
  15.                 Thread.sleep(1000);        
  16.             } catch (InterruptedException e) {        
  17.                 ;        
  18.             }        
  19.         }          
  20.         // Select the Wrapper to be used for this Request        
  21.         Wrapper wrapper = request.getWrapper();        
  22.         if (wrapper == null) {        
  23.             String requestURI = request.getDecodedRequestURI();        
  24.             notFound(requestURI, response);        
  25.             return;        
  26.         }          
  27. //ServletRequestListener. requestInitialized        
  28. ~~略~~        
  29.        
  30.    wrapper.getPipeline().getFirst().invoke(request, response);        
  31. //ServletRequestListener.requestDestroyed        
  32. ~~略~~        
  33.      }   

 

(5) 同样的,StandardWrapper这个Pipeline会有StandardWrapperValve这个处理单元。在invoke()方法调用Filter的同时,servlet.service()方法也将会被调用。

(org.apache.catalina.core.StandardWrapperValve)

Java代码

 

  1. void invoke(Request request, Response response, ValveContext valveContext)      
  2.                 throws IOException, ServletException{      
  3.          Servlet servlet = null;      
  4.           HttpServletRequest hreq = (HttpServletRequest) request.getRequest();           
  5. //org.apache.catalina.Request被封装成javax.servlet.http.HttpServletRequest.      
  6.           HttpServletResponse hres =(HttpServletResponse) response.getResponse();       
  7. // org.apache.catalina.Response被封装成javax.servlet.http.HttpServletResponse.      
  8.          servlet = wrapper.allocate();       // 装载servlet      
  9.         if ((servlet != null) && (filterChain != null)) {      
  10.            filterChain.doFilter(hreq, hres);                   //调用此servlet的filterchain      
  11.        }   

 

(6) 调用servlet的filterchain 处理 request和response

(org.apache.catalina.core.ApplicationFilterChain)

Java代码

 

  1. void doFilter(ServletRequest request, ServletResponse response) throws      
  2.                   IOException, ServletException{      
  3.              ~~略~~      
  4.            internalDoFilter(request,response);      
  5.              ~~略~~      
  6.        }    

 

(7) 调用internalDoFilter()处理请求。(org.apache.catalina.core.ApplicationFilterChain)

Java代码

 

  1. void internalDoFilter(ServletRequest request, ServletResponse response) throws      
  2.                         IOException, ServletException{      
  3.      // 此处省略filter 处理的代码,filter 被一个一个调用。      
  4.      // 如果http请求的是一个jsp页面, 下面的 servlet 会是 org.apache.jasper.servlet.JspServlet 类的一个实例      
  5.      // 若是 html 页面, 下面的 servlet 会是 org.apache.catalina.servlets.DefaultServlet 类的一个实例      
  6.      if ((request instanceof HttpServletRequest) &&      
  7.           (response instanceof HttpServletResponse)) {      
  8.       servlet.service((HttpServletRequest) request, (HttpServletResponse) response);      
  9.         servlet.service(request, response);      
  10.        } else {      
  11.         servlet.service(request, response);      
  12.       }      
  13.     }   

 

至此,servlet.service()方法被调用。

【编辑推荐】

  1. 配置Tomcat+SQL Server2000连接池流程
  2. 配置Tomcat 4使用SSL
  3. 深入剖析Subversion+Apache配置
  4. Fedora apache配置一个基于IP的虚拟主机
  5. Ubuntu Apache配置测试及安装各种指令
责任编辑:金贺 来源: JavaEye博客
相关推荐

2011-03-25 09:56:40

Nagios 安装

2009-09-14 09:04:17

CCNA考试CCNA

2010-09-26 14:39:40

DHCP故障分析

2010-07-17 01:12:31

Telnet服务

2011-03-09 09:30:52

Mina

2022-10-10 09:10:51

家庭网络网络

2010-09-06 09:22:26

CSS语法

2013-06-28 09:35:04

Hypervisor虚拟化成本

2015-05-12 10:42:53

程序员代码

2012-09-10 16:19:00

云计算公共云

2017-04-11 09:07:20

互联网

2017-02-07 14:50:39

华为

2011-08-29 16:26:50

魔兽世界LUA

2010-05-12 10:51:06

2011-08-03 10:33:05

网络管理网络拓扑管理

2019-03-28 14:45:33

数据安全数据泄露信息安全

2010-09-14 16:54:18

2018-03-19 09:35:37

人工智能

2018-03-18 15:51:59

人工智能潜力首席数据官

2009-10-22 13:09:19

VB.NET动态控件数
点赞
收藏

51CTO技术栈公众号