手写tomcat(三)-WEB服务器开发进阶

1.mytomcat【响应动态: 客户端访问小 Java 程序】

1.1.思路分析

在上一个版本中,浏览器客户端发送的请求是 http://127.0.0.1:8080/oa/index.html,从请求路径上看到浏览器客户端访问的是 WebApp 中的静态 html 页面,服务器找到该静态页面之后直接将html 页面响应到浏览器即可,但是如果浏览器发送这样的请求http://127.0.0.1:8080/oa/login?username=admin&password=123表示他们要访问的是什么呢?从请求路径的字面意思上理解,这是浏览器向服务器发送了一个登录的请求,需要服务器端执行一段“Java 程序”来处理这次的登录请求,负责处理登录的 Java 程序应当获取到浏览器提交的用户名和密码,并且负责连接数据库,验证用户名和密码是否正确,如果正确则响应给浏览器一条登录成功的信息,如果错误则响应给浏览器一条登录失败的信息。

处理登录的 Java 程序应该由谁来编写呢?编写完成之后,又应该由谁来管理调用呢?

我们一起来分析一下,将来系统中会存在很多功能,例如:银行账户转账、查询员工信息、保存员工信息、银行账户开户等,这些功能中的每一个功能都需要执行对应的服务器端 Java 程序来完成的,都是和具体的业务逻辑紧密相关的,显然这种 Java 程序的编写不应该是服务器的开发人员,因为服务器的开发人员不知道具体的业务是什么,所以像以上列举的每一个功能的实现都需要 WebApp 的开发人员来完成开发。 开发完成之后,将处理某个功能的 Java 程序放到 Web 服务器中,由 Web 服务器来负责管理调用。

大家需要注意的是,浏览器向服务器发送请求并且提交数据的格式是什么?这个提交数据的格式
其 实 属 于 HTTP 协 议 的 一 部 分 , 这 是 W3C 提 前 制 定 好 的 , 格 式 是 :
http://ip:port/uri?name=value&name=value.....,并且这个数据在 HTTP 协议的请求行上提交,最终会显示在浏览器的地址栏上。

那么,接下来让我们一起实现上面的功能,该功能的实现需要 Web 服务器开发人员、 WebApp 开发人员共同配合完成。我们现在的角色转变为 WebApp 的开发人员,开始编写 Java 程序处理用户的登录请求。服务器端的小 java 程序英文是:Server Applet,所以我们把服务器端的小 java 程序叫做:Servlet。

1.2.webapp 开发人员:

A、 新建软件包 org.bruceliu.oa.servlet
B、 该软件包中新建 LoginServlet.java,编写 LoginServlet 处理用户的登录请求

在这里插入图片描述

package org.bruceliu.oa.servlet;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 21:39
 * @QQ:1241488705
 * @Description:处理登录业务的java程序,该java程序由webApp开发人,由web服务器开人员负责调用
 */
public class LoginServlet {
    //处理业务的核心类
    public void service(){
        System.out.println("正在验证身份,请稍等....");
    }
}

1.3.角色: web 服务器开发人员

接下来我们的开发角色再次转变为 WEB 服务器开发人员,开发服务器端程序调用 LoginServlet 的service 方法。大家设想一下,将来 WebApp 中不仅有一个 LoginServlet,还会有其它的小 Java 程序,每一个小 Java 程序都对应处理某一个业务,那么对于我们服务器的开发人员来说怎么确定调用哪个Servlet 呢?就像上一个版本中/oa/index.html 是服务器中的一个资源,像 html 这样的资源还有很多,服务器怎么知道浏览器客户端要访问的是哪个资源呢?当然是通过用户的请求路径了。其实LoginServlet 也是服务器端的一种资源,只不过这种资源不是 html,而是一段 Java 程序,所以服务器也可以先截获用户的请求路径,然后通过请求 URI 决定调用哪个 Servlet。 因此可以说,“请求 URI” 和对应要执行的“Servlet 程序”之间有一种绑定关系。 那么接下来就让我们编写 Web 服务器端的程序,截获请求 URI,根据请求 URI 来对应调用 Servlet。(HandlerRequest)

