目录
手动实现 Tomcat 底层机制+ 自己设Servlet
完成小案例
运行效果
此项目用maven至于怎么配置在下一篇文章
创建cal.html
CalServlet.java
web.xml
WebUtils
问题:
Tomcat 整体架构分析
测试分析:
抓包情况
手动实现 Tomcat 底层机制+ 自己设计 Servlet
分析+代码实现
● 分析示意图
代码实现
TomcatV1
问题分析:
需求分析/图解
● 分析示意图
代码实现
RequestHandler类
TomcatV2
问题分析:
分析+代码实现
● 分析示意图
WyxRequestHandler
wyxResponse
wyxRequest
wyxServlet接口
wyxHttpServlet
wyxCalServlet
WebUtils
wyxTomcatV3
手动实现 Tomcat 底层机制+ 自己设Servlet
完成小案例
运行效果
此项目用maven至于怎么配置在下一篇文章
创建cal.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>计算器</title>
</head>
<body>
<h1>计算器</h1>
<form action="/calServlet" method="get">
num1:<input type="text" name="num1"><br />
num2:<input type="text" name="num2"><br />
<input type="submit" value="提交">
</form>
</body>
</html>
CalServlet.java
public class CalServlet extends HttpServlet {
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
//接收提交的数据进行计算
//复制当前行 ctrl+alt+下光标
String strNum1 = request.getParameter("num1");
String strNum2 = request.getParameter("num2");
//把strNum1 和 strNum2 转成 int
int num1 = WebUtils.parseInt(strNum1, 0);
int num2 = WebUtils.parseInt(strNum2, 0);
int result = num1 + num2;
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.print("<h1>" + num1 + " + " + num2 + " = " + result + "<h1>");
writer.flush();
writer.close();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request, response);
}
}
web.xml
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>CalServlet</servlet-name>
<servlet-class>com.servlet.CalServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>CalServlet</servlet-name>
<url-pattern>/calServlet</url-pattern>
</servlet-mapping>
</web-app>
WebUtils
public class WebUtils {
/**
* 将一个字符串数字,转成 int, 如果转换失败,就返回传入 defaultVal
* @param strNum
* @param defaultVal
* @return
*/
public static int parseInt(String strNum, int defaultVal) {
try {
return Integer.parseInt(strNum);
} catch (NumberFormatException e) {
System.out.println(strNum + " 格式不对,转换失败");
}
return defaultVal;
}
}
问题:
Tomcat 底层实现 和 调用到 Servlet 流程?
我们的目标: 不用 Tomcat, 不用系统提供的 Servlet, 模拟 Tomcat 底层实现并能调用我们自己设计的 Servle, 也能完成相同的功能
Tomcat 整体架构分析
● 说明: Tomcat 有三种运行模式(BIO, NIO, APR), 因为核心讲解的是 Tomcat 如何接
收客户端请求,解析请求, 调用 Servlet , 并返回结果的机制流程, 采用 BIO 线程模型来模拟.
测试分析:
浏览器 http://localhost:8080/cal.html 1.
浏览器输入 http://localhost:8080/cal.html
抓包情况
手动实现 Tomcat 底层机制+ 自己设计 Servlet
实现任务阶段
编写自己 Tomcat, 能给浏览器返回 Hi
基于 socket 开发服务端-流程
分析+代码实现
● 分析示意图
代码实现
TomcatV1
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class TomcatV1 {
public static void main(String[] args) throws IOException {
//1. 创建 ServerSocket, 在 8080 端口监听
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("=======mytomcat 在 8080 端口监听======");
while (!serverSocket.isClosed()) {
//等待浏览器/客户端的连接
//如果有连接来,就创建一个 socket
//这个 socket 就是服务端和浏览器端的连接/通道
Socket socket = serverSocket.accept();
//先接收浏览器发送的数据
//inputStream 是字节流=> BufferedReader(字符流)
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
String mes = null;
System.out.println("=======接收到浏览器发送的数据=======");
//循环的读取
while ((mes = bufferedReader.readLine()) != null) {
//判断 mes 的长度是否为 0
if (mes.length() == 0) {
break;//退出 while
}
System.out.println(mes);
}
//我们的 tomcat 会送-http 响应方式
OutputStream outputStream = socket.getOutputStream();
//构建一个 http 响应的头
//\r\n 表示换行
//http 响应体,需要前面有两个换行 \r\n\r\n
String respHeader = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html;charset=utf-8\r\n\r\n";
String resp = respHeader + "hi,";
System.out.println("========我们的 tomcat 给浏览器会送的数据 ======");
System.out.println(resp);
outputStream.write(resp.getBytes());//将 resp 字符串以 byte[] 方式返回
outputStream.flush();
outputStream.close();
inputStream.close();
socket.close();
}
}
}
问题分析:
没有使用 BIO 线程模型,没有实现多线程,性能差
实现任务阶段 2- 使用 BIO 线程模型,支持多线程
BIO 线程模型介绍
需求分析/图解
- 需求分析如图, 浏览器请求 http://localhost:8080, 服务端返回 hi后台
- wtomcat 使用 BIO 线程模型,支持多线程=> 对前面的开发模式进行改造
- 分析+代码实现
● 分析示意图
代码实现
RequestHandler类
import java.io.*;
import java.net.Socket;
public class RequestHandler implements Runnable {
//定义Socket
private Socket socket = null;
public RequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//这里我们可以对客户端/浏览器进行IO编程/交互
try {
InputStream inputStream = socket.getInputStream();
//把inputStream -> BufferedReader -> 方便进行按行读取
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
//不同的线程在和浏览器和客户端交互
System.out.println("当前线程= " + Thread.currentThread().getName());
System.out.println("=========tomcatv2 接收到的数据如下=========");
String mes = null;
while ((mes = bufferedReader.readLine()) != null) {
//如果长度为0 ""
if (mes.length() == 0) {
break; //退出
}
System.out.println(mes);
}
//构建一下http响应头
//返回的http的响应体和响应头之间有两个换行 \r\n\r\n
String respHeader = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html;charset=utf-8\r\n\r\n";
String resp = respHeader + "<h1>hi</h1>";
System.out.println("========tomcatv2返回的数据是=========");
System.out.println(resp);
//返回数据给我们的浏览器/客户端-> 封装成http响应
OutputStream outputStream = socket.getOutputStream();
// resp.getBytes() 是把字符串转成字节数组
outputStream.write(resp.getBytes());
outputStream.flush();
outputStream.close();
inputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
//最后一定确保socket要关闭
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
TomcatV2
public class TomcatV2 {
public static void main(String[] args) throws IOException {
//在8080端口监听
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("=======hsptomcatV2 在8080监听=======");
//只要 serverSocket没有关闭,就一直等待浏览器/客户端的连接
while (!serverSocket.isClosed()) {
//1. 接收到浏览器的连接后,如果成功,就会得到socket
//2. 这个socket 就是 服务器和 浏览器的数据通道
Socket socket = serverSocket.accept();
//3. 创建一个线程对象,并且把socket给该线程
// 这个是java线程基础
HspRequestHandler RequestHandler =
new RequestHandler(socket);
new Thread(RequestHandler).start();
}
}
}
问题分析:
Tomcat V2 只是简单返回结果,没有和 Servlet、web.xml 关联
实现任务阶段 3- 处理 Servlet
分析+代码实现
● 分析示意图
WyxRequestHandler
package com.wyxdu.tomcat.handler;
import com.wyxdu.tomcat.WyxTomcatV3;
import com.wyxdu.tomcat.http.WyxRequest;
import com.wyxdu.tomcat.http.WyxResponse;
import com.wyxdu.tomcat.servlet.WyxHttpServlet;
import com.wyxdu.tomcat.utils.WebUtils;
import java.io.*;
import java.net.Socket;
public class WyxRequestHandler implements Runnable {
//定义Socket
private Socket socket = null;
public WyxRequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//这里我们可以对客户端/浏览器进行IO编程/交互
try {
WyxRequest wyxRequest = new WyxRequest(socket.getInputStream());
//这里我们可以同HspResponse对象,返回数据给浏览器/客户端
WyxResponse wyxResponse = new WyxResponse(socket.getOutputStream());
//1. 得到 uri => 就是 servletUrlMapping 的 url-pattern
String uri = wyxRequest.getUri();
//=====================新增业务逻辑==========
//(1) 判断uri是什么资源 => 工具方法
//(2) 如果是静态资源,就读取该资源,并返回给浏览器 content-type text/html
//(3) 因为目前并没有起到tomcat, 不是一个标准的web项目
//(4) 把读取的静态资源放到 target/classes/cal.html
//过滤,拦截 , 权限等待 => Handler.... => 分发
if(WebUtils.isHtml(uri)) {//就是静态页面
String content = WebUtils.readHtml(uri.substring(1));
content = wyxResponse.respHeader + content;
//得到outputstream , 返回信息(静态页面)给浏览器
OutputStream outputStream = wyxResponse.getOutputStream();
outputStream.write(content.getBytes());
outputStream.flush();
outputStream.close();
socket.close();
return;
}
//有了filter机制,可以理解再调用servlet之前,先匹配filter
//1. 根据request对象封装的uri
//2. 到 filterUrlMapping 去匹配
//3. 如果匹配上就调用 filterMapping 对应的filer对象doFilter()
//4. 如果没有匹配上,就直接走我们后的servlet/jsp/html.
String servletName = WyxTomcatV3.servletUrlMapping.get(uri);
if (servletName == null) {
servletName = "";
}
//2. 通过uri->servletName->servlet的实例 , 真正的运行类型是其子类 WyxCalServlet
WyxHttpServlet wyxHttpServlet =
WyxTomcatV3.servletMapping.get(servletName);
//3. 调用service , 通过OOP的动态绑定机制,调用运行类型的 doGet/doPost
if (wyxHttpServlet != null) {//得到
wyxHttpServlet.service(wyxRequest, wyxResponse);
} else {
//没有这个servlet , 返回404的提示信息
String resp = wyxResponse.respHeader + "<h1>404 Not Found</h1>";
OutputStream outputStream = wyxResponse.getOutputStream();
outputStream.write(resp.getBytes());
outputStream.flush();
outputStream.close();
}
socket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
//最后一定确保socket要关闭
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
wyxResponse
public class wyxResponse {
private OutputStream outputStream = null;
//写一个http的响应头 => 先死后活
public static final String respHeader = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html;charset=utf-8\r\n\r\n";
//在创建 wyxResponse 对象时,传入的outputStream是和Socket关联的
public wyxResponse(OutputStream outputStream) {
this.outputStream = outputStream;
}
//当我们需要给浏览器返回数据时,可以通过HspResponse 的输出流完成
//
public OutputStream getOutputStream() {
return outputStream;
}
}
wyxRequest
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
/**
* 1. wyxRequest 作用是封装http请求的数据
* get /WyxCalServlet?num1=10&num2=30
* 2. 比如 method(get) 、 uri(/hspCalServlet) 、 还有参数列表 (num1=10&num2=30)
* 3. wyxRequest 作用就等价原生的servlet 中的HttpServletRequest
* 4. 一会走代码
* 5. 这里考虑的是GET请求
*/
public class wyxRequest {
private String method;
private String uri;
//存放参数列表 参数名-参数值 => HashMap
private HashMap<String, String> parametersMapping =
new HashMap<>();
private InputStream inputStream = null;
//构造器=> 对http请求进行封装 => 可以写的代码封装成方法
//inputStream 是和 对应http请求的socket关联
public wyxRequest(InputStream inputStream) {
this.inputStream = inputStream;
//完成对http请求数据的封装..
encapHttpRequest();
}
/**
* 将http请求的相关数据,进行封装,然后提供相关的方法,进行获取
*/
private void encapHttpRequest() {
System.out.println("wyxRequest init()");
try {
//inputstream -> BufferedReader
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
//读取第一行
/**
* GET /hspCalServlet?num1=10&num2=30 HTTP/1.1
* Host: localhost:8080
* User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Fi
*/
String requestLine = bufferedReader.readLine();
//GET - /WyxCalServlet?num1=10&num2=30 - HTTP/1.1
String[] requestLineArr = requestLine.split(" ");
//得到method
method = requestLineArr[0];
//解析得到 /WyxCalServlet
//1. 先看看uri 有没有参数列表
int index = requestLineArr[1].indexOf("?");
if (index == -1) { //说明没有参数列表
uri = requestLineArr[1];
} else {
//[0,index)
uri = requestLineArr[1].substring(0, index);
//获取参数列表->parametersMapping
//parameters => num1=10&num2=30
String parameters = requestLineArr[1].substring(index + 1);
//num1=10 , num2=30 .... parametersPair= ["num1=10","num2=30" ]
String[] parametersPair = parameters.split("&");
//防止用户提交时 /hspCalServlet?
if (null != parametersPair && !"".equals(parametersPair)) {
//再次分割 parameterPair = num1=10
for (String parameterPair : parametersPair) {
//parameterVal ["num1", "10"]
String[] parameterVal = parameterPair.split("=");
if (parameterVal.length == 2) {
//放入到 parametersMapping
parametersMapping.put(parameterVal[0], parameterVal[1]);
}
}
}
}
//这里不能关闭流 inputStream 和 socket关联
//inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
//request对象有一个特别重要方法
public String getParameter(String name) {
if (parametersMapping.containsKey(name)) {
return parametersMapping.get(name);
} else {
return "";
}
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
@Override
public String toString() {
return "wyxRequest{" +
"method='" + method + '\'' +
", uri='" + uri + '\'' +
", parametersMapping=" + parametersMapping +
'}';
}
}
wyxServlet接口
public interface wyxServlet {
void init() throws Exception;
void service(wyxRequest request, wyxResponse response) throws IOException;
void destroy();
}
wyxHttpServlet
public abstract class wyxHttpServlet implements wyxServlet {
@Override
public void service(wyxRequest request, wyxResponse response) throws IOException {
//说明 equalsIgnoreCase 比较字符串内容是相同,不区别大小写
if("GET".equalsIgnoreCase(request.getMethod())) {
//这里会有动态绑定
this.doGet(request,response);
} else if("POST".equalsIgnoreCase(request.getMethod())) {
this.doPost(request,response);
}
}
//这里我们使用的了模板设计模式 => java 基础的 抽象类专门讲过模板设计模式
//让HspHttpServlet 子类 wyxCalServlet 实现
public abstract void doGet(wyxRequest request, wyxResponse response);
public abstract void doPost(wyxRequest request, wyxResponse response);
}
wyxCalServlet
public class wyxCalServlet extends wyxHttpServlet {
@Override
public void doGet(wyxRequest request, wyxResponse response) {
//java基础的 OOP 的动态绑定机制..
//写业务代码,完成计算任务
int num1 = WebUtils.parseInt(request.getParameter("num1"), 0);
int num2 = WebUtils.parseInt(request.getParameter("num2"), 0);
int sum = num1 + num2;
//返回计算结果给浏览器
//outputStream 和 当前的socket关联
OutputStream outputStream = response.getOutputStream();
String respMes = wyxResponse.respHeader
+ "<h1>" + num1 + " + " + num2 + " = " + sum + " wyxTomcatV3 - 反射+xml创建</h1>";
try {
outputStream.write(respMes.getBytes());
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void doPost(wyxRequest request, wyxResponse response) {
this.doGet(request, response);
}
@Override
public void init() throws Exception {
}
@Override
public void destroy() {
}
}
WebUtils
public class WebUtils {
//将字符串转成数字方法
public static int parseInt(String strNum, int defaultVal) {
try {
return Integer.parseInt(strNum);
} catch (NumberFormatException e) {
System.out.println(strNum + " 不能转成数字");
}
return defaultVal;
}
//判断uri是不是html文件
public static boolean isHtml(String uri) {
return uri.endsWith(".html");
}
//根据文件名来读取该文件->String
public static String readHtml(String filename) {
String path = com.wyxdu.utils.WebUtils.class.getResource("/").getPath();
StringBuilder stringBuilder = new StringBuilder();
try {
BufferedReader bufferedReader = new BufferedReader(new FileReader(path + filename));
String buf = "";
while ((buf = bufferedReader.readLine()) != null) {
stringBuilder.append(buf);
}
} catch (Exception e) {
e.printStackTrace();
}
return stringBuilder.toString();
}
}
wyxTomcatV3
package com.wyxdu.tomcat;
import com.wyxdu.tomcat.handler.wyxRequestHandler;
import com.wyxdu.tomcat.servlet.wyxHttpServlet;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import javax.servlet.Filter;
import javax.servlet.http.HttpSession;
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* 第3版的Tomcat, 实现通过xml+反射来初始化容器
*/
public class wyxTomcatV3 {
//1. 存放容器 servletMapping
// -ConcurrentHashMap
// -HashMap
// key - value
// ServletName 对应的实例
public static final ConcurrentHashMap<String, wyxHttpServlet>
servletMapping = new ConcurrentHashMap<>();
//2容器 servletUrlMapping
// -ConcurrentHashMap
// -HashMap
// key - value
// url-pattern ServletName
public static final ConcurrentHashMap<String, String>
servletUrlMapping = new ConcurrentHashMap<>();
//你可以这里理解session, tomcat还维护一个容器
public static final ConcurrentHashMap<String, HttpSession>
sessionMapping = new ConcurrentHashMap<>();
//你可以这里理解filter, tomcat还维护了filter的容器
public static final ConcurrentHashMap<String, String>
filterUrlMapping = new ConcurrentHashMap<>();
public static final ConcurrentHashMap<String, Filter>
filterMapping = new ConcurrentHashMap<>();
public static void main(String[] args) {
wyxTomcatV3 wyxTomcatV3 = new wyxTomcatV3();
wyxTomcatV3.init();
//启动wyxtomcat容器
wyxTomcatV3.run();
}
//启动WyxTomcatV3容器
public void run() {
try {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("=====wyxtomcatv3在8080监听======");
while (!serverSocket.isClosed()) {
Socket socket = serverSocket.accept();
wyxRequestHandler wyxRequestHandler =
new wyxRequestHandler(socket);
new Thread(wyxRequestHandler).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//直接对两个容器进行初始化
public void init() {
//读取web.xml => dom4j =>
//得到web.xml文件的路径 => 拷贝一份.
String path = wyxTomcatV3.class.getResource("/").getPath();
//System.out.println("path= " + path);
//使用dom4j技术完成读取
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(new File(path + "web.xml"));
System.out.println("document= " + document);
//得到根元素
Element rootElement = document.getRootElement();
//得到根元素下面的所有元素
List<Element> elements = rootElement.elements();
//遍历并过滤到 servlet servlet-mapping
for (Element element : elements) {
if ("servlet".equalsIgnoreCase(element.getName())) {
//这是一个servlet配置
//System.out.println("发现 servlet");
//使用反射将该servlet实例放入到servletMapping
Element servletName = element.element("servlet-name");
Element servletClass = element.element("servlet-class");
servletMapping.put(servletName.getText(),
(wyxHttpServlet) Class.forName(servletClass.getText().trim()).newInstance());
} else if ("servlet-mapping".equalsIgnoreCase(element.getName())) {
//这是一个servlet-mapping
//System.out.println("发现 servlet-mapping");
Element servletName = element.element("servlet-name");
Element urlPatter = element.element("url-pattern");
servletUrlMapping.put(urlPatter.getText(), servletName.getText());
}
}
} catch (Exception e) {
e.printStackTrace();
}
//验证,这两个容器是否初始化成功
System.out.println("servletMapping= " + servletMapping);
System.out.println("servletUrlMapping= " + servletUrlMapping);
}
}