手写tomcat(五):基于Netty的Tomcat

前言

之前我写的Tomcat,都是通过获取socket对象,然后通过socket对象的输入输出流完成的.引入Netty之后,由于需要通过ByteBuf进行相关的操作,本次有较大的改动,将core包中的核心类都进行了更改.

GitHub地址:https://github.com/shenshaoming/tomcat

代码

首先是主程序HttpServer,更改为Netty后代码有所减少:

package com.tomcat.core;

import com.tomcat.annotations.Servlet;
import com.tomcat.baseservlet.AbstractServlet;
import com.tomcat.exceptions.RequestMappingException;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

import java.io.File;
import java.io.IOException;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;

/**
 * Netty版本的tomcat
 * 监听请求,调用request和response对请求作出反应
 * @author 申劭明
 * @date 2019/9/16 17:21
 * @version 5.3
 */
public class HttpServer {
    /**
     * 线程池中核心线程数的最大值(不是操作系统的最大值)
     */
    private int corePoolSize = 10;
    /**
     * 最大队列空间
     */
    private int maximumPoolSize = 50;
    /**
     * 空闲线程的最大存活时间
     */
    private long keepAliveTime = 100L;
    /**
     * keepAliveTime的时间单位设置
     */
    private TimeUnit unit = TimeUnit.SECONDS;

    /**
     * 监听端口
     */
    private static int port = 8080;
    /**
     * 关闭服务器的请求URI
     */
    static final String CLOSE_URI = "/shutdown";

    /**
     * Key值为Servlet的别名(uri),value为该Servlet对象
     * default权限
     */
    static HashMap map;

