编程语言
首页 > 编程语言> > java模拟浏览器与服务器请求与响应的过程

java模拟浏览器与服务器请求与响应的过程

作者:互联网

java模拟浏览器与服务器请求与响应的过程

浏览器端

在打开浏览器时,需要向浏览器的地址栏输入一个URL(统一资源定位符)地址串(ip:port/content?params),浏览器首先通过初步解析得到一个资源名和携带的参数。

首先创建一个类(Browser)来表示浏览器,定义三个属性:

	private Socket socket; //Socket对象
    private String ip; //ip地址
    private String portStr; //端口号

此处,用Scanner对象在控制台模拟输入URL的过程

	public void openBrowser(){
        Scanner sc = new Scanner(System.in);
        System.out.print("请输入URL:");
        // ip:port/资源名?key=value&key=value
        String url = sc.nextLine();
        //解析url
        this.parseURL(url);
	}

当用户在浏览器端输入请求的地址(URL)后,浏览器本省需要对这个URL地址串进行初步解析:

	/**
     * 解析输入的url
     * @param url 统一资源定位符
     */
    private void parseURL(String url){
        //找寻冒号的位置
        int colonIndex = url.indexOf(":");
        //找寻第一个斜杠出现的位置
        int slashIndex = url.indexOf("/");
        //解析url,拆分获取ip、port、contentAndParams
        ip = url.substring(0,colonIndex);
        portStr = url.substring(colonIndex+1,slashIndex);
        int port = Integer.parseInt(portStr);
        String contentAndParams = url.substring(slashIndex+1);
        //创建socket并向服务器发送请求
        createSocketAndSendRequest(ip,port,contentAndParams);
    }

