該項(xiàng)目源碼地址:https://github.com/lastwhispers/mimic/tree/master/tomcat-mini
1. 項(xiàng)目簡介
一個(gè)極簡的tomcat。使用java實(shí)現(xiàn)最基本的tomcat的功能,能夠接收Http請(qǐng)求、處理Http請(qǐng)求(提供Servlet接口)、響應(yīng)Http請(qǐng)求。
項(xiàng)目結(jié)構(gòu):

項(xiàng)目演示:

2. 涉及的技術(shù)
- I/O
- 網(wǎng)絡(luò)編程
- XML解析 http://m.itdecent.cn/p/8df626ea70ed
- 反射
- HTTP協(xié)議 https://www.runoob.com/http/http-messages.html
3. 各部分設(shè)計(jì)
3.1 servlet接口設(shè)計(jì)
/**
* @author lastwhisper
* @desc 服務(wù)器小腳本接口
*/
public abstract class Servlet {
/**
*
* @author lastwhisper
* @desc 處理GET請(qǐng)求
* @param request
* @param response
*/
public abstract void doGet(Request request, Response response);
/**
*
* @author lastwhisper
* @desc 處理POST請(qǐng)求
* @param request
* @param response
*/
public abstract void doPost(Request request, Response response);
/**
*
* @author lastwhisper
* @desc 處理請(qǐng)求
* @param request
* @param response
*/
public void service(Request request, Response response) {
if ("GET".equalsIgnoreCase(request.getMethod())) {
doGet(request, response);
} else if ("POST".equalsIgnoreCase(request.getMethod())) {
doPost(request, response);
}
}
}
3.2 解析web.xml、反射創(chuàng)建對(duì)象
使用過servlet的朋友都知道,tomcat通過web.xml的配置找到url與對(duì)應(yīng)的servlet全名,然后通過反射創(chuàng)建servlet。
比如有如下web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<servlet>
<servlet-name>login</servlet-name>
<servlet-class>cn.lastwhisper.server.basic.servlet.LoginServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>reg</servlet-name>
<servlet-class>cn.lastwhisper.server.basic.servlet.RegisterServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>login</servlet-name>
<url-pattern>/login</url-pattern>
<url-pattern>/g</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>reg</servlet-name>
<url-pattern>/reg</url-pattern>
</servlet-mapping>
</web-app>
使用java解析xml,并通過反射創(chuàng)建對(duì)象。
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
*
* @author lastwhisper
*
*/
public class XmlConvertion {
public static void main(String[] args) throws Exception{
// SAX解析
// 1、獲取解析工廠
SAXParserFactory factory = SAXParserFactory.newInstance();
// 2、從解析工廠獲取解析器
SAXParser parse = factory.newSAXParser();
// 3、編寫處理器
// 4、加載文檔 Document 注冊(cè)處理器
WebHandler handler = new WebHandler();
// 5、解析
parse.parse(Thread.currentThread().getContextClassLoader()
.getResourceAsStream("cn/lastwhisper/server/basic/servlet/web.xml"), handler);
// 獲取數(shù)據(jù)
WebContext context = new WebContext(handler.getEntitys(), handler.getMappings());
// 假設(shè)你輸入了 /login
String className = context.getClazz("/g");
Class clz = Class.forName(className);
Servlet servlet = (Servlet) clz.getConstructor().newInstance();
servlet.service();
}
}
class WebHandler extends DefaultHandler {
private List<Entity> entitys;
private List<Mapping> mappings;
private Entity entity;
private Mapping mapping;
private String tag; // 存儲(chǔ)操作標(biāo)簽
private boolean isMapping = false;
@Override
public void startDocument() throws SAXException {
entitys = new ArrayList<Entity>();
mappings = new ArrayList<Mapping>();
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
tag = qName;
if ("servlet".equals(tag)) {
entity = new Entity();
isMapping = false;
} else if ("servlet-mapping".equals(tag)) {
mapping = new Mapping();
isMapping = true;
}
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
String contents = new String(ch, start, length).trim();
if (!isMapping) { // 操作servlet
if ("servlet-name".equals(tag)) {
entity.setName(contents);
} else if ("servlet-class".equals(tag)) {
entity.setClazz(contents);
}
} else { // 操作servlet-mapping
if ("servlet-name".equals(tag)) {
mapping.setName(contents);
} else if ("url-pattern".equals(tag)) {
mapping.addPatterns(contents);
}
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if ("servlet".equals(qName)) {
entitys.add(entity);
} else if ("servlet-mapping".equals(qName)) {
mappings.add(mapping);
}
tag = null; // tag丟棄了
}
@Override
public void endDocument() throws SAXException {
}
public List<Entity> getEntitys() {
return entitys;
}
public List<Mapping> getMappings() {
return mappings;
}
}
3.3 接收Http請(qǐng)求
接收Http請(qǐng)求的步驟:
- 創(chuàng)建serversocket
- 建立連接獲取socket
- 通過輸入流獲取請(qǐng)求協(xié)議
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author lastwhisper
* @desc 使用ServerSocket建立與瀏覽器的連接,獲取請(qǐng)求協(xié)議
*/
public class Server01 {
private ServerSocket serverSocket;
public static void main(String[] args) {
Server01 server = new Server01();
server.start();
}
// 啟動(dòng)服務(wù)
public void start() {
try {
serverSocket = new ServerSocket(8888);
revice();
} catch (IOException e) {
e.printStackTrace();
System.out.println("服務(wù)器啟動(dòng)失敗....");
}
}
// 接受連接處理
public void revice() {
try {
// 接受連接
Socket socket = serverSocket.accept();
// 獲取請(qǐng)求協(xié)議
InputStream inputStream = socket.getInputStream();
byte[] datas = new byte[1024 * 1024];
int len = inputStream.read(datas);
String request = new String(datas, 0, len);
System.out.println(request);
} catch (IOException e) {
e.printStackTrace();
System.out.println("客戶端錯(cuò)誤");
}
}
// 停止服務(wù)
public void stop() {
}
}
運(yùn)行main方法,使用瀏覽器訪問 http://localhost:8888/ 可以看到控制臺(tái)輸出了瀏覽器發(fā)起的Http請(qǐng)求
GET / HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Purpose: prefetch
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
Cookie: Hm_lvt_8875c662941dbf07e39c556c8d97615f=1554520109,1555680733
3.4 響應(yīng)Http請(qǐng)求
響應(yīng)Http請(qǐng)求的步驟:
- 準(zhǔn)備響應(yīng)內(nèi)容
- 獲取字節(jié)數(shù)的長度
- 拼接響應(yīng)協(xié)議(注意空格與換行)
- 使用輸入輸出流
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
/**
* @author lastwhisper
* @desc 返回響應(yīng)協(xié)議
*/
public class Server02 {
private ServerSocket serverSocket;
public static void main(String[] args) {
Server02 server = new Server02();
server.start();
}
// 啟動(dòng)服務(wù)
public void start() {
try {
serverSocket = new ServerSocket(8888);
revice();
} catch (IOException e) {
e.printStackTrace();
System.out.println("服務(wù)器啟動(dòng)失敗....");
}
}
// 接受連接請(qǐng)求,響應(yīng)請(qǐng)求
public void revice() {
try {
// 1. 接受連接請(qǐng)求
Socket client = serverSocket.accept();
// 2. 獲取http請(qǐng)求內(nèi)容
InputStream inputStream = client.getInputStream();
byte[] datas = new byte[1024 * 1024];
int len = inputStream.read(datas);
String request = new String(datas, 0, len);
System.out.println(request);
// 3. 構(gòu)造響應(yīng)請(qǐng)求
StringBuilder content = new StringBuilder();
content.append("<html>");
content.append("<head>");
content.append("<title>");
content.append("服務(wù)器響應(yīng)成功");
content.append("</title>");
content.append("</head>");
content.append("<body>");
content.append("tomcat server終于回來了。。。。");
content.append("</body>");
content.append("</html>");
int size = content.toString().getBytes().length; // 必須獲取字節(jié)長度
StringBuilder response = new StringBuilder();
String blank = " ";
String CRLF = "\r\n";
// 3.1 響應(yīng)行: HTTP/1.1 200 OK
response.append("HTTP/1.1").append(blank);
response.append(200).append(blank);
response.append("OK").append(CRLF);
// 3.2 響應(yīng)頭(最后一行存在空行):
/*
* Date:Mon,31Dec209904:25:57GMT
* Server:tomcat Server/0.0.1;charset=GBK
* Content-type:text/html
* Content-length:39725426
*/
response.append("Date:").append(new Date()).append(CRLF);
response.append("Server:").append("tomcat Server/0.0.1;charset=GBK").append(CRLF);
response.append("Content-type:text/html").append(CRLF);
response.append("Content-length:").append(size).append(CRLF);
response.append(CRLF);
// 3.3 正文
response.append(content.toString());
// 4. 響應(yīng)到到客戶端
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
bw.write(response.toString());
bw.flush();
} catch (IOException e) {
e.printStackTrace();
System.out.println("客戶端錯(cuò)誤");
}
}
// 停止服務(wù)
public void stop() {
}
}
運(yùn)行main方法,在瀏覽器訪問 http://localhost:8888/ ,即可看到自己構(gòu)造的響應(yīng)。
3.5 封裝響應(yīng)Response
由于每次構(gòu)造的響應(yīng)只有響應(yīng)內(nèi)容和響應(yīng)狀態(tài)碼是不固定的,所以可用封裝響應(yīng)。
- 動(dòng)態(tài)添加內(nèi)容
- 根據(jù)狀態(tài)碼拼接響應(yīng)頭協(xié)議
- 根據(jù)狀態(tài)碼響應(yīng)請(qǐng)求
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Date;
/**
* @author lastwhisper
* @desc 封裝響應(yīng)實(shí)體
*/
public class Response {
private BufferedWriter bw;
// 響應(yīng)正文
private StringBuilder content;
// 協(xié)議頭(響應(yīng)行、響應(yīng)頭、回車)信息
private StringBuilder headInfo;
// 正文的字節(jié)數(shù)
private int len;
private final String BLANK = " ";
private final String CRLF = "\r\n";
private Response() {
content = new StringBuilder();
headInfo = new StringBuilder();
len = 0;
}
public Response(Socket client) throws IOException {
this(client.getOutputStream());
}
public Response(OutputStream os) {
this();
bw = new BufferedWriter(new OutputStreamWriter(os));
}
// 動(dòng)態(tài)添加內(nèi)容(無換行)
public Response print(String info) {
content.append(info);
len += info.getBytes().length;
return this;
}
// 動(dòng)態(tài)添加內(nèi)容(有換行)
public Response println(String info) {
content.append(info).append(CRLF);
len += (info + CRLF).getBytes().length;
return this;
}
// 構(gòu)建響應(yīng)行、響應(yīng)頭信息
private void createHeadInfo(int code) {
// 1.響應(yīng)行 HTTP/1.1 200 OK
headInfo.append("HTTP/1.1").append(BLANK);
headInfo.append(code).append(BLANK);
switch (code) {
case 200:
headInfo.append("OK").append(CRLF);
break;
case 404:
headInfo.append("NOT FOUND").append(CRLF);
break;
case 505:
headInfo.append("SERVER ERROR").append(CRLF);
break;
}
// 2.響應(yīng)頭(最后一行存在空行)
/*
* Date:Mon,31Dec209904:25:57GMT
* Server:tomcat Server/0.0.1;charset=GBK
* Content-type:text/html
* Content-length:39725426
*/
headInfo.append("Date:").append(new Date()).append(CRLF);
headInfo.append("Server:").append("tomcat Server/0.0.1;charset=GBK").append(CRLF);
headInfo.append("Content-type:text/html").append(CRLF);
headInfo.append("Content-length:").append(len).append(CRLF);
headInfo.append(CRLF);
}
// 推送響應(yīng)信息
public void pushToBrowser(int code) throws IOException {
if (null == headInfo) {
code = 505;
}
createHeadInfo(code);
bw.append(headInfo);
bw.append(content);
bw.flush();
}
}
3.6 封裝請(qǐng)求request
http的請(qǐng)求消息如下:
GET /hello HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi
我們需要將請(qǐng)求類型、url、以及請(qǐng)求參數(shù)提取出來,用來映射到servlet上。
- 通過分解字符串獲取method、url、get參數(shù)、post請(qǐng)求體
- 通過Map封裝請(qǐng)求參數(shù)
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author lastwhisper
* @desc 封裝請(qǐng)求協(xié)議: 獲取 method uri以及請(qǐng)求參數(shù)
*/
public class Request2 {
// http請(qǐng)求信息
private String requestInfo;
// 請(qǐng)求方式
private String method;
// 請(qǐng)求url
private String url;
// 請(qǐng)求參數(shù)字符串username=zhangsan&pwd=123456
private String queryParamStr;
// 存儲(chǔ)請(qǐng)求參數(shù)
private Map<String, List<String>> parameterMap;
// 回車
private final String CRLF = "\r\n";
public Request2(Socket client) throws IOException {
this(client.getInputStream());
}
public Request2(InputStream is) {
parameterMap = new HashMap<String, List<String>>();
byte[] datas = new byte[1024 * 1024];
int len;
try {
len = is.read(datas);
requestInfo = new String(datas, 0, len);
} catch (IOException e) {
e.printStackTrace();
return;
}
// 分解字符串
parseRequestInfo();
// 轉(zhuǎn)成Map uname=張三&hobby=籃球&hobby=讀書&other=
convertMap();
}
/**
*
* @author lastwhisper
* @desc 解析http請(qǐng)求信息
*/
private void parseRequestInfo() {
// 1. 獲取請(qǐng)求方式: 開頭到第一個(gè)/
method = requestInfo.substring(0, requestInfo.indexOf("/")).toLowerCase().trim();
// 2. 獲取請(qǐng)求url: 第一個(gè)/ 到 HTTP/;獲取get參數(shù)
// 2.1 獲取/的位置
int startIdx = requestInfo.indexOf("/") + 1;
// 2.2 獲取 HTTP/的位置
int endIdx = requestInfo.indexOf("HTTP/");
// 2.3 分割字符串
url = requestInfo.substring(startIdx, endIdx);
// 2.4 獲取?的位置
int queryIdx = url.indexOf("?");
// 表示存在get請(qǐng)求參數(shù)
if (queryIdx >= 0) {
String[] urlParam = url.split("\\?");
url = urlParam[0];
queryParamStr = urlParam[1].trim();
}
// 3. 獲取post請(qǐng)求參數(shù)
if ("post".equals(method)) {
String requestBody = requestInfo.substring(requestInfo.lastIndexOf(CRLF)).trim();
if (null == queryParamStr) {
queryParamStr = requestBody;
} else {
queryParamStr += "&" + requestBody;
}
}
queryParamStr = null == queryParamStr ? "" : queryParamStr;
// System.out.println(method + "-->" + url + "-->" + queryParamStr);
}
/**
* @author lastwhisper
* @desc 將請(qǐng)求參數(shù)轉(zhuǎn)為Map uname=張三&hobby=籃球&hobby=讀書&other=
*/
private void convertMap() {
// 1. 分割字符串 &
String[] keyValues = this.queryParamStr.split("&");
for (String queryStr : keyValues) {
// 2. 再次分割字符串 =
String[] kv = queryStr.split("=");
// 防止other= ,value沒有值導(dǎo)致kv[1]數(shù)組越界,
kv = Arrays.copyOf(kv, 2);
// 3. 獲取key和value
String key = kv[0];
String value = kv[1] == null ? null : decode(kv[1], "utf-8");
// 4. 存儲(chǔ)到Map中
if (!parameterMap.containsKey(key)) { // 第一次出現(xiàn)key(name)
parameterMap.put(key, new ArrayList<String>());
}
parameterMap.get(key).add(value);
}
}
/**
*
* @author lastwhisper
* @desc 處理中文
* @param value 待處理字符串
* @param enc 解碼后的編碼
* @return
*/
private String decode(String value, String enc) {
try {
return java.net.URLDecoder.decode(value, enc);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
/**
*
* @author lastwhisper
* @desc 通過name獲取對(duì)應(yīng)的多個(gè)值
* @param key 參數(shù)名
* @return
*/
public String[] getParameterValues(String key) {
List<String> values = this.parameterMap.get(key);
if (null == values || values.size() < 1) {
return null;
}
return values.toArray(new String[0]);
}
/**
*
* @author lastwhisper
* @desc 通過name獲取對(duì)應(yīng)的一個(gè)值
* @param key 參數(shù)名
* @return
*/
public String getParameter(String key) {
String[] values = getParameterValues(key);
return values == null ? null : values[0];
}
public String getMethod() {
return method;
}
public String getUrl() {
return url;
}
public String getQueryParamStr() {
return queryParamStr;
}
}
3.7 增加內(nèi)容分發(fā)器
使用內(nèi)容分發(fā)器,可以同時(shí)處理多個(gè)請(qǐng)求,并區(qū)分靜態(tài)頁面請(qǐng)求和動(dòng)態(tài)請(qǐng)求,使用的是短連接。
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
/**
* @author lastwhisper
* @desc 分發(fā)器 加入狀態(tài)內(nèi)容處理 404 505 以及首頁
*/
public class Dispatcher implements Runnable {
private Socket client;
private Request request;
private Response response;
public Dispatcher(Socket client) {
this.client = client;
try {
// 獲取請(qǐng)求
// 獲取響應(yīng)
request = new Request(client);
response = new Response(client);
} catch (IOException e) {
e.printStackTrace();
IOUtils.closeSocket(client);
}
}
@Override
public void run() {
try {
if (request.getUrl() == null || request.getUrl().equals("")) {
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("index.html");
response.print(IOUtils.InputStreamToString(is));
response.pushToBrowser(200);
is.close();
}
if (request.getUrl().endsWith("html")) {
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(request.getUrl());
response.print(IOUtils.InputStreamToString(is));
response.pushToBrowser(200);
is.close();
}
Servlet servlet = WebApp.getServletFromUrl(request.getUrl());
if (servlet != null) {
servlet.service(request, response);
response.pushToBrowser(200);
} else {
// 錯(cuò)誤....
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("error.html");
response.print(IOUtils.InputStreamToString(is));
response.pushToBrowser(404);
is.close();
}
} catch (IOException e) {
try {
response.println("服務(wù)器端錯(cuò)誤");
response.pushToBrowser(505);
} catch (IOException e1) {
e1.printStackTrace();
}
}
IOUtils.closeSocket(client);
}
}