Tomcat - 如何设计一个简单的web容器

在学习Tomcat前,很多人先入为主的对它的认知是巨复杂的;所以第一步,在学习它之前,要打破这种观念,我们通过学习如何设计一个最基本的web容器来看它需要考虑什么;进而在真正学习Tomcat时,多把重点放在它的顶层设计上,而不是某一块代码上, 思路永远比具体实现重要的多。@pdai

写在前面

我们在学习一项技术时,需要学习是它的知识体系,而不是碎片化的知识点。在构建知识体系时,我们往往需要先全局的看完一个教程或者一本书,这是构建的基础。这里我推荐大家看两本书:

特别是第一本:经典的《How Tomcat Works》的中文版,它从0基础逐步构建出Tomcat,适合新手;本节中很多内容源自这本书。

本系列在本之后,将转为直接分析Tomcat框架。

基础认知:如何实现服务器和客户端(浏览器)的交互

客户端和服务器端之间的交互式通过Socket来实现的,它术语应用层的协议。

HTTP协议

http协议相关的内容可以参看这里:网络协议 - HTTP 协议详解

Socket

Socket是网络连接的一个端点。套接字使得一个应用可以从网络中读取和写入数据。放在两 个不同计算机上的两个应用可以通过连接发送和接受字节流。为了从你的应用发送一条信息到另 一个应用,你需要知道另一个应用的 IP 地址和套接字端口。在 Java 里边,套接字指的是java.net.Socket类。

要创建一个套接字,你可以使用 Socket 类众多构造方法中的一个。其中一个接收主机名称 和端口号:

public Socket (java.lang.String host, int port)

在这里主机是指远程机器名称或者 IP 地址,端口是指远程应用的端口号。例如,要连接 yahoo.com 的 80 端口,你需要构造以下的 Socket 对象:

new Socket ("yahoo.com", 80);

一旦你成功创建了一个 Socket 类的实例,你可以使用它来发送和接受字节流。要发送字节 流,你首先必须调用Socket类的getOutputStream方法来获取一个java.io.OutputStream对象。 要 发 送 文 本 到 一 个 远 程 应 用 , 你 经 常 要 从 返 回 的 OutputStream 对 象 中 构 造 一 个 java.io.PrintWriter 对象。要从连接的另一端接受字节流,你可以调用 Socket 类的 getInputStream 方法用来返回一个 java.io.InputStream 对象。

SeverSocket

Socket 类代表一个客户端套接字,即任何时候你想连接到一个远程服务器应用的时候你构造的套接字,现在,假如你想实施一个服务器应用,例如一个 HTTP 服务器或者 FTP 服务器,你需要一种不同的做法。这是因为你的服务器必须随时待命,因为它不知道一个客户端应用什么时候会尝试去连接它。为了让你的应用能随时待命,你需要使用 java.net.ServerSocket 类。这是 服务器套接字的实现。

ServerSocketSocket 不同,服务器套接字的角色是等待来自客户端的连接请求。一旦服务器套接字获得一个连接请求,它创建一个 Socket 实例来与客户端进行通信

要创建一个服务器套接字,你需要使用 ServerSocket 类提供的四个构造方法中的一个。你 需要指定 IP 地址和服务器套接字将要进行监听的端口号。通常,IP 地址将会是 127.0.0.1,也 就是说,服务器套接字将会监听本地机器。服务器套接字正在监听的 IP 地址被称为是绑定地址。 服务器套接字的另一个重要的属性是 backlog,这是服务器套接字开始拒绝传入的请求之前,传 入的连接请求的最大队列长度。

其中一个 ServerSocket 类的构造方法如下所示:

public ServerSocket(int port, int backLog, InetAddress bindingAddress);

一个简单web容器的设计和实现:对静态资源

准备,这个例子来源于《How Tomcat Works》, 可以从这里下载源码

注意:当你跑如下程序时,可能会由于浏览器新版本不再支持的HTTP 0.9协议,而造成浏览器页面没有返回信息。

组件设计

根据上述的基础,我们可以看到,我们只需要提供三个最基本的类,分别是:

  • Request - 表示请求,这里表示浏览器发起的HTTP请求
  • HttpServer - 表示处理请求的服务器,同时这里使用我们上面铺垫的ServerSocket
  • Reponse - 表示处理请求后的响应, 这里表示服务器对HTTP请求的响应结果

组件实现

