經過前面兩篇的分析,我們對Tomcat的系統架構與原理有了一定的認識與理解了,回顧請戳:
我的架構夢:(十四)Tomcat 系統架構與原理剖析
我的架構夢:(十五)Tomcat 服務器核心配置詳解
一、需求分析
名稱:tomcat-customize
tomcat-customize
要做的事情:作爲一個服務器軟件提供服務的,也即我們可以通過瀏覽器客戶端發送http
請求, tomcat-customize
可以接收到請求進行處理,處理之後的結果可以返回瀏覽器客戶端。
1、提供服務,接收請求(Socket
通信);
2、請求信息封裝成Request
對象(Response
對象);
3、客戶端請求資源,資源分爲靜態資源(html
)和動態資源(Servlet
);
4、資源返回給客戶端瀏覽器。
我們遞進式完成以上需求,提出V1.0、V2.0、V3.0、V4.0、V5.0版本的需求。
V1.0
需求:瀏覽器請求http://localhost:8080
,返回一個固定的字符串到⻚面Hello Tomcat Customize!
;V2.0
需求:封裝Request
和Response
對象,返回html
靜態資源文件;V3.0
需求:可以請求動態資源(Servlet
);V4.0
需求:多線程改造(其他請求不受請求阻塞影響);V5.0
需求:線程池改造(高併發場景);
完成上述五個版本後,我們的代碼如下:
二、代碼詳情
1、Bootstrap 啓動類
/**
* 自定義Tomcat的主類
*/
@Data
public class Bootstrap {
/**
* 定義socket監聽的端口號
*/
private int port = 8080;
/**
* 自定義Tomcat啓動需要初始化展開的一些操作
*/
public void start() throws Exception {
// 加載解析相關的配置,web.xml
loadServlet();
// 定義一個線程池
int corePoolSize = 10;
int maximumPoolSize =50;
long keepAliveTime = 100L;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(50);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
/**
* 自定義Tomcat 1.0版本
* 需求:瀏覽器請求http://localhost:8080,返回一個固定的字符串到頁面"Hello Tomcat Customize!"
*/
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("Tomcat Customize start on port: " + port);
/*while (true) {
Socket socket = serverSocket.accept();
// 有了socket,接收到請求,獲取輸出流
OutputStream outputStream = socket.getOutputStream();
String data = "Hello Tomcat Customize!";
String responseText = HttpProtocolUtil.getHttpHeader200(data.getBytes().length) + data;
outputStream.write(responseText.getBytes());
socket.close();
}*/
/**
* 自定義Tomcat 2.0版本
* 需求:封裝Request和Response對象,返回html靜態資源文件
*/
/*while (true) {
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
// 封裝Request對象和Response對象
Request request = new Request(inputStream);
Response response = new Response(socket.getOutputStream());
response.outputHtml(request.getUrl());
socket.close();
}*/
/**
* 自定義Tomcat 3.0版本
* 需求:可以請求動態資源(Servlet)
*/
/*while (true) {
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
// 封裝Request對象和Response對象
Request request = new Request(inputStream);
Response response = new Response(socket.getOutputStream());
HttpServlet httpServlet = servletMap.get(request.getUrl());
// 靜態資源處理
if (httpServlet == null) {
response.outputHtml(request.getUrl());
} else {
// 動態資源servlet請求
httpServlet.service(request, response);
}
socket.close();
}*/
/**
* 自定義Tomcat 4.0版本
* 需求:多線程改造(其他請求不受請求阻塞影響)
*/
/*while (true) {
Socket socket = serverSocket.accept();
RequestProcessor requestProcessor = new RequestProcessor(socket, servletMap);
requestProcessor.start();
}*/
/**
* 自定義Tomcat 5.0版本
* 需求:線程池改造
*/
while (true) {
Socket socket = serverSocket.accept();
RequestProcessor requestProcessor = new RequestProcessor(socket, servletMap);
threadPoolExecutor.execute(requestProcessor);
}
}
private Map<String, HttpServlet> servletMap = new HashMap<>();
/**
* 加載解析web.xml,初始化Servlet
*/
private void loadServlet() {
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("web.xml");
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(resourceAsStream);
Element rootElement = document.getRootElement();
List<Element> selectNodes = rootElement.selectNodes("//servlet");
for (int i = 0; i < selectNodes.size(); i++) {
Element element = selectNodes.get(i);
// <servlet-name>riemann</servlet-name>
Element servletnameElement = (Element) element.selectSingleNode("servlet-name");
String servletName = servletnameElement.getStringValue();
// <servlet-class>com.riemann.server.servlet.RiemannServlet</servlet-class>
Element servletclassElement = (Element) element.selectSingleNode("servlet-class");
String servletClass = servletclassElement.getStringValue();
// 根據servlet-name的值找到url-pattern
Element servletMapping = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
// /riemann
String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 自定義Tomcat的程序啓動入口
* @param args
*/
public static void main(String[] args) {
Bootstrap bootstrap = new Bootstrap();
try {
// 啓動自定義Tomcat
bootstrap.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
2、Http協議工具類
/**
* http協議工具類,主要是提供響應頭信息,這裏我們只提供200和404的情況
*/
public class HttpProtocolUtil {
/**
* 爲響應碼200提供請求頭信息
* @param contentLength
* @return
*/
public static String getHttpHeader200(long contentLength) {
return "HTTP/1.1 200 OK \n" +
"Content-Type: text/html \n" +
"Content-Length: " + contentLength + " \n" +
"\r\n";
}
/**
* 爲響應碼404提供請求頭信息(此處也包含了數據內容)
* @return
*/
public static String getHttpHeader404() {
String str404 = "<h1>404 not found</h1>";
return "HTTP/1.1 404 NOT Found \n" +
"Content-Type: text/html \n" +
"Content-Length: " + str404.getBytes().length + " \n" +
"\r\n" + str404;
}
}
3、Request、 Response封裝類
/**
* 把請求信息封裝爲Request對象(根據InputSteam輸入流封裝)
*/
@Data
public class Request {
private String method; // 請求方式,比如GET/POST
private String url; // 例如 /,/index.html
private InputStream inputStream; // 輸入流,其他屬性從輸入流中解析出來
/**
* 構造器,輸入流傳入
* @param inputStream
*/
public Request(InputStream inputStream) throws IOException {
this.inputStream = inputStream;
// 從輸入流中獲取請求信息
// 從輸入流中獲取請求信息
int count = 0;
while (count == 0) {
count = inputStream.available();
}
byte[] bytes = new byte[count];
inputStream.read(bytes);
String inputStr = new String(bytes);
// 獲取第一行請求頭信息
String firstLine = inputStr.split("\\n")[0]; // GET / HTTP/1.1
String[] strings = firstLine.split(" ");
this.method = strings[0];
this.url = strings[1];
System.out.println("method: " + method);
System.out.println("url: " + url);
}
}
/**
* 封裝Response對象,需要依賴於OutputStream
*
* 該對象需要提供核心方法,輸出html
*/
@Data
@AllArgsConstructor
public class Response {
private OutputStream outputStream;
/**
* 使用輸出流輸出指定字符串
* @param content
* @throws IOException
*/
public void output(String content) throws IOException {
outputStream.write(content.getBytes());
}
/**
* 輸出html格式
* @param path url 隨後要根據url來獲取到靜態資源的絕對路徑,進一步根據絕對路徑讀取該靜態資源文件,最終通過輸出流輸出。
* / -----> classes
* @throws IOException
*/
public void outputHtml(String path) throws IOException {
// 獲取靜態資源文件的絕對路徑
String absoluteResourcePath = StaticResourceUtil.getAbsolutePath(path);
// 輸入靜態資源文件
File file = new File(absoluteResourcePath);
if (file.exists() && file.isFile()) {
// 讀取靜態資源文件,輸出靜態資源
StaticResourceUtil.outputStaticResource(new FileInputStream(file), outputStream);
} else {
// 輸出404
output(HttpProtocolUtil.getHttpHeader404());
}
}
}
4、靜態資源請求處理工具類
/**
* 靜態資源工具類
*/
public class StaticResourceUtil {
/**
* 獲取靜態資源文件的絕對路徑
* @param path
* @return
*/
public static String getAbsolutePath(String path) {
String absolutePath = StaticResourceUtil.class.getResource("/").getPath();
return absolutePath.replaceAll("\\\\", "/") + path;
}
/**
* 讀取靜態資源文件輸入流,通過輸出流輸出
* @param inputStream
* @param outputStream
* @throws IOException
*/
public static void outputStaticResource(InputStream inputStream, OutputStream outputStream) throws IOException {
int count = 0;
while (count == 0) {
count = inputStream.available();
}
int resourceSize = count;
// 輸出http請求頭,然後再輸出具體內容
outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes());
// 讀取內容輸出
long written = 0 ; // 已經讀取的內容長度
int byteSize = 1024; // 計劃每次緩衝的長度
byte[] bytes = new byte[byteSize];
while (written < resourceSize) {
if (written + byteSize > resourceSize) { // 說明剩餘未讀取大小不足一個1024長度,那就按真實長度處理
byteSize = (int) (resourceSize - written); // 剩餘的文件內容長度
bytes = new byte[byteSize];
}
inputStream.read(bytes);
outputStream.write(bytes);
outputStream.flush();
written += byteSize;
}
}
}
5、動態資源請求
5.1 Servlet接口定義
public interface Servlet {
void init() throws Exception;
void destory() throws Exception;
void service(Request request, Response response) throws Exception;
}
5.2 HttpServlet抽象類定義
public abstract class HttpServlet implements Servlet {
public abstract void doGet(Request request, Response response);
public abstract void doPost(Request request, Response response);
@Override
public void service(Request request, Response response) throws Exception {
if ("GET".equalsIgnoreCase(request.getMethod())) {
doGet(request, response);
} else {
doPost(request, response);
}
}
}
5.3 業務類Servlet定義RiemannServlet
public class RiemannServlet extends HttpServlet {
@Override
public void doGet(Request request, Response response) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String content = "<h1>RiemannServlet get</h1>";
try {
response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void doPost(Request request, Response response) {
String content = "<h1>RiemannServlet post</h1>";
try {
response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void init() throws Exception {
}
@Override
public void destory() throws Exception {
}
}
6、多線程改造封裝的RequestProcessor類
@Data
@AllArgsConstructor
public class RequestProcessor extends Thread {
private Socket socket;
private Map<String, HttpServlet> servletMap;
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
// 封裝Request對象和Response對象
Request request = new Request(inputStream);
Response response = new Response(socket.getOutputStream());
HttpServlet httpServlet = servletMap.get(request.getUrl());
// 靜態資源處理
if (httpServlet == null) {
response.outputHtml(request.getUrl());
} else {
// 動態資源servlet請求
httpServlet.service(request, response);
}
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
三、測試結果
1、訪問靜態資源
2、訪問動態資源
3、找不到資源錯誤頁面訪問
4、多線程訪問,其中演示動態資源阻塞,不影響其他資源的訪問。
動態資源由於給了休眠10秒,訪問的時候請求被阻塞。
但是不影響同時訪問其他資源,這裏是靜態資源。