Tomcat应用服务器的简单实现

Tomcat应用服务器的简单实现

简单来说就是实现一个基于TCP的HTTP服务器,可以完成静态资源动态资源的访问。

1 Tomcat的职责:

1)正确监听TCP端口并处理客户端发起的TCP连接请求。
2)在已经建立TCP连接的基础上,可以正确的从TCP连接中读取数据
3)可以按照HTTP协议,解析数据(HTTP请求),对方发过来的不一定是HTTP请求。
4)把请求封装成HttpServletRequest请求对象
5)根据请求中的URL信息,决定交给哪个 Web应用处理。
6)根据请求中的URL信息,决定交给哪个 Servlet处理。
7)得到一个含有内容的HttpServletResponse响应对象
8)把HttpServletResponse响应对象,构建成HTTP响应,并发送。

Tomcat负责建立TCP连接,根据Http协议解析数据,封装请求,交给内部的Servlet来处理,得到HttpServletResponse响应,再构建成HTTP响应,通过socket发送出去。

2 实际的Servlet代码有三部分构成:
  • Servlet标准(一组接口)
  • Tomcat实现Servlet
  • 自己的Servlet应用
    在这里插入图片描述
3 类设计:
  • 仿照Tomcat 写一个Servlet , HttpServletRequest,HttpServletResponse接口和
    HttpServlet(抽象类)
  • Server (程序入口,用线程池的方式处理多个客户端的连接,负责前台事务)
  • TransactionTask(线程池任务:读取请求,发送响应)
  • HttpServletRequestImpl(封装请求内容对象)
  • HttpServletResponseImp(封装响应内容对象)
  • StaticResourceServlet(用来处理静态资源)
  • NotFoundServlet(用来处理404无法找到资源)
  • 自定义一些继承HttpServlet抽象类的Servlet,来根据请求做出响应。
  • 仿照web.xml,用WebXML类实现动态资源路径的匹配,根据路径找到对应的自定义Servlet.

TransactionTask具体的任务主要分为三大块:

  • 读取请求
  • 发送响应
  • 关闭连接

整体流程:
(1)用HttpServletRequest中的方法将Socket输入字节流中的请求进行解析,并封装成一个HttpServletRequest对象。
(2)在HttpServletResponse类中建立一个空的response对象。
(3)根据请求方法及路径后缀确定要寻找静态资源还是动态资源。
(4)无论是静态资源还是动态资源或者是查找不到(404),都能指向一个对应的servlet对象。
(5)调用Servlet中的doGet/doPost封装该响应对象,然后向HTTP写入响应,先将响应头部分写入,再写入响应体部分。

Ⅰ Servlet模块

在这里插入图片描述

编写Servlet接口:

public interface Servlet {
    void init();

    void service(HttpServletRequest req, HttpServletResponse resp) throws IOException;

    void destroy();
}

编写实现了Servlet接口的抽象类:

public abstract class HttpServlet implements Servlet {
    @Override
    public void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        switch (req.getMethod()) {
            case "GET":
                doGet(req, resp);
                break;
            case "POST":
                doPost(req, resp);
                break;
            default:
                resp.setStatus(400);
        }
    }

    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException{
        // Method Not Allow
        resp.setStatus(405);
    }

    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // Method Not Allow
        resp.setStatus(405);
    }

    @Override
    public void init() {
    }

    @Override
    public void destroy() {
    }
}

编写HttpServletRequest请求接口:

public interface HttpServletRequest {
    String getMethod();

    String getPath();

    String getParameter(String name);

    String getHeader(String name);
}

编写HttpServletResponse响应接口:

public interface HttpServletResponse {
    void setStatus(int status);

    void setHeader(String name, String value);

    void setContentType(String contentType);

    PrintWriter getWriter() throws IOException;

    OutputStream getOutputStream() throws IOException;
}
Ⅱ 编写tomcat模块

在这里插入图片描述

(1) Server

TCP 服务器

  • 创建 Socket
  • 绑定本地 ip + port
  • 对 socket 进行 Listen
  • 通过调用 accept 等待三次握手成功的客户端

TCP建立连接后, 接下来所有和该客户端通信的过程,全部封装成 TransactionTask 任务

