Tomcat作為Web服務器深受市場歡迎,有必要對其進行深入的研究。在工作中,我們經常會把寫好的代碼打包放在Tomcat里並啟動,然后在瀏覽器里就能愉快的調用我們寫的代碼來實現相應的功能了,那么Tomcat是如何工作的?
一、Tomcat工作原理
我們啟動Tomcat時雙擊的startup.bat文件的主要作用是找到catalina.bat,並且把參數傳遞給它,而catalina.bat中有這樣一段話:

Bootstrap.class是整個Tomcat 的入口,我們在Tomcat源碼里找到這個類,其中就有我們經常使用的main方法:

這個類有兩個作用 :1.初始化一個守護進程變量、加載類和相應參數。2.解析命令,並執行。
源碼不過多贅述,我們在這里只需要把握整體架構,有興趣的同學可以自己研究下源碼。Tomcat的server.xml配置文件中可以對應構架圖中位置,多層的表示可以配置多個:

即一個由 Server->Service->Engine->Host->Context 組成的結構,從里層向外層分別是:
- Server:服務器Tomcat的頂級元素,它包含了所有東西。
- Service:一組 Engine(引擎) 的集合,包括線程池 Executor 和連接器 Connector 的定義。
- Engine(引擎):一個 Engine代表一個完整的 Servlet 引擎,它接收來自Connector的請求,並決定傳給哪個Host來處理。
- Container(容器):Host、Context、Engine和Wraper都繼承自Container接口,它們都是容器。
- Connector(連接器):將Service和Container連接起來,注冊到一個Service,把來自客戶端的請求轉發到Container。
- Host:即虛擬主機,所謂的”一個虛擬主機”可簡單理解為”一個網站”。
- Context(上下文 ): 即 Web 應用程序,一個 Context 即對於一個 Web 應用程序。Context容器直接管理Servlet的運行,Servlet會被其給包裝成一個StandardWrapper類去運行。Wrapper負責管理一個Servlet的裝載、初始化、執行以及資源回收,它是最底層容器。
比如現在有以下網址,根據“/”切割的鏈接就會定位到具體的處理邏輯上,且每個容器都有過濾功能。

二、梳理自己的Tomcat實現思路
本文實現效果比較簡單,僅供新手參考,大神勿噴。當瀏覽器訪問對應地址時:

實現以上效果整體思路如下:
1.ServerSocket占用8080端口,用while(true)循環等待用戶發請求。
2.拿到瀏覽器的請求,解析並返回URL地址,用I/O輸入流讀取本地磁盤上相應文件。
3.讀取文件,不存在構建響應報文頭、HTML正文內容,存在則寫到瀏覽器端。
三、實現自己的Tomcat
工程文件結構和pom.xml文件:

1.HttpServer核心處理類,用於接受用戶請求,傳遞HTTP請求頭信息,關閉容器:
1 public class HttpServer { 2 // 用於判斷是否需要關閉容器 3 private boolean shutdown = false; 4 5 public void acceptWait() { 6 ServerSocket serverSocket = null; 7 try { 8 //端口號,最大鏈接數,ip地址 9 serverSocket = new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1")); 10 } 11 catch (IOException e) { 12 e.printStackTrace(); 13 System.exit(1); 14 } 15 // 等待用戶發請求 16 while (!shutdown) { 17 try { 18 Socket socket = serverSocket.accept(); 19 InputStream is = socket.getInputStream(); 20 OutputStream os = socket.getOutputStream(); 21 // 接受請求參數 22 Request request = new Request(is); 23 request.parse(); 24 // 創建用於返回瀏覽器的對象 25 Response response = new Response(os); 26 response.setRequest(request); 27 response.sendStaticResource(); 28 //關閉一次請求的socket,因為http請求就是采用短連接的方式 29 socket.close(); 30 //如果請求地址是/shutdown 則關閉容器 31 if(null != request){ 32 shutdown = request.getUrL().equals("/shutdown"); 33 } 34 } 35 catch (Exception e) { 36 e.printStackTrace(); 37 continue; 38 } 39 } 40 } 41 public static void main(String[] args) { 42 HttpServer server = new HttpServer(); 43 server.acceptWait(); 44 } 45 }
2.創建Request類,獲取HTTP的請求頭所有信息並截取URL地址返回:
1 public class Request { 2 private InputStream is; 3 private String url; 4 5 public Request(InputStream input) { 6 this.is = input; 7 } 8 public void parse() { 9 //從socket中讀取一個2048長度字符 10 StringBuffer request = new StringBuffer(Response.BUFFER_SIZE); 11 int i; 12 byte[] buffer = new byte[Response.BUFFER_SIZE]; 13 try { 14 i = is.read(buffer); 15 } 16 catch (IOException e) { 17 e.printStackTrace(); 18 i = -1; 19 } 20 for (int j=0; j<i; j++) { 21 request.append((char) buffer[j]); 22 } 23 //打印讀取的socket中的內容 24 System.out.print(request.toString()); 25 url = parseUrL(request.toString()); 26 } 27 28 private String parseUrL(String requestString) { 29 int index1, index2; 30 index1 = requestString.indexOf(' ');//看socket獲取請求頭是否有值 31 if (index1 != -1) { 32 index2 = requestString.indexOf(' ', index1 + 1); 33 if (index2 > index1) 34 return requestString.substring(index1 + 1, index2); 35 } 36 return null; 37 } 38 39 public String getUrL() { 40 return url; 41 } 42 43 }
3.創建Response類,響應請求讀取文件並寫回到瀏覽器
public class Response { public static final int BUFFER_SIZE = 2048; //瀏覽器訪問D盤的文件 private static final String WEB_ROOT ="D:"; private Request request; private OutputStream output; public Response(OutputStream output) { this.output = output; } public void setRequest(Request request) { this.request = request; } public void sendStaticResource() throws IOException { byte[] bytes = new byte[BUFFER_SIZE]; FileInputStream fis = null; try { //拼接本地目錄和瀏覽器端口號后面的目錄 File file = new File(WEB_ROOT, request.getUrL()); //如果文件存在,且不是個目錄 if (file.exists() && !file.isDirectory()) { fis = new FileInputStream(file); int ch = fis.read(bytes, 0, BUFFER_SIZE); while (ch!=-1) { output.write(bytes, 0, ch); ch = fis.read(bytes, 0, BUFFER_SIZE); } }else { //文件不存在,返回給瀏覽器響應提示,這里可以拼接HTML任何元素 String retMessage = "<h1>"+file.getName()+" file or directory not exists</h1>"; String returnMessage ="HTTP/1.1 404 File Not Found\r\n" + "Content-Type: text/html\r\n" + "Content-Length: "+retMessage.length()+"\r\n" + "\r\n" + retMessage; output.write(returnMessage.getBytes()); } } catch (Exception e) { System.out.println(e.toString() ); } finally { if (fis!=null) fis.close(); } } }
四、讀者可以自己做的優化,擴展的點
1.在WEB_INF文件夾下讀取web.xml解析,通過請求名找到對應的類名,通過類名創建對象,用反射來初始化配置信息,如welcome頁面,Servlet、servlet-mapping,filter,listener,啟動加載級別等。
2.抽象Servlet類來轉碼處理請求和響應的業務。發過來的請求會有很多,也就意味着我們應該會有很多的Servlet,例如:RegisterServlet、LoginServlet等等還有很多其他的訪問。可以用到類似於工廠模式的方法處理,隨時產生很多的Servlet,來滿足不同的功能性的請求。
3.使用多線程技術。本文的代碼是死循環,且只能有一個鏈接,而現實中的情況是往往會有很多很多的客戶端發請求,可以把每個瀏覽器的通信封裝到一個線程當中。
參考文檔:https://my.oschina.net/liughDevelop/blog/1790893
