JavaEE平台技术——预备知识(Web、Sevlet、Tomcat)

JavaEE平台技术——预备知识(Web、Sevlet、Tomcat)

  • 1. Web基础知识
  • 2. Servlet
  • 3. Tomcat并发原理

在这里插入图片描述

在这里插入图片描述

1. Web基础知识

🆒🆒上个CSDN我们讲的是JavaEE的这个渊源,实际上讲了两个小时的历史课,给大家梳理了一下,这二三十年来,JavaEE的规范,和Spring的规范如何是相生相克的。

我们在整个课程设计中间或这门课中间,会用到一些什么样的相关知识,有一部分的知识可能很简单,大家就当听听过去了,有一部分知识可能大家没有接触到,可能听不懂也没关系,知道我们会用到它,然后知道它用的目的是,为了什么之后的话,大家在课后或者用到它的时候,再去深入的去了解这些相关的预备知识。

🆒🆒引出HTTP
我们首先从最简单最基础的开始讲起啊,我们知道JavaEE的规范主要是用来做后台服务器系统的,后台的服务器系统基本上都是现在是HTTP协议的一统天下,基本上我们不会再用其它的协议去实现我们的后台的服务器系统。

🆒🆒引出统一资源定位符URL
在HTTP的这样的一个协议上头,它就是它依赖的是,我们的统一资源定位符(Uniform Resource Locator),大家都很熟悉了它其实是由这么几个部分构成。

🆒🆒资源定位符URL的构成

  1. 协议,我们主要用的是HTTP的协议,所以这个部分是HTTP或者HTTPS;
  2. 服务器的域名,那域名实际上对应的是IP,我们会用域名服务域名去查那个IP,所以IP是什么其实是没有关系的,这里看到的是一个DNS的域名,我们在用的时候其实我们,往往不会用DNS的域名,而是用我们自己的一个名字,DNS域名是到DNS服务器上去查,我们自己的名字其实就是到我们的服务器上,去查那个名字所对应的IP,所以后面会看到我们的程序,其实可以任意装在任何一个地方,只要我们给它提供了查名服务,用名字去查到这个IP,这个问题就能够解决,这是第二个部分。

DNS(Domain Name System)域名系统是互联网的一项基础设施技术。它的主要功能是将易于记忆的域名(例如www.example.com)转换为IP地址(例如192.0.2.1),以便在网络上定位和识别计算机服务和设备。DNS域名系统可以看作是互联网的电话簿,它通过将域名映射到相应的IP地址来帮助用户定位所需的网络资源。

在实际应用中,当您在浏览器中键入网址时,浏览器首先会向DNS服务器发送查询请求,以获取相应域名所对应的IP地址,然后才能建立连接并加载所需的网页内容。这个过程使得用户无需记住复杂的数字地址,而是能够使用更易于记忆的域名来访问网站。

  1. 端口号因为在同一台服务器上可以装多个应用,所以每个应用需要自己的一个端口号,你才会知道说你访问这个服务器的时候,是访问的是这个应用,而不是其它的应用,这是端口号。
  2. 后面的东西是路径,路径通常是用来在一个应用里头去标识,说这个部分是什么样的东西,这个路径其实它并不代表说在这台服务器上的,某个文件的目录,它就是用来去标识去区分,在我们的应用之间去区分说,这是应用的这半部分,还是这是应用的另外一个部分,比如说我们以后在用到路径的时候,通常是用来区分什么,区分模块,我们会有若干个模块,那就会用路径去区分这个模块,这是路径。
  3. 最后则是资源,就是我们真正提供的东西,这个不是一个文件,往往可能是我们在服务器上头的一段代码。

现在我们拿到应该网址,我们可以清楚的看到以下部分是什么!
在这里插入图片描述
我们会描述了,我们用什么样的协议,访问哪台服务器的哪个端口,在这台服务器的端口的应用里头,我们要访问哪一部分,以及这一部分的什么东西,构成了这样的一个URL。

在这里插入图片描述

🆒应该大家都熟悉,在web的服务中间访问的过程这样子,我们前端不一定是人,也可能是另外一台服务器,它通过一个URL的地址,发送到一台服务器上,它中间会有比较复杂的查名过程,所以它查到这个服务器的IP,然后把这个请求发到服务器上,然后服务器就根据端口,根据路径,然后根据资源就是后面的三个部分,去找到对应的东西,去执行它,得到一个结果,这个结果再返回给前端,然后前端去用这个结果,我们在图上看到是HTML,我们现在在这个过程里面,很多的时候是一段数据,就是发一个请求,向服务器去执行一个什么样的东西,然后服务器通过这个端口,知道是什么样的应用,通过后面的路径知道是应用的哪一块部分,通过资源知道说我在这块部分要执行什么,然后执行它得到一个结果,把这个结果再返回给前端,这就是这样的一个访问过程,我想这个应该大家都很熟悉。
在这里插入图片描述

🆒其中HTTP协议或者HTTPS协议,是这个里头最基本的部分,最上面一层应用层的协议,对吧,所以它这种协议是要基于底层的TCP/IP来实现的,所以它是一层一层的啊,网络的7层协议。

在这里插入图片描述

🆒我们这里讨论的,就是它这个http协议的特点在于说——它是一对的,一问一答发出一个请求,我们称之为HTTP的Request,然后它返回一个HTTP的Response,不问不答,如果我不发HTTP Request,它不会回来一个HTTP Response,而且我不会记录之前问过什么,所以它的每一个相关的内容,都应该是基于它的HTTP的Request,去返回HTTP的Response。

在这里插入图片描述

🆒🆒🆒HTTP之后我们会引出Request Response

当然我们在应用中间,我们会发现HTTP的协议的这个特性,其实挺挺恼火的,因为我们确实是需要知道它之前干了什么,但是HTTP协议本身来说,它只提供一问一答的这样一个机制,所以我们要靠其它的部分来完成这个东西。

