手写SpringBoot

发布于:2023-01-01 ⋅ 阅读:(187) ⋅ 点赞:(0)

目录

一、简介

二、创建Maven项目

二、操作步骤

1.创建一个WebServer web容器 用于实现Tomcat基本功能。 

2.建立TCP连接

3.解析请求

 4.继续解析请求

 5.重构HttpServletRequest

6.响应客户端

7.响应404

8.重构HttpServletResponse

        9.重构ClientHandler

        10.设置响应头属性

        11.响应正确的MIME类型​​​​​​​

        12.解决空请求问题

总结



一、简介

当首次使用SpringBoot 只需要√上一个web 就内置了tomcat, 也会自动生成我们所需要的依赖,创建好项目后,在java包下的Application同包目录下就可以创建我们的处理业务的逻辑代码此时我们只需要在类中添加一个@Controller注解,就说明这是我们处理业务的类,当浏览器发送请求过来时,服务器会优先加载被@Controller注解的类来到内存中。并通过反射机制,映射到被我们@RequestMapping()注解的方法上,就可以给浏览器回页面了。
我们也可以一起来实现一下SpringBoot中Tomcat容器以及SpringMVC

 按照HTTP协议要求,与客户端完成一次交互流程为一问一答
 因此,这里分为三步完成该工作:
 1:解析请求  目的:将浏览器发送的请求内容读取并整理
 2:处理请求  目的:根据浏览器的请求进行对应的处理工作
 3:发送响应  目的:将服务端的处理结果回馈给浏览器


二、创建Maven项目

创建Maven 我们取名叫WebServer

 然后下一步

 找到根目录下的pom配置文件

 我们设置一下jdk版本,以及编码格式

二、操作步骤

1.创建一个WebServer web容器 用于实现Tomcat基本功能。 

package com.webserver.core;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;


public class WebServerApplication {
    /**
     * 运行在服务端的ServerSocket主要完成两个工作
     * 1;向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立连接
     * 2;监听端口,一旦一个客户端建立连接,会立即返回一个Socket。通过这个Socket
     * 就可以和该客户端交互了
     */
    private ServerSocket serverSocket;


     /**
     * 服务端构造方法,用来初始化
     */
    public WebServerApplication(){
        try {
            System.out.println("正在启动服务端...");
            /*
                实例化serverSocket时要指定服务端口,该端口不能与操作系统其他
                应用程序占用的端口相同,否则会抛出异常;

                端口是一个数字,取值范围;0-65535之间
                6000之前的端口不要使用,密集绑定系统应用程序和流行应用程序。
             */
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public void start(){
        try {
            System.out.println("等待客户端连接...");
                /*
                    ServerSocket提供了接受客户端链接的方法;
                    Socket.accept()
                    这个方法是一个阻塞方法,调用方法后"卡住”,此时开始等待客户端的连接
                    ,直到一个客户端连接,此时该方法会立即返回一个socket实例
                    通过这个可Socket就可以与客户端进行交互了

                    可以理解为此操作是接电话,电话没响就一直等。
                 */
            Socket socket = serverSocket.accept();//阻塞方法
            System.out.println("一个客户端连接了!");
        } catch (IOException e) {
            e.printStackTrace();
        }

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

这时候可以把服务端跑起来在网页输入http://localhost:8088,返回控制台看看是否能连接上


2.建立TCP连接

 实现: 由于服务端可以同时接收多客户端的连接,主线程仅负责接受客户端的连接,一旦一个 客户端连接后则启动一个线程来处理。 1:在com.webserver.core下新建类:ClientHandler(实现Runnable接口),作为线程任务。 工作是负责与连接的客户端进行HTTP交互 2:WebServerApplication主线程接收连接后启动线程执行ClientHandler这个任务处理客户端交互

package com.webserver.core;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

/**
 * 与客户端完成一次HTTP的交互
 */
public class ClientHandler implements Runnable{
    private Socket socket;
    public ClientHandler(Socket socket){
        this.socket = socket;
    }
    public void run(){
        try {
            InputStream in = socket.getInputStream();
//            http://localhost:8088/index.html
            int d;
            while((d = in.read())!=-1){
                System.out.print((char)d);
            }

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

 ClientHandler 实现runnable接口作为线程任务  返回WebServerApplication在start方法中创建线程


3.解析请求

HTTP协议要求浏览器连接服务端后应当发送一个请求,因此实现读取请求并输出到控制台来了解请求
的格式和内容。
解析请求
HTTP协议要求客户端连接后会发送一个请求,每个请求由三部分构成:
请求行 消息头 消息正文
首先请求行和消息头有一个共同的特点:都是以CRLF结尾的一行字符串.
因此先实现读取一行字符串的操作,测试将请求行读取出来并进行解析.之后可以再利用这个
操作完成消息头的读取并解析.

实现:
在ClientHandler中完成读取一行字符串的操作.
package com.webserver.core;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

/**
 * 1:解析请求  目的:将浏览器发送的请求内容读取并整理
 */
public class ClientHandler implements Runnable{
    private Socket socket;
    public ClientHandler(Socket socket){
        this.socket = socket;
    }
    public void run(){
        try {
            InputStream in = socket.getInputStream();
            /*
                http://localhost:8088/index.html
                请求行:GET /index.html HTTP/1.1

                http://localhost:8088
                请求行:GET / HTTP/1.1
             */
            int d;
            StringBuilder builder = new StringBuilder();
            char pre='a',cur='a';//pre记录上次读取的字符 cur记录本次读取的字符
            while((d = in.read())!=-1){
                cur = (char)d;
                if(pre==13&&cur==10){//判断是否连续读取到了回车+换行
                    break;
                }
                builder.append(cur);
                pre = cur;
            }
            String line = builder.toString().trim();
            System.out.println("请求行:"+line);

            //请求行相关信息
            String method;//请求方式
            String uri;//抽象路径
            String protocol;//协议版本
            /*
                GET /index.html HTTP/1.1
                data:{GET, /index.html, HTTP/1.1}
                提示:\s在正则表达式中表示一个空白字符
             */
            String[] data = line.split("\\s");
            method = data[0];
            uri = data[1];/*这里可能出现数组下标越界异常ArrayIndexOutOfBoundsException,                                
          原因是浏览器的问题!!!后期我们解决。
         建议:浏览器测试时尽量不使用后退,前进这样的功能测试。*/
            protocol = data[2];
            System.out.println("method:"+method);//method:GET
            System.out.println("uri:"+uri);//uri:/index.html
            System.out.println("protocol:"+protocol);//protocol:HTTP/1.1
//          http://localhost:8088/index.html
        } catch (IOException e) {
            e.printStackTrace();
        }


    }
}

 WebServerApplication服务端跑起来在网页输入http://localhost:8088/index.html,

看看是否能读取到请求行

 


 4.继续解析请求

上面完成了解析请求行的操作,继续使用该操作完成解析消息头
实现:
1:在ClientHandler中定义方法:readLine,用于将读取一行字符串的操作重用
2:将解析请求行中读取第一行内容的操作改为调用readLine,便于代码复用
3:继续利用readLine读取消息头并保存每个消息头
package com.webserver.core;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
/**
 * 1:解析请求  目的:将浏览器发送的请求内容读取并整理
 */
public class ClientHandler implements Runnable{
    private Socket socket;
    public ClientHandler(Socket socket){
        this.socket = socket;
    }
    public void run(){
        try {
            String line = readLine();
            System.out.println("请求行:"+line);

            //请求行相关信息
            String method;//请求方式
            String uri;//抽象路径
            String protocol;//协议版本
            String[] data = line.split("\\s");
            method = data[0];
            uri = data[1];//这里可能出现数组下标越界异常ArrayIndexOutOfBoundsException,原因是浏览器的问题!!!后期我们解决。建议:浏览器测试时尽量不使用后退,前进这样的功能测试。
            protocol = data[2];
            System.out.println("method:"+method);
            System.out.println("uri:"+uri);
            System.out.println("protocol:"+protocol);

            //读取消息头
            Map<String,String> headers = new HashMap<>();
//            while(!(line = readLine()).isEmpty()){
            while(true) {
                line = readLine();
                if(line.isEmpty()){//如果单独读取回车+换行,readLine方法会返回空字符串
                    break;
                }
                System.out.println("消息头:" + line);
                //将消息头按照冒号空格拆分为消息头的名字和值,并以key,value形式存入headers中
                data = line.split(":\\s");//Connection: keep-alive==>data:[Connection, keep-alive]
                headers.put(data[0],data[1]);
            }
            System.out.println("headers:"+headers);


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

    private String readLine() throws IOException {
        //当对同一个socket调用多次getInputStream方法时,获取回来的输入流始终是同一条流
        InputStream in = socket.getInputStream();
        int d;
        StringBuilder builder = new StringBuilder();
        char pre='a',cur='a';
        while((d = in.read())!=-1){
            cur = (char)d;
            if(pre==13&&cur==10){
                break;
            }
            builder.append(cur);
            pre = cur;
        }
        return builder.toString().trim();
    }

}


 5.重构HttpServletRequest

进行功能个拆分,将ClientHandler中第一个环节解析请求的细节拆分出去,使得
ClientHandler仅关心处理一次HTTP交互的流程控制.

实现:
1:新建一个包:com.webserver.http
2:在http包下新建类:HttpServletRequest 请求对象
  使用这个类的每一个实例表示客户端发送过来的一个HTTP请求
3:在HttpServletRequest的构造方法中完成解析请求的工作
4:ClientHandler第一步解析请求只需要实例化一个HttpServletRequest即可.

package com.webserver.core;

import com.webserver.http.HttpServletRequest;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
/**
 * 按照HTTP协议要求,与客户端完成一次交互流程为一问一答
 * 因此,这里分为三步完成该工作:
 * 1:解析请求  目的:将浏览器发送的请求内容读取并整理
 * 2:处理请求  目的:根据浏览器的请求进行对应的处理工作
 * 3:发送响应  目的:将服务端的处理结果回馈给浏览器
 */
public class ClientHandler implements Runnable{
    private Socket socket;
    public ClientHandler(Socket socket){
        this.socket = socket;
    }
    public void run(){
        try {
            //1解析请求,实例化请求对象的过程就是解析的过程
            HttpServletRequest request = new HttpServletRequest(socket);

            //2处理请求
            String path = request.getUri();
            System.out.println("请求的抽象路径:"+path);


            //3发送响应

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

 将处理请求的工作拆分给HttpServletRequest,

package com.webserver.http;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

public class HttpServletRequest {
    private Socket socket;
    //请求行相关信息
    private String method;//请求方式
    private String uri;//抽象路径
    private String protocol;//协议版本
    //消息头相关信息
    private Map<String,String> headers = new HashMap<>();

    public HttpServletRequest(Socket socket) throws IOException {
        this.socket = socket;
        //1解析请求行
        parseRequestLine();
        //2解析消息头
        parseHeaders();
        //3解析消息正文
        parseContent();
    }
    //解析请求行
    private void parseRequestLine() throws IOException {
        String line = readLine();
        System.out.println("请求行:"+line);
        String[] data = line.split("\\s");
        method = data[0];
        uri = data[1];//这里可能出现数组下标越界异常ArrayIndexOutOfBoundsException,原因是浏览器的问题!!!后期我们解决。建议:浏览器测试时尽量不使用后退,前进这样的功能测试。
        protocol = data[2];
        System.out.println("method:"+method);
        System.out.println("uri:"+uri);
        System.out.println("protocol:"+protocol);
    }
    //解析消息头
    private void parseHeaders() throws IOException {
        //读取消息头
        while(true) {
            String line = readLine();
            if(line.isEmpty()){
                break;
            }
            System.out.println("消息头:" + line);
            String[] data = line.split(":\\s");
            //key:Connection value:keep-alive
            headers.put(data[0],data[1]);//key:消息头的名字  value:消息头的值

        }
        System.out.println("headers:"+headers);
    }
    //解析消息正文
    private void parseContent(){}

    private String readLine() throws IOException {
        //当对同一个socket调用多次getInputStream方法时,获取回来的输入流始终是同一条流
        InputStream in = socket.getInputStream();
        int d;
        StringBuilder builder = new StringBuilder();
        char pre='a',cur='a';
        while((d = in.read())!=-1){
            cur = (char)d;
            if(pre==13&&cur==10){
                break;
            }
            builder.append(cur);
            pre = cur;
        }
        return builder.toString().trim();
    }


    public String getMethod() {
        return method;
    }

    public String getUri() {
        return uri;
    }

    public String getProtocol() {
        return protocol;
    }

    public String getHeader(String name) {
        return headers.get(name);
    }
}

 此处附上流程图-已完成第一步解析请求


6.响应客户端

本次完成响应客户端的工作
这里先将ClientHandler中处理一次交互的第三步:响应客户端 实现出来。
目标:将一个固定的html页面通过发送一个标准的HTTP响应回复给浏览器使其呈现出来。
需要的知识点:
1:HTML基础语法,html是超文本标记语言,用于构成一个"网页"的语言。
2:HTTP的响应格式。
实现:
一:先创建第一个页面index.html
1:在src/main/resource下新建目录static
  这个目录用于存放当前服务端下所有的静态资源。
 2:在static目录下新建目录新建第一个页面:index.html
二:实现将index.html页面响应给浏览器
在ClientHandler第三步发送响应处,按照HTTP协议规定的响应格式,将该页面包含在正文部分将其发送给浏览器即可。
三:第二步测试成功后,我们就可以根据请求中浏览器传递过来的抽象路径去static目录下定位浏览器实际
   请求的页面,然后用上述方式将该页面响应给浏览器,达到浏览器自主请求其需要的资源。
四:一问一答实现后,在ClientHandler异常处理机制最后添加finally并最终与客户端断开链接。这也是
   HTTP协议的要求,因此在这里调用socket.close()
五:可以在WebServerApplication中的start方法里将接受客户端链接的动作放在死循环里重复进行了。
   无论浏览器请求多少次,我们都可以遵从一问一答的原则响应用户请求的所有页面了。

 在SpringBoot中java目录下存放的是我们的业务代码,
那么resources下的static就是存放着我们的页面,我们也按照SpringBoot的操作来
先新建一个index页面

响应的大致内容:
        HTTP/1.1 404 NotFound(CRLF)      //状态行   (CRLF)为回车换行符
        Content-Type: text/html(CRLF)    //响应头    中间的空格也不能少
        Content-Length: 2546(CRLF)(CRLF) //响应头
        1011101010101010101......       //响应正文  二进制数据

 注意:最后结尾处完成一问一答应当断开流连接,客户端若想访问其他数据应当再次发送请求

package com.webserver.core;

import com.webserver.http.HttpServletRequest;

import java.io.*;
import java.net.Socket;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;

public class ClientHandler implements Runnable {

    private static File dir;
    private static File staticDir;

    static {
        try {
            //类加载路径
            //定位环境变量ClassPath(类加载路径)中"."的位置
            //在IDEA中执行项目时,类加载路径是从target/classes开始的

            dir = new File(
                    ClientHandler.class.getClassLoader()
                            .getResource(".").toURI());
            //定位target/classes/static目录
            staticDir = new File(dir, "static");
            System.out.println("static是否存在:" + staticDir.exists());
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }


    private Socket socket;

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


    public void run() {
        try {
            //1解析请求,实例化请求对象的过程就是解析的过程
            HttpServletRequest request = new HttpServletRequest(socket);


            //2处理请求
            String path = request.getUri();
            System.out.println("请求的抽象路径:" + path);


            //3发送响应
            File file = new File(staticDir,path);
            /*
            响应的大致内容:
                    HTTP/1.1 404 NotFound(CRLF)
                    Content-Type: text/html(CRLF)
                    Content-Length: 2546(CRLF)(CRLF)
                    1011101010101010101......
             */
            OutputStream os = socket.getOutputStream();
            //发送状态行
            String line = "HTTP/1.1 200 OK";
            byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
            os.write(data);
            os.write(13);//写入回车符
            os.write(10);//换行符
            //发送响应头
            line = "Content-Type: text/html";
            data = line.getBytes(StandardCharsets.ISO_8859_1);
            os.write(data);
            os.write(13);
            os.write(10);

            //发送响应头
            line = "Content-Length: "+file.length();
            data = line.getBytes(StandardCharsets.ISO_8859_1);
            os.write(data);
            os.write(13);
            os.write(10);

            //单独发送个回车+换行表示响应头发送完毕
            os.write(13);
            os.write(10);

            FileInputStream fis = new FileInputStream(file);
            byte[] buf = new byte[1024*10];
            int len = 0;
            while ((len = fis.read(buf)) != -1){
                os.write(buf,0,len);
            }

            //http://localhost:8088/index.html
            //path:/index.html
 
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //按照HTTP协议要求,一问一答后断开连接
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

7.响应404

我们已经实现了根据浏览器中用户在地址栏上输入的URL中的抽象路径去
static目录下寻找对应资源进行响应的工作。

但是会存在路径输入有误,导致定位不对(要么定位的是一个目录,要么该文件不存在)
此时再发送响应的响应正文时使用文件输入流读取就会出现异常提示该资源不存在。

这是一个典型的404情况,因此我们在ClientHandler处理请求的环节,在实例化File
对象根据抽象路径定位static下的资源后,要添加一个分支,若该资源存在则将其响应
回去,如果不存在则要响应404状态代码和404页面提示用户。

实现:
1:在static下新建一个子目录root
  该目录用于保存当前服务端所有网络应用共用的资源,比如404页面,因为无论请求哪个
  网络应用中的资源都可能发生不存在的情况。
2:在root目录下新建页面:404.html
  该页面居中显示一行字即可:404,资源不存在!
3:在ClientHandler处理请求的环节,当实例化File对象后添加一个分支,如果该File
  对象存在且表示的是一个文件则将其响应给浏览器
  否则发送的响应做如下变化:
  1:状态行中的状态代码改为404,状态描述改为NotFound
  2:响应头Content-Length发送的是404页面的长度
  3:响应正文为404页面内容
完成后,在浏览器地址栏输入一个不存在的资源地址,检查服务端是否正确响应404页面

 在clientHandler中的run方法处理请求添加分支

此时发送响应这个操作基本都是重复的动作,我们代码的最下面可以提成一个方法,println()

private void println(String line) throws IOException {
        OutputStream out = socket.getOutputStream();
        byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
        out.write(data);
        out.write(13);
        out.write(10);
    }

 发送响应这里调用println()

 目前已经初步完成http交互.

 


 8.重构HttpServletResponse

将ClientHandler中发送响应的工作拆分出去

实现:
1:在com.webserver.http包中新建类:HttpServletResponse 响应对象
2:在响应对象中定义对应的属性来保存响应内容并定义response方法来发送响应.
3:修改ClientHandler,使用响应对象完整响应的发送
package com.webserver.http;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

/**
 * 响应对象
 * 该类的每一个实例用于表示一个HTTP协议要求的响应内容
 * 每个响应由三部分构成:
 * 状态行,响应头,响应正文
 */
public class HttpServletResponse {
    private Socket socket;

    //状态行相关信息
    private int statusCode = 200;
    private String statusReason = "OK";

    //响应头相关信息

    //响应正文相关信息
    private File contentFile;//正文对应的实体文件

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

    /**
     * 将当前响应对象内容按照标准的响应格式发送给客户端
     */
    public void response() throws IOException {
        //发送状态行
        sendStatusLine();
        //发送响应头
        sendHeaders();
        //发送响应正文
        sendContent();
    }
    //发送状态行
    private void sendStatusLine() throws IOException {
//        HTTP/1.1 200 OK
        println("HTTP/1.1" + " " + statusCode + " " + statusReason);
    }
    //发送响应头
    private void sendHeaders() throws IOException {
        println("Content-Type: text/html");//告诉浏览器正文类型
        println("Content-Length: "+contentFile.length());//告诉浏览器正文长度(单位字节)
        //单独发送个回车+换行表示响应头发送完毕
        println("");
    }
    //发送响应正文
    private void sendContent() throws IOException {
        OutputStream out = socket.getOutputStream();
        FileInputStream fis = new FileInputStream(contentFile);
        byte[] buf = new byte[1024*10];//10kb
        int len = 0;//记录每次实际读取的字节数
        while( (len = fis.read(buf)) != -1  ){
            out.write(buf,0,len);
        }
    }


    private void println(String line) throws IOException {
        OutputStream out = socket.getOutputStream();
        byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
        out.write(data);
        out.write(13);
        out.write(10);
    }

    public int getStatusCode() {
        return statusCode;
    }

    public void setStatusCode(int statusCode) {
        this.statusCode = statusCode;
    }

    public String getStatusReason() {
        return statusReason;
    }

    public void setStatusReason(String statusReason) {
        this.statusReason = statusReason;
    }

    public File getContentFile() {
        return contentFile;
    }

    public void setContentFile(File contentFile) {
        this.contentFile = contentFile;
    }
}

 将发送响应响应这个操作交给HttpServletResponse,ClientHandler,只做实例化对象调用方法

package com.webserver.core;

import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;

import java.io.*;
import java.net.Socket;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

public class ClientHandler implements Runnable{
    private static File dir;
    private static File staticDir;

    static {
        //定位环境变量ClassPath(类加载路径)中"."的位置
        //在IDEA中执行项目时,类加载路径是从target/classes开始的
        try {
            dir = new File(
                    ClientHandler.class.getClassLoader()
                            .getResource(".").toURI()
            );
            //定位target/classes/static目录
            staticDir = new File(dir,"static");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    private Socket socket;
    public ClientHandler(Socket socket){
        this.socket = socket;
    }
    public void run(){
        try {
            //1解析请求,实例化请求对象的过程就是解析的过程
            HttpServletRequest request = new HttpServletRequest(socket);
            HttpServletResponse response = new HttpServletResponse(socket);

            //2处理请求
            String path = request.getUri();
            System.out.println("请求的抽象路径:"+path);
            File file = new File(staticDir,path);
            String line;
            if(file.isFile()){//浏览器请求的资源是否存在且是一个文件
                //正确响应其请求的文件
                response.setContentFile(file);
            }else{
                //响应404
                response.setStatusCode(404);
                response.setStatusReason("NotFound");
                file = new File(staticDir,"/root/404.html");
                response.setContentFile(file);
            }

            //3发送响应
            response.response();

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //按照HTTP协议要求,一问一答后断开连接
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

9.重构ClientHandler

将ClientHandler中处理请求的操作拆分出去

实现:
1:在com.webserver.core包下新建类:DispatcherServlet
并定义service方法,用来处理请求
2:将ClientHandler处理请求的操作移动到service方法中去
3:ClientHandler通过调用DispatcherServlet的service完成处理请求环节. 

 DispatcherServlet是用于完成一个http交互流程中处理请求的环节工作.
 实际上这个类是Spring MVC框架提供的一个核心的类,用于和Web容器(Tomcat)整合,
 使得处理请求的环节可以由Spring MVC框架完成.

package com.webserver.core;

import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;

import java.io.File;
import java.net.URISyntaxException;

public class DispatcherServlet {
    private static DispatcherServlet instance = new DispatcherServlet();
    private static File dir;
    private static File staticDir;
    static {
        //定位环境变量ClassPath(类加载路径)中"."的位置
        //在IDEA中执行项目时,类加载路径是从target/classes开始的
        try {
            dir = new File(
                    ClientHandler.class.getClassLoader()
                            .getResource(".").toURI()
            );
            //定位target/classes/static目录
            staticDir = new File(dir,"static");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    private DispatcherServlet(){}

    public static DispatcherServlet getInstance(){
        return instance;
    }

    /**
     * 处理请求的方法
     * @param request 请求对象,通过这个对象可以获取来自浏览器提交的内容
     * @param response 响应对象,通过设置响应对象将处理结果最终发送给浏览器
     */
    public void service(HttpServletRequest request, HttpServletResponse response){
        String path = request.getUri();
        System.out.println("请求的抽象路径:"+path);
        File file = new File(staticDir,path);
        if(file.isFile()){//浏览器请求的资源是否存在且是一个文件
            //正确响应其请求的文件
            response.setContentFile(file);
        }else{
            //响应404
            response.setStatusCode(404);
            response.setStatusReason("NotFound");
            file = new File(staticDir,"/root/404.html");
            response.setContentFile(file);
        }
    }
}

10.设置响应头属性

1:现在的我们仅发送了两个响应头(Content-Length和Content-Type).
  虽然目前仅需要这两个头,但是服务端实际可以根据处理情况设置需要发送其他响应头
2:Content-Type的值是固定的"text/html",这导致浏览器请求到该资源后无法正确
  理解该资源因此没有发挥出实际作用.

本次处理响应头可根据设置进行发送.

实现:
1:在HttpServletResponse中添加一个Map类型的属性用于保存所有要发送的响应头
  Map<String,String> headers

2:修改发送响应头的方法sendHeaders中的逻辑,将固定发送两个头的操作改为遍历
  headers这个Map,将所有要发送的响应头逐个发送

3:只需要在发送前根据处理情况向headers中put要发送的响应头即可.这个工作需要
  3.1:在响应对象中添加一个方法:addHeader,将要发送的响应头存入headers中
  3.2:在DispatcherServlet处理请求环节调用addHeader存放要发送的响应头即可
package com.webserver.http;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * 响应对象
 * 该类的每一个实例用于表示一个HTTP协议要求的响应内容
 * 每个响应由三部分构成:
 * 状态行,响应头,响应正文
 */
public class HttpServletResponse {
    private Socket socket;

    //状态行相关信息
    private int statusCode = 200;
    private String statusReason = "OK";

    //响应头相关信息
    private Map<String,String> headers = new HashMap<>();

    //响应正文相关信息
    private File contentFile;//正文对应的实体文件

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

    /**
     * 将当前响应对象内容按照标准的响应格式发送给客户端
     */
    public void response() throws IOException {
        //发送状态行
        sendStatusLine();
        //发送响应头
        sendHeaders();
        //发送响应正文
        sendContent();
    }
    //发送状态行
    private void sendStatusLine() throws IOException {
//        HTTP/1.1 200 OK
        println("HTTP/1.1" + " " + statusCode + " " + statusReason);
    }
    //发送响应头
    private void sendHeaders() throws IOException {
        /*
            headers:
            key             value
            Content-Type    text/html
            Content-Length  245
            ...             ...
         */
        Set<Map.Entry<String,String>> entrySet = headers.entrySet();
        for(Map.Entry<String,String> e: entrySet){
            String name = e.getKey();
            String value = e.getValue();
            println(name + ": " + value);
        }
        //单独发送个回车+换行表示响应头发送完毕
        println("");
    }
    //发送响应正文
    private void sendContent() throws IOException {
        OutputStream out = socket.getOutputStream();
        FileInputStream fis = new FileInputStream(contentFile);
        byte[] buf = new byte[1024*10];//10kb
        int len = 0;//记录每次实际读取的字节数
        while( (len = fis.read(buf)) != -1  ){
            out.write(buf,0,len);
        }
    }


    private void println(String line) throws IOException {
        OutputStream out = socket.getOutputStream();
        byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
        out.write(data);
        out.write(13);
        out.write(10);
    }

    public int getStatusCode() {
        return statusCode;
    }

    public void setStatusCode(int statusCode) {
        this.statusCode = statusCode;
    }

    public String getStatusReason() {
        return statusReason;
    }

    public void setStatusReason(String statusReason) {
        this.statusReason = statusReason;
    }

    public File getContentFile() {
        return contentFile;
    }

    public void setContentFile(File contentFile) {
        this.contentFile = contentFile;
    }

    /**
     * 添加一个响应头
     * @param name  响应头的名字
     * @param value 响应头的值
     */
    public void addHeader(String name,String value){
        headers.put(name,value);
    }
}

在DispatcherServlet处理请求分支这里调用addHeader方法 


 11.响应正确的MIME类型​​​​​​​​​​​​​​

实现HttpServletResponse响应正确的MIME类型,即:Content-Type的值
这里我们使用java.nio.file.Files这个类来完成这个功能。

这样一来,服务端就可以正确响应浏览器请求的任何资源了,使得浏览器可以正确显示内容

实现:
1:将原DispatcherServlet中设置响应头Content-Type和Content-Length的
  工作移动到HttpServletResponse的设置响应正文方法setContentFile中.
  原因:一个响应中只要包含正文就应当包含说明正文信息的两个头Content-Type和
  Content-Length.因此我们完全可以在设置正文的时候自动设置这两个头.
  这样做的好处是将来设置完正文(调用setContentFile)后无需再单独设置这两个头了.
2:使用Files的静态方法probeContentType按照正文文件分析MIME类型并设置到
  响应头Content-Type上

创建成员属性   File contentFile

 提供setContentFile 方法()


12.解决空请求问题

HTTP协议注明:为了保证服务端的健壮性,应当忽略客户端空的请求。
浏览器有时会发送空请求,即:与服务端链接后没有发送标准的HTTP请求内容,直接与服务端断开链接。
此时服务端按照一问一答的处理流程在解析请求时请求行由于没有内容,在拆分后获取信息会出现数组
下标越界。
解决:当解析请求行时发现没有内容就对外抛出空请求异常(自定义的一个异常),并最终抛出给
ClientHandler,使其忽略后续的处理请求与发送响应的工作,直接与客户端断开来忽略本次操作。

实现:
1:在com.webserver.http包下新建自定义异常:EmptyRequestException,空请求异常
2:在HttpServletRequest的解析请求行方法parseRequestLine中,当读取请求行后发现是一个
  空字符串则对外抛出空请求异常并通过构造方法继续对外抛出给ClientHandler
3:ClientHandler添加一个新的catch专门捕获空请求异常,捕获后无需做任何处理,目的仅仅是
  忽略处理请求和响应客户端的工作

 

 在HttpServletRequest,

 捕获空请求后断开连接

总结

还未完结........待更新!


网站公告

今日签到

点亮在社区的每一天
去签到