JSD-2204-WebServer(项目)-log4j-Day15

1.V17(版本)

实现发表文章的功能

流程:
1:用户在首页点击发表文章的超链接来到发表文章页面
2:在页面输入文章标题和文章内容并点击发表按钮
3:服务端将该文章保存后响应发表结果页面(成功或失败)

实现:
1:在static/myweb下新建对应的页面
  1.1:writeArticle.html 发表文章页面
      页面form表单action指定的值"/myweb/writeArticle"
  1.2:article_success.html 发表成功提示页面
  1.3:article_fail.html 发表失败提示页面
2:在com.webserver.controller包下新建处理文章相关的业务类:ArticleController
  并定义处理发表文章的方法:writeArticle()
3:在com.webserver.entity下新建表示文章的对象:Article并实现序列化接口
4:在writeArticle方法中将表单提交上来的标题和文章内容以Article对象形式序列化到目录articles
  下文件名格式:标题.obj
  保存后响应发表成功。如果标题或内容没有输入则响应发表失败页面。

1.1项目目录

1.2Java文件

1.2.1ArticleController

package com.webserver.controller;

import com.webserver.core.ClientHandler;
import com.webserver.entity.Article;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.URISyntaxException;

public class ArticleController {
    private static File rootDir;
    private static File staticDir;

    //表示articles目录
    private static File articleDir;
    static{
        try {
            rootDir = new File(
                    ClientHandler.class.getClassLoader()
                            .getResource(".").toURI()
            );
            staticDir = new File(rootDir,"static");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }

        articleDir = new File("articles");
        if(!articleDir.exists()){
            articleDir.mkdirs();
        }
    }


    public void writeArticle(HttpServletRequest request, HttpServletResponse response){
        //1获取表单数据
        String title = request.getParameter("title");
        String author = request.getParameter("author");
        String content = request.getParameter("content");
        if(title==null||author==null||content==null){
            File file = new File(staticDir,"/myweb/article_info_error.html");
            response.setContentFile(file);
            return;
        }

        //2写入文件
        File articleFile = new File(articleDir,title+".obj");
        if(articleFile.exists()){//文件存在,说明重复的标题,不能发表(需求过于严苛,后期数据库可通过ID避免该问题).
            File file = new File(staticDir,"/myweb/have_article.html");
            response.setContentFile(file);
            return;
        }

        Article article = new Article(title,author,content);
        try (
                FileOutputStream fos = new FileOutputStream(articleFile);
                ObjectOutputStream oos = new ObjectOutputStream(fos);
        ){
            oos.writeObject(article);
            File file = new File(staticDir,"/myweb/article_success.html");
            response.setContentFile(file);
        } catch (IOException e) {
            e.printStackTrace();
        }


        //3响应页面



    }
}

1.2.2DispatcherServlet

package com.webserver.core;

import com.webserver.controller.ArticleController;
import com.webserver.controller.UserController;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;

/**
 * 负责处理请求环节
 */
public class DispatcherServlet {
    private static File rootDir;
    private static File staticDir;