然后需要单独创建一个方法,创建Socket对象,并且向服务器发送请求,(注意:这里上述方法调用该方法时可以不用再将ip和port当做参数传递过来了,因为此时ip和port已经放在了属性中,同一个类中的方法都可以共同访问)

	/**
     * 根据ip和port创建一个socket,并将请求发送给服务器
     * @param ip ip
     * @param port 端口号
     * @param contentAndParams 内容和参数
     */
    private void createSocketAndSendRequest(String ip,int port,String contentAndParams){
        try {
            //通过ip和port创建一个socket
            socket = new Socket(ip,port);
            //将contentAndParams发送给服务器
            PrintWriter out = new PrintWriter(socket.getOutputStream());
            out.println(contentAndParams);
            out.flush();
            //浏览器等待服务器返回的响应信息
            this.receiveResponseContent();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

到此为止,浏览器已经将我们输入的URL地址串初步解析成了ip、port、以及资源名和参数(contentAndParams)三个部分,我们利用ip和port端口号创建了一个Socket对象,然后将用户输入的资源名和参数通过这个Socket对象传递给服务器,让服务器对其做进一步解析。

服务器端

我们创建一个类(Server)来表示服务器
首先在Server类中写一个方法 startServer() 来表示启动一个服务器
此时,我们发现一个可以优化的点:
在未来的使用中,有可能我们的端口号不是固定的,所以,我们将端口号单独的放在一个配置文件中server.properties,在启动服务器时,我们先读取一遍配置文件,在配置文件中找到需要的端口号,以后只需要改配置文件就可以了。

port=9999

接下来我们创建一个类ServerFileReader ,这个类中我们读取这个server.properties的配置文件,这个ServerFileReader类中运用了一个缓存机制,避免每一次获取端口号都需要读取一次配置文件,回降低性能,所以在每次类加载之前,用静态代码块先读取一次配置文件中的信息,将这些信息全部存到一个Map集合中,以后在每次需要获取端口号的时候,就不用再使用I/O去读取配置文件了,直接去访问这个Map集合就可以了。

public class ServerFileReader {

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

    static {
        try {
            Properties prop = new Properties();
            prop.load(new FileReader("src/server.properties"));
            Enumeration en = prop.propertyNames();
            while (en.hasMoreElements()){
                String key = (String) en.nextElement();
                String value = prop.getProperty(key);
                map.put(key,value);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static String getValue(String key){
        return map.get(key);
    }
}

然后我们在每次启动服务器的时候获取端口号就调用这个getValue()方法,再通过Integer.parseInt()方法将这个端口号字符串转化成int类型

	public void startServer(){
        try {
            System.out.println("========启动服务器========");
            //获取端口号
            int port = Integer.parseInt(ServerFileReader.getValue("port"));
            //创建一个服务
            ServerSocket serverSocket = new ServerSocket(port);
            while(true){
                //等待某一个客户端过来连接,如果连接到了就会产生一个socket对象
                Socket socket = serverSocket.accept();
                //启动一个线程,负责处理当前浏览器发送过来的消息
                ServerHandler handler = new ServerHandler(socket);
                handler.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

由于服务器和浏览器之间存在一种一对多的关系(多个浏览器可以同时访问一个服务器),这里为了避免第二个用户访问是资源被占用的问题,我们创建一个线程对象ServerHandler来处理多线程并发的问题。ServerHandler类首先继承Thread类变成一个线程对象。然后再重写run()方法。当然为了接收刚才在浏览器端传输过来的数据,我们需要一个Socket对象当做属性,再提供一个构造方法。在服务器端有一个方法会等待一个客户端过来连接,serverSocket.accept()方法会返回一个Socket对象,这个Socket对象就是刚才浏览器端发送请求的那个Socket对象了

	public class ServerHandler extends Thread{
	    private Socket socket;
	
	    public ServerHandler(Socket socket) {
	        this.socket = socket;
	    }
	
	    public void run(){
	    }
    }

在这个线程对象ServerHandler类中我们需要完成以下这几件事情。

  1. 接收请求
  2. 解析内容和参数
  3. 将服务器响应的内容写回浏览器
接收请求
	private void receiveRequest(){
        try {
            InputStream is = socket.getInputStream();//最基本的字节流
            InputStreamReader isr = new InputStreamReader(is);//将字节流转化成字符流
            BufferedReader reader = new BufferedReader(isr);//将字符流包装成高级流,缓冲流,可以读取一行消息
            String contentAndParams = reader.readLine();
            //接受到浏览器发送过来的请求后,开始调用下一个方法(解析内容和参数)
            this.parseContentAndParams(contentAndParams);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
解析内容和参数
	private void parseContentAndParams(String contentAndParams){
        //创建两个变量分别存储content和params
        String content = null;
        HashMap<String,String> paramsMap = null;
        //按照?将contentAndParams拆开
        //?前面的部分是content  ?后面的部分是params
        //按照问号将其拆分
        String[] arr = contentAndParams.split("\\?");
        if(arr.length == 2){
            //如果有携带问号,content就是问号前面部分
            //再将问号后面部分的每一个key=value处理好后存入paramsMap集合
            content = arr[0];
            String params = arr[1];
            String[] keyAndValues = params.split("&");
            paramsMap = new HashMap<>();
            for (String keyAndValue : keyAndValues) {
                String[] kv = keyAndValue.split("=");
                paramsMap.put(kv[0],kv[1]);
            }
        }else{
            //如果没有问号,那么整个部分都是content
            content = arr[0];
        }
        //---------------至此,浏览器传输过来的信息已经解析完毕-------------
        //创建两个对象,一个对象是包含所有请求信息的,另一个对象是为了接收响应回来的结果
        //传递给你的是空对象,在控制层执行完毕后将这个对象填满
        HttpServletRequest request = new HttpServletRequest(content,paramsMap);
        HttpServletResponse response = new HttpServletResponse();
        ServletController.findController(request,response);
        //---------------程序执行到这里,证明response对象已经被填满了-----------
        this.responseToBrowser(response);
    }

注意:当服务器解析完成后得到了一个字符串content和一个paramsMap集合。我们需要通过这两个参数去找到具体的控制层去执行相应的事情,此时为了参数传递方便以及为了方便后续代码的维护,我们创建了一个HttpServletRequest对象去存储这个content和paramsMap

public class HttpServletRequest {
    //创建一个类,用来储存浏览器发送过来的请求信息
    private String content;
    private HashMap<String,String> paramsMap;

    public HttpServletRequest(String content, HashMap<String, String> paramsMap) {
        this.content = content;
        this.paramsMap = paramsMap;
    }

    public HttpServletRequest() {
    }

    /**
     * 获取paramsMap集合中的值,根据具体哪一个键就可以取出与之对应的值了
     * @param key 键
     * @return 返回与该key对应的值
     */
    public String getParameter(String key){
        return this.paramsMap.get(key);
    }

    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
    public HashMap<String, String> getParamsMap() {
        return paramsMap;
    }
    public void setParamsMap(HashMap<String, String> paramsMap) {
        this.paramsMap = paramsMap;
    }
}

当然,找到对应的控制层Controller之后,需要将他们执行完毕后的结果返回给ServerHandler对象,这里我们不用再findController()方法上设计返回值,我们直接在调用这个findController()方法的时候传递一个HttpServletResponse 对象,(注意:此时传递的HttpServletResponse 对象是一个空对象,没有任何东西,我们之所以可以利用这种方式来接收返回值的目的是因为HttpServletResponse 对象本身就是一个应用数据类型,我们可以在方法执行完毕后将这个对象填满,就可以接收到返回值了)
我们在这个HttpServletResponse类中提供了几个方法,可以调用这个write()方法将响应信息写出去。

//创建一个类,用来储存服务器响应回来的信息
public class HttpServletResponse {

    //创建一个StringBuilder处理字符串
    private StringBuilder responseContent = new StringBuilder();

    public void write(String str){
        this.responseContent.append(str);
    }

    public String getResponseContent(){
        return this.responseContent.toString();
    }
}

也可以通过读取一个文件的方式(可以将响应信息提前写到一个路径下的文件中去)然后通过I/O去读取这个文件,将这个文件中的信息每一行字符串写回去,这个文件目前我们暂时用index.view的方式描述,当然这个文件就是将来我们的jsp文件或者是html文件,这个文件中有可能携带有一些特殊含义的符号,当响应回浏览器后再在浏览器进行解析响应回去的字符串,然后展示,呈现在用户眼前。

	public void sendRedirect(String filePath){
        BufferedReader reader = null;
        try {
            File file = new File("src/file/"+filePath);
            reader = new BufferedReader(new FileReader(file));
            String value = reader.readLine();
            while(value != null){
                this.responseContent.append(value);
                value = reader.readLine();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

现在,我们已经写好了HttpServletRequest这个类和HttpServletResponse这个类,可以去调用findController()这个方法,由于findController()这个方法做的事情,与这个ServerHandler类中的方法做的事情有些不一致,我们将其抽离出去形成一个新的类ServletController,在ServerHandler类中写一个findController()方法去找寻与资源名对应的控制层(通过反射机制)
注意:(由于我们在浏览器端输入的URL地址串中的每一个符号都有特殊意义,点号代表所表示文件的后缀名,表示文件类型,所以我们在输入URL地址串的时候只能输入资源的名字和后缀名,还不能输入这个资源对应的Controller控制层具体在哪一个包下,但是,如果通过反射机制Class.forName()方法去加载类的时候是必须要传递一个类全名给他的,所以我们只能用过使用配置文件的方式将资源名与之对应的Controller的类全名写到配置文件中,这里我们定义了一个web.properties的配置文件)

index=controller.IndexController
login=controller.LoginController

然后我们有了这个配置文件,就可以每次通过资源名,参考配置文件中与资源名相对应的类全名去找到具体对应的Controller类,从而就可以通过反射机制,去加载找到该类了
但是此时出现了两个可以优化的点:
1、我们每次在执行findController方法的时候都会去读取一次配置文件,这样会大大降低程序的性能,所以我们在这个类中增加一个缓存机制,就是在每次类加载之前就将配置文件中的信息全部读取一个,存储到一个Map集合中,我们以后执行findController()方法的时候就可以不再需要读取配置文件了,直接在这个Map集合中去找,这样就可以优化一下性能的问题。

	static{
        try {
            Properties prop = new Properties();
            prop.load(new FileReader("src/web.properties"));
            //将配置文件中的信息全部读取出来放入到controllerNameMap集合中
            Enumeration en = prop.propertyNames();
            while (en.hasMoreElements()){
                String content = (String)en.nextElement();
                String realControllerName = prop.getProperty(content);
                controllerNameMap.put(content,realControllerName);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

2、我们发现Controller对象与之前我们写的Service和Dao的对象特别相似,里面只有方法在做事情,没有属性,所以我们可以考虑将每一个Controller对象设计成一个单例模式,就是每次在找到一个Controller对象的时候,我们就可以将这个对象存到Map集合中,下一次要使用这个Controller对象的时候,就可以直接在这个Map集合中去取就行了。
最后我们,整理优化一下findController()方法:

	public static void findController(HttpServletRequest request,HttpServletResponse response){
        //获取资源名
        String content = request.getContent();
        try {
            //获取这个类的对象
            HttpServlet controllerObject = controllerObjectMap.get(content);
            //如果对象不存在,证明之前没有使用过这个类,那就再通过反射的方式
            //参考配置文件(缓存)去创建一个该类的对象
            if(controllerObject == null){
                //参考配置文件(缓存),找到真实的类名
                String realControllerName = controllerNameMap.get(content);
                //判断请求的真实名字是否存在,如果请求的真实名字也不存在,就不处理了
                if(realControllerName != null){
                    //如果存在就通过反射区找到这个类
                    Class clazz = Class.forName(realControllerName);
                    //创建一个该类的对象
                    controllerObject = (HttpServlet) clazz.newInstance();
                    //将新创建的类对象放入集合中,下一次使用就不用创建对象了
                    controllerObjectMap.put(content,controllerObject);
                }else{
                    throw new ClassNotFoundException();
                }
            }
            //-------------以上可以确保controllerObject对象肯定存在---------------
            //通过反射获取这个类中的对应方法
            Class controllerClass = controllerObject.getClass();
            Method serviceMethod = controllerClass.getMethod("service", HttpServletRequest.class, HttpServletResponse.class);
            //找到对应的方法后就执行这个方法
            serviceMethod.invoke(controllerObject,request,response);
        } catch (ClassNotFoundException e) {
            response.write("请求的"+content+"Controller找不到");
        } catch (NoSuchMethodException e) {
            response.write("405 没有可以执行的方法");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

现在我们通过findController()方法找到了资源名对应的Controller控制层,所以我们就可以通过这个控制层去找到我们要响应给浏览器的内容。
此时我么通过控制层找到了要响应的内容,我们提前把这些内容写到了文件中。现在我们只需要读取文件,把文件中的内容字符串,这些字符串就是即将要写回浏览器的响应信息,我们把这些信息存到HttpServletResponse对象中,此时这个HttpServletResponse的内容不再为空了。
我们模拟了一个简单的html语句,表示控制层对应的响应信息

欢迎进入银行系统<br>
<form action="login" method="get">
请输入用户名<input name="username" value=""><br>
请输入密码<input name="password" value=""><br>
感谢使用,再见
将服务器响应的内容写回浏览器

我们单独设计一个方法,将刚才的HttpServletResponse对象传递给这个方法,我们将这里面的内容字符串(响应信息),响应给浏览器

	private void responseToBrowser(HttpServletResponse response){
        try {
            PrintWriter out = new PrintWriter(socket.getOutputStream());
            out.println(response.getResponseContent());
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

现在,我们通过输入了URL地址串,浏览器初步解析了这个URL地址串中的ip和端口号,通过ip和端口号创建了一个Socket对象,访问了服务器,服务器对这个资源名和参数做了进一步解析,得到了具体的资源名和参数,通过资源名找到了对应的控制层,从而找到了对应的响应信息,服务器也将响应信息写回浏览器了。我们现在回到浏览器这一端,将服务器响应给我们的信息做一个解析。
此时,由于我们模拟的响应信息是一个html标签的内容,所以我们根据html标签的规则对这些字符串进行进一步解析

浏览器端

首先我们接收一下服务器响应回来的信息

    /**
     * 接收服务器响应回来的信息
     */
    private void receiveResponseContent(){
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String responseContent = reader.readLine();
            this.parseResponseContentAndShow(responseContent);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

现在我们将接收到的响应信息,做一个解析,转换为浏览器能看懂的内容,并展示给用户

	/**
     * 解析并展示服务器响应回来的信息
     * @param responseContent 响应信息
     */
    private void parseResponseContentAndShow(String responseContent){
        //创建两个容器存储下一次发送请求的信息
        String content = null; //存储第二次发送请求的ip和端口号等信息
        HashMap<String,String> paramsMap = null; //存储解析标签后用户输入的数据
        //解析<br>标签
        responseContent = responseContent.replace("<br>","\r\n");
        while(true){
            //解析<input name="" value="">标签
            int ltIndex = responseContent.indexOf("<");
            int gtIndex = responseContent.indexOf(">");
            //判断是否存在成对的尖角号<>
            if(ltIndex != -1 && gtIndex != -1 && ltIndex < gtIndex){
                //先展示<>前面的部分
                System.out.println(responseContent.substring(0,ltIndex));
                //将整个标签和内容一起截取出来  <input name="" value="">
                String tag = responseContent.substring(ltIndex,gtIndex+1);
                //判断标签类型,进行不同的处理
                if(tag.contains("input")){
                    Scanner sc = new Scanner(System.in);
                    String value = sc.nextLine();
                    //如果刚开始没有使用过paramsMap数组就创建一个
                    if(paramsMap == null){
                        paramsMap = new HashMap<>();
                    }
                    //先将整个标签按照空格拆分成一个个小的键值对
                    //<input name="" value="">   -->  <input、name=""、value="">
                    String[] keyAndValues = tag.split(" ");
                    //循环遍历每一对键值对 找到name对应的键值
                    for (String keyAndValue : keyAndValues) {
                        if(keyAndValue.contains("=")){
                            String[] kv = keyAndValue.split("=");
                            if("name".equals(kv[0])){
                                String name = kv[1].substring(1,kv[1].length()-1);
                                //将name的值当做key  输入的value当做值 组合成一对键值对,存入paramsMap集合中
                                paramsMap.put(name,value);
                            }
                        }
                    }
                }else if(tag.contains("form")){
                    String[] keyAndValues = tag.split(" ");
                    //循环遍历每一对键值对 找到action对应的键值
                    for (String keyAndValue : keyAndValues) {
                        if(keyAndValue.contains("=")){
                            String[] kv = keyAndValue.split("=");
                            if("action".equals(kv[0])){
                                content = kv[1].substring(1,kv[1].length()-1);
                            }
                        }
                    }
                }
                //将处理完了的部分截取掉
                responseContent = responseContent.substring(gtIndex+1);
            }else{
                //证明不存在成对的尖角号,直接输出即可
                System.out.println(responseContent);
                break;
            }
        }
        //------------------至此,所有的标签全部解析完毕------------------
        //如果有出现form标签,证明需要发送一次新的请求
        this.sendNewResponse(content,paramsMap);
    }

在解析的过程中,我们模拟解析了几个html标签,如果解析到有form 表单元素,我们就将这个action=""的值截取出来,当做下一次要发送请求的资源名,如果没有解析到form表单元素,就证明此次响应已经结束了,不需要再发送第二次请求了。
现在我们再设计一个方法来发送下一次请求,现在需要吧这个content和input标签用户输入的数据当做资源名和参数传递给这个方法,这个方法会将这些数据,重新拼接成ip:port/资源名?key=value&key=value的形式。再调用浏览器之前写好的parseURL()方法去解析这个新的请求URL地址串,以及下一次的请求。

	//发送新请求
    private void sendNewResponse(String content,HashMap<String,String> paramsMap){
        if(content != null){
            StringBuilder url = new StringBuilder(ip);
            url.append(":");
            url.append(portStr);
            url.append("/");
            url.append(content);
            if(paramsMap != null){
                url.append("?");
                for (String key : paramsMap.keySet()) {
                    String value = paramsMap.get(key);
                    url.append(key);
                    url.append("=");
                    url.append(value);
                    url.append("&");
                }
                //循环结束后会多出一个&  需要将其删除
                url.delete(url.length()-1,url.length());
            }
            this.parseURL(url.toString());
        }
    }

ok,到此为止,我们模拟的浏览器与服务器之间的请求与响应的过程就已经全部结束了,当然这只是做了一个简单的模拟。
下面是模拟请求与响应过程的图示:
在这里插入图片描述

标签:浏览器,String,url,paramsMap,new,content,服务器,java
来源: https://blog.csdn.net/jhy17340226919/article/details/116375663