在手写Tomcat前,先了解Tomcat和客户端的三个层次
1.客户端发出请求后,Tomcat接收后直接返回
2.客户端发出请求后,Tomcat启动一个线程并返回响应
3.客户端发出请求后,Tomcat启动一个线程,选择一个servlet并返回给客户端
那么由此,就会产生HttpRequest,HttpResponse
在设计时,我们先设计Request和Response
Request:是一个请求数据,其作用就是将客户端发送的http请求数据进行拆解后,将信息封装到其类本身
那么里面会持有一个InputStream,还会有参数类型,比如uri,method,以及还有参数信息,那么参数信息我们需要用一个集合来进行存放,就使用hashMap(key,value)key:参数名字, value:参数对应的值。
如何获取到数据呢?
1.使用转换流,用字符流读取第一行
2.用 “ ” 进行分割,将uri,method赋值给对应属性
2.1 uri 比较难获取,那么就用index获取?的的位置,如果-1则说明后面没有带参数,如果是数字,那么就截取到该字符串的位置,因是前包含,后不包含的获取方式
3.获取参数信息
(1)先截取到“?”号后面的字符串
(2)通过“&”号进行分割
(3)再用“=”号来进行分割,获取到数组,进行判断下看数组是不是两个长度,如果是就将其添加到HashMap中
4.getParameter()方法
(1)内部其本质是调用hashmap.get()方法,获取到key对应的value值
(2)担心返回null,所以用异常包裹,出现异常,则返回空字符串
/**
* 解读:
* 1. HspRequest 对象是用来封装客户端发送的http请求中的信息,以便于更好的读取
* /calServlet?num1=10&num2=20
* 2. 将http请求中的 method(get或post)、uri(也就是请求的哪个servlet:/calServlet)、 参数信息
* 3. HspRequest 对象 等价于 HttpServletRequest对象
* 4. 这里考虑的是Get请求
*/
public class HspRequest {
//思路:
//1.目前只取出三个属性,所以设计三个属性变量
//2.取出变量,并赋值给对应属性
private String method;
private String uri;
//存放参数列表 参数名-参数值
private HashMap<String, String> parametersMapping
= new HashMap<>();
InputStream inputStream = null;
//构造器
// inputStream 是和 对应的http请求socket关联
public HspRequest(InputStream inputStream) {
this.inputStream = inputStream;
//完成对http请求数据的封装
encapHttpRequest();
}
/**
* 将http请求的相关信息,进行封装,然后提供相关的方法,进行获取
*/
private void encapHttpRequest() {
//2.取出变量,并赋值给对应属性
//思路:
// (1)强转成字符
// (2)取出来的是 请求头 和 请求行连接在一起,所以只取出第一行
/*
http请求
GET /calServlet?num1=10&num2=20 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0
*/
System.out.println("======== HspRequest init() 被调用 ==========");
try {
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
//取出第一行
String requestLine = bufferedReader.readLine();
String[] requestLineArr = requestLine.split(" ");
method = requestLineArr[0];
//取出 uri 赋值给 uri
//(1) 这里是判断uri后面有没有跟参数信息,如果有参数信息,就一定会有?号
// 如果没有参数信息,就没有?号,根据indexOf()方法判断其是否存在
// 没有存在index就是-1
int index = requestLineArr[1].indexOf("?");
if (index == -1) {//说明字符串中没有找到“?”
uri = requestLineArr[1];
}else {
//这里的截取是[0, index)
uri = requestLineArr[1].substring(0, index);
//获取参数信息 parameters
//(1) 截取requestLineArr[1]中"?"以后的字符串
String parameters = requestLineArr[1].substring(index + 1);
//(2) 通过“&”截取成字符串数组{“num1=10”, "num2=30"}
String[] parameterPair = parameters.split("&");
//防止用户提交时为:servlet?后没有跟参数,所以需要进行判断
if (null != parameterPair && ! "".equals(parameterPair)) {
for (String parameter : parameterPair) {
//(3) 通过"="号,再将其字符串数组中的每个元素截取成字符串数组
String[] parameterVal = parameter.split("=");
//(4) 判断其内每个长度是否等于2,然后将其添加到HshMap中
if (parameterVal.length == 2) {
parametersMapping.put(parameterVal[0], parameterVal[1]);
}
}
}
//关闭流
//这里不能关闭流,因inputStream 和 socket 是相关联的,
//关闭了inputStream 就相当于关闭了 socket
//其本质是输入流关闭了,代表着socket也就关闭了
//socket关闭代表无法获取到输出流OutputStream
//inputStream.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
// request有一个特别重要的方法:通过name获取到其value值
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 "HspRequest{" +
"method='" + method + '\'' +
", uri='" + uri + '\'' +
", parametersMapping=" + parametersMapping +
'}';
}
}
Response 设计
是将服务器信息返回给客户端
1.既然是输出,那么就是有OutputStream
2.返回给客户端,那么就需要是Http信息,所以需要一个http响应头
/**
* 解读:
* 1. HspResponse 对象可以封装OutputStream(是和socket绑定的)
* 2. 即可以发送http响应信息给 客户端
* 3. HspResponse 对象 等价于 HttpServletResponse
*/
public class HspResponse {
//思路:
//1.先写一个OutputStream 是和socket的关联的
//2.获取OutputStream
//3.其内部会有一个请求头
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";
public HspResponse(OutputStream outputStream) {
this.outputStream = outputStream;
}
public OutputStream getOutputStream() {
return outputStream;
}
}
线程 设计
线程的作用是:处理请求:解析客户端发送的请求,然后再寻找对应的servlet,再调用对应的servlet方法
1.既然是处理请求,那么就必须有一个socket,通过socket才能拿到输入或输出流
2.拆解请求数据,因数据都封装在Request,只用通过获取Request类就可以得到数据
3.获取到数据后,得到其请求想要的uri后,在Tomcat维护的集合中查询uri
4.得到其servletName,再通过其他集合获取到servletName对应的Servlet实例
5.调用该实例方法
6.如未查询到该实例,就会返回404给客户端
public class HspRequestHandler implements Runnable {
//定义一个socket 怎么确定是哪个socket? 用构造器来接收
//获取socket的输入流的内容
//获取输出流 发出响应,但要包装成http
private Socket socket = null;
public HspRequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//InputStream inputStream = null;try {
//使用HspRequest
HspRequest hspRequest = new HspRequest(socket.getInputStream());
//使用 HspResponse对象发送响应
//1.创建 HspResponse
HspResponse hspResponse =
new HspResponse(socket.getOutputStream());
/**
* 版本HspTomcatV3代码
*/
//版本HspTomcatV3代码,使用反射来构建对象
//1.通过uri来获取到客户端请求的是哪个servlet路径 url-pattern
String uri = hspRequest.getUri();
//=========新增业务===========
//返回静态页面
//(1) 判断uri是什么资源
//(2) 如果是静态资源,就读取该资源,并返回给浏览器 设置content-type
//(3) 因为目前没有启动tomcat,不是一个标准的web项目
//(4) 把读取的静态资源放到 target/classes/cal.html
//1.判断uri是否是一个html文件
boolean html = WebUtils.isHtml(uri);
//2.if语句判断执行里面
if (html) {
//3. 将html文件内容取出
String calHtml = WebUtils.readHtml(uri.substring(1));
//4. 将其发送
//4.1 获取客户端请求输出流
OutputStream outputStream =
hspResponse.getOutputStream();
// 为什么发送出去的html代码,客户端却没有渲染?
//原因是因为发出的不是http响应格式,所以客户端(浏览器)是不会进行渲染
//解决方法:
//4.2 发送内容
//outputStream.write(calHtml.getBytes());
outputStream.write((HspResponse.respHeader + calHtml).getBytes());
//关闭流
outputStream.flush();
outputStream.close();
socket.close();
return;
}
//2.uri 其就是 HspTomcatV3 中 servletUrlMapping集合中的key值
// 通过key值获取到servlet-name
String servletName = HspTomcatV3.servletUrlMapping.get(uri);
//2.1 假设输入一个错误的servlet,那么 HspTomcatV3.servletMapping.get(servletName) 就会抛出异常
//2.2 抛出异常的原因是:因其所放置servlet的集合 ConcurrentHashMap是不能存在null的
//2.3 但在 servletUrlMapping 中如果未查找到对应的uri,则会返回null
//2.4 解决方法:(1) 将 ConcurrentHashMap 改成 HashMap (2) 如果是异常则将其改成""
if (servletName == null) {
servletName = " ";
}
//3.通过 servletName 获取 servletMapping 中对应的对象实例
HspHttpServlet hspHttpServlet = HspTomcatV3.servletMapping.get(servletName);
//4.获取到对象实例后,因其编译类型是 HspHttpServlet, 但运行类型是 HspCalServlet
//4.1 调用其父类 HspHttpServlet中的service()方法,会选择doGet()或doPost()
//4.2 这里又会因动态绑定机制,动态绑定机制会首先调用其运行类型中的doGet()或doPost()方法
if (hspHttpServlet != null) {
//这里为什么会最终是HspCalServlet会调用方法呢?
//因再servletMapping.get()得到的是实例其实就是HspCalServlet实例,只不过编译类型是其父类
//根据动态绑定机制,所以最终会调用到其子类的方法
hspHttpServlet.service(hspRequest, hspResponse);
} else {
//如果没有找到该servlet实例,则返回404
String resp = HspResponse.respHeader + "<h1>404 Not Found</h1>";
OutputStream outputStream = hspResponse.getOutputStream();
outputStream.write(resp.getBytes());
outputStream.flush();
outputStream.close();
}
//输入流不能提前关闭,提前关闭代表着socket 也就关闭了
//其本质是输入流关闭了,代表着socket也就关闭了
//socket关闭代表无法获取到输出流OutputStream
//inputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Servlet 设计
Servlet其就是客户端想要请求的信息,而这个才是最终的业务层
设计主要分三个层次:
1. Servlet 接口 -- 里面会有一些抽象方法如(init(),service(request,response)等)
public interface HspServlet {
void init() throws ServletException;
void service(HspRequest request, HspResponse response) throws IOException;
void destroy();
}
2. 实现了Servlet接口的抽象类HttpServlet --- 实现了service()方法,里面会有分发机制,比如根据是get或post请求来调用doget或dopost
public abstract class HspHttpServlet implements HspServlet {
@Override
public void service(HspRequest request, HspResponse response) throws IOException {
//equalsIgnoreCase(): 是比较字符串内容相同,不区分大小写
if ("GET".equalsIgnoreCase(request.getMethod())) {
this.doGet(request, response);
} else if ("POST".equalsIgnoreCase(request.getMethod())) {
this.doPost(request, response);
}
}
//这里使用了模板设计模式
//让 HspHttpServlet 子类 HspCalServlet 来实现
public abstract void doGet(HspRequest request, HspResponse response);
public abstract void doPost(HspRequest request, HspResponse response);
}
3. 自己写的业务层,继承了HttpServlet的HshpCalServelt,这里最终是在这里面写如果是get()或post()会怎么走的业务
public class HspCalServlet extends HspHttpServlet {
@Override
public void doGet(HspRequest request, HspResponse response) {
//思路:
//1.先获取到num1和num2,相加
//2.将其结果返回个客户端
int num1 = WebUtils.parseInt(request.getParameter("num1"), 0);
int num2 = WebUtils.parseInt(request.getParameter("num2"), 0);
int sum = num1 + num2;
//将其结果返回给客户端
OutputStream outputStream = response.getOutputStream();
//将响应头 和 响应体拼接,并发出
try {
outputStream.write( (response.respHeader +
"<h1> " + num1 + "+" + num2 + "=" + sum + "HspTomcatV3 反射+xml</h1>").getBytes());
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void doPost(HspRequest request, HspResponse response) {
doGet(request, response);
}
@Override
public void init() throws ServletException {
}
@Override
public void destroy() {
}
}
Tomcat的设计
tomcat的主要任务就是通过读取web.xml,将业务servlet封装到自己维护的HashMap集合中,这样就可以让线程拆解请求数据,查找对应的servlet实例,并调用方法了
1.Tomcat维护了两个集合,一个是servletMapping主要存放(类名字,类的反射实例)一个是servletUrlMapping主要存放是(uri(客户端请求servle的路径), 类名字)
2.怎么将servlet实例加入到集合当中呢?
(1)读取当前Tomcat的运行路径,获取路径后加上web.xml文件,就得到其web.xml文件路径
(2)通过dom4J 来读取web.xml的dmo节点,读取到Root节点
(3)通过root节点,读取到web文件中所有servlet节点
(4)遍历servlet节点,读取每个servlet节点中servlet-name和servlet-class标签
(5)通过反射机制生成servlet的class实例,将其servle-name 和 servlet-class实例添加到集合大当中
3.在主方法中,创建servletSocket,放入端口号,并一直等待监听,调用其servlet实例加入的集合的方法,再调用线程
public class HspTomcatV3 {
//设计两个放置web.xml中元素的容器
public static final ConcurrentHashMap<String, HspHttpServlet>
servletMapping = new ConcurrentHashMap<>();
public static final ConcurrentHashMap<String, String>
servletUrlMapping = new ConcurrentHashMap<>();
public static void main(String[] args) {
HspTomcatV3 hspTomcatV3 = new HspTomcatV3();
hspTomcatV3.init();
//启动tomcat
hspTomcatV3.run();
}
//启动HspTomcat
public void run() {
try {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("============HspTomcatV3================");
while (!serverSocket.isClosed()) {
Socket socket = serverSocket.accept();
HspRequestHandler hspRequestHandler = new HspRequestHandler(socket);
//启动线程
new Thread(hspRequestHandler).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//直接对两个容器进行初始化
public void init() {
// 因其是自己手写的Servlet,没有继承java真正的Servlet类,所以web.xml是无法复制到
// 工作路径(也就是运行路径)中,所以需要手动复制,而1就是获取运行路径,将其手动复制到目录下
//1.先获取到 HspTomcatV3 运行的工作路径
String path = HspTomcatV3.class.getResource("/").getPath();
System.out.println(path);
//2.读取web.xml文件内容,通过dom4j来读取web.xml中的dom节
//2.1通过dom4j来读取web.xml中的dom节
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(new File(path + "web.xml"));
System.out.println(document);//验证有没有读取到web.xml
//3.得到web.xml中的根元素
Element rootElement = document.getRootElement();
//4.获取根元素中的所有element元素节点
List<Element> elements = rootElement.elements();
//5.遍历得到servlet和servlet-mapping element元素节点
//这里因是 Object 类型,所以无法使用Element类型中自己的方法,所以需要在List<Element>接种添加泛型
//这样再遍历时,就是Element类型,可以使用element类型中的自己的方法了
//for (Object element : elements) {}
for (Element element : elements) {
//6.判断是不是servlet 和 servlet-mapping
if ("servlet".equalsIgnoreCase(element.getName())) {
//7.获取servlet元素中的servlet-name 和 servlet-class
Element servletName = element.element("servlet-name");
Element elementClass = element.element("servlet-class");
//8.添加到servletMapping容器中
servletMapping.put(servletName.getText(),
// (1).这里实例化对象是Object对象,但因是在集合中泛型指定的是HspHttpServlet类型,所以需要强转
// Object o = Class.forName(elementClass.getText()).newInstance();
// (2).trim():是取消web.xml 中 <servlet-class> </servlet-class>中间填写配置两边的空格,
// 这样出现空格会无法识别,无法通过路径来进行发射实例化
// <servlet-class> com.hspedu.tomcat.servlet.HspCalServlet </servlet-class>两边的空格
(HspHttpServlet) Class.forName(elementClass.getText().trim()).newInstance());
} else if ("servlet-mapping".equalsIgnoreCase(element.getName())) {
//9.获取servlet-mapping标签中的 servlet-name 和 url-pattern
Element elementName = element.element("servlet-name");
Element urlPattern = element.element("url-pattern");
//10.添加到servletUrlMapping容器中
servletUrlMapping.put(urlPattern.getText(), elementName.getText());
}
}
} catch (Exception e) {
e.printStackTrace();
}
//查看容器中的元素
System.out.println("servletMapping= " + servletMapping);
System.out.println("servletUrlMapping= " + servletUrlMapping);
}
}
工具类WebUtils
public class WebUtils {
public static int parseInt(String str, int defaultVal){
try {
return Integer.parseInt(str);
} catch (NumberFormatException e) {
System.out.println("您所转换的字符串格式不对");
}
return defaultVal;
}
//判断其输入的uri是否为html
public static boolean isHtml(String uri) {
//判断字符串末尾是不是“.html”
return uri.endsWith(".html");
}
//根据文件名来读取该文件
public static String readHtml(String filename) {
//1.获取该类的工作路径
String path = com.hspedu.utils.WebUtils.class.getResource("/").getPath();
//2.读取cal.html
StringBuilder stringBuilder = new StringBuilder();
try {
//2.1 读取cal.html文件路径
BufferedReader bufferedReader =
new BufferedReader(new FileReader(path + filename));
String buf = "";
//2.2 按行来读取内容,并将内容赋值给StringBuffered
while ((buf = bufferedReader.readLine()) != null) {
stringBuilder.append(buf);
}
} catch (Exception e) {
e.printStackTrace();
}
//3.返回
return stringBuilder.toString();
}
}