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請求頭資訊,關閉容器:
public class HttpServer {
// 用于判斷是否需要關閉容器
private boolean shutdown = false;
public void acceptWait() {
ServerSocket serverSocket = null;
try {
//端口号,最大連結數,ip位址
serverSocket = new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
}
catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
// 等待使用者發請求
while (!shutdown) {
try {
Socket socket = serverSocket.accept();
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
// 接受請求參數
Request request = new Request(is);
request.parse();
// 建立用于傳回浏覽器的對象
Response response = new Response(os);
response.setRequest(request);
response.sendStaticResource();
//關閉一次請求的socket,因為http請求就是采用短連接配接的方式
socket.close();
//如果請求位址是/shutdown 則關閉容器
if(null != request){
shutdown = request.getUrL().equals("/shutdown");
}
}
catch (Exception e) {
e.printStackTrace();
continue;
}
}
}
public static void main(String[] args) {
HttpServer server = new HttpServer();
server.acceptWait();
}
}
2.建立Request類,擷取HTTP的請求頭所有資訊并截取URL位址傳回:
public class Request {
private InputStream is;
private String url;
public Request(InputStream input) {
this.is = input;
}
public void parse() {
//從socket中讀取一個2048長度字元
StringBuffer request = new StringBuffer(Response.BUFFER_SIZE);
int i;
byte[] buffer = new byte[Response.BUFFER_SIZE];
try {
i = is.read(buffer);
}
catch (IOException e) {
e.printStackTrace();
i = -1;
}
for (int j=0; j<i; j++) {
request.append((char) buffer[j]);
}
//列印讀取的socket中的内容
System.out.print(request.toString());
url = parseUrL(request.toString());
}
private String parseUrL(String requestString) {
int index1, index2;
index1 = requestString.indexOf(' ');//看socket擷取請求頭是否有值
if (index1 != -1) {
index2 = requestString.indexOf(' ', index1 + 1);
if (index2 > index1)
return requestString.substring(index1 + 1, index2);
}
return null;
}
public String getUrL() {
return url;
}
}
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://github.com/qq53182347/liugh-tomcat