Tomcat源码:连接器与Executor、Connector

news2024/12/23 5:29:12

前文:

《Tomcat源码:启动类Bootstrap与Catalina的加载》

《Tomcat源码:容器的生命周期管理与事件监听》

《Tomcat源码:StandardServer与StandardService》

《Tomcat源码:Container接口》

《Tomcat源码:StandardEngine、StandardHost、StandardContext、StandardWrapper》

《Tomcat源码:Pipeline与Valve》

        写在开头:本文为个人学习笔记,内容比较随意,夹杂个人理解,如有错误,欢迎指正。

前言

        在前面得文章中,我们介绍了Tomcat中得容器是如何从service启动到具体得servlet包装类wrapper得。servlet容器启动后就可以为我们提供访问服务了吗?答案是否定得,因为servlet只规定了如何处理请求,但没有实现请求得分发,这个功能是由tomcat得另一部分连接器来完成得。

(图片来源《Tomcat连接器》,左侧为连接器,右侧为容器)

        连接器的启动点为connector组件,在前文《Tomcat源码:StandardServer与StandardService》中,我们介绍了StandardService的生命周期方法initInternal、startInternal,在启动了子容器engine后就会启动executor、mapperlistener以及connector。

        本文我们就来介绍下executor与connector。

#initInternal
engine.init() 
executor.init 
mapperListener.init()
connector.init()

#startInternal
engine.start();
executor.start();
mapperListener.start();
connector.start();

 

目录

前言

一、连接器

        连接器的作用

        连接器的组成

        ProtocolHandler 组件

        EndPoint

        Processor

        Adapter

二、Executor

        1、Executor组件介绍

         2、线程池与任务队列

        2.1、JUC中得ThreadPoolExecutor

        2.2、线程池ThreadPoolExecutor

        2.3、任务队列

        3、Executor参数

三、connector

        1、构造方法

        2、生命周期方法

        3、connector与Executor得关联

        4、connector参数