    static {
        //包名,可以通过application.properties设置
        getServlets("com.tomcat.servlet");
    }
    /**
     * @Description : nio监听数据请求
     * @author : 申劭明
     * @date : 2019/9/17 10:29
     */
    public void acceptWait() {

        //监听请求
        EventLoopGroup listenGroup = new NioEventLoopGroup();
        //请求处理
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();

        //绑定监听请求和处理请求的group
        bootstrap.group(listenGroup,workerGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline().addLast(new RequestHandler());
                    }
                });
        ChannelFuture future = null;
        try {
            future = bootstrap.bind(port).sync();
            future.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            listenGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
    /**
     * @param packageName 包名,如com.tomcat.servlet
     * @return : void
     * @Description : 扫描packageName包下的所有带有@Servlet注解的类文件
     * @author : 申劭明
     * @date : 2019/9/18 10:36
     */
    private static void getServlets(String packageName) {

        //class类的集合
        Set> classes = new LinkedHashSet<>();

        try {
            //com.tomcat.servlet,com/tomcat/servelet
            String packageDirName = packageName.replace(".", "/");
            Enumeration resources = Thread.currentThread().getContextClassLoader().getResources(packageDirName);

            while (resources.hasMoreElements()) {
                URL url = resources.nextElement();
                System.out.println("****************" + url);
                System.out.println();
                String protocol = url.getProtocol();
                if ("file".equals(protocol)) {
                    String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
                    findAndAddClassesInPackageByFile(packageName, filePath,true,classes);
                }else if("jar".equals(protocol)){
                    //扫描JAR包
                }
            }
            //遍历class集合
            if (map == null){
                map = new HashMap<>(classes.size());
            }
            for (Class aClass : classes) {
                //如果该class有Servlet注解
                if (aClass.isAnnotationPresent(Servlet.class)){
                    try {
                        String value = aClass.getAnnotation(Servlet.class).value();
                        //如果已经包含有该key值,则抛出异常
                        if (map.containsKey(value)){
                            //当前正在扫描的Servlet
                            String now = aClass.getName();
                            //已经存在的Servlet
                            String old = map.get(value).getClass().getName();
                            throw new RequestMappingException(now,old);

                        }else{
                            Class superclass = aClass.getSuperclass();
                            if (AbstractServlet.class != superclass){
                                System.err.println("带有Servlet注解的类'" + aClass.getName() + "'没有继承自AbstractServlet");
                                continue;
                            }
                        }
                        //添加至map集合中
                        map.put(value, (AbstractServlet) aClass.newInstance());
                    } catch (InstantiationException | IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (RequestMappingException e) {
                        e.printStackTrace();
                        System.exit(-1);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * @Description : 对于file类型获取该类型的所有class
     *
     * @param packageName 包名,com.tomcat.servlet
     * @param packagePath 包路径,com/tomcat/servlet
     * @param recursive 是否循环遍历子包内的文件
     * @param classes class集合
     * @author : 申劭明
     * @date : 2019/9/18 16:55
    */
    public static void findAndAddClassesInPackageByFile(String packageName,
                                                        String packagePath, final boolean recursive, Set> classes) {
        // 获取此包的目录 建立一个File
        File dir = new File(packagePath);
        // 如果不存在或者 也不是目录就直接返回
        if (!dir.exists() || !dir.isDirectory()) {
            return;
        }
        // 如果存在 就获取包下的所有文件 包括目录
        // 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件)
        File[] dirfiles = dir.listFiles(file -> (recursive && file.isDirectory())
                || (file.getName().endsWith(".class")));
        // 循环所有文件
        for (File file : dirfiles) {
            // 如果是目录 则继续扫描
            if (file.isDirectory()) {
                findAndAddClassesInPackageByFile(packageName + "."
                                + file.getName(), file.getAbsolutePath(), recursive,
                        classes);
            } else {
                // 如果是java类文件 去掉后面的.class 只留下类名
                String className = file.getName().substring(0,
                        file.getName().length() - 6);
                try {
                    // 添加到集合中去
                    // classes.add(Class.forName(packageName + '.' +
                    // className));
                    // 经过回复同学的提醒,这里用forName有一些不好,会触发static方法,没有使用classLoader的load干净
                    classes.add(Thread.currentThread().getContextClassLoader()
                            .loadClass(packageName + '.' + className));
                } catch (ClassNotFoundException e) {
                    // log.error("添加用户自定义视图类错误 找不到此类的.class文件");
                    e.printStackTrace();
                }
            }
        }
    }
}

紧接着就是处理请求的Handler,RequestHandler.java,由原先的实现Runnable接口改为继承SimpleChannelInboundHandler.

package com.tomcat.core;

import com.tomcat.baseservlet.AbstractServlet;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

/**
 * @Author: 申劭明
 * @Date: 2019/9/17 17:45
 */
public class RequestHandler extends SimpleChannelInboundHandler {

    public RequestHandler(){
    }
    @Override
    protected void messageReceived(ChannelHandlerContext channelHandlerContext, Object o) {
        //接收数据
        Request request = new Request((ByteBuf) o);
        //写入数据
        Response response = new Response(channelHandlerContext);
        //将请求对象放入响应对象中
        response.setRequest(request);

        AbstractServlet abstractServlet = HttpServer.map.get(request.getUri());
        try{
            if (abstractServlet != null){
                abstractServlet.service(request,response);
            }else{
                //找不到对应的Servlet则直接访问文件
                response.sendStaticResource();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            channelHandlerContext.close();
        }
    }
}

接下来就是对Http请求报文的处理,Request.java,该类中的属性由原先的输入流改为ByteBuf属性:

package com.tomcat.core;

import io.netty.buffer.ByteBuf;
import io.netty.util.CharsetUtil;

/**
 * @Author: 申劭明
 * @Date: 2019/9/16 17:24
 */
public class Request {
    /**
     * 通过Netty读取到的数据
     */
    private ByteBuf byteBuf;
    /**
     * 请求路径,如:/test.txt
     */
    private String uri;

    /**
     * 请求类型,GET或POST等
     */
    private String method;

    public String getMethod() {
        return method;
    }
    public Request(){

    }

    @Override
    public String toString() {
        return "Request{" +
                "uri='" + uri + '\'' +
                ", method='" + method + '\'' +
                '}';
    }

    public Request(ByteBuf byteBuf) {
        this.byteBuf = byteBuf;
        parse();
    }

    /**
     * @Description : 获取http请求中的相关参数
     *
     * @author : 申劭明
     * @date : 2019/9/17 10:26
    */
    private void parse() {

        String requestStr = byteBuf.toString(CharsetUtil.UTF_8);

        //获取请求uri
        uri = parseUri(requestStr);
        //获取请求类型
        method = parseMethod(requestStr);
    }

    /**
     * @Description : 获取请求路径,如http://localhost:8080/test.txt,截取/test.txt(URI)
     *
     * @param request 请求头
     * @return : 请求路径
     * @author : 申劭明
     * @date : 2019/9/17 9:33
    */
    private String parseUri(String request) {
        int index1,index2;
        //查看socket获取的请求头是否有值
        index1 = request.indexOf(' ');
        if (index1 != -1){
            index2 = request.indexOf(' ', index1 + 1);
            if (index2 > index1){
                return request.substring(index1 + 1,index2);
            }
        }
        return null;
    }

    /**
     * @Description : 获取请求类型
     *
     * @param request 请求报文
     * @return : GET,POST...
     * @author : 申劭明
     * @date : 2019/9/18 9:51
    */
    private String parseMethod(String request){
        int index = request.indexOf(' ');
        if (index != -1){
            return request.substring(0,index);
        }
        return null;
    }

    public String getUri() {
        return uri;
    }
}

接下来就是Reponse.java,作为响应类,修改主要是讲响应的内容放入ByteBuf中然后由ChannelHandlerContext返回给客户端.

package com.tomcat.core;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;

import java.io.*;

/**
 * @Author: 申劭明
 * @Date: 2019/9/16 17:28
 */
public class Response {

    /**
     * 传输数组的最大字节数
     */
    public static final int BUFFER_SIZE = 2048;

    /**
     * 访问的文件的路径,即tomcat中部署项目的目录
     */
    private static final String WEB_ROOT = "D:";

    /**
     * 响应头信息
     */
    private static final String RESPONSE_HEADER = "HTTP/1.1 200 Read File Success\r\n" +
            "Content-Type: text/html\r\n" + "\r\n";

    /**
     * 请求
     */
    private Request request;
    /**
     * 返回页面的数据
     */
    private ChannelHandlerContext output;

    public Response(ChannelHandlerContext outputStream) {
        this.output = outputStream;
    }

    public void setRequest(Request request) {
        this.request = request;
    }

    /**
     * @Description : 向页面返回静态数据
     *
     * @author : 申劭明
     * @date : 2019/9/17 10:27
    */
    public void sendStaticResource() throws IOException {
        //读取文件时的字节流
        byte[] bytes = new byte[BUFFER_SIZE];
        FileInputStream fis = null;
        if (request == null){
            return;
        }
        File file = new File(WEB_ROOT, request.getUri());
        String returnMessage = null;
        try {
            //如果文件存在,且不是个目录
            if (file.exists() && !file.isDirectory()) {
                fis = new FileInputStream(file);
                //读文件
                int ch ;

                StringBuilder sb = new StringBuilder(BUFFER_SIZE);
                //写文件
                while ((ch = fis.read(bytes,0,bytes.length)) != -1) {
                    sb.append(new String(bytes,0,ch));
                }
                returnMessage =  RESPONSE_HEADER + sb;

            }else {
                //文件不存在,返回给浏览器响应提示,这里可以拼接HTML任何元素
                String retMessage = "

" + file.getName() + " file or directory not exists

"; returnMessage = "HTTP/1.1 404 File Not Fount\r\n" + "Content-Type: text/html\r\n" + "Content-Length: " + retMessage.length() + "\r\n" + "\r\n" + retMessage; } ByteBuf buf = Unpooled.buffer(); //用输出流返回数据给页面 if (checkImage(request.getUri())){ buf = buf.writeBytes(returnMessage.replaceAll("text/html","image/jpeg;charset=UTF-8").getBytes()); }else{ buf = buf.writeBytes(returnMessage.getBytes()); } output.writeAndFlush(buf); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { if (fis != null){ fis.close(); } if (output != null){ //清空缓存区,调用close方法时会有flush操作 output.close(); } } } /** * @Description : 判断请求是否为图片类型 * * @param uri 请求的uri * @return : 是/否 * @author : 申劭明 * @date : 2019/9/18 14:21 */ private boolean checkImage(String uri) { boolean flag = uri.endsWith(".jpg") || uri.endsWith(".png") || uri.endsWith(".jpeg"); return flag; } /** * @Description : 设置返回数据 * * @param message 返回给页面的数据 * @author : 申劭明 * @date : 2019/9/18 10:19 */ public void setResponseContent(StringBuilder message){ ByteBuf buf = Unpooled.buffer(); buf = buf.writeBytes(new StringBuilder(RESPONSE_HEADER).append(message).toString().getBytes()); output.writeAndFlush(buf); } public void setResponseContent(String message){ setResponseContent(new StringBuilder(message)); } }

附:手写tomcat我放在了我的github上,之前的bio版本和nio版本也都留下了没有删,可以拿去学习- -当然了,希望对大家有所帮助.另外,aio模型的tomcat我不打算去写了,毕竟Netty本身也没有支持aio,主要是因为linux系统没有对aio的相关支持,而且nio转aio也相对较为简单,所以鄙人就不做研究了.

你可能感兴趣的:(Java,Tomcat,Netty)