26. 应用层HTTP原理(4) —— HTTP Server(完成一个登录页面)

Java
255
0
0
2022-11-29

使用技术

Cookie、session、线程池、工厂模式、html

实现代码

首先需要在这里创建一下文件,在index.html中写入想从网页上返回的内容,这里是需要完成一个登录页面,因此html中的内容如下:

img

<!DOCTYPE html>
<html lang="en">
<head> 
    <meta charset="UTF-8"> 
    <title>登陆页面</title>
</head>
<body>
<!-- /login服务器端还没有实现,一会儿再写  --> 
    <form method="post" action="/login"> 
        <div style="margin-bottom: 5px"> 
            <input type="text" name="username" placeholder="请输入姓名"> 
        </div> 
        <div style="margin-bottom: 5px"> 
            <input type="password" name="password" placeholder="请输入密码"> 
        </div> 
        <div style="margin-bottom: 5px"> 
            <input type="submit" value="登录"> 
        </div> 
    </form>

</body>
</html>

这里也是分为三部分来写: 第一部分:HttpRequest

package day0318;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

public class HttpRequest {
    private String method;
    private String url;
    private String version;
    private Map<String,String> headers = new HashMap<>();
    //url中的参数和body中的参数都放到parameters这个hash表中 
    private Map<String,String> parameters = new HashMap<>();
    private Map<String,String> cookies = new HashMap<>();
    private String body;

    public static HttpRequest build(InputStream inputStream) throws IOException {
        HttpRequest request = new HttpRequest();
        //此处不能把bufferedReader写道try括号中,一旦写进去就意味着bufferedReader就会被关闭,会影响到clientSocket的状态 
        //等到最后正给请求处理完了再统一关闭 
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        //此处的build过程就是解析请求的过程 
        //1.解析首行 
        String firstLine = bufferedReader.readLine();
        String[] firstLineTokens = firstLine.split(" ");
        request.method = firstLineTokens[0];
        request.url = firstLineTokens[1];
        request.version = firstLineTokens[2];

        //2.解析url 
        int pos = request.url.indexOf("?");
        if (pos != -1){
            //pos表示问号的下标 
            //看看有没有带问号,如果没有带问号就不用解析了 
            //index.html?a=10&b=20 
            String queryString = request.url.substring(pos+1);//从i+1开始取 
            //切分的最终结果
            parseKV(queryString,request.parameters);
        }

        //3.解析header 
        String line = "";
        //readLine()读到的一行内容,时会自动去掉换行符的 
        while ((line = bufferedReader.readLine()) != null && line.length() !=0){
            //还没读完并且读到的不是空字符串
            String[] headerTokens = line.split(": ");//用冒号空格来切分
            request.headers.put(headerTokens[0],headerTokens[1]);
        }

        //4.解析cookie 
        String cookie = request.headers.get("cookie");
        if (cookie != null){
            parseCookie(cookie,request.cookies);
        }

        //5.解析body 
        if ("POST".equalsIgnoreCase(request.method)
        || "PUT".equalsIgnoreCase(request.method)){
            //这两种方法需要处理body,其他暂时不考虑 
            //此处的 长度单位是字节 
            int contentLength = Integer.parseInt(request.headers.get("Content-Length"));
            //注意体会:此处的ContentLength为100,就说明body中有100个字节,下面创建的缓冲区为100个char,相当于是两百个字节 
            //缓冲区不怕长,就怕不够,这样创建的缓冲区才能保证管够 
            char[] buffer = new char[contentLength];
            int length = bufferedReader.read(buffer);
            request.body = new String(buffer,0,length);
            parseKV(request.body,request.parameters);
        }
        return request;
    }

    private static void parseCookie(String cookie, Map<String, String> cookies) {
        //1.先按照&分成若干组键值对
        String[] kvTokens = cookie.split(": ");//这里不一样 
        //2.针对切分结果再分别进行按照=切分,得到了键和值 
        for (String kv : kvTokens){
            String[] result = kv.split("=");
            cookies.put(result[0],result[1]);
        }
    }

