成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

【Tomcat源碼分析】從零開始理解 HTTP 請求處理

開發(fā) 前端
在 Endpoint.start()? 方法中,我們首先會調(diào)用 bind() 方法,完成 Socket 的綁定,確保 Connector 能夠監(jiān)聽來自網(wǎng)絡的請求。接著,我們會創(chuàng)建工作者線程池,為后續(xù)處理請求提供充足的線程資源。隨后,我們會初始化連接 latch,用于限制請求的并發(fā)量,避免過多的請求涌入,造成系統(tǒng)崩潰。

前言

終于步入 Connector 的解析階段,這無疑是 Tomcat 架構(gòu)中最為復雜的一環(huán)。作為連接器,它的職責顯而易見——連接。那么,它連接的究竟是什么呢?

Connector 宛如一座橋梁,將來自客戶端的請求,經(jīng)過精心封裝成 Request 和 Response 對象,傳遞給 Container 進行處理。Container 完成業(yè)務邏輯后,Connector 再將處理后的結(jié)果,通過 Response 對象返回給遠方的客戶端。

要深入理解 Connector 的精髓,需要我們從四個關鍵問題出發(fā),逐一探索。

  1. Connector 如何接收來自遠方的請求?
  2. 如何將這呼喚化作 Request 和 Response 的身影?
  3. 封裝后的 Request 和 Response 如何被遞交給 Container 處理?
  4. Container 處理完畢后,如何將結(jié)果托付給 Connector,并最終送回客戶端手中?

為了更好地理解 Connector 的內(nèi)部運作,讓我們先來欣賞一幅 Connector 結(jié)構(gòu)圖,它將幫助我們更直觀地感受其內(nèi)部的精妙設計。

圖片圖片

【注意】:不同的協(xié)議和通信方式,將催生出不同的 ProtocolHandler 實現(xiàn)。在 Tomcat 8.5 版本中,ProtocolHandler 的類繼承關系圖譜如下:

圖片圖片

針對這幅類繼承層級圖,我們可以做如下解讀:

ajp 和 http11 代表著兩種截然不同的協(xié)議,而 nio、nio2 和 apr 則分別代表著三種不同的通信方式。值得注意的是,協(xié)議與通信方式并非相互獨立,它們可以靈活組合,以適應不同的場景需求。

ProtocolHandler 內(nèi)部,包含著三個核心部件:Endpoint、Processor 和 Adapter,它們共同協(xié)作,完成請求的接收、處理和響應。

  • Endpoint 負責處理底層的 Socket 網(wǎng)絡連接,它就像是一位網(wǎng)絡守衛(wèi),負責迎接來自網(wǎng)絡的呼喚,并將其轉(zhuǎn)化為可供處理的 Socket 連接。Processor 則肩負著將 Endpoint 接收到的 Socket 封裝成 Request 對象的重任,它就像一位翻譯官,將網(wǎng)絡語言轉(zhuǎn)化為服務器可以理解的語言。Adapter 則充當著連接器,它將 Request 對象傳遞給 Container,以便 Container 進行具體的處理。
  • 由于 Endpoint 負責處理底層的 Socket 網(wǎng)絡連接,因此它需要實現(xiàn) TCP/IP 協(xié)議,而 Processor 則需要實現(xiàn) HTTP 協(xié)議,以解析 HTTP 請求。Adapter 則將請求適配到 Servlet 容器,使其能夠理解并處理來自外部的請求。
  • Endpoint 的抽象實現(xiàn)類 AbstractEndpoint 定義了 Acceptor、AsyncTimeout 兩個內(nèi)部類和一個 Handler 接口。Acceptor 負責監(jiān)聽來自網(wǎng)絡的請求,一旦有新的請求到來,便會將其捕獲。AsyncTimeout 則負責檢查異步 Request 的超時,確保請求在合理的時間內(nèi)得到處理。Handler 則負責處理接收到的 Socket,它將調(diào)用 Processor 進行處理,將 Socket 轉(zhuǎn)換為 Request 對象,并最終傳遞給 Container。