🆒🆒HTTPS是在这个协议上做了加密, HTTP协议是明文的,所以我们如果说,想要对我们在网络上头传输的内容,无论是Request的内容,还是Response内容,进行加密的话,进行保密处理的话,都应该使用HTTPS的协议。

原来Google警告这么来的!

所以大家现在可以看到,基本上所有的应用都是用的,HTTPS不是HTTP,甚至极端情况下的比如像谷歌的浏览器,你用HTTP它要警告,你说这个服务器用的是HTTP是不安全的,你确定要不要用,你用才能继续访问它。

🆒虽然我们在课上一直是,用HTTP的协议来举例子,但真实在用的时候其实是用HTTPS,但是无论是HTTP还是HTTPS,对于我们的程序来说其实没差别的,它是一个网络传输层的问题,它是用了加密还是没有用加密的传输对于我们程序来说,我们其实是不关心,它是用HTTP还是HTTPS的。

🆒只是在配置上如果说,要用HTTPS的话,在Web服务器的配置上,是要做些很特别的配置,不管是HTTP还是HTTPS的协议,它都是HTTP的Request和Response的过程,所有的协议都是分为包头和正文的部分,就是Head和Body的部分,Head是控制信息

我们为什么要把这个协议的,控制信息拿出来讲——因为我们有很多重要的数据,都在控制信息里头

🆒控制信息其实我们在写程序的时候,我们也会人为的在Head里头 ,在头里头插入我们所需要的控制信息,我们在服务和服务之间调用的时候,我们也会在转发 HTTP协议时候,加入我们所需要的头的信息。

所以每年在做课程设计的时候,同学们在做微服务体系结构的时候,会犯错的一个主要的地方,就是这个协议的头没控制好,调进来的时候是ok的,然后再去调别人就不ok了,出现这种情况你首先就,应该怀疑说这个头有问题,你要看看它进来的头ok的头是什么,你再去调别的时候你应该保持一样的头,才能保证整个的调用是连贯的,所以头的格式大家应该都很清楚,它描述了请求的方法。

在这里插入图片描述

HTTP Request 部分
在这里插入图片描述

Head的大致论述
🆒我们后面讲HTTP有若干种请求的方法,也是跟我们的设计有关,因为我们后面会讲RestFul的API,其实它就利用 HTTP的请求的方法,去做 API的设计,然后是URL,然后是它的协议的版本号,然后是它的主机然后后面它的控制字符,我们在里头会要用到其中的一些控制字符,或者要去加一些控制字符,就是它的变量它的头里头所放的这些字段名和值,这是我们要用到的东西。

Body的大致论述
🆒Body里头放的就是内容,在Body里头通常放的,就是一个JSON的字串,一个JSON的内容放在里面,对于这个请求的方法,就是HTTP的Request方法,这个也应该知道,我们一共有OPTIONS GET,POST PUT DELETE,CONNECT和TRACE这些方法。

请求方法描述
OPTIONS返回服务器针对特定资源所支持的HTTP请求方法,
GET向特定的资源发出请求。请求体中无内容
POST向指定资源提交数据进行处理请求。数据被包含在请求体中。
PUT从客户端向服务器传送的数据取代指定的文档的内容。
DELETE请求服务器删除指定的页面。
CONNECTHTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
TRACE回显服务器收到的请求,主要用于测试或诊断。

🆒我们其中会用到的部分来说,主要是前面的5个,就是OPTIONS GET POST PUTDELETE这五种方法,这5种方法在RestFul API中间,它是有特别的定义的,就是我们会利用HTTP的请求,针对同一个URL的,不同的请求的方式,去做不同的语义,然后去设计说,这是用来去实现什么样特定的功能。

在这里插入图片描述

HTTP Response部分

在这里插入图片描述

🆒Response部分,跟Request的部分一样,仍然分为包头和正文

🆒包头依然是控制信息,所以我们在Response的包头部分,也会要插入一些,我们所要的东西。

🆒而Body的部分主要是JSON的数据,我们会以JSON的格式,把数据放到正文里头,放到Body的部分,这是HTTP的协议。

在这里插入图片描述

最后是它的状态码,状态码是在HTTP的,Response里头的字段,大家应该知道在头里头,它应该会返回它的状态码是什么。

🆒🆒这个状态码表示了什么?
表示了服务器端向客户端返回,说服务器端的你发的请求它的结果是什么

🆒🆒成功
我们最常见到的结果是200和201,200和210都是ok,说明服务器一切正常,你无论是一个请求,还是一个POST的请求, GET请求都是200,POST的请求的返回的ok是201,这是它的规范所决定的。

🆒🆒400系列小解读
其它的这些值就代表特别的含义,比如说我们说400开头的这些东西,通常是你发过来的东西有问题,比如说你发过来的 Request,它的正文的值我解释不出来,不符合我的格式,所以就会以400的方式:

  • 你发过来的请求要访问的这个程序,是需要登录的,但是你没有登录就会返回401
  • 你如果登录了你来访问它这个部分,如果你没有权限,它就会返回403;
  • 如果你要请求的 URL根本就是错的,在我这里头就不知道这是个什么东西,我就会给你返回404

所以400系列的状态,基本上是跟你的请求有关的,就是你请求有问题,说到底你请求有问题,就会返回400类的错误码,这些错误都是会用程序去完成的,比如说我们在解析它的一个Request的时候,发现它对不到我们的一个参数上,那就会返回400。

🆒🆒500系列小解读
500开头的错误,通常就是我们服务器本身有问题

  • 比如说你的服务器崩掉了,你就会返回500。
  • 比如说你的服务器阻塞了,就是我服务器没崩,但是我服务器已经响应不过来了,就会返回503。

所以400的系列的错误,是表示说Request本身有问题,500的错误是表示说Request,没问题,但是我服务器有问题,Response返回里头,值是没有意义的,就会返回500的错误,这是我们主要的状态码

🆒对于所有的状态码都是有定义的,就是我们除了这里所看到的,400和500的状态码以外,我们在这个基础上头,针对比如说200 201的状态码上头,我们还有新的错误码去定义,表示说更新的含义,但是状态码我们还是符合,HTTP协议所规定的状态码,在它这个语义上头去扩充,我们需要返回的更多的值,这是这个状态码。

