手写tomcat(二):能够访问一个Servlet

手写Tomcat(一)中,我实现了通过服务器能够以二进制流的形式访问WEB-ROOT目录下的所有文件(除图片).在这篇文章中,我要实现通过服务器能够访问到Servlet.

gitHub地址(持续更新中):https://github.com/shenshaoming/tomcat

本项目为纯Java语言,克隆项目之后只需要用src中的文件就可以了.

先截个图解释一下我的项目结构:

其中AbstractServlet实现了GenericServlet接口,并且重写了service方法,在service中通过判断方法类型(GET,POST)选择对应的方法.Main是启动服务器的程序.

接下来是代码,我把注释都写在代码中了,文章里只做一些简单介绍:

首先是Servlet相关的.java文件:

GenericServlet.java:规定了Servlet中的方法.

package com.tomcat.baseservlet;

import com.tomcat.core.Request;
import com.tomcat.core.Response;

/**
 * Servlet约束,子类必须拥有这些方法
 * @Author: 申劭明
 * @Date: 2019/9/18 9:43
 */
public interface GenericServlet {
    /**
     * @Description : Servlet初始化时执行的方法
     *
     * @author : 申劭明
     * @date : 2019/9/18 10:10
    */
    void init() throws Exception;

    /**
     * @Description : Servlet销毁时执行的方法
     *
     * @return : void
     * @author : 申劭明
     * @date : 2019/9/18 10:10
    */
    void destroy();

    /**
     * @Description : Servlet分发请求时用到的方法
     *
     * @param request 请求
     * @param response 响应
     * @author : 申劭明
     * @date : 2019/9/18 10:11
    */
    void service(Request request, Response response) throws Exception;

}

AbstractServlet.java:实现接口,并通过判断http请求的类型决定具体访问的方法

package com.tomcat.baseservlet;

import com.tomcat.core.Request;
import com.tomcat.core.Response;

/**
 * GenericServlet的抽象实现
 * @Author: 申劭明
 * @Date: 2019/9/18 10:03
 */
public abstract class AbstractServlet implements GenericServlet{

    private static final String GET_METHOD = "GET";
    
    private static final String POST_METHOD = "POST";

    /**
     * @Description : 在抽象类中实现请求的分发
     *
     * @param request 请求
     * @param response 响应
     * @author : 申劭明
     * @date : 2019/9/18 10:13
    */
    @Override
    public void service(Request request, Response response) throws Exception {
        if (GET_METHOD.equalsIgnoreCase(request.getMethod())){
            this.doGet(request,response);
        }else if (POST_METHOD.equalsIgnoreCase(request.getMethod())){
            this.doPost(request,response);
        }
    }

    /**
     * @Description : 请求类型为GET时执行的方法
     *
     * @param request 请求
     * @param response 响应
     * @author : 申劭明
     * @date : 2019/9/18 10:12
    */
    protected abstract void doGet(Request request, Response response);

    /**
     * @Description : 请求类型为POST时执行的方法
     *
     * @param request 请求
     * @param response 响应
     * @author : 申劭明
     * @date : 2019/9/18 10:12
    */
    protected abstract void doPost(Request request, Response response);


}

UserServlet.java:Servlet测试

package com.tomcat.servlet;

import com.tomcat.annotations.Servlet;
import com.tomcat.baseservlet.AbstractServlet;
import com.tomcat.core.Request;
import com.tomcat.core.Response;

/**
 * @Author: 申劭明
 * @Date: 2019/9/18 10:09
 */
@Servlet("/user")
public class UserServlet extends AbstractServlet {

    @Override
    protected void doGet(Request request, Response response) {
        this.doPost(request,response);
    }

    @Override
    protected void doPost(Request request, Response response) {
        //处理
        //响应
        response.setResponseContent(new StringBuilder("<h1>Your are requesting the UserServlet</h1>"));
    }

    @Override
    public void init() throws Exception {

    }

    @Override
    public void destroy() {

    }
}

其次是core包中的.java文件:

HttpServer.java:负责监听端口与启动线程处理收到的请求.