public class Server {
    private static final int PORT = 8080;
    // 主线程只处理前台的事务
    public static void main(String[] args) throws IOException {
        /**
         * 通过使用线程池的方式,支持客户端并发处理
         * TODO: NIO 的方式支持并发
         */
        ExecutorService pool = Executors.newFixedThreadPool(8);

        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            while (true) {
                Socket socket = serverSocket.accept();
                // 应用层取到一个 ESTABLISHED 状态连接对应的 socket
                Runnable task = new TransactionTask(socket);
                // 交给线程池去处理
                pool.execute(task);
            }
        }
    }
}
(2) TransactionTask.Java
  • 解析请求并封装;
  • 初始化响应;
  • 判断是否是静态文件,如果是静态文件,就将servlet对象指向staticResourceServlet对象
  • 如果不是静态文件,则调用WebXML.map(request.getPath());拿到对应的servlet;
        // <servlet>
        servlet.put("Hello", new HelloServlet());
        servlet.put("Login", new LoginServlet());
        // <servlet-mapping>
        servletMapping.put("/login", "Login");
        servletMapping.put("/hello", "Hello");
  • 如果servlet==null就让servlet = notFoundServlet;
(3)开始解析请求:
//请求行
GET /homebd/egf.gif?authorization=bce&name=glp HTTP/1.1 

//请求头
Host: static.home.baidu.com
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
HttpServletRequestImpl request = 
HttpServletRequestImpl.readAndParse(socket.getInputStream());

通过socket.getInputStream()获得TCP输入流,使用 Scanner scanner = new Scanner(inputStream, "UTF-8"); 一行一行的读取输入流(HTTP 协议中大多使用 CRLF 进行分割,需要一个方便读取一行的类进行处理 —— Scanner). 每读取一行就调用一下 String line = scanner.nextLine();

1)解析请求行

  String[] group = line.split(" ");
  request.method = group[0]; //方法
  String url = group[1]; //url部分

请求行以空格分割后,可以轻易拿到请求方法,然后再解析 url 部分,HTTP/1.1 版本协议我们省略,分出 url 的 path 和 parameter 部分
注:
解析请求时我们不考虑完整的 URL(http://www.baidu.com/index.html), 只考虑相对的 URL(/index.html)

划分url的路径及参数请求部分:

///homebd/egf.gif?authorization=bce&name=glp
String[] group = url.split("\\?");
request.path = URLDecoder.decode(group[0], "UTF-8");

//authorization=bce&name=glp
//当有多个参数时
 String[] fragments = group[1].split("&");

2) 解析请求头

//请求头
Host: static.home.baidu.com
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache

使用 readAndParseRequestHeaders(request, scanner);解析请求头;
在这里插入图片描述
以key-Value的形式划分请求头,使用Scanner一行一行的读,当读到空行时,请求头读取结束

line = scanner.nextLine();
// Key: Value
 String[] group = line.split(":");

到此为止:请求封装完成。

(4) 构建一个空的 HttpServletResponseImpl 对象(进行初始化)

调用:HttpServletResponseImpl.build(socket.getOutputStream());

 public static HttpServletResponseImpl build(OutputStream outputStream) throws UnsupportedEncodingException {
        HttpServletResponseImpl response = new HttpServletResponseImpl();

        // 基本的初始化功能
        response.outputStream = outputStream;
        return response;
    }
(5) 根据请求路径,寻找对应的servlet

对应的servlet即为继承了HttpServlet的类,public class StaticResourceServlet extends HttpServlet 。 如果对应路径为静态资源,则让servlet指向StaticResourceServlet(解析静态资源是tomcat的工作),如果对应路径为我们编写的servlet类,则让servlet指向对应的自定义servlet类,如果路径不匹配则让servlet指向notFoundServlet;

采用url优先匹配静态方法,根据路径和方法(GET/POST),确定Servlet对象:
在这里插入图片描述

(6)调用servlet中的service方法

会根据不同的 method,调用 doGet或doPost, response 被填充了

 servlet.service(request, response); 

注意:

  • 这里的request是HttpServletRequest的实现类对象,HttpServletRequestImpl request =new HttpServletRequestImpl();
  • 这里的response是HttpServletResponse的实现类对象,HttpServletResponseImpl response =new HttpServletResponseImpl();
(7)封装HTTP响应并发送

要先写入相应行,再写入响应头,最后写入响应体

   response.send();