一、连接器

        连接器的作用

        Tomcat 要实现 2 个核心功能:

  • 处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化。
  • 加载和管理 Servlet,以及处理具体的 Request 请求。

        为此,Tomcat 设计了两个核心组件连接器(负责和外部通信)与容器(负责内部业务),容器我们之前得文章已经做了介绍,下面我们来介绍下连接器得内容。

       连接器的主要功能是:

  • 网络通信
  • 应用层协议解析
  • Tomcat Request/Response 与 ServletRequest/ServletResponse 的转化

        Tomcat 设计了 3 个组件来实现这 3 个功能,分别是 EndPointProcessor 和 Adapter

        组件间通过抽象接口交互。这样做的好处是封装变化。封装是面向对象设计的精髓,将系统中经常变化的部分和稳定的部分隔离,有助于增加复用性,并降低系统耦合度。网络通信的 I/O 模型是变化的,可能是非阻塞 I/O、异步 I/O 或者 APR。应用层协议也是变化的,可能是 HTTP、HTTPS、AJP。浏览器端发送的请求信息也是变化的。但是整体的处理逻辑是不变的,EndPoint 负责提供字节流给 Processor,Processor 负责提供 Tomcat Request 对象给 Adapter,Adapter 负责提供 ServletRequest 对象给容器

        如果要支持新的 I/O 方案、新的应用层协议,只需要实现相关的具体子类,上层通用的处理逻辑是不变的。由于 I/O 模型和应用层协议可以自由组合,比如 NIO + HTTP 或者 NIO2 + AJP。Tomcat 的设计者将网络通信和应用层协议解析放在一起考虑,设计了一个叫 ProtocolHandler 的接口来封装这两种变化点。各种协议和通信模型的组合有相应的具体实现类。比如:Http11NioProtocol 和 AjpNioProtocol。

        连接器的组成

        ProtocolHandler 组件

        连接器用 ProtocolHandler 接口来封装通信协议和 I/O 模型的差异ProtocolHandler 内部又分为 EndPoint 和 Processor 模块,EndPoint 负责底层 Socket 通信,Proccesor 负责应用层协议解析。

        EndPoint

        EndPoint 是通信端点,即通信监听的接口,是具体的 Socket 接收和发送处理器,是对传输层的抽象,因此 EndPoint 是用来实现 TCP/IP 协议的。

        EndPoint 是一个接口,对应的抽象实现类是 AbstractEndpoint,而 AbstractEndpoint 的具体子类,比如在 NioEndpoint 和 Nio2Endpoint 中,有两个重要的子组件:Acceptor 和 SocketProcessor。

        其中 Acceptor 用于监听 Socket 连接请求。SocketProcessor 用于处理接收到的 Socket 请求,它实现 Runnable 接口,在 Run 方法里调用协议处理组件 Processor 进行处理。为了提高处理能力,SocketProcessor 被提交到线程池来执行。而这个线程池叫作执行器(Executor)。

        Processor

        如果说 EndPoint 是用来实现 TCP/IP 协议的,那么 Processor 用来实现 HTTP 协议,Processor 接收来自 EndPoint 的 Socket,读取字节流解析成 Tomcat Request 和 Response 对象,并通过 Adapter 将其提交到容器处理,Processor 是对应用层协议的抽象。

        Processor 是一个接口,定义了请求的处理等方法。它的抽象实现类 AbstractProcessor 对一些协议共有的属性进行封装,没有对方法进行实现。具体的实现有 AJPProcessor、HTTP11Processor 等,这些具体实现类实现了特定协议的解析方法和请求处理方式。

        从图中我们看到,EndPoint 接收到 Socket 连接后,生成一个 SocketProcessor 任务提交到线程池去处理,SocketProcessor 的 Run 方法会调用 Processor 组件去解析应用层协议,Processor 通过解析生成 Request 对象后,会调用 Adapter 的 Service 方法。

        Adapter

        连接器通过适配器 Adapter 调用容器。由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat 定义了自己的 Request 类来适配这些请求信息。

        ProtocolHandler 接口负责解析请求并生成 Tomcat Request 类。但是这个 Request 对象不是标准的 ServletRequest,也就意味着,不能用 Tomcat Request 作为参数来调用容器。Tomcat 的解决方案是引入 CoyoteAdapter,这是适配器模式的经典运用,连接器调用 CoyoteAdapter 的 Sevice 方法,传入的是 Tomcat Request 对象,CoyoteAdapter 负责将 Tomcat Request 转成 ServletRequest,再调用容器的 Service 方法。

 

二、Executor

        1、Executor组件介绍

        注意:下文只会简略的介绍下tomcat中得线程池,不会太多涉及线程相关知识点。

        首先来看下Executor组件,这是一个tomcat内部自定义的线程池管理组件,作用是帮助connector连接器实现并发,读取的是server.xml中的Executor的配置。

<Service name="Catalina">
<!-- 1. 属性说明
	name:Service的名称
-->

    <!--2. 一个或多个excecutors --> 
    <!--
    <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
        maxThreads="150" minSpareThreads="4"/>
    -->

    <!--
		3.Connector元素:
			由Connector接口定义.<Connector>元素代表与客户程序实际交互的组件,它负责接收客户请求,以及向客户返回响应结果.
    -->
    <Connector port="80" maxHttpHeaderSize="8192"
               maxThreads="150" minSpareThreads="25" maxSpareThreads="75"
               enableLookups="false" redirectPort="8443" acceptCount="100"
               connectionTimeout="20000" disableUploadTimeout="true" />
