ServletAPI和运行原理

目录​​​​​​​

1.Servlet运行原理

1.1Tomcat的定位

1.2交互过程:

1.3Tomcat 的伪代码

2.Servlet API详解

1.HttpServlet

2.HttpServletRequest

3.HttpServletResponse

3.实现服务器版简易表白墙 


1.Servlet运行原理

Servlet 的代码中我们并没有写 main 方法, 那么对应的 doGet 代码是如何被调用的呢? 响应又是如何返回给浏览器的?

1.1Tomcat的定位

我们自己的实现是在 Tomcat 基础上运行的。

当浏览器给服务器发送请求的时候, Tomcat 作为 HTTP 服务器, 就可接收到这个请求.

HTTP 协议作为一个应用层协议, 需要底层协议栈来支持工作. 如下图所示:

更详细的交互过程可以参考下图:

1.2交互过程:

1. 接收请求:

用户在浏览器输入一个 URL, 此时浏览器就会构造一个 HTTP 请求.

这个 HTTP 请求会经过网络协议栈逐层进行封装成二进制的 bit , 最终通过物理层的硬件设备转换成光信号/电信号传输出去.这些承载信息的光信号/电信号通过互联网上的一系列网络设备, 最终到达目标主机(这个过程也需要 网络层和数据链路层参与).

服务器主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行 分用, 层层解析, 最终还原成 HTTP 请求. 并交给 Tomcat 进程进行处理(根据端口号确定进程)

Tomcat 通过 Socket 读取到这个请求(一个字符串), 并按照 HTTP 请求的格式来解析这个请求, 根据请求中的 Context Path 确定一个 webapp, 再通过 Servlet Path 确定一个具体的类. 再根据当前请求的方法 (GET/POST/...), 决定调用这个类的 doGet doPost 等方法. 此时我们的代码中的

doGet / doPost 方法的第一个参数 HttpServletRequest 就包含了这个 HTTP 请求的详细信息.

2. 根据请求计算响应:

在我们的 doGet / doPost 方法中, 就执行到了我们自己的代码. 我们自己的代码会根据请求中的一些信息, HttpServletResponse 对象设置一些属性. 例如状态码, header, body .

3. 返回响应:

  我们的 doGet / doPost 执行完毕后, Tomcat 就会自动把 HttpServletResponse 这个我们刚设置好的对象转换成一个符合 HTTP 协议的字符串, 通过 Socket 把这个响应发送出去.

  此时响应数据在服务器的主机上通过网络协议栈层层 封装, 最终又得到一个二进制的 bit , 通过物理层硬件设备转换成光信号/电信号传输出去.

  这些承载信息的光信号/电信号通过互联网上的一系列网络设备, 最终到达浏览器所在的主机(这个过程也需要网络层和数据链路层参与).

  浏览器主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行分用, 层层解析, 最终还原成 HTTP 响应, 并交给浏览器处理.

  浏览器也通过 Socket 读到这个响应(一个字符串), 按照 HTTP 响应的格式来解析这个响应. 并且把 body 中的数据按照一定的格式显示在浏览器的界面.

1.3Tomcat 的伪代码

下面的代码通过 "伪代码" 的形式描述了 Tomcat 初始化/处理请求 两部分核心逻辑.

所谓 "伪代码", 并不是一些语法严谨, 能完备的代码, 只是通过这种形式来大概表达某种逻辑.

1. Tomcat 初始化流程

class Tomcat {
    // 用来存储所有的 Servlet 对象
    private List<Servlet> instanceList = new ArrayList<>();

    public void start() {
        // 根据约定,读取 WEB-INF/web.xml 配置文件;
        // 并解析被 @WebServlet 注解修饰的类

        // 假定这个数组里就包含了我们解析到的所有被 @WebServlet 注解修饰的类 .
        Class<Servlet>[] allServletClasses = ...;

        // 这里要做的的是实例化出所有的 Servlet 对象出来;
        for (Class<Servlet> cls : allServletClasses) {
            // 这里是利用 java 中的反射特性做的
            // 实际上还得涉及一个类的加载问题,因为我们的类字节码文件,是按照约定的
            // 方式(全部在 WEB-INF/classes 文件夹下)存放的,所以 tomcat 内部是 
            // 实现了一个自定义的类加载器(ClassLoader)用来负责这部分工作。

            Servlet ins = cls.newInstance();
            instanceList.add(ins);
        }
        // 调用每个 Servlet 对象的 init() 方法,这个方法在对象的生命中只会被调用这一次;
        for (Servlet ins : instanceList) {
            ins.init();
        }
        // 利用我们之前学过的知识,启动一个 HTTP 服务器
        // 并用线程池的方式分别处理每一个 Request
        ServerSocket serverSocket = new ServerSocket(8080);
        // 实际上 tomcat 不是用的固定线程池,这里只是为了说明情况
        ExecuteService pool = Executors.newFixedThreadPool(100);

        while (true) {
            Socket socket = ServerSocket.accept();
        // 每个请求都是用一个线程独立支持,这里体现了我们 Servlet 是运行在多线程环境下的
            pool.execute(new Runnable() {
                doHttpRequest(socket);
            });
    }


        for(Servlet ins :instanceList) {
            ins.destroy();
    }

}

