手寫Tomcat(ServerSocket、HTTP協議)
阿新 • • 發佈:2018-12-25
Tomcat本質上就一個請求+響應請求的JAVA程式,當我們從瀏覽器輸入一個url時,我們將傳送請求到Tomcat上,tomcat對該請求進行解析,並將響應的內容返回瀏覽器。
Tomcat通過Socket+HTTP協議進行實現,這裡做了一個簡單的流程圖。
下面簡單介紹下HTTP協議:
HTTP協議
- HTTP協議運行於TCP協議之上,預設埠80,HTTP則是443。
- HTTP協議都是客戶端發起請求,是一個無狀態協議,這次請求和上次沒有對應關係。
- HTTP請求(響應)報文包括:請求行(狀態行)、首部、空行和實體。
請求頭(重點幾個)
- Host:本次請求主機的路徑
- User-Agent:告訴伺服器本次請求客戶端所在的平臺以及本次請求採用的瀏覽器
- Accept:用於指定客戶端接收哪些型別的資訊,*/*都接收
- Accept-Language:語言
- Accept-Encoding:壓縮資料的格式,例如gzip,表示瀏覽器可以識別gzip型別的資料
響應頭(重點幾個)
- Date:響應事件
- content-Type:本次響應內容型別
- content-Encoding:本次內容採用的壓縮格式
- content-length:本次內容長度
動手實現
當請求是靜態請求,即請求一個html介面的時候,我們只需要讀取html檔案的內容,並組織成http響應報文即可。這裡給出兩個html:
而如果是動態的請求,則就是servlet,通過java程式碼返回響應體,這裡我們配置兩個簡單servlet在配置檔案中:
aa=com.fty.tomcatv2.AAServlet
bb=com.fty.tomcatv2.BBServlet
並實現簡單java程式碼:
package com.fty.tomcatv2; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; public interface Servlet {//所有服務端JAVA要實現的介面 //初始化 public void init(); //服務 public void Service(InputStream is, OutputStream os) throws IOException; //銷燬 public void destroy(); }
這是servlet的整體介面,而我們的service就是具體的業務邏輯
package com.fty.tomcatv2;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class AAServlet implements Servlet {
public void init() {
System.out.println("aaServlet...init");
}
public void Service(InputStream is, OutputStream os) throws IOException {
System.out.println("aaServlet...service");
os.write("I'm from AAServlet".getBytes());
os.flush();
}
public void destroy() {
System.out.println("aa...destroy...");
}
}
package com.fty.tomcatv2;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class BBServlet implements Servlet {
public void init() {
System.out.println("bbServlet...init");
}
public void Service(InputStream is, OutputStream os) throws IOException {
System.out.println("bbServlet...service");
os.write("I'm from BBServlet".getBytes());
os.flush();
}
public void destroy() {
System.out.println("bb...destroy...");
}
}
具體的servlet的實現類,每個servlet可以看成不同業務,即對不同的請求進行特定的響應。最後給出最重要的程式碼,已經進行很詳細的註釋:
package com.fty.tomcatv2;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;
public class TestServer {
//獲取resources路徑
public static String WEB_ROOT = TestServer.class.getClassLoader().
getResource("./").getPath();
//請求的內容,域名後的地址
private static String url = "";
//定義一個靜態型別的MAP,儲存服務conf.properties中的配置資訊
private static Map<String, String> map = new HashMap<String, String>();
static {
//伺服器啟動之前將配置引數中的資訊載入到MAP中
//建立一個Properties物件
Properties prop = new Properties();
try {
prop.load(new FileInputStream(WEB_ROOT + "conf.properties"));
//將配置檔案中的資料讀取到Map中
Set set = prop.keySet();
Iterator iter = set.iterator();
while (iter.hasNext()) {
String key = (String) iter.next();
String value = prop.getProperty(key);
map.put(key, value);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = null;
Socket socket = null;
OutputStream outputStream = null;
InputStream inputStream = null;
try {
//建立ServerSocket,監聽本機80埠
serverSocket = new ServerSocket(8080);
while (true) {
//獲取到客戶端對應的socket
socket = serverSocket.accept();
//獲取輸入輸出流物件
outputStream = socket.getOutputStream();
inputStream = socket.getInputStream();
//獲取HTTP請求部分並解析請求
//判斷本次請求是靜態demo.html還是執行在服務端的一段JAVA小程式
parse(inputStream);
if(null != url) {
if(url.indexOf(".") != -1) {
//傳送靜態資原始檔
sendStaticResource(outputStream);
} else {
//傳送動態資源
sendDynamicResource(outputStream, inputStream);
}
}
relaxStream(outputStream, inputStream);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//釋放資源
relaxStream(outputStream, inputStream);
if(null != socket) {
socket.close();
socket = null;
}
}
}
private static void relaxStream(OutputStream outputStream, InputStream inputStream) throws IOException {
if(null != outputStream) {
outputStream.close();
outputStream = null;
}
if(null != inputStream) {
inputStream.close();
inputStream = null;
}
}
private static void sendDynamicResource(OutputStream outputStream, InputStream inputStream) throws Exception {
//將HTTP協議的響應行和響應頭髮送到客戶端
outputStream.write("HTTP/1.1 200 OK\n".getBytes());
outputStream.write("Server:apache-Coyote/1.1\n".getBytes());
outputStream.write("Content-Type:text/html;charset=utf-8\n".getBytes());
outputStream.write("\n".getBytes());
//判斷map中key是否和本次待請求的資源路徑一致
if(map.containsKey(url)) {
//如果包含指定的key,獲取到map中key對應的value部分
String value = map.get(url);
//通過反射將對應的java程式載入到記憶體
Class clazz = Class.forName(value);
//執行init方法
Servlet servlet = (Servlet) clazz.newInstance();
//執行service方法
servlet.init();
servlet.Service(inputStream, outputStream);
}
}
private static void sendStaticResource(OutputStream outputStream) throws IOException {
//傳送靜態資源
//定義一個檔案輸入流,使用者獲取靜態資源demo01.html中的內容
byte[] bytes = new byte[2048];
FileInputStream fileInputStream = null;
try {
//建立檔案物件File,代表本次要請求的資源demo01.html
File file = new File(WEB_ROOT, url);
//如果檔案存在
if(file.exists()) {
//向客戶端輸出HTTP協議的響應行/頭
outputStream.write("HTTP/1.1 200 OK\n".getBytes());
outputStream.write("Server:apache-Coyote/1.1\n".getBytes());
outputStream.write("Content-Type:text/html;charset=utf-8\n".getBytes());
outputStream.write("\n".getBytes());
//獲取到檔案輸入流物件
fileInputStream = new FileInputStream(file);
//讀取靜態資源demo01.html中的內容到陣列中
int ch = fileInputStream.read(bytes);
while (ch!=-1) {
//將讀取到陣列中的內容通過輸出流傳送到客戶端
outputStream.write(bytes, 0, ch);
ch = fileInputStream.read(bytes);
}
} else {
//如果檔案不存在
//向客戶端響應檔案不存在訊息
outputStream.write("HTTP/1.1 404 not found\n".getBytes());
outputStream.write("Server:apache-Coyote/1.1\n".getBytes());
outputStream.write("Content-Type:text/html;charset=utf-8\n".getBytes());
outputStream.write("\n".getBytes());
String errorMessage = "file not found";
outputStream.write(errorMessage.getBytes());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(null != null) {
fileInputStream.close();
fileInputStream = null;
}
}
}
private static void parse(InputStream inputStream) throws IOException {
//讀取請求部分,擷取請求資源名稱
//定義一個變數,存放HTTP協議請求部分的資料
StringBuffer content = new StringBuffer(2048);
//定義一個數組,存放HTTP協議請求部分資料
byte[] buffer = new byte[2048];
//定義一個變數i,代表讀取資料到陣列中之後,資料量的大小
int i = -1;
//讀取客戶端傳送過來的資料,將資料讀取到位元組組buffer中,i表示讀取資料量的大小
i = inputStream.read(buffer);
//遍歷位元組陣列,將陣列中的資料追加到content中
for(int j=0; j<i; j++) {
content.append((char) buffer[j]);
}
//System.out.println(content.toString());
//擷取客戶端請求的資源路徑
parseUrl(content.toString());
}
//擷取客戶端請求的資源路徑
private static void parseUrl(String content) {
//定義2個變數,存放請求後2個空格位置
int index1, index2;
index1 = content.indexOf(" ");
if(index1 != -1) {
index2 = content.indexOf(" ", index1+1);
if(index2 > index1) {
url = content.substring(index1+2, index2);
}
}
System.out.println(url);
}
}
主要的就是main函式中的邏輯,通過SocketServer繫結一個特定的埠從而可以接收請求,接收請求後判斷該請求是動態還是靜態請求,然後根據請求的內容呼叫特定的html或者java程式碼,得到需要響應的內容,組織成http響應報文,返回瀏覽器即可。