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

前言

看到网上有很多现成的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 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 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 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("Hello

".getBytes()); // 写出指定的信息 buf.put(bytes); buf.put("

".getBytes()); buf.flip(); wrapper.write(buf); } }

最后

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

你可能感兴趣的:(自己实现Servlet---我自己的“tomcat”)