    public static void main(String[] args) {
        new Tomcat().start();
    }
}

小结

Tomcat 的代码中内置了 main 方法. 我们启动 Tomcat , 是从 Tomcat main 方法开始执行

 @WebServlet 注解修饰的类会在 Tomcat 启动的时候就被获取到, 并集中管理. 

Tomcat 通过 反射 这样的语法机制来创建被 @WebServlet 注解修饰的类的实例.

这些实例被创建完了之后, 会点调用其中的 init 方法进行初始化. (这个方法是 HttpServlet 带的, 我们自己写的类可以重写 init)

这些实例被销毁之前, 会调用其中的 destory 方法进行收尾工作. (这个方法是 HttpServlet 自带的, 我们自己写的类可以重写 destory)

Tomcat 内部也是通过 Socket API 进行网络通信.

Tomcat 为了同时相应多个 HTTP 请求, 用多线程的方式实现. 因此 Servlet 是运行在多线程环境

 2. Tomcat 处理请求流程

class Tomcat {
    void doHttpRequest(Socket socket) {
        // 参照我们之前学习的HTTP服务器类似的原理,进行 HTTP 协议的请求解析,和响应构建
        HttpServletRequest req = HttpServletRequest.parse(socket);
        HttpServletRequest resp = HttpServletRequest.build(socket);
// 判断 URL 对应的文件是否可以直接在我们的根路径上找到对应的文件,如果找到,就是静态内容
// 直接使用我们学习过的 IO 进行内容输出
        if (file.exists()) {
            // 返回静态内容
            return;
         }

// 根据我们在配置中说的,按照 URL -> servlet-name -> Servlet 对象的链条 
// 最终找到要处理本次请求的 Servlet 对象
        Servlet ins = findInstance(req.getURL());

        // 调用 Servlet 对象的 service 方法
// 这里就会最终调用到我们自己写的 HttpServlet 的子类里的方法了

        try {
            ins.service(req, resp);
        } catch (Exception e) {
            // 返回 500 页面,表示服务器内部错误

        }
    }
}

小结

 Tomcat Socket 中读到的 HTTP 请求是一个字符串, 然后会按照 HTTP 协议的格式解析成一个HttpServletRequest 对象.

Tomcat 会根据 URL 中的 path 判定这个请求是请求一个静态资源还是动态资源. 如果是静态资源, 直接找到对应的文件把文件的内容通过 Socket 返回. 如果是动态资源, 才会执行到 Servlet 的相关逻辑.

Tomcat 会根据 URL 中的 Context Path Servlet Path 确定要调用哪个 Servlet 实例的 service . 通过 service 方法, 就会进一步调用到我们之前写的 doGet 或者 doPost

3. Servlet service 方法的实现

class Servlet {
public void service(HttpServletRequest req, HttpServletResponse resp) { 
    String method = req.getMethod();
        if (method.equals("GET")) {
            doGet(req, resp);

        } else if (method.equals("POST")) { 
            doPost(req, resp);
        } else if (method.equals("PUT")) {
            doPut(req, resp);
        } else if (method.equals("DELETE")) { 
            doDelete(req, resp);
        }
        ......
    }
}

小结

 Servlet service 方法内部会根据当前请求的方法, 决定调用其中的某个 doXXX .

 在调用 doXXX 方法的时候, 就会触发 多态 机制, 从而执行到我们自己写的子类中的 doXXX 方法.

等价代码:

Servlet ins = new HelloServlet();
ins.doGet(req, resp);

2.Servlet API详解

1.HttpServlet

我们写 Servlet 代码的时候, 首先第一步就是先创建类, 继承自 HttpServlet, 并重写其中的某些方法

核心方法:

方法名称

调用时机

init

HttpServlet 实例化之后被调用一次

destory