从上图中我们可以看到,组织这几个类的入口在Server的启动方法中,即main方法中, 所以我们透过main方法从Server类进行分析:

  • Server是如何启动的?
public class HttpServer {

  // 存放静态资源的位置
public static final String WEB_ROOT =
System.getProperty("user.dir") + File.separator + "webroot"; // 关闭Server的请求
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN"; // 是否关闭Server
private boolean shutdown = false; // 主入口
public static void main(String[] args) {
HttpServer server = new HttpServer();
server.await();
} public void await() {
// 启动ServerSocket
ServerSocket serverSocket = null;
int port = 8080;
try {
serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
}
catch (IOException e) {
e.printStackTrace();
System.exit(1);
} // 循环等待一个Request请求
while (!shutdown) {
Socket socket = null;
InputStream input = null;
OutputStream output = null;
try {
// 创建socket
socket = serverSocket.accept();
input = socket.getInputStream();
output = socket.getOutputStream(); // 封装input至request, 并处理请求
Request request = new Request(input);
request.parse(); // 封装output至response
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource(); // 关闭socket
socket.close(); // 如果接受的是关闭请求,则设置关闭监听request的标志
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
}
catch (Exception e) {
e.printStackTrace();
continue;
}
}
}
}
  • Request请求是如何封装和处理的?
public class Request {

  private InputStream input;
private String uri; // 初始化Request
public Request(InputStream input) {
this.input = input;
} // 处理request的方法
public void parse() {
// 从socket中读取字符
StringBuffer request = new StringBuffer(2048);
int i;
byte[] buffer = new byte[2048];
try {
i = input.read(buffer);
}
catch (IOException e) {
e.printStackTrace();
i = -1;
}
for (int j=0; j<i; j++) {
request.append((char) buffer[j]);
}
System.out.print(request.toString()); // 获得两个空格之间的内容, 这里将是HttpServer.WEB_ROOT中静态文件的文件名称
uri = parseUri(request.toString());
} private String parseUri(String requestString) {
int index1, index2;
index1 = requestString.indexOf(' ');
if (index1 != -1) {
index2 = requestString.indexOf(' ', index1 + 1);
if (index2 > index1)
return requestString.substring(index1 + 1, index2);
}
return null;
} public String getUri() {
return uri;
} }
  • Response中响应了什么?
public class Response {

  private static final int BUFFER_SIZE = 1024;
Request request;
OutputStream output; public Response(OutputStream output) {
this.output = output;
} // response中封装了request,以便获取request中的请求参数
public void setRequest(Request request) {
this.request = request;
} public void sendStaticResource() throws IOException {
byte[] bytes = new byte[BUFFER_SIZE];
FileInputStream fis = null;
try {
// 读取文件内容
File file = new File(HttpServer.WEB_ROOT, request.getUri());
if (file.exists()) {
fis = new FileInputStream(file);
int ch = fis.read(bytes, 0, BUFFER_SIZE);
while (ch!=-1) {
output.write(bytes, 0, ch);
ch = fis.read(bytes, 0, BUFFER_SIZE);
}
}
else {
// 文件不存在时,输出404信息
String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +
"Content-Type: text/html\r\n" +
"Content-Length: 23\r\n" +
"\r\n" +
"<h1>File Not Found</h1>";
output.write(errorMessage.getBytes());
}
}
catch (Exception e) {
// thrown if cannot instantiate a File object
System.out.println(e.toString() );
}
finally {
if (fis!=null)
fis.close();
}
}
}
  • 启动输出

当我们run上面HttpServer中的main方法之后,我们就可以打开浏览器http://localhost:8080, 后面添加参数看返回webroot目录中静态文件的内容了(比如这里我加了hello.txt文件到webroot下,并访问http://localhost:8080/hello.txt)。

一个简单web容器的设计和实现:对Servelet

上面这个例子是不是很简单?是否打破了对一个简单http服务器的认知,减少了对它的恐惧。

但是上述的例子中只处理了静态资源,我们如果要处理Servlet呢?

组件设计

不难发现,我们只需要在HttpServer只需要请求的处理委托给ServletProcessor, 让它接受请求,并处理Response即可。

组件实现

  • 在HttpServer中
