WEB开发介绍

WEB资源分类(理解)

什么是web

​ WEB,在英语中web即表示网页的意思,它用于表示Internet主机(服务器)上供外界访问的资源

WEB资源分类

静态资源

  • web页面中供人们浏览的数据始终是不变 (eg: html,css,js、音视频)

动态资源

  • 指web页面中供人们浏览的数据是由程序产生的,不同的用户或者不同时间点访问web页面看到的内容各不相同。(eg: servlet,jsp)

小结

  1. 什么是WEB资源:放在服务器上供客户端访问的资源
  2. WEB资源的分类:
    1. 静态资源:WEB页面中共用户访问的数据始终是不变的,比如说:html、css、js、图片、音视频等等
    2. 动态资源:WEB页面中供用户访问的数据是由程序产生的,是会发生变化的,比如Servlet、jsp

软件架构(理解)

架构类别

C/S架构

​ Client / Server,客户端和服务器端,用户需要安装专门客户端程序。

B/S架构

​ Browser / Server,浏览器和服务器端,不需要安装专门客户端程序,浏览器是操作系统内置。

B/S 和C/S交互模型的比较

  • 相同点

    ​ 都是基于请求-响应交互模型:即浏览器(客户端) 向 服务器发送 一个 请求。服务器 向 浏览器(客户端)回送 一个 响应 。

    ​ 必须先有请求 再有响应

    ​ 请求和响应成对出现

  • 不同点

    ​ 实现C/S模型需要用户在自己的操作系统安装各种客户端软件(百度网盘、腾讯QQ等);实现B/S模型,只需要用户在操作系统中安装浏览器即可。

注:B/S模型可以理解为一种特殊C/S模型。

小结

  1. 架构类别

    • CS: 客户端-服务器; 必须要安装指定的客户端
    • BS: 浏览器-服务器; 不需要安装客户端的, 通过浏览器

    我们以BS架构为主.

  2. Java

    • JavaSE java基础
    • JavaMe 移动端的,嵌入式
    • JavaEE 企业级应用(eg: 网站, 后台系统, 移动端提供数据….)

web通信【重点】

​ 基于http协议,请求响应的机制

​ 请求一次响应一次

​ 先有请求后有响应

image-20191208091344175

小结

  1. 浏览器必须先请求服务器, 服务器处理请求, 给浏览器响应
  2. 一次请求, 一次响应
  3. 先有请求,再有响应
  4. 请求响应基于HTTP协议

服务器

processon的画图软件的注册地址:

https://www.processon.com/i/5f0440b81e085326375eb062

服务器介绍

什么是服务器

​ 服务器就是一个软件,任何电脑只需要安装上了服务器软件, 我们的电脑就可以当做一台服务器了.

​ 服务器: 硬件(电脑)+软件(mysql, tomcat,nginx)

常见web服务器

  • WebLogic

    ​ Oracle公司的产品,是目前应用比较多的Web服务器,支持J2EE规范。WebLogic是用于开发、集成、部署和管理大型分布式Web应用、网络应用和数据库应用的Java应用服务器。

    image-20230918120229386

  • WebSphere

    ​ IBM公司的WebSphere,支持JavaEE规范。WebSphere 是随需应变的电子商务时代的最主要的软件平台,可用于企业开发、部署和整合新一代的电子商务应用。

    image-20230918120235595

  • Glass Fish

    ​ 最早是Sun公司的产品,后来被Oracle收购,开源免费,中型服务器。

  • JBoss

    ​ JBoss公司产品,开源,支持JavaEE规范,占用内存、硬盘小,安全性和性能高。

    image-20230918120305825

  • Tomcat

    ​ 中小型的应用系统,免费,开源,效率特别高, 适合扩展(搭集群)支持JSP和Servlet.

    image-20230918120314490

Tomcat7 快速部署

1.解压 apache-tomcat-7.0.78-windows-x64.zip 到非中文无空格目录中

2.检查是否配置了 JAVA_HOME

image-20230918120322351

3.新建环境变量 CATALINA_HOME=解压目录

image-20230918120327462

4.在 Path 环境变量中加入 Tomcat 解压目录\bin 目录

image-20230918120330919

5.在命令行中运行 catalina run 启动 Tomcat 服务器,在浏览器地址栏访问如下地址进行测试: http://localhost:8080

image-20230918120355421

6.如果启动失败,提示端口号被占用,则将默认的 8080 端口修改为其他未使用的值,例如 8989 等。

打开:解压目录\\conf\\server.xml,找到第一个 Connector 标签,修改 port属性

7.在 Eclipse 中创建 Tomcat 镜像

image-20230918120407878

image-20230918120414572

image-20230918120419113

8.创建动态 Web 工程进行测试

[1]在 WebContent 目录下创建 index.jsp,加入如下代码

1
2
<%@page import="java.util.Date"%>
<%=new Date() %>

[2]在 index.jsp 上点右键:Run as→Run on Server 查看运行结果

9.说明:关联 Tomcat 镜像时,Eclipse 会从本地 Tomcat 中复制信息及文件,之后二者的配置信息就没有关系了,其中任何一个的配置信息发生变化都不会自动同步到另外一个。

image-20230918120435834

tomcat8 安装和使用

概述

​ Tomcat服务器是一个免费的开放源代码的Web应用服务器。

​ Tomcat是Apache软件基金会(Apache Software Foundation)的Jakarta项目中的一个核心项目,由Apache、Sun和其他一些公司及个人共同开发而成。由于有了Sun的参与和支持,最新的Servlet 和JSP规范总是能在Tomcat中得到体现。

​ 因为Tomcat技术先进、性能稳定,而且免费,因而深受Java爱好者的喜爱并得到了部分软件开发商的认可,是目前比较流行的Web应用服务器。

​ Tomcat安装分为解压版和安装版,这里着重介绍解压版。当然,我们在安装Tomcat前都应该将jdk安装好且配置好环境变量。Apache Tomcat 8.5需要一个Java标准版运行时环境(JRE)版本7或更高版本。

