B/S模式下的MVC架构迭代(1)

文章详细介绍了HTTP协议的基本结构,包括请求行、消息头和消息正文,并通过Java实现了一个简单的Web服务器,模拟了浏览器与服务器之间的交互。改造过程中涉及了请求解析、响应构建、状态码处理、单例模式应用以及异常处理等技术。最后讨论了如何处理空请求和设置正确的MIME类型。
摘要由CSDN通过智能技术生成

目录

HTTP/HTTPS简介

浏览器请求消息(Request)

1.请求行

2.消息头

3.消息正文

服务器响应消息(Response)

1.状态行

2.响应头

3.响应正文

HTTP协议以及浏览器与服务端之间的处理:

一改造:解析客户端发送的请求行

二改造:继续解析请求,解析消息头

三改造:重构代码将ClientHandler中请求解析的工作拆分HTTPServletRequest类中​编辑

HTTPServletRequest 类:解析请求行,解析消息头,解析消息正文。

ClientHandler类:第一步解析请求只需要实例化一个HttpServletRequest即可 。

四改造:完成服务器响应客户端的工作

ClientHandler类:处理请求,发送响应给浏览器(状态行,响应头,响应正文) 。

五改造:完成404的响应

ClientHandler类:处理请求 ,根据用户提供的抽象路径path定位文件,文件没有返回404页面。

六改造:重构代码将ClientHandler中发送响应的工作拆HTTPServletResponse类中

HTTPServletResponse类: 响应对象(状态行,响应头,响应正文)

ClientHandler类: 响应对象要实例化一个HttpServletResponse,发送响应

单例模式(基础)

七改造:重构代码将ClientHandler中处理请求的操作拆分到DispatcherServlet类中(利用springMVC)

DispatcherServlet类:service方法处理请求,设置为单例模式,全局仅创建一个实例 

ClientHandler类:ClientHandler通过调用DispatcherServlet的service方法完成处理请求环节 。

解决空请求问题:浏览器有时会发送空请求

EmptyRequestException类:新建自定义异常,空请求异常

HTTPServletResponse类:解析请求行方法parseRequestLine中,当读取请求是一个空字符串时,则对外抛出空请求异常并通过构造方法对外抛出给ClientHandler 。

ClientHandler类:添加一个新的catch专门捕获请求异常,不做处理目的仅是为了断开连接 。

八改造:解决响应头问题,可根据设置进行发送

HTTPServletResponse类:根据设置自动发送响应头,添加addHeader方法。

DispatcherServlet类: 调用addHeader方法,响应消息头。

九改造:实现HttpServletResponse响应正确的MIME类型,即:Content-Type的值

HTTPServletResponse类:设置响应正文方法setContentFile。

DispatcherServlet类:调用setContentFile方法,发送正文内容 。


HTTP/HTTPS简介

HTTP 协议是 Hyper Text Transfer Protocol(超文本传输协议)的缩写,由万维网制定,是浏览器与服务器通讯的应用层协议,规定了浏览器与服务器之间的交互规则以及交互数据的格式信息。

HTTP 要求浏览器与服务器的传输层协议是可靠的,因此使用 TCP/IP 通信协议来传递数据。

HTTPS 协议是 HyperText Transfer Protocol Secure(超文本传输安全协议)的缩写,是一种通过计算机网络进行安全通信的传输协议。

HTTPS 经由 HTTP 进行通信,但利用 SSL/TLS 来加密数据包,HTTPS 开发的主要目的,是提供对网站服务器的身份认证,保护交换资料的隐私与完整性。

HTTP 的 URL 是由 http:// 起始与默认使用端口 80,而 HTTPS 的 URL 则是由 https:// 起始与默认使用端口443

浏览器请求消息(Request)

请求是浏览器给服务端发送的内容

Http协议将一个请求分为三部分:请求行,消息头,消息正文(可以没有)

GET /index.html HTTP/1.1   请求行
Host: localhost:8088       消息头
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36
Sec-Fetch-User: ?1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
1010101101001.....          请求正文

1.请求行

  • 请求行是一行字符串,以连续的字符(回车符和换行符)作为结束一行的标志。
        回车符:在ASC编码中2进制内容对应的整数是13.回车符通常用cr表示。
        换行符:在ASC编码中2进制内容对应的整数是10.换行符通常用lf表示。
  • 请求行分为为三部:请求方式(SP)抽象路径(SP)协议版本(CRLF)  SP:是空格
        GET /myweb/index.html HTTP/1.1
  • URL地址格式:
        协议://主机地址信息/抽象路径
        http://localhost:8088/TeduStore/index.html
        GET /TeduStore/index.html HTTP/1.1

2.消息头

消息头是浏览器可以给服务器端发送的一些附加信息,有的用来说明浏览器自身内容,有的用来告知服务器端交互细节,有的告知服务端消息正文详情等。

消息头以若干行组成,每行以(CRLF)结尾

消息头格式:消息头名字(:SP)消息的值

消息头结尾部分是以单独的(CRLF)

例如:

Host: localhost:8088(CRLF)
Connection: keep-alive(CRLF)
Upgrade-Insecure-Requests: 1(CRLF)
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36(CRLF)
Sec-Fetch-User: ?1(CRLF)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9(CRLF)
Sec-Fetch-Site: none(CRLF)
Sec-Fetch-Mode: navigate(CRLF)
Accept-Encoding: gzip, deflate, br(CRLF)
Accept-Language: zh-CN,zh;q=0.9(CRLF)(CRLF)