</Service>  

        如果未显示的申明Executor的话StandardService则会创建一个默认对象。

    protected void initInternal() throws LifecycleException {
        // ...
         for (Executor executor : findExecutors()) {
            if (executor instanceof JmxEnabled) {
               ((JmxEnabled) executor).setDomain(getDomain());
              }
             executor.init();
         }
        // ...
    }

    protected void startInternal() throws LifecycleException {
        // ...
        synchronized (executors) {
            for (Executor executor : executors) {
                executor.start();
            }
        }
        // ...
    }

    public Executor[] findExecutors() {
        synchronized (executors) {
            return executors.toArray(new Executor[0]);
        }
    }

        Executor的实现类为StandardThreadExecutor,也继承了Lifecycle接口,因此同样具有生命周期方法。

       startInternal方法中会创建任务队列TaskQueue 与连接池ThreadPoolExecutor,并将连接池与任务队列关联起来。

    private TaskQueue taskqueue = null;
    protected ThreadPoolExecutor executor = null;

    protected void startInternal() throws LifecycleException {
        taskqueue = new TaskQueue(maxQueueSize);
        TaskThreadFactory tf = new TaskThreadFactory(namePrefix, daemon, getThreadPriority());
        executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,
                taskqueue, tf);
        executor.setThreadRenewalDelay(threadRenewalDelay);
        taskqueue.setParent(executor);
        setState(LifecycleState.STARTING);
    }

         StandardThreadExecutor通过execute方法调用ThreadPoolExecutor的execute方法来执行并发任务。

    public void execute(Runnable command) {
        if (executor != null) {
            executor.execute(command);
        } else {
            throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted"));
        }
    }

         2、线程池与任务队列

        首先来了解下线程池,由于线程的创建销毁都比较消耗资源,因此JDK采取了池化技术(类似数据库连接池),帮助我们在并发的环境下管理线程。

        2.1、JUC中得ThreadPoolExecutor

        JUC包中的ThreadPoolExecutor,其由一个线程集合workerSet和一个阻塞队列workQueue组成,workQueue用来临时存储接收到的任务,当workerSet有空闲线程时就会从workQueue中取出待执行的任务来执行。

        workerSet有2个核心变量corePoolSize与maximumPoolSize分别表示线程集合的初始线程数和最大线程数,workQueue会根据这两个值得大小判断是否接收线程,其运行逻辑如下:

  1. 当前线程数小于corePoolSize时,线程池为每个新任务创建线程。
  2. 线程数大于等于corePoolSize时,尝试将任务添加到任务队列中。
  3. 入队失败再尝试创建新得线程,如果总线程数达到 maximumPoolSize,执行拒绝策略。
private final BlockingQueue<Runnable> workQueue;

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    // 判断当前线程数是否小于核心线程数
    if (workerCountOf(c) < corePoolSize) {  
        // 创建新的线程,并启动里面的Thread
       if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 如果线程池处于RUNNING态,将任务放入队列成功
    if (isRunning(c) && workQueue.offer(command)) {
        // 其余内容
    }
    // 往线程池中创建新的线程,如果失败则拒绝任务
    else if (!addWorker(command, false))
        reject(command);
}

​

        这里得入队方法通过workQueue得offer方法实现,在tomcat得定制化线程池中就是有一块重要内容就是重写了offer方法。

        2.2、线程池ThreadPoolExecutor

        tomcat中用来实现线程池的类是ThreadPoolExecutor,类名与JUC中的线程池管理类ThreadPoolExecutor一致,但却是两个不同的类。

// Tomcat中的ThreadPoolExecutor
org.apache.tomcat.util.threads.ThreadPoolExecutor