tomcat的下载

强调: 我们使用的软件版本,要和老师用的版本一致

目前阶段: jdk8、mysql5、tomcat8

  1. 先去官网下载:http://tomcat.apache.org/,选择tomcat8版本(资料已提供)(红框所示)

    image-20230918120459691

  2. 选择要下载的文件(红框所示):

    image-20230918120504127

    tar.gz 文件 是linux操作系统下的安装版本

    exe文件是window操作系统下的安装版本

    zip文件是window操作系统下压缩版本(我们选择zip文件)

  3. 下载完成

image-20230918120508881

tomcat服务器软件安装

  1. 直接解压当前这个tomcat压缩包:(不要有中文,不要有空格)

  2. 配置环境变量:

    tomcat运行依赖于java环境:
    image-20230918120512921

也可以配置环境变量 CATALINA_HOME 和 Path

​ 系统变量增加: CATALINA_HOME D:\develop\tomcat\apache-tomcat-8.5.55

​ path增加: %CATALINA_HOME%\bin

tomcat的目录结构

image-20230918120528147

启动Tocmat

通过以上安装,Tomcat启动方式就有很多种了:

  1. 查找tomcat目录下bin目录,查找其中的startup.bat命令,双击启动服务器:
    image-20230918120532543

    启动效果:
    image-20230918120714310

2.在Tomcat解压目录中的bin文件夹下,找到tomcat8.exe并双击启动。

image-20230918120720822

3.在Tomcat解压目录中的bin文件夹下,找到tomcat8w.exe并双击启动。

image-20230918120725424

在图形界面中点击 Start 按钮启动。

image-20230918120730858

4.在CMD命令行中输入startup回车启动Tomcat。

image-20230918120735655

5.在Windows系统服务中找到Tomcat服务进行启动。

image-20230918120740130

测试Tomcat

打开浏览器在,在浏览器的地址栏中输入:

http://127.0.0.1:8080或者http://localhost:8080

image-20230918120756944

注: Localhost相当于127.0.0.1

关闭Tomcat

查找tomcat目录下bin目录,查找其中的shutdown.bat命令,双击关闭服务器:
image-20230918120800875

常见问题

安装注意点

  • 解压到一个==没有中文和空格==目录下
  • 使用之前, 配置java_home和path(jdk环境变量)
    • java_home 不要配到bin目录,配到jdk的安装目录
    • path 才是配到bin目录

一台服务器无法显示2个tomcat

1
2
3
/bin/start.bat 26行,修改 start 为 run
因为端口占用过了
将server.xml 8005,8443,8080等端口修改为9005,9443,9080.

端口号冲突

​ 报如下异常: java.net.BindException: Address already in use: JVM_Bind 8080

​ 解决办法:

​ 第一种:修改Tomcat的端口号

image-20230918120814291

​ 修改conf/server.xml , 第70行左右

image-20230918120819709

第二种:查询出来哪一个进程把8080占用了, 结束掉占用8080端口后的程序

​ 打开命令行输入: netstat -ano

​ 找到占用了8080 端口的 进程的id

​ 去任务管理器kill掉这个id对应的程序

image-20230918120824468

JAVA_HOME没有配置

  • 会出现闪退 (如果java_home配置了还是闪退 忽略它了, 后面在IDEA里面进行启动, 就没有这个问题)

tomcat 8.5 控制台中文乱码问题

新装的,启动乱码,不影响,但是体验不好

image-20230918120830551

本质原因就一个:字节流解码为字符串时,使用了错误的字符集(和编码所用字符集不一致)!

因为windows系统中,其命令行窗口在解码字节数组时,默认使用本地字符集(对于我们就是GBK),而tomcat默认输出的启动信息是通过utf8进行编码的,这就导致编码与解码所使用字符集的不一致,从而出现了乱码情况!

image-20230918120841684

解决方案1

conf/logging.properties,将 UTF-8 修改为 GBK

java.util.logging.ConsoleHandler.encoding = GBK

要改的话,其他几个都改了

解决方案2

修改console窗口为UTF-8格式

2个我都不改…tomcat就临时用下,后面内嵌使用.懒得改了

总结说明

1.配置JDK环境变量名为JAVA_HOME,Tomcat会找JAVA_HOME环境变量来定位JDK。

2.同样Tomcat环境变量名为CATALINA_HOME,Tomcat启动时会找CATALINA_HOME环境变量,来调用startup.bat。

3.Tomcat配置环境变量主要是为了在CMD命令行可以直接输入Tomcat脚本命令,这样不用定位到安装目录的bin文件夹下。

4.Tomcat安装版要比解压版省事儿,一些工作安装版完成了。

5.需要注意Tomcat对java版本要求。

Tomcat服务注册

由于上面配置了Tomcat的环境变量,所以现在可以在CMD命令行中直接输入bin中的命令脚本。服务器注册输入 service install回车。

image-20230918120928642

在 服务 中我们便看到了 Apache Tomcat8.5服务,可以在其属性中设置为 自启。一般项目部署实施需要进行设置,这样服务器重启后Tomcat便可自行启动。

image-20230918120933702

Tomcat服务移除:service remove

image-20230918120939050

运用Tomcat服务器部署WEB项目

标准的JavaWeb应用目录结构

1
2
3
4
5
6
WebAPP(文件夹,项目)  
|---静态资源: html,css,js,图片(它们可以以文件存在,也可以以文件夹存在)
|---WEB-INF 固定写法。此目录下的文件不能被外部(浏览器)直接访问
|---lib:jar包存放的目录
|---web.xml:当前项目的配置文件(3.0规范之后可以省略)
|---classes:java类编译后生成class文件存放的路径

发布项目到tomcat

直接发布

​ 只要将准备好的web资源直接复制到tomcat/webapps文件夹下,就可以通过浏览器使用http协议访问获取