    private static void parseKV(String input, Map<String, String> output) {
        //1.先按照&分成若干组键值对
        String[] kvTokens = input.split("&");
        //2.针对切分结果再分别进行按照=切分,得到了键和值 
        for (String kv : kvTokens){
            String[] result = kv.split("=");
            output.put(result[0],result[1]);
        }
    }

    public String getMethod() {
        return method;
    }

    public String getUrl() {
        return url;
    }

    public String getVersion() {
        return version;
    }

    public String getBody() {
        return body;
    }

    public String getParameter(String key){
        return parameters.get(key);
    }

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

    public String geCookie(String key){
        return cookies.get(key);
    }
}

第二部分:HttpResponse

package day0318;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.Map;

public class HttpResponse {
    private String version = "HTTP/1.1";
    private int status; //状态码 
    private String message; //状态码描述信息 
    private Map<String,String> headers = new HashMap<>();
    private StringBuilder body = new StringBuilder();
    //当代码需要把响应写回客户端的时候,就往这个OutputStream里面写就好了 
    private OutputStream outputStream = null;

    //工厂方法 
    public static HttpResponse build(OutputStream outputStream){
        HttpResponse response = new HttpResponse();
        response.outputStream = outputStream;
        return response;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public void setHeaders(String key, String value) {
        headers.put(key,value);
    }

    public void writeBody(String content) {
        body.append(content);
    }

    //以上的设置属性操作都是在内存中搞得,还需要一个专门的方法,把这些属性按照HTTP协议,都写到Socket中 
    public void flush() throws IOException {
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
        bufferedWriter.write(version+" "+status+ " "+message+"\n");
        headers.put("Content-Length",body.toString().getBytes().length+"");
        for (Map.Entry<String,String> entry : headers.entrySet()){
            bufferedWriter.write(entry.getKey()+": "+entry.getValue()+"\n");
        }
        bufferedWriter.write("\n");
        bufferedWriter.write(body.toString());
        bufferedWriter.flush();
    }
}

第三部分:HttpServerV3

package day0318;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 1.让服务器支持返回一个静态的html文件
 * 2.解析处理cookie,把cookie处理成键值对保存好
 * 3.解析body,把body中的数据成键值对结构
 * 4.完成一个登录功能,session的简单实现
 */
public class HttpServerV3 {
    static class User{
        //保存用户的相关信息 
        public String userName;
        public int age;
        public String school;
    }

    private ServerSocket serverSocket = null;

    //session 会话,指的是同一个用户的一组访问服务操作,归类到一起,就是一个会话 
    //每个键值对就是一个会话 
    private HashMap<String,User> sessions = new HashMap<String, User>();