在这里插入图片描述

2. Servlet

简单来个小导读
💻这个部分我们要讲的就是,关于Servlet,它是一个有着悠久历史,JavaEE的第一版的规范Servlet就出现了,然后到JavaEE的最后一版规范,它还在它!

💻为什么会这么长的时间,存在于JavaEE的规范中间,Spring中也有这个规范?

因为它是我们目前一个,非常重要的技术规范,也是重要基础的规范。在Spring的框架中间的,Servlet Stack的所有的技术,都是基于Servlet规范来实现的。

我们略微讲一下Servlet规范的定义是什么,以及它的实现机制是什么?

虽然我们今天已经很少有机会,去直接来写这部分的Servlet的代码,但是因为它是我们整个Servlet Stack的,一个最基础的东西,所以我们还是有必要去,了解它的实现的机理,这样的话会方便我们去理解,后面的东西是怎样,在这个机制上头去实现的,以及我们如何去调优我们的服务器,特别是Servlet Stack的这个调优,其实跟Servlet的机制是有密切的关系的。

💻Servlet这样的一个机制,我们在JavaEE的规范中间其实有提到,它是用来去做服务器的表现层的,最开始是出于这样的一个目的,它接收的是HTTP的Request

  • 根据这个HTTP的Request,它把服务器端分成了容器和组件两个部分。
  • 容器叫做Servlet容器,用规范去定义了这个容器该做什么。
  • 组件叫做Servlet,规范去定义了Servlet要做什么。

在这里插入图片描述

  • 当一个HTTP的Request过来的时候,它是发到容器;
  • 容器则会根据这个HTTP的,Request去运行Servlet,执行一些逻辑;
  • 这个逻辑就是用Java代码写的你爱做什么做什么,比如说我们这里看到的是访问数据库,然后得到数据,产生你的HTTP Response,里头要放的那些数据,给它送回来这样的一个机制。

💻💻我们要稍微深入讨论的是内部发生了什么?
这就是JavaEE的Servlet的规范里头所定义的东西
在这里插入图片描述

💻JavaEE的Servlet的规范里头定义了一系列的接口和一系列的类,我们这个里头这张图上头,只有第一个接口,是有写了包名的
在这里插入图片描述

💻其它的没写,不是说它没有包名,它的包名跟第一个都是一样的,全在javax.servlet的包里头,这个就是JavaEE的Servlet的规范,定义了一系列的接口和基类。

💻💻这一个接口里头定义了什么?
你要去实现一个Servlet,你首先要去实现一个叫做javax.servlet.Servlet的接口,这个接口里头有很多的方法,其实它最重要的一个方法,就是service方法,service方法有两个参数,一个是ServletRequest,一个是ServletResponse,就这张图上的这个东西,我要实现一个这样的对象,这个对象必须实现Servlet的接口,这个接口里头有service的方法,这个方法接收过来的是,HTTP的Request,返回的是HTTP的response,送过来的Request,返回去的response,作为接口的两个参数。
在这里插入图片描述

但它在这个接口上没有写HTTP两个字,原因是它在设定规范的时候,它想Servlet其实还可以在其它的网络协议上去实现,但事实30年过去了,Servlet也没有在其它的网络协议上去实现,但是它是想这么来做的。

在这里插入图片描述

💻这左右两个接口又有子接口,这个子接口才是HTTP协议的,HttpServletRequest,和HttpServletResponse,这就具体到我们真正的HTTP协议上了。
在这里插入图片描述

💻对于Servlet的接口来说,它有实现了基类,但是基类做了两层。
在这里插入图片描述

💻一个是GenericServlet,一个是HttpServlet,大家知道,这个HttpServlet就是用来去实现HTTP协议的Servlet,在这个基类中间大家可以看到,它实现了一系列的do的方法,这些do的方法全是两个参数,分别是HttpServletRequest,和HttpServletResponse,全是这两个参数,这两个参数是这个(ServletRequest和ServletResponse)的子类。

💻这张图就画的是我刚才所说的这样的一个事情,对于Servlet容器来说,它接到HTTP的Request以后,它把Request变成Request对象,然后创建了一个response的对象

💻然后把Request对象和Response对象,(Servlet是一个对象,实例化成一个对象)通过一个线程去调Servlet的service的方法, service的方法会根据请求的类型,去调doGet还是doPost,doPut还是doDelete,然后把传过来的 HTTP的Request对象和HTTP的response对象传给它,里头你自己去写代码,就这样的一个过程。

💻💻JavaEE规范的特征——就是它针对我们在JavaEE规范中间所定义的接口和基类。

你去继承或者实现这些接口和基类,在它的规范下去写代码,你就是符合它的规范的!!
符合它的规范有什么样的好处,Servlet它的容器是可以替换的,我们目前常见的Servlet的容器有两种,Tomcat、Jetty。

Tomcat是大家最熟悉的,我们一般在写程序的时候,可能优选就用Tomcat,用Tomcat以后,你会发现说有时候它特别慢,然后你就想说我能不能换一个容器,它的速度会更快一点,Jetty就是一个比Tomcat更加高效的容器,但是它用的比较少。

在这里插入图片描述
在这里插入图片描述

我们有一段演示的代码,如何在JavaEE规范上去写一个Servlet,当然这个代码的主要的作用,只是说给大家演示一下代码怎么写,然后大家去理解,Servlet的这样一个机制,我们今天基本上是不会去手写Servlet代码了,但是我觉得还是有必要,大家看一下那个代码怎么写的,在这里我们之所以给大家来讲,这个Servlet的代码,目的并不让大家去学习,如何基于JavaEE的Servlet的规范,来写代码,因为这个部分的技术其实已经过时了,今天我们是不会再来写Servlet代码的,主要的目的就是让大家去了解,Servlet的原理,因为它到今天依然是我们Servlet Stack的一个最基础的部分,这个代码非常的简单。

在这里插入图片描述
在这里插入图片描述

💻我们一共写了三个,Servlet第一个Servlet,是WelcomeServlet

package cn.edu.xmu.javaee.servlet;import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;public class WelcomeGetServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("text/html;charset=UTF-8");String firstName = req.getParameter("firstname");firstName = new String(firstName.getBytes("ISO-8859-1"),"UTF-8");PrintWriter out = resp.getWriter();out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");out.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");out.println("<html xmlns=\"http://www.w3.org/1999/xhtml\">");out.println("<head>");out.println("<title>处理get提交的数据</title>");out.println("</head>");out.println("<body>");out.println("<h1>欢迎使用Servlet</h1><br/>");out.println("提交的姓名是:"+firstName);out.println("</body>");out.println("</html>");out.close();}
}

💻我们可以看到就像我们前面在课上所说的,一个Servlet的代码,是需要实现一个特定的基类的,所以我们可以看到我们的,WelcomeServlet类,extends了HTTPServlet,就是我们前面所说的,实现了HTTP协议的Servlet基类。
在这里插入图片描述

💻在WelcomeServlet的,代码中间其实非常的简单,里头只写了一个方法,就是我们重写了它的doGet的方法,这意味着说我们会去响应它的get的请求。doGet的方法里头两个参数,大家可以看到HttpServletRequest,HttpServletResponse。
在这里插入图片描述

💻既没有从HttpServletRequest拿到任何的数据
在这里插入图片描述

HttpServletResponse中间,获得它的Writer,这个Writer的目的,就是向Http Response中间,写入数据,当然这我们写的数据是一串的HTML,这个HTML就是为了打印出来一个欢迎使用Servlet这样的一个欢迎语,所以把这一堆的,HTML的东西,通过out,把它输出到Http Response里头去,那它就会被送到前端浏览器,就会解析这一堆送过去的HTML,然后就把它呈现出来

在这里插入图片描述
💻💻第二个例子我们就稍微复杂一点,我们写了一个HTML,把一些数据传过去,Servlet的工程中HTML其实是,放在,另外一个目录里头的,我们把它叫做webapp目录,在这个目录底下,我们放了一个get.html的文件

<!DOCTYPE html>
<html lang="cn">
<head><meta charset="UTF-8"><title>HTTP Get 请求</title>
</head>
<body><form action = "get" method="get"><p><label>输入姓名,点击提交<br/> <input type="text" name="firstname"/><input type="submit" value="提交"/></label></p></form>
</body>
</html>

💻当然还有我们后面要讲的,post.html文件,所有的HTML文件,包括图片等等,静态文件,都是放在webapp的目录底下,我们看一下get.html,其实非常的简单,它里头就是一个form action,在 form action中间,我们主要是提供了一个input框,这个input框是要向前端,去提交一个你的姓名的

package cn.edu.xmu.javaee.servlet;import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;public class WelcomeGetServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("text/html;charset=UTF-8");String firstName = req.getParameter("firstname");firstName = new String(firstName.getBytes("ISO-8859-1"),"UTF-8");PrintWriter out = resp.getWriter();out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");out.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");out.println("<html xmlns=\"http://www.w3.org/1999/xhtml\">");out.println("<head>");out.println("<title>处理get提交的数据</title>");out.println("</head>");out.println("<body>");out.println("<h1>欢迎使用Servlet</h1><br/>");out.println("提交的姓名是:"+firstName);out.println("</body>");out.println("</html>");out.close();}
}

💻回到Servlet代码,我们回来看,WelcomeGetServlet,同样继承的是HttpServlet的,它也是个get请求,所以同样重载了这个doGet的方法,它从Http Resquest中间用它的getParameter的方法去获得了我们从前端传过来的值。

在这里插入图片描述

💻然后把这个值输出到了,Http Response里去,当然作为HTML输出,到Http Response里去,这就是一个非常简单的get,但是跟我们前面那个例子相比,它从Http Resquest中间去拿到了一些值。

在这里插入图片描述

package cn.edu.xmu.javaee.servlet;import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;public class WelcomePostServlet extends HttpServlet {@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("text/html;charset=UTF-8");req.setCharacterEncoding("utf-8");resp.setCharacterEncoding("utf-8");String firstName = req.getParameter("firstname");String bornPlace = req.getParameter("bornplace");PrintWriter out = resp.getWriter();out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");out.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");out.println("<html xmlns=\"http://www.w3.org/1999/xhtml\">");out.println("<head>");out.println("<title>处理get提交的数据</title>");out.println("</head>");out.println("<body>");out.println("<h1>欢迎使用Servlet</h1><br/>");out.println("提交的姓名是:"+firstName);out.println("<br/>提交的籍贯是:"+bornPlace);out.println("</body>");out.println("</html>");out.close();}
}

最后我们看一下,HTTP的post的这种请求,是怎样实现的,post通常是用来传更多的值,当然这个例子中间我们其实就传了一个,非常少的两个值,依然是依赖于 HTML,post.html,来把两个值把它送过来,对于Servlet的这一端来说,它依然还是一个HTTP的,Servlet,但是我们重载了doPost的方法,依然是两个参数HTTP的,Request,HTTP的response,无论是 get的请求,还是post的请求,其实它的值都放到了,HttpServletRequest里头,所以我们依然从HTTP的,Request里头去获得,我们从前端传过来的两个值,然后又把它写到了,Http Response去,然后把它送回前端。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HTTP Get 请求</title>
</head>
<body>
<form action = "post" method="post"><p><label>姓名: <input type="text" name="firstname"/><br/>籍贯:<input type="text" name="bornplace"/><input type="submit" value="提交"/></label></p>
</form>
</body>
</html>