// JUC中的ThreadPoolExecutor
java.util.concurrent.ThreadPoolExecutor

        tomcat得线程池中得对新线程得处理略有不同,我们直接来看源码,execute方法内部首先会使submittedCount变量加1来记录接收到得任务(任务执行完后会自动减1),然后调用executeInternal方法,而该方法得内容与JUC中得实现是一致得。

        接下来是最大得区别,那就是该execute方法会捕捉executeInternal抛出得RejectedExecutionException异常,然后尝试调用任务队列得force方法将任务强制加入到队列中,如果还是不行才拒绝该任务。

        原生实现中我们知道任务队列满员后才会创建新得线程,而从下文中catch块得处理逻辑可以推断当出现RejectedExecutionException异常时队列并不一定是满得,这实际上是通过重写任务队列得offer方法实现得。

    private final AtomicInteger submittedCount = new AtomicInteger(0);

    public void execute(Runnable command, long timeout, TimeUnit unit) {
        submittedCount.incrementAndGet();
        try {
            // executeInternal内容与JUC中线程池的execute的内容一致
            executeInternal(command);
        } catch (RejectedExecutionException rx) {
            // 如果总线程数达到 maximumPoolSize,抛出RejectedExecutionException异常
            if (getQueue() instanceof TaskQueue) {
                final TaskQueue queue = (TaskQueue) getQueue();
                try {
                    // 继续尝试把任务放到任务队列中去
                    if (!queue.force(command, timeout, unit)) {
                        submittedCount.decrementAndGet();
                        throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
                    }
                } catch (InterruptedException x) {
                    submittedCount.decrementAndGet();
                    // 如果缓冲队列也满了,插入失败,执行拒绝策略
                    throw new RejectedExecutionException(x);
                }
            } else {
                submittedCount.decrementAndGet();
                throw rx;
            }
        }
    }

    private void executeInternal(Runnable command) {
        if (command == null) {
            throw new NullPointerException();
        }
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true)) {
                return;
            }
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            // 其他内容
        }
        else if (!addWorker(command, false)) {
            reject(command);
        }
    }

        2.3、任务队列

        在StandardThreadExecutor中为线程池创建了任务队列,可以看到默认长度为Integer.MAX_VALUE。

    // StandardThreadExecutor.java
    protected int maxQueueSize = Integer.MAX_VALUE; 
    protected void startInternal() throws LifecycleException {
      // 其余代码
      taskqueue = new TaskQueue(maxQueueSize);
    }

   // TaskQueue.java
   public TaskQueue(int capacity) {
        super(capacity);
    }

        offer方法是任务队列得核心,原生实现中该方法是将任务直接入队,而这里会做具体得判断决定是入队还是创建新得线程。

        第一步是判断当前线程数是否已经达到了最大线程数,如果是得话那处理方案只能是入队。

        第二步判断已提交得任务数(上文中得submittedCount)与线程池得大小,即整个线程池接收到得但还未执行完得任务是否小于等于当前线程池中得线程数,如果小于得话说明有空闲线程,不用再创建,因此直接入队。

        第三步判断已提交得任务数是否小于最大线程数(注意上一步已经判断了和当前线程数得关系),如果小于则说明线程不够用了,此时选择创建新得线程。

    public boolean offer(Runnable o) {
        // 如果线程数已经到了最大值,不能创建新线程了,只能把任务添加到任务队列。
        if (parent.getPoolSizeNoLock() == parent.getMaximumPoolSize()) {
            return super.offer(o);
        }
        // 如果已提交的任务数小于等于当前线程数,表示还有空闲线程,无需创建新线程
        if (parent.getSubmittedCount() <= parent.getPoolSizeNoLock()) {
            return super.offer(o);
        }
        // 如果已提交的任务数大于当前线程数,线程不够用了,返回 false 去创建新线程
        if (parent.getPoolSizeNoLock() < parent.getMaximumPoolSize()) {
            return false;
        }
        // 默认情况下总是把任务添加到任务队列
        return super.offer(o);
    }

         而force则是调用父类方法,即直接入队。

    public boolean force(Runnable o) {
        if (parent == null || parent.isShutdown()) {
            throw new RejectedExecutionException(sm.getString("taskQueue.notRunning"));
        }
        return super.offer(o);
    }

         从上面的代码我们看到,如果当前线程数大于核心线程数、小于最大线程数,并且已提交的任务个数大于当前线程数时,也就是说线程不够用了,但是线程数又没达到极限,会去创建新的线程。

        与JUC中得实现方式相比,tomcat中得实现在面对线程数不足时更倾向于创建线程而非入队,同时由于StandardThreadExecutor继承了Lifecycle接口,因此也被纳入了生命周期管理。

        3、Executor参数

        经过了上文得介绍,我们再来看下Executor提供得可选配置参数

属性描述备注
className这个类必须实现org.apache.catalina.Executor接口。默认 org.apache.catalina.core.StandardThreadExecutor
name线程池名称。要求唯一, 供 Connector 元素的 executor 属性使用
namePrefix线程名称前缀。
maxThreads最大活跃线程数。默认 200
minSpareThreads最小活跃线程数。默认 25
maxIdleTime当前活跃线程大于 minSpareThreads 时,空闲线程关闭的等待最大时间。默认 60000ms
maxQueueSize线程池满情况下的请求排队大小。默认 Integer.MAX_VALUE