HttpServlet 实例不再使用的时候调用一次

service

收到 HTTP 请求的时候调用

doGet

收到 GET 请求的时候调用(service 方法调用)

doPost

收到 POST 请求的时候调用(service 方法调用)

doPut/doDelete/doOptions/...

收到其他请求的时候调用(service 方法调用)

这些方法的调用时机, 就称为 "Servlet 生命周期". (是描述了一个 Servlet 实例从生到死的过程).

 

注意: HttpServlet 的实例只是在程序启动时创建一次. 而不是每次收到 HTTP 请求都重新创建实例.

2.HttpServletRequest

Tomcat 通过 Socket API 读取 HTTP 请求(字符串), 并且按照 HTTP 协议的格式把字符串解析成 HttpServletRequest 对象.

核心方法

方法

描述

String getProtocol()

返回请求协议的名称和版本。

String getMethod()

返回请求的 HTTP 方法的名称,例如 GET POST  PUT

String getRequestURI()

从协议名称直到 HTTP 请求的第一行的查询字符串中,返回该请求的 URL 的一部分。

String getContextPath()

返回指示请求上下文的请求 URI 部分。

String getQueryString()

返回包含在路径后的请求 URL 中的查询字符串。

Enumeration

返回一个 String 对象的枚举,包含在该请求中包含的参数的名称。

String getParameter(String name)

以字符串形式返回请求参数的值,或者如果参数不存在则返回 null

String[]

返回一个字符串对象的数组,包含所有给定的请求参数的值,如 果参数不存在则返回 null

Enumeration

返回一个枚举,包含在该请求中包含的所有的头名。

String getHeader(String name)

以字符串形式返回指定的请求头的值。

String getCharacterEncoding()

返回请求主体中使用的字符编码的名称。

String getContentType()

返回请求主体的 MIME 类型,如果不知道类型则返回 null 

intgetContentLength()

以字节为单位返回请求主体的长度,并提供输入流,或者如果长度未知则返回 -1

InputStream getInputStream()

用于读取请求的 body 内容. 返回一个 InputStream 对象.

通过这些方法可以获取到一个请求中的各个方面的信息.

注意: 请求对象是服务器收到的内容, 不应该修改. 因此上面的方法也都只是 "" 方法, 而不是 "" 方法.

关于乱码问题

如果我们在响应代码中写入中文, 例如

resp.getWriter().write("GET 响应");

此时在浏览器访问的时候, 会看到 "乱码" 的情况. 

可以在代码中, 通过  resp.setContentType("text/html;   charset=utf-8");  显式的指定编码方式.

此时通过抓包可以看到, 当加上了  resp.setContentType("text/html;  charset=utf-8");   代码之后, 响应中多了 Content-Type 字段, 内部指定了编码方式. 浏览器看到这个字段就能够正确解析中文了.

代码示例:打印请求信息 

@WebServlet("/request")
public class RequestServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 上面这个操作也是必要的. 显式告诉浏览器, 你拿到的数据是 html.
        resp.setContentType("text/html");
        // 调用 req 的各个方法, 把得到的结果汇总到一个字符串中, 统一返回到页面上.
        StringBuilder respBody = new StringBuilder();

        // 下列内容是在浏览器上按照 html 的方式来展示的. 此时 \n 在 html 中并不是换行.
        // 而使用 <br> 标签表示换行
        respBody.append(req.getProtocol());
        respBody.append("<br>");
        respBody.append(req.getMethod());
        respBody.append("<br>");
        respBody.append(req.getRequestURI());
        respBody.append("<br>");
        respBody.append(req.getContextPath());
        respBody.append("<br>");
        respBody.append(req.getQueryString());
        respBody.append("<br>");

        // 拼接 header
        Enumeration<String> headers = req.getHeaderNames();
        while (headers.hasMoreElements()) {
            String header = headers.nextElement();
            respBody.append(header + ": " + req.getHeader(header));
            respBody.append("<br>");
        }

        // 统一返回结果
        resp.getWriter().write(respBody.toString());
    }
}

运行结果:

3.HttpServletResponse

Servlet 中的 doXXX 方法的目的就是根据请求计算得到相应, 然后把响应的数据设置到 HttpServletResponse 对象中.

然后 Tomcat 就会把这个HttpServletResponse 对象按照 HTTP 协议的格式, 转成一个字符串, 并通过 Socket 写回给浏览器.

核心方法

方法

描述

void setStatus(intsc)

为该响应设置状态码

void setHeader(String name, String value)

