第五篇 - 手写Tomcat(基于Netty)&热部署

第五篇 - 手写Tomcat(基于Netty)&热部署_第1张图片
Github源码下载地址:https://github.com/chenxingxing6/sourcecode/tree/master/code-netty-tomcat

第五篇 - 手写Tomcat(基于Netty)&热部署_第2张图片


一、前言

Tomcat是一个轻量级的应用服务器,或者说是一个Servlet容器。Tomcat默认的Http实现是采用阻塞的Socket通信,每个请求都需要创建一个线程处理,当线程>500,性能非常低。Tomcat默认最大请求数为150,具体需要看硬件配置。此篇博客,需要通过netty实现tomcat通信,提高tomcat的并发量,同时加深对netty的学习。除此之外,我们还将了解一下热部署的原理,自己简单实现一个热部署。


二、Netty实现简易Tomcat

2.1 依赖配置文件
 <dependencies>
     <dependency>
         <groupId>io.nettygroupId>
         <artifactId>netty-allartifactId>
         <version>4.1.31.Finalversion>
     dependency>
dependencies>
2.2项目结构目录

第五篇 - 手写Tomcat(基于Netty)&热部署_第3张图片

2.3 配置文件
application.name=web

## 这里为了解析方便,我们用properties进行配置
servlet.test=testServlet
servlet.test.className=com.catalina.servlet.TestServlet
servlet.test.urlPattern=/test*
2.4 核心代码Tomcat
package com.catalina.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;

/**
 * User: lanxinghua
 * Date: 2019/10/9 09:30
 * Desc:
 */
public class Tomcat {

    /**
     * 开启服务
     * @param port
     * @throws Exception
     */
    public void start(int port) {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            // 配置
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workGroup)
                    // 主线程处理类
                    .channel(NioServerSocketChannel.class)
                    // 子线程处理类
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) {
                            // response编码
                            socketChannel.pipeline().addLast(new HttpResponseEncoder());
                            // request解码
                            socketChannel.pipeline().addLast(new HttpRequestDecoder());
                            // 业务逻辑处理
                            socketChannel.pipeline().addLast(new TomcatHandler());
                        }
                    })
                    // 对主线程,最大分配128个线程
                    .option(ChannelOption.SO_BACKLOG, 128)
                    // 子线程保存长连接
                    .childOption(ChannelOption.SO_KEEPALIVE, true);
            // 启动服务器
            ChannelFuture f = bootstrap.bind(port).sync();
            System.out.println("Netty Tomcat Server Is Start...http://localhost:" + port);
            // 主线程wait
            f.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
}
2.5 核心代码TomcatHandler
package com.catalina.server;

import com.catalina.config.Config;
import com.catalina.http.Request;
import com.catalina.http.Response;
import com.catalina.http.Servlet;
import com.hot.MyClassLoader;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.HttpRequest;

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * User: lanxinghua
 * Date: 2019/10/9 09:41
 * Desc:
 */
public class TomcatHandler extends ChannelInboundHandlerAdapter {
    private static final Map<Pattern,String> servletMapping = new HashMap<Pattern, String>();
    private static String applicationName;