虚拟路径的方式发布项目

  1. 第一步:在tomcat/conf目录下新建一个Catalina目录(如果已经存在无需创建)

image-20230918120949837

  1. 第二步:在Catalina目录下创建localhost目录(如果已经存在无需创建)

image-20230918120954355

  1. 第三步:在localhost中创建xml配置文件,名称为:随便写,比如叫做second.xml(注:这个名称是浏览器访问路径)

image-20230918120958616

  1. 第四步:添加second.xml文件的内容为: docBase就是你需要作为虚拟路径的项目的路径

    1
    2
    <?xml version = "1.0" encoding = "utf-8"?>
    <Context docBase="C:\JavaEE_Relation\JavaEE101\itheima101_staticWeb\day24_html" />

    image-20230918121002976

  2. 第五步:直接访问(通过写配置文件的路径来访问):

    http://localhost:8080/second/a.html(second就是配置文件的名字, 映射成了myApp)

http协议

http协议概述

什么是HTTP协议

​ HTTP是HyperText Transfer Protocol(超文本传输协议)的简写,传输HTML文件。

​ HTTP是互联网上用的最多的一个协议, 所有的www开头的都是遵循这个协议的(可能是https)

HTTP协议的作用

​ HTTP作用:用于定义WEB浏览器与WEB服务器之间交换数据的过程和数据本身的内容

​ 浏览器和服务器交互过程: 浏览器请求, 服务请求响应

​ 请求(请求行,请求头,请求体)

​ 响应(响应行,响应头,响应体)

小结

  1. HTTP协议:超文本传输协议,它就定义了客户端与服务器端进行交互时候的规则

  2. HTTP协议的作用:定义客户端与服务器端交互的过程以及传输的数据

请求部分(了解)

image-20230918121017000

  • get方式请求
1
2
3
4
5
6
7
8
9
10
11
12
13
【请求行】
GET /myApp/success.html?username=zs&password=123456 HTTP/1.1

