【Java】手动写一个非常简易的 web server(二)

写在前面的话:

  1. 版权声明:本文为博主原创文章,转载请注明出处!
  2. 博主是一个小菜鸟,并且非常玻璃心!如果文中有什么问题,请友好地指出来,博主查证后会进行更正,啾咪~~
  3. 每篇文章都是博主现阶段的理解,如果理解的更深入的话,博主会不定时更新文章。
  4. 本文初次更新时间:2020.12.25,最后更新时间:2021.01.01

正文开始

上篇【Java】手动写一个非常简易的web server(一)我们写到解析消息头。
在这里插入图片描述
本来吧,接下来该写解析消息正文,但是我偏不,就要先写处理请求。
在这里插入图片描述

1. 处理请求

1.1 webapps目录

上篇文章我们讲到,URL统一资源定位,我们在浏览器中输入的网址就是一个URL,而URL分为三部分:

protocol://host/abs_path
即:
应用协议://服务器地址信息/请求资源的抽象路径

http://www.baidu.com/index.html为例:
在这里插入图片描述
当用户在浏览器访问一个页面时,就比如上面的http://www.baidu.com/index.html,在解析请求中我们可以得到抽象路径部分为/index.html,可以看到,其实抽象路径就相当于相对路径,我们要在服务端定义该路径相对的位置,以便找到该资源。

我们需要在当前的项目目录下新建一个目录:webapps,用这个目录保存每一个在服务器上运行的web应用。简单来说,一个web应用就是一个网站的一切内容,包含素材(网页,图片,样式等)和业务逻辑。

web apps 就是web的应用,一个网站就是web应用,一台服务器上不止可以部署一个网站,如果做了好几个网站,可以都部署在同一个服务器上;如果申请了好几个域名,在同一台服务器上,每个域名都可以映射成不同的路径。不同路径就是一套网站资源,webapps里可以放好几套网站,例如:

www.xxxbbs.com ------ 192.168.1.1:9999/bbs/xxx
www.xxxxx.com  ------ 192.168.1.1:9999/xxx/xxxxxx

通过【右击项目名】->【New】->【Folder】,新建webapps目录:
在这里插入图片描述
在以后,我们可以在webapps下面,用一个子目录保存一个web应用,像tomcat,里面也是用webapps保存不同的网站资源,用子目录划分不同的网站。

接下来,我们先在webapps下创建一个名为myweb的目录,用作我们测试使用,然后在webapps/myweb目录下新建第一个页面:index.html,目录结构是这样子滴【webapps -> myweb -> index.html】:
在这里插入图片描述
这时,当用户在浏览器输入http://localhost:9999/myweb/index.html,我们得到抽象路径/myweb/index.html后,就从webapps目录作为当前目录,在当前目录下根据相对目录查找,就可以找到对应资源了。变相理解为http://localhost:9999访问服务器就是访问webapps目录内容,所有请求的后续资源都从webapps目录下开始找起。

简单来说就是把给用户看的页面统一放到一个目录webapps里,所有的资源都从这个目录里开始找起。

1.2 响应

ClientHandler中,当通过请求中的资源路径到webapps目录下找到对应资源后,就会发送一个响应内容回复客户端。

什么是响应:

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

一个响应包含三部分内容:

  • 状态行
  • 响应头
  • 响应正文
1.2.1 状态行

状态行由一行字符串构成,CRLF结尾。

格式:

protocol statusCode statusReason(CRLF)
即:
协议版本 状态代码 状态描述(CRLF)

例如:

HTTP/1.1 200 OK(CRLF)

状态代码:

状态代码是一个3位数字,客户端通过这个代码判断自己的请求是否被服务器处理,以及处理结果如何。

状态代码分为5类:

  • 1xx:保留
  • 2xx:成功,表示服务端成功处理了该请求
  • 3xx:重定向,服务端希望客户端继续后续操作
  • 4xx:客户端错误,客户端的请求有误
  • 5xx:服务端错误,服务端处理请求时出现了错误

状态代码描述:

代码描述
200OK
201Created
202Accepted
204No Content
301Moved Permanently
302Moved Temporarily
304Not Modified
400Bad Request
401Unauthorized
403Forbidden
404Not Found 请求无效
500Internal Server Error
501Not Implemented
502Bad Gateway
503Service Unavailable
1.2.2 响应头

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

1.2.3 响应正文

响应正文是二进制数据,是服务端实际响应给客户端请求的结果。

一个响应中也可以不包含响应正文,客户端也是通过响应头中是否包含Content-TypeContent-Length来读取的。

一个响应内容大致如下:

HTTP/1.1 200 OK(CRLF)
Content-Type: text/html(CRLF)
Content-Length: 125(CRLF)(CRLF)
1101010100101010100101011......

1.3 写一个基本页面

HTML基础内容就不写咯,直接放index.html的内容吧,随便写一个就好。

myweb目录下放一个logo.png,酱紫滴:
在这里插入图片描述