设置一个带有给定的名称和值的 header.如果 name 已经存在, 则覆盖旧的值.

void addHeader(String name, String value)

添加一个带有给定的名称和值的 header.如果 name 已经存在, 不覆盖旧的值, 并列添加新的键值对

void setContentType(String type)

设置被发送到客户端的响应的内容类型。

void setCharacterEncoding(String charset)

设置被发送到客户端的响应的字符编码(MIME 字符集)例如 UTF-8

void sendRedirect(String location)

使用指定的重定向位置 URL 发送临时重定向响应到客户端。

PrintWriter getWriter()

用于往 body 中写入文本格式数据.

OutputStream getOutputStream()

用于往 body 中写入二进制格式数据.

注意: 响应对象是服务器要返回给浏览器的内容, 这里的重要信息都是程序猿设置的. 因此上面的方法都是“写”方法. 对于状态码/响应头的设置要放到 getWriter / getOutputStream 之前. 否则可能设置失效.

3.实现服务器版简易表白墙 

前端代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>表白墙</title>
    <style>
        /* * 通配符选择器, 是选中页面所有元素 */
        * {
            /* 消除浏览器的默认样式. */
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        .container {
            width: 600px;
            margin: 20px auto;
        }

        h1 {
            text-align: center;
        }

        p {
            text-align: center;
            color: #666;
            margin: 20px 0;
        }

        .row {
            /* 开启弹性布局 */
            display: flex;
            height: 40px;
            /* 水平方向居中 */
            justify-content: center;
            /* 垂直方向居中 */
            align-items: center;
        }

        .row span {
            width: 80px;
        }

        .row input {
            width: 200px;
            height: 30px;
        }

        .row button {
            width: 280px;
            height: 30px;
            color: white;
            background-color: orange;
            /* 去掉边框 */
            border: none;
            border-radius: 5px;
        }

        /* 点击的时候有个反馈 */
        .row button:active {
            background-color: grey;
        }
    </style>
</head>
<body>
<div class="container">
    <h1>表白墙</h1>
    <p>输入内容后点击提交, 信息会显示到下方表格中</p>
    <div class="row">
        <span>谁: </span>
        <input type="text">
    </div>
    <div class="row">
        <span>对谁: </span>
        <input type="text">
    </div>
    <div class="row">
        <span>说: </span>
        <input type="text">
    </div>
    <div class="row">
        <button id="submit">提交</button>
    </div>
    <!-- <div class="row">
        xxx 对 xx 说 xxxx
    </div> -->
</div>

<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>

<script>
    // 实现提交操作. 点击提交按钮, 就能够把用户输入的内容提交到页面上显示.
    // 点击的时候, 获取到三个输入框中的文本内容
    // 创建一个新的 div.row 把内容构造到这个 div 中即可.
    let containerDiv = document.querySelector('.container');
    let inputs = document.querySelectorAll('input');
    let button = document.querySelector('#submit');
    button.onclick = function() {
        // 1. 获取到三个输入框的内容
        let from = inputs[0].value;
        let to = inputs[1].value;
        let msg = inputs[2].value;
        if (from == '' || to == '' || msg == '') {
            return;
        }
        // 2. 构造新 div
        let rowDiv = document.createElement('div');
        rowDiv.className = 'row message';
        rowDiv.innerHTML = from + ' 对 ' + to + ' 说: ' + msg;
        containerDiv.appendChild(rowDiv);
        // 3. 清空之前的输入框内容
        for (let input of inputs) {
            input.value = '';
        }

        // 4. 把用户输入的数据, 构造出 HTTP 请求, 发送给服务器.
        let body = {
            from: from,
            to: to,
            message: msg
        };
        $.ajax({
            type: 'post',
            // url: '/messageWall/message',
            url: 'message',
            contentType: 'application/json; charset=utf8',
            data: JSON.stringify(body),
            success: function(body) {
                // 预期 body 中返回 ok
                console.log(body);
            }
        });
    }

    // 在页面加载的时候, 发起一个 GET 请求给服务器, 从服务器拿到提交过的数据.
    $.ajax({
        type: 'get',
        url: 'message',
        success: function(body) {
            // 但是由于 jquery 见到响应中的 application/json , 就会自动的把响应
            // 转换成 js 对象数组. body 其实是 js 对象数组, 而不是 json 字符串了.
            // 就可以直接按照数组的方式来操作 body, 每个元素都是 js 对象

            // 1. 遍历数组, 取出每个 js 对象.
            // 2. 根据这里的 js 对象构造出 页面元素, 显示到页面上
            let container = document.querySelector('.container');
            for (let i = 0; i < body.length; i++) {
                let message = body[i];
                // 此处 message 对象, 就形如
                // {
                //   from: '黑猫',
                //   to: '白猫',
                //   message: '喵'
                // }
                // 构造出 html 元素!!
                // 使用浏览器提供的 api (dom api) 创建 div 标签.
                let div = document.createElement('div');
                // 设置一个 CSS 的类. 应用到 CSS 样式了.
                div.className = 'row';
                // 给 div 标签里设置内容, 此处显示一行文本.
                div.innerHTML = message.from + " 对 " + message.to + " 说: " + message.message;
                container.appendChild(div);
            }
        }
    });
</script>
</body>
</html>

 后端代码:

class Message {
    public String from;
    public String to;
    public String message;

    @Override
    public String toString() {
        return "Message{" +
                "from='" + from + '\'' +
                ", to='" + to + '\'' +
                ", message='" + message + '\'' +
                '}';
    }
}

@WebServlet("/message")
public class MessageServlet extends HttpServlet {
    private ObjectMapper objectMapper = new ObjectMapper();

    // 此处把消息保存到内存中. (一旦重启服务器, 内存数据就没了), 更科学的做法, 应该是保存到数据库中.
    // 不打算使用内存存储了. 这个 List 就可以删掉了.
//     private List<Message> messageList = new ArrayList<>();

    // 用来实现 "存档" 功能
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1. 读取请求 body, 转换成 java 对象
        Message message = objectMapper.readValue(req.getInputStream(), Message.class);
        // 2. 得到 message 了之后, 把这个 message 保存到服务器中.
//         messageList.add(message);
        try {
            save(message);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        System.out.println("服务器收到 message: " + message);
        // 3. 返回响应. (其实不太必要, 主要是返回一个 200 OK 就行了, body 可以没有
        resp.setStatus(200);
        resp.getWriter().write("ok");
    }

    // 用来实现 "读档" 功能
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 把内存中的这些 Message, 组织成 json 格式, 返回到响应中.
        List<Message> messageList = null;
        try {
            messageList = load();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        String respJson = objectMapper.writeValueAsString(messageList);
        resp.setContentType("application/json; charset=utf8");
        resp.getWriter().write(respJson);
    }

    private void save(Message message) throws SQLException {
        // 通过 jdbc, 往数据库中存一个数据.
        // 1. 创建数据源
        DataSource dataSource = new MysqlDataSource();
        ((MysqlDataSource) dataSource).setUrl("jdbc:mysql://127.0.0.1:3306/msgwall?characterEncoding=utf8&useSSL=false");
        ((MysqlDataSource) dataSource).setUser("root");
        ((MysqlDataSource) dataSource).setPassword("123456");
        // 2. 建立连接
        Connection connection = dataSource.getConnection();
        // 3. 构造 SQL
        String sql = "insert into message values(?, ?, ?)";
        PreparedStatement statement = connection.prepareStatement(sql);
        statement.setString(1, message.from);
        statement.setString(2, message.to);
        statement.setString(3, message.message);
        // 4. 执行 SQL
        statement.executeUpdate();
        // 5. 释放资源, 关闭连接
        statement.close();
        connection.close();
    }

    private List<Message> load() throws SQLException {
        // 通过 jdbc, 从数据库读取出数据
        // 1. 创建数据源
        DataSource dataSource = new MysqlDataSource();
        ((MysqlDataSource) dataSource).setUrl("jdbc:mysql://127.0.0.1:3306/msgwall?characterEncoding=utf8&useSSL=false");
        ((MysqlDataSource) dataSource).setUser("root");
        ((MysqlDataSource) dataSource).setPassword("123456");
        // 2. 建立连接
        Connection connection = dataSource.getConnection();
        // 3. 构造 SQL
        String sql = "select * from message";
        PreparedStatement statement = connection.prepareStatement(sql);
        // 4. 执行 SQL
        ResultSet resultSet = statement.executeQuery();
        // 5. 遍历结果集合
        List<Message> messageList = new ArrayList<>();
        while (resultSet.next()) {
            Message message = new Message();
            message.from = resultSet.getString("from");
            message.to = resultSet.getString("to");
            message.message = resultSet.getString("message");
            messageList.add(message);
        }
        // 6. 关闭连接释放资源
        resultSet.close();
        statement.close();
        connection.close();
        return messageList;
    }
}

效果展示:

数据存储在数据库中,所以重启服务器和刷新页面都不会影响内容显示

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值