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

写在前面的话:

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

文章目录

  • 正文开始
    • 1. 支持 POST 请求
      • 1.1 将页面中表单的提交方式改为 POST
      • 1.2 解析消息正文
    • 2. 重构代码
      • 2.1 定义抽象类 HttpServlet
      • 2.2 所有 Servlet 继承 HttpServlet
    • 3. 使服务端支持重定向
      • 3.1 为什么要用重定向
      • 3.2 重定向
  • 参考
  • 相关文章

正文开始

1. 支持 POST 请求

之前我们写的全部都是GET请求,请求的内容会直接显示在地址栏;接下来我们看一下如何支持POST请求,需要注意的是,该请求会把内容放到请求的消息正文中(这里只支持POST请求提交的表单数据)。

在实际开发中,当表单中包含用户隐私信息,或者上传附件等操作时一定要使用POST请求。

对于我们之前的注册和登录功能而言,由于表单中含有用户隐私信息,对此我们将这些form表单的提交形式改为post,而当表单提交形式变为 post 后,所有输入域的内容不会再被拼接到 URL 的 “?” 右侧。而是将原 “?” 右侧内容包含在消息正文中被提交。

1.1 将页面中表单的提交方式改为 POST

这里就不贴代码了,主要是将method="get"改为method="post",修改的文件为下面两个:

  • reg.html
  • login.html

修改完之后可以运行并试着看一下结果,以login.html为例,截取部分结果:

