手写简单的Tomcat

在手写Tomcat前,先了解Tomcat和客户端的三个层次

1.客户端发出请求后,Tomcat接收后直接返回

2.客户端发出请求后,Tomcat启动一个线程并返回响应

3.客户端发出请求后,Tomcat启动一个线程,选择一个servlet并返回给客户端

那么由此,就会产生HttpRequest,HttpResponse

在设计时,我们先设计Request和Response

Request:是一个请求数据,其作用就是将客户端发送的http请求数据进行拆解后,将信息封装到其类本身

那么里面会持有一个InputStream,还会有参数类型,比如uri,method,以及还有参数信息,那么参数信息我们需要用一个集合来进行存放,就使用hashMap(key,value)key:参数名字, value:参数对应的值。

如何获取到数据呢?

1.使用转换流,用字符流读取第一行

2.用 “ ” 进行分割,将uri,method赋值给对应属性

2.1 uri 比较难获取,那么就用index获取?的的位置,如果-1则说明后面没有带参数,如果是数字,那么就截取到该字符串的位置,因是前包含,后不包含的获取方式

3.获取参数信息

(1)先截取到“?”号后面的字符串

(2)通过“&”号进行分割

(3)再用“=”号来进行分割,获取到数组,进行判断下看数组是不是两个长度,如果是就将其添加到HashMap中

4.getParameter()方法

(1)内部其本质是调用hashmap.get()方法,获取到key对应的value值

(2)担心返回null,所以用异常包裹,出现异常,则返回空字符串

/**
 * 解读:
 * 1. HspRequest 对象是用来封装客户端发送的http请求中的信息,以便于更好的读取
 *  /calServlet?num1=10&num2=20
 * 2. 将http请求中的 method(get或post)、uri(也就是请求的哪个servlet:/calServlet)、 参数信息
 * 3. HspRequest 对象 等价于 HttpServletRequest对象
 * 4. 这里考虑的是Get请求
 */
public class HspRequest {
    //思路:
    //1.目前只取出三个属性,所以设计三个属性变量
    //2.取出变量,并赋值给对应属性

    private String method;
    private String uri;
    //存放参数列表 参数名-参数值
    private HashMap<String, String> parametersMapping
            = new HashMap<>();

    InputStream inputStream = null;

    //构造器
    // inputStream 是和 对应的http请求socket关联
    public HspRequest(InputStream inputStream) {
        this.inputStream = inputStream;
        //完成对http请求数据的封装
        encapHttpRequest();
    }

    /**
     * 将http请求的相关信息,进行封装,然后提供相关的方法,进行获取
     */
    private void encapHttpRequest() {
        //2.取出变量,并赋值给对应属性
        //思路:
        // (1)强转成字符
        // (2)取出来的是 请求头 和 请求行连接在一起,所以只取出第一行

        /*
        http请求
        GET /calServlet?num1=10&num2=20 HTTP/1.1
        Host: localhost:8080
        User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0
         */

        System.out.println("======== HspRequest init() 被调用 ==========");

        try {
            BufferedReader bufferedReader =
                    new BufferedReader(new InputStreamReader(inputStream, "utf-8"));

            //取出第一行
            String requestLine = bufferedReader.readLine();

            String[] requestLineArr = requestLine.split(" ");

            method = requestLineArr[0];

            //取出 uri 赋值给 uri
            //(1) 这里是判断uri后面有没有跟参数信息,如果有参数信息,就一定会有?号
            //     如果没有参数信息,就没有?号,根据indexOf()方法判断其是否存在
            //    没有存在index就是-1
            int index = requestLineArr[1].indexOf("?");

            if (index == -1) {//说明字符串中没有找到“?”
                uri = requestLineArr[1];
            }else {
                //这里的截取是[0, index)
                uri = requestLineArr[1].substring(0, index);

                //获取参数信息 parameters
                //(1) 截取requestLineArr[1]中"?"以后的字符串
                String parameters = requestLineArr[1].substring(index + 1);

                //(2) 通过“&”截取成字符串数组{“num1=10”, "num2=30"}
                String[] parameterPair = parameters.split("&");

                //防止用户提交时为:servlet?后没有跟参数,所以需要进行判断
                if (null != parameterPair && ! "".equals(parameterPair)) {

                    for (String parameter : parameterPair) {
                        //(3) 通过"="号,再将其字符串数组中的每个元素截取成字符串数组
                        String[] parameterVal = parameter.split("=");
                        //(4) 判断其内每个长度是否等于2,然后将其添加到HshMap中
                        if (parameterVal.length == 2) {
                            parametersMapping.put(parameterVal[0], parameterVal[1]);
                        }
                    }
                }


                //关闭流
                //这里不能关闭流,因inputStream 和 socket 是相关联的,
                //关闭了inputStream 就相当于关闭了 socket
                //其本质是输入流关闭了,代表着socket也就关闭了
                //socket关闭代表无法获取到输出流OutputStream
                //inputStream.close();
            }


        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // request有一个特别重要的方法:通过name获取到其value值
    public String getParameter(String name) {
        if (parametersMapping.containsKey(name)) {
            return parametersMapping.get(name);
        } else {
            return "";
        }
    }


    public String getMethod() {
        return method;
    }

    public void setMethod(String method) {
        this.method = method;
    }

    public String getUri() {
        return uri;
    }

    public void setUri(String uri) {
        this.uri = uri;
    }


    @Override
    public String toString() {
        return "HspRequest{" +
                "method='" + method + '\'' +
                ", uri='" + uri + '\'' +
                ", parametersMapping=" + parametersMapping +
                '}';
    }
}

Response 设计

是将服务器信息返回给客户端

1.既然是输出,那么就是有OutputStream

2.返回给客户端,那么就需要是Http信息,所以需要一个http响应头

/**
 * 解读:
 * 1. HspResponse 对象可以封装OutputStream(是和socket绑定的)
 * 2. 即可以发送http响应信息给 客户端
 * 3. HspResponse 对象 等价于 HttpServletResponse
 */
public class HspResponse {
    //思路:
    //1.先写一个OutputStream 是和socket的关联的
    //2.获取OutputStream
    //3.其内部会有一个请求头

    OutputStream outputStream = null;

    //写一个http请求头
    public static final String respHeader = "HTTP/1.1 200 OK\r\n" +
            "Content-Type: text/html;charset=utf-8\r\n\r\n";

    public HspResponse(OutputStream outputStream) {
        this.outputStream = outputStream;
    }

    public OutputStream getOutputStream() {
        return outputStream;
    }
}

线程 设计

线程的作用是:处理请求:解析客户端发送的请求,然后再寻找对应的servlet,再调用对应的servlet方法

1.既然是处理请求,那么就必须有一个socket,通过socket才能拿到输入或输出流

2.拆解请求数据,因数据都封装在Request,只用通过获取Request类就可以得到数据

3.获取到数据后,得到其请求想要的uri后,在Tomcat维护的集合中查询uri

4.得到其servletName,再通过其他集合获取到servletName对应的Servlet实例

5.调用该实例方法

6.如未查询到该实例,就会返回404给客户端

public class HspRequestHandler implements Runnable {

    //定义一个socket 怎么确定是哪个socket? 用构造器来接收
    //获取socket的输入流的内容
    //获取输出流 发出响应,但要包装成http

    private Socket socket = null;

    public HspRequestHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {

        //InputStream inputStream = null;try {
            

            //使用HspRequest
            HspRequest hspRequest = new HspRequest(socket.getInputStream());
         

            //使用 HspResponse对象发送响应
            //1.创建 HspResponse
            HspResponse hspResponse =
                    new HspResponse(socket.getOutputStream());

            /**
             * 版本HspTomcatV3代码
             */

            //版本HspTomcatV3代码,使用反射来构建对象
            //1.通过uri来获取到客户端请求的是哪个servlet路径 url-pattern
            String uri = hspRequest.getUri();
            //=========新增业务===========
            //返回静态页面
            //(1) 判断uri是什么资源
            //(2) 如果是静态资源,就读取该资源,并返回给浏览器 设置content-type
            //(3) 因为目前没有启动tomcat,不是一个标准的web项目
            //(4) 把读取的静态资源放到 target/classes/cal.html
            //1.判断uri是否是一个html文件
            boolean html = WebUtils.isHtml(uri);
            //2.if语句判断执行里面
            if (html) {
                //3. 将html文件内容取出
                String calHtml = WebUtils.readHtml(uri.substring(1));
                //4. 将其发送
                //4.1 获取客户端请求输出流
                OutputStream outputStream =
                        hspResponse.getOutputStream();
                // 为什么发送出去的html代码,客户端却没有渲染?
                //原因是因为发出的不是http响应格式,所以客户端(浏览器)是不会进行渲染
                //解决方法:
                //4.2 发送内容
                //outputStream.write(calHtml.getBytes());
                outputStream.write((HspResponse.respHeader + calHtml).getBytes());
                //关闭流
                outputStream.flush();
                outputStream.close();
                socket.close();
                return;
            }
            


            //2.uri 其就是 HspTomcatV3 中 servletUrlMapping集合中的key值
            //  通过key值获取到servlet-name
            String servletName = HspTomcatV3.servletUrlMapping.get(uri);
            //2.1 假设输入一个错误的servlet,那么 HspTomcatV3.servletMapping.get(servletName) 就会抛出异常
            //2.2 抛出异常的原因是:因其所放置servlet的集合 ConcurrentHashMap是不能存在null的
            //2.3 但在 servletUrlMapping 中如果未查找到对应的uri,则会返回null
            //2.4 解决方法:(1) 将 ConcurrentHashMap 改成 HashMap (2) 如果是异常则将其改成""
            if (servletName == null) {
                servletName = " ";
            }
            //3.通过 servletName 获取 servletMapping 中对应的对象实例
            HspHttpServlet hspHttpServlet = HspTomcatV3.servletMapping.get(servletName);
            //4.获取到对象实例后,因其编译类型是 HspHttpServlet, 但运行类型是 HspCalServlet
            //4.1 调用其父类 HspHttpServlet中的service()方法,会选择doGet()或doPost()
            //4.2 这里又会因动态绑定机制,动态绑定机制会首先调用其运行类型中的doGet()或doPost()方法
            if (hspHttpServlet != null) {
                //这里为什么会最终是HspCalServlet会调用方法呢?
                //因再servletMapping.get()得到的是实例其实就是HspCalServlet实例,只不过编译类型是其父类
                //根据动态绑定机制,所以最终会调用到其子类的方法
                hspHttpServlet.service(hspRequest, hspResponse);
            } else {
                //如果没有找到该servlet实例,则返回404
                String resp = HspResponse.respHeader + "<h1>404 Not Found</h1>";
                OutputStream outputStream = hspResponse.getOutputStream();
                outputStream.write(resp.getBytes());
                outputStream.flush();
                outputStream.close();
            }

            //输入流不能提前关闭,提前关闭代表着socket 也就关闭了
            //其本质是输入流关闭了,代表着socket也就关闭了
            //socket关闭代表无法获取到输出流OutputStream
            //inputStream.close();
            socket.close();


        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Servlet 设计

Servlet其就是客户端想要请求的信息,而这个才是最终的业务层

设计主要分三个层次:

1. Servlet 接口 -- 里面会有一些抽象方法如(init(),service(request,response)等)

public interface HspServlet {

    void init() throws ServletException;


    void service(HspRequest request, HspResponse response) throws  IOException;



    void destroy();
}

2. 实现了Servlet接口的抽象类HttpServlet --- 实现了service()方法,里面会有分发机制,比如根据是get或post请求来调用doget或dopost

public abstract class HspHttpServlet implements HspServlet {


    @Override
    public void service(HspRequest request, HspResponse response) throws IOException {

        //equalsIgnoreCase(): 是比较字符串内容相同,不区分大小写
        if ("GET".equalsIgnoreCase(request.getMethod())) {
            this.doGet(request, response);
        } else if ("POST".equalsIgnoreCase(request.getMethod())) {
            this.doPost(request, response);
        }
    }

    //这里使用了模板设计模式
    //让 HspHttpServlet 子类 HspCalServlet 来实现
    public abstract void doGet(HspRequest request, HspResponse response);
    public abstract void doPost(HspRequest request, HspResponse response);

}

3. 自己写的业务层,继承了HttpServlet的HshpCalServelt,这里最终是在这里面写如果是get()或post()会怎么走的业务

public class HspCalServlet extends HspHttpServlet {

    @Override
    public void doGet(HspRequest request, HspResponse response) {
        //思路:
        //1.先获取到num1和num2,相加
        //2.将其结果返回个客户端
        int num1 = WebUtils.parseInt(request.getParameter("num1"), 0);
        int num2 = WebUtils.parseInt(request.getParameter("num2"), 0);

        int sum = num1 + num2;

        //将其结果返回给客户端
        OutputStream outputStream = response.getOutputStream();
        //将响应头 和 响应体拼接,并发出
        try {
            outputStream.write( (response.respHeader +
                    "<h1> " + num1 + "+" + num2 + "=" + sum + "HspTomcatV3 反射+xml</h1>").getBytes());
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    @Override
    public void doPost(HspRequest request, HspResponse response) {
        doGet(request, response);
    }

    @Override
    public void init() throws ServletException {

    }

    @Override
    public void destroy() {

    }
}

Tomcat的设计

tomcat的主要任务就是通过读取web.xml,将业务servlet封装到自己维护的HashMap集合中,这样就可以让线程拆解请求数据,查找对应的servlet实例,并调用方法了

1.Tomcat维护了两个集合,一个是servletMapping主要存放(类名字,类的反射实例)一个是servletUrlMapping主要存放是(uri(客户端请求servle的路径), 类名字)

2.怎么将servlet实例加入到集合当中呢?

(1)读取当前Tomcat的运行路径,获取路径后加上web.xml文件,就得到其web.xml文件路径

(2)通过dom4J 来读取web.xml的dmo节点,读取到Root节点

(3)通过root节点,读取到web文件中所有servlet节点

(4)遍历servlet节点,读取每个servlet节点中servlet-name和servlet-class标签

(5)通过反射机制生成servlet的class实例,将其servle-name 和 servlet-class实例添加到集合大当中

3.在主方法中,创建servletSocket,放入端口号,并一直等待监听,调用其servlet实例加入的集合的方法,再调用线程

public class HspTomcatV3 {

    //设计两个放置web.xml中元素的容器
    public static final ConcurrentHashMap<String, HspHttpServlet>
            servletMapping = new ConcurrentHashMap<>();

    public static final ConcurrentHashMap<String, String>
            servletUrlMapping = new ConcurrentHashMap<>();


    public static void main(String[] args) {
        HspTomcatV3 hspTomcatV3 = new HspTomcatV3();
        hspTomcatV3.init();

        //启动tomcat
        hspTomcatV3.run();
    }

    //启动HspTomcat
    public void run() {
        try {
            ServerSocket serverSocket = new ServerSocket(8080);
            System.out.println("============HspTomcatV3================");

            while (!serverSocket.isClosed()) {
                Socket socket = serverSocket.accept();
                HspRequestHandler hspRequestHandler = new HspRequestHandler(socket);
                //启动线程
                new Thread(hspRequestHandler).start();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //直接对两个容器进行初始化
    public void init() {
        //  因其是自己手写的Servlet,没有继承java真正的Servlet类,所以web.xml是无法复制到
        //  工作路径(也就是运行路径)中,所以需要手动复制,而1就是获取运行路径,将其手动复制到目录下

        //1.先获取到 HspTomcatV3 运行的工作路径
        String path = HspTomcatV3.class.getResource("/").getPath();
        System.out.println(path);

        //2.读取web.xml文件内容,通过dom4j来读取web.xml中的dom节
        //2.1通过dom4j来读取web.xml中的dom节
        SAXReader saxReader = new SAXReader();
        try {
            Document document = saxReader.read(new File(path + "web.xml"));
            System.out.println(document);//验证有没有读取到web.xml
            //3.得到web.xml中的根元素
            Element rootElement = document.getRootElement();
            //4.获取根元素中的所有element元素节点
            List<Element> elements = rootElement.elements();
            //5.遍历得到servlet和servlet-mapping element元素节点
            //这里因是 Object 类型,所以无法使用Element类型中自己的方法,所以需要在List<Element>接种添加泛型
            //这样再遍历时,就是Element类型,可以使用element类型中的自己的方法了
            //for (Object element : elements) {}
            for (Element element : elements) {
                //6.判断是不是servlet 和 servlet-mapping
                if ("servlet".equalsIgnoreCase(element.getName())) {
                    //7.获取servlet元素中的servlet-name 和 servlet-class
                    Element servletName = element.element("servlet-name");
                    Element elementClass = element.element("servlet-class");
                    //8.添加到servletMapping容器中
                    servletMapping.put(servletName.getText(),
                            // (1).这里实例化对象是Object对象,但因是在集合中泛型指定的是HspHttpServlet类型,所以需要强转
                            //   Object o = Class.forName(elementClass.getText()).newInstance();
                            // (2).trim():是取消web.xml 中 <servlet-class>  </servlet-class>中间填写配置两边的空格,
                            //   这样出现空格会无法识别,无法通过路径来进行发射实例化
                            // <servlet-class>  com.hspedu.tomcat.servlet.HspCalServlet  </servlet-class>两边的空格
                            (HspHttpServlet) Class.forName(elementClass.getText().trim()).newInstance());
                } else if ("servlet-mapping".equalsIgnoreCase(element.getName())) {
                    //9.获取servlet-mapping标签中的 servlet-name 和 url-pattern
                    Element elementName = element.element("servlet-name");
                    Element urlPattern = element.element("url-pattern");
                    //10.添加到servletUrlMapping容器中
                    servletUrlMapping.put(urlPattern.getText(), elementName.getText());
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        //查看容器中的元素
        System.out.println("servletMapping= " + servletMapping);
        System.out.println("servletUrlMapping= " + servletUrlMapping);
    }
}

工具类WebUtils

public class WebUtils {

    public static int parseInt(String str, int defaultVal){

        try {
            return Integer.parseInt(str);
        } catch (NumberFormatException e) {
            System.out.println("您所转换的字符串格式不对");
        }
        return defaultVal;
    }

    //判断其输入的uri是否为html
    public static boolean isHtml(String uri) {
        //判断字符串末尾是不是“.html”
        return uri.endsWith(".html");
    }

    //根据文件名来读取该文件
    public static String readHtml(String filename) {
        //1.获取该类的工作路径
        String path = com.hspedu.utils.WebUtils.class.getResource("/").getPath();
        //2.读取cal.html

        StringBuilder stringBuilder = new StringBuilder();
        try {
            //2.1 读取cal.html文件路径
            BufferedReader bufferedReader =
                    new BufferedReader(new FileReader(path + filename));
            String buf = "";
            //2.2 按行来读取内容,并将内容赋值给StringBuffered
            while ((buf = bufferedReader.readLine()) != null) {
                stringBuilder.append(buf);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        //3.返回
        return stringBuilder.toString();
    }
}

  • 19
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值