web(05)实现类似tomcat功能
目的:了解tomcat机制手动实现 Tomcat 底层机制+ 自己设计 Servlet目标: 不用 Tomcat, 不用系统提供的 Servlet, 模拟 Tomcat 底层实现并能调用我们自己设计的 Servle, 也能完成相同的功能注:是和韩顺平老师学习的一、Tomcat 整体架构分析Tomcat 有三种运行模式(BIO, NIO, APR), 核心讲解的是 Tomcat 如何接收客户端请求,
目的:了解tomcat机制
手动实现 Tomcat 底层机制+ 自己设计 Servlet
目标: 不用 Tomcat, 不用系统提供的 Servlet, 模拟 Tomcat 底层实现并能调用我们自己设计的 Servle, 也能完成相同的功能
注:是和韩顺平老师学习的
一、Tomcat 整体架构分析
Tomcat 有三种运行模式(BIO, NIO, APR), 核心讲解的是 Tomcat 如何接收客户端请求,解析请求, 调用 Servlet , 并返回结果的机制流程, 采用 BIO 线程模型来模拟
二、手动实现 Tomcat 底层机制+ 自己设计 Servlet
2.1 基于 socket 开发服务端-流程
2.2 第一步 ,浏览器请求 http://localhost:8080/, 服务端返回hello 你好呀
创建MzqTomcatV01.java
package com.mzq.mytomcat;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
/**
* @author MengZhiQiang
* @version 1.0
* @date 2022/4/2 21:08
* 接收浏览器的请求,并返回信息
*/
public class MzqTomcatV01 {
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();
//接收浏览器发送的请求 io
//为了方便读取,使用字符流进行读取,将InputStream转为BufferedReader
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream,"utf-8"));
String mes = null;
//循环读取
while ((mes= bufferedReader.readLine())!=null){
if (mes.length()==0){
break;
}
System.out.println(mes);
}
//我们tomcat会发送http响应方式
OutputStream outputStream = socket.getOutputStream();
//构建一个 http 响应的头
//\r\n 表示换行
//http 响应体,需要前面有两个换行 \r\n\r\n(中间空了1行)
String respHeader = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html;charset=utf-8\r\n\r\n";
String resp = respHeader + "hello 你好呀";
System.out.println("========我们给浏览器回应数据==========");
System.out.println(resp);
outputStream.write(resp.getBytes());
outputStream.flush();
outputStream.close();
inputStream.close();
socket.close();
}
}
}
运行结果:
三、 使用 BIO 线程模型,支持多线程
3.1 BIO 线程模型介绍
3.2 需求分析
浏览器请求 http://localhost:8080, 服务端返回 helloword ,后台mytomcat 使用 BIO 线程模型,支持多线程=> 对前面的开发模式进行改造
分析示意图:
代码实现:
MzqRequestHandler类
**
* @author MengZhiQiang
* @version 1.0
* @date 2022/4/6 19:02
* MzqRequestHandler 对象是一个线程对象
* 处理一个http请求
*/
public class MzqRequestHandler implements Runnable {
//定义一个Socket
private Socket socket = null;
//无参构造
public MzqRequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
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("=========Mzqomcatv2 接收到的数据如下=========");
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>helloword</h1>";
System.out.println("========hsptomcatv2返回的数据是=========");
System.out.println(resp);
//返回数据给我们的浏览器/客户端-> 封装成http响应
OutputStream outputStream = socket.getOutputStream();
resp.getBytes(); //是把字符串转成字节数组
outputStream.write(resp.getBytes());
outputStream.flush();
outputStream.close();
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
//最后一定确保socket要关闭
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
启动:
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("=========我在8080端口监听=========");
while (!serverSocket.isClosed()) {
//1. 接收到浏览器的连接后,如果成功,就会得到socket
//2. 这个socket 就是 服务器和 浏览器的数据通道
Socket socket = serverSocket.accept();
//3. 创建一个线程对象,并且把socket给该线程
// 这个是java线程基础
MzqRequestHandler mzqRequestHandler =
new MzqRequestHandler(socket);
new Thread(mzqRequestHandler).start();
}
}
运行结果:
四、实现自己的servlet,resquest,sponse
4.1 实现自己resquest
创建MzqRequest类:
package com.mzq.mytomcat.http;
/**
* @author MengZhiQiang
* @version 1.0
* @date 2022/4/7 20:45
*/
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
/**
* 解读
* 1. HspRequest 作用是封装http请求的数据
* get /mzqCalServlet?num1=10&num2=30
* 2. 比如 method(get) 、 uri(/mzqCalServlet) 、 还有参数列表 (num1=10&num2=30)
* 3. HspRequest 作用就等价原生的servlet 中的HttpServletRequest
* 4. 这里考虑的是GET请求
*/
public class MzqRequest {
private String method;
private String uri;
//request对象有一个特别重要方法 获取uri参数
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;
}
//存放参数列表 参数名-参数值=》HashMap
private HashMap<String,String > parametersMapping = new HashMap<>();
private InputStream inputStream = null;
//构造器=> 对http请求进行封装
//inputStream 是和 对应http请求的socket关联
public MzqRequest(InputStream inputStream) {
this.inputStream = inputStream;
encapHttpRequest();
}
@Override
public String toString() {
return "MzqRequest{" +
"method='" + method + '\'' +
", uri='" + uri + '\'' +
", parametersMapping=" + parametersMapping +
'}';
}
/**
* 将http请求的相关数据,进行封装,然后提供相关的方法,进行获取
*/
private void encapHttpRequest() {
System.out.println("MzqRequest init()");
try {
//inputstream -> BufferedReader
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
//读取第一行
/**
* GET /mzqCalServlet?num1=10&num2=30 HTTP/1.1
* Host: localhost:8080
*/
String requestLine = bufferedReader.readLine();
//GET - /hspCalServlet?num1=10&num2=30 - HTTP/1.1
String[] requestLineArr = requestLine.split(" ");
//得到method
method = requestLineArr[0];
//解析得到 /mzqCalServlet
//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("&");
//防止用户提交时 /mzqCalServlet?
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();
}
}
}
4.2 实现自己response
创建MzqResponse类:
package com.mzq.mytomcat.http;
/**
* @author MengZhiQiang
* @version 1.0
* @date 2022/4/7 20:45
*/
import java.io.OutputStream;
/**
* 解读
* 1. MzqResponse对象可以封装OutputStream(是socket关联)
* 2. 即可以通过 MzqResponse对象 返回Http响应给浏览器/客户端
* 3. MzqResponse对象 的作用等价于原生的servlet的 HttpServletResponse
*/
public class MzqResponse {
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";
//如果有兴趣, 在编写更多的方法
//比如 setContentType
//在创建 HspResponse 对象时,传入的outputStream是和Socket关联的
public MzqResponse(OutputStream outputStream) {
this.outputStream = outputStream;
}
//当我们需要给浏览器返回数据时,可以通过HspResponse 的输出流完成
public OutputStream getOutputStream() {
return outputStream;
}
}
4.3 实现自己servlet
Servlet 生命周期:
根据原生类图:
创建一个MzqServlet接口:
import com.mzq.mytomcat.http.MzqRequest;
import com.mzq.mytomcat.http.MzqResponse;
import java.io.IOException;
/**
* @author MengZhiQiang
* @version 1.0
* @date 2022/4/7 21:06
*/
public interface MzqServlet {
/**
* 自己构建三个核心方法
*/
void init() throws Exception;
//(MzqRequest request, MzqResponse response为上面自己写的两个类
void service(MzqRequest request, MzqResponse response) throws IOException;
void destroy();
}
创建一个抽象类:使用的了模板设计模式
package com.mzq.mytomcat.servlet;
import com.mzq.mytomcat.http.MzqRequest;
import com.mzq.mytomcat.http.MzqResponse;
import java.io.IOException;
/**
* @author MengZhiQiang
* @version 1.0
* @date 2022/4/7 21:08
*/
public abstract class MzqHttpServlet implements MzqServlet{
@Override
public void service(MzqRequest request, MzqResponse response) throws IOException {
//老师说明 equalsIgnoreCase 比较字符串内容是相同,不区别大小写
if("GET".equalsIgnoreCase(request.getMethod())) {
//这里会有动态绑定
this.doGet(request,response);
} else if("POST".equalsIgnoreCase(request.getMethod())) {
this.doPost(request,response);
}
}
//这里我们使用的了模板设计模式
//让MzqHttpServlet 子类 MzqCalServlet 实现
public abstract void doGet(MzqRequest request, MzqResponse response);
public abstract void doPost(MzqRequest request, MzqResponse response);
}
创建一个自己的Servlet类MzqMyServlet:
package com.mzq.mytomcat.servlet;
import com.mzq.mytomcat.http.MzqRequest;
import com.mzq.mytomcat.http.MzqResponse;
import com.mzq.utils.WebUtils;
import java.io.IOException;
import java.io.OutputStream;
/**
* @author MengZhiQiang
* @version 1.0
* @date 2022/4/7 21:11
*/
public class MzqMyServlet extends MzqHttpServlet{
@Override
public void doGet(MzqRequest request, MzqResponse 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 = MzqResponse.respHeader
+ "<h1>" + num1 + " + " + num2 + " = " + sum + " MyTomcatV3 - 反射+xml创建</h1>";
try {
outputStream.write(respMes.getBytes());
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void doPost(MzqRequest request, MzqResponse response) {
this.doGet(request, response);
}
@Override
public void init() throws Exception {
}
@Override
public void destroy() {
}
}
根据需求编写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;
}
//判断uri是不是html文件
public static boolean isHtml(String uri) {
return uri.endsWith(".html");
}
//根据文件名来读取该文件->String
public static String readHtml(String filename) {
String path = com.mzq.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();
}
}
4.4 实现通过xml+反射来初始化容器
使用dom4j技术:
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.1</version>
</dependency>
创建一个初始化容器的类:
package com.mzq.mytomcat;
import com.mzq.mytomcat.servlet.MzqHttpServlet;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
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;
/**
* @author MengZhiQiang
* @version 1.0
* @date 2022/4/7 21:14
* 实现通过xml+反射来初始化容器
*/
public class MzqTomcatV3 {
//1. 存放容器 servletMapping
// -ConcurrentHashMap
// -HashMap
// key - value
// ServletName 对应的实例
public static final ConcurrentHashMap<String, MzqHttpServlet>
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<>();
//变强..
public static void main(String[] args) {
MzqTomcatV3 hspTomcatV3 = new MzqTomcatV3();
hspTomcatV3.init();
//启动hsptomcat容器
hspTomcatV3.run();
}
//启动HspTomcatV3容器
public void run() {
try {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("=====mzqtomcatv3在8080监听======");
while (!serverSocket.isClosed()) {
Socket socket = serverSocket.accept();
MzqRequestHandler hspRequestHandler =
new MzqRequestHandler(socket);
new Thread(hspRequestHandler).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//直接对两个容器进行初始化
public void init() {
//读取web.xml => dom4j =>
//得到web.xml文件的路径 => 拷贝一份.
String path = MzqTomcatV3.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配置
//使用反射将该servlet实例放入到servletMapping
Element servletName = element.element("servlet-name");
Element servletClass = element.element("servlet-class");
servletMapping.put(servletName.getText(),
(MzqHttpServlet) 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);
}
}
修改原先线程类MzqRequestHandler为:
package com.mzq.mytomcat;
import com.mzq.mytomcat.http.MzqRequest;
import com.mzq.mytomcat.http.MzqResponse;
import com.mzq.mytomcat.servlet.MzqHttpServlet;
import com.mzq.utils.WebUtils;
import java.io.*;
import java.net.Socket;
/**
* @author MengZhiQiang
* @version 1.0
* @date 2022/4/6 19:02
* MzqRequestHandler 对象是一个线程对象
* 处理一个http请求
*/
public class MzqRequestHandler implements Runnable {
//定义一个Socket
private Socket socket = null;
//无参构造
public MzqRequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
MzqRequest hspRequest = new MzqRequest(socket.getInputStream());
//这里我们可以同HspResponse对象,返回数据给浏览器/客户端
MzqResponse hspResponse = new MzqResponse(socket.getOutputStream());
//1. 得到 uri => 就是 servletUrlMapping 的 url-pattern
String uri = hspRequest.getUri();
String servletName = MzqTomcatV3.servletUrlMapping.get(uri);
if (servletName == null) {
servletName = "";
}
//2. 通过uri->servletName->servlet的实例 , 真正的运行类型是其子类 HspCalServlet
MzqHttpServlet hspHttpServlet =
MzqTomcatV3.servletMapping.get(servletName);
//3. 调用service , 通过OOP的动态绑定机制,调用运行类型的 doGet/doPost
if (hspHttpServlet != null) {//得到
hspHttpServlet.service(hspRequest, hspResponse);
} else {
//没有这个servlet , 返回404的提示信息
String resp = MzqResponse.respHeader + "<h1>404 Not Found</h1>";
OutputStream outputStream = hspResponse.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();
}
}
}
}
}
五、测试
运行MzqTomcatV3中main方法(类似启动tomcat):
更多推荐
所有评论(0)