美文网首页Java 杂谈
自己实现Servlet---我自己的“tomcat”

自己实现Servlet---我自己的“tomcat”

作者: WANGGGGG | 来源:发表于2019-07-30 20:44 被阅读3次

    前言

    看到网上有很多现成的Web容器:Tomcat、Jetty、Undertow、JBoss、WebLogic、WebSphere等,心里就痒痒的,也想要尝试自己写一个。然后我就从最熟悉的Tomcat下手,先学习了一下它,不过自从上回写了《学Springboot必须要懂的内嵌式Tomcat启动与请求处理》之后,总感觉思路还不是很清晰,毕竟Tomcat是一个较为重量级的容器了,它内部有很多分支在初学时会使得理解变的很困难,索性就边写边理解吧。

    一、我的理解

    整体流程如下所示:


    image.png

    线程池处理请求的时候需要解析请求报文,然后将请求报文转成请求对象,方便后续处理,最后将执行结果返回给终端

    二、代码实现

    2.1主函数开启容器

    public class Main {
        public static void main(String[] args) {
            WcfServlet servlet=new WcfServlet();
            servlet.start();
        }
    }
    

    2.2容器的入口,包括端口号和ip的绑定,以及 轮询器的启动

    package com.wcf.test;
    
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    
    import java.io.IOException;
    import java.net.InetAddress;
    import java.net.InetSocketAddress;
    import java.nio.channels.Selector;
    import java.nio.channels.ServerSocketChannel;
    
    /**
     * @author wangcanfeng
     * @description 服务容器
     * @Date Created in 15:44-2019/7/29
     */
    public class WcfServlet {
    
        Logger logger = LogManager.getLogger(WcfServlet.class);
        /**
         * 系统默认端口号
         */
        private final static int DEFAULT_PORT = 7528;
    
        /**
         * socketChannel
         */
        private volatile ServerSocketChannel socketChannel = null;
    
        public WcfServlet() {
            try {
                // 新建一个channel
                socketChannel = ServerSocketChannel.open();
                // 绑定一下ip和端口号
                socketChannel.socket().bind(new InetSocketAddress(InetAddress.getLocalHost(), DEFAULT_PORT));
                //设置ServerSocket以非阻塞方式工作
                socketChannel.configureBlocking(false);
            } catch (Exception e) {
                logger.error("construct servlet failed", e);
            }
        }
    
        /**
         * 功能描述: 启动容器
         *
         * @param
         * @return:void
         * @since: v1.0
         * @Author:wangcanfeng
         * @Date: 2019/7/29-16:26
         */
        public void start() {
            try {
                Poller poller=new Poller();
                poller.register(this.socketChannel);
                poller.start();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    

    2.3轮询器,用于轮询进来的请求,然后将请求交给分配器处理

    package com.wcf.test;
    
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    
    import java.io.IOException;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.util.Iterator;
    
    /**
     * @author wangcanfeng
     * @description 轮询器,轮询请求
     * @Date Created in 16:19-2019/7/29
     */
    public class Poller extends Thread {
    
        private Logger logger = LogManager.getLogger(Poller.class);
        /**
         * 通道选择器
         */
        private final Selector selector;
    
        /**
         * 可以使用的通道数
         */
        private volatile int keyCount = 0;
    
        private volatile ServerSocketChannel socketChannel;
    
        public Poller() throws IOException {
            // 新建一个通道选择器
            // open的方法实现各个操作系统都是不一样的,需要注意一下
            selector = Selector.open();
        }
    
        /**
         * 功能描述: 给选择器绑定通道
         *
         * @param ssc 服务器通道
         * @return:void
         * @since: v1.0
         * @Author:wangcanfeng
         * @Date: 2019/7/29-16:30
         */
        public void register(ServerSocketChannel ssc) throws Exception {
            // 将通道注册到selector上,当有多个终端连接时,都是由这个selector来管理,实现传说中的多路复用
            // 让选择器监听socket的变化
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            this.socketChannel = ssc;
        }
    
    
        @Override
        public void run() {
            while (true) {
                try {
                    keyCount = selector.select();
                } catch (IOException e) {
                    logger.error("find selector failed", e);
                }
                Iterator<SelectionKey> keys =
                        keyCount > 0 ? selector.selectedKeys().iterator() : null;
                while (keys != null && keys.hasNext()) {
                    SelectionKey sk = keys.next();
                    keys.remove();
                    // ssc需要先处理连接请求
                    if (sk.isValid() && sk.isAcceptable()) {
                        accept(sk);
                    }
                    // 附件的包装器,把附件绑定到sk上,附件的生命周期和sk一致,但是附件不会随着网络传播
                    InformationWrapper wrapper = new InformationWrapper();
                    sk.attach(wrapper);
                    if (sk.isValid() && sk.isReadable()) {
                        Dispatcher dispatcher = new Dispatcher(sk);
                        dispatcher.dispatch();
                    }
                }
            }
        }
    
        /**
         * 功能描述: 接收连接请求,重新绑定通道,并关注读请求
         *
         * @param sk
         * @return:void
         * @since: v1.0
         * @Author:wangcanfeng
         * @Date: 2019/7/29-19:28
         */
        private void accept(SelectionKey sk) {
            try {
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel) sk.channel();
                SocketChannel sc = serverSocketChannel.accept();
                sc.configureBlocking(false);
                sc.register(selector, SelectionKey.OP_READ);
                //将sk对应的Channel设置成准备接受其他请求
                sk.interestOps(SelectionKey.OP_ACCEPT);
            } catch (Exception e) {
                logger.error("accept information from client failed", e);
            }
        }
    
    }
    
    

    2.4分配器将任务分配给处理器去处理

    package com.wcf.test;
    
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    
    import java.io.IOException;
    import java.nio.channels.SelectionKey;
    
    /**
     * @author wangcanfeng
     * @description 任务分配器,分配任务给处理器
     * @Date Created in 16:51-2019/7/29
     */
    public class Dispatcher {
    
        private final static Logger logger = LogManager.getLogger(Dispatcher.class);
    
        private SelectionKey sk;
    
        public Dispatcher(SelectionKey sk) {
            this.sk = sk;
        }
    
        /**
         * 功能描述: 处理事件内容
         *
         * @param
         * @return:void
         * @since: v1.0
         * @Author:wangcanfeng
         * @Date: 2019/7/29-19:58
         */
        public void dispatch() {
            InformationWrapper wrapper = (InformationWrapper) sk.attachment();
            if (wrapper == null) {
                cancelledKey(sk);
            } else {
                processKey(wrapper);
            }
        }
    
        /**
         * 功能描述: 处理包装器中的数据
         *
         * @param wrapper
         * @return:void
         * @since: v1.0
         * @Author:wangcanfeng
         * @Date: 2019/7/29-18:40
         */
        private void processKey(InformationWrapper wrapper) {
            try {
                // 将数据读入信息包装器中
                wrapper.read(sk);
            } catch (IOException e) {
                cancelledKey(sk);
                logger.error("read information failed", e);
            }
            ThreadPool.getExecutors().execute(new Handler(wrapper));
        }
    
        /**
         * 功能描述: 将SelectionKey从select的Set中取消
         *
         * @param sk
         * @return:void
         * @since: v1.0
         * @Author:wangcanfeng
         * @Date: 2019/7/29-18:39
         */
        public static void cancelledKey(SelectionKey sk) {
            try {
                if (sk != null) {
                    sk.attach(null);
                    if (sk.isValid()) {
                        sk.cancel();
                    }
                    // 虽然被cancel了,但是这个通道还是可以使用的,所以这个通道需要关闭一下
                    if (sk.channel().isOpen()) {
                        sk.channel().close();
                    }
                }
            } catch (Throwable e) {
                logger.error("channelCloseFail", e);
            }
        }
    }
    

    2.5处理器,处理请求内容

    package com.wcf.test;
    
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    
    /**
     * @author wangcanfeng
     * @description 处理器,具体处理请求内容
     * @Date Created in 11:09-2019/7/30
     */
    public class Handler implements Runnable {
    
        private final static Logger logger = LogManager.getLogger(Dispatcher.class);
    
        /**
         * 请求附件信息
         */
        private InformationWrapper wrapper;
    
        /**
         * 长连接标识
         */
        private final static String PROTOCOL_LONG = "long";
    
        public Handler(InformationWrapper wrapper) {
            this.wrapper = wrapper;
        }
    
        @Override
        public void run() {
            // 判断是长连接还是短连接
            // 假设长连接请求是有long开头的
            try {
                if (wrapper.getInfoStr().startsWith(PROTOCOL_LONG)) {
                    // 长连接只需要写数据到通道中,不需要消除sk
                    wrapper.write();
                } else {
                    // 处理一下http请求的报文,生成请求对象
                    HttpRequest request = new HttpRequest();
                    request.getRequest(wrapper);
                    // 处理请求
                    // ......
                    // 处理完毕
                    HttpResponse.response(wrapper, "请求已处理".getBytes());
                    // 短连接用完之后需要消除当前的SelectionKey
                    Dispatcher.cancelledKey(wrapper.getSelectionKey());
                }
            } catch (Exception e) {
                logger.error("handler information failed", e);
                Dispatcher.cancelledKey(wrapper.getSelectionKey());
            }
        }
    
    }
    

    2.6附件对象,和sk一起消亡

    package com.wcf.test;
    
    import java.io.IOException;
    import java.nio.ByteBuffer;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.SocketChannel;
    import java.nio.charset.Charset;
    
    /**
     * @author wangcanfeng
     * @description 信息包装器
     * @Date Created in 16:01-2019/7/29
     */
    public class InformationWrapper {
    
        /**
         * 信息流
         */
        private final ByteBuffer information;
    
        /**
         * 通道和选择器的绑定键
         */
        private SelectionKey selectionKey;
    
        /**
         * socket的通道
         */
        private SocketChannel socketChannel;
    
        /**
         * 编码后的字符串信息
         */
        private final StringBuffer stringBuffer;
    
        /**
         * 默认的编码格式
         */
        private final static Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
    
        /**
         * 允许的单次传送最大信息体积
         */
        private final static int ALLOW_CONTENT_LENGTH = 2 * 1024 * 1024;
    
        public InformationWrapper() {
            information = ByteBuffer.allocate(ALLOW_CONTENT_LENGTH);
            stringBuffer = new StringBuffer(ALLOW_CONTENT_LENGTH);
        }
    
        /**
         * 功能描述: 通过SelectionKey获取到通道,再从通道路面读取流
         *
         * @param sk
         * @return:void
         * @since: v1.0
         * @Author:wangcanfeng
         * @Date: 2019/7/29-18:37
         */
        public void read(SelectionKey sk) throws IOException {
            this.selectionKey = sk;
            this.socketChannel = (SocketChannel) sk.channel();
            while (socketChannel.read(information) > 0) {
                information.flip();
                stringBuffer.append(DEFAULT_CHARSET.decode(information));
            }
        }
    
        /**
         * 功能描述: 将指定的信息写到通道中
         *
         * @param bbf
         * @return:void
         * @since: v1.0
         * @Author:wangcanfeng
         * @Date: 2019/7/30-11:01
         */
        public void write(ByteBuffer bbf) throws IOException {
            this.socketChannel.write(bbf);
        }
    
        /**
         * 功能描述: 直接写当前附件中的信息到通道中
         *
         * @param
         * @return:void
         * @since: v1.0
         * @Author:wangcanfeng
         * @Date: 2019/7/30-11:01
         */
        public void write() throws IOException {
            this.socketChannel.write(DEFAULT_CHARSET.encode(stringBuffer.toString()));
    
        }
    
        public SelectionKey getSelectionKey() {
            return selectionKey;
        }
    
        public ByteBuffer getInformation() {
            return information;
        }
    
        public String getInfoStr() {
            return stringBuffer.toString();
        }
    
        public SocketChannel getChannel() {
            return socketChannel;
        }
    
    }
    

    2.7请求对象

    package com.wcf.test;
    
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    
    import java.io.UnsupportedEncodingException;
    import java.net.URLDecoder;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @author wangcanfeng
     * @description
     * @Date Created in 13:42-2019/7/30
     */
    public class HttpRequest {
    
        private final static Logger logger = LogManager.getLogger(HttpRequest.class);
        /**
         * 请求方法
         */
        private String method;
    
        /**
         * 请求路径
         */
        private String path;
    
        /**
         * 请求版本信息
         */
        private String version;
    
        /**
         * 请求头信息
         */
        private Map<String, String> headers = new HashMap<>();
    
        /**
         * 请求体
         */
        private String body;
    
        private final static String UTF8 = "UTF-8";
    
        public HttpRequest getRequest(InformationWrapper wrapper) {
            String info = wrapper.getInfoStr();
            // 首先获取请求方法
            if (info == null || info.length() == 0) {
                return null;
            } else {
                // 解析请求报文
                // 浏览器中换行符使用了\r\n,估计是为了适配windows和linux
                String[] arr = info.split("\r\n");
                //
                if (arr.length < 2) {
                    return null;
                }
                // 解析请求行
                try {
                    resolveRequestLine(arr[0]);
                } catch (UnsupportedEncodingException e) {
                    logger.error("UnsupportedEncodingException", e);
                }
                int count = 1;
                for (; count < arr.length; count++) {
                    if ("".equals(arr[count])) {
                        break;
                    } else {
                        resolveRequestHeader(arr[count]);
                    }
                }
                // 获取请求体
                if (count < arr.length) {
                    resolveRequestBody(arr[count + 1]);
                }
                return this;
            }
        }
    
        /**
         * 功能描述: 解析请求行,获取请求方法名,请求路径,http协议版本
         *
         * @param line 请求行
         * @return:void
         * @since: v1.0
         * @Author:wangcanfeng
         * @Date: 2019/7/30-14:09
         */
        private void resolveRequestLine(String line) throws UnsupportedEncodingException {
            // 根据空格切割字符串
            String[] arr = line.split(" ");
            this.method = arr[0];
            // 因为在请求中可能存在特殊字符,一般都会做URL编码处理
            // 所以这里需要对请求路径需要进行解码
            this.path = URLDecoder.decode(arr[1], UTF8);
            this.version = arr[2];
        }
    
        /**
         * 功能描述: 解析请求头
         *
         * @param header
         * @return:void
         * @since: v1.0
         * @Author:wangcanfeng
         * @Date: 2019/7/30-14:31
         */
        private void resolveRequestHeader(String header) {
            // 切割字符串,获取请求头中的字段名和字段值,存入map中
            String[] arr = header.split(": ");
            if (arr.length > 1) {
                headers.put(arr[0], arr[1]);
            } else {
                logger.warn("this header's format is not right");
            }
        }
    
        /**
         * 功能描述: 解析请求体
         *
         * @param body
         * @return:void
         * @since: v1.0
         * @Author:wangcanfeng
         * @Date: 2019/7/30-14:32
         */
        private void resolveRequestBody(String body) {
            this.body = body;
        }
    
        public String getMethod() {
            return method;
        }
    
        public String getPath() {
            return path;
        }
    
        public String getVersion() {
            return version;
        }
    
        public Map<String, String> getHeaders() {
            return headers;
        }
    }
    
    

    2.8返回对象

    package com.wcf.test;
    
    import java.io.IOException;
    import java.nio.ByteBuffer;
    
    /**
     * @author wangcanfeng
     * @description
     * @Date Created in 10:53-2019/7/30
     */
    public class HttpResponse {
    
        /**
         * 功能描述: 给终端回应
         *
         * @param wrapper 附件信息
         * @param bytes 需要返回的实际内容
         * @return:void
         * @since: v1.0
         * @Author:wangcanfeng
         * @Date: 2019/7/30-10:57
         */
        public static void response(InformationWrapper wrapper, byte[] bytes) throws IOException {
            ByteBuffer buf = ByteBuffer.allocate(1024);
            buf.put("HTTP/1.1 200\r\n".getBytes());
            buf.put("Content-Type: text/html; charset=utf-8\r\n".getBytes());
            buf.put("Date: Thu, 26 Jul 2018 01:54:53 GMT\r\n".getBytes());
            buf.put("\r\n".getBytes());
            buf.put("<html><head><title>Hello</title></head><body><h1>".getBytes());
            // 写出指定的信息
            buf.put(bytes);
            buf.put("</h1></body></html>".getBytes());
            buf.flip();
            wrapper.write(buf);
        }
    }
    

    最后

    到这里我们只需要修改Handler中的处理过程就可以简单的完成我们的容器了,如果需要更好的实现这个容器,还可以对处理器进行复用等优化操作

    相关文章

      网友评论

        本文标题:自己实现Servlet---我自己的“tomcat”

        本文链接:https://www.haomeiwen.com/subject/hpinrctx.html