【请求头】
Accept: text/html, application/xhtml+xml, */*
X-HttpWatch-RID: 41723-10011
Referer: http://localhost:8080/myApp/login.html
Accept-Language: zh-Hans-CN,zh-Hans;q=0.5
User-Agent: Mozilla/5.0 (MSIE 9.0; qdesk 2.4.1266.203; Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko
Accept-Encoding: gzip, deflate
Host: localhost:8080
Connection: Keep-Alive
Cookie: Idea-b77ddca6=4bc282fe-febf-4fd1-b6c9-72e9e0f381e8
  • post请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
【请求行】
POST /myApp/success.html HTTP/1.1

【请求头】
Accept: text/html, application/xhtml+xml, */*
X-HttpWatch-RID: 37569-10012
Referer: http://localhost:8080/myApp/login.html
Accept-Language: zh-Hans-CN,zh-Hans;q=0.5
User-Agent: Mozilla/5.0 (MSIE 9.0; qdesk 2.4.1266.203; Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
Host: localhost:8080
Content-Length: 27
Connection: Keep-Alive
Cache-Control: no-cache

【请求体】
username=zs&password=123456

请求行

  • 请求行
1
2
GET  /myApp/success.html?username=zs&password=123456 HTTP/1.1	
POST /myApp/success.html HTTP/1.1
  • 请求方式(8种,put,delete等)

    ​ GET:明文传输, 不安全,参数跟在请求路径后面,对请求参数大小有限制,

    ​ POST: 暗文传输,安全一些,请求参数在请求体里,对请求参数大小没有有限制,

  • URI:统一资源标识符(即:去掉协议和IP地址部分)

  • 协议版本:HTTP/1.1

请求头

​ 从第2行到空行处,都叫请求头,以键值对的形式存在,但存在一个key对应多个值的请求头.

作用:浏览器告诉服务器自己相关的设置.

  • Accept:浏览器可接受的MIME类型 ,告诉服务器客户端能接收什么样类型的文件。
  • User-Agent:浏览器信息.(浏览器类型, 浏览器的版本….)
  • Accept-Charset: 浏览器通过这个头告诉服务器,它支持哪种字符集
  • Content-Length:表示请求参数的长度
  • Host:初始URL中的主机和端口
  • Referrer:从哪里里来的(之前是哪个资源)、防盗链
  • Content-Type:内容类型,告诉服务器,浏览器传输数据的MIME类型,文件传输的类型,application/x-www-form-urlencoded .
  • Accept-Encoding:浏览器能够进行解码的数据编码方式,比如gzip
  • Connection:表示是否需要持久连接。如果服务器看到这里的值为“Keep -Alive”,或者看到请求使用的是HTTP 1.1(HTTP 1.1默认进行持久连接 )
  • Cookie:这是最重要的请求头信息之一(会话技术, 后面会有专门的时间来讲的)
  • Date:Date: Mon, 22Aug 2011 01:55:39 GMT请求时间GMT

请求体

​ 只有请求方式是post的时候,才有请求体. 即post请求时,请求参数(提交的数据)所在的位置

小结

 1. 请求行
   	   	1. 请求方式
   	   	2. 请求路径
 2. 请求头: 它是由键值对构成
 3. 请求体: 只有post请求才有请求体,post请求的请求体是用于携带请求参数的

响应部分(了解)

image-20230918121635986

  • 响应部分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    【响应行】
    HTTP/1.1 200 OK

    【响应头】
    Accept-Ranges: bytes
    ETag: W/"143-1557909081579"
    Last-Modified: Wed, 15 May 2019 08:31:21 GMT
    Content-Type: text/html
    Content-Length: 143
    Date: Sun, 08 Dec 2019 02:20:04 GMT

    【响应体】
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Title</title>
    </head>
    <body>
    Success
    </body>
    </html>

响应行

1
HTTP/1.1 200
  • 协议/版本

  • 响应状态码  (记住-背诵下来)

    image-20230918121642236

    ​ 200:正常,跟服务器连接成功,发送请求成功

    ​ 302:重定向(跳转)

    ​ 304:读取缓存,表示客户机缓存的版本是最新的,客户机可以继续使用它,无需到服务器请求. 读取缓存

    ​ 403: forbidden 权限不够,服务器接收到了客户端的请求,但是拒绝处理

    ​ 404:服务器接收到了客户端的请求,但是我服务器里面没有你要找的资源

    ​ 500:服务器接收到了客户端的请求,也找到了具体的资源处理请求,但是处理的过程中服务器出异常了

响应头

响应头以key:vaue存在, 可能多个value情况. ==服务器指示浏览器去干什么,去配置什么.==

a.mp3 b.mp4

  • Refresh: 5;url=http://www.baidu.com 指示客户端刷新频率。单位是秒 eg: 告诉浏览器5s之后跳转到百度

  • Content-Disposition: attachment; filename=a.jpg 指示客户端(浏览器)下载文件

  • Content-Length:80 告诉浏览器正文的长度

  • Server:apachetomcat 服务器的类型

  • Content-Encoding: gzip服务器发送的数据采用的编码类型

  • Set-Cookie:SS=Q0=5Lb_nQ;path=/search服务器端发送的Cookie

  • Cache-Control: no-cache (1.1) 

  • Pragma: no-cache  (1.0)  表示告诉客户端不要使用缓存

  • Connection:close/Keep-Alive  

  • Date:Tue, 11 Jul 2000 18:23:51 GMT

响应体

​ 页面展示内容, 和网页右键查看的源码一样

小结

 1. 响应行:包含响应状态码
   	   	1. 常见的响应状态码:
   	          	1. 200 OK
   	          	2. 302 Redirect 重定向
   	          	3. 304 Cache 读取缓存
   	          	4. 400 BAD REQUEST 请求有问题(可能是请求参数等等不符合规定)
   	          	5. 403 Forbidden 拒绝处理
   	          	6. 404 NOT FOUND 找不到资源
   	          	7. 500 SERVER ERROR 服务器异常
 2. 响应头: 由多个键值对构成
 3. 响应体:
      	1. 可以用于客户端页面的展示
      	2. 可以用于下载

Tomcat架构设计&源码剖析

  • Tomcat功能需求分析

  • Tomcat套娃式架构设计(Connector层次架构、容器层次结构)

  • Tomcat源码构建

  • Tomcat源码剖析-链式初始化过程

  • Tomcat流程剖析-Servlet请求处理链路追踪

Tomcat架构设计

Tomcat的功能(需求)

浏览器发给服务端的是一个 HTTP 格式的请求,HTTP 服务器收到这个请求后,需要调用服务端程序来处理,所谓的服务端程序就是你写的 Java 类,一般来说不同的请求需要由不同的 Java 类来处理。

那么问题来了,HTTP 服务器怎么知道要调用哪个 Java 类的哪个方法呢?

image-20220707094852453

  • HTTP 服务器直接调用具体业务类,它们是紧耦合的。

解决:HTTP 服务器不直接调用业务类,而是把请求交给容器来处理,容器通过 Servlet 接口调用业务类。因此 Servlet 接口和 Servlet 容器的出现,达到了 HTTP 服务器与业务类解耦的目的。

image-20220707094943533

Tomcat两个非常重要的功能(身份)

  • Http服务器功能:Socket通信(TCP/IP)、解析Http报文
  • Servlet容器功能:有很多Servlet(自带系统级Servlet+自定义Servlet),Servlet处理具体的业务逻辑

Tomcat的架构(设计实现)

需求

Tomcat的需求是要实现 2 个核心功能:

  • 处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化。=> 网络通信,协议解析
  • 加载和管理 Servlet,以及具体处理 Request 请求。=> servlet容器

Tomcat架构设计

基于Tomcat需求,所以 Tomcat 设计了两个核心组件连接器(Connector)和容器(Container)来分别做这两件事情。
连接器负责对外交流,容器负责内部处理。

image-20220707095221858

Tomcat中一个容器可能对接多个连接器,每一个连接器都对应某种协议某种IO模型,Tomcat将多个连接器和单个容器组成一个service组件,一个tomcat中可能存在多个Service组件Connector:将不同协议不同IO模型的请求转换为标准的标准的ServletRequest 对象交给容器处理。

  • Container:Container本质上是一个Servlet容器,负责servelt的加载和管理,处理请求
  • ServletRequest,并返回标准的 ServletResponse 对象给连接器

连接器是如何设计的?

铺垫:支持协议&IO模型

铺垫:Tomcat 是支持多种 I/O 模型和应用层协议的
Tomcat 支持的 I/O 模型有:

  • NIO:非阻塞 I/O,采用 Java NIO 类库实现
  • NIO.2:异步 I/O,采用 JDK 7 最新的 NIO.2 类库实现
  • APR:采用 Apache 可移植运行库实现,是 C/C++ 编写的本地库

Tomcat 支持的应用层协议有:

  • HTTP/1.1:这是大部分 Web 应用采用的访问协议
  • AJP:用于和 Web 服务器集成(如 Apache)
  • HTTP/2:HTTP 2.0 大幅度的提升了 Web 性能
连接器架构分析

Tomcat 为了实现支持多种 I/O 模型和应用层协议,一个容器可能对接多个连接器,就好比一个房间有多个门。但是单独的连接器或者容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫作 Service 组件。这里请注意,Service 本身没有做什么重要的事情,只是在连接器和容器外面多包了一层,把它们组装在一起。Tomcat 内可能有多个 Service,这样的设计也是出于灵活性的考虑。通过在 Tomcat 中配置多个 Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。

image-20220707095402196

从图上可以看到,最顶层是 Server,这里的 Server 指的就是一个 Tomcat 实例。一个 Server 中有一个或者多个 Service,一个 Service 中有多个连接器和一个容器。连接器与容器之间通过标准的ServletRequest 和 ServletResponse 通信。

核心功能

连接器对 Servlet 容器屏蔽了协议及 I/O 模型等的区别,无论是 HTTP 还是 AJP,在容器中获取到的都是一个标准的 ServletRequest 对象。我们可以把连接器的功能需求进一步细化,比如:

  • 监听网络端口。
  • 接受网络连接请求。读取网络请求字节流。
  • 根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的 Tomcat Request 对象。
  • 将 Tomcat Request 对象转成标准的 ServletRequest。
  • 调用 Servlet 容器,得到 ServletResponse。 将 ServletResponse 转成 Tomcat Response 对象。
  • 将 Tomcat Response 转成网络字节流。
  • 将响应字节流写回给浏览器。
通用架构设计

需求列清楚后,我们要考虑的下一个问题是,连接器应该有哪些子模块?优秀的模块化设计应该考虑高内聚、低耦合。

  • 高内聚是指相关度比较高的功能要尽可能集中,不要分散。
  • 低耦合是指两个相关的模块要尽可能减少依赖的部分和降低依赖的程度,不要让两个模块产生强依赖。

通过分析连接器的详细功能列表,我们发现连接器需要完成 3 个高内聚的功能:

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

因此 Tomcat 的设计者设计了 3 个组件来实现这 3 个功能,分别是 Endpoint、Processor 和 Adapter。

image-20220707095531409

由于 I/O 模型和应用层协议可以自由组合,比如 NIO + HTTP 或者 NIO.2 + AJP。Tomcat 的设计者将网络通信和应用层协议解析放在一起考虑,设计了一个叫 ProtocolHandler 的接口来封装这两种变化点

image-20220707095544595

通过图清晰地看到它们的继承和层次关系,这样设计的目的是尽量将稳定的部分放到抽象基类,同时每一种 I/O 模型和协议的组合都有相应的具体实现类,我们在使用时可以自由选择。

ProtocolHandler 组件

现在我们知道,连接器用 ProtocolHandler 来处理网络连接和应用层协议,包含了 2 个重要部件:

Endpoint 和 Processor,下面来详细介绍它们的工作原理

EndPoint组件

Endpoint 翻译过来是”通信端点”,主要负责网络通信,这其中就包括,监听客户端连接创建于客户端连接Socket,并负责连接Socket 接收和发送处理器。因此Endpoint是对传输层的抽象,是用来实现 TCP/IP 协议的。

EndPoint类结构图

EndPoint用基类用抽象类AbstractEndpoint来表示,对于不同的Linux IO模型通过使用不同子类来实现。

image-20220707095641120

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

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

Processor组件

Processor:翻译过来是”处理器”,主要负责根据具体应用层协议(HTTP/AJP)读取字节流解析成Tomcat Request 和 Response,因此Processor是对应用层的抽象,是用来实现 HTTP/AJP 协议的。

Processor类结构图

image-20220707095735224

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

Adapter组件

由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat 定义了自己的 Request 类来“存放”这些请求信息。ProtocolHandler 接口负责解析请求并生成 Tomcat Request/Response类。但是这个Request/Response 对象不是标准的 ServletRequest/ServletResponse,也就意味着,不能用TomcatRequest/Response 作为参数来调用容器。

Tomcat 设计者的解决方案是引入 CoyoteAdapter,这是适配器模式的经典运用,负责将TomcatRequest/Response 与 ServletRequest/ServletResponse 的相互转化,实现连接器(Connector)和容器(Container)的解耦。

ProtocolHandler组件

ProtocolHandler组件EndPoint组件,Processor组件合并在一起表示协议处理器。用来处理tomcat支持多种IO模型和多种协议的组件。

ProtocolHandler类图

image-20220707100309425

Connector处理流程

我们再来看看连接器的组件图:

image-20220707100327036

  • Endpoint内部Acceptor组件用于监听Socket 连接请求,当发送客户端连接到服务端Acceptor组件负责与客户端建立连接创建Socket,每当连接客户端发起请求,Endpoint会创建一个SocketProcessor对象SocketProcessor 用于处理接收到的 Socket 请求,它实现 Runnable 接口,在 run 方法里调用协议处理组件 Processor 进行处理。为了提高处理能力,SocketProcessor 被提交到线程池来执行。而这个线程池叫作执行器(Executor)
  • Processor 接收来自 Endpoint 的 Socket,读取字节流解析成 Tomcat Request 和 Response 对象,接着会调用 Adapter 的 Service 方法。并通过 Adapter 将其提交到容器处理
  • 连接器调用 CoyoteAdapter 的 sevice 方法,传入的是 Tomcat Request 对象,CoyoteAdapter 负责将 Tomcat Request 转成 ServletRequest,再调用容器的 service 方法。
容器的本质

Tomcat 有两个核心组件:连接器和容器

image-20220707100638150

容器,顾名思义就是用来装载东西的器具,在 Tomcat 里,容器就是用来装载 Servlet 的。那 Tomcat的 Servlet 容器是如何设计的呢?

Container本质上是一个Servlet容器,负责servelt的加载和管理,处理请求ServletRequest,并返回标准的 ServletResponse 对象给连接器

容器工作流程

当客户请求某个资源时,HTTP 服务器会用一个 ServletRequest 对象把客户的请求信息封装起来,然后调用 Servlet 容器的 service 方法,Servlet 容器拿到请求后,根据请求的 URL 和 Servlet 的映射关系,找到相应的 Servlet,如果 Servlet 还没有被加载,就用反射机制创建这个 Servlet,并调用 Servlet 的init 方法来完成初始化,接着调用 Servlet 的 service 方法来处理请求,把 ServletResponse 对象返回给HTTP 服务器,HTTP 服务器会把响应发送给客户端

image-20220707100739382

容器层次结构

Tomcat 设计了 4 种容器组件,分别是 Engine、Host、Context 和 Wrapper。这 4 种容器不是平行关系,而是父子关系。

如图:

image-20220707100908090

  • Wrapper:表示一个 Servlet
  • Context:表示一个 Web 应用程序,一个 Web 应用程序中可能会有多个 Servlet
  • Host:表示的是一个虚拟主机,或者说一个站点,可以给 Tomcat 配置多个虚拟主机地址,而一个虚拟主机下可以部署多个 Web 应用程序
  • Engine:表示引擎,用来管理多个虚拟站点,一个 Service 最多只能有一个 Engine。

可以再通过 Tomcat 的server.xml配置文件来加深对 Tomcat 容器的理解。Tomcat 采用了组件化的设计,它的构成组件都是可配置的,其中最外层的是 Server,其他组件按照一定的格式要求配置在这个顶层容器中。

image-20220707100937809

组件类图

问题:Tomcat 是怎么管理这些容器组件?

Container容器中定义了Container 接口用来描述Container容器中所有的组件,不同的子组件分别定义了不同子接口做描述。容器组件之间具有父子关系。

1
2
3
4
5
6
7
8
public interface Container extends Lifecycle { 
public void setName(String name);
public Container getParent();
public void setParent(Container container);
public void addChild(Container child);
public void removeChild(Container child);
public Container findChild(String name);
}

image-20220707101535636

从接口看到了 getParent、setParent、addChild 和 removeChild 等方法。可能还注意到 Container 接口扩展了 Lifecycle 接口,Lifecycle 接口用来统一管理各组件的生命周期

套娃式架构设计的好处

image-20220707101553907

  • 一层套一层的方式,其实组件关系还是很清晰的,也便于后期组件生命周期管理
  • tomcat这种架构设计正好和xml配置文件中标签的包含方式对应上,那么后续在解读xml以及封装对象的过程中就容易对应
  • 便于子容器继承父容器的一些配置

Tomcat源码环境构建

Apache Tomcat源码下载

下载地址:https://tomcat.apache.org/download-80.cg

image-20220707101650332

解压apache-tomcat-8.5.73-src.zip

image-20220707101710136

apache-tomcat-8.5.73-src目录下添加pom文件

因为下载下来的源码包没有pom文件,为了编译并以maven项目运行,需要手动构建一下pom文件

pom文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <groupId>org.apache.tomcat</groupId>
   <artifactId>apache-tomcat-8.5.73-src</artifactId>
   <name>Tomcat8.5</name>
   <version>8.5</version>
   <build>
       <!--指定源目录-->
       <finalName>Tomcat8.5</finalName>
       <sourceDirectory>java</sourceDirectory>
       <resources>
       <resource>
               <directory>java</directory>
           </resource>
       </resources>
       <plugins>
           <!--引入编译插件-->
           <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-compiler-plugin</artifactId>
               <version>3.1</version>
               <configuration>
                   <encoding>UTF-8</encoding>
                   <source>1.8</source>
                   <target>1.8</target>
               </configuration>
           </plugin>
       </plugins>
   </build>
   <!--tomcat 依赖的基础包-->
   <dependencies>
       <dependency>
           <groupId>org.easymock</groupId>
           <artifactId>easymock</artifactId>
           <version>3.4</version>
       </dependency>
       <dependency>
           <groupId>ant</groupId>
           <artifactId>ant</artifactId>
           <version>1.7.0</version>
       </dependency>
       <dependency>
           <groupId>wsdl4j</groupId>
           <artifactId>wsdl4j</artifactId>
           <version>1.6.2</version>
       </dependency>
       <dependency>
           <groupId>javax.xml</groupId>
           <artifactId>jaxrpc</artifactId>
           <version>1.1</version>
       </dependency>
       <dependency>
           <groupId>org.eclipse.jdt.core.compiler</groupId>
           <artifactId>ecj</artifactId>
           <version>4.5.1</version>
       </dependency>
   </dependencies>
</project>

创建catalina-home目录

image-20220707101832239

image-20220707101838069

将bin、conf、webapps目录复制到catalina-home目录中:

image-20220707101845965

其余文件夹手动进行创建:

image-20220707101856580

导入IDEA

image-20220707101914213

新建application 设置main类和vm参数

1
2
Main class: org.apache.catalina.startup.Bootstrap 
VM options: 配置自己的catalina-home目录

image-20220707101931503

1
2
3
-Dcatalina.home=catalina-home
-Dcatalina.base=catalina-home
-Djava.io.tmpdir=catalina-home/temp

打开ContextConfig 添加一行代码

1
2
//初始化JSP解析引擎 
context.addServletContainerInitializer(new JasperInitializer(),null);

image-20220707101959617

启动执行【常见错误解决】

image-20220707102011869

报错

image-20220707102025207

解决:

将JmxRemoteLifecycleListener类 全部注释,再次启动

image-20220707102029146

启动成功:

image-20220707102036811

如何实现一键式启停

Tomcat里面有各种各样的组件,每个组件各司其职,组件之间又相互协作共同完成web服务器这样的工程。

组件的层次关系:

image-20220707102047832

上面这张图描述了组件之间的静态关系,如果想让一个系统能够对外提供服务,我们需要创建、组装并启动这些组件;在服务停止的时候,我们还需要释放资源,销毁这些组件,因此这是一个动态的过程。

也就是说,Tomcat 需要动态地管理这些组件的生命周期。

组件关系:

先来看看组件之间的关系。如果你仔细分析过这些组件,可以发现它们具有两层关系。

  • 第一层关系是组件有大有小,大组件管理小组件,比如 Server 管理 Service,Service 又管理连接器和容器。
  • 第二层关系是组件有外有内,外层组件控制内层组件,比如连接器是外层组件,负责对外交流,外层组件调用内层组件完成业务功能。也就是说,请求的处理过程是由外层组件来驱动的。

这两层关系决定了系统在创建组件时应该遵循一定的顺序。

  • 第一个原则是先创建子组件,再创建父组件,子组件需要被“注入”到父组件中。
  • 第二个原则是先创建内层组件,再创建外层组件,内层组件需要被“注入”到外层组件

因此,最直观的做法就是将图上所有的组件按照先小后大、先内后外的顺序创建出来,然后组装在一起。不知道注意到没有,这个思路其实很有问题!因为这样不仅会造成代码逻辑混乱和组件遗漏,而且也不利于后期的功能扩展。为了解决这个问题,我们希望找到一种通用的、统一的方法来管理组件的生命周期,就像汽车“一键启动”那样的效果。

思考:如何统一管理组件的创建、初始化、启动、停止和销毁?

一键式启停:Lifecycle 接口

Lifecycle 接口里应该定义这么几个方法:init、start、stop 和 destroy,每个具体的组件去实现这些方法。

Lifecycle 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface Lifecycle {
  ....
   // 初始化方法
   public void init() throws LifecycleException;
   // 启动方法
   public void start() throws LifecycleException;
   // 停止方法,和start对应
   public void stop() throws LifecycleException;
   // 销毁方法,和init对应
   public void destroy() throws LifecycleException;
   // 获取生命周期状态
   public LifecycleState getState();
   // 获取字符串类型的生命周期状态
   public String getStateName();
}

在这样的设计中,在父组件的 init 方法里需要创建子组件并调用子组件的 init 方法。同样,在父组件的 start 方法里也需要调用子组件的 start 方法,因此调用者可以无差别的调用各组件的 init 方法和 start 方法,这就是组合模式的使用,并且只要调用最顶层组件,也就是 Server 组件的 init 和 start 方法,整个 Tomcat 就被启动起来了

重用性:LifecycleBase 抽象基类

有了接口,我们就要用类去实现接口。一般来说实现类不止一个,不同的类在实现接口时往往会有一些
相同的逻辑,如果让各个子类都去实现一遍,就会有重复代码。那子类如何重用这部分逻辑呢?其实就是定义一个基类来实现共同的逻辑,然后让各个子类去继承它,就达到了重用的目的。而基类中往往会定义一些抽象方法,所谓的抽象方法就是说基类不会去实现这些方法,而是调用这些方法来实现骨架逻辑。抽象方法是留给各个子类去实现的,并且子类必须实现,否则无法实例化。

image-20220707102230680

Tomcat 定义一个基类 LifecycleBase 来实现 Lifecycle 接口,把一些公共的逻辑放到基类中去,比如生命状态的转变与维护、生命事件的触发以及监听器的添加和删除等,而子类就负责实现自己的初始化、启动和停止等方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public final synchronized void init() throws LifecycleException {
   //1. 状态检查
   if (!state.equals(LifecycleState.NEW)) {
       invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
  }
   try {
       //2.触发INITIALIZING事件的监听器
       setStateInternal(LifecycleState.INITIALIZING, null, false);
       
       //3.调用具体子类的初始化方法
       initInternal();
       
       //4. 触发INITIALIZED事件的监听器
       setStateInternal(LifecycleState.INITIALIZED, null, false);
  } catch (Throwable t) {
    ...
  }
}

源码剖析-Tomcat启动流程

启动总流程图

使用Tomcat时,通过 Tomcat 的/bin目录下的脚本startup.sh来启动 Tomcat,那执行了这个脚本后发生了什么呢?

流程图:

image-20220707102351453

  • 1.Tomcat 本质上是一个 Java 程序,因此startup.sh脚本会启动一个 JVM 来运行 Tomcat 的启动类 Bootstrap
  • 2.Bootstrap 的主要任务是初始化 Tomcat 的类加载器,并且创建 Catalina
  • 3.Catalina 是一个启动类,它通过解析server.xml、创建相应的组件,并调用 Server 的 start 方法
  • 4.Server 组件的职责就是管理 Service 组件,它会负责调用 Service 的 start 方法
  • 5.Service 组件的职责就是管理连接器和顶层容器 Engine,因此它会调用连接器和 Engine 的 start 方法。

这样 Tomcat 的启动就算完成了

(1)启动流程细节

1
2
startup.sh --> catalina.sh start --> java xxxx.jar org.apache.catalina.startup.Bootstrap(main) 
start(参数)

image-20220707102444775

tips:

  • Bootstrap.init
  • Catalina.load
  • Catalina.start

Bootstrap#init();

1、初始化类加载器

2、加载catalina类,并且实例化

3、反射调用Catalina的setParentClassLoader方法

4、实例 赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//1、初始化类加载器
   //2、加载catalina类,并且实例化
   //3、反射调用Catalina的setParentClassLoader方法
   //4、实例 赋值
   public void init() throws Exception {
       // 1. 初始化Tomcat类加载器(3个类加载器)
       initClassLoaders();
       Thread.currentThread().setContextClassLoader(catalinaLoader);
       SecurityClassLoad.securityClassLoad(catalinaLoader);
       // Load our startup class and call its process() method
       if (log.isDebugEnabled())
           log.debug("Loading startup class");
       // 2. 实例化Catalina实例
       Class<?> startupClass =
catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
       Object startupInstance = startupClass.getConstructor().newInstance();
       // Set the shared extensions class loader
       if (log.isDebugEnabled())
           log.debug("Setting startup class properties");
       String methodName = "setParentClassLoader";
       Class<?> paramTypes[] = new Class[1];
       paramTypes[0] = Class.forName("java.lang.ClassLoader");
       Object paramValues[] = new Object[1];
       paramValues[0] = sharedLoader;
       // 3. 反射调用Catalina的setParentClassLoader方法,将sharedLoader设置为
Catalina的parentClassLoader成员变量
       Method method =
               startupInstance.getClass().getMethod(methodName, paramTypes);
       method.invoke(startupInstance, paramValues);
       //4、将catalina实例赋值
       catalinaDaemon = startupInstance;
   }

Catalina#load();

1
org.apache.catalina.startup.Bootstrap#main中的load方法调用的是catalina中的方法

1)load初始化流程

