实际上 Tomcat 的原理更多的是关于 计算机网络 的知识
下文仅展示 Tomcat 的 核心功能 而不是全部功能
首先回顾Tomcat的核心作用
1、与浏览器建立连接,接收Http报文,解析获取请求和参数,然后根据请求作响应,最后将结果封装成Http报文响应给浏览器
2、管理Servlet应用的生命周期
要完成 第一点功能,需要明白 socket 和 http 报文
socket是网络通信必需的技术,主要作用就是让客户端和服务端建立连接,客户端和服务端都能从该连接中获取输入和输出流,从而进行数据交流
而http报文就是规定的一种数据格式,规定第几个字节是什么内容,这样双方才能知道对方说的是什么
要完成 第二点功能,需要明白 JAVA反射,反射就不过多介绍
Tomcat为了完成上述功能,需要经历三个阶段
1、初始化阶段:Web容器加载Servlet类并创建Servlet对象,Servlet容器会运行该对象的init()方法对该对象进行初始化。
2、运行时阶段:请求到达时,Servlet容器会针对该请求创建ServletRequest对象和ServletResponse对象,然后调用相关Servlet对象的service()方法,service()方法会根据请求调用对应的doGet或doPost等方法。
3、销毁阶段:Java web应用被终止时,Servlet容器会调用所有Servlet对象的destroy()方法(释放Servlet对象所占用的资源)。
现在我们就依次实现这三个周期(结尾有完整代码)
初始阶段
在初始阶段,Tomcat需要扫描 Sevlet 所在的目录,获取该目录下的所有 .java 后缀文件,获取这些Java类的全类名,然后通过反射,获取类上的注解,挑选出包含 @Servlet 注解的类,极为Servlet,实际上 @Servlet 可以自己定义,该注解应该至少包含一个属性用于标注Servlet的路径,如下
//该注解可以应用于类、接口(包括注解类型)、枚举ElementType.TYPE) (//该注解标记的元素可以被Javadoc 或类似的工具文档化//该注解的生命周期,由JVM 加载,包含在类文件中,在运行时可以被获取到RetentionPolicy.RUNTIME) (public@interfaceServletDemo { Stringpath() default""; }
初级阶段代码
全局静态变量
//用于存放全类名privatestaticArrayList<String>arr=newArrayList<>(); //用于存放Servlet的类对象privatestaticHashMap<String,Class>servletMap=newHashMap<>();
main方法内容 ,其中的Servlet所在目录要根据自己的实际情况进行修改
//启动阶段StringinputPath="D:\\javaDemo\\src"; //Servlet所在目录Filefile=newFile(inputPath); //获取其file对象func(file); //调用该方法用于获取 .java 后缀文件,并获取全类名System.out.println(arr); choseServlet(); //调用该方法获取 Servlet 类对象System.out.println(servletMap);
main中调用到的全局静态方法
privatestaticvoidfunc(Filefile) { File[] fs=file.listFiles(); Strings; for (Filef : fs) { if (f.isDirectory()) //若是目录,则递归打印该目录下的文件func(f); if (f.isFile()){ s=f.toString().split("src")[1]; s=s.substring(1); if(s.length()>=5&&s.substring(s.length()-5).equals(".java")){ s=s.replace('\\','.'); s=s.substring(0,s.length()-5); arr.add(s); } } } }
privatestaticvoidchoseServlet() throwsClassNotFoundException { for(inti=0;i<arr.size();i++){ Stringpath=arr.get(i); Class<?>cl=Class.forName(path); if(cl.isAnnotationPresent(ServletDemo.class)){ servletMap.put(cl.getAnnotation(ServletDemo.class).path(),cl); } } }
运行阶段
运行阶段通过socket与浏览器建立连接,然后处理浏览器的请求,并做出响应,为了简单起见,在这我使用了单线程来完成,但真正的Tomcat是多线程的
下面代码完成的内容是注册端口,建立连接,并接收来之浏览器的Http报文
//注册端口InetAddresslocalHost=InetAddress.getLocalHost(); System.out.println("localhost:"+localHost); ServerSocketserverSocket=newServerSocket(8080,10, localHost); //单线程下System.out.println("等待建立连接"); Socketserver=serverSocket.accept(); System.out.println("连接已建立"); //定义线程去接收 Http 报文HttpAcceptThreadhttpAcceptThread=newHttpAcceptThread(server); Threadaccept=newThread(httpAcceptThread); accept.start(); accept.join(); //处理请求requestHttp(server,httpAcceptThread.strings.get(0));
classHttpAcceptThreadimplementsRunnable{ privateSocketsocket; ArrayList<String>strings=newArrayList<>(); publicHttpAcceptThread(Socketsocket) { this.socket=socket; } publicvoidrun() { try { System.out.println("开始接收Http"); BufferedReaderreader=newBufferedReader(newInputStreamReader(socket.getInputStream())); Strings; while ((s=reader.readLine()).length()!=0){ //每次循环接收一行的Http数据try { strings.add(s); System.out.println(s); }catch (Exceptione){ System.out.println("接收Http进程结束"); break; } } System.out.println("接收Http进程结束"); } catch (IOExceptione) { e.printStackTrace(); } } }
最后的重点就是对请求作处理了,在这一步中主要是解析Http报文,获取请求方式和请求内容,通过反射调用请求的方法,并且返回结果给浏览器,实际上响应Http报文的封装以及回传应该在Tomcat中完成,这里简单起见,直接在Servlet中完成
privatestaticvoidrequestHttp(Socketsocket,Stringhttp) throwsNoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, IOException { //获取请求方式StringrequestStyle=http.split(" ")[0]; if(requestStyle.equals("GET")){ StringhttpPathAndParameter=http.split(" ")[1]; StringhttpPath; //创建HttpRequest对象HttpRequestDemohttpRequestDemo=newHttpRequestDemo(); if(httpPathAndParameter.indexOf("?")!=-1){ httpPath=httpPathAndParameter.substring(1); httpPath=httpPath.split("\\?")[0]; System.out.println(httpPath); StringparameterString=httpPathAndParameter.split("\\?")[1]; String[] parameters=parameterString.split("&"); for (inti=0;i<parameters.length;i++){ httpRequestDemo.map.put(parameters[i].split("=")[0],parameters[i].split("=")[1]); } }else{ httpPath=httpPathAndParameter.substring(1); System.out.println(httpPath); } //创建HttpResponse对象OutputStreamoutputStream=socket.getOutputStream(); HttpResponseDemohttpResponseDemo=newHttpResponseDemo(outputStream); //反射调用doGetClass<?>servletClass=servletMap.get(httpPath); Methodmethod=servletClass.getMethod("doGet",HttpRequestDemo.class,HttpResponseDemo.class); method.invoke(servletClass.newInstance(),httpRequestDemo,httpResponseDemo); }else{ StringhttpPath=http.split(" ")[1]; httpPath=httpPath.substring(1); System.out.println(httpPath); HttpRequestDemohttpRequestDemo=newHttpRequestDemo(); OutputStreamoutputStream=socket.getOutputStream(); HttpResponseDemohttpResponseDemo=newHttpResponseDemo(outputStream); Class<?>servletClass=servletMap.get(httpPath); Methodmethod=servletClass.getMethod("doPost",HttpRequestDemo.class,HttpResponseDemo.class); method.invoke(servletClass.newInstance(),httpRequestDemo,httpResponseDemo); } }
其中一个Servlet的源码如下
importjava.io.IOException; importjava.io.OutputStream; importjava.util.Map; path="address1") (publicclassServlet1 { publicvoiddoGet(HttpRequestDemorequest,HttpResponseDemoresponse) throwsIOException { System.out.println("address1 GET响应:"); System.out.println("a="+request.getParameter("a")); System.out.println("\n响应的http如下:"); Stringresp=HttpResponseDemo.responsebody+"<!DOCTYPE html>\n"+"<html>\n"+"<head>\n"+" <meta charset=\"utf-8\" />\n"+"</head>\n"+"<body>\n"+" \n"+" <form name=\"my_form\" method=\"POST\">\n"+" <input type=\"button\" value=\"按下\" onclick=\"alert('你按下了按钮')\">\n"+" </form>\n"+" \n"+"</body>\n"+"</html>"; System.out.println(resp); response.outputStream.write(resp.getBytes()); response.outputStream.flush(); response.outputStream.close(); } publicvoiddoPost(HttpRequestDemorequest,HttpResponseDemoresponse) throwsIOException { System.out.println("\n响应的http如下:"); Stringresp=HttpResponseDemo.responsebody+"{\"sorry\":\"we only respond to method GET now\"},\r\n"+""; System.out.println(resp); response.outputStream.write(resp.getBytes()); response.outputStream.flush(); response.outputStream.close(); } }
HttpRequestDemo 如下
importjava.util.HashMap; publicclassHttpRequestDemo { publicHashMap<String, String>map=newHashMap<>(); publicStringgetParameter(Stringkey) { returnmap.get(key); } }
HttpResponseDemo 如下
importjava.io.OutputStream; publicclassHttpResponseDemo { publicOutputStreamoutputStream; publicstaticfinalStringresponsebody="HTTP/1.1 200+\r\n"+"Content-Type:text/html+\r\n"+"\r\n"; publicHttpResponseDemo(OutputStreamoutputStream){ this.outputStream=outputStream; } }
我将代码开源到了 gitee 中,需要的通过以下获取
效果展示
运行tomcatDemo主方法,控制台会输出
这里启动阶段完成,控制台输出了 url 路径对应的 Servlet 类对象
并输出了本机的IP地址 169.254.214.160
然后浏览器进行访问
编辑
得到的结果
控制台输出接受到的Http请求报文
以及响应的Http报文
并且中间也获取到了 url 中传入的参数
到此一个Tomcat核心功能仿写完成,你也去试试吧 ^-^