Ⅲ 自定义servlet
public class LoginServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String username = req.getParameter("username");
        String password = req.getParameter("password");

        // TODO: 验证用户名密码是否正确
        // TODO: 设置 Session 信息

        resp.setContentType("text/plain; charset=utf-8");
        PrintWriter writer = resp.getWriter();
        writer.println("登录成功");
    }
}
Ⅳ 流程及注意事项
1 大体流程:

(1)根据读到的数据,构建请求对象;
(2)构建一个空的响应对象;
(3)选择合适的Servlet对象;
(4)调用service()方法,会填充响应对象的状态、头信息、正文;
(5)拿到完整的响应对象;
(6)根据完整的响应对象,按照HTTP协议标准,调用send()发送数据到socket()中;
(7)关闭socket();

2 具体需要实现:

1.TCP Server(程序入口,用线程池的方式处理多个客户端的连接,负责前台事务,具体的任务交给TransactionTask去做)
2.接收HTTP请求,解析HTTP请求,并将其封装为HttpServletRequest对象。
3.实例化HTTPServletResponse对象
4.区分静态资源,动态资源,资源访问不到的情况
(1)静态资源:读取静态资源,并返回一个Servlet对象。
(2)动态资源:把request/response交给Servlet中的doGet/doPost进行处理,并返回一个Servlet对象。
(3)404无法找到资源:返回一个NotFoundServlet对象。
5. 把HTTPServletResponse对象转换为HTTP响应并发送。

注意:
(1)利用service方法执行对应的doGet或doPost方法(这里用到动态绑定),完成对response的封装。其中response响应体的内容都是写在bodyOutputStream(一个输出到内存数组的输出流)中, 在向HTTP写入响应时,需要先将响应内容刷新到bodyOutputStream中,然后才能在bodyOutputStream中拿到内容,写入到HTTP响应中。

因为标准的HTTP响应,应该先写入相应行,再写入响应头,最后才是正文,所以
在写入响应时,不能直接将响应写入到socket中,因为响应必须先写响应行再写响应头,最后写正文。所以我们需要先将响应体部分写入到bodyOutputStream中,经过组合后,再将响应写入到socket中。

3 接口动态绑定及刷新缓冲区

HttpServletResponse的实现类,HttpServletResponseImpl implements HttpServletResponse:

private OutputStream outputStream;

private ByteArrayOutputStream bodyOutputStream = new ByteArrayOutputStream(8192);
    
private PrintWriter bodyPrintWriter;

public HttpServletResponseImpl() throws UnsupportedEncodingException {
    bodyPrintWriter = new PrintWriter(new OutputStreamWriter(bodyOutputStream, "UTF-8"));
}

@Override
public PrintWriter getWriter() throws IOException {
    return bodyPrintWriter;
}

 @Override
public OutputStream getOutputStream() {
     return bodyOutputStream;
}

因为调用service时 servlet.service(request, response);传入的是HttpServletResponse的实现类,以及HttpServletRequest的实现类。所以在HttpServlet实现类中(自定义的servlet,静态servlet,notfundServlet),调用doGet(HttpServletRequest req, HttpServletResponse resp)时,HttpServletRequest req,以及HttpServletResponse resp都发生了动态绑定:
即自定义servlet中调用的PrintWriter writer = resp.getWriter();方法实际是HttpServletResponseImpl 中的方法。即响应内容都输出到了bodyOutputStream 中;

public void send() throws IOException {
        // 1. 强制把所有 body 的内容都刷新到最终的目的 buffer(缓冲区) 中
        bodyPrintWriter.flush();

        // 3. 最后写响应体
        sendResponseBody(outputStream);
}

    private void sendResponseBody(OutputStream outputStream) throws IOException {
        outputStream.write(bodyOutputStream.toByteArray());
    }

注:
什么是缓冲区:
缓冲区(buffer),它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来 缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区,显然缓冲区是具有一定大小的。

为什么需要刷新缓冲区:
举实例来说,比如设定缓冲长度是50字符。也就是说,每次cout输送50字符就强制输出一次。如果最后一次是个hello world,那么缓冲没有填满,就不会立即打印,而等待下一次缓冲满一并输出。 如果在此之前程序结束了,那么这行输出很可能就被吞掉了。如果输出hello world之后发送一个endl到stdout,那么此时这句hello world就被强行冲出缓冲区直接进行打印,也就没有输出被吞没的问题了。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值