一步一步实现一个破产版tomcat
tomcat服务器在我们java后台开发中可以说是经常接触到的,每次当我们写好我们的程序时,都要启动tomcat才能生效。
那么我们有没有想过tomcat到底为我们完成了什么事情?再进一步的说,我们开发时肯定离不开Servlet,那就有一个疑问为什么我们写好的Sevlet并不需要编写一个main函数new一个示例就可以生效执行我们的代码?其实这些工作都是由tomcat默默帮我们完成了。接下来我们可以自己写一个简易的破产版tomcat来简单的理解其中的奥妙。
一、动手前的准备工作
- 1.对HTTP协议有所了解
- 2.了解对javase的io流操作
- 3.了解javase的Socket编程
- 4.了解java的反射机制
如果你已经准备好了,那么现在就开始和我一起动手吧
二、需求分析
-
1.建立一个专门存放静态资源文件的WebContent文件夹,并能够在浏览器实现对静态资源的访问
-
2.能够实现浏览器对Servlet的访问(即对动态资源的访问)
-
3.在浏览器发送shutdown能实现对tomcat的关闭。
三、实现对静态资源文件的访问
首先,我们需要打开ide创建好项目,并且在项目的路径下创建WebContent文件夹存放我们写好的html文件,demo_1.html和demo_2.html,并创建好存放我们代码的包,里面包含TomcatServer类来完成我们本次的工作。结构图如下:
demo_01.html内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>demo_01.html</title>
</head>
<body>
<h1>这是demo_01html</h1>
</body>
</html>
demo_02.html内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>demo_02.html</title>
</head>
<body>
<h1>这是demo_02.html</h1>
</body>
</html>
接下来终于进入正题了.我们的服务器需要处理对WebContent目录中的静态资源的请求,因此我们需要定义一个final常量WEB_ROOT来存放WebContent的路径,同时还要考虑到良好的跨平台性。我们可以这样写。
private static final String WEB_ROOT=System.getProperty("user.dir")+File.separator+"WebContent";
我们还需要一个变量url存放客户端请求的资源(即端口号后面的部分)
private static String url = "";
最后我们还需要一个常量存放停止tomcat命令,和一个布尔变量判断是否停止
private static final String SHUTDOWN_COMMAND="SHUTDOWN";
private static boolean shutdown=false;
经过这些工作之后我们终于可以写我们的main函数,我监听的是8080端口
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
InputStream is = null;
OutputStream out = null;
try {
serverSocket = new ServerSocket(8080);
while (!shutdown) {
socket = serverSocket.accept();
is = socket.getInputStream();
out = socket.getOutputStream();
//解析http请求并完成对url的赋值
parse(is);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != is) {
is.close();
}
if (null != out) {
out.close();
}
if (null != socket) {
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
对于parse(is)函数,我们通过输入流获取到全部的http请求(http请求的发送在客户端的浏览器已经帮我们完成了)。
/**
* 解析http请求
*/
private static void parse(InputStream is) throws IOException {
StringBuffer httprequest=new StringBuffer(2048);
byte[] bytes=new byte[2048];
int len=is.read(bytes);
httprequest.append(new String(bytes, 0, len));
//获取url,parseurl(httprequest)函数完成后记得去掉注释
//parseurl(httprequest);
System.out.println(httprequest);
}
然后我们在浏览器中输入http://localhost:8080/demo01.html控制台打印,成功获取到了http请求
GET /demo_01.html HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
现在要完成parseurl(is)函数,就是要获取到http请求行中的demo_01.html,通过简单的字符串操作就行了.
/**
* 获取http请求中的请求资源名
* @param httprequest
*/
private static void parseurl(StringBuffer httprequest) {
int index1, index2;
index1 = httprequest.indexOf(" ");
if (index1 != -1) {
index2 = httprequest.indexOf(" ", index1 + 1);
if (index2 > index1) {
url = httprequest.substring(index1 + 2, index2);
}
System.out.println(url);
}
}
}
再次在浏览器发起请求,可以发现控制台打印的正是我们要请求的资源名称。
请求工作完成后,接下来就是发送静态资源的响应工作了,代码如下
/**
* 发送静态资源文件
*/
private static void sendStaticResource(OutputStream out) {
byte[] bytes = new byte[2048];
FileInputStream fis = null;
try {
File file = new File(WEB_ROOT, url);
//如果文件存在就把该文件内容发送到客户端的浏览器
if (file.exists()) {
out.write("HTTP/1.1 200 OK\n".getBytes());
out.write("Server:Apache-Coyote/1.1\n".getBytes());
out.write("Content-Type:text/html;charset=utf-8\n".getBytes());
out.write("\n".getBytes());
fis = new FileInputStream(file);
int len = fis.read(bytes);
while (len != -1) {
out.write(bytes, 0, len);
len = fis.read(bytes);
}
}
else {
out.write("HTTP/1.1 404 not found\n".getBytes());
out.write("Server:apache-Coyote/1.1\n".getBytes());
out.write("Content-Type:text/html;charset=utf-8\n".getBytes());
out.write("\n".getBytes());
String errorMessage = "file not found";
out.write(errorMessage.getBytes());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != fis) {
fis.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
虽然代码是有点长,但是逻辑是十分的简单的,我们把获取到的静态资源名称(即url)去到存放静态资源文件的WebContent文件夹去查找,如果存在就返回200和文件中的内容,不存在就返回404和file not found错误信息。更多的是要熟悉http协议,因此我才在最前面提醒要了解常用http协议.
接下来我们把该函数放入main函数进行测试,我们在浏览器输入http://localhost:8080/demo_01.html,显示如下:
在浏览器输入http://localhost:8080/demo_02.html显示如下:
四、实现对动态资源的访问
首先我们要模仿实现一个简易的Servlet,因此我们创建一个Servlet接口,并且拥有如下生命周期,我们直接使用InputStream和OutputStream作为参数
/**
* init创建之后执行
* service请求后执行
* destroy销毁后执行
*/
public interface Servlet {
public void init();
public void service(InputStream is, OutputStream out);
public void destroy();
}
然后创建两个类Servlet_01和Servlet_02来实现该接口成为一个Servlet,分别如下
public class Servlet_01 implements Servlet{
@Override
public void init() {
System.out.println("Servlet_01被创建");
}
@Override
public void service(InputStream is, OutputStream out) {
System.out.println("Servlet_01被执行");
}
@Override
public void destroy() {
System.out.println("Servlet_01被销毁");
}
}
public class Servlet_02 implements Servlet{
@Override
public void init() {
System.out.println("Servlet_02被创建");
}
@Override
public void service(InputStream is, OutputStream out) {
System.out.println("Servlet_02被执行");
}
@Override
public void destroy() {
System.out.println("Servlet_02被销毁");
}
}
在我们平时写完Servlet之后我们就需要在xml中进行配置或者在高版本的Servlet中进行注解配置。为了节省篇幅,这里我们使用properties文件进行配置Servlet映射让我们的tomcat服务器能够找到.
在WebContent下创建mapper.properties文件,并添加如下内容
Servlet1=com.tuling.mytomcat.Servlet_01
Servlet2=com.tuling.mytomcat.Servlet_02
同时我们需要在服务器一启动就能够把我们的配置文件内容进行加载,因此我们需要添加一个static代码块加载配置文件和一个Map存放键值对。代码如下:
private static Map<String, String> map = new HashMap<String, String>();//存放配置文件中的键值信息
/**
* 服务器一启动就加载配置文件
*/
static{
Properties prop = new Properties();
try {
prop.load(new FileInputStream(WEB_ROOT + File.separator + "conf.properties"));
Set set = prop.keySet();
Iterator iterator = set.iterator();
//迭代操作
while (iterator.hasNext()) {
String key = (String) iterator.next();
String value = prop.getProperty(key);
map.put(key, value);
}
} catch (IOException e) {
e.printStackTrace();
}
}
然后我们就可以写一个方法利用java的反射机制动态的创建对象实例,并执行他的方法
/**
* 发送动态资源
*/
private static void sendDynamicResource(InputStream is,OutputStream out) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
out.write("HTTP/1.1 200 OK\n".getBytes());
out.write("Server:apache-Coyote/1.1\n".getBytes());
out.write("Content-Type:text/html;charset=utf-8\n".getBytes());
out.write("\n".getBytes());
//如果配置文件中的键匹配则创建Servlet实例并执行方法
if (map.containsKey(url)) {
String value = map.get(url);
Class clazz = Class.forName(value);
Servlet servlet = (Servlet) clazz.newInstance();
servlet.init();
servlet.service(is,out);
}
}
最后我们添加该方法到主函数,但我们还要判断请求的资源是动态还是静态,在这里我们简单的用后缀名判断,有后缀名就是静态,没有后缀名就是动态。主函数修改如下。
if (null != url) {
if (url.indexOf(".") != -1) { //如果有后缀名
sendStaticResource(out);
} else {
sendDynamicResource(is,out);
}
}
进行测试,我们浏览器输入http://localhost:8080/Servlet1然后进行关闭,再输入http://localhost:8080/Servlet2,控制台打印结果。
五、程序的关闭
//main函数修改如下
if (null != url) {
if(!url.equals(SHUTDOWN_COMMAND)) {
if (url.indexOf(".") != -1) {
sendStaticResource(out);
} else {
sendDynamicResource(is, out);
}
}
else{
shutdown=true;
}
}
当我们发送http://localhost:8080/SHUTDOWN程序关闭
六、总结
最终的源码如下
public class TomcatServer {
private static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "WebContent";
private static String url = "";
private static final String SHUTDOWN_COMMAND = "SHUTDOWN";
private static boolean shutdown = false;
private static Map<String, String> map = new HashMap<String, String>();
/**
* 服务器一启动就加载配置文件
*/
static{
Properties prop = new Properties();
try {
prop.load(new FileInputStream(WEB_ROOT + File.separator + "mapper.properties"));
Set set = prop.keySet();
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
String key = (String) iterator.next();
String value = prop.getProperty(key);
map.put(key, value);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
InputStream is = null;
OutputStream out = null;
try {
serverSocket = new ServerSocket(8080);
while (!shutdown) {
socket = serverSocket.accept();
is = socket.getInputStream();
out = socket.getOutputStream();
parse(is);
if (null != url) {
if(!url.equals(SHUTDOWN_COMMAND)) {
if (url.indexOf(".") != -1) {
sendStaticResource(out);
} else {
sendDynamicResource(is, out);
}
}
else{
shutdown=true;
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != is) {
is.close();
}
if (null != out) {
out.close();
}
if (null != socket) {
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 解析http请求
*/
private static void parse(InputStream is) throws IOException {
StringBuffer httprequest = new StringBuffer(2048);
byte[] bytes = new byte[2048];
int len=is.read(bytes);
httprequest.append(new String(bytes, 0,len ));
parseurl(httprequest);
}
/**
* 获取http请求中的请求资源名
*
* @param httprequest
*/
private static void parseurl(StringBuffer httprequest) {
int index1, index2;
index1 = httprequest.indexOf(" ");
if (index1 != -1) {
index2 = httprequest.indexOf(" ", index1 + 1);
if (index2 > index1) {
url = httprequest.substring(index1 + 2, index2);
}
}
}
/**
* 发送动态资源
*/
private static void sendDynamicResource(InputStream is,OutputStream out) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
out.write("HTTP/1.1 200 OK\n".getBytes());
out.write("Server:apache-Coyote/1.1\n".getBytes());
out.write("Content-Type:text/html;charset=utf-8\n".getBytes());
out.write("\n".getBytes());
//如果配置文件中的键匹配则创建Servlet实例并执行方法
if (map.containsKey(url)) {
String value = map.get(url);
Class clazz = Class.forName(value);
Servlet servlet = (Servlet) clazz.newInstance();
servlet.init();
servlet.service(is,out);
}
}
/**
* 发送静态资源文件
*/
private static void sendStaticResource(OutputStream out) {
byte[] bytes = new byte[2048];
FileInputStream fis = null;
try {
File file = new File(WEB_ROOT, url);
if (file.exists()) {
out.write("HTTP/1.1 200 OK\n".getBytes());
out.write("Server:Apache-Coyote/1.1\n".getBytes());
out.write("Content-Type:text/html;charset=utf-8\n".getBytes());
out.write("\n".getBytes());
fis = new FileInputStream(file);
int len = fis.read(bytes);
while (len != -1) {
out.write(bytes, 0, len);
len = fis.read(bytes);
}
} else {
out.write("HTTP/1.1 404 not found\n".getBytes());
out.write("Server:apache-Coyote/1.1\n".getBytes());
out.write("Content-Type:text/html;charset=utf-8\n".getBytes());
out.write("\n".getBytes());
String errorMessage = "file not found";
out.write(errorMessage.getBytes());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != fis) {
fis.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
总算是实现了一个属于自己的破产版tocat,但在我们平时的生产中,肯定还是要站在巨人的肩膀上。tomcat本身是一个非常复杂的系统,包含了很多的功能模块,如果对tomcat本身十分感兴趣而且想了解他的架构,推荐去看《How Tomcat Works》一本全英书,如果英语和作者一样差的话可以去看《深入剖析Tomcat》,这本书对前者进行了翻译,作者会从0开始一步一步去实现tomcat的功能,读完肯定能让你对tomcat有进一步的深入。
感谢你们阅读我编写的推文!今天就介绍这么多啦,你们能够有所收获,代码也不一定完全的正确,如果我有任何写得不正确和不准确的地方,欢迎大家向我提出来,我们可以一起学习和交流!