public void await() {
//.... // create Response object
Response response = new Response(output);
response.setRequest(request); // 不再有response自己处理
//response.sendStaticResource(); // 而是如果以/servlet/开头,则委托ServletProcessor处理
if (request.getUri().startsWith("/servlet/")) {
ServletProcessor1 processor = new ServletProcessor1();
processor.process(request, response);
} else {
// 原有的静态资源处理
StaticResourceProcessor processor = new StaticResourceProcessor();
processor.process(request, response);
} // ....
}
  • ServletProcessor 如何处理的?
public class ServletProcessor1 {

  public void process(Request request, Response response) {

    // 获取servlet名字
String uri = request.getUri();
String servletName = uri.substring(uri.lastIndexOf("/") + 1); // 初始化URLClassLoader
URLClassLoader loader = null;
try {
// create a URLClassLoader
URL[] urls = new URL[1];
URLStreamHandler streamHandler = null;
File classPath = new File(Constants.WEB_ROOT);
// the forming of repository is taken from the createClassLoader method in
// org.apache.catalina.startup.ClassLoaderFactory
String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString() ;
// the code for forming the URL is taken from the addRepository method in
// org.apache.catalina.loader.StandardClassLoader class.
urls[0] = new URL(null, repository, streamHandler);
loader = new URLClassLoader(urls);
} catch (IOException e) {
System.out.println(e.toString() );
} // 用classLoader加载上面的servlet
Class myClass = null;
try {
myClass = loader.loadClass(servletName);
}
catch (ClassNotFoundException e) {
System.out.println(e.toString());
} // 将加载到的class转成Servlet,并调用service方法处理
Servlet servlet = null;
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) request, (ServletResponse) response);
} catch (Exception e) {
System.out.println(e.toString());
} catch (Throwable e) {
System.out.println(e.toString());
} }
}
  • Repsonse
public class PrimitiveServlet implements Servlet {

  public void init(ServletConfig config) throws ServletException {
System.out.println("init");
} public void service(ServletRequest request, ServletResponse response)
throws ServletException, IOException {
System.out.println("from service");
PrintWriter out = response.getWriter();
out.println("Hello. Roses are red.");
out.print("Violets are blue.");
} public void destroy() {
System.out.println("destroy");
} public String getServletInfo() {
return null;
}
public ServletConfig getServletConfig() {
return null;
} }
  • 访问 URL

利用外观模式改造

上述代码存在一个问题,

// 将加载到的class转成Servlet,并调用service方法处理
Servlet servlet = null;
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) request, (ServletResponse) response);
} catch (Exception e) {
System.out.println(e.toString());
} catch (Throwable e) {
System.out.println(e.toString());
}

这里直接处理将request和response传给servlet处理是不安全的,因为request可以向下转型为Request类,从而ServeletRequest便具备了访问Request中方法的能力。

public class Request implements ServletRequest {
// 一些public方法
}
public class Response implements ServletResponse { }

解决的方法便是通过外观模式进行改造:

  • RequestFacade为例
public class RequestFacade implements ServletRequest {

  private ServletRequest request = null;

  public RequestFacade(Request request) {
this.request = request;
} /* implementation of the ServletRequest*/
public Object getAttribute(String attribute) {
return request.getAttribute(attribute);
} public Enumeration getAttributeNames() {
return request.getAttributeNames();
} public String getRealPath(String path) {
return request.getRealPath(path);
} ...
  • Process中由传入外观类
Servlet servlet = null;
RequestFacade requestFacade = new RequestFacade(request); // 转换成外观类
ResponseFacade responseFacade = new ResponseFacade(response);// 转换成外观类
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) requestFacade, (ServletResponse) responseFacade);
}
catch (Exception e) {
System.out.println(e.toString());
}
catch (Throwable e) {
System.out.println(e.toString());
}

总结

当我们看到这么一个简单的web容器实现之后,我们便不再觉得Tomcat高高在上;这将为我们继续分析Tomcat中核心源码提供基础。

更多

更多文章请参考 Java 全栈知识体系