模板模式:

每个节点自己完成的任务后,会接着调用子节点(如果有的话)的同样的方法,引起链式反应。

image-20220707102653576

Catalina#start();

流程图

与load过程很相似

image-20220707102718169

流程分析-Servlet请求处理链路跟踪

问题:设计了这么多层次的容器,Tomcat 是怎么确定请求是由哪个 Wrapper 容器里的 Servlet 来处理的呢?答案是,Tomcat 是用 Mapper 组件来完成这个任务的。

Mapper 组件的功能就是将用户请求的 URL 定位到一个 Servlet,它的工作原理是:Mapper 组件里保存了 Web 应用的配置信息,其实就是容器组件与访问路径的映射关系,比如 Host 容器里配置的域名、Context 容器里的 Web 应用路径,以及 Wrapper 容器里 Servlet 映射的路径,你可以想象这些配置信息就是一个多层次的 Map。

当一个请求到来时,Mapper 组件通过解析请求 URL 里的域名和路径,再到自己保存的 Map 里去查找,就能定位到一个 Servlet。请你注意,一个请求 URL 最后只会定位到一个 Wrapper 容器,也就是一个 Servlet。

例子:

image-20220707102749389

假如有用户访问一个 URL,比如图中的http://user.shopping.com:8080/order/buy,Tomcat 如何将这个 URL 定位到一个 Servlet 呢?