在这里插入图片描述
我们看一下这些代码和HTML,是怎样能够合起来,被Servlet的容器知道说我有多少Servlet的它依赖的是一个叫做web.xml的配置文件,在webapp/WEB-INF的目录下,当然这个是约定在这个目录下,它才会去认web.xml文件,在 web.xml中间其实主要是两个部分。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"version="4.0"><display-name>Archetype Created Web Application</display-name><servlet><servlet-name>Welcome</servlet-name><!--包名+类型--><servlet-class>cn.edu.xmu.javaee.servlet.WelcomeServlet</servlet-class></servlet><servlet><servlet-name>WelcomeGet</servlet-name><!--包名+类型--><servlet-class>cn.edu.xmu.javaee.servlet.WelcomeGetServlet</servlet-class></servlet><servlet><servlet-name>WelcomePost</servlet-name><!--包名+类型--><servlet-class>cn.edu.xmu.javaee.servlet.WelcomePostServlet</servlet-class></servlet><servlet-mapping><servlet-name>Welcome</servlet-name><url-pattern>/Welcome</url-pattern></servlet-mapping><servlet-mapping><servlet-name>WelcomeGet</servlet-name><url-pattern>/get</url-pattern></servlet-mapping><servlet-mapping><servlet-name>WelcomePost</servlet-name><url-pattern>/post</url-pattern></servlet-mapping>
</web-app>

在这里插入图片描述

一个部分是Servlet的mapping,就是我们看到的下面这个部分,这个部分主要是把我们的请求,映射到对应的Servlet上头,所以我们可以看到,映射到了名字叫做Welcome的Servlet的上头。

在这里插入图片描述

一个部分都是关于Servlet的定义,我们可以看到它定义了三个Servlet,对应的就是我们说的Servlet层的名字,还有我们的代码中间的Servlet的类,所以这样的话Servlet的容器,才会根据请求去,从线程池里头去拿到线程去,执行对应的Servlet的类。

在这里插入图片描述

3. Tomcat并发原理

这样跑的一个过程,我们再要讨论一下它的细节,它是用多线程来跑的,我们做的是服务器端的一个应用,服务器端一个应用,同时都要响应很多的请求,就是有不同的客户端,会同时发过来,很多的HTTP的Request,针对每一个HTTP的Request,在Servlet的容器中间,都是以一个独立的线程去跑。
在这里插入图片描述

就我们这张图上其实有看到,它是用一个独立的线程去跑的,会起一个线程里头去调Servlet,对象的service的方法,Servlet的对象是唯一的,因为我们在代码中间,会写很多的Servlet的对象,然后这些Servlet的对象会被很多的请求来调。

如果说Servlet的对象不唯一的话,内存吃得太厉害,所有的对象就只有一个。

另外一个问题:Servlet的对象是不能有属性的,线程和进程的差异,线程是共享Java虚拟机的内存空间的,a线程和b线程,如果说它去用一个Servlet的对象,它们俩是同一个对象,它们俩的属性就是同一个属性,如果a线程改了b线程就会受影响,就会出现这样的问题为了避免这样的问题,简单的方法就是Servlet对象是没有属性的,没有属性你就不存在改的问题,它在不同的线程里头去调的话也不会打架。

在比如说service方法或者doGet方法中间的局域变量,局域变量是不会有问题的,在Java虚拟机里头,不同的线程是有线程栈的,不同的线程它的局域变量全是分开的,分别保存在自己的线程栈里头,这里就会带来另外一个调优的问题,如果当线程很多的时候,线程栈占的空间也会很大,所以你要适当调整线程栈的大小,才能容纳更多的线程,不是说你简单的把线程调到1万,它就能够跑的调到1万,线程栈不够了它也跑不了。
所以 线程与线程栈 是有对映关系的

为了避免这个问题,我们对象通通不写属性,凡是会有存在的线程冲突的这些对象,我们都不写属性这样的话它,线程就是安全了就没有问题。

每一个请求用一个线程去执行,这个线程里头会去调Servlet的service方法,其实在这个容器中间,在执行线程的时候,它是分成了5步,哪5步?

在这里插入图片描述


💻💻Read的重要性——读为什么还要单独写一个?

因为网络的特性,它并不是一个可靠的稳定的,你永远是一个匀速的在读一个东西,你的网络会受到各种各样的因素影响,比如说你从美国发一个请求过来,Request读起来可能从头读到到完整的读完,大家知道HTTP是应用层协议,底下的TCP/IP协议可能一个Request,会被分成了若干个TCP/IP的包,所以说你拿到第一个包到最后一个包完整读完,最后拼成一个HTTP的Request,可能是要不知道多少时间,完全是看对面发过来的有多远,网络的状况是怎么样,会不会丢包会不会重发都不知道,read的过程其实是完整的读完HTTP的Request的过程,这个是要时间的


💻💻Decode的重要性

读完以后读到的是一个Head和Body的这样的一个协议,然后它要转成一个HTTP的Request的对象(实现了一个接口的对象),所以它要做decode,就是把网络协议读到的数据,变成我们的一个Java的对象,这叫做decode。


💻💻Compute的重要性

decode的主要是指 Request的对象,拿到这个对象以后,创建一个Response对象一起传给了service的方法。

Service方法中间会去根据你的请求的,类型去调doGet还是doPost,还是doDelete等等,这就是我们说的compute过程,因为那个方法是你自己写的,就数据已经拿到了你去做逻辑,然后产生结果这个都是compute的过程。


💻💻Encode的重要性
拿到这个结果以后,你把它丢到 Response里头返回回去,它要把它变成Response的协议,那是encode的过程。


💻💻send的重要性
变成协议以后再传回给客户端,那是send的过程同样传回去的数据.


在TCP/IP上有可能如果比较大的话,可能会分成若干个包,然后一个包中间会有丢包,所以send的过程也是不可控的,整个这个过程,都是在一个线程中间去完成的,这样的话它用多线程的方式,就是多少个请求开一个线程,我们Servlet对象的不写属性大家相互不打架,所有的局域变量都在自己的线程栈里头,所有的读写decode encode,都各自在线程中间去处理,这样就能做到并发。

