使用java基礎實現一個簡陋的web服務器軟件


使用java基礎實現一個簡陋的web服務器軟件

1、寫在前面

大學已經過了一年半了,從接觸各種web服務器軟件已經有一年多了,從大一上最開始折騰Windows電腦自帶的IIS開始,上手了自己的第一個靜態網站,從此開啟了web方向學習的興趣。到現在,從陪伴了javaweb階段的Tomcat走來,也陸續接觸了jetty,Nginx等web服務器軟件。但是,這些web服務器的軟件也一直都是開箱即用,從未探究過其背后的原理。今天,盡量用最簡單的java代碼,實現一個最簡陋的web服務器軟件,揭開web服務器軟件的神秘面紗。

2、Tomcat的架構模式

由上圖可以看出,Tomcat作為如今相對成熟的web服務器軟件,有着相對較為復雜的架構,有着Server、Service、Engine、Connerctor、Host、Context等諸多組件。對於Tomcat的源碼分析將在以后的博文中分篇講解

,在此不在敘述。本節主要是實現一個自己的web服務器軟件,其架構也超級簡單。

3、編寫一個簡單的web服務器類

3.1、web服務器軟件面向的瀏覽器客戶,因此在同一時間肯定不止有一個http請求,因此肯定需要開啟多線程來進行服務,對類上實現Runnable接口,並重寫其中的run方法。

public class ServerThread implements Runnable {
    @Override
    public void run() {}
}

3.2、在本類中只有兩個方法,其中構造方法用來初始化該web服務器需要的資源,run方法用來處理請求,開啟服務。

3.3、首先,我們先需要定義一堆類級別的變量,如:

  • 瀏覽器發送Http請求時,需要有一個Socket來接受,並且需要或等輸入、輸出流。

        private Socket client;
        private InputStream in;
        private OutputStream out;
    
  • 在Tomcat中,有一個webapp文件夾用來存放靜態資源,在此,我們也在D盤根路徑下定義一個webroot文件夾,用來存儲靜態的資源。(該路徑也可以通過獲取當前j軟件的相對路徑來動態生成,但是為了簡單起見,更好的揭示web服務器的工作流程,在此采用的是絕對路徑)

        private static final String WEBROOT = "D:\\webroot\\";
    