首先,根据协议和端口号选定 Service 和 Engine。

我们知道 Tomcat 的每个连接器都监听不同的端口,比如 Tomcat 默认的 HTTP 连接器监听 8080 端口、默认的 AJP 连接器监听 8009 端口。上面例子中的 URL 访问的是 8080 端口,因此这个请求会被 HTTP 连接器接收,而一个连接器是属于一个 Service 组件的,这样 Service 组件就确定了。我们还知道一个 Service 组件里除了有多个连接器,还有一个容器组件,具体来说就是一个 Engine 容器,因此 Service 确定了也就意味着 Engine 也确定了。

然后,根据域名选定 Host。
Service 和 Engine 确定后,Mapper 组件通过 URL 中的域名去查找相应的 Host 容器,比如例子中的 URL 访问的域名是user.shopping.com,因此 Mapper 会找到 Host2 这个容器。

之后,根据 URL 路径找到 Context 组件。

Host 确定以后,Mapper 根据 URL 的路径来匹配相应的 Web 应用的路径,比如例子中访问的是/order,因此找到了 Context4 这个 Context 容器。

最后,根据 URL 路径找到 Wrapper(Servlet)。
Context 确定后,Mapper 再根据web.xml中配置的 Servlet 映射路径来找到具体的 Wrapper 和 Servlet。