A、 在 HandlerRequest 截获请求 URI,根据请求 URI 来对应调用 Servlet。
B、 紧接上一版本,判断完是否为静态资源,如不是静态页面那肯定是动态资源
//判断用户请求是否为一个静态页面:以.html或.htm结尾的文件叫作html页面
if(requestURI.endsWith(".html") || requestURI.endsWith(".htm")){
	//处理静态页面的方法
	responseStaticPage(requestURI,out);
}else{//动态资源:java程序,业务处理类
	//requestURI: /oa/login?username=zhangsan&password=111
	//requestURI: /oa/login
	
	String servletPath = requestURI;
	//判断servletPath是否包含参数
	if(servletPath.contains("?")){
		servletPath = servletPath.split("[?]")[0];// /oa/login
	}
		
	if("/oa/login".equals(servletPath)){
		LoginServlet loginServlet = new LoginServlet();
		loginServlet.service();
	}
}

a、 动态请求分两种情况
requestURI: /oa/login?username=zhangsan&password=123

if(servletPath.contains(?))
servletPath = servletPath.split("[?]")[0];

requestURI: /oa/login

if("/oa/login".equals(servletPath))
loginServlet.service()

(4) 启动 httpserver,打开浏览器,输入 URL,发送请求

http://localhost:8080/oa/login?username=zhangsan&password=123

在这里插入图片描述

http://localhost:8080/oa/login

在这里插入图片描述

1.4.mytomcat【Web 服务器代码和 WebApp 代码解耦合】

在这里插入图片描述
分析以上代码, LoginServlet 类是 JavaWeb 程序员开发的, 而 HandlerRequest 类是服务器开发人员开发的,在服务器中的代码关心了具体的 Servlet 类,显然服务器的程序和 JavaWeb 程序产生了依赖,具有高强度的耦合,实际上对于 Web 服务器来说, 根本就不知道 Web 应用中有一个LoginServlet 类,上面的程序中还使用了“new LoginServlet();”,这显然是错误的。 另外在上面的 Web 服务器程序中编写了具体的请求路径/oa/login,这显然是不合理的, 对于 Web 服务器来说浏览器客户端发送的请求是未知的。 但是我们知道浏览器发送的请求路径/oa/login 是和底层 WebApp 中的 LoginServlet 存在映射关系/绑定关系的。 而这种关系的指定必须由 WebApp 的开发人员来指定,我相信大家此时应该想到了“配置文件”,在配置文件中指定请求 URI 和对应要执行的 Servlet。 该配置文件的编写由 WebApp程序员来完成,但是该文件由 Web 服务器开发人员读取并解析,所以该文件的名字、该文件的存放位置、该文件中所编写的配置元素都不能随意编写,必须提前制定好一个规范,那么这个规范由谁来制定呢?

当然是由 SUN 公司来负责制定。

接下来我们的角色再次发生了转变,我们现在是规范的制定者 SUN。制定规范如下所示,规范是规定,没有为什么,以后服务器开发人员和 WebApp 的开发人员只要严格按照规范开发即可。 下面这段规范是这样制定的:

(1)、SUN 制定的配置文件规范:

A、 在配置文件 web.xml 中描述请求 URI 和对应的 Servlet 类之间的关系
B、 web.xml 文件统一存放到 WebAppRoot/WEB-INF/目录下
C、 web.xml 文件中采用这样的标签描述请求 URI 和对应 Servlet 类之间的关系

在这里插入图片描述
以上配置文件规定了什么呢?

第一:配置文件的名字;
第二:配置文件存放位置;
第三:配置文件中编写哪些标签。 接下来我们的角色转变为 WebApp 的开发人员,按照规范新建 web.xml 文件,并将其存放到规范中要求的路径下,在该文件中编写规范中规定的标签,如下图所示

D、 在 OA 应用下创建 WEB-INF 文件夹,并且在该文件夹下创建 web.xml
在这里插入图片描述

<?xml version="1.0" encoding="UTF-8"?>
<web-app>
    <servlet>
        <servlet-name>loginServlet</servlet-name>
        <servlet-class>org.bruceliu.oa.servlet.LoginServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>loginServlet</servlet-name>
        <url-pattern>/login</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>userSaveServlet</servlet-name>
        <servlet-class>org.bruceliu.oa.servlet.UserSaveServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>userSaveServlet</servlet-name>
        <url-pattern>/user/save</url-pattern>
    </servlet-mapping>
</web-app>

E、 在 org.bruceliu.oa.servlet 下创建 LoginServlet 类,处理登录信息
在这里插入图片描述

1.4.角色: web 服务器开发人员

A、 服务器开发者负责解析 web.xml 文件
a、 通过 Dom4j+xpath 解析 web.xml