3.4、通過構造函數來初始化全局變量

    /**
     * 構造函數初始化客戶端
     */
    public ServerThread(Socket client) {
        this.client = client;
        //其他初始化信息
        try {
            //獲取客戶端連接的流對象
            in = client.getInputStream();
            out = client.getOutputStream();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

該構造函數相當的簡單,就是獲取瀏覽器發來的Socket,並拿到其中的輸入、輸出流,然后賦值給全局變量。

3.5、run()方法方法體的編寫

  1. 通過輸入流獲得請求的內容
            //讀取請求的內容
            reader = new BufferedReader(new InputStreamReader(in));
  1. 解析獲取的內容,並且放回網站得首頁(index.html)

                //取得:后面得內容
                String line = reader.readLine().split(" ")[1].replace("/","\\");
                if("\\".equals(line)) {
                    line += "index.html";
                }
    
                System.out.println(line);
                //獲取文件的后綴名
                String strType = line.substring(line.lastIndexOf(".")+1, line.length());
                System.out.println("strType = " + strType);
    
  2. 給瀏覽器進行響應(用瀏覽器打開任意一個網站,調出控制台觀查其響應頭,因此我們的web服務器也應該把響應頭給瀏覽器寫出)

所以我們的代碼應該為:

            //給用戶響應
            pw = new PrintWriter(out);
            input = new FileInputStream(WEBROOT + line);

            //BufferedReader buffer = new BufferedReader(new InputStreamReader(input));
            //寫響應頭
            pw.println("HTTP/1.1 200 ok");
            pw.println("Content-Type: "+ contentMap.get(strType)  +";charset=utf-8");
            pw.println("Content-Length: " + input.available());
            pw.println("Server: hello");
            pw.println("Date: " + new Date());
            pw.println();
            pw.flush();

因為放返回數據的類型有多樣,所以我們可以用一個map集合來存儲,並在類加載前將數據存入。

    /**
     * 靜態資源的集合(對應的文本類型)
     */
    private static Map<String,String> contentMap = new HashMap<>();

    //初始化靜態資源的集合
    static {
        contentMap.put("html", "text/html");
        contentMap.put("htm", "text/html");
        contentMap.put("jpg", "image/jpeg");
        contentMap.put("jpeg", "image/jpeg");
        contentMap.put("gif", "image/gif");
        contentMap.put("js", "application/javascript");
        contentMap.put("css", "text/css");
        contentMap.put("json", "application/json");
        contentMap.put("mp3", "audio/mpeg");
        contentMap.put("mp4", "video/mp4");
    }

3.6、向瀏覽器寫回數據,並寫完后進行刷新

            //向瀏覽器寫數據
            byte[] bytes = new byte[1024];
            int len = 0;
            while ((len = input.read(bytes)) != -1){
                out.write(bytes, 0, len);
            }
            pw.flush();

3.7、關閉流、釋放資源

				if(input != null) {
                    input.close();
                }

                if(pw != null) {
                    pw.close();
                }

                if(reader != null) {
                    reader.close();
                }
                if(out != null) {
                    out.close();
                }

                if(client != null) {
                    client.close();

                }

3.8、該類完整的代碼為:

package com.xgp.company;

import java.io.*;
import java.net.Socket;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 服務線程
 * @author 薛國鵬
 */
public class ServerThread implements Runnable {

    /**
     * 靜態資源的集合(對應的文本類型)
     */
    private static Map<String,String> contentMap = new HashMap<>();

    //初始化靜態資源的集合
    static {
        contentMap.put("html", "text/html");
        contentMap.put("htm", "text/html");
        contentMap.put("jpg", "image/jpeg");
        contentMap.put("jpeg", "image/jpeg");
        contentMap.put("gif", "image/gif");
        contentMap.put("js", "application/javascript");
        contentMap.put("css", "text/css");
        contentMap.put("json", "application/json");
        contentMap.put("mp3", "audio/mpeg");
        contentMap.put("mp4", "video/mp4");
    }

    private Socket client;
    private InputStream in;
    private OutputStream out;

    private static final String WEBROOT = "D:\\webroot\\";

    /**
     * 構造函數初始化客戶端
     */
    public ServerThread(Socket client) {
        this.client = client;
        //其他初始化信息
        try {
            //獲取客戶端連接的流對象
            in = client.getInputStream();
            out = client.getOutputStream();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 解析信息,給用戶響應
     */
    @Override
    public void run() {
        PrintWriter pw = null;
        BufferedReader reader = null;
        FileInputStream input = null;
        try {
            //讀取請求的內容
            reader = new BufferedReader(new InputStreamReader(in));

            /**
             * //請求的資源
             * //解析請求頭
             * Host: static.zhihu.com
             * User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0
             * Accept: text/css,
             */
            //取得:后面得內容
            String line = reader.readLine().split(" ")[1].replace("/","\\");
            if("\\".equals(line)) {
                line += "index.html";
            }

            System.out.println(line);
            //獲取文件的后綴名
            String strType = line.substring(line.lastIndexOf(".")+1, line.length());
            System.out.println("strType = " + strType);


            //給用戶響應
            pw = new PrintWriter(out);
            input = new FileInputStream(WEBROOT + line);

            //BufferedReader buffer = new BufferedReader(new InputStreamReader(input));
            //寫響應頭
            pw.println("HTTP/1.1 200 ok");
            pw.println("Content-Type: "+ contentMap.get(strType)  +";charset=utf-8");
            pw.println("Content-Length: " + input.available());
            pw.println("Server: hello");
            pw.println("Date: " + new Date());
            pw.println();
            pw.flush();

            //向瀏覽器寫數據
            byte[] bytes = new byte[1024];
            int len = 0;
            while ((len = input.read(bytes)) != -1){
                out.write(bytes, 0, len);
            }
            pw.flush();

        }catch (Exception e) {
            throw new RuntimeException(e.getMessage() + "服務端的run方法出錯");
        }finally {
            try {
                if(input != null) {
                    input.close();
                }

                if(pw != null) {
                    pw.close();
                }

                if(reader != null) {
                    reader.close();
                }
                if(out != null) {
                    out.close();
                }

                if(client != null) {
                    client.close();

                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
}

4、編寫啟動類

4.1、一般連接性行為會采用池化技術,這里使用一個可以彈性伸縮的線程池。(如果想要跟為專業化,最好是使用一個默認的線程數量的線程池,並且可以讓開發者自行設定)

            //創建一個可伸縮的連接池
            pool = Executors.newCachedThreadPool();

4.2、監聽端口。(這里監聽的是80端口,其實監聽端口的權力應該交給使用者指定)

            //啟動服務器,監聽8080端口
            server = new ServerSocket(80);
            System.out.println("服務器啟動,當前端口為80");

4.3、啟動服務器,處理來自於瀏覽器的請求

while (!Thread.interrupted()){
    //不停接收客戶端請求
    Socket client = server.accept();
    //向線程池中提交任務
    pool.execute(new ServerThread(client));
}

4.4、關閉連接,釋放資源

if(server != null) {
    server.close();
}

if(pool != null) {
    pool.shutdown();
}

4.5、本類完整的代碼為:

package com.xgp.company;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 服務端
 * @author 薛國鵬
 */
public class MyHttpServer {
    public static void main(String[] args) {
        ServerSocket server = null;
        ExecutorService pool = null;
        try {
            //創建一個可伸縮的連接池
            pool = Executors.newCachedThreadPool();
            //啟動服務器,監聽8080端口
            server = new ServerSocket(80);
            System.out.println("服務器啟動,當前端口為80");
            while (!Thread.interrupted()){
                //不停接收客戶端請求
                Socket client = server.accept();
                //向線程池中提交任務
                pool.execute(new ServerThread(client));
            }
        }catch (Exception e) {
            throw new RuntimeException(e.getMessage() + "服務端異常");
        }finally {
            try {
                if(server != null) {
                    server.close();
                }

                if(pool != null) {
                    pool.shutdown();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

5、進行測試

5.1、將測試的靜態文件放在D:\webroot目錄下,如圖是一個使用Vue編寫的一個靜態的前端項目

5.2、啟動自己編寫的web服務器軟件,看到控制台出現了"服務器啟動,當前端口為80"則服務啟動成功

5.3、輸入域名,進行訪問

調出瀏覽器控制台,看請求的資源是否正常解析:

可以看到,頁面正確渲染了,請求的資源也沒有發生問題,因此我們自己編寫的簡陋版本的web服務器軟件編寫成功。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM