【手写一个Tomcat】SimpleTomcat-01 - SimpleTomcat-02

本文介绍了如何使用Netty技术改进简易Tomcat,实现了一个小巧的Java Web服务器,通过实例展示了如何解析HTTP请求、响应及处理Servlet。通过配置web.properties和pom.xml,可以轻松部署和运行多个Servlet。

在这里插入图片描述

前言

本文实现一个简易Tomcat,遵循【Tomcat】第八篇:150代码手写Tomcat…

注:Tomcat实现了 Java Servlet 和 JSP 规范, 是一个开源、轻量级的 Java Web 应用服务器,主要用于运行和部署基于 Java Servlet、JavaServer Pages (JSP)、WebSocket 和 Java Expression Language (EL) 的应用程序。

Tomcat 是由 Apache 软件基金会开发和维护的开源项目,遵循 Apache 许可证,可以免费下载和使用。相比于其他更大型的企业级应用服务器(如 IBM WebSphere 或 Oracle WebLogic),Tomcat 更加轻量和灵活,适合小型到中型的应用场景,其通过高效的内存管理和多线程处理能力,能够支持高并发的 Web 请求,其提供了一系列的安全特性,包括用户认证、访问控制、SSL/TLS 加密等,以确保 Web 应用程序的安全运行。虽然 Tomcat 自身提供了基本的 Web 服务功能,但可以通过添加各种连接器(Connector)和容器(Container)来扩展其功能,例如支持 AJP 协议、HTTPS 加密等。

tomcat 提供了图形化和命令行管理工具,如 Tomcat Manager,使得管理员可以方便地部署、启动、停止和监控应用程序,并且常常被用作开发和测试环境中的 Web 服务器,也可以用于生产环境中的小型到中型应用。对于需要更多企业级特性的大型应用,通常会选择与其他应用服务器(如 Apache HTTP Server、Nginx 等)集成或使用更全面的 Java 应用服务器产品。

SimpleTomcat-01

实现

http.TomcatRequest

TomcatRequest.java

package com.sample.http;

import java.io.IOException;
import java.io.InputStream;

public class TomcatRequest {

    /**
     * 请求方法 get post delete put
     */
    private String method;

    private String url;