三、connector

        1、构造方法

        connector得构造方法会使用反射创建一个protocolHandler对象,默认实现类为Http11NioProtocol。protocolHandler相关内容我们会在后续介绍,这里只需要知道protocolHandler负责为connector实现具体得连接处理。

   protected String protocolHandlerClassName = "org.apache.coyote.http11.Http11NioProtocol"; 
   
    public Connector() {
        this(null);
    }

    public Connector(String protocol) {
        setProtocol(protocol);
        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;
        }
        // 其余代码
    }

        2、生命周期方法

        connector也同样继承了Lifecycle接口,因此也具有了生命周期方法。

        首先来看 initInternal,这里会创建一个CoyoteAdapter,该类负责将网络请求从连接器传递到容器中,然后关联CoyoteAdapter与protocolHandler,最后调用protocolHandler得init方法。

    protected void initInternal() throws LifecycleException {
        super.initInternal();
        adapter = new CoyoteAdapter(this);
        protocolHandler.setAdapter(adapter);
        // 其余代码
        try {
            protocolHandler.init();
        } catch (Exception e) {
            throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
        }
    }

         startInternal方法也很简单,调用protocolHandler得start方法就没了。

    protected void startInternal() throws LifecycleException {
        // 其余代码
        try {
            protocolHandler.start();
        } catch (Exception e) {
            throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerStartFailed"), e);
        }
    }

        3、connector与Executor得关联

        前文中我们介绍过,server.xml在解析时会创建一些监听规则,这里就有一个关于connector得规则ConnectorCreateRule。

    protected Digester createStartDigester(){
        //...
        digester.addRule("Server/Service/Connector",
                         new ConnectorCreateRule());
        //...
    }

         在ConnectorCreateRule中得begin方法调用setExecutor,会为Connector中得ProtocolHandler对象设置Executor,使得连接器中得组件能够使用我们上文创建得连接池。

    public void begin(String namespace, String name, Attributes attributes)
            throws Exception {
        Service svc = (Service)digester.peek();
        Executor ex = null;
        // 获取配置中得Executor 与Connector 
        if ( attributes.getValue("executor")!=null ) {
            ex = svc.getExecutor(attributes.getValue("executor"));
        }
        Connector con = new Connector(attributes.getValue("protocol"));
        if (ex != null) {
            // 调用setExecutor方法
            setExecutor(con, ex);
        }
        // ...
        digester.push(con);
    }

    private static void setExecutor(Connector con, Executor ex) throws Exception {
        // 获取Connector中得ProtocolHandler,调用其setExecutor方法
        Method m = IntrospectionUtils.findMethod(con.getProtocolHandler().getClass(),"setExecutor",new Class[] {java.util.concurrent.Executor.class});
        if (m!=null) {
            m.invoke(con.getProtocolHandler(), new Object[] {ex});
        }else {
            log.warn(sm.getString("connector.noSetExecutor", con));
        }
    }

        4、connector参数

        注意这里得maxConnections、acceptCount与Executor 中属性 maxQueueSize 的区别。

属性说明备注
acceptCount当最大请求连接 maxConnections 满时的最大排队大小默认 100,注意此属性和 Executor 中属性 maxQueueSize 的区别.这个指的是请求连接满时的堆栈大小,Executor 的 maxQueueSize 指的是处理线程满时的堆栈大小
connectionTimeout请求连接超时默认 60000ms
executor指定配置的线程池名称
keepAliveTimeoutkeeAlive 超时时间默认值为 connectionTimeout 配置值.-1 表示不超时
maxConnections最大连接数连接满时后续连接放入最大为 acceptCount 的队列中. 对 NIO 和 NIO2 连接,默认值为 10000;对 APR/native,默认值为 8192
maxThreads如果指定了 Executor, 此属性忽略;否则为 Connector 创建的内部线程池最大值默认 200
minSpareThreads如果指定了 Executor, 此属性忽略;否则为 Connector 创建线程池的最小活跃线程数默认 10
processorCache协议处理器缓存 Processor 对象的大小-1 表示不限制.当不使用 servlet3.0 的异步处理情况下: 如果配置 Executor,配置为 Executor 的 maxThreads;否则配置为 Connnector 的 maxThreads. 如果使用 Serlvet3.0 异步处理, 取 maxThreads 和 maxConnections 的最大值

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/514703.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