💻这样的话会有一定的问题,我们其实对于一台具体的物理机上,是没有办法无限制的增加线程的,为什么没办法无限制增加线程?

  • 我们已经说到每个线程要占一定的,内存空间,针对一台特定的物理机内存是有限的,你不可能无限制的1万 2万 3万, 4万的线程上去内存就没有了。
  • 第二个是你的计算资源是有限的,因为你任何一台物理机的,CPU就是那么多核,或者就是那么多个CPU,你中间这个计算过程就要靠CPU来完成。

如果你无限制的增加线程的话,我们在操作系统中间会讲到,说CPU会忙不过来, CPU就会要不断的分片轮转,然后CPU就会把大量的时间,花在这个轮转上头,其实它并不是一个高效的方式,我们会根据一台物理机器的,实际状况它有多少内存,它的CPU的计算能力,再结合我们任务的这样一个情况,去制定一个线程池。

💻 线程池是什么?

  • 就是我们最多允许你跑多少个线程。
  • 如果多于这个线程的话怎么办,就让它去等待。
  • 如果在多于等待数的时候我就拒绝,这就是线程池的概念。

有了这个线程池以后,我们实际上是预先创建了若干个线程,放到线程池里头,因为孵化线程是需要消耗时间的,要用代码来消耗时间的,比如说内存中间开一些空间等等,所以我们把线程事先孵化出来,把它放到线程池里头,当请求过来的时候,我们是从线程池里头,拿空闲的线程,拿出来直接去用的,这样可以加快它的响应时间,而且可以管理整个服务器上,对于内存和CPU的消耗,不会出现说它的请求多了,它的内存不够,它的服务器崩溃的这样的一种情况,因为它用线程池卡住了它的上限。

💻其实我们现在如果在做是Servlet Stack,这一条技术线的时候,我们始终会关心这么4个参数

#最大工作线程数、默认200
server.tomcat.max-threads = 200
#最大连接数默认是10000
server.tomcat.max-connections = 10000
#等待队列长度,默认100
server.tomcat.accept-count = 100
#最小工作空闲线程数,默认10
server.tomcat.min-spare-threads = 100
  • max-threads,这个就是线程池的上限,这个就是因为你的服务器的特性,你的物理性能决定的你有多少内存,你的CPU计算能力是多少,所以你要定一个上限,上限不能超过你的物理资源所提供的能力,
  • accept-count,这个是排队队列的长度,对如果说发来的请求,超过了你的处理的上限的话,我还可以让它排队等待不是直接拒绝它,我现在定的是100,就意味着说上限是200,它最多可以同时处理多少,300个请求,其中200是在处理的,100是在等待的,如果超过300以后就会怎么样,就会拒绝,就是我们刚才看到的503, server unavailable,就直接就拒绝了,发挥503的错误码回去,这是根据你的服务器的能力来决定的,
  • max-connections,最大连接数通常我们不会动,因为它默认值是1万,我们现在基本上,我们的服务器很难达到,1万的这样的一个东西,1万是代表什么意思,是max-threads加上accept-count,这些连接全部都在的,就是你正在处理的最大数,和你让它等待的最大数,加起来就是它最大连接数,所以我们现在是300,它默认值是1万还远的了,所以一般来说我们不会去动它
  • min-spare-threads,这个是在说线程池的问题,当然我们可以说线程池有200,我们一起来就占200,占200你觉得有点浪费了,有些时候因为占了200以后,它其实会占一定的内存空间,但其实很多时候我们其实还没有到200,所以我们会定一个min-spare-threads,就是线程在启动的时候,它只有100个线程在里头,如果说超过了100个线程,它就开始孵化线程放进去,上限是200。然后如果说它的压力下来了,就是没有200个请求慢慢下来了,它又会把孵化的线程给它释放掉,释放出我们物理机的一些内存空间,下限又会降到慢慢最后降到100,少于100请求以后,它就一直保持100个线程,线程在里头少于100个线程,它也不会再去释放线程池里的线程了,所以这是一个下限。

这4个参数是我们之后会特别是在用Tomcat时候,会频繁去调的参数,这个参数其实不是一个恒定的值,这个值其实除了跟你的服务器的物理的性能有关以外,还跟你的应用有关,你的应用到底是io密集型的,还是计算密集型的。

如果说你的应用,是io密集型的意味着什么,因为这在read和send的,过程中间是需要消耗尽量多的时间的,计算的时间其实是比较少的,意味着如果你内存足够大的话,你可以尽量多开线程,就是max-threads可以开很高,开1000 ,2000都没问题,因为对CPU压力不大,只要内存够你就尽量的开到顶。

如果说你是计算型的,就是你主要会消耗它的CPU的资源,意味着说CPU就是你的上限,你不能说内存足够,你就开很多的线程那个没用,因为CPU只算那么快,你开到1000 2000它也算不过来。

💻💻所以到底会开多少线程数,你其实要去分析说你的应用,到底是io密集型的,还是计算密集型的,去综合决定说,你的线程数是一个什么样的值,这个是多线程管理。

在这里插入图片描述

💻什么是NIO的模式和APR的模式

它们都是异步io的模式,差别在于NIO用的是Java来实现的,APR用的是原生库,大家知道Java的字节码的运行效率是低于原生库的,APR利用的是阿帕奇的,HTTP的原生库,来实现的异步io 这个就,意味着说你不能单纯的,装一个Tomcat就好了,你还需要先装阿帕奇的原生库,再装Tomcat才能跑出它的APR模式,所以它装起来会稍微麻烦一点,NIO的话还是基于Java来做的,所以说就不需要去装原生库,我们现在的新的版本的,默认方式是NIO。

在这里插入图片描述
Compute

BIO和NIO的差别

所谓"BIO"即我们前文所述的,指的是在线程中进行的I/O操作。因此,在其内部代码实现过程中,正如我们刚才所观察到的那个过程,客户端发出请求,在其Servlet容器的代码中,首先是一个名为"Acceptor"的对象。
在这里插入图片描述
该"Acceptor"对象接收到客户端的请求后,随即创建一个"SocketProcessor"。
在这里插入图片描述

大家应该对这个名字有所了解,它与网络通信有关,它打开了一个Socket,并将此对象传递给从线程池中获取的一个线程。
在这里插入图片描述