B、 在 Web 服务器启动阶段解析 web.xml 文件
接下来我们的角色再次转变为 Web 服务器的开发人员,服务器开发者负责解析 web.xml 文件,那么这个文件在什么时候解析呢?解析出来的数据存放到哪里呢? 这些问题由服务器开发者决定,SUN 的规范中并没有特殊的要求。 web.xml 文件中主要配置了请求 URI 和对应的 Servlet 完整类名,请URI 更像一个 Map 集合的 key,对应的 Servlet 完整类名更像一个 Map 集合的 value,所以我们决定采用一个 Map 集合存储解析出来的数据。在浏览器发送请求的时候再去解析 web.xml 文件时有点晚了,所以我们决定在 Web 服务器启动阶段解析 web.xml 文件,以下是 Web 服务器开发人员编写的解析web.xml 文件的代码.

解析 web.xml 类 WebParser 类如下:

package com.bruceliu.httpserver.core;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 22:14
 * @QQ:1241488705
 * @Description:解析服务器中的web.xml配置文件
 */
public class WebParser {
    public static Map<String,Map<String,String>> servletMaps = new HashMap<String,Map<String,String>>();

    /**
     * 解析服务器所有应用的web.xml
     * @param webAppNames 服务器中所有应用的名称
     * @throws DocumentException
     */
    public static void parser(String[] webAppNames) throws DocumentException {
        for(String webAppName:webAppNames){
            Map<String,String> servletMap = parser(webAppName);
            servletMaps.put(webAppName, servletMap);
        }
    }

    /**
     * 解析单个应用的web.xml配置文件
     * @param webAppName 应用名称
     * @return servletMap<String,String> 
     * @throws DocumentException
     */
    private static Map<String,String> parser(String webAppName) throws DocumentException{
        //获取web.xml的路径
        String webPath = webAppName + "/WEB-INF/web.xml";
        //创建解析器
        SAXReader saxReader = new SAXReader();
        //通过解析器的read方法将配置文件读取到内存中,生成一个Document【org.dom4j】对象树
        Document document = saxReader.read(new File(webPath));

        //获取servlet节点元素: web-app -> servlet
        List<Element> servletNodes = document.selectNodes("/web-app/servlet");
        //创建一个servletInfoMap集合,将servlet-name和servlet-class的值分别当作key和value存放到该集合中
        Map<String,String> servletInfoMap = new HashMap<String,String>();
        //开始遍历servletNodes
        for(Element servletNode:servletNodes){
            //获取servlet-name节点元素对象
            Element servletNameElt = (Element) servletNode.selectSingleNode("servlet-name");
            //获取servletNameElt节点元素对象的值
            String servletName = servletNameElt.getStringValue();

            //获取servlet-class节点元素对象
            Element servletClassElt = (Element) servletNode.selectSingleNode("servlet-class");
            //获取servletClassElt节点元素对象的值
            String servletClassName = servletClassElt.getStringValue();

            //将servletName和servletClassName分别当作key和value存放到servletInfoMap集合中
            servletInfoMap.put(servletName, servletClassName);
        }

        //获取servlet-mapping节点元素对象: web-app -> servlet-mapping
        List<Element> servletMappingNodes = document.selectNodes("/web-app/servlet-mapping");
        //创建一个servletMappingInfoMap集合,将servlet-name和url-pattern节点元素对象的值分别当作key和value存放到该map集合中
        Map<String,String> servletMappingInfoMap = new HashMap<String,String>();
        //开始遍历servletMappingNodes
        for(Element servletMappingNode:servletMappingNodes){
            //获取servlet-name节点元素对象
            Element servletNameElt = (Element) servletMappingNode.selectSingleNode("servlet-name");
            //获取servletNameElt节点元素对象的值
            String servletName = servletNameElt.getStringValue();

            //获取url-pattern节点元素对象
            Element urlPatternElt = (Element) servletMappingNode.selectSingleNode("url-pattern");
            //获取urlPatternElt节点元素对象的值
            String urlPattern = urlPatternElt.getStringValue();

            //将servletName和urlPattern分别当作key和value存放到servletMappingInfoMap集合中
            servletMappingInfoMap.put(servletName, urlPattern);
        }

        //获取servletInfoMap或者servletMappingInfoMap任何一个key值的集合
        Set<String> servletNames = servletInfoMap.keySet();
        //创建一个servletMap集合,将servletMappingInfoMap的value和servletInfoMap的value分别当作key和value存放到该map集合中
        Map<String,String> servletMap = new HashMap<String,String>();
        //开始遍历servletNames
        for(String servletName:servletNames){
            //获取servletMappingInfoMap集合中的value:urlPattern
            String urlPattern = servletMappingInfoMap.get(servletName);
            //获取servletInfoMap集合中的value:servletClass
            String servletClassName = servletInfoMap.get(servletName);

            //将urlPattern和servletClassName分别当作key和value存放到servletMap集合中
            servletMap.put(urlPattern, servletClassName);
        }

        return servletMap;
    }
}

(3) 在服务器启动阶段解析每个 WebApp的 web.xml文件,在 BootStrap中加入如下代码

A、 修改 BootStrap 类中 start()方法
a、 在服务端套接字绑定端口号之后, 添加如下代码, 开始读取 web.xml 文件
b、 WebServer.parser(new String[]{“oa”});

/**
     * 启动入口
     */
    public static void start(){
        ServerSocket serverSocket = null;
        Socket clientSocket = null;
        BufferedReader br = null;
        try {
            Logger.log("httpserver start");
            //获取当前时间
            long start = System.currentTimeMillis();
            
            //解析服务中包含的web.xml配置文件
            String[] webAppNames = {"oa"};
            WebParser.parser(webAppNames);

            //获取系统端口号
            int port = ServerParser.getPort();
            Logger.log("httpserver-port: " + port);
            //创建服务器套接字,并且绑定端口号:8080
            serverSocket = new ServerSocket(port);
            //获取结束时间
            long end = System.currentTimeMillis();
            Logger.log("httpserver started: " + (end-start) + " ms");
            while(true){
                //开始监听网络,此时程序处于等待状态,等待接收客户端的消息
                clientSocket = serverSocket.accept();
				/*//接收客户端消息
				br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
				String temp = null;
				while((temp = br.readLine()) != null){
					System.out.println(temp);
				}*/
                new Thread(new HandlerRequest(clientSocket)).start();
            }
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally{
            //关闭资源
            if(br != null){
                try {
                    br.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            if(clientSocket != null){
                try {
                    clientSocket.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            if(serverSocket != null){
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }

以上代码在服务器启动阶段解析每个 WebApp 的 web.xml 文件,其中 WebApp 的名字 oa 写死到服务器程序中了,其实对于 Web 服务器来说根本就不知道该 Web 服务器下有哪些 WebApp。这个问题怎么解决呢?我们可以规定一个目录,例如该目录的名字叫做 webapps,让所有的 WebApp 都放到我们所指定的 webapps 目录下,这样我们就可以在 Web 服务器中动态获取所有 WebApp 的名字了,这个功能大家自己完成, 这里不再实现这个功能了。

现在我们的角色还是 Web 服务器开发人员, web.xml 文件已经在服务器启动阶段解析了,用户的
请求路径已经和对应的 Servlet 完整类名绑定在一起了,分析以下程序应该如何修改
在这里插入图片描述
(4) 我们在服务器端程序中可以获取到请求 URI,我们通过请求 URI 获取对应的 Servlet 完整类名
(5) 再通过反射机制,调用该 Servlet 类的无参数构造方法,完成 Servlet对象的创建,代码如下所示
A、 在 HandlerRequest 类中 run 方法里,修改”/oa/login”.equals(servletPath)

    @Override
    public void run() {
        //处理客户端请求
        BufferedReader br = null;
        PrintWriter out = null;
        Logger.log("httpserver thread: " + Thread.currentThread().getName());
        try {
            //接收客户端消息
            br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            //获取响应流对象
            out = new PrintWriter(clientSocket.getOutputStream());
            //获取请求协议的请求行
            String requestLine = br.readLine();// GET /oa/index.html HTTP/1.1
            //获取URI -> 请求行(requestLine) -> 请求方式  URI 请求协议版本号 -> 三者之间是通过一个空格进行连接
            String requestURI = requestLine.split(" ")[1];//{"GET","/oa/index.html","HTTP/1.1"}
            //判断用户请求是否为一个静态页面:以.html或.htm结尾的文件叫作html页面
            if(requestURI.endsWith(".html") || requestURI.endsWith(".htm")){
                //处理静态页面的方法
                responseStaticPage(requestURI,out);
            }else{

                //动态资源:java程序,业务处理类
                //requestURI: /oa/login?username=zhangsan&password=111
                //requestURI: /oa/login

                String servletPath = requestURI;
                //判断servletPath是否包含参数
                if(servletPath.contains("?")){
                    servletPath = servletPath.split("[?]")[0];// /oa/login
                }

                /*if("/oa/login".equals(servletPath)){
					LoginServlet loginServlet = new LoginServlet();
					loginServlet.service();
				}*/

                //获取应用的名称:oa 在 uri里:/oa/login
                String webAppName = servletPath.split("[/]")[1];
                //获取servletMaps集合中的value值->servletMap -> key:urlPattern value:servletClassName
                Map<String,String> servletMap = WebParser.servletMaps.get(webAppName);
                //获取servletMap集合中的key值 -> 存在于uri中/oa/login /oa/use/xxx/xxx/xxx/xxx
                String urlPattern = servletPath.substring(1 + webAppName.length());
                //获取servletClassName
                String servletClassName = servletMap.get(urlPattern);
                //通过反射机制创建该业务处理类
                Class c = Class.forName(servletClassName);
                Object obj = c.newInstance();
                //调用Servlet对象方法,怎么调用呢?我不知道Servlet中有什么方法,怎么办?
            }
            //强制刷新
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            //关闭资源
            if(br != null){
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(clientSocket != null){
                try {
                    clientSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

(6) Web 服务器的开发人员又遇到了新的问题
A、 虽然我们在服务器端根据请求 URI 找到了对应的 Servlet 完整类名,也通过反射机制将 Servlet 对象创建成功了
B、 但是对于 Web 服务器的开发人员来说并不知道该 Servlet 对象中有什么方法
C、 这个时候只有 SUN 再次出面制定规范了

a、 接下来我们的角色发生了改变,我们现在是 SUN 公司,制定一个 Servlet 接口
b、 该接口的实现者是 WebApp, 调用者 Web 服务器,以下是 SUN 公司制定的 Servlet
规范的核心接口
c、 新建 javax.servlet 包,创建一个 Servlet 接口
d、 该接口中包含:处理请求的核心方法 void service();

在这里插入图片描述

public interface Servlet{
       void service();//处理请求的核心方法
}

(7) 接下来我们的角色转变为 WebApp 的开发人员,让 LoginServlet 实现 Servlet 接口
在这里插入图片描述

/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 21:39
 * @QQ:1241488705
 * @Description:处理登录业务的java程序,该java程序由webApp开发人,由web服务器开人员负责调用
 */
public class LoginServlet implements Servlet {
    //处理业务的核心类
    public void service(){
        System.out.println("正在验证身份,请稍等....");

    }
}

(8) 接下来我们的角色转变为 Web 服务器的开发人员,
A、 在 HandlerRequest 类中面向 Servlet 接口调用 service 方法即可
B、 修改 HandlerRequest 方法,紧接着(5)继续编写代码

Servlet servlet = (Servlet)obj;
servlet.service();

在这里插入图片描述
(9) 启动 httpserver,打开浏览器, 输入 URL, 发送请求

http://127.0.0.1:8080/oa/login?user=zhangsan&password=123

在这里插入图片描述

1.5. mytomcat【访问的小 Java 不存在】

(1) 在以上程序的基础之上我们需要考虑请求的 URI 是否不存在,如果不存在需要提示客户端 404 错误,代码如下

//判断 servletClassName 是否为空
if(servletClassName != null){//不为空说明有请求处理的 servlet 类对象
    Class c = Class.forName(servletClassName);
    Object obj = c.newInstance();
    Servlet servlet = (Servlet)obj;
    servlet.service();
}else{//说明没有请求的 servlet 对象,返回 404 页面
    //404找不到资源
    StringBuilder html = new StringBuilder();
    html.append("HTTP/1.1 404 NotFound\n");
    html.append("Content-Type:text/html;charset=utf-8\n\n");
    html.append("<html>");
    html.append("<head>");
    html.append("<title>404-错误</title>");
    html.append("<meta content='text/html;charset=utf-8'/>");
    html.append("</head>");
    html.append("<body>");
    html.append("<center><font size='35px' color='red'>404-Not Found</font></center>");
    html.append("</body>");
    html.append("</html>");
    out.print(html);
}

(2) 启动 httpserver,打开浏览器,发送请求访问

http://127.0.0.1:8080/oa/test

在这里插入图片描述

1.6. mytomcat【小 Java 程序输出 html 到浏览器】

修改 Servlet 接口方法 service(PrintWriter out)
(1) 输出 html 的内容是由 WebApp 开发者决定的
(2) 所以对于 Web 服务器来说只需要将负责响应的标准输出流 out 传递给 Servlet 即可
(3) 将 HandlerRequest 类中的 servlet.service();代码修改为:

servlet.service(out);

(4) 在判断完该请求存在后, 添加将响应头的固定输出标准

out.print(“HTTP/1.1 200 OK\n”);//状态行
out.print(“Content-Type:html/text;charset=utf-8\n\n”);//响应头
发布了274 篇原创文章 · 获赞 80 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/BruceLiu_code/article/details/104188981
今日推荐