NHWC和NCHW数据排布及转换(模型部署)

1.概念 首先这是两种批量图片的数据存储方式&#xff0c;定义了一批图片在计算机存储空间内的数据存储layout。N表示这批图片的数量&#xff0c;C表示每张图片所包含的通道数&#xff0c;H表示这批图片的像素高度&#xff0c;W表示这批图片的像素宽度。其中C表示的通道数可能有…

被热议的DataOps,到底是什么?

近几年&#xff0c;DevOps的火热程度日渐高涨&#xff0c;同时涌现出了各种Ops&#xff0c;包括DevSecOps、GitOps、AIOps、NoOps、DataOps、MLOps、FeatureOps、ModelOps、FinOps等等。其中&#xff0c;对于企业来说&#xff0c;确保数据以高效和合规的方式使用&#xff0c;Da…

git commit 设置 eslint + pretter 格式化校验

系统版本 node 版本: v14.17.5 npm 版本: 6.14.14 vue-cli 版本: vue/cli 4.5.19 目录 系统版本 1. 新建一个 vue2.X 空项目 2. 安装插件 eslint ,并初始化 eslint 配置,根目录生成 .eslintrc 配置文件 3. 测试 eslint 配置 4. 安装 husky、lint-staged 5. 在package.j…

【Linux从入门到精通】了解冯诺依曼体系结构

本片文章会对冯诺依曼体系结构进行详解。同时&#xff0c;我们对冯诺依曼的理解&#xff0c;不能停留在概念上&#xff0c;要深入到对软件数据流理解上。本片文章同时也会对数据在冯诺依曼结构上的交互进行讲解。希望本篇文章会对你有所所帮助。 文章目录 一、简单认识冯诺依曼…

牛客网面试必刷:BM17 二分查找-I

牛客网面试必刷&#xff1a;BM17 二分查找-I 前言一、什么是二分查找&#xff1f;二、二分查找具体代码1.第一种写法&#xff1a;left < right2.第二种写法&#xff1a;left < right 三、复杂度分析 前言 二分查找是一个常见、基础、难度较低问题&#xff0c;本文记录了…

【JMeter入门】—— JMeter介绍

1、什么是JMeter Apache JMeter是Apache组织开发的基于Java的压力测试工具&#xff0c;用于对软件做压力测试。它最初被设计用于Web应用测试&#xff0c;但后来扩展到其他测试领域。 &#xff08;Apache JMeter是100%纯JAVA桌面应用程序&#xff09;Apache JMeter可以用于对静…

linux系统挂载硬盘

linux系统挂载硬盘 1、背景2、环境3、准备工作4、挂载分区4.1、查看分区信息4.2、创建分区4.3、设置分区格式4.4、创建挂载目录4.5、挂载分区4.6、设置开机自动挂载4.7、验证是否挂载成功 1、背景 日常使用过程中随着系统业务量的新增对磁盘的空间和性能提出了更高的要求&…

功能测试之设计语言测试:功能测试包含哪些测试?分别有什么作用

Web 设计语言版本的差异可以引起客户端或服务器端严重的问题&#xff0c;例如使用哪种版本的HTML 等。当在分布式环境中开发时&#xff0c;开发人员都不在一起&#xff0c;这个问题就显得尤为重要。除了HTML 的版本问题外&#xff0c;不同的脚本语言&#xff0c;例如Java、Java…

按照这6步学习测试,月薪不过万,我给你介绍测试工作

上周一刚入职不久&#xff0c;是在上海的一家软件公司&#xff0c;税前11K&#xff0c;五险一金&#xff0c;996的工作制&#xff0c;已经上班了一个月&#xff0c;说下自己的感受。 因为我专科毕业4年&#xff0c;之前一直在做电商运营&#xff0c;大专学的专业是电子商务&am…

Linux上Nacos基本使用:连接MySQL并修改密码、启动、停止命令等

Nacos如何连接MySQL并修改密码 说明如何将内嵌数据库Derby切换为MySQL数据库直接新建MySQL数据库: 必须是MySQL5.7及以上 如何修改密码启动、停止命令 说明 nacos默认&#xff1a; 使用内嵌的数据库&#xff08;Derby&#xff09;默认登录地址 ip:8848/nacos; 账号&#xff1…

