冲冲冲

本博客详细的代码已上传至github,所以本篇博客就不将代码写太详细了

我们知道,Servlet不是代码,也不是框架,而是一组规范,是一组所谓的Tomcat服务器实现的规范/接口。如果想要写出一个Tomcat,则必须要对Servlet这组规范有非常清晰的了解,我们才能实现它

  • 如果只是写一个简单的Web服务器的话,我们很清楚,只需要创建三个类,一个是Response, 另一个是Request,分别负责响应和请求;最后一个类则是HttpServer负责服务器的运行,接收和解析Request,发送Response

  • 因为是servlet服务器,所以ResponseRequest一定要实现ServletResponseServletRequest,以及自定义的Servlet类要实现Servlet接口

只能返回静态文件的Http服务器

要点

只能返回静态文件说明了两点:

  1. 我们需要找到解析出请求的静态文件的路径
  2. 将请求的静态文件放到输出流中,作为response的一部分输出出去

对于服务器来说,整个流程是这样的:

  1. HttpServer等待客户端的Request
  2. HttpServer拿到请求之后解析该Http请求
  3. 解析之后拿到请求路径
  4. 通过请求路径匹配自己的资源路径,找到静态文件
  5. 将静态文件作为response的一部分输出出去

我们需要注意的是,因为Http请求,所以在输出的时候一定要满足Http协议的格式

HttpServer实现

该类无疑有一个main方法,是一个启动类,它所要完成的任务就是等待request,然后解析,处理,再将response返回。下面是等待的过程:

private void await(){

        // 获得服务端的socket
        ServerSocket serverSocket = null;
        String host = "127.0.0.1";
        int port = 8080;

        try {
            // 对该socket进行初始化
            serverSocket = new ServerSocket(port, 1, InetAddress.getByName(host));
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }

        // 等待响应
        while (!shutdown){
            // 拿到服务端接收的socket
            try(Socket socket = serverSocket.accept()) {

                // 拿到客户端传给服务端的输入流,并构建输出流
                InputStream inputStream = socket.getInputStream();
                OutputStream outputStream = socket.getOutputStream();

                // 把客户端的输入流解析为request
                Request request = new Request(inputStream);
                request.parse();

                // 服务端作出响应并把输出流response到客户端
                Response response = new Response(outputStream);
                response.setRequest(request);
                response.sendStaticResource();

                String uri = request.getUri();
                if (uri != null){
                    shutdown = uri.equals(SHUTDOWN_COMMAND);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
Request类实现

因为是Http请求,Request类最重要的职责就是解析Http请求的URI(PS:不了解HTTP协议请自行百度)。因为我们这只是一个简单的服务器,请求的URI即是我们自己的资源路径

// 将request的字节流变为String    
void parse() {
        // 读出socket中的字符
        StringBuilder request = new StringBuilder(2048);
        // read的次数
        int i;
        // 每次read的缓冲区大小
        byte[] buffer = new byte[2048];

        try {
            i = inputStream.read(buffer);
        } catch (IOException e) {
            e.printStackTrace();
            // i = -1 说明没有读入
            i = -1;
        }

        // 把缓冲buffer中的输入流读入request
        for (int j = 0; j < i; j++) {
            // 此处必须要转换为char,不然是一堆输入流
            request.append((char)buffer[j]);
        }
        System.out.println(request.toString());
        uri = parseUri(request.toString());
    }

    /**
     * 解析HTTP请求中的URI
     * @param requestString 请求
     * @return URI
     */
    private String parseUri(String requestString) {
        // POST /uri HTTP/1.1
        int index1, index2;
        index1 = requestString.indexOf(' ');
        // 如果出现该字符串
        if (index1 != -1){
            index2 = requestString.indexOf(' ', index1 + 1);
            if (index1 < index2){
                return requestString.substring(index1 + 1, index2);
            }
        }
        return null;
    }
Response类的实现

response主要的功能就是拿到request解析好的路径,找到静态资源,并将资源转为二进制到输出流返回给客户端

 void sendStaticResource() throws IOException {
        byte[] bytes = new byte[2048];
        FileInputStream fileInputStream = null;
        try {
            // 通过父路径(自己设置的) + 子路径(URI)拿到文件
            File file = new File(HttpServer.WEB_ROOT, request.getUri());
            if (file.exists()) {
                // 把文件作为输入流
                fileInputStream = new FileInputStream(file);
                // 把从文件中读入的字节放到bytes中,缓冲区为BUFFER_SIZE,字节个数为ch
                int ch = fileInputStream.read(bytes, 0, BUFFER_SIZE);
                while (ch != -1) {
                    // 把bytes中读到的字节写入输出流中
                    outputStream.write(bytes, 0, ch);
                    ch = fileInputStream.read(bytes, 0, BUFFER_SIZE);
                }
            } else {
                String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +
                        "Content-Type: text/html\r\n" +
                        "\r\n" +
                        "<h1>File Not Found</h1>";
                outputStream.write(errorMessage.getBytes());
            }
        } catch (Exception e){
            System.out.println(e.toString());
        } finally {
            if (fileInputStream != null) {
                fileInputStream.close();
            }
        }
    }

但是上面的代码其实是有错的,如果服务器找到静态资源后,把静态资源的二进制文件返回给客户端,客户端是无法接收到的,并且会爆出405,原因就是我们返回的文件没有遵循Http协议,具体如何解决,请自行思考,然后参考我在上文中提到的github的代码

可以解析Servlet类的Servlet服务器

要点

可以解析Servlet类和上一个返回静态资源其实没有什么大的不同,主要有三点:

  1. 因为是servlet服务器,所以ResponseRequest一定要实现ServletResponseServletRequest,以及自定义的Servlet类要实现Servlet接口
  2. 我们通过Http请求头拿到Servlet类的路径和类名,进而通过反射去拿到Servlet类的实例,之后调用其service方法,完成解析
  3. 因为资源可分为servlet和静态资源,所以我们分两个类来解析StaticResourceProcessorServletProcessor
ServletProcessor类的实现

此时的重点就落在了如何拿到Servlet类的实例,

class ServletProcessor {

    void process(Request request, Response response) {
        String uri = request.getUri();
        // 截取URI的最后一个路径作为servletName
        String servletName = uri.substring(uri.lastIndexOf('/') + 1);
        System.out.println(servletName);
        URLClassLoader urlClassLoader = null;

        try {
            URL[] urls = new URL[1];
            URLStreamHandler streamHandler = null;
            File classPath = new File(HttpServer.WEB_CONTROLLER);

            // 拿到servlet的路径
            String repository = String.valueOf(new URL("file", null, classPath.getCanonicalPath() + File.separator));
            System.out.println(repository);
            urls[0] = new URL(null, repository, streamHandler);
            // 通过文件路径去加载urls,进而加载servlet类
            urlClassLoader = new URLClassLoader(urls);
        } catch (IOException e){
            e.printStackTrace();
        }

        Class clazz = null;
        try {
            // 获取包名
            String packName = PrimitiveServlet.class.getPackage().toString();
            System.out.println(packName);
            // 此处加载的是.class字节码文件,不是Java文件,同时,不仅仅是文件名,还有 它的包名
            clazz = urlClassLoader != null ? urlClassLoader.loadClass(packName.substring(packName.indexOf(" ") + 1) + "." + servletName) : null;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        Servlet servlet;

        try {
            if (clazz != null) {
                servlet = (Servlet) clazz.newInstance();
                // 防止servlet调用request和response的其他方法,故产生了外观类
                ResponseFacade responseFacade = new ResponseFacade(response);
                RequestFacade requestFacade = new RequestFacade(request);
                servlet.service(requestFacade, responseFacade);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } catch (Throwable e) {
            System.out.println(e.toString());
        }
    }
}
其他类

我们一定要注意,Servlet类,request和response一定要实现servlet接口,同时,因为servlet终归是向输出流中写东西的,所以response需要重写ServletResponse的getWriter方法

    @Override
    public PrintWriter getWriter() throws IOException {
        // 把自动刷新设置为true,对println自动刷新到输出流中
        return new PrintWriter(outputStream, true);
    }

至于我们自定义的Servlet类,它会先拿到response的输出流,然后向输出流写入数据,再返回给response。则可以这样写:

public class PrimitiveServlet implements Servlet {

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("from service");
        PrintWriter out = servletResponse.getWriter();
        out.println("Hello, Servlet");
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {

    }
}

我们可以看到,这个servlet类返回的输出流并没有遵循Http协议,即没有返回响应头,那么会什么接收它的客户端还可以正常解析呢?

因为Servlet规范已经定义好了呀,当我们实现Servlet的时候,就可以不用自己去实现 HTTP 协议报文了

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