我们知道容器组件最重要的功能是处理请求,最先拿到请求的是 Engine 容器,Engine 容器对请求做一些处理后,会把请求传给自己子容器 Host 继续处理,依次类推,最后这个请求会传给 Wrapper 容器,Wrapper 会调用最终的 Servlet 来处理。那么这个调用过程具体是怎么实现的呢?答案是使用 Pipeline-Valve 管道。

Pipeline-Valve 是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。Valve 表示一个处理点,比如权限认证和记录日志。如果还不太理解的话,可以来看看 Valve 和 Pipeline 接口中的关键方法

1
2
3
4
5
public interface Valve {
 public Valve getNext();
 public void setNext(Valve valve);
 public void invoke(Request request, Response response)
}

由于 Valve 是一个处理点,因此 invoke 方法就是来处理请求的。注意到 Valve 中有 getNext 和 setNext 方法,因此我们大概可以猜到有一个链表将 Valve 链起来了。请你继续看 Pipeline 接口:

1
2
3
4
5
6
public interface Pipeline extends Contained {
 public void addValve(Valve valve);
 public Valve getBasic();
 public void setBasic(Valve valve);
 public Valve getFirst();
}

没错,Pipeline 中有 addValve 方法。Pipeline 中维护了 Valve 链表,Valve 可以插入到 Pipeline 中,对请求做某些处理。我们还发现 Pipeline 中没有 invoke 方法,因为整个调用链的触发是 Valve 来完成的,Valve 完成自己的处理后,调用getNext.invoke来触发下一个 Valve 调用。