    public HttpServerV3(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        ExecutorService executorService = Executors.newCachedThreadPool();

        while (true){
            //1.获取连接 
            Socket clientSocket = serverSocket.accept();
            executorService.execute(new Runnable() {
                @Override 
                public void run() {
                    //2.处理连接(采用短连接) 
                    try {
                        process(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    public void process(Socket clientSocket) throws IOException {
        try {
            //1.读取并解析请求 
            HttpRequest request = HttpRequest.build(clientSocket.getInputStream());
            HttpResponse response = HttpResponse.build(clientSocket.getOutputStream());
            response.setHeaders("Content-Type", "text.html");

            //2.根据请求计算相应 
            //此处按照不同的HTTP方法拆分成多个不同的逻辑 
            if ("GET".equalsIgnoreCase(request.getMethod())){
                doGet(request,response);
            }else if ("POST".equalsIgnoreCase(request.getMethod())){
                doPost(request,response);
            }else {
                response.setStatus(405);
                response.setMessage("Method Not Allowed");
            }

            //3.把响应写回到客户端
            response.flush();

        } catch (IOException | NullPointerException e) {
            e.printStackTrace();
        }finally {
            clientSocket.close();
        }
    }



    private void doGet(HttpRequest request, HttpResponse response) throws IOException {
        //1.能够支持返回一个HTML文件 
        if (request.getUrl().startsWith("/index.html")){
            String sessionId = request.geCookie("sessionId");
            User user = sessions.get(sessionId);
            if (sessionId == null || user == null) {
                //说明当前用户尚未登陆,就返回一个登陆页面

                //这种情况下,就让代码读取一个index.html这样的文件 
                //此时可以自己来约定路径

                //把这个文件内容写入到响应的body中
                response.setStatus(200);
                response.setMessage("OK");
                response.setHeaders("Content-Type","text/html; charset=utf-8");

                InputStream inputStream = HttpServerV3.class.getClassLoader().getResourceAsStream("index.html");
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                //HttpServerV3.class: 获取到一个类的类对象 
                //getClassLoader(): 获取到当前类的“类加载器”, 
                //一个类想要工作,需要先由JVM把这个类的.class文件找到,然后读取内容,加载到内存中(类加载过程) 
                //.getResourceAsStream("index.html"): 根据文件名,在Resources目录中找到对应的文件并打开 
                //返回这个文件的InputStream对象

                //按行读取 
                String line = null;
                while ((line = bufferedReader.readLine()) != null){
                    response.writeBody(line+"\n");
                }
                bufferedReader.close();
            }else {
                //已经登陆了,就不需要再登陆了
                response.setStatus(200);
                response.setMessage("OK");
                response.setHeaders("Content-Type","text/html; charset=utf-8");
                response.writeBody("<html>");
                response.writeBody("<div>您已经登录了无需再次登录,用户名为:"+user.userName+"</div>");
                response.writeBody("<div>"+user.age+"</div>");
                response.writeBody("<div>"+user.school+"</div>");
                response.writeBody("</html>");
            }
        }
    }

    private void doPost(HttpRequest request, HttpResponse response) {
        //2.实现一个/login的处理 
        if (request.getUrl().startsWith("/login")){
            //读取用户提交的用户名和密码 
            String userName = request.getParameter("username");
            String password = request.getParameter("password");
//            System.out.println("username: "+userName);
//            System.out.println("password: "+password); 
            //验证用户名密码是否正确 
            if ("dqy".equals(userName) && "123".equals(password)){
                //登陆成功
                response.setStatus(200);
                response.setMessage("OK");
                response.setHeaders("Content-Type","text/html; charset=utf-8");
                //原来登陆成功是给浏览器写了一个cookie,cookie中保存的是用户名 
               // response.setHeaders("Set-Cookie","userName="+userName);

                String sessionId = UUID.randomUUID().toString();//会生成一个随机字符串,能保证每次调用这个方法生成的字符串都不一样 
                //此时身份信息保存在服务器中,就不会再有泄露的问题了, 
                //给浏览器返回cookie中包含sessionId即可 
                User user = new User();
                user.userName = "dqy";
                user.age = 20;
                user.school = "大学";
                sessions.put(sessionId,user);
                response.setHeaders("Set-Cookie","sessionId="+sessionId);

                //在响应中添加一个cookie,浏览器就会自动存储这个cookie
                response.writeBody("<html>");
                response.writeBody("<div>欢迎您!"+userName+"</div>");
                response.writeBody("</html>");
            }else {
                //登陆失败
                response.setStatus(403);
                response.setMessage("Forbidden");
                response.setHeaders("Content-Type","text/html; charset=utf-8");
                response.writeBody("<html>");
                response.writeBody("<div>登陆失败</div>");
                response.writeBody("</html>");
            }
        }
    }

    public static void main(String[] args) throws IOException {
        HttpServerV3 serverV3 = new HttpServerV3(9090);
        serverV3.start();
    }
}

成果展示

启动服务器

img

就会看到登录页面

img

输入用户名和密码之后,能看到:

img

此时就登陆成功了

抓包并且可以看到session字段

img

img

在浏览器中可以看到多了一个Cookie

img