至此,我們已經(jīng)解開了 Connector 如何接收請求、如何將請求封裝成 Request 和 Response,以及封裝后的 Request 和 Response 如何被傳遞給 Container 進行處理這三個關鍵問題。而對于最后一個問題,即 Container 處理完后如何將結(jié)果返回給客戶端,我們將在深入了解 Container 的運作機制后自然明了,前面章節(jié)已對此進行了詳細的分析。

Connector 源碼分析入口

在 Service 的標準實現(xiàn) StandardService 的源碼中,我們發(fā)現(xiàn)其 init()、start()、stop() 和 destroy() 方法分別會對 Connectors 的同名方法進行調(diào)用。值得注意的是,一個 Service 通常會對應多個 Connector,這意味著 Service 的生命周期管理會影響到所有與其關聯(lián)的 Connector。

Service.initInternal()

@Override
protected void initInternal() throws LifecycleException {
    super.initInternal();

    if (engine != null) {
        engine.init();
    }

    // Initialize any Executors
    for (Executor executor : findExecutors()) {
        if (executor instanceof JmxEnabled) {
            ((JmxEnabled) executor).setDomain(getDomain());
        }
        executor.init();
    }

    // Initialize mapper listener
    mapperListener.init();

    // Initialize our defined Connectors
    synchronized (connectorsLock) {
        for (Connector connector : connectors) {
            try {
                connector.init();
            } catch (Exception e) {
                String message = sm.getString(
                        "standardService.connector.initFailed", connector);
                log.error(message, e);

                if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"))
                    throw new LifecycleException(message);
            }
        }
    }
}

Service.startInternal()

@Override
protected void startInternal() throws LifecycleException {
    if(log.isInfoEnabled())
        log.info(sm.getString("standardService.start.name", this.name));
    setState(LifecycleState.STARTING);

    // Start our defined Container first
    if (engine != null) {
        synchronized (engine) {
            engine.start();
        }
    }

    synchronized (executors) {
        for (Executor executor: executors) {
            executor.start();
        }
    }

    mapperListener.start();

    // Start our defined Connectors second
    synchronized (connectorsLock) {
        for (Connector connector: connectors) {
            try {
                // If it has already failed, don't try and start it
                if (connector.getState() != LifecycleState.FAILED) {
                    connector.start();
                }
            } catch (Exception e) {
                log.error(sm.getString(
                        "standardService.connector.startFailed",
                        connector), e);
            }
        }
    }
}

正如我們所知,Connector 實現(xiàn)了 Lifecycle 接口,這使得它成為一個擁有生命周期的組件。因此,Connector 的啟動邏輯入口自然而然地落在 init() 和 start() 方法之中。

Connector 構(gòu)造方法

在深入分析 Connector 的啟動邏輯之前,不妨先來觀摩一下 server.xml 文件。這份文件如同 Tomcat 架構(gòu)的藍圖,清晰地展現(xiàn)了各個組件之間的聯(lián)系和布局,為我們理解 Connector 的運作提供了一個宏觀的視角。

<?xml versinotallow='1.0' encoding='utf-8'?>
<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <GlobalNamingResources>
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>

  <Service name="Catalina">
    <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

    <Engine name="Catalina" defaultHost="localhost">
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>

      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t "%r" %s %b" />
      </Host>
    </Engine>
  </Service>
</Server>

在 server.xml 文件中,我們發(fā)現(xiàn) Connector 擁有多個關鍵屬性,其中 port 和 protocol 尤為重要。默認情況下,server.xml 支持兩種協(xié)議:HTTP/1.1 和 AJP/1.3。HTTP/1.1 用于支持傳統(tǒng)的 HTTP 1.1 協(xié)議,而 AJP/1.3 則專門用于支持與 Apache 服務器的通信,為 Apache 服務器提供一個與 Tomcat 交互的橋梁。

現(xiàn)在,讓我們將目光轉(zhuǎn)向 Connector 的構(gòu)造方法:

public Connector() {
    this(null); // 1. 無參構(gòu)造方法,傳入?yún)?shù)為空協(xié)議,會默認使用`HTTP/1.1`
}

