1. 程式人生 > >手寫Tomcat(ServerSocket、HTTP協議)

手寫Tomcat(ServerSocket、HTTP協議)

Tomcat本質上就一個請求+響應請求的JAVA程式,當我們從瀏覽器輸入一個url時,我們將傳送請求到Tomcat上,tomcat對該請求進行解析,並將響應的內容返回瀏覽器。

Tomcat通過Socket+HTTP協議進行實現,這裡做了一個簡單的流程圖。
在這裡插入圖片描述
下面簡單介紹下HTTP協議:
HTTP協議

  1. HTTP協議運行於TCP協議之上,預設埠80,HTTP則是443。
  2. HTTP協議都是客戶端發起請求,是一個無狀態協議,這次請求和上次沒有對應關係。
  3. HTTP請求(響應)報文包括:請求行(狀態行)、首部、空行和實體。

在這裡插入圖片描述
請求頭(重點幾個)

  • Host:本次請求主機的路徑
  • User-Agent:告訴伺服器本次請求客戶端所在的平臺以及本次請求採用的瀏覽器
  • Accept:用於指定客戶端接收哪些型別的資訊,*/*都接收
  • Accept-Language:語言
  • Accept-Encoding:壓縮資料的格式,例如gzip,表示瀏覽器可以識別gzip型別的資料

響應頭(重點幾個)

  • Date:響應事件
  • content-Type:本次響應內容型別
  • content-Encoding:本次內容採用的壓縮格式
  • content-length:本次內容長度

動手實現

在這裡插入圖片描述
當請求是靜態請求,即請求一個html介面的時候,我們只需要讀取html檔案的內容,並組織成http響應報文即可。這裡給出兩個html:
在這裡插入圖片描述


在這裡插入圖片描述
而如果是動態的請求,則就是servlet,通過java程式碼返回響應體,這裡我們配置兩個簡單servlet在配置檔案中:

aa=com.fty.tomcatv2.AAServlet
bb=com.fty.tomcatv2.BBServlet

並實現簡單java程式碼:

package com.fty.tomcatv2;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public interface Servlet {//所有服務端JAVA要實現的介面
    //初始化
    public void init();
    //服務
    public void Service(InputStream is, OutputStream os) throws IOException;
    //銷燬
    public void destroy();
}

這是servlet的整體介面,而我們的service就是具體的業務邏輯

package com.fty.tomcatv2;	
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class AAServlet implements Servlet {


    public void init() {
        System.out.println("aaServlet...init");
    }	
    public void Service(InputStream is, OutputStream os) throws IOException {
        System.out.println("aaServlet...service");
        os.write("I'm from AAServlet".getBytes());
        os.flush();
    }	
    public void destroy() {
        System.out.println("aa...destroy...");
    }
}

package com.fty.tomcatv2;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class BBServlet implements Servlet {	
    public void init() {
        System.out.println("bbServlet...init");
    }
    public void Service(InputStream is, OutputStream os) throws IOException {
        System.out.println("bbServlet...service");
        os.write("I'm from BBServlet".getBytes());
        os.flush();
    }
    public void destroy() {
        System.out.println("bb...destroy...");
    }
}

具體的servlet的實現類,每個servlet可以看成不同業務,即對不同的請求進行特定的響應。最後給出最重要的程式碼,已經進行很詳細的註釋:

package com.fty.tomcatv2;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;
public class TestServer {

    //獲取resources路徑
    public static String WEB_ROOT = TestServer.class.getClassLoader().
            getResource("./").getPath();
    //請求的內容,域名後的地址
    private static String url = "";
    //定義一個靜態型別的MAP,儲存服務conf.properties中的配置資訊
    private static Map<String, String> map = new HashMap<String, String>();

    static {
        //伺服器啟動之前將配置引數中的資訊載入到MAP中
        //建立一個Properties物件
        Properties prop = new Properties();
        try {
            prop.load(new FileInputStream(WEB_ROOT + "conf.properties"));
            //將配置檔案中的資料讀取到Map中
            Set set = prop.keySet();
            Iterator iter = set.iterator();
            while (iter.hasNext()) {
                String key = (String) iter.next();
                String value = prop.getProperty(key);
                map.put(key, value);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = null;
        Socket socket = null;
        OutputStream outputStream = null;
        InputStream inputStream = null;

        try {
            //建立ServerSocket,監聽本機80埠
            serverSocket = new ServerSocket(8080);
            while (true) {
                //獲取到客戶端對應的socket
                socket = serverSocket.accept();

                //獲取輸入輸出流物件
                outputStream = socket.getOutputStream();
                inputStream = socket.getInputStream();

                //獲取HTTP請求部分並解析請求
                //判斷本次請求是靜態demo.html還是執行在服務端的一段JAVA小程式
                parse(inputStream);
                if(null != url) {
                    if(url.indexOf(".") != -1) {
                        //傳送靜態資原始檔
                        sendStaticResource(outputStream);
                    } else {
                        //傳送動態資源
                        sendDynamicResource(outputStream, inputStream);
                    }
                }
                relaxStream(outputStream, inputStream);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //釋放資源
            relaxStream(outputStream, inputStream);
            if(null != socket) {
                socket.close();
                socket = null;
            }
        }
    }

    private static void relaxStream(OutputStream outputStream, InputStream inputStream) throws IOException {
        if(null != outputStream) {
            outputStream.close();
            outputStream = null;
        }
        if(null != inputStream) {
            inputStream.close();
            inputStream = null;
        }
    }

    private static void sendDynamicResource(OutputStream outputStream, InputStream inputStream) throws Exception {
        //將HTTP協議的響應行和響應頭髮送到客戶端
        outputStream.write("HTTP/1.1 200 OK\n".getBytes());
        outputStream.write("Server:apache-Coyote/1.1\n".getBytes());
        outputStream.write("Content-Type:text/html;charset=utf-8\n".getBytes());
        outputStream.write("\n".getBytes());

        //判斷map中key是否和本次待請求的資源路徑一致
        if(map.containsKey(url)) {
            //如果包含指定的key,獲取到map中key對應的value部分
            String value = map.get(url);
            //通過反射將對應的java程式載入到記憶體
            Class clazz = Class.forName(value);
            //執行init方法
            Servlet servlet = (Servlet) clazz.newInstance();
            //執行service方法
            servlet.init();
            servlet.Service(inputStream, outputStream);
        }
    }

    private static void sendStaticResource(OutputStream outputStream) throws IOException {
        //傳送靜態資源

        //定義一個檔案輸入流,使用者獲取靜態資源demo01.html中的內容
        byte[] bytes = new byte[2048];

        FileInputStream fileInputStream = null;
        try {
            //建立檔案物件File,代表本次要請求的資源demo01.html
            File file = new File(WEB_ROOT, url);
            //如果檔案存在
            if(file.exists()) {
                //向客戶端輸出HTTP協議的響應行/頭
                outputStream.write("HTTP/1.1 200 OK\n".getBytes());
                outputStream.write("Server:apache-Coyote/1.1\n".getBytes());
                outputStream.write("Content-Type:text/html;charset=utf-8\n".getBytes());
                outputStream.write("\n".getBytes());
                //獲取到檔案輸入流物件
                fileInputStream = new FileInputStream(file);
                //讀取靜態資源demo01.html中的內容到陣列中
                int ch = fileInputStream.read(bytes);
                while (ch!=-1) {
                    //將讀取到陣列中的內容通過輸出流傳送到客戶端
                    outputStream.write(bytes, 0, ch);
                    ch = fileInputStream.read(bytes);
                }

            } else {
                //如果檔案不存在
                //向客戶端響應檔案不存在訊息
                outputStream.write("HTTP/1.1 404 not found\n".getBytes());
                outputStream.write("Server:apache-Coyote/1.1\n".getBytes());
                outputStream.write("Content-Type:text/html;charset=utf-8\n".getBytes());
                outputStream.write("\n".getBytes());
                String errorMessage = "file not found";
                outputStream.write(errorMessage.getBytes());
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(null != null) {
                fileInputStream.close();
                fileInputStream = null;
            }
        }

    }

    private static void parse(InputStream inputStream) throws IOException {
        //讀取請求部分,擷取請求資源名稱
        //定義一個變數,存放HTTP協議請求部分的資料
        StringBuffer content = new StringBuffer(2048);
        //定義一個數組,存放HTTP協議請求部分資料
        byte[] buffer = new byte[2048];
        //定義一個變數i,代表讀取資料到陣列中之後,資料量的大小
        int i = -1;
        //讀取客戶端傳送過來的資料,將資料讀取到位元組組buffer中,i表示讀取資料量的大小
        i = inputStream.read(buffer);
        //遍歷位元組陣列,將陣列中的資料追加到content中
        for(int j=0; j<i; j++) {
            content.append((char) buffer[j]);
        }
        //System.out.println(content.toString());
        //擷取客戶端請求的資源路徑
        parseUrl(content.toString());
    }
    //擷取客戶端請求的資源路徑
    private static void parseUrl(String content) {
        //定義2個變數,存放請求後2個空格位置
        int index1, index2;
        index1 = content.indexOf(" ");
        if(index1 != -1) {
            index2 = content.indexOf(" ", index1+1);
            if(index2 > index1) {
                url = content.substring(index1+2, index2);
            }
        }
        System.out.println(url);
    }
}

主要的就是main函式中的邏輯,通過SocketServer繫結一個特定的埠從而可以接收請求,接收請求後判斷該請求是動態還是靜態請求,然後根據請求的內容呼叫特定的html或者java程式碼,得到需要響應的內容,組織成http響應報文,返回瀏覽器即可。