手写一个简易版的 Tomcat!
共 9586字,需浏览 20分钟
·
2021-11-23 20:23
前言
使用tomcat的时候当浏览器输入url之后,开始发送http请求,这个请求发送到哪儿呢,Url解析的过程中
1 先通过域名解析请求得到ip 2 然后通过ip找到对应的主机 3 再通过响应的端口找到进程 4 然后再去根据程序去处理这个请求,再到原路返回
思考
对于1,2步骤我们本地测试可以不用去扣这个,明白这么回事儿就可以,本地localhost
实际上对应我们自己本机127.0.0.1
对于第三部,我们本地可以去通过一个socket去监听响应的端口,去获取到请求,然后再响应给客户端让客户端浏览器去解析我们返回的http报文,从而展示数据;
具体如下步骤:
1)提供服务,接收请求(可以使用Socket通信)
2)请求信息封装成Request(Response)
3)客户端请求资源,资源分为静态资源(html)和动态资源(Servlet)
4)资源返回给客户端浏览器
具体实现
首先新建maven工程
然后定义一个启动类Bootstrap
然后实现一个启动方法start
,在这个方法中启动一个socket监听8080端口。
package com.udeam.v1;
import com.udeam.util.HttpUtil;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 启动类入库
* 用于启动tomcat
*/
public class Bootstrap {
/**
* 监听端口号
* 用于启动socket监听的端口号
*/
private int port = 8080;
/**
* 启动方法
*/
public void start() throws IOException {
//返回固定字符串到客户端
ServerSocket socket = new ServerSocket(port);
System.out.println("--------- start port : " + port);
while (true) {
Socket accept = socket.accept();
//获取输入流
//InputStream inputStream = accept.getInputStream();
//输出流
OutputStream outputStream = accept.getOutputStream();
System.out.println(" ------ 响应返回内容 : " + result);
outputStream.write("hello world ...".getBytes());
outputStream.flush();
outputStream.close();
socket.close();
}
}
public static void main(String[] args) {
try {
new Bootstrap().start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
通过这个Socket返回hello world...
给客户端
我们浏览器输入127
可以看到后台代码输出信息
前台浏览器显示信息
返回信息浏览器不认,响应无效,出现这种情况是浏览器只认识http报文,故此需要包装一个返回浏览器,然后浏览器才能解析
新建一个http包装类HttpUtil
包装响应信息给浏览器
这里我们只返回200和404状态的
package com.udeam.util;
/**
* 封装http响应
*/
public class HttpUtil {
/**
* 404 page
*/
private static final String content = "404 page...
";
/**
* 添加响应头信息
*
* http响应体格式
*
* 响应头(多参数空格换行)
* 换行
* 响应体
*/
public static String addHeadParam(int len) {
String head = "HTTP/1.1 200 OK \n";
head += "Content-Type: text/html; charset=UTF-8 \n";
head += "Content-Length: " + len + " \n" + "\r\n";
return head;
}
/**
* 4040响应
*
* @return
*/
public static String resp_404() {
String head = "HTTP/1.1 404 not found \n";
head += "Content-Type: text/html; charset=UTF-8 \n";
head += "Content-Length: " + content.length() + " \n" + "\r\n";
return head + content;
}
/**
* 200响应
*
* @param content 响应内容
* @return
*/
public static String resp_200(String content) {
return addHeadParam(content.length()) + content;
}
}
然后再请求,可以看到成功返回信息
然后我们再去请求一个静态页面index.html
新建一个html页面
html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
hello tomcat....
body>
html>
这次前台请求Url是http://localhost:8080/index.html
还是从Socket进行监听8080端口
请求静态的html,那如何去在后台找到这个资源呢?
通过url也就是/index.html
去找到这个文件,后台文件我们去放到resource
下
那如何获取url呢?
浏览器在请求后台的时候发送的也是http请求,我们可以获取http请求报文
可以看一下,请求报文
从请求头中获取到url以及method等
获取输入流
Socket accept = socket.accept();
//获取输入流
InputStream inputStream = accept.getInputStream();
然后对输入流进行解析,通过解析http请求头第一行得到url和method封装到Request
对象中
/**
* 封装的请求实体类
*/
public class Request {
/**
* 请求方式
*/
private String method;
/**
* 请求url
*/
private String url;
/**
* 输入流
*/
public InputStream inputStream;
public Request() {
}
//构造器输入流
public Request(InputStream inputStream) throws IOException {
this.inputStream = inputStream;
//读取请求信息,封装属性
int count = 0;
//读取请求信息
while (count == 0) {
count = inputStream.available();
}
byte[] b = new byte[count];
inputStream.read(b);
String reqStr = new String(b);
System.out.println("请求信息 : " + reqStr);
//根据http请求报文 换行符截取
String[] split = reqStr.split("\\n");
//获取第一行请求头信息
String s = split[0];
//根据空格进行截取请求方式和url
String[] s1 = s.split(" ");
System.out.println("method : " + s1[0]);
System.out.println("url : " + s1[1]);
this.method = s1[0];
this.url = s1[1];
}
//.... get set省略
}
然后根据请求的url从磁盘找到静态资源读取到然后以流的形式返回给浏览器
这里封装返回对象Response
public class Response {
/**
* 响应流
*/
private OutputStream outputStream;
public Response(OutputStream outputStream) {
this.outputStream = outputStream;
}
//输出指定字符串
public void outPutStr(String content) throws IOException {
outputStream.write(content.getBytes());
outputStream.flush();
outputStream.close();
}
}
根据url获取静态资源
public void outPutHtml(String url) throws IOException {
//排除浏览器的/favicon.ico请求
if (("/favicon.ico").equals(url)){
return;
}
//获取静态资源的绝对路径
String abPath = ResourUtil.getStaticPath(url);
//查询静态资源是否存在
File file = new File(abPath);
if (file.exists()) {
//输出静态资源
ResourUtil.readFile(new FileInputStream(abPath), outputStream);
} else {
//404
try {
outPutStr(HttpUtil.resp_404());
} catch (IOException e) {
e.printStackTrace();
}
}
}
ResourUtil 工具类,封装解析读取静态资源。
/**
* 静态资源工具类
*/
public class ResourUtil {
/**
* 获取classes文件目录
*/
private static URL url = ResourUtil.class.getClassLoader().getResource("\\\\");
/**
* 获取静态资源文件路径
*
* @param path
* @return
*/
public static String getStaticPath(String path) throws UnsupportedEncodingException {
//获取目录的绝对路径
try {
String decode = URLDecoder.decode(url.getPath(), "UTF-8");
String replace1 = decode.replace("\\", "/");
String replace2 = replace1.replace("//", "");
replace2 = replace2.substring(0,replace2.lastIndexOf("/")) + path;
return replace2;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
/**
* 读取静态资源文件输入流
*
* @param inputStream
*/
public static void readFile(InputStream inputStream, OutputStream outputStream) throws IOException {
int count = 0;
//读取请求信息
while (count == 0) {
count = inputStream.available();
}
int content = 0;
//读取文件
content = count;
//输出头
outputStream.write(HttpUtil.addHeadParam(content).getBytes());
//输出内容
long written = 0;
int byteSize = 1024;
byte[] b = new byte[byteSize];
//读取
while (written < content) {
if (written + 1024 > content) {
byteSize = (int) (content - written);
b = new byte[byteSize];
}
inputStream.read(b);
outputStream.write(b);
outputStream.flush();
written += byteSize;
}
}
}
socket中完整请求代码
public void start() throws IOException {
//返回固定字符串到客户端
ServerSocket socket = new ServerSocket(port);
System.out.println("--------- start port : " + port);
while (true) {
Socket accept = socket.accept();
//获取输入流
InputStream inputStream = accept.getInputStream();
//封装请求和响应对象
Request request = new Request(inputStream);
Response response = new Response(accept.getOutputStream());
response.outPutHtml(request.getUrl());
}
}
浏览器测试,可以看到正确返回
接下来实现定义请求动态资源,具体实现在java web中处理一个请求是使用servlet请求
tomcat处理servlet请求需要实现servlet规范
什么是servlet规范呢?
简单来说就是http请求在接收到请求之后将请求交给Servlet容器来处理,Servlet容器通过Servlet接口来调用不同的业务类,这一整套称作Servlet规范;
接口规范
/**
* 自定义servlet规范
*/
public interface Servlet {
void init() throws Exception;
void destory() throws Exception;
void service(Request request, Response response) throws Exception;
}
实现
/**
* 实现servlet规范
*/
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);
}
}
}
业务请求servlet
/**
* 业务类servelt
*/
public class MyServlet extends HttpServlet {
@Override
public void init() throws Exception {
}
@Override
public void doGet(Request request, Response response) {
//动态业务请求
String content = " GET 业务请求
";
try {
response.outPutStr(HttpUtil.resp_200(content));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void doPost(Request request, Response response) {
//动态业务请求
String content = " Post 业务请求
";
try {
response.outPutStr(HttpUtil.resp_200(content));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void destory() throws Exception {
}
}
定义完之后,如何请求呢,如何根据请求Ur去得到相应的servlet
在Java web中我们是在web.xml中进行配置,同样新建web.xml,配置servlet
<web-app>
<servlet>
<servlet-name>testservlet-name>
<servlet-class>com.udeam.v4.MyServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>testservlet-name>
<url-pattern>/testurl-pattern>
servlet-mapping>
web-app>
解析web.xml文件
讲url和每一个servlet对应起来存储到map中
/**
* 加载解析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 selectNodes = rootElement.selectNodes("//servlet");
for (int i = 0; i < selectNodes.size(); i++) {
Element element = selectNodes.get(i);
// test
Element servletnameElement = (Element) element.selectSingleNode("servlet-name");
String servletName = servletnameElement.getStringValue();
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 + "']");
// /test
String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
}
} catch (DocumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
启动方法
根据url找到servlet去执行service方法
private static final Map servletMap = new HashMap<>();
public void start() throws IOException {
ServerSocket socket = new ServerSocket(port);
System.out.println("--------- start port : " + port);
while (true) {
Socket accept = socket.accept();
//获取输入流
InputStream inputStream = accept.getInputStream();
//封装请求和响应对象
Request request = new Request(inputStream);
Response response = new Response(accept.getOutputStream());
//静态资源
if (request.getUrl().contains(".html")) {
response.outPutHtml(request.getUrl());
} else {
if (!servletMap.containsKey(request.getUrl())) {
response.outPutStr(HttpUtil.resp_200(request.getUrl() + " is not found ... "));
} else {
HttpServlet httpServlet = servletMap.get(request.getUrl());
try {
//处理请求
httpServlet.service(request, response);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
然后请求http://localhost:8080/test
可以看到正确返回
这里在deget方法中增加睡眠停顿模拟业务请求时间
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
请求可以可以看到请求阻塞,这是因为同一个socket 当前test这个没有请求结束,第二个请求进来然后阻塞。
必须等到第一个请求结束后才能处理请求。
然后再请求index.html
发现,并不是静态资源并不是立即返回,需要等到test请求结束后才能返回
故此需要对这个进行改造,让彼此请求互不干扰
我们可以使用多线程来进行解决,线程互不干扰,每个请求去执行
在Socket中添加方法
//1 单线程处理
MyThread myThread = new MyThread(httpServlet, response, request);
new Thread(myThread).start();
线程是宝贵的资源,频繁创建和销毁线程对开销很大,故此使用线程池来解决
/**
* 参数可以配置在xml里面
*/
private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 1, TimeUnit.HOURS, new ArrayBlockingQueue<>(500));
//2 线程池执行
threadPoolExecutor.submit(myThread);
threadPoolExecutor.shutdown();
这样子就可以立即返回响应,互不干扰;
简易版的tomcat实现就可以实现了,代码的话没有像tomcat那样子可以将war包解析之类的..
而且代码耦合性也大,tomcat和业务代码在一个Maven中...
说明
分别在指定包下如v1,v2,v3,v4每个代表一个版本
v1 简单的返回指定字符串 v2 返回静态页面 v3 单线程处理servelt请求(多个请求会阻塞) v4 多线程处理
其中需要用到解析xml依赖
<dependencies>
<dependency>
<groupId>dom4jgroupId>
<artifactId>dom4jartifactId>
<version>1.6.1version>
dependency>
<dependency>
<groupId>jaxengroupId>
<artifactId>jaxenartifactId>
<version>1.1.6version>
dependency>
dependencies>
至此,一个简单的 tomcat 就撸完了,如需本文源码,请加我微信:xttblog2,免费发送!