3.消息正文

消息正文是2进制数据,通常是用户上传的的信息,比如:在页面输入注册信息,上传的附件内容。

服务器响应消息(Response)

响应是服务器发送给客户端的内容。

一个响应包含三部分:状态行,响应头,响应正文

1.状态行

状态行是一行字符串(CRLF结尾)

状态行三部分组成:协议版本(SP)状态代码(SP)状态描述(CRLF)

                                protocol(SP)statusCode(SP)statusReason(CRLF)

                  例如:  HTTP/1.1 200 OK

  状态代码是一个3位数,分为5类:

1xx:保留
2xx:成功,表示处理成功,并正常响应
3xx:重定向,表示处理成功,但是需要浏览器进一步请求
4xx:客户端错误,表示客户端请求错误导致服务端无法处理
5xx:服务端错误,表示服务端处理请求过程出现了错误

2.响应头

响应头与请求中的消息头格式一致,表示的是服务发送给客户端的附加信息。

这里的两个重要响应头:
1.Content—Length
        是用来告知浏览器响应正文的长度,单位是字节
2.Content-Type
是用来告知浏览器响应正文中的内容是什么类型的数据(图片,页面等等)
不同的类型对应的值是不同的,比如:
文件类型      Content-Type对应的值
  html          text/html
  css           text/css
  js            application/javascript
  png           image/png
  gif           image/gif
  jpg           image/jpeg

浏览器回接收正文前,根据上述两个响应头来得知长度和类型从而读取出来做对应的处理以显示给用户看。

3.响应正文

二进制数据部分,包含的通常是客户端实际请求的资源内容。

响应正文:
1011101010101010101......

HTTP协议以及浏览器与服务端之间的处理:

包结构:

 主启动类:项目的主类BirdBootApplication,用于启动服务器,连接客户端。

package com.webserver.core;

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

/**BirdBootApplication
 * 项目的主类,用于启动WebServer
 * 该项目主要功能是实现Tomcat这个WebServer的核心功能。了解HTTP协议以及浏览器与服务端
 * 之间的处理细节。
 * 了解SpringMVC核心类的设计与实现
 */
