手写一个简化版Tomcat
Posted 布衣小工
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手写一个简化版Tomcat相关的知识,希望对你有一定的参考价值。
Tomcat作为Web服务器深受市场欢迎,有必要对其进行深入的研究。在工作中,我们经常会把写好的代码打包放在Tomcat里并启动,然后在浏览器里就能愉快的调用我们写的代码来实现相应的功能了,那么Tomcat是如何工作的?
一、Tomcat工作原理
我们启动Tomcat时双击的startup.bat文件的主要作用是找到catalina.bat,并且把参数传递给它,而catalina.bat中有这样一段话:
Bootstrap.class是整个Tomcat 的入口,我们在Tomcat源码里找到这个类,其中就有我们经常使用的main方法:
这个类有两个作用 :1.初始化一个守护进程变量、加载类和相应参数。2.解析命令,并执行。
源码不过多赘述,我们在这里只需要把握整体架构,有兴趣的同学可以自己研究下源码。Tomcat的server.xml配置文件中可以对应构架图中位置,多层的表示可以配置多个:
即一个由 Server->Service->Engine->Host->Context 组成的结构,从里层向外层分别是:
- Server:服务器Tomcat的顶级元素,它包含了所有东西。
- Service:一组 Engine(引擎) 的集合,包括线程池 Executor 和连接器 Connector 的定义。
- Engine(引擎):一个 Engine代表一个完整的 Servlet 引擎,它接收来自Connector的请求,并决定传给哪个Host来处理。
- Container(容器):Host、Context、Engine和Wraper都继承自Container接口,它们都是容器。
- Connector(连接器):将Service和Container连接起来,注册到一个Service,把来自客户端的请求转发到Container。
- Host:即虚拟主机,所谓的”一个虚拟主机”可简单理解为”一个网站”。
- Context(上下文 ): 即 Web 应用程序,一个 Context 即对于一个 Web 应用程序。Context容器直接管理Servlet的运行,Servlet会被其给包装成一个StandardWrapper类去运行。Wrapper负责管理一个Servlet的装载、初始化、执行以及资源回收,它是最底层容器。
比如现在有以下网址,根据“/”切割的链接就会定位到具体的处理逻辑上,且每个容器都有过滤功能。
二、梳理自己的Tomcat实现思路
本文实现效果比较简单,仅供新手参考,大神勿喷。当浏览器访问对应地址时:
实现以上效果整体思路如下:
1.ServerSocket占用8080端口,用while(true)循环等待用户发请求。
2.拿到浏览器的请求,解析并返回URL地址,用I/O输入流读取本地磁盘上相应文件。
3.读取文件,不存在构建响应报文头、HTML正文内容,存在则写到浏览器端。
三、实现自己的Tomcat
工程文件结构和pom.xml文件:
1.HttpServer核心处理类,用于接受用户请求,传递HTTP请求头信息,关闭容器:
1 public class HttpServer { 2 // 用于判断是否需要关闭容器 3 private boolean shutdown = false; 4 5 public void acceptWait() { 6 ServerSocket serverSocket = null; 7 try { 8 //端口号,最大链接数,ip地址 9 serverSocket = new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1")); 10 } 11 catch (IOException e) { 12 e.printStackTrace(); 13 System.exit(1); 14 } 15 // 等待用户发请求 16 while (!shutdown) { 17 try { 18 Socket socket = serverSocket.accept(); 19 InputStream is = socket.getInputStream(); 20 OutputStream os = socket.getOutputStream(); 21 // 接受请求参数 22 Request request = new Request(is); 23 request.parse(); 24 // 创建用于返回浏览器的对象 25 Response response = new Response(os); 26 response.setRequest(request); 27 response.sendStaticResource(); 28 //关闭一次请求的socket,因为http请求就是采用短连接的方式 29 socket.close(); 30 //如果请求地址是/shutdown 则关闭容器 31 if(null != request){ 32 shutdown = request.getUrL().equals("/shutdown"); 33 } 34 } 35 catch (Exception e) { 36 e.printStackTrace(); 37 continue; 38 } 39 } 40 } 41 public static void main(String[] args) { 42 HttpServer server = new HttpServer(); 43 server.acceptWait(); 44 } 45 }
2.创建Request类,获取HTTP的请求头所有信息并截取URL地址返回:
1 public class Request { 2 private InputStream is; 3 private String url; 4 5 public Request(InputStream input) { 6 this.is = input; 7 } 8 public void parse() { 9 //从socket中读取一个2048长度字符 10 StringBuffer request = new StringBuffer(Response.BUFFER_SIZE); 11 int i; 12 byte[] buffer = new byte[Response.BUFFER_SIZE]; 13 try { 14 i = is.read(buffer); 15 } 16 catch (IOException e) { 17 e.printStackTrace(); 18 i = -1; 19 } 20 for (int j=0; j<i; j++) { 21 request.append((char) buffer[j]); 22 } 23 //打印读取的socket中的内容 24 System.out.print(request.toString()); 25 url = parseUrL(request.toString()); 26 } 27 28 private String parseUrL(String requestString) { 29 int index1, index2; 30 index1 = requestString.indexOf(‘ ‘);//看socket获取请求头是否有值 31 if (index1 != -1) { 32 index2 = requestString.indexOf(‘ ‘, index1 + 1); 33 if (index2 > index1) 34 return requestString.substring(index1 + 1, index2); 35 } 36 return null; 37 } 38 39 public String getUrL() { 40 return url; 41 } 42 43 }
3.创建Response类,响应请求读取文件并写回到浏览器
public class Response { public static final int BUFFER_SIZE = 2048; //浏览器访问D盘的文件 private static final String WEB_ROOT ="D:"; private Request request; private OutputStream output; public Response(OutputStream output) { this.output = output; } public void setRequest(Request request) { this.request = request; } public void sendStaticResource() throws IOException { byte[] bytes = new byte[BUFFER_SIZE]; FileInputStream fis = null; try { //拼接本地目录和浏览器端口号后面的目录 File file = new File(WEB_ROOT, request.getUrL()); //如果文件存在,且不是个目录 if (file.exists() && !file.isDirectory()) { fis = new FileInputStream(file); int ch = fis.read(bytes, 0, BUFFER_SIZE); while (ch!=-1) { output.write(bytes, 0, ch); ch = fis.read(bytes, 0, BUFFER_SIZE); } }else { //文件不存在,返回给浏览器响应提示,这里可以拼接html任何元素 String retMessage = "<h1>"+file.getName()+" file or directory not exists</h1>"; String returnMessage ="HTTP/1.1 404 File Not Found\r\n" + "Content-Type: text/html\r\n" + "Content-Length: "+retMessage.length()+"\r\n" + "\r\n" + retMessage; output.write(returnMessage.getBytes()); } } catch (Exception e) { System.out.println(e.toString() ); } finally { if (fis!=null) fis.close(); } } }
四、读者可以自己做的优化,扩展的点
1.在WEB_INF文件夹下读取web.xml解析,通过请求名找到对应的类名,通过类名创建对象,用反射来初始化配置信息,如welcome页面,Servlet、servlet-mapping,filter,listener,启动加载级别等。
2.抽象Servlet类来转码处理请求和响应的业务。发过来的请求会有很多,也就意味着我们应该会有很多的Servlet,例如:RegisterServlet、LoginServlet等等还有很多其他的访问。可以用到类似于工厂模式的方法处理,随时产生很多的Servlet,来满足不同的功能性的请求。
3.使用多线程技术。本文的代码是死循环,且只能有一个链接,而现实中的情况是往往会有很多很多的客户端发请求,可以把每个浏览器的通信封装到一个线程当中。
参考文档:https://my.oschina.net/liughDevelop/blog/1790893
以上是关于手写一个简化版Tomcat的主要内容,如果未能解决你的问题,请参考以下文章
为啥我的 C 代码片段不起作用?简化版可以。为 unsigned long long 传递不带 VA_ARGS 的 args