基于Tomcat7
1、Tomcat
是一个Servlet
容器。
2、使用Java
代码模拟一个Tomcat
容器:
class Tomcat{
List<Servlet> servlets;
Connector connect;//处理请求,生成了Request
}
3、回顾servlet
的定义
public class MyHttpServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().println("http");
}
}
问题1:我们定义的servlet
,它的实例是怎么生成的?doGet
方法又是怎么调用到的呢?在哪执行的?
MyHttpServlet myHttpServlet=new MyHttpServlet ();
myHttpServlet.doGet();
问题2:调用doGet
()方法时,HttpServletRequest
,HttpServletResponse
都是接口,那么入参的实际类型是什么?
实际上HttpServletRequest
,HttpServletResponse
接口的实现类,由servlet
容器实现,比如:tomcat
,jetty
。我们把项目部署到tomcat
中,tomcat
就提供了一个HttpServletRequest
和HttpServletResponse
的实现类,部署到jetty
,jetty
就提供了相应的实现类,这是一个规范。
其中,在Tomcat
中,HttpServletRequest
的实现类就是RequestFacade
4、RequestFacade
-门面模式
RequestFacade
实现了HttpServletRequest
,充当门面模式中的外观类。RequestFacade
屏蔽内部子系统的细节。
RequestFacade
代表的是一个请求,实际上是RequestFacade
的属性Request
,才是真正代表的一个请求,外界的http
请求信息,都封装在这个Request
对象中,只是利用门面模式的外观类,把它屏蔽在里面了。
Request
本身也是实现了接口HttpServletRequest
5、应用部署到Tomcat
有几种部署方式。
war
包部署<Context path="/ServletHello1" docBase="xxx" />
好了,现在这个Context
就引出来一个容器,在Tomcat
里有一个接口就叫Context
Container
容器是父接口,所有的子容器都必须实现这个接口,在Tomcat
中Container
容器的设计是典型的责任链设计模式,其有四个子容器:Engine、Host、Context
和Wrapper
。这四个容器之间是父子关系,Engine
容器包含Host
,Host
包含Context
,Context
包含Wrapper
。
我们在web
项目中的一个Servlet
类对应一个Wrapper
,多个Servlet
就对应多个Wrapper
,当有多个Wrapper
的时候就需要一个容器来管理这些Wrapper
了,这就是Context
容器了,Context
容器对应一个工程,所以我们新部署一个工程到Tomcat
中就会新创建一个Context
容器。
Engine->Host->Context->Wrapper->Servlet
Context
继承自Container
容器接口
context
表示的就是应用,这个应用是一个servlet
容器。
path
:是访问时的根地址,表示访问的路径。就是项目路径,根据请求带的项目路径,来确定使用哪个Context
来处理请求
docbase
:表示应用程序的路径,注意斜杠的方向“/
”。应用编译后的class
文件的路径。
Host
表示虚拟主机,一个虚拟主机下可以定义很多个Context
,即可以部署多个项目
name
:表示虚拟主机的名字,就是对应请求的域名,根据域名来确定使用哪个虚拟主机appBase
:表示应用存放的目录unpackWARs
:表示是否需要解压autoDeploy
:热部署Engine
引擎包含多个Host
,它的责任就是将用户请求分配给一个虚拟上机处理。
Service
时,name
必须唯一。
元素中的其中一个相同。Context
是一个servlet
容器,但是它并不是直接装servlet
实例,可以简单的理解,Context
包含了多个Wrapper
。
class Context{
List<Wrapper> wrappers;
}
Wrapper
才是装了多个Servlet
实例,注意装的是某一个类型的servlet
实例,比如,我自定一了一个servlet
,就叫MyServlet
,那么就有一个Wrapper
里装的都是MyServlet
的实例。
class Wrapper{
List<servlet> servlet;//装的是某一个类型的servlet实例
}
servlet
都是单例的,所有访问同一个servlet
的请求是共用同一个servlet
实例的。servlet
实现了SingleThreadModel
接口,每一个访问这个servlet
的请求,单独有一个servlet
实例,既然servlet
支持了这个功能,肯定要去实现这个功能,因此,就有了wrapper
前面的4
个容器都包含Pipiline
管道
在Tomcat
中,对于每一个容器,都有一个公共的组件Pipiline
管道,每个管道下可以有多个阀门Valve
,一个阀表示一个具体的执行任务,在servlet
容器的管道中,除了有一个基础阀BaseValve
,还可以添加任意数量的阀。阀的数量指的是额外添加的阀数量,即不包括基础阀。可以通过编辑Tomcat
的配置文件(server.xml
)来动态地添加阀。
public class TestValve extends RequestFilterValve {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
}
@Override
protected Log getLog() {
return null;
}
}
下图显示了一条管道及其阀:
如果对servlet
编程中的过滤器有所了解的话,那么应该不难想像管道和阀的工作机制。管道就像过滤器链一样,而阀则好似是过滤器。阀与过滤器类似,可以处理传递给它的request
对象和response
对象。当一个阀执行完成后,会调用下一个阀继续执行。基础阀总是最后一个执行的。
问:Wrapper
容器的管道中的最后一个阀门,是怎样把请求转发给对应的servlet
的?
看一下阀门的StandardWrapperValve
的invoke
方法的核心代码:
allocate
方法里的loadServlet
方法,直接newInstance
一个serlvet
实例
servlet = (Servlet) instanceManager.newInstance(servletClass);
这也回答了前言里的问题1,servlet
是怎么生成的,在哪生成的?
servlet
是在Wrapper
的基础阀里生成的。
servlet
实例有了,那么doGet/doPost
在哪执行呢?
回到阀门的StandardWrapperValve
的invoke
方法的核心代码:来到这一行代码,传进servlet
实例,并且返回一个过滤器链
我们定义一个Filter
的时候,可以像下面这样写:
执行过程:filter->servlet->再回到filter
因此,filterChain.doFilter(servletRequest, servletResponse)
;虽然是调用其他过滤器,但是过滤器调用完之后,必然要去调用servlet
的doget
方法。
所以,上面才需要传入servlet
实例,然后获取一个过滤器链,因为要用到这个servlet
继续往下走:
进入doFilter
,再进入internalDoFilter
方法:发现并没有执行,servlet
的doGet/dopost
方法,执行的是servlet
的service
方法
我们自定义servlet
的时候,并没有service
啊
往父类HttpServlet
中找找:
因此,在哪里调用的doGet
、doPost
方法,与Tomcat
没有关系,这个实际上是servlet
规范所定义的。
Request
对象怎么生成的?
1、Request
对象表示的是一个请求,Tomcat
要生成一个Request
对象,首先就要有数据,这个数据拿来的呢?
操作系统。Tomcat
仅仅是操作系统上的一个应用程序,因此,它的数据开源于操作系统。
2、那操作系统的数据又从哪来的呢?
操作系统安装在服务器上面的,因此,操作系统的数据来源于服务器。
3、那服务器的数据又来源于哪里呢?
一个服务器通过网络将数据发给另一个服务器的。这就涉及到很多很多的协议了,服务器之间想要完成数据的传输和接受,就和计算机网络有关系了,跟各种协议有关系。
4、如果服务器A有数据,想把数据发给服务器B,但是仅仅有数据和IP(没有端口,端口是和应用程序对应的),服务器A能够保证数据安全可靠的发给服务器B吗
不能。可能数据非常大,数据在网络的传输过程中,会经过机房或者交换机,很有可能数据就会丢失,是不可靠的。
5、如何保证数据的可靠传输呢?
使用Tcp协议。该协议是一个可靠的协议,但是该协议,毕竟只是一个协议,这个协议肯定是需要去实现的。
6、Tcp
协议由谁实现?
操作系统。linux
和Windows
操作系统,或者其他操作系统都会去实现Tcp
协议。
Tcp
协议在服务器之间建立连接时,会进行三次握手,linux
源码就有关于Tcp
三次握手的相关源码:
注意:Tcp
协议只是保证数据在传输层可靠的传输,但是它并不关心数据长什么样子,也不关心数据的格式以及代表的意义,谁才关系数据的格式是怎么的,内容是怎么的呢?当然是使用数据的人和发送数据的人啊。浏览器和应用程序,因此,Http
协议就有了,它是应用层协议,对我们要发送的数据进行规范,数据的格式,内容,数据的意义。
7、Http
协议由谁实现?
浏览器、应用程序(包括Tomcat
)
8、如果用java
代码去实现一个浏览器,当用户在浏览器的地址栏输入地址后,按下回车键,代码执行的流程是怎么样的
1、肯定是要根据Http
协议,去构造出符合Http
协议的数据格式
2、发送数据,建立Tcp
连接。
3、应用程序,接受数据
问题来了,java
代码里怎么去建立Tcp
连接,我们知道操作系统的源码里有建立Tcp
连接的代码,那么Java
能不能去调用操作系统的建立三次握手的代码,比如:tcp_connect
()方法,以此来建立Tcp
连接。
实际上建立Tcp
连接的方法,java
是不能直接调用的,因为这些方法是linux
操作系统非常核心的方法,不会直接让你调的,实际上像这种情况,我们通常会想到,写一个API
,重新定义一个方法,比如:
create_tcp(){
//验证
xxxx
//验证通过之后,才让调这个方法
tcp_connect();
}
liunx
里也一样,不会让我们直接调用tcp_connect
方法,它提供了一个对外的接口,就是socket
,别人不能直接访问tcp_connect
,只能通过socket
去访问。
因此,回到java
代码里怎么建立Tcp
连接?通过Socket
接口,建立Tcp
连接。
其实不仅仅Java
应用程序,运行在操作系统上的各种程序,都只能通过Socket
去建立Tcp
连接。
使用Java Socket
建立一个tcp
连接,如下:
public static void main(String[] args) throws IOException {
Socket socket = new Socket();//tcp
socket.connect(new InetSocketAddress("localhost",9090));
DatagramSocket datagramSocket = new DatagramSocket();//udp
}
Java
的Socket
类底层是不是直接调的操作系统的Socket
呢?它们有没有什么联系呢?
1、进入connect
方法
2、进入createImpl
3、进入AbstractPlainSocketImpl
的create
4、进入socketCreate
,创建一个Socket
5、进入DualStackPlainSocketImpl
的socketCreate
发现socket0
是一个native
方法
native
的socket0
,代码只能去open jdk
中去看socket0
是怎么实现的.
7、DualStackPlainSocketImpl.c
文件:
8、net_util_md.c
文件:
socket(domain,type,protocol)
又是怎么实现的呢?
但是该方法的实现,是在open jdk
中找不到的。那么它到底在哪里实现的
9、看到net_util_md.c
文件的头文件
那么,在当前的windows
操作系统中找找有没有这个头文件。
win10
如下:
这个就是上面socket(domain,type,protocol)
的真正实现
10、回头看Java
在创建一个Socket
连接的时候,socket.connect(new InetSocketAddress("localhost",9090))
这行代码,会先去调用Jdk
代码,jdk
最终调用的是操作系统的代码
回到Tomcat
架构平视图中的8
,浏览器会负责去构造数据,发送数据,那么Tomcat
接受数据后,需要解析数据,这个时候就要去实现Http
协议。
Tomcat
使用socket
接受数据,然后就要取数据,这里就涉及到一个概念,叫做IO
模型,就是你通过什么方式去取数据的呢,是以BIO
还是NIO
呢?
Connector
组件,会从socket
中去取数据,然后根据Http
协议去解析数据,解析成Request
对象。
这个Connector
组件,在Tomcat
中对应有一个类Connector
,有一个方法setProtocol
,入参就是上图的protocol
属性(Tomcat
启动会去解析server.xml
)
public void setProtocol(String protocol) {
if (AprLifecycleListener.isAprAvailable()) {
if ("HTTP/1.1".equals(protocol)) {
setProtocolHandlerClassName
("org.apache.coyote.http11.Http11AprProtocol");
} else if ("AJP/1.3".equals(protocol)) {
setProtocolHandlerClassName
("org.apache.coyote.ajp.AjpAprProtocol");
} else if (protocol != null) {
setProtocolHandlerClassName(protocol);
} else {
setProtocolHandlerClassName
("org.apache.coyote.http11.Http11AprProtocol");
}
} else {
//当`protocol` 属性设置为`Http1.1`时,对应的类是`org.apache.coyote.http11.Http11Protocol`
if ("HTTP/1.1".equals(protocol)) {
setProtocolHandlerClassName
("org.apache.coyote.http11.Http11Protocol");
} else if ("AJP/1.3".equals(protocol)) {
setProtocolHandlerClassName
("org.apache.coyote.ajp.AjpProtocol");
} else if (protocol != null) {
setProtocolHandlerClassName(protocol);
}
}
}
当protocol
属性设置为Http1.1
时,代码里对应的类是org.apache.coyote.http11.Http11Protocol
,它对应的协议是Http1.1
,对应的io
模型是BIO
。
1、看看Http11Protocol
源码,为啥说它对应的是BIO
2、看看JIoEndpoint
使用的工厂模式创建socket
对象。
如果想使用NIO
,只需要修改:protocol="HTTP/1.1"
变为org.apache.coyote.http11.Http11NioProtocol
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8443" />
启动Tomcat
就户发现走的是下面的红框了
1、看看Http11NioProtocol
。查看方法类似上面查看BIO
2、看看NioEndpoint
使用的是SocketChannel
,明显使用的是NIO
,学过nio
就知道了
不管是BIO
还是NIO
方式取数据,反正是获取到了对应的Socket
。接下来就是解析数据了。
对于BIO
方式:会在processSocket
方法中处理数据,对应nio
方式。猜想Tomcat
取数据,应该是socket.getInputStream()
来取数据,然后按照Http1.1
的格式解析数据
1、processSocket
方法
Http
协议的格式:
2、包装socket
,然后把这个socket
连接交给线程池,去处理。这个线程池在Tomcat7
中默认10
条线程,private int minSpareThreads = 10
;
3、进入SocketProcessor
,是一个线程
4、process
方法
5、AbstractHttp11Processor
的process
,因为BIO
对应的是Http11
解析请求行和请求头的方法里,都会把解析出来的数据,设置到Request
对象里。
socket.getInputStream()
取出来的数据,不是直接使用的,会放到缓存中。