public Connector(String protocol) {
    setProtocol(protocol);
    // Instantiate protocol handler
    // 5. 使用protocolHandler的類名構(gòu)造ProtocolHandler的實例
    ProtocolHandler p = null;
    try {
        Class<?> clazz = Class.forName(protocolHandlerClassName);
        p = (ProtocolHandler) clazz.getConstructor().newInstance();
    } catch (Exception e) {
        log.error(sm.getString(
                "coyoteConnector.protocolHandlerInstantiationFailed"), e);
    } finally {
        this.protocolHandler = p;
    }

    if (Globals.STRICT_SERVLET_COMPLIANCE) {
        uriCharset = StandardCharsets.ISO_8859_1;
    } else {
        uriCharset = StandardCharsets.UTF_8;
    }
}

@Deprecated
public void setProtocol(String protocol) {
    boolean aprConnector = AprLifecycleListener.isAprAvailable() &&
            AprLifecycleListener.getUseAprConnector();

    // 2. `HTTP/1.1`或`null`,protocolHandler使用`org.apache.coyote.http11.Http11NioProtocol`,不考慮apr
    if ("HTTP/1.1".equals(protocol) || protocol == null) {
        if (aprConnector) {
            setProtocolHandlerClassName("org.apache.coyote.http11.Http11AprProtocol");
        } else {
            setProtocolHandlerClassName("org.apache.coyote.http11.Http11NioProtocol");
        }
    }
    // 3. `AJP/1.3`,protocolHandler使用`org.apache.coyote.ajp.AjpNioProtocol`,不考慮apr
    else if ("AJP/1.3".equals(protocol)) {
        if (aprConnector) {
            setProtocolHandlerClassName("org.apache.coyote.ajp.AjpAprProtocol");
        } else {
            setProtocolHandlerClassName("org.apache.coyote.ajp.AjpNioProtocol");
        }
    }
    // 4. 其他情況,使用傳入的protocol作為protocolHandler的類名
    else {
        setProtocolHandlerClassName(protocol);
    }
}

在 Connector 的構(gòu)造方法中,我們發(fā)現(xiàn)它主要完成了以下幾項工作:

  • 當傳入的參數(shù)為空協(xié)議時,它會默認使用 HTTP/1.1 協(xié)議。
  • 當傳入的協(xié)議為 HTTP/1.1 或 null 時,它會選擇 org.apache.coyote.http11.Http11NioProtocol 作為 ProtocolHandler,并忽略 apr 選項。
  • 當傳入的協(xié)議為 AJP/1.3 時,它會選擇 org.apache.coyote.ajp.AjpNioProtocol 作為 ProtocolHandler,同樣忽略 apr 選項。
  • 對于其他情況,它會直接使用傳入的 protocol 作為 ProtocolHandler 的類名。
  • 最后,它會使用 ProtocolHandler 的類名來構(gòu)造 ProtocolHandler 的實例。

Connector.initInternal()

@Override
protected void initInternal() throws LifecycleException {
    super.initInternal();

    // Initialize adapter
    // 1. 初始化adapter
    adapter = new CoyoteAdapter(this);
    protocolHandler.setAdapter(adapter);

    // Make sure parseBodyMethodsSet has a default
    // 2. 設置接受body的method列表,默認為POST
    if (null == parseBodyMethodsSet) {
        setParseBodyMethods(getParseBodyMethods());
    }

    if (protocolHandler.isAprRequired() && !AprLifecycleListener.isAprAvailable()) {
        throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerNoApr",
                getProtocolHandlerClassName()));
    }
    if (AprLifecycleListener.isAprAvailable() && AprLifecycleListener.getUseOpenSSL() &&
            protocolHandler instanceof AbstractHttp11JsseProtocol) {
        AbstractHttp11JsseProtocol<?> jsseProtocolHandler =
                (AbstractHttp11JsseProtocol<?>) protocolHandler;
        if (jsseProtocolHandler.isSSLEnabled() &&
                jsseProtocolHandler.getSslImplementationName() == null) {
            // OpenSSL is compatible with the JSSE configuration, so use it if APR is available
            jsseProtocolHandler.setSslImplementationName(OpenSSLImplementation.class.getName());
        }
    }

    // 3. 初始化protocolHandler
    try {
        protocolHandler.init();
    } catch (Exception e) {
        throw new LifecycleException(
                sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
    }
}

