使用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()方法方法體的編寫
- 通過輸入流獲得請求的內容
//讀取請求的內容
reader = new BufferedReader(new InputStreamReader(in));
解析獲取的內容,並且放回網站得首頁(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);
給瀏覽器進行響應(用瀏覽器開啟任意一個網站,調出控制檯觀查其響應頭,因此我們的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伺服器軟體編寫成功