模拟浏览器与服务器交互(简易TomCat框架)

时间:2021-10-22 00:38:18

浏览器发送请求到服务器获取资源的流程和概念

日常我们使用的浏览器,底层都是帮我们做了很多事情,我们只需要用,比如输入www.baidu.com,就可以访问百度的首页

那么它是如何做到的呢,其实简单来说就是浏览器在底层利用socket,将我们输入的地址进行解析,拆解成多个部分发送到服务器端获取资源

大致步骤为:

  1. 打开浏览器
  2. 输入一个url , url包含: ip:port/content?key=value&key=value
  3. 解析url, 解析为: ip , port , content?, key=value, key=value
  4. 底层创建socket连接
  5. 发送请求到该地址所在的服务端(使用流的方式写出去,也就是OutPut),浏览器会接收这些请求,接收就是输入,input
  6. 浏览器收到请求,解析地址并找到所携带的条件,进行返回数据(返回数据需要写出去,也就是output)
  7. 浏览器读取服务器写回来的数据,按照某一个规则生成的字符串,接收读取是输入,也就是input
  8. 解析服务器传回来的数据进行解析,并在浏览器中展示出来

模拟过程

接下来我们用代码的形式来逐步实现上述步骤

我们首先创建一个Client类,作为客户端

public class Client {
    
}

Client类中的操作:

//第一步和第二步我们可以合并为一个步骤
//创建一个方法,通过Scanner在控制台模拟用户输入url
public void open() {
        //1,打开浏览器
        System.out.println("==打开浏览器==");
        //2,输入url
        System.out.print("输入url");
        Scanner scanner = new Scanner(System.in);
        String url = scanner.nextLine();
        //3,解析url,找一个小弟帮忙处理这件事
        parseUrl(url);
    }

    private void parseUrl(String url) {
        // ip:port/content?key=value
        //我们要将url拆分成 ip , port , content?key=value三部分
        int index1 = url.indexOf(":");
        int index2 = url.indexOf("/");
        String ip = url.substring(1, index1);
        int port = Integer.parseInt(url.substring(index1 + 1, index2));
        String content = url.substring(index2 + 1);
        //4,解析完成以后创建socket连接并发送请求数据,找小弟帮忙处理这件事
        createSocketAndSendRequest(ip, port,content);
    }

    private void createSocketAndSendRequest(String ip,int port,String content) {
        Socket socket = new Socket();
        try {
            //InetSocketAddress对象负责将ip,port传递给服务器端对应的socket
            socket.connect(new InetSocketAddress(ip, port));
            //这里我们需要发送请求,就是写出,要用到输出流,但是这个输出流不能我们自己new
            //原因很简单,客户端和服务器的socket进行传递连接,就像是两个特工对暗号一样
            //只有它们知道暗号是什么,或者说什么规则,这是两个socket读取和输出的流的规则
            //所以我们要通过socket来获取输出流
            OutputStream outputStream = socket.getOutputStream();
            //url中可能会有中文,OutputStream是字节流,遇到中文,可能就会很麻烦,所以需要将它包装一下
            //new PrintWriter(outputStream);这里使用了装饰着模式
            PrintWriter writer = new PrintWriter(outputStream);
            //写出去一行,因为浏览器地址栏中正好输入的就是一行,此方法正好
            writer.println(content);
            //刷新管道
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

					//这都是客户端做的事情,接下来我们看看服务器这边的流程实现

创建一个Service类,作为服务端

public class Service {

}

Service类中的操作

//启动服务器socket
public void start() {
    try {
        System.out.println("==server start==");
        //1,首先创建服务器端的ServerSocket
        ServerSocket serverSocket = new ServerSocket();
        //通过InetSocketAddress将ip,port传入
        InetSocketAddress address = new InetSocketAddress("127.0.0.1",9999);
        //然后将ip和端口绑定,意思就是,我这次服务端socket定义好了ip和端口,如果客户端输入的ip和端口和我一样,那就连上我了
        serverSocket.bind(address);
        //2,等待客户端连接,开启等待(等待有客户端连接上我),accept()是阻塞式的,只有有客户端连接上我了,我才会放行去执行下边的操作
        Socket socket = serverSocket.accept();
        //3,接收客户端发送过来的请求信息,找一个小弟帮忙完成
        receiveRequest(socket);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

private void receiveRequest(Socket socket) {
    //既然是接收,那么就需要一个输入流,这个输入流和客户端的输出流一样,不能自己new
    //得是通过socket获取,(对暗号)
    try {
        InputStream inputStream = socket.getInputStream();
        //还需要解决中文问题,可以使用InputStreamReader来转换,装饰者模式
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
        //转换完成以后还需要注意一点就是,客户端那边传过来的是读取一行,但是inputStreamReader并没有读取一行的方法
        //所以我们还需要使用一个BufferedReader流,它有读取一行的方法,这都是装饰者模式
        BufferedReader reader = new BufferedReader(inputStreamReader);
        String content = reader.readLine();
        System.out.println(content);
        //4,解析接收到的请求信息,并存入相应的结构中,找一个小弟来帮忙完成这个任务
        //将接收到的content资源信息解析处理,找一个小弟来处理这件事情:index.html?name=xzc
        praseContent(content);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
private void praseContent(String content) {
        if (content == null) return;
        String requestName = null;
        Map<String, String> map = null;
        int index = content.indexOf("?");
        if (index != -1) {
            requestName = content.substring(0, index);
            map = new HashMap<>();
            String allKeyAndValues = content.substring(index + 1);
            String[] keyAndValues = allKeyAndValues.split("&");
            for (String keyAndValue : keyAndValues) {
                String[] kv = keyAndValue.split("=");
                map.put(kv[0], kv[1]);
            }
        } else {
            requestName = content;
        }
        //到此就将content解析完毕,分成了两个部分,一部分是资源名字:requestName
        //一部分是请求参数:map里的一个个kv对儿
        //然后将得到的这两个参数传给下边的小弟方法帮我们去找资源
        /*
            ⭐⭐⭐但是,⭐重点⭐: 因为现在的这个解析方法只能解析http请求的资源,所以现在只能解析成两个部分
            未来可能要解析各种协议,比如说emial协议里带有@符号,规则不同,所以解析出来的部分可能是3个部分,也可能是4个部分
            那意味着我们这个方法要根据协议而改动,给下边小弟方法传递的参数也会时常的改变
            这就造成了方法间的高度耦合,我们能不能这样,让此方法变动的时候不影响下边做事的小弟方法
            我们给下边的小弟方法传递一个固定的大容器,不管传递几个部分的参数,我们都放在一个大容器中,将这个大容器传给小弟方法
            这样,我们可以使用一个对象,来聚集所有可能的参数部分 我们可以创建一个HttpServletRequest类
        */
    	HttpServletRequest request = new HttpServletRequest(requestName,map);
        //5,找一个小弟,来帮忙找资源
        findServlet(request);
    }

创建HttpServletRequest类:

⭐原来这就是HttpServletRequest类的由来,HttpServletRequest是在服务器解析数据的时候创建的,而且是每一个请求进行都会创建一个新的HttpServletRequest;

package com.xzc.webbs;

import java.util.Map;

public class HttpServletRequest {

    private String requestName;
    private Map<String,String> parameterMap;

    public HttpServletRequest(String requestName, Map<String, String> parameterMap) {
        this.requestName = requestName;
        this.parameterMap = parameterMap;
    }

    public Map<String,String> getParameterMap() {
        return parameterMap;
    }

    public String getRequestName() {
        return requestName;
    }

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

⭐⭐⭐下面的就是模拟浏览器向服务器发送请求的重点,所以分开来写和记录⭐⭐⭐

服务器如何找到资源呢?

​ 其实可以理解为,用户发送的请求资源名,对应着一个资源类(资源类中的方法):例如127.0.0.1:9999/index.html?name=xzc

​ 这个index.html就是一个资源名,只有我们写程序的人才知道这个资源名和资源类的对应关系,也就是说,这个资源名用户不能乱写,乱写就 找不到了,这样的话,通过解析,我可以在程序中写一个判断,如果资源名叫做index.html,那么我就创建一个类,然后执行这个类中相应的方法,这样,资源就找到啦

但是, 请思考一个问题,我们现在做的事情是
模拟浏览器与服务器交互(简易TomCat框架)

上边的service包日常不用我们写,有写好的框架

上述的问题是,我们是客户端开发人员,我们写的东西就是资源文件(资源类),我们最后要将资源文件打包后放到服务器中帮我们管理起来,

用户发送过来的请求,走到服务器,服务器找到资源然后返回给用户,这个过程有一个问题,就是服务器并不知道你会将什么资源名传递给我

这个资源名和资源文件在我们没有打包上传给服务器之前,服务器是不知道你的资源名要和哪个资源类文件对应,所以,在判断中是不能写死或者用if else判断的,那么,这里我们就要做一个配置,让服务器知道哪一个资源名,对应哪一个资源类: 也就是说, 我们写好资源类后打包放到服务器上的时候,给服务器一个说明书(配置),让服务器知道,用户发送这个请求资源名,你给我找到这个资源类,资源类去调对应的方法

那么这个配置(说明书)我们是写在文件里 还是注解里,这就是形式的问题了(注解的形式,xml文件的形式,propreties文件的形式,yml文件的形式)

接下来我们就写一个propreties文件,来将说明写入

新建一个web.properties文件

index=com.xzc.webbs.controller.IndexController

这里有几个问题:

index表示的是用户发送过来的请求资源名

com.xzc.webbs.controller.IndexController是请求名对应的资源类,为什么要写类全名呢??是因为我们要通过反射的形式来找到此类

反射的创建方式有三种,其中有一种是Class.forName(String className),这里的参数className要传递的是类的全路径名

接下来我们来写上面的第四步findServlet(request);方法,也就是那个小弟,来帮忙找资源

private void findServlet(HttpServletRequest request) {
	try {
        //1,获取到请求资源的名称
        String requestName = request.getRequestName();
        //2,接下来我们要加载properties配置文件(说明书),有一个现成的类Properties
        //Properties此类其实就是一个map,也是一个流,当prop.load()执行完以后prop里就有值了,从配置文件中读出来的
        Properties prop = new Properties();
        //这里我们需要传入一个输入流到load()方法的参数中
        //使用Thread的类加载器来获取流,为什么呢,因为我们是客户端程序员,打包好以后包的路径会放在服务器的某个位置,可能就不是我们           现在的包路径了
        InputStream inputStream = 		     							                                             Thread.currentThread().getContextClassLoader().getResourceAsStream("web.properties");
        //加载完成后prop中就有值了
        prop.load(inputStream);
        //3,通过properties文件中的key找到对应的value,也就是className
        String className = prop.getProperty(requestName);
        //4,通过类名反射加载类
        Class<?> clazz = Class.forName(className);
        //5,通过clazz来创建对象
        Object obj = clazz.newInstance();
        //6,通过clazz获取类中的方法
        Method method = clazz.getDeclaredMethod("test");
        //7,传入obj,让这个方法执行
        method.invoke(obj);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

这是我们的资源类:

package com.xzc.webbs.controller;

public class IndexController {

    public void test() {
        System.out.println("恭喜你看到了我,我就是你要找的资源");
    }

}

⭐以上如果能做出来,看到了最后的输出,那么恭喜你,完成了基本的模拟浏览器与服务器的交互

接下来我们来分析一下上面的findServlet方法的问题所在:

1,首先,test方法是我们定义的,我们在这里是写死的,这样肯定是不对的,因为我们现在是客户端程序员和服务器都是自己写的,但真正的服务 器是不知道你要定义什么方法的,你的方法名叫什么服务器是不知道的

2,我们每一个请求进来都要执行一遍findServlet方法,每执行一遍都要去加载一次properties文件,它是从硬盘读取,所以一定是很慢的

​ 这里我们可以想办法优化一下

​ 我们站在服务器的角度上增加一个类,这个类就是专门来加载我们的properties文件的,并且将加载出来的信息放在一个map集合中作为 缓存

​ 专门用来读取properties文件的MyServerReader类,并提供一个根据key获取map中数据的方法

package com.xzc.webbs.server;

import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

public class MyServerReader {

    public static Map<String, String> map = new HashMap<>();

    //我们将加载properties文件的程序放在静态代码块中,这样在类被加载的时候就去读取properties文件
    static {
        Properties prop = new Properties();
        InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("web.properties");
        try {
            prop.load(inputStream);
            //获取一个Enumeration类型的迭代器
            Enumeration<?> enumeration = prop.propertyNames();
            while (enumeration.hasMoreElements()) {
                String key = (String) enumeration.nextElement();
                String value = prop.getProperty(key);
                //装入缓存map,这样,在类被加载的时候,这个map中就存在了所有properties文件中的键值对了
                map.put(key, value);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    //提供一个根据key获取map中数据的方法
    public static String getValue(String key) {
        return map.get(key);
    }
}

3,每次请求过来,我们都要执行一次findServlet方法,同样的,也都会重新通过反射再new一个实例,这样就会消耗内存空间,服务器其实就是想 通过这个controller类 来调用service方法,所以,我们可以将每一个资源类定义成单例的,那么怎么做呢??

​ 我们可以在service类中添加一个map,来存放每一个controller,然后在findServlet方法中先去判断map中是否有这个controller,有就直 接调方法,没有再通过反射去创建对象,并放入map中

4,这里通过反射创建的Method并没有参数,如果有参数怎么办??

​ 上面我们说到,我们作为客户端程序员,写好的资源类都是要打包交给服务器管理的,那么服务器就可以制定一套规则,我们遵循这个规则, 就能解决上述问题,定义规则,那么就要使用接口或者抽象类

​ 接下来,我们站在服务器的层面上定义一个抽象类,并定义一个抽象方法,让我们客户端程序员写的controller来继承这个抽象类,并重写这 个方法,这样就遵循了服务器的规则,并在底层帮我们调用这个重写的方法来处理请求

抽象类HttpServlet:

package com.xzc.webbs.server;

public abstract class HttpServlet {

    public abstract void service(HttpServletRequest request);
}

controller继承抽象类HttpServlet并重写service方法:

package com.xzc.webbs.controller;

import com.xzc.webbs.server.HttpServlet;
import com.xzc.webbs.server.HttpServletRequest;
import com.xzc.webbs.service.UserService;

public class IndexController extends HttpServlet {

    private UserService service = new UserService();

    @Override
    public void service(HttpServletRequest request) {
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        String success = service.login(username, password);
        //调用完业务层处理完逻辑后,要将数据返回给客户端,那么怎么返回呢?
        //最终的结果是要返回给浏览器的(响应)
        //就要通过服务器,获取一个输出流对象,写回浏览器,这里先跳过...
    }
}

所以,最后findServlet方法可以优化为以下形式:

private void findServlet(HttpServletRequest request) {
    try {
        //1,获取到请求资源的名称
        String requestName = request.getRequestName();
        //定义的map集合,来获取集合中是否有controller,
        //controller继承了HttpServlet,所以map中放的是controller的父类HttpServlet
        HttpServlet servlet = map.get(requestName);
        if (servlet == null) {
            //如果为null,就使用专用的加载properties文件的类,来根据请求资源名(key)获取到类全名来进行反射
            String className = MyServerReader.getValue(requestName);
            //通过类名反射加载类
            Class<?> clazz = Class.forName(className);
            //通过clazz来创建对象
            servlet = (HttpServlet) clazz.newInstance();
            //放入map集合
            map.put(requestName, servlet);
        }
        //这里就是一种方式而已,就是还可以通过反射来执行方法
        Class<? extends HttpServlet> clazz = servlet.getClass();
        //加上了参数,找的方法叫service,因为重写了服务器制定的规则,规定就叫service方法,所以可以写死
        Method method = clazz.getDeclaredMethod("service", HttpServletRequest.class);
        //执行方法的时候也要传入参数request
        method.invoke(servlet,request);

        //或者,可以直接调用servlet.service(request);
        //servlet.service(request);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

上述完成后,基本整个流程就快结束了,接下来就是将数据响应给浏览器了,就要用到服务器的输出流把数据写回去

在controller层中我们的数据要怎么传递给服务器,并让服务器返回给浏览器呢? 我们重写的service方法是没有返回值的

这个时候就要想一个办法,让服务器得到, 我们可以定义一个类:HttpServletResponse,将此类作为service的参数

类中定义一个StringBuilder,controller返回的数据写入到StringBuilder中,服务器底层通过反射来获取到StringBuilder中的数据

HttpServletResponse类:

package com.xzc.webbs.server;

/**
 * 此类是服务器为了接收响应回去的数据而生
 *
 * 我们将controller层返回的数据放入此类中,然后底层通过反射机制来获取到此类中的数据
 */
public class HttpServletResponse {

    private StringBuilder responseContent = new StringBuilder();

    //此方法是给客户端程序员用的,将数据写入StringBuilder
    public void write(String message) {
        responseContent.append(message);
    }
	//此方法是给服务器用的,在底层通过反射,将StringBuilder的数据取出来用于返回给浏览器
    public String getResponseContent() {
        return responseContent.toString();
    }
}

接下来,我们要对以上代码做一个最终优化

首先就是,我们想要实现打开真正的浏览器来发送请求访问我们自己写的服务器,这样就会有一个问题: 现在的socket只要连接一次,就停止了也就是关闭了,我们使用真正的浏览器访问的话不能只请求一次就把socket关闭了,因为服务器只有一个,但是浏览器却有很多个,比如你访问完了以后别人还要通过自己的电脑输入网址访问,所以我们的服务器的socket要一直开着

为了解决这个问题,我们需要创建线程来完成,将service中的方法全部挪到Handler类中,Handler类继承Thread类,重写run方法

其次,我们增加了HttpServiletResponse来装返回的数据,那么就要在findServlet()方法的反射部分加上参数HttpServiletResponse

Handler类:

package com.xzc.webbs.server;

import java.io.*;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

public class Handler extends Thread{

    private Socket socket;

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

    @Override
    public void run() {
        receiveRequest();
    }

    private Map<String, HttpServlet> map = new HashMap<>();

    //启动服务器socket
//    public void start() {
//        try {
//            System.out.println("==server start==");
//            //1首先创建服务器端的ServerSocket
//            ServerSocket serverSocket = new ServerSocket();
//            //通过InetSocketAddress将ip,port传入
//            InetSocketAddress address = new InetSocketAddress("127.0.0.1", 9999);
//            //然后将ip和端口绑定,意思就是,我这次服务端socket定义好了ip和端口,如果客户端输入的ip和端口和我一样,那就连上我了
//            serverSocket.bind(address);
//            //2,等待客户端连接,开启等待(等待有客户端连接上我),accept()是阻塞式的,只有有客户端连接上我了,我才会放行去执行下边的操作
//            socket = serverSocket.accept();
//            //3,接收客户端发送过来的请求信息,找一个小弟帮忙完成
//            receiveRequest();
//        } catch (IOException e) {
//            e.printStackTrace();
//        }
//    }

    private void receiveRequest() {
        //既然是接收,那么就需要一个输入流,这个输入流和客户端的输出流一样,不能自己new
        //得是通过socket获取,(对暗号)
        try {
            InputStream inputStream = socket.getInputStream();
            //还需要解决中文问题,可以使用InputStreamReader来转换,装饰者模式
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
            //转换完成以后还需要注意一点就是,客户端那边传过来的是读取一行,但是inputStreamReader并没有读取一行的方法
            //所以我们还需要使用一个BufferedReader流,它有读取一行的方法,这都是装饰者模式
            BufferedReader reader = new BufferedReader(inputStreamReader);
            String content = reader.readLine();
            //将接收到的content资源信息解析处理,找一个小弟来处理这件事情:index.html?name=xzc
            praseContent(content);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void praseContent(String content) {
        if (content == null) return;

        String[] vvv = content.split(" ");
        if (vvv.length > 0) {
            content = vvv[1].substring(1);
        }


        String requestName = null;
        Map<String, String> map = null;
        int index = content.indexOf("?");
        if (index != -1) {
            requestName = content.substring(0, index);
            map = new HashMap<>();
            String allKeyAndValues = content.substring(index + 1);
            String[] keyAndValues = allKeyAndValues.split("&");
            for (String keyAndValue : keyAndValues) {
                String[] kv = keyAndValue.split("=");
                map.put(kv[0], kv[1]);
            }
        } else {
            requestName = content;
        }
        //到此就将content解析完毕,分成了两个部分,一部分是资源名字:requestName
        //一部分是请求参数:map里的一个个kv对儿
        //然后将得到的这两个参数传给下边的小弟方法帮我们去找资源
        /*
            但是,重点: 因为现在的这个解析方法只能解析http请求的资源,所以现在只能解析成两个部分
            未来可能要解析各种协议,比如说emial协议里带有@符号,规则不同,所以解析出来的部分可能是3个部分,也可能是4个部分
            那意味着我们这个方法要根据协议而改动,给下边小弟方法传递的参数也会时常的改变
            这就造成了方法间的高度耦合,我们能不能这样,让此方法变动的时候不影响下边做事的小弟方法
            我们给下边的小弟方法传递一个固定的大容器,不管传递几个部分的参数,我们都放在一个大容器中,将这个大容器传给小弟方法
            这样,我们可以使用一个对象,来聚集所有可能的参数部分 我们可以创建一个HttpServletRequest类
        */
        HttpServletRequest request = new HttpServletRequest(requestName, map);
        HttpServletResponse response = new HttpServletResponse();
        //找一个小弟,来帮忙找资源
        findServlet(request,response);
    }

    private void findServlet(HttpServletRequest request,HttpServletResponse response) {
        try {
            //1,获取到请求资源的名称
            String requestName = request.getRequestName();
            //定义的map集合,来获取集合中是否有controller,
            //controller继承了HttpServlet,所以map中放的是controller的父类HttpServlet
            HttpServlet servlet = map.get(requestName);
            if (servlet == null) {
                //如果为null,就使用专用的加载properties文件的类,来根据请求资源名(key)获取到类全名来进行反射
                String className = MyServerReader.getValue(requestName);
                //通过类名反射加载类
                Class<?> clazz = Class.forName(className);
                //通过clazz来创建对象
                servlet = (HttpServlet) clazz.newInstance();
                //放入map集合
                map.put(requestName, servlet);
            }
            //这里就是一种方式而已,就是还可以通过反射来执行方法
            Class<? extends HttpServlet> clazz = servlet.getClass();
            Method method = 
                //这里加上了HttpServiletResponse参数
                clazz.getDeclaredMethod("service",HttpServletRequest.class,HttpServletResponse.class);
            //这里加上了HttpServiletResponse参数
            method.invoke(servlet,request,response);
            //或者,可以直接调用servlet.service(request);
            //servlet.service(request);

            //上面的invoke方法执行完以后,response中的StringBuilder就有值了
            //找一个小弟,将response响应给浏览器(browser)
            responseToBrowser(response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void responseToBrowser(HttpServletResponse response) {
        String responseContent = response.getResponseContent();
        try {
            OutputStream outputStream = socket.getOutputStream();
            PrintWriter writer = new PrintWriter(outputStream);
            //这里是服务器真正写出去数据也就是响应给浏览器的操作
            writer.println(responseContent);
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

通过上述步骤,服务器成功将数据通过输出流传给了浏览器,浏览器需要通过socket获取输入流来得到响应回来的数据并进行解析,最终展示

浏览器Browser类:

package com.xzc.webbs.browser;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Scanner;

public class Browser {
    public static void main(String[] args) {
        Browser client = new Browser();
        client.open();
    }


    //存储一个socker对象作为浏览器的对象
    private Socket socket;

    public void open() {
        //1,打开浏览器
        System.out.println("==打开浏览器==");
        //2,输入url
        System.out.print("输入url:");
        Scanner scanner = new Scanner(System.in);
        String url = scanner.nextLine();
        //3,解析url,找一个小弟帮忙处理这件事
        parseUrl(url);
    }

    private void parseUrl(String url) {
        // ip:port/content?key=value
        //我们要将url拆分成 ip , port , content?key=value三部分
        int index1 = url.indexOf(":");
        int index2 = url.indexOf("/");
        String ip = url.substring(0, index1);
        int port = Integer.parseInt(url.substring(index1 + 1, index2));
        String content = url.substring(index2 + 1);
        //4,解析完成以后创建socket连接并发送请求数据,找小弟帮忙处理这件事
        createSocketAndSendRequest(ip, port,content);
    }

    private void createSocketAndSendRequest(String ip,int port,String content) {
        socket = new Socket();
        try {
            //InetSocketAddress对象负责将ip,port传递给服务器端对应的socket
            socket.connect(new InetSocketAddress(ip, port));
            //这里我们需要发送请求,就是写出,要用到输出流,但是这个输出流不能我们自己new
            //原因很简单,客户端和服务器的socket进行传递连接,就像是两个特工对暗号一样
            //只有它们知道暗号是什么,或者说什么规则,这是两个socket读取和输出的流的规则
            //所以我们要通过socket来获取输出流
            OutputStream outputStream = socket.getOutputStream();
            //url中可能会有中文,OutputStream是字节流,遇到中文,可能就会很麻烦,所以需要将它包装一下
            //new PrintWriter(outputStream);这里使用了装饰着模式
            PrintWriter writer = new PrintWriter(outputStream);
            //写出去一行,因为浏览器地址栏中正好输入的就是一行,此方法正好
            writer.println(content);
            //刷新管道
            writer.flush();
            //将数据发送出去刷新管道后,socket就处于等待状态了,等服务器端的docket响应数据回来后才会往下执行
            //下面的方法是将服务器返回的数据解析并展示
            receiveResponse();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
	//通过socket获取输入流,得到服务器传来的数据
    private void receiveResponse() {
        try {
            //使用socket获取输入流,得到响应数据,这里还是会存在中文乱码问题,所以需要包装,装饰者模式
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String content = reader.readLine();
            //找一个小弟,帮助我将content解析在浏览器中展示
            parseResponseContent(content);
        } catch (IOException e) {


        }
    }
	//将content解析在浏览器中展示
    private void parseResponseContent(String responseContent) {
        int index1 = responseContent.indexOf("<");
        int index2 = responseContent.indexOf(">");

        if (index1 != -1 && index2 != -1) {
            String key = responseContent.substring(index1 + 1, index2);
            if (key.equals("br")) {
                responseContent = responseContent.replace("<br>","\r\n");
            }
        }
        System.out.println(responseContent);
    }
}

下面的IndexController是我们作为客户端程序员写的controller层

package com.xzc.webbs.controller;

import com.xzc.webbs.server.HttpServlet;
import com.xzc.webbs.server.HttpServletRequest;
import com.xzc.webbs.server.HttpServletResponse;
import com.xzc.webbs.service.UserService;

public class IndexController extends HttpServlet {

    public IndexController() {
        System.out.println("controller加载了");
    }

    private UserService service = new UserService();

    @Override
    public void service(HttpServletRequest request, HttpServletResponse response) {
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        System.out.println(username);
        System.out.println(password);
        String success = service.login(username, password);
        System.out.println(success + "<br>了吗");
        //调用完业务层处理完逻辑后,要将数据返回给客户端,那么怎么返回呢?
        //我们定义一个容器,来接收需要返回的数据,这个容器就是HttpServletResponse
        //这是为了测试使用真正的浏览器向我们自己写的服务器发送请求后我们服务器将数据发送给谷歌浏览器以后谷歌浏览器会解析
        //下面的响应数据,并在页面展示一个按钮图标,这是按照谷歌浏览器的解析规则写的
        response.write("HTTP1.1 200 OK\r\n");
        response.write("Content-Type: text/html;charset=UTF-8\r\n");
        response.write("\r\n");
        response.write("<html>");
        response.write("<body>");
        response.write("<input type='button' value='按钮'> ");
        response.write("</body>");
        response.write("</html>");

    }
}

然后是我们通过Service类的入口进入

package com.xzc.webbs.server;

import java.io.*;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

public class Service {

    public static void main(String[] args) throws IOException {
        System.out.println("==server start==");
        //1首先创建服务器端的ServerSocket
        ServerSocket serverSocket = new ServerSocket();
        //通过InetSocketAddress将ip,port传入
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 9999);
        //然后将ip和端口绑定,意思就是,我这次服务端socket定义好了ip和端口,如果客户端输入的ip和端口和我一样,那就连上我了
        serverSocket.bind(address);
        //这里创建一个死循环,是为了让服务器的socket一直保持着连接
        while (true) {
        
            //2,等待客户端连接,开启等待(等待有客户端连接上我),accept()是阻塞式的,只有有客户端连接上我了,我才会放行去执行下边			 //的操作
            Socket socket = serverSocket.accept();
            Handler handler = new Handler(socket);
            handler.start();
        }
    }
}

至此,一个简易版的模拟浏览器向服务器发送请求并解析响应的整个流程就完成了,相当于我们自己写了一个简易版的TomCat

我们知道了HttpServletRequest是在什么时候被创建的,知道了HttpServletResponse是什么时候被创建的

知道了为什么HttpServletRequest是单例的,以及服务器是通过反射来帮助我们在底层调用了资源类的方法

知道了服务器在底层帮我们解析了请求,知道了配置文件是如何被加载的等等

总结:

最后,我们来做一个总结:

/*
	浏览器(browser)						服务器(service)
     1,打开并输入URL					1,首先服务器开启一个ServiceSocket
     2,解析URL: ip port content		                2,等待着浏览器端连接 -> accept方法,产生一个socket
     3,创建输出流,发送content并等待服务器响应数据		3,启动一个线程Handler,处理当前浏览器的请求
     4,读取服务器响应信息 responseContent                       一,读取请求信息content
     5,按照浏览器解析规则,来解析这个响应信息                      二,获取requestName和parameterMap,这是封装在
     6,解析完后浏览器展示最后的结果                                 HttpservletRequest类中的
				                        三,new HttpservletRequest()来获取
							   requestName和parameterMap
							   new HttpservletResponse()来存储响应数据
							4,通过request对象中的请求名找资源
							  参考一个"说明书" -> web.properties
							  找到一个真正的controller类对象,通过
							  反射执行类中的service方法
							5,执行完以后得到一个响应信息,就是一个string
							  只是这个string要遵循浏览器解析的规则:
							  <html>字符数据</html><body>数据字符</body>
							  6,响应信息写回浏览器

*/