每一个容器都有一个 Pipeline 对象,只要触发这个 Pipeline 的第一个 Valve,这个容器里 Pipeline 中的 Valve 就都会被调用到。但是,不同容器的 Pipeline 是怎么链式触发的呢,比如 Engine 中 Pipeline 需要调用下层容器 Host 中的 Pipeline

这是因为 Pipeline 中还有个 getBasic 方法。这个 BasicValve 处于 Valve 链表的末端,它是 Pipeline 中必不可少的一个 Valve,负责调用下层容器的 Pipeline 里的第一个 Valve。还是通过一张图来解释

image-20220707102922853

每一个容器组件都有一个 Pipeline,而 Pipeline 中有一个基础阀(Basic Valve),而 Engine 容器的基础阀定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final class StandardEngineValve extends ValveBase {
   public final void invoke(Request request, Response response)
     throws IOException, ServletException {
 
     //拿到请求中的Host容器
     Host host = request.getHost();
     if (host == null) {
         return;
    }
 
     // 调用Host容器中的Pipeline中的第一个Valve
     host.getPipeline().getFirst().invoke(request, response);
}
j
}

这个基础阀实现非常简单,就是把请求转发到 Host 容器。问题是处理请求的 Host 容器对象是从请求中拿到的,请求对象中怎么会有 Host 容器呢?这是因为请求到达 Engine 容器中之前,Mapper 组件已经对请求进行了路由处理,Mapper 组件通过请求的 URL 定位了相应的容器,并且把容器对象保存到了请求对象中。