index.html

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>我的Google</title>
</head>
<body>
    <div align="center">
        <img src="logo.png"><br>
        <input type="text">
        <input type="button" value="搜一下"><br>
    </div>
</body>
</html>

最后页面长这样:
在这里插入图片描述

1.4 判断请求资源是否存在

上篇我们用HttpRequest request = new HttpRequest(socket);解析请求,并且可以通过getUrl()直接获取到url,然后我们可以通过获取到的url,查看请求的资源是否存在。

步骤如下:

  1. 通过request获取请求的资源路径
  2. 从webapps目录中根据资源路径位置寻找请求的资源是否存在

没错是这样子的:

//处理请求
String url = request.getUrl();
File file = new File("webapps" + url);
if (file.exists()) {
    System.out.println("该资源已找到!");
} else {
    System.out.println("该资源不存在!");
}

此时的 ClientHandler 代码:

package com.webserver.core;

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

import com.webserver.http.HttpRequest;

/**
 * 处理客户端请求
 * @author returnzc
 *
 */
public class ClientHandler implements Runnable {
    private Socket socket;
    
    public ClientHandler(Socket socket) {
        this.socket = socket;
    }
    
    public void run() {
        try {
            //解析请求
            HttpRequest request = new HttpRequest(socket);
            
            //处理请求
            String url = request.getUrl();
            File file = new File("webapps" + url);
            if (file.exists()) {
                System.out.println("该资源已找到!");
            } else {
                System.out.println("该资源不存在!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果(地址为http://localhost:9999/myweb/index.html):

等待客户端连接...
一个客户端连接了。
开始解析请求行...
请求行:GET /myweb/index.html HTTP/1.1
method: GET
url: /myweb/index.html
protocol: HTTP/1.1
请求行解析完毕!
开始解析消息头...
Headers: {Accept=text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9, Upgrade-Insecure-Requests=1, Connection=keep-alive, User-Agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36, Sec-Fetch-Site=none, Sec-Fetch-Dest=document, Host=localhost:9999, Sec-Fetch-User=?1, Accept-Encoding=gzip, deflate, br, Accept-Language=zh-CN,zh-TW;q=0.9,zh;q=0.8,en-US;q=0.7,en;q=0.6, Sec-Fetch-Mode=navigate}
消息头解析完毕!
开始解析消息正文...
消息正文解析完毕!
该资源已找到!

可以看到 “该资源已找到!”,如果没有找到,可以看一下是不是地址输错了哟。

1.5 将资源响应给客户端

上节我们已经判断了客户端所请求的资源是否存在,如果存在的话,我们便可以将该资源响应给客户端,这时便需要向客户端发送一个标准的响应内容。当然,向客户端发送数据也需要一个输出流:

OutputStream out = socket.getOutputStream();

发送一个标准的响应内容:

  1. 发送状态行
  2. 发送响应头
  3. 发送响应正文
1.5.1 发送状态行

前面提到状态行的格式为:

protocol statusCode statusReason(CRLF)
协议版本 状态代码 状态描述(CRLF)

例如:

HTTP/1.1 200 OK(CRLF)

状态代码200表示成功,表示服务端成功处理了该请求。

这里我们先设定好一个状态行HTTP/1.1 200 OK,未来还会进行修改,以返回不同的状态码。前面我们说到HTML采用ISO8859-1编码,所以在发送字符串时也需要指定编码方式。由于状态行以CRLF结尾,所以字符串发送完之后,还需要发送一个CRLF以表示该行已经结束:

//发送状态行
String line = "HTTP/1.1 200 OK";
out.write(line.getBytes("ISO8859-1"));
out.write(13);    //CR
out.write(10);    //LF
1.5.2 发送响应头

前面我们提到,一个响应可以不包含响应正文,客户端通过响应头中是否包含Content-TypeContent-Length来判断是否包含响应正文,由于我们是有响应正文的,所以需要发送相关数据:

  • Content-Type:告知客户端响应正文的数据类型,例如 text/html 表示该正文是一个html页面。
  • Content-Length:告知客户端正文的长度,单位是字节。

由于响应的每行字符串都以CRLF结尾,并且响应头是有多行字符串的,所以我们需要在响应头发送完毕之后,再单独发送CRLF表示响应头发送完毕:

//发送响应头
line = "Content-Type: text/html";  //响应正文的数据类型
out.write(line.getBytes("ISO8859-1"));
out.write(13);
out.write(10);

line = "Content-Length: " + file.length();  //正文的长度,单位是字节
out.write(line.getBytes("ISO8859-1"));
out.write(13);
out.write(10);
//单独发送CRLF表示响应头发送完毕
out.write(13);
out.write(10);
1.5.3 发送响应正文

接下来就是发送响应正文了,发送正文内容就是把文件数据读完发送出去,相当于复制:

//发送正文内容
FileInputStream fis = new FileInputStream(file);
int len = -1;
byte[] data = new byte[1024*1024];
while ((len = fis.read(data)) != -1) {
        out.write(data, 0, len);
}

目前 ClientHandler 代码如下,这里会发现有大量重复的代码,没关系,之后会进行调整的,现在主要是先实现功能。需要注意的是,响应完毕后应该与客户端断开连接

package com.webserver.core;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

import com.webserver.http.HttpRequest;

/**
 * 处理客户端请求
 * @author returnzc
 *
 */
public class ClientHandler implements Runnable {
    private Socket socket;
    
    public ClientHandler(Socket socket) {
        this.socket = socket;
    }
    
    public void run() {
        try {
            //解析请求
            HttpRequest request = new HttpRequest(socket);
            
            //处理请求
            String url = request.getUrl();
            File file = new File("webapps" + url);
            if (file.exists()) {
                System.out.println("该资源已找到!");
                
                OutputStream out = socket.getOutputStream();
                
                //发送状态行
                String line = "HTTP/1.1 200 OK";
                out.write(line.getBytes("ISO8859-1"));
                out.write(13);
                out.write(10);
                
                //发送响应头
                line = "Content-Type: text/html";  //响应正文的数据类型
                out.write(line.getBytes("ISO8859-1"));
                out.write(13);
                out.write(10);
                
                line = "Content-Length: " + file.length();  //正文的长度,单位是字节
                out.write(line.getBytes("ISO8859-1"));
                out.write(13);
                out.write(10);
                //单独发送CRLF表示响应头发送完毕
                out.write(13);
                out.write(10);
                
                //发送正文内容
                FileInputStream fis = new FileInputStream(file);
                int len = -1;
                byte[] data = new byte[1024*1024];
                while ((len = fis.read(data)) != -1) {
                    out.write(data, 0, len);
                }
            } else {
                System.out.println("该资源不存在!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                //响应完毕后与客户端断开连接
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

我们来看一下测试结果,首先开启服务器,然后在浏览器中输入地址http://localhost:9999/myweb/index.html,这里我们需要通过浏览器的调试器来查看我们是否操作成功。使用方法:在浏览器中按下【F12】开启调试器,也可以在空白区域【按下右键】->【检查】,开启调试器后,选择最上面一排的【Network】,可以在【Name】看到请求资源的名字,点击需要查看的资源,查看【Headers】,具体如下:
在这里插入图片描述
可以看到:

  • Status Code: 200 OK
  • Content-Length: 291
  • Content-Type: text/html

index.html文件的长度是291字节,我们将这些内容发完了以后,便是告诉我们响应的客户端我的文件有多长,也告诉他这是什么类型的文件了,最终会把这个文件所有数据读出来给客户端传过去。

而客户端通过响应头得知了这个文件有291字节,在它把所有的响应头读完了以后,就紧接着再读一个291字节,还知道它是一个html页面,于是就把它翻译过来,就能看到页面效果了。

2. 功能拆分和重构代码

通过上面的代码会感觉ClientHandler代码非常长,实际上ClientHandler应该只处理大流程,不应该处理这些琐碎复杂的信息,所以我们进行功能拆分,将实际响应客户端的工作从ClientHandler中抽离出去,宗旨还是让ClientHandler只控制流程,具体操作分开。

我们的请求是创建了一个对象,专门用于保存请求信息,一个对象请求对象就表示客户端发过来的一个请求。所以,对于响应我们也可以以一个对象的形式表示响应。与HttpRequest的设计目的类似,我们在http包中再定义一个类HttpResponse,用这个类的每一个实例表示一个服务端具体发送给客户端的响应内容。

2.1 设计 HttpResponse 类

http包中定义一个类HttpResponse
在这里插入图片描述
请求和响应是不一样的,请求的时候所有的数据都有了,因为请求是客户端发过来的,那么在new的过程中就可以解析;而响应是处理的时候才能知道回复什么,要先new出来,把想返回的东西设置好,再交给对方。

流程框架如下:

package com.webserver.http;

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

/**
 * 响应对象
 * @author returnzc
 *
 */
public class HttpResponse {
    //和连接相关的信息
    private Socket socket;
    private OutputStream out;
    
    public HttpResponse(Socket socket) {
        try {
            this.socket = socket;
            this.out = socket.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 将当前响应对象内容按照HTTP响应格式发送给客户端
     */
    public void flush() {
        /*
         * 1. 发送状态行
         * 2. 发送响应头
         * 3. 发送响应正文
         */
        sendStatusLine();
        sendHeaders();
        sendContent();
    }
    
    /**
     * 发送状态行
     */
    public void sendStatusLine() {
        
    }
    
    /**
     * 发送响应头
     */
    public void sendHeaders() {
        
    }
    
    /**
     * 发送响应正文
     */
    public void sendContent() {
        
    }
}

2.2 完善 HttpResponse 类

2.2.1 发送一行字符串

之前我们写的发送状态行发送响应头发送响应正文中,都需要将发送的字符串转换为SO8859-1编码,并且需要发送CRLF,所以我们将这部分直接写成一个println方法,以减少代码重复:

/**
 * 向客户端发送一行字符串(发送后会自动发送CRLF结尾)
 * @param line
 */
private void println(String line) {
    try {
        out.write(line.getBytes("ISO8859-1"));
        out.write(13);    //CR
        out.write(10);    //LF
    } catch (IOException e) {
        e.printStackTrace();
    }
}
2.2.2 发送状态行
/**
 * 发送状态行
 */
public void sendStatusLine() {
    String line = "HTTP/1.1 200 OK";
    println(line);
}
2.2.3 响应的实体文件信息

由于我们在处理请求时通过File获取资源,所以需要定义一个响应的实体文件,在获取到资源时,通过set方法设置实体文件。

//响应的实体文件
private File entity;

public File getEntity() {
    return entity;
}

public void setEntity(File entity) {
    this.entity = entity;
}
2.2.4 发送响应头
/**
 * 发送响应头
 */
public void sendHeaders() {
    //响应正文的数据类型
    String line = "Content-Type: text/html";
    println(line);
    
    //正文的长度,单位是字节
    line = "Content-Length: " + entity.length();
    println(line);
    
    //单独发送CRLF表示响应头发送完毕
    println("");
}
2.2.5 发送响应正文
/**
 * 发送响应正文
 */
public void sendContent() {
    try {
        FileInputStream fis = new FileInputStream(entity);
        int len = -1;
        byte[] data = new byte[1024*1024];
        while ((len = fis.read(data)) != -1) {
            out.write(data, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

需要注意的是,文件流用完是需要关闭的,上面的代码并没有关闭文件流,所以我们需要做一些调整如下(自动关闭):

/**
 * 发送响应正文
 */
public void sendContent() {
    try (
        FileInputStream fis = new FileInputStream(entity);
    ) {
        int len = -1;
        byte[] data = new byte[1024*1024];
        while ((len = fis.read(data)) != -1) {
            out.write(data, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

如果报错,是因为需要在JDK7以上:

Resource specification not allowed here for source level below 1.7

如果编译错误,需要改编译器版本:

右键【项目名称】->【Properties】->【Java Compller】,改成1.7以上就好。
在这里插入图片描述

2.2.6 修改ClientHandler响应部分
//创建响应
HttpResponse response = new HttpResponse(socket);

//将该资源响应给客户端
response.setEntity(file);
response.flush();

目前代码(WebServer、HttpRequest 暂未修改):
ClientHandler:

package com.webserver.core;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

import com.webserver.http.HttpRequest;
import com.webserver.http.HttpResponse;

/**
 * 处理客户端请求
 * @author returnzc
 *
 */
public class ClientHandler implements Runnable {
    private Socket socket;
    
    public ClientHandler(Socket socket) {
        this.socket = socket;
    }
    
    public void run() {
        try {
            //解析请求
            HttpRequest request = new HttpRequest(socket);
            //创建响应
            HttpResponse response = new HttpResponse(socket);
            
            //处理请求
            String url = request.getUrl();
            File file = new File("webapps" + url);
            if (file.exists()) {
                System.out.println("该资源已找到!");
                //将该资源响应给客户端
                response.setEntity(file);
                response.flush();
            } else {
                System.out.println("该资源不存在!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                //响应完毕后与客户端断开连接
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

HttpResponse:

package com.webserver.http;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;

/**
 * 响应对象
 * @author returnzc
 *
 */
public class HttpResponse {
    //和连接相关的信息
    private Socket socket;
    private OutputStream out;
    
    //响应的实体文件
    private File entity;
    
    public HttpResponse(Socket socket) {
        try {
            this.socket = socket;
            this.out = socket.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 将当前响应对象内容按照HTTP响应格式发送给客户端
     */
    public void flush() {
        /*
         * 1. 发送状态行
         * 2. 发送响应头
         * 3. 发送响应正文
         */
        sendStatusLine();
        sendHeaders();
        sendContent();
    }
    
    /**
     * 发送状态行
     */
    public void sendStatusLine() {
        String line = "HTTP/1.1 200 OK";
        println(line);
    }
    
    /**
     * 发送响应头
     */
    public void sendHeaders() {
        //响应正文的数据类型
        String line = "Content-Type: text/html";
        println(line);
        
        //正文的长度,单位是字节
        line = "Content-Length: " + entity.length();
        println(line);
        
        //单独发送CRLF表示响应头发送完毕
        println("");
    }
    
    /**
     * 发送响应正文
     */
    public void sendContent() {
        try (
            FileInputStream fis = new FileInputStream(entity);
        ) {
            int len = -1;
            byte[] data = new byte[1024*1024];
            while ((len = fis.read(data)) != -1) {
                out.write(data, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 向客户端发送一行字符串(发送后会自动发送CRLF结尾)
     * @param line
     */
    private void println(String line) {
        try {
            out.write(line.getBytes("ISO8859-1"));
            out.write(13);    //CR
            out.write(10);    //LF
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public File getEntity() {
        return entity;
    }

    public void setEntity(File entity) {
        this.entity = entity;
    }
}

可以测试一下运行结果,与拆分和重构前结果是一样的哟。

3. 服务端支持响应不同类型资源

当一个页面上有一张图片的时候,就不是一次请求就能搞定的。因为一次请求之后,顶多拿到页面的源代码,然后浏览器解析源代码,想把页面给用户展示出来,但是在展示的过程中,发现有一张图片,就会立刻自动再去请求服务端要这张图片,要过来之后才能正常显示。

即当用户请求一个页面时,服务端将该页面的html代码响应回去后,浏览器解析时若发现该页面存在图片时,会再次发起连接向服务器请求该图片资源,这时服务端应当将该图片响应回去。所以当一个页面中存在其他资源时,构成这个页面会经历数次请求响应来完成,我们需要使服务端支持响应不同类型资源给客户端。

如果希望浏览器能正确理解其请求的资源,要求服务端在响应该资源时在响应头中指定的Content-Type必须准确。不同的资源的类型有不同的值,这些都是有定义的。可以参考tomcat安装目录中conf里的web.xml文件,该文件整理出了这些类型,有1000多个。

我们需要修改HttpResponse,首先发送的响应头不能是固定值,而是应该根据实际响应需求进行变化,大概流程如下:

  1. 在HttpResponse中定义响应头相关属性:Map headers,并且对外提供设置响应头和获取响应头信息的相关方法。
  2. 修改发送响应头方法,将原来固定发送两个响应头:Content-Type和Content-Length改变为遍历headers,将每个响应头进行发送。
  3. 在http包中定义一个类:HttpContext,使用这个类来定义所有有关Http协议的内容。这里先定义相关Content-Type对应值的一组信息:
    3.1 定义一个Map类型的静态属性:mimeTypeMapping,其中key保存资源的后缀名,value保存mime类型(Content-Type对应的值)。
    3.2 定义一个私有的静态方法用于初始化这个Map。
    3.3 在HttpContext的静态块中调用这个初始化方法来进行初始化操作。
    3.4 定义一个共有的静态方法:getMimeType,该方法可根据文件的后缀名获取对应的mime类型。
  4. 修改HttpResponse的setEntity方法,在该方法中除了将给定的File文件设置到对应属性entity之外,再根据该文件向响应头对应的Map属性headers中添加Content-Type与Content-Length其中Content-Type的值要根据该文件后缀调用3.4的方法获取对应的值。

3.1 循环接收请求

之前我们的页面上已经有图片logo.png了,之前我们注意到,启动服务器,访问页面,得到的页面如下(图片挂了):
在这里插入图片描述
可以看出,实际上浏览器是发送了好几次请求,其中logo.pngfavicon.icofailed

  • logo.png 是我们的图片
  • favicon.ico 实际上是浏览器的行为,自主想要一个图标,即显示在title上的图标(“我的Google” 前面那个)。
    在这里插入图片描述

将鼠标放在 logo.png 后面的 failed 上,会看到,失败的原因是连接拒绝了:

(failed) net::ERR_CONNECTION_REFUSED

这是因为现在的服务器只接收一次请求,一次请求以后就断了,所以我们需要将服务器修改成循环接收请求,增加while (true)即可:

package com.webserver.core;

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

/**
 * WebServer主类
 * @author returnzc
 *
 */
public class WebServer {
    private ServerSocket server;
    
    /**
     * 初始化服务端
     */
    public WebServer() {
        try {
            server = new ServerSocket(9999);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 服务端开始工作的方法
     */
    public void start() {
        try {
            while (true) {
                //暂时不启用接收多次客户端连接的操作
                System.out.println("等待客户端连接...");
                Socket socket = server.accept();
                System.out.println("一个客户端连接了。");
                
                //启动一个线程处理与该客户端的交互
                ClientHandler handler = new ClientHandler(socket);
                Thread thread = new Thread(handler);
                thread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        WebServer server = new WebServer();
        server.start();
    }
}

加上之后就可以了哟~虽然这个时候图片显示出来了,但是这是因为浏览器有强大的容错能力,虽然状态是200,但是类型是text/html,也就是说实际上请求的是一张图片,但是服务器告诉的类型是个页面,但是浏览器比较聪明,看见在标签上,就当成一个图片看待了,就正常显示出来了:
在这里插入图片描述
所以服务端现在有一个缺陷是,我告诉你这个类型实际上和客户端现在请求这个资源的实际类型不匹配。因为响应对象里把文件类型写死了:

/**
 * 发送响应头
 */
public void sendHeaders() {
    //响应正文的数据类型
    String line = "Content-Type: text/html";
    println(line);
    
    //正文的长度,单位是字节
    line = "Content-Length: " + entity.length();
    println(line);
    
    //单独发送CRLF表示响应头发送完毕
    println("");
}

3.2 定义响应头相关属性

在 HttpResponse 中定义响应头相关属性Map headers

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

还需要对外提供设置响应头和获取响应头信息的相关方法,见【3.5 对外提供设置响应头和获取响应头信息的相关方法】。

3.3 修改发送响应头方法

将原来固定发送两个响应头Content-TypeContent-Length改变为遍历headers,将每个响应头进行发送:

/**
 * 发送响应头
 */
public void sendHeaders() {
    //遍历headers,将每个响应头发送给客户端
    Set<Entry<String, String>> entrySet = headers.entrySet();
    for (Entry<String, String> header : entrySet) {
        String line = header.getKey() + ": " + header.getValue();
        println(line);
    }
    
    //单独发送CRLF表示响应头发送完毕
    println("");
}

3.4 定义 HttpContext 类

在http包中定义一个类HttpContext,使用这个类来定义所有有关HTTP协议的内容:
在这里插入图片描述

先定义相关Content-Type对应值的一组信息:

  1. 定义一个Map类型的静态属性:mimeTypeMapping,其中key保存资源的后缀名,value保存mime类型(Content-Type对应的值)。
//介质类型映射
private static Map<String, String> mimeTypeMapping = new HashMap<String, String>();
  1. 定义一个私有的静态方法用于初始化这个Map。
/**
 * 初始化Mime类型映射
 */
private static void initMimeTypeMapping() {
    mimeTypeMapping.put("html", "text/html");
    mimeTypeMapping.put("png", "image/png");
    mimeTypeMapping.put("jpg", "image/jpeg");
    mimeTypeMapping.put("gif", "image/gif");
    mimeTypeMapping.put("css", "image/css");
    mimeTypeMapping.put("js", "application/javascript");
}
  1. 在HttpContext的静态块中调用这个初始化方法来进行初始化操作。
/**
 * 初始化静态资源
 */
static {
    //初始化mime类型
    initMimeTypeMapping();
}
  1. 定义一个共有的静态方法:getMimeType,该方法可根据文件的后缀名获取对应的mime类型。
/**
 * 根据资源后缀获取对应的mime类型
 * @param ext
 * @return
 */
public static String getMimeType(String ext) {
    return mimeTypeMapping.get(ext);
}

可以写一个main方法用于测试:

public static void main(String[] args) {
    String fileName = "header.css";
    
    //根据文件名的后缀,获取对应的mime类型
    String ext = fileName.substring(fileName.lastIndexOf(".") + 1);
    System.out.println(getMimeType(ext));
    
    String type = getMimeType("png");
    System.out.println(type);
}

最终代码:

package com.webserver.http;

import java.util.HashMap;
import java.util.Map;

/**
 * HTTP协议相关信息定义
 * @author returnzc
 *
 */
public class HttpContext {
    //介质类型映射
    private static Map<String, String> mimeTypeMapping = new HashMap<String, String>();
    
    /**
     * 初始化静态资源
     */
    static {
        //初始化mime类型
        initMimeTypeMapping();
    }
    
    /**
     * 初始化Mime类型映射
     */
    private static void initMimeTypeMapping() {
        mimeTypeMapping.put("html", "text/html");
        mimeTypeMapping.put("png", "image/png");
        mimeTypeMapping.put("jpg", "image/jpeg");
        mimeTypeMapping.put("gif", "image/gif");
        mimeTypeMapping.put("css", "image/css");
        mimeTypeMapping.put("js", "application/javascript");
    }
    
    /**
     * 根据资源后缀获取对应的mime类型
     * @param ext
     * @return
     */
    public static String getMimeType(String ext) {
        return mimeTypeMapping.get(ext);
    }
    
    public static void main(String[] args) {
        String fileName = "header.css";
        
        //根据文件名的后缀,获取对应的mime类型
        String ext = fileName.substring(fileName.lastIndexOf(".") + 1);
        System.out.println(getMimeType(ext));
        
        String type = getMimeType("png");
        System.out.println(type);
    }
}

运行结果:

image/css
image/png

证明我们写的是正确的哦。

3.5 对外提供设置响应头和获取响应头信息的相关方法

这里我们需要对外提供设置响应头和获取响应头信息的相关方法。由于只要发送响应,并且含有响应正文的时候就会有这两个头Content-TypeContent-Length,所以我们可以在setEntity(file)中加上这两个头,设置这个头的目的就是将来要把这个响应正文发出去,而setEntity(file)就是设置正文的文件,只要设置就说明肯定是有正文的,可以在设置的同时把两个头追加进去。

即修改HttpResponsesetEntity方法,在该方法中除了将给定的File文件设置到对应属性entity之外,再根据该文件向响应头对应的Map属性headers中添加Content-TypeContent-Length,其中Content-Type的值要根据该文件后缀调用getMimeType方法获取对应的值。

之前是这样的:

//HttpResponse中
public void setEntity(File entity) {
    this.entity = entity;
}

//ClientHandler中
response.setEntity(file);

修改后:

/**
 * 设置响应正文的实体文件
 * 在设置该文件的同时,自动设置对应该正文内容的两个响应头:
 * Content-Type、Content-Length
 * @param entity
 */
public void setEntity(File entity) {
    String fileName = entity.getName();
    
    //根据文件名的后缀,获取对应的mime类型
    String ext = fileName.substring(fileName.lastIndexOf(".") + 1);
    String type = HttpContext.getMimeType(ext);
    
    headers.put("Content-Type", type);
    headers.put("Content-Length", entity.length() + "");
    
    this.entity = entity;
}

重构一下代码:

/**
 * 设置响应正文的实体文件
 * 在设置该文件的同时,自动设置对应该正文内容的两个响应头:
 * Content-Type、Content-Length
 * @param entity
 */
public void setEntity(File entity) {
    this.entity = entity;
    putHeaderByEntity();
}

/**
 * 根据响应正文的实体文件设置对应的响应头:
 * Content-Type和Content-Length
 */
private void putHeaderByEntity() {
    String fileName = entity.getName();
    
    //根据文件名的后缀,获取对应的mime类型
    String ext = fileName.substring(fileName.lastIndexOf(".") + 1);
    String type = HttpContext.getMimeType(ext);
    
    headers.put("Content-Type", type);
    headers.put("Content-Length", entity.length() + "");
}

运行一下代码,发现功能正常哦:
在这里插入图片描述

目前代码:

WebServer

package com.webserver.core;

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

/**
 * WebServer主类
 * @author returnzc
 *
 */
public class WebServer {
    private ServerSocket server;
    
    /**
     * 初始化服务端
     */
    public WebServer() {
        try {
            server = new ServerSocket(9999);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 服务端开始工作的方法
     */
    public void start() {
        try {
            while (true) {
                //暂时不启用接收多次客户端连接的操作
                System.out.println("等待客户端连接...");
                Socket socket = server.accept();
                System.out.println("一个客户端连接了。");
                
                //启动一个线程处理与该客户端的交互
                ClientHandler handler = new ClientHandler(socket);
                Thread thread = new Thread(handler);
                thread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        WebServer server = new WebServer();
        server.start();
    }
}

ClientHandler

package com.webserver.core;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

import com.webserver.http.HttpRequest;
import com.webserver.http.HttpResponse;

/**
 * 处理客户端请求
 * @author returnzc
 *
 */
public class ClientHandler implements Runnable {
    private Socket socket;
    
    public ClientHandler(Socket socket) {
        this.socket = socket;
    }
    
    public void run() {
        try {
            //解析请求
            HttpRequest request = new HttpRequest(socket);
            //创建响应
            HttpResponse response = new HttpResponse(socket);
            
            //处理请求
            String url = request.getUrl();
            File file = new File("webapps" + url);
            if (file.exists()) {
                System.out.println("该资源已找到!");
                //将该资源响应给客户端
                response.setEntity(file);
                response.flush();
            } else {
                System.out.println("该资源不存在!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                //响应完毕后与客户端断开连接
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

HttpRequest

package com.webserver.http;

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

/**
 * 请求对象
 * 每个实例表示客户端发送过来的一个具体请求
 * @author returnzc
 *
 */
public class HttpRequest {
    /*
     * 请求行相关信息定义
     */
    private String method;    //请求方式
    private String url;       //资源路径
    private String protocol;  //协议版本
    
    /*
     * 消息头相关信息定义
     */
    private Map<String, String> headers = new HashMap<String, String>();
    
    /*
     * 消息正文相关信息定义
     */
    
    /*
     * 客户端连接相关信息
     */
    private Socket socket;
    private InputStream in;
    
    public HttpRequest(Socket socket) {
        try {
            this.socket = socket;
            this.in = socket.getInputStream();
            
            /*
             * 解析请求的过程:
             * 1. 解析请求行
             * 2. 解析消息头
             * 3. 解析消息正文
             */
            parseRequestLine();
            parseHeaders();
            parseContent();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 解析请求行
     */
    private void parseRequestLine() {
        System.out.println("开始解析请求行...");
        try {
            String line = readLine();
            System.out.println("请求行:" + line);
            
            //将请求行进行拆分,将每部分内容对应的设置到属性上
            String[] data = line.split("\\s");
            this.method = data[0];
            this.url = data[1];
            this.protocol = data[2];
            
            System.out.println("method: " + method);
            System.out.println("url: " + url);
            System.out.println("protocol: " + protocol);
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("请求行解析完毕!");
    }
    
    /**
     * 解析消息头
     */
    private void parseHeaders() {
        System.out.println("开始解析消息头...");
        try {
            while (true) {
                String line = readLine();
                if ("".equals(line)) {
                    break;
                }
                
                String[] data = line.split(":\\s");
                headers.put(data[0], data[1]);
            }
            System.out.println("Headers: " + headers);
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("消息头解析完毕!");
    }
    
    /**
     * 解析消息正文
     */
    private void parseContent() {
        System.out.println("开始解析消息正文...");
        System.out.println("消息正文解析完毕!");
    }
    
    /**
     * 读取一行字符串,返回的字符串不含最后的CRLF
     * @param in
     * @return
     * @throws IOException 
     */
    public String readLine() throws IOException {
        StringBuilder builder = new StringBuilder();
        
        int d = -1;      //本次读取到的字节
        char c1 = 'a';   //上次读取的字符
        char c2 = 'a';   //本次读取的字符
        while ((d = in.read()) != -1) {
            c2 = (char)d;
            if (c1 == 13 && c2 == 10) {
                break;
            }
            builder.append(c2);  //本次的拼接到字符串里
            c1 = c2;             //本次的给上次
        }
        
        return builder.toString().trim();
    }

    public String getMethod() {
        return method;
    }

    public String getUrl() {
        return url;
    }

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

HttpResponse

package com.webserver.http;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * 响应对象
 * @author returnzc
 *
 */
public class HttpResponse {
    //和连接相关的信息
    private Socket socket;
    private OutputStream out;
    
    //响应的实体文件
    private File entity;
    
    /*
     * 响应头相关信息
     */
    private Map<String, String> headers = new HashMap<String, String>();
    
    public HttpResponse(Socket socket) {
        try {
            this.socket = socket;
            this.out = socket.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 将当前响应对象内容按照HTTP响应格式发送给客户端
     */
    public void flush() {
        /*
         * 1. 发送状态行
         * 2. 发送响应头
         * 3. 发送响应正文
         */
        sendStatusLine();
        sendHeaders();
        sendContent();
    }
    
    /**
     * 发送状态行
     */
    public void sendStatusLine() {
        String line = "HTTP/1.1 200 OK";
        println(line);
    }
    
    /**
     * 发送响应头
     */
    public void sendHeaders() {
        //遍历headers,将每个响应头发送给客户端
        Set<Entry<String, String>> entrySet = headers.entrySet();
        for (Entry<String, String> header : entrySet) {
            String line = header.getKey() + ": " + header.getValue();
            println(line);
        }
        
        //单独发送CRLF表示响应头发送完毕
        println("");
    }
    
    /**
     * 发送响应正文
     */
    public void sendContent() {
        try (
            FileInputStream fis = new FileInputStream(entity);
        ) {
            int len = -1;
            byte[] data = new byte[1024*1024];
            while ((len = fis.read(data)) != -1) {
                out.write(data, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 向客户端发送一行字符串(发送后会自动发送CRLF结尾)
     * @param line
     */
    private void println(String line) {
        try {
            out.write(line.getBytes("ISO8859-1"));
            out.write(13);    //CR
            out.write(10);    //LF
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public File getEntity() {
        return entity;
    }

    /**
     * 设置响应正文的实体文件
     * 在设置该文件的同时,自动设置对应该正文内容的两个响应头:
     * Content-Type、Content-Length
     * @param entity
     */
    public void setEntity(File entity) {
        this.entity = entity;
        putHeaderByEntity();
    }
    
    /**
     * 根据响应正文的实体文件设置对应的响应头:
     * Content-Type和Content-Length
     */
    private void putHeaderByEntity() {
        String fileName = entity.getName();
        
        //根据文件名的后缀,获取对应的mime类型
        String ext = fileName.substring(fileName.lastIndexOf(".") + 1);
        String type = HttpContext.getMimeType(ext);
        
        headers.put("Content-Type", type);
        headers.put("Content-Length", entity.length() + "");
    }
}

HttpContext

package com.webserver.http;

import java.util.HashMap;
import java.util.Map;

/**
 * HTTP协议相关信息定义
 * @author returnzc
 *
 */
public class HttpContext {
    //介质类型映射
    private static Map<String, String> mimeTypeMapping = new HashMap<String, String>();
    
    /**
     * 初始化静态资源
     */
    static {
        //初始化mime类型
        initMimeTypeMapping();
    }
    
    /**
     * 初始化Mime类型映射
     */
    private static void initMimeTypeMapping() {
        mimeTypeMapping.put("html", "text/html");
        mimeTypeMapping.put("png", "image/png");
        mimeTypeMapping.put("jpg", "image/jpeg");
        mimeTypeMapping.put("gif", "image/gif");
        mimeTypeMapping.put("css", "image/css");
        mimeTypeMapping.put("js", "application/javascript");
    }
    
    /**
     * 根据资源后缀获取对应的mime类型
     * @param ext
     * @return
     */
    public static String getMimeType(String ext) {
        return mimeTypeMapping.get(ext);
    }
    
    public static void main(String[] args) {
        String fileName = "header.css";
        
        //根据文件名的后缀,获取对应的mime类型
        String ext = fileName.substring(fileName.lastIndexOf(".") + 1);
        System.out.println(getMimeType(ext));
        
        String type = getMimeType("png");
        System.out.println(type);
    }
}

参考

《Java核心技术》(原书第10版)

相关文章

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值