    static {
        Config.load("web.properties");
        applicationName = Config.getValue("application.name");
        for (String key : Config.getKeys()) {
            if (key.startsWith("servlet")){
                String name = key.replaceFirst("servlet.", "");
                if (name.indexOf(".") != -1){
                    name = name.substring(0, name.indexOf("."));
                }
                String pattern = Config.getValue("servlet." + name + ".urlPattern").replaceAll("\\*", ".*");
                if (applicationName != null){
                    pattern = "/"+ applicationName + pattern;
                }
                String className = Config.getValue("servlet." + name + ".className");
                if (!servletMapping.containsKey(pattern)){
                    try {
                        servletMapping.put(Pattern.compile(pattern), className);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof HttpRequest){
            HttpRequest httpRequest = (HttpRequest) msg;
            Request request = new Request(ctx, httpRequest);
            Response response = new Response(ctx, httpRequest);
            doServlet(request, response);
        }
    }

    public void doServlet(Request request, Response response){
        String uri = request.getUri();
        String requestType = request.getRequestType();
        System.out.println("请求:" + uri + "type:" + requestType);
        if (applicationName != null && !uri.contains(applicationName)){
            String out = String.format("404 Not Found");
            response.write(out);
            return;
        }
        try {
            boolean hasPattern = false;
            for (Map.Entry<Pattern, String> entry : servletMapping.entrySet()) {
                if (entry.getKey().matcher(uri).matches()){
                    // 自定义类加载器,实现热部署
                    Class<?> clz = null;
                    try {
                        // 用自定义类加载器,是为了实现热部署
                        MyClassLoader myLoader = new MyClassLoader();
                        clz = myLoader.findClass(entry.getValue());
                    }catch (Exception e){
                        throw new RuntimeException("类加载异常," + e.getMessage());
                    }
                    Servlet servlet = (Servlet) clz.newInstance();
                    if ("get".equalsIgnoreCase(requestType)){
                        servlet.doGet(request, response);
                    }else {
                        servlet.doPost(request, response);
                    }
                    hasPattern = true;
                }
            }
            if (!hasPattern){
                String out = String.format("404 Not Found");
                response.write(out);
                return;
            }
        }catch (Exception e){
            String out = String.format("500 Error msg:%s", e.getStackTrace());
            response.write(out);
        }
    }
}

2.6 服务启动BootStrap
package com.catalina.server;
/**
 * User: lanxinghua
 * Date: 2019/10/9 09:31
 * Desc: 服务启动
 */
public class BootStrap {
    public static void main(String[] args) {
        new Tomcat().start(8080);
    }
}

2.7 测试结果

第五篇 - 手写Tomcat(基于Netty)&热部署_第4张图片
第五篇 - 手写Tomcat(基于Netty)&热部署_第5张图片

到这里一个基于netty简易版的Tomcat就实现了。全部代码,可以看github哦,亲…


三、热部署

3.1 类加载器

Java类的加载是由虚拟机来完成的,虚拟机把描述类的Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成能被Java虚拟机直接使用的Java类型, 这就是虚拟机的类加载机制.JVM中用来完成上述功能的具体实现就是类加载器.类加载器读取.class字节码文件将其转换成java.lang.Class类的一个实例.每个 实例用来表示一个java类.通过该实例的newInstance()方法可以创建出一个该类的对象.
第五篇 - 手写Tomcat(基于Netty)&热部署_第6张图片

第五篇 - 手写Tomcat(基于Netty)&热部署_第7张图片

3.2 热部署

对于Java应用程序来说,热部署就是在运行时更新Java类文件。也就是不重启服务器的情况下实现java类文件的替换修改等.举个例子,就像电脑可以在不重启 的情况下,更换U盘。简单一句话让JVM重新加载新的class文件!

3.3 实现思路

1.监听修改文件,生成class文件,并进行替换
2.创建新的类加载器,加载更新的class文件

3.4 核心代码FileMonitor
package com.hot;

import sun.nio.ch.IOUtil;

import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.file.*;
import java.util.concurrent.TimeUnit;

/**
 * User: lanxinghua
 * Date: 2019/10/9 15:00
 * Desc: 开启热部署,com.catalina.servlet下的文件
 * 修改servlet文件,保存,会自动实现热更新部署
 */
public class FileMonitor {
    private static final String projectName = "code-netty-tomcat";
    private static final String packagePath = "com/catalina/servlet/";

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

    public void start(){
        System.out.println("开启热部署.....");
        try {
            Path path = Paths.get(projectName + "/src/main/java/" + packagePath);
            WatchService watcher = FileSystems.getDefault().newWatchService();
            path.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);
            new Thread(() -> {
                while (true) {
                    try {
                        WatchKey key = watcher.take();
                        for (WatchEvent<?> event : key.pollEvents()) {
                            if (event.kind() == StandardWatchEventKinds.OVERFLOW){
                                // 事件可能是lost or discarded
                                continue;
                            }
                            Path p = (Path) event.context();
                            System.out.println("------------ start 热部署 --------------");
                            hotDeploy(p);
                            System.out.println("------------- end 热部署 --------------");
                        }
                        if (!key.reset()){
                            break;
                        }
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }).start();
        }catch (Exception e){
            e.printStackTrace();
        }
        try {
            TimeUnit.SECONDS.sleep(60*10);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 热部署
     * @param path
     */
    public static void hotDeploy(Path path){
        String fileName = path.toFile().getName();
        // java源码路径
        String prefixPath = projectName + "/src/main/java/" + packagePath;
        String sourceCodePath = prefixPath+fileName;
        fileName = fileName.replace(".java", ".class");
        try {
            System.gc();
            String p = projectName + "/target/classes/"+ packagePath +fileName;
            File oldFile = new File(p);
            oldFile.delete();
            // 对源码进行编译
            doCompile(sourceCodePath);
            // 编译后端class文件移动到target对应的目录中去
            moveFile(prefixPath + fileName, p);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    private static void doCompile(String sourceCodePath){
        try {
            System.out.println("源码文件进行编译:"+sourceCodePath);
            File file = new File(sourceCodePath);
            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
            StandardJavaFileManager manager = compiler.getStandardFileManager(null, null, null);
            Iterable iterable = manager.getJavaFileObjects(file);
            JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, iterable);
            task.call();
            manager.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    private static void moveFile(String srcPathStr, String desPathStr) {
        try{
            // 创建输入输出流对象
            File file = new File(desPathStr);
            if (!file.exists()){
                file.createNewFile();
            }
            FileInputStream fis = new FileInputStream(srcPathStr);
            FileOutputStream fos = new FileOutputStream(desPathStr);
            //创建搬运工具
            byte datas[] = new byte[1024*8];
            //创建长度
            int len = 0;
            //循环读取数据
            while((len = fis.read(datas))!=-1){
                fos.write(datas,0,len);
            }
            //释放资源
            fis.close();
            fis.close();
            File srcFile = new File(srcPathStr);
            srcFile.delete();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

这里我们默认监听servlet目录下的文件,修改servlet文件后,可以实现自动热部署。

3.5 修改servlet文件TestServlet
public class TestServlet extends Servlet {
    public void doGet(Request req, Response resp) throws Exception {
        this.doPost(req, resp);
    }

    public void doPost(Request req, Response resp) throws Exception {
        String name = req.getParamter("name");
        resp.setCharSet("UTF-8");
        resp.setContentType("text/html; charset=utf-8");
        String html = "

自己手写Netty实现Tomcat&热部署


"
; html += "
name:"+ name +"
"; // 修改的东西 html += "update....."; resp.write(html); } }
3.6 文件修改监听日志
------------ start 热部署 --------------
源码文件进行编译:code-netty-tomcat/src/main/java/com/catalina/servlet/TestServlet.java
------------- end 热部署 --------------
3.7 测试结果

第五篇 - 手写Tomcat(基于Netty)&热部署_第8张图片


希望给路过的朋友们可以解决一些疑惑,走过路过,不用错过,感兴趣就关注一波吧,感谢…

你可能感兴趣的:(手写源码)