Tomcat详解系列(1) - 如何设计一个简单的web容器的更多相关文章

  1. Tomcat详解系列(2) - 理解Tomcat架构设计

    Tomcat - 理解Tomcat架构设计 前文我们已经介绍了一个简单的Servlet容器是如何设计出来,我们就可以开始正式学习Tomcat了,在学习开始,我们有必要站在高点去看看Tomcat的架构设 ...

  2. Tomcat详解系列(3) - 源码分析准备和分析入口

    Tomcat - 源码分析准备和分析入口 上文我们介绍了Tomcat的架构设计,接下来我们便可以下载源码以及寻找源码入口了.@pdai 源代码下载和编译 首先是去官网下载Tomcat的源代码和二进制安 ...

  3. 如何设计一个可用的web容器

    之前在另外一个平台(http://www.jointforce.com/jfperiodical/article/1035)发表的一篇文章,现在发布到自己的博客上. 开发一个web容器涉及很多不同方面 ...

  4. 编写自己的一个简单的web容器(一)

    在之前的博客中我更大家说过Http协议是对tcp协议的封装,其底层还是使用tcp协议来进行数据传出的,浏览器实际上就是一个Socket客户端,今天呢我们就开始着手利用ServerSocket来编写一个 ...

  5. 编写自己的一个简单的web容器(二)

    昨天我们已经能够确定浏览器的请求能够被我们自己编写的服务类所接收并且我们服务类响应的数据也能够正常发送到浏览器客户端,那么我们今天要解决的问题就是让我们的数据能够被浏览器识别并解析. Http(Htt ...

  6. .Net Attribute详解(上)-Attribute本质以及一个简单示例

    Attribute的直接翻译是属性,这和Property容易产生混淆,所以一般翻译成特性加以区分.Attribute常常的表现形式就是[AttributeName], 随意地添加在class, met ...

  7. JDBC详解系列(一)之流程

    ---[来自我的CSDN博客](http://blog.csdn.net/weixin_37139197/article/details/78838091)--- JDBC概述   使用JDBC也挺长 ...

  8. 一个简单的web服务器例子

    一个简单的web容器小例子,功能十分简单,只能访问静态资源,对于新手来说还是有一定的意义.主要分三个类 1.server类:主要功能开启socketServer,阻塞server,接收socket访问 ...

  9. 源码详解系列(六) ------ 全面讲解druid的使用和源码

    简介 druid是用于创建和管理连接,利用"池"的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制.连接可靠性测试.连接泄露控制.缓存语句等功能,另外,druid还扩展 ...

随机推荐

  1. Linux 驱动框架---驱动中的中断

    在单片机开发中中断就是执行过程中发生了一些事件需要及时处理,所以需要停止当前正在运行的处理的事情转而去执行中断服务函数,已完成必要的事件的处理.在Linux中断一样是如此使用但是基于常见的中断控制器的 ...

  2. XSS (跨站脚本攻击) 的原理分析,测试 && 应对措施

    1 1 1 XSS (跨站脚本攻击) 的原理分析,测试 1 demo: <!DOCTYPE html> <html lang="en"> <head& ...

  3. 如何获取豆瓣电影 API Key

    如何获取豆瓣电影 API Key 豆瓣 API Key 不能使用了 ! solutions & !== ? https://frodo.douban.com/api/v2/subject_co ...

  4. ws & websocket & 掉线重连

    ws & websocket & 掉线重连 reconnecting websocket https://github.com/joewalnes/reconnecting-webso ...

  5. macOS open url from terminal

    macOS open url from terminal open URL && start terminal bash open url in chrome open chrome ...

  6. CSS前端性能优化

    1.Google 资深web开发工程师Steve Souders对CSS选择器的效率从高到低做了一个排序: 1. id选择器(#myid) 2. 类选择器(.myclassname) 3. 标签选择器 ...

  7. C++使用libcurl进行http通讯

    借着curl 7.75.0版本更新, 最近又下载下来玩了玩, 在此做个简单记录 1.环境搭建 首先是libcurl动态库, 自己下载源码编译的话如果要使用https协议还要下载OpenSSL和libs ...

  8. Mybatis高级:Mybatis注解开发单表操作,Mybatis注解开发多表操作,构建sql语句,综合案例学生管理系统使用接口注解方式优化

    知识点梳理 课堂讲义 一.Mybatis注解开发单表操作 *** 1.1 MyBatis的常用注解 之前我们在Mapper映射文件中编写的sql语句已经各种配置,其实是比较麻烦的 而这几年来注解开发越 ...

  9. eclipse自定义快捷键

    eclipse系统自带的快捷键设置有时候使用起来并不习惯,可以自定义某些常用的快捷键. 依次打开 Window->Preference->General->Keys,这里显示了所有的 ...

  10. SpringCloud之服务降级

    1.Hystrix(断路器) 1.1定义 扇出:多个微服务调用的时候,假设微服务A调用微服务B和C,微服务B和C又调用其他的服务.服务雪崩:如果扇出的链路上某个微服务的调用时间过长或不可用,对微服务A ...