模擬瀏覽器與伺服器互動(簡易TomCat框架)

XUEZHAOCHANG發表於2023-03-01

模擬瀏覽器傳送請求到伺服器獲取資源的思想和程式碼實現

瀏覽器傳送請求到伺服器獲取資源的流程和概念

日常我們使用的瀏覽器,底層都是幫我們做了很多事情,我們只需要用,比如輸入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,那麼我就建立一個類,然後執行這個類中相應的方法,這樣,資源就找到啦

但是, 請思考一個問題,我們現在做的事情是

上邊的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,響應資訊寫回瀏覽器

*/

相關文章