Connector 的 init() 方法主要完成了三項重要的初始化工作:

  • 初始化 adapter:Adapter 負責將請求傳遞給 Container,因此需要在 init() 方法中完成初始化,以便后續(xù)能夠正常地將請求傳遞給 Container 進行處理。
  • 設置接受 body 的 method 列表:默認情況下,Connector 只允許 POST 方法提交 body 數(shù)據(jù),但在某些情況下,可能需要允許其他方法提交 body 數(shù)據(jù),因此需要在 init() 方法中設置允許提交 body 的方法列表。
  • 初始化 protocolHandler:ProtocolHandler 是 Connector 的核心組件,負責處理請求和響應,因此需要在 init() 方法中完成 protocolHandler 的初始化,以便后續(xù)能夠正常地處理請求和響應。

從 ProtocolHandler 的類繼承層級關系圖 中,我們可以看到 ProtocolHandler 的子類都必須實現(xiàn) AbstractProtocol 抽象類。而 protocolHandler.init(); 方法的具體實現(xiàn)則取決于具體的 ProtocolHandler 子類,它會根據(jù)不同的協(xié)議和通信方式進行相應的初始化操作。

代碼正是在這個抽象類里面。我們來分析一下。

@Override
public void init() throws Exception {
    if (getLog().isInfoEnabled()) {
        getLog().info(sm.getString("abstractProtocolHandler.init", getName()));
    }

    if (oname == null) {
        // Component not pre-registered so register it
        oname = createObjectName();
        if (oname != null) {
            Registry.getRegistry(null, null).registerComponent(this, oname, null);
        }
    }

    if (this.domain != null) {
        rgOname = new ObjectName(domain + ":type=GlobalRequestProcessor,name=" + getName());
        Registry.getRegistry(null, null).registerComponent(
                getHandler().getGlobal(), rgOname, null);
    }

    // 1. 設置endpoint的名字,默認為:http-nio-{port}
    String endpointName = getName();
    endpoint.setName(endpointName.substring(1, endpointName.length()-1));
    endpoint.setDomain(domain);

    // 2. 初始化endpoint
    endpoint.init();
}

接下來,讓我們一同探究 Endpoint.init() 方法的內(nèi)部。它位于 AbstractEndpoint 抽象類中,采用模板方法模式,巧妙地將核心邏輯委托給子類的 bind() 方法。

public abstract void bind() throws Exception;
public abstract void unbind() throws Exception;
public abstract void startInternal() throws Exception;
public abstract void stopInternal() throws Exception;

public void init() throws Exception {
    // 執(zhí)行bind()方法
    if (bindOnInit) {
        bind();
        bindState = BindState.BOUND_ON_INIT;
    }
    if (this.domain != null) {
        // Register endpoint (as ThreadPool - historical name)
        oname = new ObjectName(domain + ":type=ThreadPool,name=\"" + getName() + "\"");
        Registry.getRegistry(null, null).registerComponent(this, oname, null);

        ObjectName socketPropertiesOname = new ObjectName(domain +
                ":type=ThreadPool,name=\"" + getName() + "\",subType=SocketProperties");
        socketProperties.setObjectName(socketPropertiesOname);
        Registry.getRegistry(null, null).registerComponent(socketProperties, socketPropertiesOname, null);

        for (SSLHostConfig sslHostConfig : findSslHostConfigs()) {
            registerJmx(sslHostConfig);
        }
    }
}

繼續(xù)追尋著代碼的蹤跡,我們終于來到了 bind() 方法,它揭示了 Connector 初始化的精髓所在。關鍵的代碼片段 serverSock.socket().bind(addr, getAcceptCount());用于 將 ServerSocket 綁定到指定的 IP 地址和端口

@Override
public void bind() throws Exception {

    if (!getUseInheritedChannel()) {
        serverSock = ServerSocketChannel.open();
        socketProperties.setProperties(serverSock.socket());
        InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));
        //綁定ServerSocket到指定的IP和端口
        serverSock.socket().bind(addr,getAcceptCount());
    } else {
        // Retrieve the channel provided by the OS
        Channel ic = System.inheritedChannel();
        if (ic instanceof ServerSocketChannel) {
            serverSock = (ServerSocketChannel) ic;
        }
        if (serverSock == null) {
            throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited"));
        }
    }

    serverSock.configureBlocking(true); //mimic APR behavior

    // Initialize thread count defaults for acceptor, poller
    if (acceptorThreadCount == 0) {
        // FIXME: Doesn't seem to work that well with multiple accept threads
        acceptorThreadCount = 1;
    }
    if (pollerThreadCount <= 0) {
        //minimum one poller thread
        pollerThreadCount = 1;
    }
    setStopLatch(new CountDownLatch(pollerThreadCount));

    // Initialize SSL if needed
    initialiseSsl();

    selectorPool.open();
}