Flutter组件——Getx入门01

前言 最近要正式开始写一个flutter项目了&#xff0c;我在浏览flutter如何进行框架设计的时候突然看到了一篇关于如何管理flutter状态的文章。flutter中的状态管理并不是很好理解&#xff0c;但是你需要在页面之间传值或者改变组件中的某个值的时候就必须更改状态。当我在这篇…

C生万物 | 字符串函数与内存函数解读【附英译中图解】

文章目录 求字符串长度一、strlen() 长度不受限制的字符串函数一、strcpy()二、strcat()三、strcmp() 长度受限制的字符串函数一、引入二、strncpy()三、strncat()四、strncmp() 字符串查找函数一、strstr()二、strtok() 错误信息报告函数一、strerror() 字符操作函数内存操作函…

从“能用”到“好用”:它的出现,解决你80%的转型困境【内含免费试用附教程】

免费试用地址&#xff1a;引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构引迈信息&#xff0c;提供快速开发平台、快速开发框架、低代码开发平台、低代码开发框架、0代码开发平台、0代码开发框架、零代码开发平台、零代码开发…

战略投资奥琦玮,微盟冲在餐饮复苏最前线

作者 | 辰纹 来源 | 洞见新研社 好起来了&#xff0c;一切都好起来了。 刚刚过去的五一假期&#xff0c;广州费大厨正佳广场店每天取号1000多桌&#xff0c;餐厅翻台率达到了1200%&#xff1b;长沙文和友单日最高排号超过1万&#xff0c;到店人数近6万&#xff1b;武汉主力龙…

OpenGL高级-帧缓冲

效果展示 知识点 颜色缓冲记录帧的颜色值&#xff0c;深度缓冲记录深度信息&#xff0c;模板缓冲允许我们基于一些条件丢弃指定片段。这几种缓冲结合起来叫做帧缓冲(FrameBuffer)&#xff0c;它被储存于内存中。  OpenGL给了我们自己定义帧缓冲的自由&#xff0c;我们可以选择…

Linux网络——shell脚本之正则表达式

Linux网络——shell脚本之正则表达式 一、概述二、基本的正则表达式三、实践操作1.匹配输出规定的电话号码2.匹配规定格式的邮箱 一、概述 正则表达式是对字符串操作的一种逻辑公式&#xff0c;就是用事先定义好的一些特定字符、及这些特定字符的组合&#xff0c;组成一个“规则…

实时聊天如何做,让客户眼前一亮(二)

让我们继续讨论一下如何利用SaleSmartly&#xff08;ss客服&#xff09;在网站中的实时聊天视图如何提供出色的实时聊天体验。 四、在实时聊天会话期间 让我们来看看我们可以确保尽可能的提高客户体验的各种方法&#xff0c;使用SaleSmartly&#xff08;ss客服&#xff09;时聊…

Magic-API的部署

目录 概述简介特性 搭建创建元数据表idea新建spring-boot项目pom.xmlapplication.properties打包上传MagicAPI-0.0.1-SNAPSHOT.jar开启服务访问 magic语法 概述 简介 magic-api是一个基于Java的接口快速开发框架&#xff0c;编写接口将通过magic-api提供的UI界面完成&#xf…

性能优化之Tomcat优化策略

一、优化策略 系统性能的衡量指标&#xff0c;主要是响应时间和吞吐量。 1&#xff09;响应时间&#xff1a;执行某个操作的耗时&#xff1b; 2) 吞吐量&#xff1a;系统在给定时间内能够支持的事务数量&#xff0c;单位为TPS&#xff08;Transactions PerSecond的缩写&…

WhatsApp App Vs WhatsApp API,哪一个更适合你?

WhatsApp在全球拥有超过20亿月度活跃用户&#xff0c;是一个深受欢迎、可靠和安全的跨平台信息服务&#xff0c;使其成为与朋友、家人、同事和客户通信的首选移动信息程序。使用WhatsApp聊天机器人使推销你的公司和获得新客户变得更简单。 一、让我们先来看看WhatsApp个人应用…