正如我们所提到的,我们使用线程池来控制线程数量的上限和下限。因此,它从线程池中取出一个空闲线程,并将"SocketProcessor"交给该线程,然后开始读取网络数据。

整个读取过程都在线程范围内完成。如果读取速度很慢,它将一直占用线程。当读取完成后,线程才会调用Servlet对象的service方法,然后根据请求类型调用其doDelete、doPut或doGet方法。读取完成后,它会生成一个HTTP响应,并依靠"SocketProcessor"将HTTP响应传回给前端。

无论是产生HTTP请求还是获取HTTP响应,整个过程都是在线程中进行的。直到最后完成并将其放回线程池,供下一个请求使用。因此,它被称为阻塞式I/O方式,也就是所谓的"Blocked IO"。之所以如此命名,是因为在线程的I/O过程中,该线程一直被占用。这是传统模式,默认情况下仍采用这种模式。

在这里插入图片描述
在这里插入图片描述

这种模式与传统模式的不同之处在于,I/O不在线程范围内。它是如何实现的呢?

前面提到,请求仍然由一个"Acceptor"处理,当请求到达时,也仍然是一个"Acceptor"。然而,在这种情况下,它不是创建一个"SocketProcessor",而是创建一个名为"NioChannel"的对象。
在这里插入图片描述

这是什么呢?它同样能够接收来自客户端的数据,并将其放入缓冲区中。关键是,当传输完成后,"NioChannel"将产生一个事件,表示数据已经读取完成。

因此,它会将"NioChannel"放入一个事件列表或队列中。无论前面有多少个请求过来,它都会为每一个请求产生一个"NioChannel",并将其放入列表中。此时与线程无关,它只是将其放入列表中。

然后,使用一个独立的"Poller"线程来轮询列表。这个轮询的作用是查看哪个通道已经读取完成。

在这里插入图片描述

一旦某个通道读取完成,后续操作与我们之前所描述的"BIO"相同。它仍然会创建一个"SocketProcessor",将读取完成的数据交给"SocketProcessor",然后将其交给一个线程。接下来的步骤与之前相同。

在这里插入图片描述

首先是读取过程。当线程获得"SocketProcessor"时,将数据从读取状态转换为HTTP请求时,此时没有延迟,因为数据已经准备好并放入缓冲区,所以可以直接进行解码,并将其转换为HTTP请求对象。然后执行后续操作,如调用service方法,根据请求类型调用doGet、doPut或doDelete方法。在获得HTTP响应后,它仍然会将其返回给"SocketProcessor",然后将其传回"NioChannel"。线程就完成了其使命,然后再次回到线程池中。

"NioChannel"会负责将数据传回给前端。整个I/O过程都发生在这个过程中,而不是在线程的范围内。线程中只有我们前面提到的部分,或者是这个部分,或者是那个部分,具体取决于实现方式。总之,前面的读取和后面的发送都不在该线程的范围内。
在这里插入图片描述
Compute

进入NIO时代或者APR时代。

APR也是异步的,只不过与NIO的异步方式不同。它是使用原生代码来实现的,因此速度更快,I/O速度也会更快。进入这个时代之后,当我们考虑线程池时,我们只考虑其中间部分的计算消耗。无论是在Tomcat的NIO还是APR上,我们实际上无法进行优化,因为当您进行数据库读写时,I/O会阻塞。

对于计算部分来说,它仍然会阻塞。如果想要进一步提高效率,我们只能放弃Servlet这种架构,转而使用Reactive Stack,完全采用函数式编程。通过将与数据库交互的这些慢速操作从代码中分离出来,您才有可能将这些慢速操作转换为事件响应方式。

将非慢速操作转换为可以直接在CPU上调度的方式,才能进一步提高性能。

技术的发展已经很明显,它是一步一步地将代码中与I/O相关的部分分离出来,无论是网络I/O还是本地I/O部分。只有在将这些部分从代码中分离出来之后,它才可能在多线程上进行更进一步的优化。如果不进行分离,实际上是不可能的。就目前而言,对于命令式编程来说,最优化的方式就是采用APR。其中实际上还包括了一些本地数据库的I/O操作。由于命令式编程,即Servlet堆栈,并未将这部分代码分离出来。

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

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

相关文章

计算机组成与结构-计算机体系结构

计算机体系结构 指令系统 Flynn分类法 SISD&#xff08;单指令流单数据流&#xff09; 结构 控制部分&#xff1a;一个处理器&#xff1a;一个主存模块&#xff1a;一个 代表 单处理器系统 SIMD&#xff08;单指令流多数据流&#xff09; 结构 控制部分&#xff1a;一个处理…

C++ 算法:区间和的个数

涉及知识点 归并排序 题目 给你一个整数数组 nums 以及两个整数 lower 和 upper 。求数组中&#xff0c;值位于范围 [lower, upper] &#xff08;包含 lower 和 upper&#xff09;之内的 区间和的个数 。 区间和 S(i, j) 表示在 nums 中&#xff0c;位置从 i 到 j 的元素之和…

基于单片机设计的电子柜锁

一、前言 随着现代社会的不断发展&#xff0c;电子柜锁的应用越来越广泛。传统的机械柜锁存在一些不便之处&#xff0c;例如钥匙容易丢失、密码容易泄露等问题。设计一款基于单片机的电子柜锁系统成为了一个有趣而有意义的项目。 该电子柜锁系统通过电磁锁作为柜锁的开关&…

【JMeter参数化】上一个接口返回作为下一个接口入参

前言: 实际工作场景当中,比如获取到商品列表,并查看商品详情。如果将商品id写死,就很笨拙。所以我们可以进行参数化动态去更新商品id 目录 【同一个线程组内的】 场景1:接口A仅取一个值,作为接口B的入参 场景:接口A是获取教师列表中某个教室的id,接口B是查看该教师的详…

建筑能源管理(8)——合同能源管理

