【Tomcat源码分析】从零开始理解 HTTP 请求处理 (第一篇)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
简介: 本文详细解析了Tomcat架构中复杂的`Connector`组件。作为客户端与服务器间沟通的桥梁,`Connector`负责接收请求、封装为`Request`和`Response`对象,并传递给`Container`处理。文章通过四个关键问题逐步剖析了`Connector`的工作原理,并深入探讨了其构造方法、`init()`与`start()`方法。通过分析`ProtocolHandler`、`Endpoint`等核心组件,揭示了`Connector`初始化及启动的全过程。本文适合希望深入了解Tomcat内部机制的读者。欢迎关注并点赞,持续更新中。如有问题,可搜索【码上遇见你】交流。

前言

终于步入 Connector 的解析阶段,这无疑是 Tomcat 架构中最为复杂的一环。作为连接器,它的职责显而易见——连接。那么,它连接的究竟是什么呢?

Connector 宛如一座桥梁,将来自客户端的请求,经过精心封装成 RequestResponse 对象,传递给 Container 进行处理。Container 完成业务逻辑后,Connector 再将处理后的结果,通过 Response 对象返回给远方的客户端。

要深入理解 Connector 的精髓,需要我们从四个关键问题出发,逐一探索。

  1. Connector 如何接收来自远方的请求?
  2. 如何将这呼唤化作 Request 和 Response 的身影?
  3. 封装后的 Request 和 Response 如何被递交给 Container 处理?
  4. Container 处理完毕后,如何将结果托付给 Connector,并最终送回客户端手中?

为了更好地理解 Connector 的内部运作,让我们先来欣赏一幅 Connector 结构图,它将帮助我们更直观地感受其内部的精妙设计。

【注意】:不同的协议和通信方式,将催生出不同的 ProtocolHandler 实现。在 Tomcat 8.5 版本中,ProtocolHandler 的类继承关系图谱如下:

针对这幅类继承层级图,我们可以做如下解读:

ajp 和 http11 代表着两种截然不同的协议,而 nio、nio2 和 apr 则分别代表着三种不同的通信方式。值得注意的是,协议与通信方式并非相互独立,它们可以灵活组合,以适应不同的场景需求。

ProtocolHandler 内部,包含着三个核心部件:Endpoint、Processor 和 Adapter,它们共同协作,完成请求的接收、处理和响应。

  • Endpoint 负责处理底层的 Socket 网络连接,它就像是一位网络守卫,负责迎接来自网络的呼唤,并将其转化为可供处理的 Socket 连接。Processor 则肩负着将 Endpoint 接收到的 Socket 封装成 Request 对象的重任,它就像一位翻译官,将网络语言转化为服务器可以理解的语言。Adapter 则充当着连接器,它将 Request 对象传递给 Container,以便 Container 进行具体的处理。
  • 由于 Endpoint 负责处理底层的 Socket 网络连接,因此它需要实现 TCP/IP 协议,而 Processor 则需要实现 HTTP 协议,以解析 HTTP 请求。Adapter 则将请求适配到 Servlet 容器,使其能够理解并处理来自外部的请求。
  • Endpoint 的抽象实现类 AbstractEndpoint 定义了 Acceptor、AsyncTimeout 两个内部类和一个 Handler 接口。Acceptor 负责监听来自网络的请求,一旦有新的请求到来,便会将其捕获。AsyncTimeout 则负责检查异步 Request 的超时,确保请求在合理的时间内得到处理。Handler 则负责处理接收到的 Socket,它将调用 Processor 进行处理,将 Socket 转换为 Request 对象,并最终传递给 Container。

至此,我们已经解开了 Connector 如何接收请求、如何将请求封装成 Request 和 Response,以及封装后的 Request 和 Response 如何被传递给 Container 进行处理这三个关键问题。而对于最后一个问题,即 Container 处理完后如何将结果返回给客户端,我们将在深入了解 Container 的运作机制后自然明了,前面章节已对此进行了详细的分析。

Connector 源码分析入口

