老铁们,大家好,相信还有很多朋友对于手写迷你 Tomcat 体验Java 核心和的相关问题不太懂,没关系,今天就由我来为大家分享分享手写迷你 Tomcat 体验Java 核心以及的问题,文章篇幅可能偏长,希望可以帮助到大家,下面一起来看看吧!
基本结构
tomcat架构图
我们可以把上面这张架构图做简化,简化后为:
什么是http协议
Http是一种网络应用层协议,规定了浏览器与web服务器之间如何通信以及数据包的结构。
通信大致可以分为四步:
- 先建立连接。
- 发送请求数据包。
- 发送响应数据包。
- 关闭连接。
优点
web服务器可以利用有限的连接为尽可能多的客户请求服务。
tomcat中Servlet的运作方式
- 在浏览器地址栏输入http://ip:port/servlet-day01/hello
- 浏览器依据IP、port建立连接(即与web服务器之间建立网络连接)。
- 浏览器需要将相关数据打包(即按照http协议要求,制作一个 请求数据包,包含了一些数据,比如请求资源路径),并且将请求 数据包发送出去。
- web服务器会将请求数据包中数据解析出来,并且将这些数据添加 到request对象,同时,还会创建一个response对象。
- web服务器创建Servlet对象,然后调用该对象的service方法(会将request和response作为参数)。注:在service方法里面,通过使用request获得请求相关的数据, 比如请求参数值,然后将处理结果写到response。
- web服务器将response中的数据取出来,制作响应数据包,然后发送给浏览器。
- 浏览器解析响应数据包,然后展现。
可以总结唯一张图:
什么是Servlet呢?
Servlet是JavaEE规范的一种,主要是为了扩展Java作为Web服务的功能,统一接口。由其他内部厂商如tomcat,jetty内部实现web的功能。如一个http请求到来:容器将请求封装为servlet中的HttpServletRequest对象,调用init(),service()等方法输出response,由容器包装为httpresponse返回给客户端的过程。
什么是Servlet规范?
- 从 Jar 包上来说,Servlet 规范就是两个 Jar 文件。servlet-api.jar 和 jsp-api.jar,Jsp 也是一种 Servlet。
- 从package上来说,就是 javax.servlet 和 javax.servlet.http 两个包。
- 从接口来说,就是规范了 Servlet 接口、Filter 接口、Listener 接口、ServletRequest 接口、ServletResponse 接口等。类图如下:
第一版:Socket版
使用Socket编程,实现简单的客户端和服务端的聊天。
服务端代码如下:
- package com.tian.v1;
- import java.io.*;
- import java.net.*;
- public class Server {
- public static String readline = null;
- public static String inTemp = null;
- public static String turnLine = "\n";
- public static final String client = "客户端:";
- public static final String server = "服务端:";
- public static final int PORT = 8090;
- public static void main(String[] args) throws Exception {
- ServerSocket serverSocket = new ServerSocket(PORT);
- System.out.println("服务端已经准备好了");
- Socket socket = serverSocket.accept();
- BufferedReader systemIn = new BufferedReader(new InputStreamReader(System.in));
- BufferedReader socketIn = new BufferedReader(new InputStreamReader(socket.getInputStream()));
- PrintWriter socketOut = new PrintWriter(socket.getOutputStream());
- while (true) {
- inTemp = socketIn.readLine();
- if (inTemp != null &&inTemp.contains("over")) {
- systemIn.close();
- socketIn.close();
- socketOut.close();
- socket.close();
- serverSocket.close();
- }
- System.out.println(client + inTemp);
- System.out.print(server);
- readline = systemIn.readLine();
- socketOut.println(readline);
- socketOut.flush();
- }
- }
- }
客户端代码如下:
- package com.tian.v1;
- import java.io.*;
- import java.net.*;
- public class Client {
- public static void main(String[] args) throws Exception {
- String readline;
- String inTemp;
- final String client = "客户端说:";
- final String server = "服务端回复:";
- int port = 8090;
- byte[] ipAddressTemp = {127, 0, 0, 1};
- InetAddress ipAddress = InetAddress.getByAddress(ipAddressTemp);
- //首先直接创建socket,端口号1~1023为系统保存,一般设在1023之外
- Socket socket = new Socket(ipAddress, port);
- BufferedReader systemIn = new BufferedReader(new InputStreamReader(System.in));
- BufferedReader socketIn = new BufferedReader(new InputStreamReader(socket.getInputStream()));
- PrintWriter socketOut = new PrintWriter(socket.getOutputStream());
- while (true) {
- System.out.print(client);
- readline = systemIn.readLine();
- socketOut.println(readline);
- socketOut.flush();
- //处理
- inTemp = socketIn.readLine();
- if (inTemp != null && inTemp.contains("over")) {
- systemIn.close();
- socketIn.close();
- socketOut.close();
- socket.close();
- }
- System.out.println(server + inTemp);
- }
- }
- }
过程如下:
第二版:我们直接请求http://localhost:8090
实现代码如下:
- package com.tian.v2;
- import java.io.IOException;
- import java.io.OutputStream;
- import java.net.ServerSocket;
- import java.net.Socket;
- public class MyTomcat {
- /**
- * 设定启动和监听端口
- */
- private int port = 8090;
- /**
- * 启动函数
- *
- * @throws IOException
- */
- public void start() throws IOException {
- System.out.println("my tomcat starting...");
- String responseData = "6666666";
- ServerSocket socket = new ServerSocket(port);
- while (true) {
- Socket accept = socket.accept();
- OutputStream outputStream = accept.getOutputStream();
- String responseText = HttpProtocolUtil.getHttpHeader200(responseData.length()) + responseData;
- outputStream.write(responseText.getBytes());
- accept.close();
- }
- }
- /**
- * 启动入口
- */
- public static void main(String[] args) throws IOException {
- MyTomcat tomcat = new MyTomcat();
- tomcat.start();
- }
- }
再写一个工具类,内容如下;
- ackage com.tian.v2;
- public class HttpProtocolUtil {
- /**
- * 200 状态码,头信息
- *
- * @param contentLength 响应信息长度
- * @return 200 header info
- */
- public static String getHttpHeader200(long contentLength) {
- return "HTTP/1.1 200 OK \n" + "Content-Type: text/html \n"
- + "Content-Length: " + contentLength + " \n" + "\r\n";
- }
- /**
- * 为响应码 404 提供请求头信息(此处也包含了数据内容)
- *
- * @return 404 header info
- */
- public static String getHttpHeader404() {
- String str404 = "<h1>404 not found</h1>";
- return "HTTP/1.1 404 NOT Found \n" + "Content-Type: text/html \n"
- + "Content-Length: " + str404.getBytes().length + " \n" + "\r\n" + str404;
- }
- }
启动main方法:
使用IDEA访问:
在浏览器访问:
自此,我们的第二版本搞定。下面继续第三个版本;
第三版:封装请求信息和响应信息
一个http协议的请求包含三部分:
- 方法 URI 协议/版本
- 请求的头部
- 主体内容
比如
- POST /index.html HTTP/1.1
- Accept: text/plain; text/html
- Accept-Language: en-gb
- Connection: Keep-Alive
- Host: localhost
- User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)
- Content-Length: 33
- Content-Type: application/x-www-form-urlencoded
- Accept-Encoding: gzip, deflate
- lastName=tian&firstName=JohnTian
简单的解释
- 数据的第一行包括:方法、URI、协议和版本。在这个例子里,方法为POST,URI为/index.html,协议为HTTP/1.1,协议版本号为1.1。他们之间通过空格来分离。
- 请求头部从第二行开始,使用英文冒号(:)来分离键和值。
- 请求头部和主体内容之间通过空行来分离,例子中的请求体为表单数据。
类似于http协议的请求,响应也包含三个部分。
- 协议 状态 状态描述
- 响应的头部
- 主体内容
比如:
- HTTP/1.1 200 OK
- Server: Microsoft-IIS/4.0
- Date: Mon, 5 Jan 2004 13:13:33 GMT
- Content-Type: text/html
- Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT
- Content-Length: 112
- <html>
- <head>
- <title>HTTP Response Example</title> </head>
- <body>
- Welcome to Brainy Software
- </body>
- </html>
简单解释
- 第一行,HTTP/1.1 200 OK表示协议、状态和状态描述。
- 之后表示响应头部。
- 响应头部和主体内容之间使用空行来分离。
代码实现
创建一个工具类,用来获取静态资源信息。
- package com.tian.v3;
- import com.tian.v2.HttpProtocolUtil;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- /**
- * 提取了一些共用类和函数
- */
- public class ResourceUtil {
- /**
- * 根据请求 url 获取完整绝对路径
- */
- public static String getPath(String url) {
- String path = ResourceUtil.class.getResource("/").getPath();
- return path.replaceAll("\\\\", "/") + url;
- }
- /**
- * 输出静态资源信息
- */
- public static void outputResource(InputStream input, OutputStream output) throws IOException {
- int count = 0;
- while (count == 0) {
- count = input.available();
- }
- int resourceSize = count;
- output.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes());
- long written = 0;
- int byteSize = 1024;
- byte[] bytes = new byte[byteSize];
- while (written < resourceSize) {
- if (written + byteSize > resourceSize) {
- byteSize = (int) (resourceSize - written);
- bytes = new byte[byteSize];
- }
- input.read(bytes);
- output.write(bytes);
- output.flush();
- written += byteSize;
- }
- }
- }
另外HttpProtocolUtil照样用第二版本中。
再创建Request类,用来解析并存放请求相关参数。
- package com.tian.v3;
- import java.io.IOException;
- import java.io.InputStream;
- public class Request {
- /**
- * 请求方式, eg: GET、POST
- */
- private String method;
- /**
- * 请求路径,eg: /index.html
- */
- private String url;
- /**
- * 请求信息输入流 <br>
- * 示例
- * <pre>
- * GET / HTTP/1.1
- * Host: localhost
- * Connection: keep-alive
- * Pragma: no-cache
- * Cache-Control: no-cache
- * Upgrade-Insecure-Requests: 1
- * User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
- * </pre>
- */
- private InputStream inputStream;
- public Request() {
- }
- public Request(InputStream inputStream) throws IOException {
- this.inputStream = inputStream;
- int count = 0;
- while (count == 0) {
- count = inputStream.available();
- }
- byte[] bytes = new byte[count];
- inputStream.read(bytes);
- // requestString 参考:this.inputStream 示例
- String requestString = new String(bytes);
- // 按换行分隔
- String[] requestStringArray = requestString.split("\\n");
- // 读取第一行数据,即:GET / HTTP/1.1
- String firstLine = requestStringArray[0];
- // 遍历第一行数据按空格分隔
- String[] firstLineArray = firstLine.split(" ");
- this.method = firstLineArray[0];
- this.url = firstLineArray[1];
- }
- public String getMethod() {
- return method;
- }
- public void setMethod(String method) {
- this.method = method;
- }
- public String getUrl() {
- return url;
- }
- public void setUrl(String url) {
- this.url = url;
- }
- public InputStream getInputStream() {
- return inputStream;
- }
- public void setInputStream(InputStream inputStream) {
- this.inputStream = inputStream;
- }
- }
把第二版的MyTomcat进行小小调整:
- package com.tian.v3;
- import java.io.IOException;
- import java.io.OutputStream;
- import java.net.ServerSocket;
- import java.net.Socket;
- public class MyTomcat {
- private static final int PORT = 8090;
- public void start() throws IOException {
- System.out.println("my tomcat starting...");
- ServerSocket socket = new ServerSocket(PORT);
- while (true) {
- Socket accept = socket.accept();
- OutputStream outputStream = accept.getOutputStream();
- // 分别封装 Request 和 Response
- Request request = new Request(accept.getInputStream());
- Response response = new Response(outputStream);
- // 根据 request 中的 url,输出
- response.outputHtml(request.getUrl());
- accept.close();
- }
- }
- public static void main(String[] args) throws IOException {
- MyTomcat tomcat = new MyTomcat();
- tomcat.start();
- }
- }
然后再创建一个index.html,内容很简单:
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>hello world</title>
- </head>
- <body>
- <h2> you already succeed!</h2>
- </body>
- </html>
这一需要注意,index.html文件的存放路径不放错了,视本地路径来定哈,放在classes文件夹下的。你可以debug试试,看看你应该放在那个目录下。
启动MyTomcat。
访问http://localhost:8090/index.html
自此,我们针对于Http请求参数和相应参数做了一个简单的解析以及封装。
尽管其中还有很多问题,但是字少看起来有那点像样了。我们继续第四版,
第四版:实现动态请求资源
用过servlet的同学都知道,Servlet中有三个很重要的方法init、destroy 、service 。其中还记得我们自己写LoginServlet的时候,还会重写HttpServlet中的doGet()和doPost()方法。下面们就自己来搞一个:
Servlet类代码如下:
- public interface Servlet {
- void init() throws Exception;
- void destroy() throws Exception;
- void service(Request request, Response response) throws Exception;
- }
然后再写一个HttpServlet来实现Servlet。
代码实现如下:
- package com.tian.v4;
- public abstract class HttpServlet implements Servlet {
- @Override
- public void init() throws Exception {
- }
- @Override
- public void destroy() throws Exception {
- }
- @Override
- public void service(Request request, Response response) throws Exception {
- String method = request.getMethod();
- if ("GET".equalsIgnoreCase(method)) {
- doGet(request, response);
- } else {
- doPost(request, response);
- }
- }
- public abstract void doGet(Request request, Response response) throws Exception;
- public abstract void doPost(Request request, Response response) throws Exception;
- }
下面我们就来写一个自己的Servlet,比如LoginServlet。
- package com.tian.v4;
- public class LoginServlet extends HttpServlet {
- @Override
- public void doGet(Request request, Response response) throws Exception {
- String repText = "<h1> LoginServlet by GET method</h1>";
- response.output(HttpProtocolUtil.getHttpHeader200(repText.length()) + repText);
- }
- @Override
- public void doPost(Request request, Response response) throws Exception {
- String repText = "<h1>LoginServlet by POST method</h1>";
- response.output(HttpProtocolUtil.getHttpHeader200(repText.length()) + repText);
- }
- @Override
- public void init() throws Exception {
- }
- @Override
- public void destroy() throws Exception {
- }
- }
大家是否还记得,我们在学习Servlet的时候,在resources目录下面有个web.xml。我们这个版本也把这个xml文件给引入。
- <?xml version="1.0" encoding="utf-8"?>
- <web-app>
- <servlet>
- <servlet-name>login</servlet-name>
- <servlet-class>com.tian.v4.LoginServlet</servlet-class>
- </servlet>
- <servlet-mapping>
- <servlet-name>login</servlet-name>
- <url-pattern>/login</url-pattern>
- </servlet-mapping>
- </web-app>
既然引入了xml文件,那我们就需要去读取这个xml文件,并解析器内容。所以这里我们需要引入两个jar包。
- <dependencies>
- <dependency>
- <groupId>dom4j</groupId>
- <artifactId>dom4j</artifactId>
- <version>1.6.1</version>
- </dependency>
- <dependency>
- <groupId>jaxen</groupId>
- <artifactId>jaxen</artifactId>
- <version>1.1.6</version>
- </dependency>
- </dependencies>
万事俱备,只欠东风了。这时候我们来吧MyTomcat这个类做一些调整即可。
下面有个很重要的initServlet()方法,刚刚是对应下面这张图中的List servlets,但是我们代码里使用的是Map来存储Servlet的,意思就那么个意思,把Servlet放在集合里。
这也就是为什么大家都把Tomcat叫做Servlet容器的原因,其实真正的容器还是java集合。
- package com.tian.v4;
- import com.tian.v3.RequestV3;
- import com.tian.v3.ResponseV3;
- import org.dom4j.Document;
- import org.dom4j.Element;
- import org.dom4j.io.SAXReader;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.net.ServerSocket;
- import java.net.Socket;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- public class MyTomcat {
- /**
- * 设定启动和监听端口
- */
- private static final int PORT = 8090;
- /**
- * 存放 Servlet信息,url: Servlet 实例
- */
- private Map<String, HttpServlet> servletMap = new HashMap<>();
- public void start() throws Exception {
- System.out.println("my tomcat starting...");
- initServlet();
- ServerSocket socket = new ServerSocket(PORT);
- while (true) {
- Socket accept = socket.accept();
- OutputStream outputStream = accept.getOutputStream();
- // 分别封装 RequestV3 和 ResponseV3
- RequestV4 requestV3 = new RequestV4(accept.getInputStream());
- ResponseV4 responseV3 = new ResponseV4(outputStream);
- // 根据 url 来获取 Servlet
- HttpServlet httpServlet = servletMap.get(requestV3.getUrl());
- // 如果 Servlet 为空,说明是静态资源,不为空即为动态资源,需要执行 Servlet 里的方法
- if (httpServlet == null) {
- responseV3.outputHtml(requestV3.getUrl());
- } else {
- httpServlet.service(requestV3, responseV3);
- }
- accept.close();
- }
- }
- public static void main(String[] args) throws Exception {
- MyTomcat tomcat = new MyTomcat();
- tomcat.start();
- }
- /**
- * 解析web.xml文件,把url和servlet解析出来,
- * 并保存到一个java集合里(Map)
- */
- public void initServlet() throws Exception {
- InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("web.xml");
- SAXReader saxReader = new SAXReader();
- Document document = saxReader.read(resourceAsStream);
- Element rootElement = document.getRootElement();
- List<Element> list = rootElement.selectNodes("//servlet");
- for (Element element : list) {
- // <servlet-name>show</servlet-name>
- Element servletnameElement = (Element) element.selectSingleNode("servlet-name");
- String servletName = servletnameElement.getStringValue();
- // <servlet-class>server.ShowServlet</servlet-class>
- 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 + "']");
- // /show
- String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
- servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).getDeclaredConstructor().newInstance());
- }
- }
- }
启动,再次访问http://localhost:8090/index.html
同时,我们可以访问http://localhost:8090/login图片
到此,第四个版本也搞定了。
但是前面四个版本都有一个共同的问题,全部使用的是BIO。
BIO:同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
所以,大家在网上看到的手写tomcat的,也有使用线程池来做的,这里希望大家能get到为什么使用线程池来实现。另外,其实在tomcat高版本中已经没有使用BIO了。
而 HTTP/1.1默认使用的就是NIO了。
但这个只是通信方式,重点是我们要理解和掌握tomcat的整体实现。
总结
另外,发现上面都是讲配置文件解析,并将对应数据保存起来。熟悉这个套路后,大家是不是想到,我们很多配置项都是在server.xml中,还记得否?也是可以通过解析某个目录下的server.xml文件,并把内容赋给java中相应的变量罢了。
比如:
1.server.xml中的端口配置,我们是在代码里写死的而已,改成MyTomcat启动的时候去解析并获取不久得了吗?
2.我们通常是将我们项目的打成war,然后解压到某个目录下,最后还不是可以通过读取这个解压后的某个目录中找到web.xml,然后用回到上面的web.xml解析了。
技术点:Socket编程、InputStream、OutputStream、线程池、xml文件解析、反射。更高级版本中NIO,AIO等。
不是为了装逼而来搞这个tomcat,而是为了我们更深刻的理解tomcat的原理。
OK,关于手写迷你 Tomcat 体验Java 核心和的内容到此结束了,希望对大家有所帮助。
本文采摘于网络,不代表本站立场,转载联系作者并注明出处:https://www.iotsj.com//kuaixun/7163.html
用户评论
这个想法太酷了!迷你版Tomcat,能试试看吗?
有13位网友表示赞同!
厉害了,手工打造Mini Tomcat!这可真考验编程能力和耐心。
有13位网友表示赞同!
想了解一下手写的代码逻辑,在哪儿能看到呢?
有10位网友表示赞同!
太神奇了,小巧的应用功能强大吗?
有20位网友表示赞同!
会不会比原版Tomcat还稳定一点?好奇它运行的速度怎么样?
有11位网友表示赞同!
51CTO真是一个资源丰富的网站啊,总能看到新奇的内容。
有19位网友表示赞同!
手写代码这种事,不是太适合玩一玩吗?可以学习到很多东西哦!
有17位网友表示赞同!
Tomcat本身就是一个小巧的应用服务器,迷你版更加方便使用吧。
有16位网友表示赞同!
期待相关的教程或文档,这样我才能去尝试一下!
有12位网友表示赞同!
迷你版的Tomcat应用场景在哪里?能不能分享一些具体的例子?
有19位网友表示赞同!
这样的小型版本是否能够满足个人开发的需求呢?
有19位网友表示赞同!
手写代码的过程一定很不容易吧,佩服作者的技术水平。
有20位网友表示赞同!
Mini Tomcat 能用来开发哪些类型的项目?感兴趣啊。
有16位网友表示赞同!
有没有想过把它开源共享给开发者社区?
有14位网友表示赞同!
这真是一个很有创意的设计理念!
有10位网友表示赞同!
这种迷你版应用,会不会成为未来软件发展趋势之一呢?
有14位网友表示赞同!
学习一下手写代码,也能增强我的逻辑思维能力吧。
有12位网友表示赞同!
看到这样的文章,真的会让我对编程充满兴趣!
有7位网友表示赞同!
希望以后能看到更多关于Mini Tomcat的信息和教程!
有18位网友表示赞同!
这个迷你版Tomcat可能非常适合初学者学习服务器的原理。
有8位网友表示赞同!
期待更多开发者尝试这种手写的方式开发应用程序!
有11位网友表示赞同!