至此,我們已將 Connector 的 init() 方法剖析完畢,接下來,讓我們將目光轉(zhuǎn)向 start() 方法。start() 方法的核心邏輯,僅僅是簡潔的一行代碼:調(diào)用 ProtocolHandler.start() 方法,將 Connector 的啟動大任委托給 ProtocolHandler。

Connector.startInternal()

@Override
protected void startInternal() throws LifecycleException {

    // Validate settings before starting
    if (getPort() < 0) {
        throw new LifecycleException(sm.getString(
                "coyoteConnector.invalidPort", Integer.valueOf(getPort())));
    }

    setState(LifecycleState.STARTING);

    try {
        protocolHandler.start();
    } catch (Exception e) {
        throw new LifecycleException(
                sm.getString("coyoteConnector.protocolHandlerStartFailed"), e);
    }
}

現(xiàn)在,讓我們深入 ProtocolHandler.start() 方法,探索啟動過程中的關鍵步驟。它首先會調(diào)用 Endpoint.start() 方法,啟動 Endpoint,以便監(jiān)聽來自網(wǎng)絡的請求。接著,它會開啟異步超時線程,負責監(jiān)控異步請求的超時情況。該線程的執(zhí)行單元為 AsyncTimeout。

@Override
public void start() throws Exception {
    if (getLog().isInfoEnabled()) {
        getLog().info(sm.getString("abstractProtocolHandler.start", getName()));
    }

    // 1. 調(diào)用`Endpoint.start()`方法
    endpoint.start();

    // Start async timeout thread
    // 2. 開啟異步超時線程,線程執(zhí)行單元為`Asynctimeout`
    asyncTimeout = new AsyncTimeout();
    Thread timeoutThread = new Thread(asyncTimeout, getNameInternal() + "-AsyncTimeout");
    int priority = endpoint.getThreadPriority();
    if (priority < Thread.MIN_PRIORITY || priority > Thread.MAX_PRIORITY) {
        priority = Thread.NORM_PRIORITY;
    }
    timeoutThread.setPriority(priority);
    timeoutThread.setDaemon(true);
    timeoutThread.start();
}

現(xiàn)在,我們將注意力集中在 Endpoint.start() 方法,它負責啟動 Endpoint,為 Connector 迎接來自網(wǎng)絡的請求做好準備。

public final void start() throws Exception {
    // 1. `bind()`已經(jīng)在`init()`中分析過了
    if (bindState == BindState.UNBOUND) {
        bind();
        bindState = BindState.BOUND_ON_START;
    }
    startInternal();
}

@Override
public void startInternal() throws Exception {
    if (!running) {
        running = true;
        paused = false;

        processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                socketProperties.getProcessorCache());
        eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                        socketProperties.getEventCache());
        nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                socketProperties.getBufferPool());

        // Create worker collection
        // 2. 創(chuàng)建工作者線程池
        if ( getExecutor() == null ) {
            createExecutor();
        }

        // 3. 初始化連接latch,用于限制請求的并發(fā)量
        initializeConnectionLatch();

        // Start poller threads
        // 4. 開啟poller線程。poller用于對接受者線程生產(chǎn)的消息(或事件)進行處理,poller最終調(diào)用的是Handler的代碼
        pollers = new Poller[getPollerThreadCount()];
        for (int i=0; i<pollers.length; i++) {
            pollers[i] = new Poller();
            Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
            pollerThread.setPriority(threadPriority);
            pollerThread.setDaemon(true);
            pollerThread.start();
        }
        // 5. 開啟acceptor線程
        startAcceptorThreads();
    }
}

