手寫Tomcat




學習JavaWeb之后,只知道如何部署項目到Tomcat中,而並不了解其內部如何運行,底層原理為何,因此寫下此篇博客初步探究一下。學習之前需要知識鋪墊已列出:Tomcat目錄結構HTTP協議IO網絡編程(未完善)


1. Tomcat(正版)

筆者稱自己手寫的Tomcat為盜版,反之則為正版。在手寫簡易版Tomcat之前,我們來看看如何使用正版的Tomcat


1.1 創建JavaWeb工程

這里以Myeclipse為例


1.2 新建Servlet

新建MyServlet類繼承HttpServlet,重寫里面的doPost、doGet方法

public class MyServlet extends HttpServlet {
	
	@Override
	public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		resp.getWriter().write("This is serlvet");
	}
	
	@Override
	public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		
	}
}

1.3 配置web.xml

寫完MyServlet之后要讓Tomcat知道他在哪,什么地址映射情況才會啟用他

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
  
  <display-name>tomcat</display-name>
  
  <servlet>
    <servlet-name>MyServlet</servlet-name>
    <servlet-class>com.howl.MyServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>MyServlet</servlet-name>
    <url-pattern>/howl</url-pattern>
  </servlet-mapping>
  
</web-app>

1.4 在WebRoot下放入靜態資源

這里放出項目的目錄結構


1.5 把項目部署到Tomcat(Webapps)

這里使用自帶的Tomcat,一鍵添加部署,再啟動(本項目叫tomcat)


1.6 訪問


至此我們已經能簡單地使用正版Tomcat來訪問靜態資源、Servlet,那么接下來就開始盜版之旅





2. 手寫Tomcat

我們來縷清 瀏覽器發送請求,然后服務器響應瀏覽器 到底經歷了什么


  1. 瀏覽器發出HTTP請求,Tomcat中的Web服務器負責接收解析,並創建請求和響應對象(request、response)
  2. 若無Servlet映射,則可直接訪問解析的資源,把資源封裝到response並返回到Web服務器,Web服務器將信息拆解成HTTP響應返回給瀏覽器顯示
  3. 若有Servlet映射,則去web.xml查詢對應的Servlet路徑,並將請求、響應傳輸給對應的Servlet對象,處理完邏輯后,把信息封裝到response返回給Web服務器拆解,然后響應給瀏覽器顯示
  4. 若既無資源,也無Servlet映射則返回404頁面

上面只是簡易版的流程,並不完全正確,筆者這里為了方便而簡化的流程,具體像Servlet實例化時間,defaultServlet、多層映射這些並未提及


到現在我們可以知道,簡易版的Tomcat設計的對象大概有:

  • 請求(Request)
  • 響應(Response)
  • Servlet總父類(Servlet)
  • 服務器(Server)

2.1 手寫的結構目錄

給出目錄能讓人一目了然,並在下面的閱讀中有個大概



那么就開始Coding把


2.2 Request

負責將瀏覽器的請求信息封裝起來,當然這里是簡單得不能再簡單的封裝了,詳細內容可看代碼里的注釋

public class Request {
	
	// 請求地址
	private String url;
	
	// 請求方法
	private String method;
	
	// 構造函數,參數為后面2.4中Socket建立的IO流
	public Request(InputStream in) throws IOException{
		
		// IO讀取請求
        // 這里踩坑、因為http/1.1是長連接,所以瀏覽器未超時是不會主動關閉的
        // 不能使用循環來讀取數據,因為讀取不了-1(未主動關閉)
		byte[] bytes = new byte[1024];
		int length = in.read(bytes);
		String str = new String(bytes,0,length);
		
		// 取請求的第一行(具體請求信息請看序文中的HTTP知識鋪墊)
		String strFirst = str.split("\n")[0];
		// 按空格分割
		String[] arr = strFirst.split(" ");
        
        // 從第一行中獲取方法名和請求地址
		method = arr[0];
		url = arr[1];
	}

	public String getUrl() {
		return url;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	public String getMethod() {
		return method;
	}

	public void setMethod(String method) {
		this.method = method;
	}
}

2.3 Response

負責封裝響應信息,這里注意響應信息有狀態碼

public class Response {
	
	private OutputStream out;

	public Response(OutputStream out) {
		super();
		this.out = out;
	}
	
    // 該方法前面的步驟都是為了寫好響應頭,最后一句話才是寫入響應內容
	public void write(String content, int statusCode) throws IOException{
		out.write( ("HTTP/1.1 " + statusCode + " OK\n").getBytes() );
		out.write("Content-Type:text/html;Charset=utf-8\n".getBytes());
		out.write("\n".getBytes());
		out.write(content.getBytes("UTF-8"));	// 這里處理編碼問題
	}
}

2.3 Servlet

學習Servlet的時候,我們都是繼承HttpServlet類的,該類實現了Serlvet接口並為我們實現了里面眾多的方法,只需重寫doPost、doGet方法,並且增強了處理Http協議的方法

public abstract class Servlet {
	
