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

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Tair(兼容Redis),内存型 2GB
简介: 本文详细解析了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 的内部世界。 欢迎点赞关注,持续更新。

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

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

相关文章
|
1月前
|
监控 Java 应用服务中间件
Spring Boot整合Tomcat底层源码分析
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置和起步依赖等特性,大大简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是其与Tomcat的整合。
56 1
|
15天前
|
JSON Java 数据格式
java操作http请求针对不同提交方式(application/json和application/x-www-form-urlencoded)
java操作http请求针对不同提交方式(application/json和application/x-www-form-urlencoded)
66 25
java操作http请求针对不同提交方式(application/json和application/x-www-form-urlencoded)
|
14天前
|
Web App开发 大数据 应用服务中间件
什么是 HTTP Range请求(范围请求)
HTTP Range 请求是一种非常有用的 HTTP 功能,允许客户端请求资源的特定部分,从而提高传输效率和用户体验。通过合理使用 Range 请求,可以实现断点续传、视频流播放和按需加载等功能。了解并掌握 HTTP Range 请求的工作原理和应用场景,对开发高效的网络应用至关重要。
54 15
|
17天前
|
数据采集 JSON 测试技术
Grequests,非常 Nice 的 Python 异步 HTTP 请求神器
在Python开发中,处理HTTP请求至关重要。`grequests`库基于`requests`,支持异步请求,通过`gevent`实现并发,提高性能。本文介绍了`grequests`的安装、基本与高级功能,如GET/POST请求、并发控制等,并探讨其在实际项目中的应用。
27 3
|
23天前
|
前端开发 UED 开发者
CSS Sprites和图标字体在网页图标加载优化中的应用。CSS Sprites通过合并多图标减少HTTP请求,提升加载速度
本文探讨了CSS Sprites和图标字体在网页图标加载优化中的应用。CSS Sprites通过合并多图标减少HTTP请求,提升加载速度;图标字体则以字体形式呈现图标,便于调整样式。文章分析了两者的优缺点及应用场景,并提供了应用技巧和注意事项,旨在帮助开发者提升页面性能,改善用户体验。
23 5
|
1月前
|
JSON API 数据格式
Python中获取HTTP请求响应体的详解
本文介绍了如何使用Python的`requests`和`urllib`库发送HTTP请求并处理响应体。`requests`库简化了HTTP请求过程,适合快速开发;`urllib`库则更为底层,适用于性能要求较高的场景。文章详细演示了发送GET请求、处理JSON响应等常见操作。
45 3
|
14天前
|
Web App开发 网络安全 数据安全/隐私保护
Lua中实现HTTP请求的User-Agent自定义
Lua中实现HTTP请求的User-Agent自定义
|
1月前
|
前端开发 JavaScript Java
如何捕获和处理HTTP GET请求的异常
如何捕获和处理HTTP GET请求的异常
|
1月前
|
开发者
HTTP 协议请求方法的发展历程
【10月更文挑战第21天】
|
1月前
|
安全
HTTP 协议的请求方法
【10月更文挑战第21天】
下一篇
DataWorks