在 Service 的标准实现 StandardService 的源码中,我们发现其 init()、start()、stop() 和 destroy() 方法分别会对 Connectors 的同名方法进行调用。值得注意的是,一个 Service 通常会对应多个 Connector,这意味着 Service 的生命周期管理会影响到所有与其关联的 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 实现了 Lifecycle 接口,这使得它成为一个拥有生命周期的组件。因此,Connector 的启动逻辑入口自然而然地落在 init() 和 start() 方法之中。

Connector 构造方法

在深入分析 Connector 的启动逻辑之前,不妨先来观摩一下 server.xml 文件。这份文件如同 Tomcat 架构的蓝图,清晰地展现了各个组件之间的联系和布局,为我们理解 Connector 的运作提供了一个宏观的视角。

<?xml version='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 文件中,我们发现 Connector 拥有多个关键属性,其中 portprotocol 尤为重要。默认情况下,server.xml 支持两种协议:HTTP/1.1 和 AJP/1.3。HTTP/1.1 用于支持传统的 HTTP 1.1 协议,而 AJP/1.3 则专门用于支持与 Apache 服务器的通信,为 Apache 服务器提供一个与 Tomcat 交互的桥梁。

现在,让我们将目光转向 Connector 的构造方法:

public Connector() {
   
    this(null); // 1. 无参构造方法,传入参数为空协议,会默认使用`HTTP/1.1`
}

public Connector(String protocol) {
   
    setProtocol(protocol);
    // Instantiate protocol handler
    // 5. 使用protocolHandler的类名构造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 的构造方法中,我们发现它主要完成了以下几项工作:

  • 当传入的参数为空协议时,它会默认使用 HTTP/1.1 协议。
  • 当传入的协议为 HTTP/1.1 或 null 时,它会选择 org.apache.coyote.http11.Http11NioProtocol 作为 ProtocolHandler,并忽略 apr 选项。
  • 当传入的协议为 AJP/1.3 时,它会选择 org.apache.coyote.ajp.AjpNioProtocol 作为 ProtocolHandler,同样忽略 apr 选项。
  • 对于其他情况,它会直接使用传入的 protocol 作为 ProtocolHandler 的类名。
  • 最后,它会使用 ProtocolHandler 的类名来构造 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() 方法中完成初始化,以便后续能够正常地将请求传递给 Container 进行处理。
  • 设置接受 body 的 method 列表:默认情况下,Connector 只允许 POST 方法提交 body 数据,但在某些情况下,可能需要允许其他方法提交 body 数据,因此需要在 init() 方法中设置允许提交 body 的方法列表。
  • 初始化 protocolHandler:ProtocolHandler 是 Connector 的核心组件,负责处理请求和响应,因此需要在 init() 方法中完成 protocolHandler 的初始化,以便后续能够正常地处理请求和响应。

ProtocolHandler 的类继承层级关系图 中,我们可以看到 ProtocolHandler 的子类都必须实现 AbstractProtocol 抽象类。而 protocolHandler.init(); 方法的具体实现则取决于具体的 ProtocolHandler 子类,它会根据不同的协议和通信方式进行相应的初始化操作。

代码正是在这个抽象类里面。我们来分析一下。

@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() 方法的内部。它位于 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 {
   
    // 执行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);
        }
    }
}

继续追寻着代码的踪迹,我们终于来到了 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() 方法剖析完毕,接下来,让我们将目光转向 start() 方法。start() 方法的核心逻辑,仅仅是简洁的一行代码:调用 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);
    }
}

现在,让我们深入 ProtocolHandler.start() 方法,探索启动过程中的关键步骤。它首先会调用 Endpoint.start() 方法,启动 Endpoint,以便监听来自网络的请求。接着,它会开启异步超时线程,负责监控异步请求的超时情况。该线程的执行单元为 AsyncTimeout

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

    // 1. 调用`Endpoint.start()`方法
    endpoint.start();

    // Start async timeout thread
    // 2. 开启异步超时线程,线程执行单元为`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();
}

现在,我们将注意力集中在 Endpoint.start() 方法,它负责启动 Endpoint,为 Connector 迎接来自网络的请求做好准备。

public final void start() throws Exception {
   
    // 1. `bind()`已经在`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. 创建工作者线程池
        if ( getExecutor() == null ) {
   
            createExecutor();
        }

        // 3. 初始化连接latch,用于限制请求的并发量
        initializeConnectionLatch();

        // Start poller threads
        // 4. 开启poller线程。poller用于对接受者线程生产的消息(或事件)进行处理,poller最终调用的是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() 方法中,我们首先会调用 bind() 方法,完成 Socket 的绑定,确保 Connector 能够监听来自网络的请求。接着,我们会创建工作者线程池,为后续处理请求提供充足的线程资源。随后,我们会初始化连接 latch,用于限制请求的并发量,避免过多的请求涌入,造成系统崩溃。

接下来,我们会创建一个轮询 Poller 线程,负责处理来自 Acceptor 线程的事件,并将处理后的事件传递给 Handler。Poller 线程会调用 Handler 的代码进行处理,最终完成对请求的处理。最后,我们会创建一个 Acceptor 线程,专门负责监听网络请求,并将接收到的请求传递给 Poller 线程进行处理。

至此,我们已将 Connector 源码入口的分析告一段落,揭开了 Connector 启动过程的神秘面纱。接下来我们将继续深入探索 Connector 的请求逻辑,深入理解 Connector 如何接收请求,如何将请求封装成 Request 和 Response 对象,以及如何将这些对象传递给 Container 进行处理。让我们一起探索 Tomcat 的内部世界。 欢迎点赞关注,持续更新。

如有问题,欢迎微信搜索【码上遇见你】。

好了,本章节到此告一段落。希望对你有所帮助,祝学习顺利。

相关文章
|
14天前
|
缓存 前端开发 API
|
19天前
|
数据采集 前端开发 算法
Python Requests 的高级使用技巧:应对复杂 HTTP 请求场景
本文介绍了如何使用 Python 的 `requests` 库应对复杂的 HTTP 请求场景,包括 Spider Trap(蜘蛛陷阱)、SESSION 访问限制和请求频率限制。通过代理、CSS 类链接数控制、多账号切换和限流算法等技术手段,提高爬虫的稳定性和效率,增强在反爬虫环境中的生存能力。文中提供了详细的代码示例,帮助读者掌握这些高级用法。
Python Requests 的高级使用技巧:应对复杂 HTTP 请求场景
|
1天前
|
监控 Java 应用服务中间件
Spring Boot整合Tomcat底层源码分析
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置和起步依赖等特性,大大简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是其与Tomcat的整合。
9 1
|
2天前
|
JSON API 数据格式
Python中获取HTTP请求响应体的详解
本文介绍了如何使用Python的`requests`和`urllib`库发送HTTP请求并处理响应体。`requests`库简化了HTTP请求过程,适合快速开发;`urllib`库则更为底层,适用于性能要求较高的场景。文章详细演示了发送GET请求、处理JSON响应等常见操作。
|
8天前
|
前端开发 JavaScript Java
如何捕获和处理HTTP GET请求的异常
如何捕获和处理HTTP GET请求的异常
|
10天前
|
开发者
HTTP 协议请求方法的发展历程
【10月更文挑战第21天】
|
10天前
|
安全
HTTP 协议的请求方法
【10月更文挑战第21天】
|
10天前
|
缓存 安全 前端开发
HTTP 协议的请求方法在实际应用中有哪些注意事项?
【10月更文挑战第29天】HTTP协议的请求方法在实际应用中需要根据具体的业务场景和需求,合理选择和使用,并注意各种方法的特点和限制,以确保网络通信的安全、高效和数据的一致性。
|
14天前
|
安全 API 数据安全/隐私保护
常见的HTTP请求方法
【10月更文挑战第25天】这些HTTP请求方法共同构成了客户端与服务器之间交互的基础,使得客户端能够根据不同的需求对服务器资源进行各种操作。在实际应用中,开发者需要根据具体的业务场景和资源的性质选择合适的请求方法来实现客户端与服务器之间的有效通信。
|
18天前
|
存储 安全 网络协议
HTTP 请求方法
【10月更文挑战第22天】HTTP 请求方法
32 2