	// 類似於HttpSerlvet
	public void service(Request request, Response response) {
        if(request.getMethod().equalsIgnoreCase("POST")) {
            doPost(request, response);
        }else if(request.getMethod().equalsIgnoreCase("GET")) {
            doGet(request, response);
        }
    }
	
	// 分別處理POST和GET請求
	public abstract void doPost(Request request, Response response);
    
    public abstract void doGet(Request request, Response response);
    
}

2.4 Server(重點)

負責加載web.xml,(筆者技術不夠,用properties來代替,即web.properties,鍵為映射地址,值為全限定類名),監聽客戶端的請求並建立連接,還有指派各種請求的訪問

public class Server {
	
	// 資源根目錄
	public static String WEB_ROOT = System.getProperty("user.dir") + "\\WebRoot";
	// 請求的資源地址
	public static String url = "";
	// 讀取web.properties,保存映射關系
	private static HashMap<String,String> map = new HashMap<String,String>();
	
    // 靜態代碼塊,加載時運行一次
	static {
		try {
            // 將映射地址存到map集合中
			Properties prop = new Properties();
			prop.load(new FileInputStream(WEB_ROOT + "\\WEB-INF\\web.properties"));
			Set set = prop.keySet();
			Iterator iterator = set.iterator();
			while(iterator.hasNext()){
				String key= (String) iterator.next();
				String value = prop.getProperty(key);
				map.put(key,value);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	// 開啟服務器
	public void start() {
		try {
			System.out.println("MyTomcat is starting... \n");
			
			// 監聽8080端口
			ServerSocket serverSocket = new ServerSocket(8080);
			// 后期改成NIO,Tomcat默認NIO模式,目前使用BIO (阻塞IO,並不使用多線程了)
			while(true){
				
				// 監聽客戶端連接
				Socket socket = serverSocket.accept();
				
				// 由Tomcat服務器來創建請求響應對象
				InputStream in = socket.getInputStream();
				OutputStream out = socket.getOutputStream();
				Request request = new Request(in);
				System.out.println("請求地址:" + request.getUrl());
                Response response = new Response (out);
                System.out.println("一個請求連接了");
				
                // 分派器
                dispatch(request, response);
				
                // 關閉各種資源
				in.close();
				out.close();
				socket.close();
				System.out.println("一個請求關閉連接了 \n");
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	
	// 負責指派去哪訪問
	private void dispatch(Request request, Response response) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException{
		
		int length = 0;
		byte[] bytes = new byte[1024];
		FileInputStream fileInputStream = null;
		StringBuffer stringBuffer = new StringBuffer();
		
		// 有Servlet映射
		if( map.containsKey(request.getUrl().replace("/", ""))){
			String value = map.get(request.getUrl().replace("/", ""));
			
			// 反射
			Class clazz = Class.forName("com.howl.servlet.LoginServlet");
			Servlet servlet = (Servlet) clazz.newInstance();
			servlet.service(request, response);
			
		// 訪問靜態資源
		}else{
			File file = new File(WEB_ROOT,request.getUrl());
			
			// 靜態資源存在
			if(file.exists()){
				fileInputStream = new FileInputStream(file);
				while(  (length = fileInputStream.read(bytes)) != -1 ){
					stringBuffer.append(new String(bytes,0,length));
				}
				response.write(stringBuffer.toString(),200);
			
			// 靜態資源不存在
			}else{
				file = new File(WEB_ROOT,"/404.html");
				fileInputStream = new FileInputStream(file);
				while(  (length = fileInputStream.read(bytes)) != -1 ){
					stringBuffer.append(new String(bytes,0,length));
				}
				response.write(stringBuffer.toString(),404);
			}
		}
	}
}

2.5 Start

上面的對象都沒有main方法入口,所以筆者這里寫了一個啟動類,就像Tomcat的Start.bat

public class Start {
	
	public static void main(String[] args) {
		
		Server webServer = new Server();
		webServer.start();
		
	}
}

至此我們手寫版的Tomcat已經完成了,下面開始我們盜版Tomcat的使用





3. 手寫版Tomcat的使用

既然我們是模仿正版Tomcat來寫的,那么使用流程也就差不多了


3.1 新建Servlet

繼承我們編寫的Servlet類,寫一個登錄的LoginServlet,當然這里就不做任何邏輯判斷了,直接返回密碼錯誤

public class LoginServlet extends Servlet {

	@Override
	public void doPost(Request request, Response response) {

	}

	@Override
	public void doGet(Request request, Response response) {
		try {
			response.write("訪問了LoginServlet,但是密碼錯誤", 200);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

3.2 在web.properties中寫入配置

# url mapping = class
LoginServlet = com.howl.servlet.LoginServlet

3.3 在WebRoot中放入靜態資源

放入index.html、404.html頁面


3.4 測試


測試通過

  • 總體來說請求和響應的功能完成了,但采用了BIO模式,訪問量並不高,建議后期修改為NIO模式

  • 還需完善請求中獲取參數的功能,這里並沒有寫出

  • 還可以增加Listener、Filter組件




免責聲明!

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



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