public class BirdBootApplication {
    private ServerSocket serverSocket;
    public BirdBootApplication(){
        try {
            System.out.println("正在启动服务端...");
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start(){
        try {
            System.out.println("等待客户端连接...");
            Socket socket = serverSocket.accept();
            System.out.println("一个客户端连接了!");
            //启动一个线程处理该客户端交互
            ClientHandler handler = new ClientHandler(socket);
            Thread t = new Thread(handler);
            t.start();

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

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

ClientHandler类:该线程任务负责处理与某个客户端的一次HTTP交互。

package com.webserver.core;

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

/**ClientHandler
 * 该线程任务负责处理与某个客户端的一次HTTP交互。
 * 由于HTTP协议要求客户端和服务端采取一问一答的原则,因此这里处理与客户端的一次交互规划
 * 为三个步骤:
 * 1:解析请求(将客户端发送过来的请求内容读取到)
 * 2:处理请求(根据请求内容进行对应的处理)
 * 3:发送响应(将处理结果回馈给浏览器)
 * 断开连接
 */
public class ClientHandler implements Runnable{
    private Socket socket;
    public ClientHandler(Socket socket){
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            InputStream in = socket.getInputStream();
            int d;
            while((d = in.read())!=-1){
                System.out.print((char)d);
            }

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


一改造:解析客户端发送的请求行

HTTP协议要求客户端连接后会发送一个请求,每个请求由三部分构成:请求行 消息头 消息正文。

首先请求行和消息头有一个共同的特点:都是以CRLF结尾的一行字符串.
因此先实现读取一行字符串的操作,测试将请求行读取出来并进行解析,之后可以再利用这个
操作完成消息头的读取并解析。

实现:
启动类不变,在ClientHandler中完成读取一行字符串的操作.

package com.webserver.core;

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

/**
 * 该线程任务负责处理与某个客户端的一次HTTP交互。
 * 由于HTTP协议要求客户端和服务端采取一问一答的原则,因此这里处理与客户端的一次交互规则,划
 * 为三个步骤:
 * 1:解析请求(将客户端发送过来的请求内容读取到)
 * 2:处理请求(根据请求内容进行对应的处理)
 * 3:发送响应(将处理结果回馈给浏览器)
 * 断开连接
 */
public class ClientHandler implements Runnable{
    private Socket socket;
    public ClientHandler(Socket socket){
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            InputStream in = socket.getInputStream();
            StringBuilder builder = new StringBuilder();
            char pre='a',cur='a';//pre表示上次读取的字符,cur表示本次读取的字符
            int d;
            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;//协议版本

            String[] data = line.split("\\s");
            method = data[0];
            uri = data[1];
            protocol = data[2];
            System.out.println("method:"+method);
            System.out.println("uri:"+uri);
            System.out.println("protocol:"+protocol);

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

二改造:继续解析请求,解析消息头

实现:
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;

/**
 * 该线程任务负责处理与某个客户端的一次HTTP交互。
 * 由于HTTP协议要求客户端和服务端采取一问一答的原则,因此这里处理与客户端的一次交互规划
 * 为三个步骤:
 * 1:解析请求(将客户端发送过来的请求内容读取到)
 * 2:处理请求(根据请求内容进行对应的处理)
 * 3:发送响应(将处理结果回馈给浏览器)
 * 断开连接
 */
public class ClientHandler implements Runnable{
    private Socket socket;
    public ClientHandler(Socket socket){
        this.socket = socket;
    }

    @Override
    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];
            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(true) {
                line = readLine();
                if(line.isEmpty()){//如果读取的消息头是空字符串,说明单独读取到了CRLF
                    break;
                }
                System.out.println("消息头:"+line);
                //将消息头按照": "拆分为名字和值并作为key,value存入到headers中
                data = line.split(":\\s");
                headers.put(data[0],data[1]);
            }
            System.out.println("headers:"+headers);


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

    private String readLine() throws IOException {
        //调用同一个socket对象若干次getInputStream()方法返回的始终是同一条输入流
        InputStream in = socket.getInputStream();
        StringBuilder builder = new StringBuilder();
        char pre='a',cur='a';//pre表示上次读取的字符,cur表示本次读取的字符
        int d;
        while((d = in.read())!=-1){
            cur = (char)d;//本次读取到的字符
            if(pre==13&&cur==10){//若上次读取的是回车符并且本次读取的是换行符
                break;
            }
            builder.append(cur);//拼接本次读取到的字符
            pre = cur;//进入下次循环前将本次读取的字符记作上次读取的字符
        }
        return builder.toString().trim();
    }
}

三改造:重构代码将ClientHandler中请求解析的工作拆分HTTPServletRequest类中

将线程ClientHandler中第一个环节的解析请求的细节拆分出去,通过实例化HttpServletRequest中的三个方法具体解析请求行,消息头,消息正文。使得ClientHandler仅关心处理一次HTTP交互的流程控制。

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

包结构:

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;

/**HttpServletRequest
 * 请求对象,该类的每一个实例用于标识HTTP协议规定的请求内容
 * 一个请求由三部分构成:
 * 1:请求行
 * 2:消息头
 * 3:消息正文
 */
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];
        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()){//如果读取的消息头是空字符串,说明单独读取到了CRLF
                break;
            }
            System.out.println("消息头:"+line);
            //将消息头按照": "拆分为名字和值并作为key,value存入到headers中
            String[] data = line.split(":\\s");
            headers.put(data[0],data[1]);
        }
        System.out.println("headers:"+headers);
    }
    //解析消息正文
    private void parseContent(){}

    private String readLine() throws IOException {
        //调用同一个socket对象若干次getInputStream()方法返回的始终是同一条输入流
        InputStream in = socket.getInputStream();
        StringBuilder builder = new StringBuilder();
        char pre='a',cur='a';//pre表示上次读取的字符,cur表示本次读取的字符
        int d;
        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;
    }

    /**
     * 根据消息头的名字获取对应的值
     * @param name
     * @return
     */
    public String getHeader(String name) {
        return headers.get(name);
    }
}

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;

/**ClientHandler
 * 该线程任务负责处理与某个客户端的一次HTTP交互。
 * 由于HTTP协议要求客户端和服务端采取一问一答的原则,因此这里处理与客户端的一次交互规划
 * 为三个步骤:
 * 1:解析请求(将客户端发送过来的请求内容读取到)
 * 2:处理请求(根据请求内容进行对应的处理)
 * 3:发送响应(将处理结果回馈给浏览器)
 * 断开连接
 */
public class ClientHandler implements Runnable{
    private Socket socket;
    public ClientHandler(Socket socket){
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
//            1:解析请求(将客户端发送过来的请求内容读取到)
            HttpServletRequest request = new HttpServletRequest(socket);
//            2:处理请求(根据请求内容进行对应的处理)

//            3:发送响应(将处理结果回馈给浏览器)


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

四改造:完成服务器响应客户端的工作

这里先将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方法里将接受客户端链接的动作放在死循环里重复进行了。无论浏览器请求多少次,我们都可以遵从一问一答的原则响应用户请求的所有页面了。

包结构:

ClientHandler类:处理请求,发送响应给浏览器(状态行,响应头,响应正文) 。

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;
import java.util.HashMap;
import java.util.Map;

/**ClientHandler
 * 该线程任务负责处理与某个客户端的一次HTTP交互。
 * 由于HTTP协议要求客户端和服务端采取一问一答的原则,因此这里处理与客户端的一次交互规划
 * 为三个步骤:
 * 1:解析请求(将客户端发送过来的请求内容读取到)
 * 2:处理请求(根据请求内容进行对应的处理)
 * 3:发送响应(将处理结果回馈给浏览器)
 * 断开连接
 */
public class ClientHandler implements Runnable{
    private Socket socket;
    private static File rootDir;
    private static File staticDir;

    static{
        try {
            //rootDir表示类加载路径:target/classes目录
            rootDir = new File(
                    ClientHandler.class.getClassLoader()
                            .getResource(".").toURI()
            );
            //定位static目录(static目录下存放的是所有静态资源)
            staticDir = new File(rootDir,"static");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }


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

    @Override
    public void run() {
        try {
            /*
                http://localhost:8088/index.html
                http://localhost:8088/classtable.html

                http://localhost:8088/abc.html
                http://localhost:8088/
             */
//            1:解析请求(将客户端发送过来的请求内容读取到)
            HttpServletRequest request = new HttpServletRequest(socket);
//            2:处理请求(根据请求内容进行对应的处理)
            String path = request.getUri();

//            3:发送响应(将处理结果回馈给浏览器)
            //定位static目录下的index.html
            File file = new File(staticDir,path);
            System.out.println("文件是否存在:"+file.exists());

            /*
                测试:给浏览器发送一个响应,包含static目录下的index.html
                HTTP/1.1 200 OK(CRLF)
                Content-Type: text/html(CRLF)
                Content-Length: 2546(CRLF)(CRLF)
                1011101010101010101......
             */
            OutputStream out = socket.getOutputStream();
            //发送状态行
            //HTTP/1.1 200 OK(CRLF)
            println("HTTP/1.1 200 OK");
            //发送响应头
            //Content-Type: text/html(CRLF)
            println("Content-Type: text/html");
            //Content-Length: 2546(CRLF)
            println("Content-Length: "+file.length());
            //单独发送回车+换行表达响应头发送完毕
            println("");

            //发送响应正文
            //将index.html文件所有数据发送
            FileInputStream fis = new FileInputStream(file);
            byte[] data = new byte[1024*10];
            int len;
            while((len = fis.read(data))!=-1){
                out.write(data,0,len);
            }

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

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


    public static void main(String[] args) throws URISyntaxException {
        File rootDir = new File(
                ClientHandler.class.getClassLoader()
                        .getResource(".").toURI()
        );
        //定位static目录(static目录下存放的是所有静态资源)
        File staticDir = new File(rootDir,"static");
        //定位static目录下的index.html
        File file = new File(staticDir,"index.html");
        System.out.println("文件是否存在:"+file.exists());
        System.out.println(rootDir);
    }

}

五改造:完成404的响应

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

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

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

实现:
1:在static下新建一个子目录root
      该目录用于保存当前服务端所有网络应用共用的资源,比如404页面,因为无论请求哪个
      网络应用中的资源都可能发生不存在的情况。
2:在root目录下新建页面:404.html
      该页面居中显示一行字即可:404,资源不存在!
3:在ClientHandler处理请求的环节,当实例化File对象后添加一个分支,如果该File
      对象存在且表示的是一个文件(isFile方法)则将其响应给浏览器
      否则发送的响应做如下变化:

  • 状态行中的状态代码改为404,状态描述改为NotFound
  • 响应头Content-Length发送的是404页面的长度
  • 响应正文为404页面内容

测试:完成后,在浏览器地址栏输入一个不存在的资源地址,检查服务端是否正确响应404页面

 包结构:

ClientHandler类:处理请求 ,根据用户提供的抽象路径path定位文件,文件没有返回404页面。

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;
import java.util.HashMap;
import java.util.Map;

/**ClientHandler
 * 该线程任务负责处理与某个客户端的一次HTTP交互。
 * 由于HTTP协议要求客户端和服务端采取一问一答的原则,因此这里处理与客户端的一次交互规划
 * 为三个步骤:
 * 1:解析请求(将客户端发送过来的请求内容读取到)
 * 2:处理请求(根据请求内容进行对应的处理)
 * 3:发送响应(将处理结果回馈给浏览器)
 * 断开连接
 */
public class ClientHandler implements Runnable{
    private Socket socket;
    private static File rootDir;
    private static File staticDir;
    static{
        try {
            //rootDir表示类加载路径:target/classes目录
            rootDir = new File(
                    ClientHandler.class.getClassLoader()
                            .getResource(".").toURI()
            );
            //定位static目录(static目录下存放的是所有静态资源)
            staticDir = new File(rootDir,"static");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }


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

    @Override
    public void run() {
        try {
//            1:解析请求(将客户端发送过来的请求内容读取到)
            HttpServletRequest request = new HttpServletRequest(socket);
//            2:处理请求(根据请求内容进行对应的处理)
            String path = request.getUri();  //得到浏览器发送过来的请求路径
            File file = new File(staticDir,path);
            int statusCode;//状态代码
            String statusReason;//状态描述
            if(file.isFile()){//根据用户提供的抽象路径去static目录下定位到一个文件
                statusCode = 200;
                statusReason = "OK";
            }else{
                statusCode = 404;
                statusReason = "NotFound";
                file = new File(staticDir,"/root/404.html");
            }

//            3:发送响应(将处理结果回馈给浏览器)
            OutputStream out = socket.getOutputStream();
            //发送状态行
            println("HTTP/1.1"+" "+statusCode+" "+statusReason);
            //发送响应头
            //Content-Type: text/html(CRLF)
            println("Content-Type: text/html");
            //Content-Length: 2546(CRLF)
            println("Content-Length: "+file.length());
            //单独发送回车+换行表达响应头发送完毕
            println("");

            //发送响应正文
            //将index.html文件所有数据发送
            FileInputStream fis = new FileInputStream(file);
            byte[] data = new byte[1024*10];
            int len;
            while((len = fis.read(data))!=-1){
                out.write(data,0,len);
            }

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

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


    public static void main(String[] args) throws URISyntaxException {
        File rootDir = new File(
                ClientHandler.class.getClassLoader()
                        .getResource(".").toURI()
        );
        //定位static目录(static目录下存放的是所有静态资源)
        File staticDir = new File(rootDir,"static");
        //定位static目录下的index.html
        File file = new File(staticDir,"index.html");
        System.out.println("文件是否存在:"+file.exists());
        System.out.println(rootDir);
    }

}

六改造:重构代码将ClientHandler中发送响应的工作拆HTTPServletResponse类中

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

 包结构:

HTTPServletResponse类: 响应对象(状态行,响应头,响应正文)

package com.webserver.http;

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

/**HttpServletResponse
 * 响应对象
 * 该类的每一个实例用于表示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;
    }

    /**
     * 该方法会将当前响应对象内容以标准的HTTP响应格式通过socket获取的输出流发送给
     * 对应的客户端。
     */
    public void response() throws IOException {
        //发送状态行
        sendStatusLine();
        //发送响应头
        sendHeaders();
        //发送响应正文
        sendContent();
    }
    //发送状态行
    private void sendStatusLine() throws IOException {
        println("HTTP/1.1"+" "+statusCode+" "+statusReason);
    }
    //发送响应头
    private void sendHeaders() throws IOException {
        //Content-Type: text/html(CRLF)
        println("Content-Type: text/html");
        //Content-Length: 2546(CRLF)
        println("Content-Length: "+contentFile.length());
        //单独发送回车+换行表达响应头发送完毕
        println("");
    }
    //发送响应正文
    private void sendContent() throws IOException {
        OutputStream out = socket.getOutputStream();  //给浏览器响应的内容 
        try(
            //读取浏览器传入的消息正文
            FileInputStream fis = new FileInputStream(contentFile);  
        ) {
            byte[] data = new byte[1024 * 10];
            int len;
            while ((len = fis.read(data)) != -1) {
                out.write(data, 0, len);
            }
        }
    }


    private void println(String line) throws IOException {
        OutputStream out = socket.getOutputStream();
        out.write(line.getBytes(StandardCharsets.ISO_8859_1));
        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;
    }
}

ClientHandler类: 响应对象要实例化一个HttpServletResponse,发送响应

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;

/**ClientHandler
 * 该线程任务负责处理与某个客户端的一次HTTP交互。
 * 由于HTTP协议要求客户端和服务端采取一问一答的原则,因此这里处理与客户端的一次交互规划
 * 为三个步骤:
 * 1:解析请求(将客户端发送过来的请求内容读取到)
 * 2:处理请求(根据请求内容进行对应的处理)
 * 3:发送响应(将处理结果回馈给浏览器)
 * 断开连接
 */
public class ClientHandler implements Runnable{
    private Socket socket;
    private static File rootDir;
    private static File staticDir;
    static{
        try {
            //rootDir表示类加载路径:target/classes目录
            rootDir = new File(
                    ClientHandler.class.getClassLoader()
                            .getResource(".").toURI()
            );
            //定位static目录(static目录下存放的是所有静态资源)
            staticDir = new File(rootDir,"static");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }


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

    @Override
    public void run() {
        try {
//            1:解析请求(将客户端发送过来的请求内容读取到)
            HttpServletRequest request = new HttpServletRequest(socket);
            HttpServletResponse response = new HttpServletResponse(socket);

//            2:处理请求(根据请求内容进行对应的处理)
            String path = request.getUri();
            File file = new File(staticDir,path);
            if(file.isFile()){//根据用户提供的抽象路径去static目录下定位到一个文件
                /*
                   将file这个页面传参给setContent方法,通过HTTPServletResponse类中的                
                   sendContent()方法发送响应
                */
                response.setContentFile(file);  
            }else{
                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();
            }
        }
    }




    public static void main(String[] args) throws URISyntaxException {
        File rootDir = new File(
                ClientHandler.class.getClassLoader()
                        .getResource(".").toURI()
        );
        //定位static目录(static目录下存放的是所有静态资源)
        File staticDir = new File(rootDir,"static");
        //定位static目录下的index.html
        File file = new File(staticDir,"index.html");
        System.out.println("文件是否存在:"+file.exists());
        System.out.println(rootDir);
    }

}

单例模式(基础)

单例模式是java23中设计模式之一,使用它定义的类全局仅可创建一个实例

实现:

1:私有化构造器(防止外界通过new来实例化对象)

2:提供一个静态方法用于将当前类实例返回给外界

3:提供一个静态的私有的当前类型实例的属性并初始化(确保只有一个实例)

package com.webserver.test;

/**
 *  单例模式
 */
public class Singleton {
    //提供一个静态的私有的当前类型实例的属性并初始化(确保只有一个实例)
    private static Singleton instance = new Singleton();

    //私有化构造器(防止外界通过new来实例化对象)
    private Singleton(){}

    //提供一个静态方法用于将当前类实例返回给外界
    public static Singleton getInstance(){
        return instance;
    }

}
package com.webserver.test;

/**测试
 * 单例模式
 */

public class SingletonDemo {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        System.out.println(s1);
        Singleton s2 = Singleton.getInstance();
        System.out.println(s2);
    }
}

七改造:重构代码将ClientHandler中处理请求的操作拆分到DispatcherServlet类中(利用springMVC)

实现:
1:在com.webserver.core包下新建类:DispatcherServlet,并定义service方法,用来处理请求。                DispatchServlet类全局仅可创建一个实例(单例模式)
2:将ClientHandler处理请求的操作移动到service方法中去
3:ClientHandler通过调用DispatcherServlet的service完成处理请求环节

包结构:

DispatcherServlet类:service方法处理请求,设置为单例模式,全局仅创建一个实例 

package com.webserver.core;

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

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

/**DispatcherServlet
 * 这个类是SpringMVC框架与tomcat容器整合的一个关键类,接管了处理请求的工作。
 * 这样当tomcat将请求对象和响应对象创建完毕后在处理请求的环节通过调用这个类来完成,从而
 * 将处理请求交给了SpringMVC框架
 * 并在处理后发送响应给浏览器
 */
public class DispatcherServlet {
    //单例模式
    //定义一个私有的静态的当前类型的实例的属性并初始化
    private static DispatcherServlet instance = new DispatcherServlet();
    private static File rootDir;
    private static File staticDir;
    static{
        try {
            //rootDir表示类加载路径:target/classes目录
            rootDir = new File(
                    //当前类
                    DispatcherServlet.class.getClassLoader()
                            .getResource(".").toURI()
            );
            //定位static目录(static目录下存放的是所有静态资源)
            staticDir = new File(rootDir,"static");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    //私有化构造器
    private DispatcherServlet(){}

    //提供一个静态方法用于将当前类实例返回给外界
    public static DispatcherServlet getInstance(){
        return instance;
    }


    public void service(HttpServletRequest request, HttpServletResponse response){
        String path = request.getUri();
        File file = new File(staticDir,path);
        if(file.isFile()){//根据用户提供的抽象路径去static目录下定位到一个文件
            response.setContentFile(file);
        }else{
            response.setStatusCode(404);
            response.setStatusReason("NotFound");
            file = new File(staticDir,"/root/404.html");
            response.setContentFile(file);
        }
    }
}

ClientHandler类:ClientHandler通过调用DispatcherServlet的service方法完成处理请求环节 。

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;


/**ClientHandler
 * 该线程任务负责处理与某个客户端的一次HTTP交互。
 * 由于HTTP协议要求客户端和服务端采取一问一答的原则,因此这里处理与客户端的一次交互规划
 * 为三个步骤:
 * 1:解析请求(将客户端发送过来的请求内容读取到)
 * 2:处理请求(根据请求内容进行对应的处理)
 * 3:发送响应(将处理结果回馈给浏览器)
 * 断开连接
 */
public class ClientHandler implements Runnable{
    private Socket socket;

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

    @Override
    public void run() {
        try {
//            1:解析请求(将客户端发送过来的请求内容读取到)
            HttpServletRequest request = new HttpServletRequest(socket);
            HttpServletResponse response = new HttpServletResponse(socket);

//            2:处理请求(根据请求内容进行对应的处理)
            DispatcherServlet.getInstance().service(request,response);

//            3:发送响应(将处理结果回馈给浏览器)
            response.response();


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


    public static void main(String[] args) throws URISyntaxException {
        File rootDir = new File(
                ClientHandler.class.getClassLoader()
                        .getResource(".").toURI()
        );
        //定位static目录(static目录下存放的是所有静态资源)
        File staticDir = new File(rootDir,"static");
        //定位static目录下的index.html
        File file = new File(staticDir,"index.html");
        System.out.println("文件是否存在:"+file.exists());
        System.out.println(rootDir);
    }

}

解决空请求问题:浏览器有时会发送空请求

Http协议注明:为了保证服务器端的健壮性,应当忽略客户端的请求

产生原因:

浏览器有时会发送空请求,即:与服务器连接后没有发送标准的HTTP请求内容,直接与服务器断开连接,此时服务器按照一问一答的处理流程在解析请求时,请求行由于没有内容,在拆分后获取信息时,会出现数组下标越界。

解决方法:

当解析请求行时发现没有内容就对外抛出空请求异常(自定义一个异常),并最终抛给ClientHandler,使其忽略后续的请求处理与发送响应的工作,直接与客户端断开 忽略本次操作。

实现:

1:在com.webserver.http包下新建自定义异常:EmptyRequestException,空请求异常

2:在HTTPServletResponse的解析请求行方法parseRequestLine中,当读取请求是一个空字符        串时,则对外抛出空请求异常并通过构造方法对外抛出给ClientHandler

3:ClientHandler添加一个新的catch专门捕获请求异常,捕获后无需做处理,目的仅是为了断        开连接

包结构:

EmptyRequestException类:新建自定义异常,空请求异常

package com.webserver.http;

/**EmptyRequestException
 * 空请求异常
 * 当浏览器连接服务器后没有发送请求内容时,HTTPServletRequest在解析过程是会抛出请求异常
 */
public class EmptyRequestException extends Exception {
    public EmptyRequestException() {
    }

    public EmptyRequestException(String message) {
        super(message);
    }

    public EmptyRequestException(String message, Throwable cause) {
        super(message, cause);
    }

    public EmptyRequestException(Throwable cause) {
        super(cause);
    }

    public EmptyRequestException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

HTTPServletResponse类:解析请求行方法parseRequestLine中,当读取请求是一个空字符串时,则对外抛出空请求异常并通过构造方法对外抛出给ClientHandler 。

package com.webserver.http;

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

/**HTTPServletRequest
 * 请求对象,该类的每一个实例用于标识HTTP协议规定的请求内容
 * 一个请求由三部分构成:
 * 1:请求行
 * 2:消息头
 * 3:消息正文
 */
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, EmptyRequestException {
        this.socket = socket;
        //1解析请求行
        parseRequestLine();
        //2解析消息头
        parseHeaders();
        //3解析消息正文
        parseContent();

    }

    //解析请求行
    private void parseRequestLine() throws IOException, EmptyRequestException {
        String line = readLine();
        if(line.isEmpty()){//如果请求行为空字符串,则本次为空请求
            throw new EmptyRequestException();
        }
        System.out.println("请求行内容:"+line);
        String[] data = line.split("\\s");
        method = data[0];
        uri = data[1];
        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()){//如果读取的消息头是空字符串,说明单独读取到了CRLF
                break;
            }
            System.out.println("消息头:"+line);
            //将消息头按照": "拆分为名字和值并作为key,value存入到headers中
            String[] data = line.split(":\\s");
            headers.put(data[0],data[1]);
        }
        System.out.println("headers:"+headers);
    }
    //解析消息正文
    private void parseContent(){}

    private String readLine() throws IOException {
        //调用同一个socket对象若干次getInputStream()方法返回的始终是同一条输入流
        InputStream in = socket.getInputStream();
        StringBuilder builder = new StringBuilder();
        char pre='a',cur='a';//pre表示上次读取的字符,cur表示本次读取的字符
        int d;
        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;
    }

    /**
     * 根据消息头的名字获取对应的值
     * @param name
     * @return
     */
    public String getHeader(String name) {
        return headers.get(name);
    }
}

ClientHandler类:添加一个新的catch专门捕获请求异常,不做处理目的仅是为了断开连接 。

package com.webserver.core;

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

import java.io.*;
import java.net.Socket;
import java.net.URISyntaxException;


/**ClientHandler
 * 该线程任务负责处理与某个客户端的一次HTTP交互。
 * 由于HTTP协议要求客户端和服务端采取一问一答的原则,因此这里处理与客户端的一次交互规划
 * 为三个步骤:
 * 1:解析请求(将客户端发送过来的请求内容读取到)
 * 2:处理请求(根据请求内容进行对应的处理)
 * 3:发送响应(将处理结果回馈给浏览器)
 * 断开连接
 */
public class ClientHandler implements Runnable{
    private Socket socket;

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

    @Override
    public void run() {
        try {
//            1:解析请求(将客户端发送过来的请求内容读取到)
            HttpServletRequest request = new HttpServletRequest(socket);
            HttpServletResponse response = new HttpServletResponse(socket);

//            2:处理请求(根据请求内容进行对应的处理)
            DispatcherServlet.getInstance().service(request,response);

//            3:发送响应(将处理结果回馈给浏览器)
            response.response();


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

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




    public static void main(String[] args) throws URISyntaxException {
        File rootDir = new File(
                ClientHandler.class.getClassLoader()
                        .getResource(".").toURI()
        );
        //定位static目录(static目录下存放的是所有静态资源)
        File staticDir = new File(rootDir,"static");
        //定位static目录下的index.html
        File file = new File(staticDir,"index.html");
        System.out.println("文件是否存在:"+file.exists());
        System.out.println(rootDir);
    }

}

八改造:解决响应头问题,可根据设置进行发送

导入某商城webapp资源后访问其首页,发现页面无法正常显示.
浏览器F12跟踪请求和响应的交互发现两个问题:
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存放要发送的响应头即可

HTTPServletResponse类:根据设置自动发送响应头,添加addHeader方法。

package com.webserver.http;

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

/**HttpServletResponse
 * 响应对象
 * 该类的每一个实例用于表示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;
    }

    /**
     * 该方法会将当前响应对象内容以标准的HTTP响应格式通过socket获取的输出流发送给
     * 对应的客户端。
     */
    public void response() throws IOException {
        //发送状态行
        sendStatusLine();
        //发送响应头
        sendHeaders();
        //发送响应正文
        sendContent();
    }
    //发送状态行
    private void sendStatusLine() throws IOException {
        println("HTTP/1.1"+" "+statusCode+" "+statusReason);
    }
    //发送响应头
    private void sendHeaders() throws IOException {
        /*
            headers
            key                 value
            Content-Type        text/html
            Content-Length      245
            Server              BirdWebServer
            ...                 ...
         */
        //遍历headers发送每一个响应头
        Set<Map.Entry<String,String>> entrySet = headers.entrySet();
        for(Map.Entry<String,String> e : entrySet){
            String key = e.getKey();
            String value = e.getValue();
            println(key+": "+value);
        }
        //单独发送回车+换行表达响应头发送完毕
        println("");
    }
    //发送响应正文
    private void sendContent() throws IOException {
        OutputStream out = socket.getOutputStream();
        try(
            FileInputStream fis = new FileInputStream(contentFile);
        ) {
            byte[] data = new byte[1024 * 10];
            int len;
            while ((len = fis.read(data)) != -1) {
                out.write(data, 0, len);
            }
        }
    }


    private void println(String line) throws IOException {
        OutputStream out = socket.getOutputStream();
        out.write(line.getBytes(StandardCharsets.ISO_8859_1));
        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方法,响应消息头。

package com.webserver.core;

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

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

/**DispatcherServlet
 * 这个类是SpringMVC框架与tomcat容器整合的一个关键类,接管了处理请求的工作。
 * 这样当tomcat将请求对象和响应对象创建完毕后在处理请求的环节通过调用这个类来完成,从而
 * 将处理请求交给了SpringMVC框架
 * 并在处理后发送响应给浏览器
 */
public class DispatcherServlet {
    private static DispatcherServlet instance = new DispatcherServlet();
    private static File rootDir;
    private static File staticDir;
    static{
        try {
            //rootDir表示类加载路径:target/classes目录
            rootDir = new File(
                    DispatcherServlet.class.getClassLoader()
                            .getResource(".").toURI()
            );
            //定位static目录(static目录下存放的是所有静态资源)
            staticDir = new File(rootDir,"static");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    private DispatcherServlet(){}

    public static DispatcherServlet getInstance(){
        return instance;
    }


    public void service(HttpServletRequest request, HttpServletResponse response){
        String path = request.getUri();
        File file = new File(staticDir,path);
        if(file.isFile()){//根据用户提供的抽象路径去static目录下定位到一个文件
            response.setContentFile(file);

            response.addHeader("Content-Type","text/html");
            response.addHeader("Content-Length",file.length()+"");
            response.addHeader("Server","BirdWebServer");
        }else{
            response.setStatusCode(404);
            response.setStatusReason("NotFound");
            file = new File(staticDir,"/root/404.html");
            response.setContentFile(file);
            response.addHeader("Content-Type","text/html");
            response.addHeader("Content-Length",file.length()+"");
        }
    }
}

九改造:实现HttpServletResponse响应正确的MIME类型,即:Content-Type的值

这里我们使用java.nio.file.Files这个类来完成这个功能。

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

实现:
1:将原DispatcherServlet中设置响应头Content-Type和Content-Length的
      工作移动到HttpServletResponse的设置响应正文方法setContentFile中.


  原因:一个响应中只要包含正文就应当包含说明正文信息的两个头Content-Type和
  Content-Length。因此我们完全可以在设置正文的时候自动设置这两个头,
  这样做的好处是将来设置完正文(调用setContentFile)后无需再单独设置这两个头了.


2:使用MimetypesFileTypeMap的方法getContentType按照正文文件分析MIME类型并设置到
      响应头Content-Type上

包结构:

HTTPServletResponse类:设置响应正文方法setContentFile。

package com.webserver.http;

import javax.activation.MimetypesFileTypeMap;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**HttpServletResponse
 * 响应对象
 * 该类的每一个实例用于表示HTTP协议规定的响应。
 * 一个响应由三部分构成:状态行,响应头,响应正文。
 */
public class HttpServletResponse {
    //只需初始化一次,所以定义为静态的
    private static MimetypesFileTypeMap mime = new MimetypesFileTypeMap();

    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;
    }

    /**
     * 该方法会将当前响应对象内容以标准的HTTP响应格式通过socket获取的输出流发送给
     * 对应的客户端。
     */
    public void response() throws IOException {
        //发送状态行
        sendStatusLine();
        //发送响应头
        sendHeaders();
        //发送响应正文
        sendContent();
    }
    //发送状态行
    private void sendStatusLine() throws IOException {
        println("HTTP/1.1"+" "+statusCode+" "+statusReason);
    }
    //发送响应头
    private void sendHeaders() throws IOException {
        /*
            headers
            key                 value
            Content-Type        text/html
            Content-Length      245
            Server              BirdWebServer
            ...                 ...
         */
        //遍历headers发送每一个响应头
        Set<Map.Entry<String,String>> entrySet = headers.entrySet();
        for(Map.Entry<String,String> e : entrySet){
            String key = e.getKey();
            String value = e.getValue();
            println(key+": "+value);
        }
        //单独发送回车+换行表达响应头发送完毕
        println("");
    }
    //发送响应正文
    private void sendContent() throws IOException {
        OutputStream out = socket.getOutputStream();
        try(
            FileInputStream fis = new FileInputStream(contentFile);
        ) {
            byte[] data = new byte[1024 * 10];
            int len;
            while ((len = fis.read(data)) != -1) {
                out.write(data, 0, len);
            }
        }
    }


    private void println(String line) throws IOException {
        OutputStream out = socket.getOutputStream();
        out.write(line.getBytes(StandardCharsets.ISO_8859_1));
        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;
        /*
        使用MimetypesFileTypeMap的方法getContentType按照正文文件,分析MIME类型并设置到
        响应头Content-Type上
        */
        addHeader("Content-Type",mime.getContentType(contentFile));
        addHeader("Content-Length",contentFile.length()+"");

    }

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

}

DispatcherServlet类:调用setContentFile方法,发送正文内容 。

package com.webserver.core;

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

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

/**DispatcherServlet
 * 这个类是SpringMVC框架与tomcat容器整合的一个关键类,接管了处理请求的工作。
 * 这样当tomcat将请求对象和响应对象创建完毕后在处理请求的环节通过调用这个类来完成,从而
 * 将处理请求交给了SpringMVC框架
 * 并在处理后发送响应给浏览器
 */
public class DispatcherServlet {
    private static DispatcherServlet instance = new DispatcherServlet();
    private static File rootDir;
    private static File staticDir;
    static{
        try {
            //rootDir表示类加载路径:target/classes目录
            rootDir = new File(
                    DispatcherServlet.class.getClassLoader()
                            .getResource(".").toURI()
            );
            //定位static目录(static目录下存放的是所有静态资源)
            staticDir = new File(rootDir,"static");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    private DispatcherServlet(){}

    public static DispatcherServlet getInstance(){
        return instance;
    }


    public void service(HttpServletRequest request, HttpServletResponse response){
        //获取浏览器发送过来的请求中的路径
        String path = request.getUri();
        //找到static/path路径 的文件
        File file = new File(staticDir,path);

        if(file.isFile()){//根据用户提供的抽象路径去static目录下定位到一个文件
            
            response.setContentFile(file);


            response.addHeader("Server","BirdWebServer");
        }else{
            response.setStatusCode(404);
            response.setStatusReason("NotFound");
            file = new File(staticDir,"/root/404.html");
            response.setContentFile(file);
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值