    public TomcatRequest(InputStream in) {
        try {
            // 1.content用来保存InputStream中的http请求信息
            String content = "";
            byte[] buff = new byte[1024];
            int len = 0;
            if ((len = in.read(buff)) > 0) {
                content = new String(buff, 0, len);
            }

            // 2.对http请求信息进行处理,得到Method与Url
            String line = content.split("\\n")[0];
            String[] arr = line.split("\\s");
            this.method = arr[0];
            this.url = arr[1].split("\\?")[0];

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 返回url
     * @return String
     */
    public String getUrl() {
        return this.url;
    }

    /**
     * 返回请求方法
     * @return String
     */
    public String getMethod() {
        return this.method;
    }
}

http.TomcatResponse

TomcatResponse.java

package com.sample.http;

import java.io.IOException;
import java.io.OutputStream;

public class TomcatResponse {

    private OutputStream out;

    public TomcatResponse(OutputStream out) {
        this.out = out;
    }

    public void write(String s) throws IOException {
        StringBuilder sb = new StringBuilder();
        // 因为写出的内容要被http协议解析,所以要符合http协议规范,有其要求的响应头(主要是状态码和响应格式)
        sb.append("HTTP/1.1 200 OK\n")
                .append("Content-Type: text/html;\n")
                .append("\r\n")
                .append(s);
        // IO流写出
        this.out.write(sb.toString().getBytes());
    }
}

http.TomcatServlet

TomcatServlet.java

package com.sample.http;

import java.io.IOException;

public abstract class TomcatServlet {

    // 这里的request与response都是Tomcat对象创建好然后传进来的
    public void service(TomcatRequest tomcatRequest, TomcatResponse tomcatResponse) throws IOException {
        if ("GET".equalsIgnoreCase(tomcatRequest.getMethod())) {
            doGet(tomcatRequest, tomcatResponse);
        }
        else {
            doPost(tomcatRequest, tomcatResponse);
        }
    }

    // 这里是模板方法模式,交给子类去具体实现
    protected abstract void doPost(TomcatRequest tomcatRequest, TomcatResponse tomcatResponse) throws IOException;

    protected abstract void doGet(TomcatRequest tomcatRequest, TomcatResponse tomcatResponse) throws IOException;

}

SimpleTomcat

SimpleTomcat.java

package com.sample;

import com.sample.http.TomcatRequest;
import com.sample.http.TomcatResponse;
import com.sample.http.TomcatServlet;

import java.io.FileInputStream;
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.Map;
import java.util.Properties;

/**
 * Tomcat核心类
 */
public class SimpleTomcat {

    private int port = 8080;

    private ServerSocket serverSocket;

    private Map<String, TomcatServlet> servletMapping = new HashMap<>();

    private Properties webxml = new Properties();

    private void init() {
        try {
            // 1.加载web.properties文件
            String WEB_INF = this.getClass().getResource("/").getPath();
            FileInputStream fileInputStream = new FileInputStream(WEB_INF + "web.properties");
            webxml.load(fileInputStream);

            // 2.遍历配置文件,寻找url与servlet映射关系配置
            for (Object o : webxml.keySet()) {
                String key = o.toString();
                // 以url结尾的key就是要映射的路径,下面是两条配置示例:
                // servlet.one.url=/firstServlet.do
                // servlet.one.className=com.yzh.tomcat.servlet.FirstServlet
                if (key.endsWith(".url")) {
                    // 去掉.url就是servlet的name(servlet.one)
                    String servletName = key.replaceAll("\\.url$", "");

                    // 2.1 获取到url(/first.do)
                    String url = webxml.getProperty(key);

                    // 2.2 获取对应servlet全类名(com.yzh.tomcat...FirstServlet),并通过反进行实例化
                    String className = webxml.getProperty(servletName + ".className");
                    // 注:这里是将所有Servlet都强转为TomcatServlet,所以一定要继承TomcatServlet
                    TomcatServlet tomcatServlet = (TomcatServlet) Class.forName(className).newInstance();

                    // 3.将url与servlet实例保存到servletMapping中(单例模式)
                    servletMapping.put(url, tomcatServlet);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 启动tomcat
     */
    public void start() {
        // 1.调用init,目的是得到servletMapping的映射关系
        init();

        try {
            // 2.通过BIO创建socket的服务端,在指定端口开始监听
            serverSocket = new ServerSocket(this.port);
            System.out.println("SimpleTomcat已启动,监听的端口是" + this.port);

            // 3.用一个死循环持续等待并处理用户请求
            while (true) {
                Socket client = serverSocket.accept();
                // process是具体处理请求的逻辑,参数是当前连接的Socket
                process(client);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 具体处理请求
     * 1.创建IO流,并包装成Request与Response
     * 2.获取请求Url,取出对应Servlet进行处理
     */
    private void process(Socket client) throws IOException {
        // 1.获取IO流,并封装成Request与Response
        InputStream in = client.getInputStream();
        OutputStream out = client.getOutputStream();
        // 注:这里要明白,每次请求的Request和Response都是不同的(因为连接时的socket不同),他俩的作用域仅为当前会话
        TomcatRequest request = new TomcatRequest(in);
        TomcatResponse response = new TomcatResponse(out);

        // 获取请求URL,寻找相应Servlet进行处理
        String url = request.getUrl();
        // 2.判断改url是否有对应的Servlet实例
        if (servletMapping.containsKey(url)) {
            // 如果有,调用service方法进行处理
            servletMapping.get(url).service(request, response);
        }
        else {
            // 如果没有,写出404
            response.write("404 - Not Found");
        }

        // 3.关闭本次连接相关资源
        out.flush();
        out.close();
        in.close();
        client.close();
    }

    public static void main(String[] args) {
        new SimpleTomcat().start();
    }
}

servlet.FirstServlet

package com.sample.servlet;

import com.sample.http.TomcatRequest;
import com.sample.http.TomcatResponse;
import com.sample.http.TomcatServlet;

import java.io.IOException;

public class FirstServlet extends TomcatServlet {

    @Override
    protected void doPost(TomcatRequest tomcatRequest, TomcatResponse tomcatResponse) throws IOException {
        tomcatResponse.write("this is FirstServlet!");
    }

    @Override
    protected void doGet(TomcatRequest tomcatRequest, TomcatResponse tomcatResponse) throws IOException {
        doPost(tomcatRequest, tomcatResponse);
    }
}

servlet.SecondServlet

SecondServlet.java

package com.sample.servlet;


import com.sample.http.TomcatRequest;
import com.sample.http.TomcatResponse;
import com.sample.http.TomcatServlet;

import java.io.IOException;

public class SecondServlet extends TomcatServlet {

    @Override
    protected void doPost(TomcatRequest tomcatRequest, TomcatResponse tomcatResponse) throws IOException {
        tomcatResponse.write("Hello world!");
    }

    @Override
    protected void doGet(TomcatRequest tomcatRequest, TomcatResponse tomcatResponse) throws IOException {
        doPost(tomcatRequest, tomcatResponse);
    }
}

配置

web.properties

servlet.one.url=/firstServlet
servlet.one.className=com.sample.servlet.FirstServlet

servlet.two.url=/secondServlet
servlet.two.className=com.sample.servlet.SecondServlet

文件结构

在这里插入图片描述

运行

运行我们的 SimpleTomcat.java
在这里插入图片描述

在浏览器中输入

http://localhost:8080/firstServlet

或者

http://localhost:8080/secondServlet

可以看到

在这里插入图片描述

SimpleTomcat-02

基于netty改进简易Tomcat,遵循【Tomcat】第九篇:使用 Netty 重构…

配置

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>SimpleTomcat</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>SimpleTomcat-01</module>
        <module>SimpleTomcat-02</module>
    </modules>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.6.Final</version>
        </dependency>
    </dependencies>

</project>

resource.web.properties

web.properties

servlet.one.url=/firstServlet
servlet.one.className=com.sample.servlet.FirstServlet

servlet.two.url=/secondServlet
servlet.two.className=com.sample.servlet.SecondServlet

实现

http.TomcatRequest

TomcatRequest.java

package com.sample.http;

import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.QueryStringDecoder;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;

/**
 * TomcatRequest
 * @author xxx
 */
public class TomcatRequest {

    private ChannelHandlerContext channelHandlerContext;

    private HttpRequest request;

    public TomcatRequest(ChannelHandlerContext channelHandlerContext, HttpRequest request) {
        this.channelHandlerContext = channelHandlerContext;
        this.request = request;
    }

    /**
     * 返回url
     * @return String
     */
    public String getUrl() {
        return request.uri();
    }

    /**
     * 返回请求方法
     * @return String
     */
    public String getMethod() {
        return request.method().name();
    }

    public Map<String, List<String>> getParameters() {
        QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
        return decoder.parameters();
    }

    public String getParameter(String name) {
        Map<String, List<String>> params = getParameters();
        List<String> param = params.get(name);
        if (null == param) {
            return null;
        }
        else {
            return param.get(0);
        }
    }
}

http.TomcatResponse

TomcatResponse.java

package com.sample.http;

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.*;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

/**
 * TomcatResponse
 * @author xxx
 */
public class TomcatResponse {

    /**
     * SocketChannel的封装
     */
    private ChannelHandlerContext channelHandlerContext;

    private HttpRequest request;

    public TomcatResponse(ChannelHandlerContext channelHandlerContext, HttpRequest request) {
        this.channelHandlerContext = channelHandlerContext;
        this.request = request;
    }

    public void write(String out) throws IOException {
        try {
            if (out == null || out.length() == 0) {
                return;
            }
            // 设置http协议和请求头信息
            FullHttpResponse response = new DefaultFullHttpResponse(
                    // 设置http版本为1.1
                    HttpVersion.HTTP_1_1,
                    // 设置响应状态码
                    HttpResponseStatus.OK,
                    // 将输出值写出 编码为UTF-8
                    Unpooled.wrappedBuffer(out.getBytes("UTF-8"))
            );
            // 设置Content-Type
            response.headers().set("Content-Type", "text/html;");
            // 是否设置支持长连接
//            if (HttpUtil.isKeepAlive(r)) {
//                // 设置连接内容为长连接
//                response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
//            }
            channelHandlerContext.write(response);
        } finally {
            channelHandlerContext.flush();
            channelHandlerContext.close();
        }
    }
}

http.TomcatServlet

TomcatServlet.java

package com.sample.http;

import java.io.IOException;

public abstract class TomcatServlet {

    /**
     * 这里的request与response都是Tomcat对象创建好然后传进来的
     * @param tomcatRequest
     * @param tomcatResponse
     * @throws Exception
     */
    public void service(TomcatRequest tomcatRequest, TomcatResponse tomcatResponse) throws Exception {
        if ("GET".equalsIgnoreCase(tomcatRequest.getMethod())) {
            doGet(tomcatRequest, tomcatResponse);
        }
        else {
            doPost(tomcatRequest, tomcatResponse);
        }
    }

    /**
     * 这里是模板方法模式,交给子类去具体实现
     * @param tomcatRequest
     * @param tomcatResponse
     * @throws Exception
     */
    protected abstract void doPost(TomcatRequest tomcatRequest, TomcatResponse tomcatResponse) throws Exception;

    /**
     *
     * @param tomcatRequest
     * @param tomcatResponse
     * @throws Exception
     */
    protected abstract void doGet(TomcatRequest tomcatRequest, TomcatResponse tomcatResponse) throws Exception;

}

servlet.FirstServlet

FirstServlet.java

package com.sample.servlet;

import com.sample.http.TomcatRequest;
import com.sample.http.TomcatResponse;
import com.sample.http.TomcatServlet;

import java.io.IOException;

public class FirstServlet extends TomcatServlet {

    @Override
    protected void doPost(TomcatRequest tomcatRequest, TomcatResponse tomcatResponse) throws IOException {
        tomcatResponse.write("this is FirstServlet!");
    }

    @Override
    protected void doGet(TomcatRequest tomcatRequest, TomcatResponse tomcatResponse) throws IOException {
        doPost(tomcatRequest, tomcatResponse);
    }
}

servlet.SecondServlet

SecondServlet.java

package com.sample.servlet;


import com.sample.http.TomcatRequest;
import com.sample.http.TomcatResponse;
import com.sample.http.TomcatServlet;

import java.io.IOException;

public class SecondServlet extends TomcatServlet {

    @Override
    protected void doPost(TomcatRequest tomcatRequest, TomcatResponse tomcatResponse) throws IOException {
        tomcatResponse.write("Hello world!");
    }

    @Override
    protected void doGet(TomcatRequest tomcatRequest, TomcatResponse tomcatResponse) throws IOException {
        doPost(tomcatRequest, tomcatResponse);
    }
}

SimpleTomcat

SimpleTomcat.java

package com.sample;

import com.sample.http.TomcatRequest;
import com.sample.http.TomcatResponse;
import com.sample.http.TomcatServlet;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;

import java.io.FileInputStream;
import java.net.ServerSocket;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * Tomcat核心类
 */
public class SimpleTomcat {

    private int port = 8080;

    private ServerSocket serverSocket;

    /**
     * 用来保存路径与Servlet的映射关系(servlet单例模式)
     */
    private Map<String, TomcatServlet> servletMapping = new HashMap<>();

    private Properties webxml = new Properties();

    /**
     * 加载web.xml文件,同时初始化 ServletMapping对象
     */
    private void init() {
        try {
            // 1.加载web.properties文件
            String WEB_INF = this.getClass().getResource("/").getPath();
            FileInputStream fileInputStream = new FileInputStream(WEB_INF + "web.properties");

            webxml.load(fileInputStream);

            // 2.遍历配置文件,寻找url与servlet映射关系配置
            for (Object o : webxml.keySet()) {
                String key = o.toString();
                // 以url结尾的key就是要映射的路径,下面是两条配置示例:
                // servlet.one.url=/firstServlet.do
                // servlet.one.className=com.yzh.tomcat.servlet.FirstServlet
                if (key.endsWith(".url")) {
                    // 去掉.url就是servlet的name(servlet.one)
                    String servletName = key.replaceAll("\\.url$", "");

                    // 2.1 获取到url(/first.do)
                    String url = webxml.getProperty(key);

                    // 2.2 获取对应servlet全类名(com.sample.servlet...FirstServlet),并通过反进行实例化
                    String className = webxml.getProperty(servletName + ".className");
                    // 注:这里是将所有Servlet都强转为TomcatServlet,所以一定要继承TomcatServlet
                    TomcatServlet tomcatServlet = (TomcatServlet) Class.forName(className).newInstance();

                    // 3.将url与servlet实例保存到servletMapping中(单例模式)
                    servletMapping.put(url, tomcatServlet);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 启动tomcat
     * 1. 调用init方法,加载web.xml
     * 2.等待用户请求,并对每个请求进行处理
     */
    public void start() {
        // 1.调用init,目的是得到servletMapping的映射关系
        init();

        // netty封装了nio,Reactor模型,Boss,worker
        // Boss线程
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();

        // Worker线程
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // Netty服务
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            // 链路式编程
            serverBootstrap.group(bossGroup, workerGroup)
                    // 主线程处理类,看到这样的写法,底层就是用反射
                    .channel(NioServerSocketChannel.class)
                    // 子线程处理类Handler
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        // 客户端初始化处理
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            // 无锁化串行编程,Netty对HTTP协议的封装,顺序有要求
                            // HttpResponseEncoder 编码器
                            socketChannel.pipeline().addLast(new HttpResponseEncoder());
                            // HttpRequestDecoder 解码器
                            socketChannel.pipeline().addLast(new HttpRequestDecoder());
                            // 业务逻辑处理
                            socketChannel.pipeline().addLast(new SimpleTomcatHandler());
                        }
                    })
                    // 针对主线程的配置 分配线程最大数量 128
                    .option(ChannelOption.SO_BACKLOG, 128)
                    // 针对子线程的配置 保持长连接
                    .childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE);

            // 启动服务器
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
            System.out.println("SimpleTomcat 已启动,监听的端口是:" + port);
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public class SimpleTomcatHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
            if (msg instanceof HttpRequest) {
                HttpRequest httpRequest = (HttpRequest) msg;
                // 转交给我们自己的TomcatRequest实现
                TomcatRequest tomcatRequest = new TomcatRequest(channelHandlerContext, httpRequest);
                // 转交给我们自己的TomcatResponse实现
                TomcatResponse tomcatResponse = new TomcatResponse(channelHandlerContext, httpRequest);
                // 实际业务处理
                String url = tomcatRequest.getUrl();

                if (servletMapping.containsKey(url)) {
                    servletMapping.get(url).service(tomcatRequest, tomcatResponse);
                }
                else {
                    tomcatResponse.write("404 - Not Found");
                }
            }
        }
    }

    public static void main(String[] args) {
        new SimpleTomcat().start();
    }
}

运行

运行 SimpleTomcat.java

显示

SimpleTomcat 已启动,监听的端口是:8080

之后我们可以访问:

http://localhost:8080/secondServlet

http://localhost:8080/firstServlet

参考

https://blog.csdn.net/weixin_43935927/article/details/108743213

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

锥栗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值