protected final void startAcceptorThreads() {
    int count = getAcceptorThreadCount();
    acceptors = new Acceptor[count];

    for (int i = 0; i < count; i++) {
        acceptors[i] = createAcceptor();
        String threadName = getName() + "-Acceptor-" + i;
        acceptors[i].setThreadName(threadName);
        Thread t = new Thread(acceptors[i], threadName);
        t.setPriority(getAcceptorThreadPriority());
        t.setDaemon(getDaemon());
        t.start();
    }
}

在 Endpoint.start() 方法中,我們首先會調(diào)用 bind() 方法,完成 Socket 的綁定,確保 Connector 能夠監(jiān)聽來自網(wǎng)絡的請求。接著,我們會創(chuàng)建工作者線程池,為后續(xù)處理請求提供充足的線程資源。隨后,我們會初始化連接 latch,用于限制請求的并發(fā)量,避免過多的請求涌入,造成系統(tǒng)崩潰。

接下來,我們會創(chuàng)建一個輪詢 Poller 線程,負責處理來自 Acceptor 線程的事件,并將處理后的事件傳遞給 Handler。Poller 線程會調(diào)用 Handler 的代碼進行處理,最終完成對請求的處理。最后,我們會創(chuàng)建一個 Acceptor 線程,專門負責監(jiān)聽網(wǎng)絡請求,并將接收到的請求傳遞給 Poller 線程進行處理。

至此,我們已將 Connector 源碼入口的分析告一段落,揭開了 Connector 啟動過程的神秘面紗。接下來我們將繼續(xù)深入探索 Connector 的請求邏輯,深入理解 Connector 如何接收請求,如何將請求封裝成 Request 和 Response 對象,以及如何將這些對象傳遞給 Container 進行處理。讓我們一起探索 Tomcat 的內(nèi)部世界。


責任編輯:武曉燕 來源: 碼上遇見你
相關推薦

2024-10-05 00:00:06

HTTP請求處理容器

2019-01-18 12:39:45

云計算PaaS公有云

2018-09-14 17:16:22

云計算軟件計算機網(wǎng)絡

2024-11-27 16:25:54

JVMJIT編譯機制

2024-11-18 17:31:27

2015-11-17 16:11:07

Code Review

2018-04-18 07:01:59

Docker容器虛擬機

2024-12-06 17:02:26

2020-07-02 15:32:23

Kubernetes容器架構(gòu)

2021-10-29 08:07:30

Java timeout Java 基礎

2023-11-14 16:14:49

2010-05-26 17:35:08

配置Xcode SVN

2024-05-15 14:29:45

2023-11-09 23:45:01

Pytorch目標檢測

2011-04-06 15:55:50

開發(fā)webOS程序webOS

2015-10-15 14:16:24

2024-04-10 07:48:41

搜索引擎場景

2024-11-28 10:35:47

2024-11-18 16:37:35

JMMJava內(nèi)存模型

2011-09-05 14:17:54

Sencha ToucMVC
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲精品一区中文字幕乱码 | 国产一区二区不卡 | 成年人免费网站 | 免费精品久久久久久中文字幕 | 亚洲一区在线日韩在线深爱 | 国产成人综合在线 | 日韩黄| 亚洲午夜av久久乱码 | av免费网站在线观看 | 欧美久久久久久 | 天天躁日日躁狠狠很躁 | 亚洲天堂影院 | 国产精品久久久久久久免费观看 | 91成人在线| 二区欧美 | 一级片免费视频 | 精品一区二区三区av | 亚洲网站在线观看 | 99re在线播放 | 日韩欧美在线视频播放 | 少妇一级淫片免费播放 | 999久久久久久久久6666 | 欧美激情一区二区三区 | 免费三级网站 | 日韩精品一区在线 | 日韩av一区二区在线观看 | 在线国产视频 | 欧美精品99 | 九九国产在线观看 | 欧美一区二区黄 | 精品国产乱码久久久久久闺蜜 | 伊人春色在线 | 国产视频在线观看一区二区三区 | 亚洲色片网站 | 欧美精品一区二区在线观看 | 91在线视频免费观看 | 日韩精品一区二区三区中文字幕 | 精品久久久久久18免费网站 | 亚洲欧美成人 | 国产欧美日韩综合精品一区二区 | 久久久久黄色 |