开始解析请求行...
请求行:POST /myweb/login HTTP/1.1
进一步解析url......
requestURI: /myweb/login
queryString: null
parameter: {}
解析url完毕!
method: POST
url: /myweb/login
protocol: HTTP/1.1
请求行解析完毕!
开始解析消息头...
Headers: {Origin=http://localhost:9999, 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, 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, Referer=http://localhost:9999/myweb/login.html, Sec-Fetch-Site=same-origin, Sec-Fetch-Dest=document, Host=localhost:9999, Accept-Encoding=gzip, deflate, br, Sec-Fetch-Mode=navigate, Cache-Control=max-age=0, Upgrade-Insecure-Requests=1, Sec-Fetch-User=?1, Accept-Language=zh-CN,zh-TW;q=0.9,zh;q=0.8,en-US;q=0.7,en;q=0.6, Content-Length=33, Content-Type=application/x-www-form-urlencoded}
消息头解析完毕!
开始解析消息正文...
消息正文解析完毕!
开始处理登录
处理登录完毕

当然是登陆失败了:
【Java】手动写一个非常简易的web server(五)_第1张图片
只看请求的内容,会发现没有?和后面的部分了,但是请求消息头多出了Content-Length=33Content-Type=application/x-www-form-urlencoded,只要头里面有这两个,说明是有消息正文的,application/x-www-form-urlencodedform表单提交出来的内容,是一个字符串,这字符串的内容就是原来 url 的?后面的内容,只不过放到了消息正文中,所以要对消息正文进行解析。

1.2 解析消息正文

终于要把之前的坑填上啦~~

这里我们先小小地调整一下代码,将 parseUrl 方法中的拆分每个参数部分分离出来,单独放在一个方法parseParameters中:

/**
 * 解析参数
 * @param line
 */
private void parseParameters(String line) {
     
    String[] data = line.split("&");
    
    for (String paraLine : data) {
     
        //按照"="将参数拆分为两部分
        String[] paraArr = paraLine.split("=");
        
        //判断该参数是否有值
        if (paraArr.length > 1) {
     
            parameter.put(paraArr[0], paraArr[1]);
        } else {
     
            parameter.put(paraArr[0], null);
        }
    }
}

当表单提交后,浏览器地址栏中不再包含?以及参数部分,这部分内容会被包含在请求的消息正文中。这时解析请求的消息头部分会发现多出两个头:

  • Content-Length=xx:告知浏览器消息正文长度
  • Content-Type=application/x-www-form-urlencoded:正文内容的类型
    application/x-www-form-urlencoded是一个固定值,是用来表示此消息正文内容是一个字符串,是原get请求?右侧的内容。

定义消息正文相关信息(HttpRequest 中):

/*
 * 消息正文相关信息定义
 */
private byte[] content;

完成 HttpRequest 类中解析消息正文的方法parseContent,解析消息正文前首先判断消息头中是否含有Content-Length,若有则说明这个请求包含消息正文:

//首先判断该请求是否含有消息正文
//判断依据是看当前请求的消息头中是否含有Content-Length
if (headers.containsKey("Content-Length")) {
     
    System.out.println("此请求包含消息正文!!!!!!");
}

获取消息头Content-Length的值,然后通过输入流读取对应长度的字节量:

//获取消息正文的长度,并实际读取对应的字节量
int len = Integer.parseInt(headers.get("Content-Length"));
byte[] data = new byte[len];
in.read(data);   //将正文内容读取出来
content = data;  //设置到消息正文对应属性上

再获取消息头Content-Type,判断此正文类型,这里只判断是否为页面 form 表单提交上来的用户输入的数据application/x-www-form-urlencoded,若是,则将读取的正文字节转换为一组字符串,并进行解析参数操作:

//通过消息头获取该消息正文的类型
String type = headers.get("Content-Type");

//根据类型判定当前消息正文内容
//判断是否为表单提交的数据
if ("application/x-www-form-urlencoded".equals(type)) {
     
    //该正文是一行字符串
    String line = new String(content, "ISO8859-1");
    System.out.println("消息正文:" + line);
}

放上这部分完整代码:

/**
 * 解析消息正文
 * @throws IOException 
 */
private void parseContent() throws IOException {
     
    System.out.println("开始解析消息正文...");
    
    //首先判断该请求是否含有消息正文
    //判断依据是看当前请求的消息头中是否含有Content-Length
    if (headers.containsKey("Content-Length")) {
     
        System.out.println("此请求包含消息正文!!!!!!");
        
        //获取消息正文的长度,并实际读取对应的字节量
        int len = Integer.parseInt(headers.get("Content-Length"));
        byte[] data = new byte[len];
        in.read(data);   //将正文内容读取出来
        content = data;  //设置到消息正文对应属性上
        
        //通过消息头获取该消息正文的类型
        String type = headers.get("Content-Type");
        
        //根据类型判定当前消息正文内容
        //判断是否为表单提交的数据
        if ("application/x-www-form-urlencoded".equals(type)) {
     
            //该正文是一行字符串
            String line = new String(content, "ISO8859-1");
            System.out.println("消息正文:" + line);
            line = URLDecoder.decode(line, "UTF-8");  //解码(处理中文)
            
            //进一步解析参数
            parseParameters(line);
        }
    }
    
    System.out.println("消息正文解析完毕!");
}

修改后的 HttpRequest:

package com.webserver.http;

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

/**
 * 请求对象
 * 每个实例表示客户端发送过来的一个具体请求
 * @author returnzc
 *
 */
public class HttpRequest {
     
    /*
     * 请求行相关信息定义
     */
    private String method;    //请求方式
    private String url;       //资源路径
    private String protocol;  //协议版本
    
    private String requestURI;
    private String queryString;
    private Map<String, String> parameter = new HashMap<String, String>();
    
    /*
     * 消息头相关信息定义
     */
    private Map<String, String> headers = new HashMap<String, String>();
    
    /*
     * 消息正文相关信息定义
     */
    private byte[] content;
    
    /*
     * 客户端连接相关信息
     */
    private Socket socket;
    private InputStream in;
    
    public HttpRequest(Socket socket) throws EmptyRequestException {
     
        try {
     
            this.socket = socket;
            this.in = socket.getInputStream();
            
            /*
             * 解析请求的过程:
             * 1. 解析请求行
             * 2. 解析消息头
             * 3. 解析消息正文
             */
            parseRequestLine();
            parseHeaders();
            parseContent();
        } catch (IOException e) {
     
            e.printStackTrace();
        }
    }
    
    /**
     * 解析请求行
     * @throws EmptyRequestException 
     */
    private void parseRequestLine() throws EmptyRequestException {
     
        System.out.println("开始解析请求行...");
        try {
     
            String line = readLine();
            System.out.println("请求行:" + line);
            //若请求行内容是一个空串,则是空请求
            if ("".equals(line)) {
     
                throw new EmptyRequestException();
            }
            
            //将请求行进行拆分,将每部分内容对应的设置到属性上
            String[] data = line.split("\\s");
            this.method = data[0];
            this.url = data[1];
            this.protocol = data[2];
            
            parseUrl();  //进一步解析url
            
            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 parseUrl() {
     
        System.out.println("进一步解析url......");
        
        //判断请求路径中是否含有"?"
        if (url.indexOf("?") != -1) {
     
            //按照"?"将url拆分为两部分
            String[] data = url.split("\\?");
            requestURI = data[0];
            
            //看url的"?"后面是否有内容
            if (data.length > 1) {
     
                queryString = data[1];
                
                try {
     
                    System.out.println("解码前queryString:" + queryString);
                    queryString = URLDecoder.decode(queryString, "UTF-8");
                    System.out.println("解码后queryString:" + queryString);
                } catch (UnsupportedEncodingException e) {
     
                    e.printStackTrace();
                }
                
                //拆分每个参数
                parseParameters(queryString);
            }
        } else {
     
            requestURI = url;
        }
        
        System.out.println("requestURI: " + requestURI);
        System.out.println("queryString: " + queryString);
        System.out.println("parameter: " + parameter);
        
        System.out.println("解析url完毕!");
    }
    
    /**
     * 解析参数
     * @param line
     */
    private void parseParameters(String line) {
     
        String[] data = line.split("&");
        
        for (String paraLine : data) {
     
            //按照"="将参数拆分为两部分
            String[] paraArr = paraLine.split("=");
            
            //判断该参数是否有值
            if (paraArr.length > 1) {
     
                parameter.put(paraArr[0], paraArr[1]);
            } else {
     
                parameter.put(paraArr[0], null);
            }
        }
    }
    
    /**
     * 解析消息头
     */
    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("消息头解析完毕!");
    }
    
    /**
     * 解析消息正文
     * @throws IOException 
     */
    private void parseContent() throws IOException {
     
        System.out.println("开始解析消息正文...");
        
        //首先判断该请求是否含有消息正文
        //判断依据是看当前请求的消息头中是否含有Content-Length
        if (headers.containsKey("Content-Length")) {
     
            System.out.println("此请求包含消息正文!!!!!!");
            
            //获取消息正文的长度,并实际读取对应的字节量
            int len = Integer.parseInt(headers.get("Content-Length"));
            byte[] data = new byte[len];
            in.read(data);   //将正文内容读取出来
            content = data;  //设置到消息正文对应属性上
            
            //通过消息头获取该消息正文的类型
            String type = headers.get("Content-Type");
            
            //根据类型判定当前消息正文内容
            //判断是否为表单提交的数据
            if ("application/x-www-form-urlencoded".equals(type)) {
     
                //该正文是一行字符串
                String line = new String(content, "ISO8859-1");
                System.out.println("消息正文:" + line);
                line = URLDecoder.decode(line, "UTF-8");  //解码(处理中文)
                
                //进一步解析参数
                parseParameters(line);
            }
        }
        
        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);
    }

    public String getRequestURI() {
     
        return requestURI;
    }

    public String getQueryString() {
     
        return queryString;
    }

    /**
     * 根据给定的参数名获取对应的参数值
     * @param name
     * @return
     */
    public String getParameter(String name) {
     
        return parameter.get(name);
    }
}

运行服务端,尝试登陆,截取部分结果:

开始解析请求行...
请求行:POST /myweb/login HTTP/1.1
进一步解析url......
requestURI: /myweb/login
queryString: null
parameter: {}
解析url完毕!
method: POST
url: /myweb/login
protocol: HTTP/1.1
请求行解析完毕!
开始解析消息头...
Headers: {Origin=http://localhost:9999, 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, 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, Referer=http://localhost:9999/myweb/login.html, Sec-Fetch-Site=same-origin, Sec-Fetch-Dest=document, Host=localhost:9999, Accept-Encoding=gzip, deflate, br, Sec-Fetch-Mode=navigate, Cache-Control=max-age=0, Upgrade-Insecure-Requests=1, Sec-Fetch-User=?1, Accept-Language=zh-CN,zh-TW;q=0.9,zh;q=0.8,en-US;q=0.7,en;q=0.6, Content-Length=33, Content-Type=application/x-www-form-urlencoded}
消息头解析完毕!
开始解析消息正文...
此请求包含消息正文!!!!!!
消息正文:username=zhangsan&password=123456
消息正文解析完毕!
开始处理登录
处理登录完毕

成功登陆咯:
【Java】手动写一个非常简易的web server(五)_第2张图片

2. 重构代码

目前我们有两个servlet,分别是RegServletLoginServlet,这两个servlet的共同点在于都是用来处理业务的,而且都要求有一个service方法,将来写其他的业务的时候也一定要有service方法,所以可以定义一个接口,接口里要求必须得有抽象方法service,这样所有的servlet在实现该接口的时候就必须要有service方法。

接口和抽象类的区别:

接口只能有抽象方法,抽象类还可以有实现方法。
如果要求子类必须得有相同方法,方法名一样,但是代码可能不一样,那就可以定义成抽象方法来规定子类必须得有这个行为;如果要有共同的行为,代码都一样的,可以定义成一个实现方法。如果有共同的实现方法,可以定义成抽象类,如果没有,可以定义成接口。

我们先来简单重构一下代码。由于所有的Servlet都要有处理业务操作的方法service,因此我们定义一个抽象类HttpServlet,并定义此抽象方法service,这样将来所有的Servlet都继承该类时就一定会重写此方法了。

2.1 定义抽象类 HttpServlet

servlets包中定义抽象类HttpServlet
【Java】手动写一个非常简易的web server(五)_第3张图片
HttpServlet 如下:

package com.webserver.servlets;

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

/**
 * 所有Servlet的超类
 * @author returnzc
 *
 */
public abstract class HttpServlet {
     
    public abstract void service(HttpRequest request, HttpResponse response);
}

2.2 所有 Servlet 继承 HttpServlet

修改所有Servlet,让他们继承HttpServlet

public class LoginServlet extends HttpServlet {
     ...}
public class RegServlet extends HttpServlet {
     ...}

3. 使服务端支持重定向

响应客户端时,我们通常有两种模式:

  1. 内部跳转
  2. 重定向

3.1 为什么要用重定向

例如,当用户提交表单请求注册操作,服务端在处理完注册业务后直接响应客户端注册结果的页面,这种响应方式就是内部跳转。

从业务逻辑直接跳转到结果页面,对于浏览器而言,它当前地址栏的路径是提交注册请求,而实际看到的是注册结果页面。这样有一个弊端,就是当用户点击刷新按钮再次发起请求时,会将表单再次提交,重新走一遍注册业务的逻辑,这会给服务端带来无谓的资源开销。

简单来讲,我们启动服务端,访问地址http://localhost:9999/myweb/reg.html,在输入信息并提交之后,会跳转到http://localhost:9999/myweb/reg(post 模式,get 模式为http://localhost:9999/myweb/reg?username=xxx&password=xxx...),并显示“注册成功”,如果这时刷新此页面,会提示“此用户已存在”,说明表单又重新被提交了一次。而我们应该在提交了注册以后,跳转到http://localhost:9999/myweb/reg_success.html或者http://localhost:9999/myweb/reg_fail.html

这时建议采取使用重定向方式响应客户端,即:当用户提交表单后,服务端处理完毕并响应给客户端一个路径,当客户端接收到该路径后继续按照该路径再次发起请求而得到注册结果页面。这时浏览器上地址栏应当是专门请求注册结果页面的路径了,这样无论怎么刷新,都不会再经过注册业务。

3.2 重定向

响应客户端时:

  • 重定向的方式需要响应状态代码为302
  • 在响应头中指定Location,对应的值就是希望客户端再次发起请求时的路径。
  • 该响应可以不包含响应正文。因为目的是让它根据一个地址进行访问,而不是直接把页面给过去,只是给了一个路径,再去发起一个请求,所以不需要正文。

HttpResponse中添加重定向方法sendRedirect,该方法需要设置重定向对应的状态代码,并设置响应头以及重定向的位置:

/**
 * 要求客户端重定向到指定路径
 * @param url
 */
public void sendRedirect(String url) {
     
    //设置对应的状态代码
    setStatusCode(302);
    //设置对应的响应头
    headers.put("Location", url);
}

接下来需要将原来的跳转修改为sendRedirect

以登陆为例,需要注意,重定向中指定的相对路径是针对浏览器的。而浏览器之前的请求路径是请求当前Servlet时的路径为http://localhost:8088/myweb/login,所以当前目录是http://localhost:8088/myweb/,对此,我们指定重定向路径如login_success.html,浏览器在接收到后,就会自动请求http://localhost:8088/myweb/login_success.html

LoginServlet:

//原来的跳转
if (check) {
     
    //跳转登录成功页面
    response.setEntity(new File("webapps/myweb/login_success.html"));
} else {
     
    //跳转登录失败页面
    response.setEntity(new File("webapps/myweb/login_fail.html"));
}

//修改后的跳转
if (check) {
     
    //跳转登录成功页面
    response.sendRedirect("login_success.html");
} else {
     
    //跳转登录失败页面
    response.sendRedirect("login_fail.html");
}

RegServlet:

//原来的跳转
if (check) {
     
    response.setEntity(new File("webapps/myweb/reg_fail.html"));
} else {
     
    ...
    //响应客户端注册成功的页面
    response.setEntity(new File("webapps/myweb/reg_success.html"));
}

//修改后的跳转
if (check) {
     
    response.sendRedirect("reg_fail.html");
} else {
     
    ...
    //响应客户端注册成功的页面
    response.sendRedirect("reg_success.html");
}

不要急

在写了在写了~~

【Java】手动写一个非常简易的web server(五)_第4张图片

参考

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

相关文章

等全部写完以后添加

你可能感兴趣的:(Java,java,html,webserver,post)