自己实现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<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中的处理过程就可以简单的完成我们的容器了,如果需要更好的实现这个容器,还可以对处理器进行复用等优化操作

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容