前段時間去面試,被人問到了tomcat實現(xiàn)原理。由于平時沒怎么關注容器的實現(xiàn)細節(jié),這個問題基本沒回答上來。所以最近花了很多時間一直在網(wǎng)上找資料和看tomcat的源碼來研究里面處理一個HTTP請求的流程。網(wǎng)上講tomcat的帖子比較多,大多都是直接切入主題看其源碼,從我個人感受來說直接研究其源碼實現(xiàn)比較難理解和非常枯燥,需要由簡到難,慢慢深入。
tomat是一個servlet容器,來處理http請求。在平時的使用中我們都會再瀏覽器中輸入http地址來訪問服務資源,比如格式http://host[":"port][abs_path]。從瀏覽器到服務端的一次請求都遵循h(huán)ttp協(xié)議,在網(wǎng)絡上其實走仍然是tcp協(xié)議,即我們常使用的socket來處理客戶端和服務器的交互。根據(jù)輸入的http地址可以知道服務器的ip地址和端口,根據(jù)這兩個參數(shù)就可以定位到服務器的唯一地址。tomcat根據(jù)http地址端口后面的資源路徑就可以知道反饋什么樣的資源給瀏覽器。下面給出了一個非常簡單的代碼模擬了tomcat的簡單實現(xiàn)
package com;import java.io.*;import java.net.ServerSocket;import java.net.Socket;import java.net.URLDecoder;import java.util.StringTokenizer;public class TomcatServer { PRivate final static int PORT = 8080; public static void main(String[] args) { try { ServerSocket server = new ServerSocket(PORT);//根據(jù)端口號啟動一個serverSocket ServletHandler servletHandler=new ServletHandler(server); servletHandler.start(); } catch (Exception e) { e.printStackTrace(); } } private static class ServletHandler extends Thread{ ServerSocket server=null; public ServletHandler(ServerSocket server){ this.server=server; } @Override public void run() { while (true) { try { Socket client = null; client = server.accept();//ServerSocket阻塞等待客戶端請求數(shù)據(jù) if (client != null) { try { System.out.println("接收到一個客戶端的請求"); //根據(jù)客戶端的Socket對象獲取輸入流對象。 //封裝字節(jié)流到字符流 BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream())); // GET /test.jpg /HTTP1.1 //http請求由三部分組成,分別是:請求行、消息報頭、請求正文。 //這里取的第一行數(shù)據(jù)就是請求行。http協(xié)議詳解可以參考http://www.cnblogs.com/li0803/archive/2008/11/03/1324746.html說的很詳細 String line = reader.readLine(); System.out.println("line: " + line); //拆分http請求路徑,取http需要請求的資源完整路徑 String resource = line.substring(line.indexOf('/'),line.lastIndexOf('/') - 5); System.out.println("the resource you request is: "+ resource); resource = URLDecoder.decode(resource, "UTF-8"); //獲取到這次請求的方法類型,比如get或post請求 String method = new StringTokenizer(line).nextElement().toString(); System.out.println("the request method you send is: "+ method); //繼續(xù)循環(huán)讀取瀏覽器客戶端發(fā)出的一行一行的數(shù)據(jù) while ((line = reader.readLine()) != null) { if (line.equals("")) {//當line等于空行的時候標志Header消息結(jié)束 break; } System.out.println("the Http Header is : " + line); } //如果是POST的請求,直接打印POST提交上來的數(shù)據(jù) if ("post".equals(method.toLowerCase())) { System.out.println("the post request body is: " + reader.readLine()); }else if("get".equals(method.toLowerCase())){ //判斷是get類型的http請求處理 //根據(jù)http請求的資源后綴名來確定返回數(shù)據(jù) //比如下載一個圖片文件,我這里直接給定一個圖片路徑來模擬下載的情況 if (resource.endsWith(".jpg")) { transferFileHandle("d://123.jpg", client); closeSocket(client); continue; } else { //直接返回一個網(wǎng)頁數(shù)據(jù) //其實就是將html的代碼以字節(jié)流的形式寫到IO中反饋給客戶端瀏覽器。 //瀏覽器會根據(jù)http報文“Content-Type”來知道反饋給瀏覽器的數(shù)據(jù)是什么格式的,并進行什么樣的處理 PrintStream writer = new PrintStream(client.getOutputStream(), true); writer.println("HTTP/1.0 200 OK");// 返回應答消息,并結(jié)束應答 writer.println("Content-Type:text/html;charset=utf-8"); writer.println(); //writer.println("Content-Length:" + html.getBytes().length);// 返回內(nèi)容字節(jié)數(shù) writer.println("<html><body>"); writer.println("<a href='www.baidu.com'>百度</a>"); writer.println("<img src='https://ss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/logo/bd_logo1_31bdc765.png'></img>"); writer.println("</html></body>"); //writer.println("HTTP/1.0 404 Not found");// 返回應答消息,并結(jié)束應答 writer.println();// 根據(jù) HTTP 協(xié)議, 空行將結(jié)束頭信息 writer.close(); closeSocket(client);//請求資源處理完畢,關閉socket鏈接 continue; } } } catch (Exception e) { System.out.println("HTTP服務器錯誤:" + e.getLocalizedMessage()); } } } catch (Exception e) { e.printStackTrace(); } } } private void closeSocket(Socket socket) { try { socket.close(); } catch (IOException ex) { ex.printStackTrace(); } System.out.println(socket + "離開了HTTP服務器"); } private void transferFileHandle(String path, Socket client) { File fileToSend = new File(path); if (fileToSend.exists() && !fileToSend.isDirectory()) { try { //根據(jù)Socket獲取輸出流對象,將訪問的資源數(shù)據(jù)寫入到輸出流中 PrintStream writer = new PrintStream(client.getOutputStream()); writer.println("HTTP/1.0 200 OK");// 返回應答消息,并結(jié)束應答 writer.println("Content-Type:application/binary"); writer.println("Content-Length:" + fileToSend.length());// 返回內(nèi)容字節(jié)數(shù) writer.println();// 根據(jù) HTTP 協(xié)議, 空行將結(jié)束頭信息 FileInputStream fis = new FileInputStream(fileToSend); byte[] buf = new byte[fis.available()]; fis.read(buf); writer.write(buf); writer.close(); fis.close(); } catch (IOException e) { e.printStackTrace(); } } } }}三 實踐
1.在瀏覽器中輸入http://localhost:8080/123.jpg 鏈接,可以看到瀏覽器里面就將123.jpg下載到本地了。
2.在瀏覽器中輸入一個服務器不能識別的請求后綴比如http://localhost:8080/123.jpg1,可以看到瀏覽器打開了一個網(wǎng)頁。如下圖:點擊里面的百度鏈接可以跳轉(zhuǎn)
3.后臺tomcat服務器打印的http請求報文
接收到一個客戶端的請求line: GET /123.jpg1 HTTP/1.1the resource you request is: /123.jpg1the request method you send is: GETthe Http Header is : Host: localhost:8080the Http Header is : Connection: keep-alivethe Http Header is : Pragma: no-cachethe Http Header is : Cache-Control: no-cachethe Http Header is : Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8the Http Header is : Upgrade-Insecure-Requests: 1the Http Header is : User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36the Http Header is : Accept-Encoding: gzip, deflate, sdchthe Http Header is : Accept-Language: zh-CN,zh;q=0.8Socket[addr=/0:0:0:0:0:0:0:1,port=57864,localport=8080]離開了HTTP服務器
四 總結(jié)
從整個代碼和測試情況來看,一次http請求其實就是一次socket套接字的處理。瀏覽器發(fā)起scoket的請求,tomcat服務器接受請求,并根據(jù)請求的路徑定位客戶端需要訪問的資源。 只是socket客戶端和服務器數(shù)據(jù)在交互時,都遵守著http協(xié)議規(guī)范。當然真正的tomcat容器比這個demo實現(xiàn)要復雜的很多,這個簡易的tomcat服務器能夠幫我們更好的理解tomcat源碼。
新聞熱點
疑難解答