package com.tomcat.core;

import com.tomcat.annotations.Servlet;
import com.tomcat.baseservlet.AbstractServlet;

import java.io.*;
import java.net.*;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Set;

/**
 * 监听请求,调用request和response对请求作出反应
 * @Author: 申劭明
 * @Date: 2019/9/16 17:21
 * @version: 4.1
 */
public class HttpServer {

    /**
     * 监听端口
     */
    public static int port = 8080;

    /**
     * 关闭服务器的请求URI
     */
    static final String CLOSE_URI = "/shutdown";

    /**
     * Key值为Servlet的别名(uri),value为该Servlet对象
     * default权限
     */
    static HashMap<String, AbstractServlet> map;

    static {
        //包名,可以通过application.properties设置
        getServlets("com.tomcat.servlet");
    }

    /**
     * 单例,因为是通过主函数启动,不涉及多进程启动的问题,所以不需要做多线程方面的考虑
     */
    static ServerSocket serverSocket = null;

    /**
     * @Description : 多线程bio监听数据请求
     * @author : 申劭明
     * @date : 2019/9/17 10:29
     */
    public void acceptWait() {
        try {
            serverSocket = new ServerSocket(port);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //扫描包,读取包下的所有Servlet

        while (!serverSocket.isClosed()) {
            try {
                //单线程,阻塞式监听(bio)
                Socket socket = serverSocket.accept();
                RequestHandler handler = new RequestHandler(socket);
                handler.start();
            } catch (IOException e) {
                e.printStackTrace();
                continue;
            }
        }
    }

    /**
     * @param packageName 包名,如com.tomcat.servlet
     * @return : void
     * @Description : 扫描packageName包下的所有带有@Servlet注解的类文件
     * @author : 申劭明
     * @date : 2019/9/18 10:36
     */
    private static void getServlets(String packageName) {

        //class类的集合
        Set<Class<?>> classes = new LinkedHashSet<>();

        try {
            String packageDirName = packageName.replace(".", "/");
            Enumeration<URL> resources = Thread.currentThread().getContextClassLoader().getResources(packageDirName);

            while (resources.hasMoreElements()) {
                URL url = resources.nextElement();
                String protocol = url.getProtocol();
                if ("file".equals(protocol)) {
                    String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
                    findAndAddClassesInPackageByFile(packageName, filePath,true,classes);
                }else if("jar".equals(protocol)){
                    //扫描JAR包
                }
            }
            //遍历class集合
            if (map == null){
                map = new HashMap<>(classes.size());
            }
            for (Class<?> aClass : classes) {
                //如果该class有Servlet注解
                if (aClass.isAnnotationPresent(Servlet.class)){
                    try {
                        //添加至map集合中
                        map.put(aClass.getAnnotation(Servlet.class).value(), (AbstractServlet) aClass.newInstance());
                    } catch (InstantiationException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * @Description : 对于file类型获取该类型的所有class
     *
     * @param packageName 包名,com.tomcat.servlet
     * @param packagePath 包路径,com/tomcat/servlet
     * @param recursive 是否循环遍历子包内的文件
     * @param classes class集合
     * @return : void
     * @author : 申劭明
     * @date : 2019/9/18 16:55
    */
    public static void findAndAddClassesInPackageByFile(String packageName,
                                                        String packagePath, final boolean recursive, Set<Class<?>> classes) {
        // 获取此包的目录 建立一个File
        File dir = new File(packagePath);
        // 如果不存在或者 也不是目录就直接返回
        if (!dir.exists() || !dir.isDirectory()) {
            return;
        }
        // 如果存在 就获取包下的所有文件 包括目录
        File[] dirfiles = dir.listFiles(new FileFilter() {
            // 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件)
            @Override
            public boolean accept(File file) {
                return (recursive && file.isDirectory())
                        || (file.getName().endsWith(".class"));
            }
        });
        // 循环所有文件
        for (File file : dirfiles) {
            // 如果是目录 则继续扫描
            if (file.isDirectory()) {
                findAndAddClassesInPackageByFile(packageName + "."
                                + file.getName(), file.getAbsolutePath(), recursive,
                        classes);
            } else {
                // 如果是java类文件 去掉后面的.class 只留下类名
                String className = file.getName().substring(0,
                        file.getName().length() - 6);
                try {
                    // 添加到集合中去
                    // classes.add(Class.forName(packageName + '.' +
                    // className));
                    // 经过回复同学的提醒,这里用forName有一些不好,会触发static方法,没有使用classLoader的load干净
                    classes.add(Thread.currentThread().getContextClassLoader()
                            .loadClass(packageName + '.' + className));
                } catch (ClassNotFoundException e) {
                    // log.error("添加用户自定义视图类错误 找不到此类的.class文件");
                    e.printStackTrace();
                }
            }
        }
    }
}

Request.java:负责从HTTP请求报文中提取urI(和URL的概念不同)和请求的类型(GET,POST):

package com.tomcat.core;

import java.io.IOException;
import java.io.InputStream;

/**
 * @Author: 申劭明
 * @Date: 2019/9/16 17:24
 */
public class Request {

    private InputStream is;
    /**
     * 请求路径,如:/test.txt
     */
    private String uri;

    /**
     * 请求类型,GET或POST等
     */
    private String method;

    public String getMethod() {
        return method;
    }
    public Request(){

    }

    public Request(InputStream inputStream){
        this.is = inputStream;
        //读取报文
        parse();
    }

    /**
     * @Description : 获取http请求中的相关参数
     *
     * @author : 申劭明
     * @date : 2019/9/17 10:26
    */
    public void parse() {
        /**
         * 一个包没有固定长度,以太网限制在46-1500字节,
         * 1500就是以太网的MTU,超过这个量,TCP会为IP数据报设置偏移量进行分片传输,
         * 现在一般可允许应用层设置8k(NTFS系统)的缓冲区,8k的数据由底层分片,
         * 而应用层看来只是一次发送。
         */
        //创建一个容量为2048的StringBuffer对象
        StringBuffer request = new StringBuffer(Response.BUFFER_SIZE);
        //记录字节数量
        int i ;
        byte[] buffer = new byte[Response.BUFFER_SIZE];
        try {
            //从输入流中读取数据到buffer中,i表示读到了多少字节(多少个byte)
            i = is.read(buffer);
        } catch (IOException e) {
            e.printStackTrace();
            i = -1;
        }
        System.out.println(request);
        //i表示有读到了多少字节,所以此处要用<而不是<=
        for (int j = 0; j < i; j++) {
            request.append((char)buffer[j]);
        }
        System.err.println(request.toString());
        uri = parseUri(request.toString());
        method = parseMethod(request.toString());
    }

    /**
     * @Description : 获取请求路径,如http://localhost:8080/test.txt,截取/test.txt(URI)
     *
     * @param request 请求头
     * @return : 请求路径
     * @author : 申劭明
     * @date : 2019/9/17 9:33
    */
    private String parseUri(String request) {
        int index1,index2;
        //查看socket获取的请求头是否有值
        index1 = request.indexOf(' ');
        if (index1 != -1){
            index2 = request.indexOf(' ', index1 + 1);
            if (index2 > index1){
                return request.substring(index1 + 1,index2);
            }
        }
        return null;
    }

    /**
     * @Description : 获取请求类型
     *
     * @param request 请求报文
     * @return : GET,POST...
     * @author : 申劭明
     * @date : 2019/9/18 9:51
    */
    private String parseMethod(String request){
        int index = request.indexOf(' ');
        if (index != -1){
            return request.substring(0,index);
        }
        return null;
    }

    public String getUri() {
        return uri;
    }
}

Response.java:负责响应界面的类,当用户的请求不在已经注册过的Servlet中时,就会通过response中的sendStaticResource()方法获取WEB-ROOT(当前设置的是读取D盘中的资源)下的资源.

package com.tomcat.core;

import java.io.*;

/**
 * @Author: 申劭明
 * @Date: 2019/9/16 17:28
 */
public class Response {

    /**
     * 传输数组的最大字节数
     */
    public static final int BUFFER_SIZE = 2048;

    /**
     * 访问的文件的路径,即tomcat中部署项目的目录
     */
    private static final String WEB_ROOT = "D:";

    /**
     * 响应头信息
     */
    private static final String RESPONSE_HEADER = "HTTP/1.1 200 Read File Success\r\n" +
            "Content-Type: text/html;charset=UTF-9\r\n" + "\r\n";

    /**
     * 请求
     */
    private Request request;

    /**
     * 返回页面的数据
     */
    private OutputStream output;

    public Response(OutputStream outputStream) {
        this.output = outputStream;
    }

    public void setRequest(Request request) {
        this.request = request;
    }

    /**
     * @Description : 向页面返回数据
     *
     * @author : 申劭明
     * @date : 2019/9/17 10:27
    */
    public void sendStaticResource() throws IOException {
        //返回数据时所用的字节流
        byte[] bytes = new byte[BUFFER_SIZE];
        FileInputStream fis = null;
        File file = new File(WEB_ROOT, request.getUri());
        String returnMessage = null;
        try {
            //如果文件存在,且不是个目录
            if (file.exists() && !file.isDirectory()) {
                fis = new FileInputStream(file);
                //读文件
                int ch ;

                StringBuilder sb = new StringBuilder(BUFFER_SIZE);
                //写文件
                while ((ch = fis.read(bytes,0,bytes.length)) != -1) {
                    sb.append(new String(bytes,0,ch,"UTF-8"));
                }
                returnMessage =  RESPONSE_HEADER + sb;
            }else {
                //文件不存在,返回给浏览器响应提示,这里可以拼接HTML任何元素
                String retMessage = "<h1>" + file.getName() + " file or directory not exists</h1>";
                returnMessage = "HTTP/1.1 404 File Not Fount\r\n" +
                        "Content-Type: text/html\r\n" +
                        "Content-Length: " + retMessage.length() + "\r\n" +
                        "\r\n" +
                        retMessage;
            }
            //用输出流返回数据给页面
            output.write(returnMessage.getBytes());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (fis != null){
                fis.close();
            }
            if (output != null){
                //清空缓存区,调用close方法时会有flush操作
//                output.flush();
                output.close();
            }
        }
    }

    /**
     * @Description : 设置返回数据
     *
     * @param message 返回给页面的数据
     * @author : 申劭明
     * @date : 2019/9/18 10:19
    */
    public void setResponseContent(StringBuilder message){
        try {
            output.write(new StringBuilder(RESPONSE_HEADER).append(message).toString().getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void setResponseHeader(String message){
        setResponseContent(new StringBuilder(message));
    }
}

RequestHandler.java:继承自Thread类,每当HttpServer接收到一个请求,就会开一个线程(RequestHandler)去处理这个请求.

package com.tomcat.core;

import com.tomcat.baseservlet.AbstractServlet;

import java.net.Socket;

/**
 * @Author: 申劭明
 * @Date: 2019/9/17 17:45
 */
public class RequestHandler extends Thread {

    private Socket socket;

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

    @Override
    public void run() {

        try {
            //接收请求参数
            Request request = new Request(socket.getInputStream());
            AbstractServlet abstractServlet = HttpServer.map.get(request.getUri());

            //如果请求的是/shutdown 则关闭服务器
            if (HttpServer.CLOSE_URI.equals(request.getUri())){
                HttpServer.serverSocket.close();
                return;
            }
            //创建用于返回浏览器的对象
            Response response = new Response(socket.getOutputStream());
            response.setRequest(request);

            if (abstractServlet != null){
                abstractServlet.service(request,response);
            }else{
                //找不到对应的Servlet则直接访问文件
                response.sendStaticResource();
            }
            //如果http短连接则关闭socket
            //socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

启动类Main.java:

import com.tomcat.core.HttpServer;

/**
 * @author: 申劭明
 * @date: 2019-09-16
 */
public class Main {

    public static void main(String[] args) {
        HttpServer server = new HttpServer();
        server.acceptWait();
    }
}

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值