1、简介 合同能源管理是20世纪70年代中期在发达国家逐步发展起来的一种节能服务机制在国外简称EPC(Energy Performance Contracting)&#xff0c;在国内广泛地被称为EMC (Energy Management Contracting)&#xff0c;它由专门的节能服务公司(Energy Service Company,ESCO)在为…

STM32F4X SDIO(六) 例程讲解-SD_PowerON

STM32F4X SDIO&#xff08;六&#xff09; 例程讲解-SD_PowerON 例程讲解-SD_PowerONSDIO引脚初始化和时钟初始化SDIO初始化(单线模式)CMD0:GO_IDLE_STATE命令发送程序命令响应程序 CMD8:SEND_IF_CONDCMD8参数命令发送程序命令响应程序 CMD55:APP_CMDCMD55命令参数命令发送命令…

【Acwing170】加成序列(dfs+迭代加深+剪枝)题解和一点感想

本思路来自acwing算法提高课 题目描述 看本文需要准备的知识 1.dfs算法基本思想 2.对剪枝这个词有个简单的认识 迭代加深思想和此题分析 首先&#xff0c;什么是迭代加深呢&#xff1f;当一个问题的解有很大概率出现在递归树很浅的层&#xff0c;但是这个问题的解本身存在…

音视频开发:音频编码原理+采集+编码实战

原理&#xff1a; 消除冗余信息&#xff0c;压缩量最大&#xff0c;也叫有损压缩 剔除人耳听觉范围外的音频信号20Hz以下和20000Hz以上&#xff1b;去除被掩蔽的音频信号&#xff0c;信号的遮蔽可以分为频域遮蔽和时域遮蔽&#xff1b;频域遮蔽效应 屏蔽70分贝以下&#xff0…

汽车标定技术(一):XCP概述

目录 1.汽车标定概述 2.XCP协议由来及版本介绍 3.XCP技术通览 3.1 XCP上下机通信模型 3.2 XCP指令集 3.2.1 XCP帧结构定义 3.2.2 标准指令集 3.2.3 标定指令集 3.2.4 页切换指令集 3.2.5 数据采集指令集 3.2.6 刷写指令集 3.3 ECU描述文件(A2L)概述 3.3.1 标定上位…

有方N58 HTTP POST 请求连接 TDengine

串口调试软件&#xff1a;格西调试精灵 第一步先注册网络获取IP地址 建立PPP连接 ATXIIC1\r PPP链路建立成功&#xff0c;查询IP地址 ATXIIC?\r 设置网络APN ATCREG?\r 运行结果&#xff0c;红线处是获…

算法:Java构建二叉树并迭代实现二叉树的前序、中序、后序遍历

先自定义一下二叉树的类&#xff1a; // Definition for a binary tree node. public class TreeNode {int val;TreeNode left;TreeNode right;TreeNode() {}TreeNode(int val) { this.val val; }TreeNode(int val, TreeNode left, TreeNode right) {this.val val;this.left…

MongoDB安装及开发系例全教程

一、系列文章目录 一、MongoDB安装教程—官方原版 二、MongoDB 使用教程(配置、管理、监控)_linux mongodb 监控 三、MongoDB 基于角色的访问控制 四、MongoDB用户管理 五、MongoDB基础知识详解 六、MongoDB—Indexs 七、MongoDB事务详解 八、MongoDB分片教程 九、Mo…

ATE新能源汽车充电桩自动负载测试系统

随着新能源汽车的普及&#xff0c;充电桩的需求也在不断增加&#xff0c;为了确保充电桩的性能和安全性&#xff0c;对其进行负载测试是非常重要的。ATE新能源汽车充电桩自动负载测试系统是一种专门用于检测充电桩性能的设备&#xff0c;它可以模拟各种实际使用场景&#xff0c…

6、QtCharts 悬浮曲线效果

文章目录 效果dialog.hdialog.cpp悬浮槽函数 效果 dialog.h #ifndef DIALOG_H #define DIALOG_H#include <QDialog> #include <QtCharts> #include <QLineSeries> #include <QGraphicsScene> #include <QTimer> #include <QSplineSeries>…

《实战:如何搭建一个完整的 Vue2.0 项目》- 7、Vue2.x 项目 webpack 4 升级 5(半自动升级)

1.自动升级 先全局安装升级插件 npm i npm-check npm-check-updates -g检查依赖 npm-check更新检查后的依赖并展示版本号&#xff0c;此时 package.json还没有更新 npm-check-updates升级 package.json&#xff0c;下图显示更新版本&#xff0c;此时 package.json文件已变更…

FFmpeg——使用Canvas录制视频尚存问题的解决方案

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1f4c3;个人状态&#xff1a; 研发工程师&#xff0c;现效力于中国工业软件事业 &#x1f680;人生格言&#xff1a; 积跬步…

编译正点原子LINUXB报错make: arm-linux-gnueabihf-gcc:命令未找到

编译正点原子LINUX报错make: arm-linux-gnueabihf-gcc&#xff1a;命令未找到 1.报错内容2.解决办法3./bin/sh: 1: lzop: not found4.编译成功 1.报错内容 make: arm-linux-gnueabihf-gcc&#xff1a;命令未找到CHK include/config/kernel.releaseCHK include/generat…

Linux 上的轻量级浏览器

导读大多数 Linux 桌面环境中包含的基本图像查看器可能不足以满足你的需要。如果你想要一些更多的功能&#xff0c;但仍然希望它是轻量级的&#xff0c;那么看看这四个 Linux 桌面中的图像查看器&#xff0c;如果还不能满足你的需要&#xff0c;还有额外的选择。 当你需要的不…

本地部署Jellyfin影音服务器并实现远程访问影音库

文章目录 1. 前言2. Jellyfin服务网站搭建2.1. Jellyfin下载和安装2.2. Jellyfin网页测试 3.本地网页发布3.1 cpolar的安装和注册3.2 Cpolar云端设置3.3 Cpolar本地设置 4.公网访问测试5. 结语 1. 前言 随着移动智能设备的普及&#xff0c;各种各样的使用需求也被开发出来&…