    static{
        try {
            rootDir = new File(
                    ClientHandler.class.getClassLoader()
                            .getResource(".").toURI()
            );
            staticDir = new File(rootDir,"static");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    public void service(HttpServletRequest request, HttpServletResponse response){
        //不能再直接使用uri了,因为这里可能含有参数,而参数值是用户输入的,不是固定内容
//        String path = request.getUri();
        String path = request.getRequestURI();

        //根据请求路径判断是否为处理某个业务
        if("/myweb/reg".equals(path)){
            UserController controller = new UserController();
            controller.reg(request, response);

        }else if("/myweb/login".equals(path)){
            UserController controller = new UserController();
            controller.login(request, response);

        }else if("/myweb/writeArticle".equals(path)){
            ArticleController controller = new ArticleController();
            controller.writeArticle(request, response);

        }else {
            File file = new File(staticDir, path);
            System.out.println("资源是否存在:" + file.exists());
            if (file.isFile()) {
                //将请求的实际文件设置到response的正文上等待响应
                response.setContentFile(file);
            } else {
                file = new File(staticDir, "/root/404.html");
                //将404内容设置到response上等待响应
                response.setStatusCode(404);
                response.setStatusReason("NotFound");
                response.setContentFile(file);
            }
        }
    }
}

1.2.3Article

package com.webserver.entity;

import java.io.Serializable;

public class Article implements Serializable {
    public static final long serialVersionUID = 1L;

    private String title;
    private String author;
    private String content;

    public Article(String title, String author, String content) {
        this.title = title;
        this.author = author;
        this.content = content;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

1.3HTML文件

1.3.1article_info_error.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>失败</title>
</head>
<body>
  <center>
    <h1>文章输入信息有误,请<a href="/myweb/writeArticle.html">重新发表</a></h1>
  </center>
</body>
</html>

1.3.2article_success.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>成功</title>
</head>
<body>
  <center>
    <h1>发表成功!</h1>
  </center>
</body>
</html>

1.3.3have_article.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>失败</title>
</head>
<body>
  <center>
    <h1>该文章已经存在了,请<a href="/myweb/writeArticle.html">重新发表</a></h1>
  </center>
</body>
</html>

1.3.4writeArticle.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>发表文章</title>
</head>
<body>
    <center>
        <h1>发表文章</h1>
        <form action="/myweb/writeArticle" method="get">
            <table border="1">
                <tr>
                    <td>标题</td>
                    <td><input name="title" type="text"></td>
                </tr>
                <tr>
                    <td>作者</td>
                    <td><input name="author" type="text"></td>
                </tr>
                <tr>
                    <td>内容</td>
                    <td><textarea cols="50" rows="10" name="content"></textarea></td>
                </tr>
                <tr>
                    <td colspan="2" align="center"><input type="submit" value="发表"></td>
                </tr>
            </table>
        </form>
    </center>
</body>
</html>

2.V18(版本)

支持POST请求
当页面form表单中包含用户隐私信息或有附件上传时,应当使用POST形式提交。
POST会将表单数据包含在请求的消息正文中。
如果表单中没有附件,则正文中包含的表单数据就是一个字符串,而格式就是原GET形式
提交时抽象路径中"?"右侧的内容。

实现:
1:完成HttpServletRequest中的解析消息正文的方法
  当页面(reg.html或login.html)中form的提交方式改为POST时,表单数据被包含在正文里,并且
  请求的消息头中会出现Content-Type和Content-Length用于告知服务端正文内容。
  因此我们可以根据它们来解析正文。

2.1项目目录

2.2Java文件

2.2.1HttpServletRequest

package com.webserver.http;

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

/**
 * 请求对象
 * 该类的每一个实例用于表示客户端发送过来的一个HTTP请求
 * 每个请求由三部分构成:
 * 请求行,消息头,消息正文
 */
public class HttpServletRequest {
    private Socket socket;

    //请求行的相关信息
    private String method;//请求方式
    private String uri;//抽象路径
    private String protocol;//协议版本

    private String requestURI;//uri中的请求部分,"?"左侧的内容
    private String queryString;//uri中的参数部分,"?"右侧的内容
    private Map<String,String> parameters = new HashMap<>();//每一组参数 key:参数名 value:参数值


    //用一个Map保存所有的消息头,其中key:消息头名字,value:消息头的值
    private Map<String,String> headers = new HashMap<>();

    public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
        this.socket = socket;
        //1.1解析请求行
        parseRequestLine();
        //1.2解析消息头
        parseHeaders();
        //1.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];
        parseURI();//进一步解析uri
        //测试路径:http://localhost:8088/myweb/index.html
        System.out.println("method:"+method);//method:GET
        System.out.println("uri:"+uri);//uri:/myweb/index.html
        System.out.println("protocol:"+protocol);//protocol:HTTP/1.1
    }
    //进一步解析URI
    private void parseURI(){
        /*
            uri有两种情况:
            1:不含有参数的
              例如: /myweb/index.html
              直接将uri的值赋值给requestURI即可.

            2:含有参数的
              例如:/myweb/reg?username=fancq&password=123456&nickname=chuanqi&age=22
              将uri中"?"左侧的请求部分赋值给requestURI
              将uri中"?"右侧的参数部分赋值给queryString
              将参数部分首先按照"&"拆分出每一组参数,再将每一组参数按照"="拆分为参数名与参数值
              并将参数名作为key,参数值作为value存入到parameters中。
         */
        String[] data = uri.split("\\?");
        requestURI = data[0];
        if(data.length>1){
            queryString = data[1];
            parseParameters(queryString);
        }
        System.out.println("requestURI:"+requestURI);
        System.out.println("queryString:"+queryString);
        System.out.println("parameters:"+parameters);
    }
    //解析参数
    private void parseParameters(String line){
        //首先按照"&"拆分出每一组参数
        String[] data = line.split("&");
        //遍历拆分后的数组取出每一组参数
        for(String para : data){//username=fancq
            //将每一组参数按照"="拆分为参数名和参数值
            String[] paraArray = para.split("=");
            //将参数名和参数值分别以key,value形式存入parameters(Map中)
            parameters.put(paraArray[0], paraArray.length>1?paraArray[1]:null);
        }
    }

    //解析消息头
    private void parseHeaders() throws IOException {
        while(true) {
            String line = readLine();
            if(line.isEmpty()){//读取单独读取了CRLF则停止循环
                break;
            }
            System.out.println(line);
            String[] data = line.split(":\\s");
            headers.put(data[0].toLowerCase(),data[1]);

        }
        System.out.println(headers);
    }
    //解析消息正文
    private void parseContent() throws IOException {
        /*
            1:如何确定含有正文?
             post请求才含有正文
             判断请求方式是否为post请求

            2:如果存在正文,则要先将正文读取回来。读取多少个字节呢?
              通过消息头:Content-Length得知正文长度以便读取

            3:如何判定该正文的所有字节表示什么内容呢?
              通过消息头:Content-Type得知

              这里我们仅处理一种:form表单提交的用户输入信息
              Content-Type: application/x-www-form-urlencoded
              如果是上面的类型,则正文是一个字符串,内容就是原GET请求提交时
              在抽象路径中"?"右侧的内容。
              格式如:
              username=xxxxx&password=xxxx&....
              因此当我们将正文转换为字符串后,要拆分参数来初始化:parameters

              实际这里将来要写分支判断不同的类型,来进行不同的处理。
         */
        if("post".equalsIgnoreCase(method)){
            //获取Content-Length
            String contentLength = getHeader("content-length");
            if(contentLength!=null){//确保存在正文长度
                //将长度转换为int值
                int length = Integer.parseInt(contentLength);
                System.out.println("正文长度:"+length);

                //根据正文长度将正文内容读取回来
                InputStream in = socket.getInputStream();
                byte[] data = new byte[length];
                in.read(data);

                //根据Content-Type的类型来对正文数据进行解析
                String contentType = getHeader("content-type");
                //将来根据不同的值对正文进行不同的解析操作,这里仅处理form提交的数据
                if("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)){
                    String line = new String(data, StandardCharsets.ISO_8859_1);
                    System.out.println("正文内容:"+line);
                    parseParameters(line);
                }
                //这里后期继续else if分支判断其他类型...

            }
        }

    }

    /**
     * 将读取一行字符串的操作定义成方法进行重用
     * 通常复用代码的方法中如果需要处理异常时,通常都是直接抛出给调用者处理
     * @return
     */
    private String readLine() throws IOException {
        /*
            socket对象无论调用多少次getInputStream和getOutputStream
            获取回来的始终是同一条输入或输出流
         */
        InputStream in = socket.getInputStream();
        int d ;
        char cur='a',pre='a';//cur表示本次读取的字符,pre表示上次读取的字符
        StringBuilder builder = new StringBuilder();
        while((d = in.read()) != -1){
            cur = (char)d;//本次读取到的字符
            if(pre==13&&cur==10){//是否连续读取了回车+换行
                break;
            }
            builder.append(cur);
            pre = cur;//在进行下一轮读取前,要将本次读取的字符记作"上次读取的字符"
        }
        return builder.toString().trim();//获取拼接好的一行字符串内容
    }

    public String getMethod() {
        return method;
    }

    public String getUri() {
        return uri;
    }

    public String getProtocol() {
        return protocol;
    }

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

    public String getRequestURI() {
        return requestURI;
    }

    public String getQueryString() {
        return queryString;
    }

    /**
     * 根据参数名获取对应的参数值
     * @param name
     * @return
     */
    public String getParameter(String name) {
        //paratemers key:参数名(页面表单中输入框的名字)  value:参数值(输入框上用户输入的信息)
        return parameters.get(name);
    }
}

2.3HTML文件

2.3.1login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>用户登录</title>
</head>
<body>
    <center>
        <h1>用户登录</h1>
        <form action="/myweb/login" method="post">
            <table border="1">
                <tr>
                    <td>用户名</td>
                    <td><input name="username" type="text"></td>
                </tr>
                <tr>
                    <td>密码</td>
                    <td><input name="password" type="password"></td>
                </tr>
                <tr>
                    <td colspan="2" align="center"><input type="submit" value="登录"></td>
                </tr>
            </table>
        </form>
    </center>
</body>
</html>

3.V19(版本) 

解决传递中文问题

当表单传递中文时,我们在请求中看到的中文部分如下
/myweb/reg?username=%E8%8C%83%E4%BC%A0%E5%A5%87&password=123456&nickname=%E4%BC%A0%E5%A5%87&age=22

为什么是这个样子?
原因:
当我们使用GET请求提交表单时,表单数据会被拼接到URL中的抽象路径中。
而抽象路径会包含在请求的请求行中。
然而HTTP协议要求:请求的请求行和消息头都是文本,且字符集只能是ISO8859-1
该字符集不支持中文字符!!!

如何解决呢?
中心思想:用可用的字符来表达不可用的字符。

在ISO8859-1中支持的字符有哪些?英文,数字,符号
既然有数字,就有0,1这两个字符。那我们可以用这两个字符表达2进制信息。

因此:
将汉字:"范"---UTF-8--->11101000 10001100 10000011
在UTF-8编码中,每个中文占3个字节。

当我们希望传递中文时,就可以用如下方式:
/myweb/reg?username=111010001000110010000011&password=xxxxx&...

问题解决了, 但是随之而来的新问题:
/myweb/reg?username=111010001000110010000011&password=xxxxx&...
/myweb/reg?username=范&password=xxxxx&...
对比可知,为了满足要求,我们用24个'1'和'0'来表达一个汉字。带来的问题是内容太长。
数据量越大,传送速度越慢,因此要尽量缩短长度。


解决办法:用16进制来表达2进制,可以有效缩短长度。
2进制           10进制          16进制
0000            0               0
0001            1               1
0010            2               2
0011            3               3
0100            4               4
0101            5               5
0110            6               6
0111            7               7
1000            8               8
1001            9               9
1010            10              A
1011            11              B
1100            12              C
1101            13              D
1110            14              E
1111            15              F


原来2进制传递"范"时:11101000 10001100 10000011
/myweb/reg?username=111010001000110010000011&password=xxxxx&...

改为16进制传递时:
/myweb/reg?username=E88C83&password=xxxxx&...
长度缩短了,新问题:这个英文数字组合出来的表达16进制内容如何与实际的英文数字区分开?
比如:该用户注册,他写的时候就叫:E88C83
此时服务端如何区分这个内容时16进制信息,还是就叫"E88C83"?
为了解决此问题:URL地址要求:如果英文数字组合是表达16进制信息时,那么每两位16进制前
必须跟一个"%"。(2位16进制表达1字节)

因此:
/myweb/reg?username=E88C83&password=xxxxx&...
上述得到的用户名,此人就叫:E88C83(纯粹的英文和数字的组合)

/myweb/reg?username=%E8%8C%83&password=xxxxx&...
上述得到的用户名,名字中E8 8C 83要当作16进制看到,而还原为对应的文字"范"
实际上上述内容服务端要做如下操作:
1:将内容按照%拆分:E8 8C 83
2:在将16进制转换为2进制:111010001000110010000011
3:在将2进制按照UTF-8编码即可还原为:"范"

3.1项目目录

3.2Java文件

3.2.1HttpServletRequst

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

/**
 * 请求对象
 * 该类的每一个实例用于表示客户端发送过来的一个HTTP请求
 * 每个请求由三部分构成:
 * 请求行,消息头,消息正文
 */
public class HttpServletRequest {
    private Socket socket;

    //请求行的相关信息
    private String method;//请求方式
    private String uri;//抽象路径
    private String protocol;//协议版本

    private String requestURI;//uri中的请求部分,"?"左侧的内容
    private String queryString;//uri中的参数部分,"?"右侧的内容
    private Map<String,String> parameters = new HashMap<>();//每一组参数 key:参数名 value:参数值


    //用一个Map保存所有的消息头,其中key:消息头名字,value:消息头的值
    private Map<String,String> headers = new HashMap<>();

    public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
        this.socket = socket;
        //1.1解析请求行
        parseRequestLine();
        //1.2解析消息头
        parseHeaders();
        //1.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];
        parseURI();//进一步解析uri
        //测试路径:http://localhost:8088/myweb/index.html
        System.out.println("method:"+method);//method:GET
        System.out.println("uri:"+uri);//uri:/myweb/index.html
        System.out.println("protocol:"+protocol);//protocol:HTTP/1.1
    }
    //进一步解析URI
    private void parseURI(){
        /*
            uri有两种情况:
            1:不含有参数的
              例如: /myweb/index.html
              直接将uri的值赋值给requestURI即可.

            2:含有参数的
              例如:/myweb/reg?username=fancq&password=123456&nickname=chuanqi&age=22
              将uri中"?"左侧的请求部分赋值给requestURI
              将uri中"?"右侧的参数部分赋值给queryString
              将参数部分首先按照"&"拆分出每一组参数,再将每一组参数按照"="拆分为参数名与参数值
              并将参数名作为key,参数值作为value存入到parameters中。
         */
        String[] data = uri.split("\\?");
        requestURI = data[0];
        if(data.length>1){
            queryString = data[1];
            parseParameters(queryString);
        }
        System.out.println("requestURI:"+requestURI);
        System.out.println("queryString:"+queryString);
        System.out.println("parameters:"+parameters);
    }
    //解析参数V19改动
    private void parseParameters(String line){
        //将参数部分中的%XX内容转换为对应字符
        try {
            line = URLDecoder.decode(line,"UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        //首先按照"&"拆分出每一组参数
        String[] data = line.split("&");
        //遍历拆分后的数组取出每一组参数
        for(String para : data){//username=fancq
            //将每一组参数按照"="拆分为参数名和参数值
            String[] paraArray = para.split("=");
            //将参数名和参数值分别以key,value形式存入parameters(Map中)
            parameters.put(paraArray[0], paraArray.length>1?paraArray[1]:null);
        }
    }

    //解析消息头
    private void parseHeaders() throws IOException {
        while(true) {
            String line = readLine();
            if(line.isEmpty()){//读取单独读取了CRLF则停止循环
                break;
            }
            System.out.println(line);
            String[] data = line.split(":\\s");
            headers.put(data[0].toLowerCase(),data[1]);

        }
        System.out.println(headers);
    }
    //解析消息正文
    private void parseContent() throws IOException {
        /*
            1:如何确定含有正文?
             post请求才含有正文
             判断请求方式是否为post请求

            2:如果存在正文,则要先将正文读取回来。读取多少个字节呢?
              通过消息头:Content-Length得知正文长度以便读取

            3:如何判定该正文的所有字节表示什么内容呢?
              通过消息头:Content-Type得知

              这里我们仅处理一种:form表单提交的用户输入信息
              Content-Type: application/x-www-form-urlencoded
              如果是上面的类型,则正文是一个字符串,内容就是原GET请求提交时
              在抽象路径中"?"右侧的内容。
              格式如:
              username=xxxxx&password=xxxx&....
              因此当我们将正文转换为字符串后,要拆分参数来初始化:parameters

              实际这里将来要写分支判断不同的类型,来进行不同的处理。
         */
        if("post".equalsIgnoreCase(method)){
            //获取Content-Length
            String contentLength = getHeader("content-length");
            if(contentLength!=null){//确保存在正文长度
                //将长度转换为int值
                int length = Integer.parseInt(contentLength);
                System.out.println("正文长度:"+length);

                //根据正文长度将正文内容读取回来
                InputStream in = socket.getInputStream();
                byte[] data = new byte[length];
                in.read(data);

                //根据Content-Type的类型来对正文数据进行解析
                String contentType = getHeader("content-type");
                //将来根据不同的值对正文进行不同的解析操作,这里仅处理form提交的数据
                if("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)){
                    String line = new String(data, StandardCharsets.ISO_8859_1);
                    System.out.println("正文内容:"+line);
                    parseParameters(line);
                }
                //这里后期继续else if分支判断其他类型...

            }
        }

    }

    /**
     * 将读取一行字符串的操作定义成方法进行重用
     * 通常复用代码的方法中如果需要处理异常时,通常都是直接抛出给调用者处理
     * @return
     */
    private String readLine() throws IOException {
        /*
            socket对象无论调用多少次getInputStream和getOutputStream
            获取回来的始终是同一条输入或输出流
         */
        InputStream in = socket.getInputStream();
        int d ;
        char cur='a',pre='a';//cur表示本次读取的字符,pre表示上次读取的字符
        StringBuilder builder = new StringBuilder();
        while((d = in.read()) != -1){
            cur = (char)d;//本次读取到的字符
            if(pre==13&&cur==10){//是否连续读取了回车+换行
                break;
            }
            builder.append(cur);
            pre = cur;//在进行下一轮读取前,要将本次读取的字符记作"上次读取的字符"
        }
        return builder.toString().trim();//获取拼接好的一行字符串内容
    }

    public String getMethod() {
        return method;
    }

    public String getUri() {
        return uri;
    }

    public String getProtocol() {
        return protocol;
    }

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

    public String getRequestURI() {
        return requestURI;
    }

    public String getQueryString() {
        return queryString;
    }

    /**
     * 根据参数名获取对应的参数值
     * @param name
     * @return
     */
    public String getParameter(String name) {
        //paratemers key:参数名(页面表单中输入框的名字)  value:参数值(输入框上用户输入的信息)
        return parameters.get(name);
    }
}

3.3HTML文件

没有改动

4.V20(版本)

实现重定向

当浏览器提交一个请求时,比如注册,那么请求会带着表单信息请求注册功能。而注册功能处理
完毕后直接设置一个页面给浏览器,这个过程是内部跳转。
即:浏览器上的地址栏中地址显示的是提交表单的请求,而实际看到的是注册结果的提示页面。
这有一个问题,如果此时浏览器刷新,会重复上一次的请求,即:再次提交表单请求注册业务。

为了解决这个问题,我们可以使用重定向。
重定向是当我们处理完请求后,不直接响应一个页面,而是给浏览器回复一个路径,让其再次根据
该路径发起请求。这样一来,无论用户如何刷新,请求的都是该路径。避免表达的重复提交。

实现:
在HttpServletResponse中定义一个方法:sendRedirect()
该方法中设置状态代码为302,并在响应头中包含Location指定需要浏览器重新发起请求的路径
将原来Controller中内部跳转页面的操作全部改为重定向。

重定向的响应格式:
HTTP/1.1 302 Moved Temporarily
Location: /myweb/have_user.html

4.1项目目录

4.2Java文件

4.2.1UserController

package com.webserver.controller;

import com.webserver.core.ClientHandler;
import com.webserver.entity.User;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;

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

/**
 * 处理与用户相关的业务操作
 */
public class UserController {
    private static File rootDir;
    private static File staticDir;

    //表示users目录
    private static File userDir;
    static{
        try {
            rootDir = new File(
                    ClientHandler.class.getClassLoader()
                            .getResource(".").toURI()
            );
            staticDir = new File(rootDir,"static");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }

        userDir = new File("users");
        if(!userDir.exists()){
           userDir.mkdirs();
        }
    }


    public void login(HttpServletRequest request, HttpServletResponse response){
        //1获取用户输入的登录信息
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        if(username==null||password==null){
            File file = new File(staticDir,"/myweb/login_info_error.html");
            response.setContentFile(file);
            return;
        }

        //2
        File userFile = new File(userDir,username+".obj");
        if(userFile.exists()){//用户名输入正确
            try (
                    FileInputStream fis = new FileInputStream(userFile);
                    ObjectInputStream ois = new ObjectInputStream(fis);
            ){
                //读取该注册用户信息
                User user = (User)ois.readObject();
                if(user.getPassword().equals(password)){//密码正确
                    //登录成功
                    File file = new File(staticDir,"/myweb/login_success.html");
                    response.setContentFile(file);
                    return;
                }
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }

        //如果程序走到这里,情况1:用户名没有输入正确,文件不存在
        //              情况2:用户名对了,但是密码不对
        File file = new File(staticDir,"/myweb/login_fail.html");
        response.setContentFile(file);



    }

    public void reg(HttpServletRequest request, HttpServletResponse response){
        /*
            1:获取用户在注册页面上输入的注册信息
            2:将注册信息保存起来
            3:给用户回馈一个注册结果页面
         */
        //1
        //调用getParameter时传入的参数应当与页面上表单中输入框的名字一致(输入框name属性的值)
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        String nickname = request.getParameter("nickname");
        String ageStr = request.getParameter("age");
        /*
            添加一个判断,要求:如果上述四个信息有null值或者年龄不是数字
            立刻给用户响应一个错误提示页面:reg_info_error.html
            该页面居中显示一行字:注册信息输入有误,请重新注册。

            正则表达式:[0-9]+
         */
        if(username==null||password==null||nickname==null||ageStr==null||
           !ageStr.matches("[0-9]+")){
            /*
               http://localhost:8088/myweb/reg?username=fancq&password=123456&nickname=fcq&age=22

               给浏览器发送的响应内容:
               HTTP/1.1 302 Moved Temporarily(CRLF)
               Location: /myweb/reg_info_error.html(CRLF)(CRLF)

               浏览器接收到该响应后,根据状态代码302得知服务器希望他自动再发起一次请求
               来请求指定的位置
               指定的位置是哪里?浏览器根据响应头Location得知位置
               此时得到的Location位置为:/myweb/reg_info_error.html

               由于浏览器之前的请求(发起注册):
               http://localhost:8088/myweb/reg?username=fancq&password=123456&nickname=fcq&age=22
               因此浏览器理解本次重新请求的路径就是:
               http://localhost:8088/myweb/reg_info_error.html
             */
            response.sendRedirect("/myweb/reg_info_error.html");
            return;
        }

        int age = Integer.parseInt(ageStr);
        System.out.println(username+","+password+","+nickname+","+age);

        //2
        User user = new User(username,password,nickname,age);
        File userFile = new File(userDir,username+".obj");
        /*
            判断重名,如果是已注册用户,则响应页面:have_user.html告知
            该页面居中显示一行字:该用户已存在,请重新注册。
         */
        if(userFile.exists()){//该文件存在则说明是重复用户
            response.sendRedirect("/myweb/have_user.html");
            return;
        }


        try (
                FileOutputStream fos = new FileOutputStream(userFile);
                ObjectOutputStream oos = new ObjectOutputStream(fos);
        ){
            //这里序列化的是注册用户信息,因此是user对象!!!!!!!!!!!!
            oos.writeObject(user);

            //3
            response.sendRedirect("/myweb/reg_success.html");
        } catch (IOException e) {
            e.printStackTrace();
        }


    }

}

4.2.2HttpServletResponse

package com.webserver.http;

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

/**
 * 响应对象
 * 该类的每一个实例用于表示发送给客户端(浏览器)的一个HTTP响应内容
 * 每个响应由三部分构成:
 * 状态行,响应头,响应正文
 */
public class HttpServletResponse {
    private Socket socket;

    //状态行相关信息
    private int statusCode = 200;//状态代码,默认为200
    private String statusReason = "OK";//状态描述,默认为OK

    //响应头相关信息
    //key:响应头名字   value:响应头的值
    private Map<String,String> headers = new HashMap<>();


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




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

    /**
     * 将当前响应对象内容按照标准的HTTP响应格式发送给浏览器
     */
    public void response() throws IOException {
        //3.1发送状态行
        sendStatusLine();
        //3.2发送响应头
        sendHeaders();
        //3.3将文件的所有字节作为正文内容发送给浏览器
        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      2323
            ...                  ...
         */
        //遍历headers将所有的响应头发送给浏览器
        Set<Entry<String,String>> entrySet = headers.entrySet();
        for(Entry<String,String> e : entrySet){
            String key = e.getKey();
            String value = e.getValue();
            //println("Content-Type: text/html");
            println(key + ": " + value);
        }

        //单独发送回车+换行表示响应头部分发送完毕了
        println("");
    }
    //发送响应正文
    private void sendContent() throws IOException {
        if(contentFile!=null) {
            //try()意味着编译后有finally用于遍历流。所以这不违背try不能单独定义的原则
            try (
                    FileInputStream fis = new FileInputStream(contentFile);
            ) {
                OutputStream out = socket.getOutputStream();
                byte[] buf = new byte[1024 * 10];//10kb
                int len;//记录每次实际读取到的字节数
                while ((len = fis.read(buf)) != -1) {
                    out.write(buf, 0, len);
                }
            }
        }
    }




    private void println(String line) throws IOException {
        OutputStream out = socket.getOutputStream();
        byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
        out.write(data);//发送状态行内容
        out.write(13);//发送回车符
        out.write(10);//发送换行符
    }

    /**
     * 重定向到uri指定的路径V20改动
     * @param uri
     */
    public void sendRedirect(String uri){
        //1设置状态代码位302
        statusCode = 302;
        statusReason = "Moved Temporarily";

        //2添加必要的响应头Location指定浏览器再次请求的位置
        addHeader("Location",uri);
    }


    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;
         /*
                根据正文文件类型来设置Content-Type用于告知浏览器该正文类型以便其理解.
                但是在HTTP协议中规定:如果发送响应时,不包含Content-Type响应头时,则是
                让浏览器自行理解正文类型.
             */
        try {
            //该方法会自动分析文件对应的Content-Type值,若无法识别会返回null
            //http://localhost:8088/TeduStore/index.html
                /*
                    比如:file表示的是 index.html 文件
                    该方法返回值为:"text/html"

                        file表示的是 jquery.js 文件
                    该方法那会的值为:"application/javascript"

                 */
            String type = Files.probeContentType(contentFile.toPath());
            if(type!=null) {
                addHeader("Content-Type", type);
            }
        } catch (IOException e) {
        }
        addHeader("Content-Length",contentFile.length()+"");


    }

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

4.3HTML文件

没有改动

5.V21(版本)

向项目中添加Log4g日志

5.1项目目录

5.2Java文件

5.2.1UserController

package com.webserver.controller;

import com.webserver.entity.User;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;
import org.apache.log4j.Logger;
import java.io.*;

/**
 * 处理与用户相关的业务操作
 */
public class UserController {
    private static Logger logger = Logger.getLogger(UserController.class);

    //表示users目录
    private static File userDir;
    static{
        userDir = new File("users");
        if(!userDir.exists()){
           userDir.mkdirs();
        }
    }


    public void login(HttpServletRequest request, HttpServletResponse response){
        //1获取用户输入的登录信息
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        if(username==null||password==null){
            response.sendRedirect("/myweb/login_info_error.html");
            return;
        }

        //2
        File userFile = new File(userDir,username+".obj");
        if(userFile.exists()){//用户名输入正确
            try (
                    FileInputStream fis = new FileInputStream(userFile);
                    ObjectInputStream ois = new ObjectInputStream(fis);
            ){
                //读取该注册用户信息
                User user = (User)ois.readObject();
                if(user.getPassword().equals(password)){//密码正确
                    //登录成功
                    response.sendRedirect("/myweb/login_success.html");
                    return;
                }
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }

        //如果程序走到这里,情况1:用户名没有输入正确,文件不存在
        //              情况2:用户名对了,但是密码不对
        response.sendRedirect("/myweb/login_fail.html");



    }

    public void reg(HttpServletRequest request, HttpServletResponse response){
        logger.info("开始处理注册");
        /*
            1:获取用户在注册页面上输入的注册信息
            2:将注册信息保存起来
            3:给用户回馈一个注册结果页面
         */
        //1
        //调用getParameter时传入的参数应当与页面上表单中输入框的名字一致(输入框name属性的值)
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        String nickname = request.getParameter("nickname");
        String ageStr = request.getParameter("age");
        logger.info(username+","+password+","+nickname+","+ageStr);
        /*
            添加一个判断,要求:如果上述四个信息有null值或者年龄不是数字
            立刻给用户响应一个错误提示页面:reg_info_error.html
            该页面居中显示一行字:注册信息输入有误,请重新注册。

            正则表达式:[0-9]+
         */
        if(username==null||password==null||nickname==null||ageStr==null||
           !ageStr.matches("[0-9]+")){
            /*
               http://localhost:8088/myweb/reg?username=fancq&password=123456&nickname=fcq&age=22

               给浏览器发送的响应内容:
               HTTP/1.1 302 Moved Temporarily(CRLF)
               Location: /myweb/reg_info_error.html(CRLF)(CRLF)

               浏览器接收到该响应后,根据状态代码302得知服务器希望他自动再发起一次请求
               来请求指定的位置
               指定的位置是哪里?浏览器根据响应头Location得知位置
               此时得到的Location位置为:/myweb/reg_info_error.html

               由于浏览器之前的请求(发起注册):
               http://localhost:8088/myweb/reg?username=fancq&password=123456&nickname=fcq&age=22
               因此浏览器理解本次重新请求的路径就是:
               http://localhost:8088/myweb/reg_info_error.html
             */
            response.sendRedirect("/myweb/reg_info_error.html");
            return;
        }

        int age = Integer.parseInt(ageStr);
        System.out.println(username+","+password+","+nickname+","+age);

        //2
        User user = new User(username,password,nickname,age);
        File userFile = new File(userDir,username+".obj");
        /*
            判断重名,如果是已注册用户,则响应页面:have_user.html告知
            该页面居中显示一行字:该用户已存在,请重新注册。
         */
        if(userFile.exists()){//该文件存在则说明是重复用户
            response.sendRedirect("/myweb/have_user.html");
            return;
        }


        try (
                FileOutputStream fos = new FileOutputStream(userFile);
                ObjectOutputStream oos = new ObjectOutputStream(fos);
        ){
            //这里序列化的是注册用户信息,因此是user对象!!!!!!!!!!!!
            oos.writeObject(user);

            //3
            response.sendRedirect("/myweb/reg_success.html");
        } catch (IOException e) {
            e.printStackTrace();
            logger.error(e.getMessage(),e);
        }


    }

}

5.3HTML文件

没有改动

5.4pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>WebServer</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>V21</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.25</version>
        </dependency>

    </dependencies>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

</project>

5.5配置Log4j日志文件

5.5.1log4j.properties

log4j.rootLogger=INFO, console, file
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.File=logs/log.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.A3.MaxFileSize=1024KB
log4j.appender.A3.MaxBackupIndex=10
log4j.appender.file.layout.ConversionPattern=%d %p [%c] - %m%n

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值