1000行代码手写Web服务器(二)

1000行代码手写Web服务器(二)

前言

先放一下Github地址:
Github
这篇文章是继上篇1000行代码手写Web服务器(一)的后续,在写完上一个版本的代码后,经过今年4月份和5月份的两次重构补充,现在大概有2500行代码,主要变更为:
- 网络模块的增强,从最初的BIO版本,变为现在支持BIO、NIO、AIO三个版本,并且支持根据配置文件进行IO模型的切换
- Filter
- Listener
- TemplateEngine的表达式解析功能增强
- 添加了很多方法注释和行注释

本来是希望实现一下Servlet3.1标准的,但是很多API/继承体系过于繁琐,并且不是所有功能都有挑战性,所以还是选择尽量模仿Servlet API,但只选择比较感兴趣的部分去实现一下。

Network

IO模型

先复习一下几种Unix IO模型:
- 异步I/O 是指用户程序发起IO请求后,不等待数据,同时操作系统内核负责I/O操作把数据从内核拷贝到用户程序的缓冲区后通知应用程序。数据拷贝是由操作系统内核完成,用户程序从一开始就没有等待数据,发起请求后不参与任何IO操作,等内核通知完成。
- 同步I/O 就是非异步IO的情况,也就是用户程序要参与把数据拷贝到程序缓冲区(例如java的InputStream读字节流过程)。
- 同步IO里的非阻塞 是指用户程序发起IO操作请求后不等待数据,而是调用会立即返回一个标志信息告知条件不满足,数据未准备好,从而用户请求程序继续执行其它任务。执行完其它任务,用户程序会主动轮询查看IO操作条件是否满足,如果满足,则用户程序亲自参与拷贝数据动作。

对应到Java中的API:
- BIO:同步阻塞,每个客户端的连接会对应服务器的一个线程
- NIO:同步非阻塞,多路复用器轮询客户端的请求,每个客户端的IO请求会对应服务器的一个线程
- AIO: 异步非阻塞,客户端的IO请求由OS完成后再通知服务器启动线程处理(需要OS支持)
1、进程向操作系统请求数据
2、操作系统把外部数据加载到内核的缓冲区中,
3、操作系统把内核的缓冲区拷贝到进程的缓冲区
4、进程获得数据完成自己的功能

Java NIO属于同步非阻塞IO,即IO多路复用,单个线程可以支持多个IO
即询问时从IO没有完毕时直接阻塞,变成了立即返回一个是否完成IO的信号。
而异步IO就是指AIO,AIO需要操作系统支持。

配置文件与多态实现

为了实现类似于Netty的以很小的改动就可以切换IO模型的概念,设置了一个配置文件server.properties(类似于Tomcat的配置文件)。

server.port=8080
server.connector=bio

connector可以取值为bio,nio,aio。那么如何根据不同的connector来选择不同的IO模型呢?
这里写图片描述
我们重点关注network包,只有这个包与下面的IO模型有关,其他的包都是与IO模型实现无关的。
首先IO模型的入口类在Bootstrap类中被初始化:

    /**
     * 服务器启动入口
     * 用户程序与服务器的接口
     */
    public static void run() {
        String port = PropertyUtil.getProperty("server.port");
        if(port == null) {
            throw new IllegalArgumentException("server.port 不存在");
        }
        String connector = PropertyUtil.getProperty("server.connector");
        if(connector == null || (!connector.equalsIgnoreCase("bio") && !connector.equalsIgnoreCase("nio") && !connector.equalsIgnoreCase("aio"))) {
            throw new IllegalArgumentException("server.network 不存在或不符合规范");
        }
        Endpoint server = Endpoint.getInstance(connector);
        server.start(Integer.parseInt(port));
        Scanner scanner = new Scanner(System.in);
        String order;
        while (scanner.hasNext()) {
            order = scanner.next();
            if (order.equals("EXIT")) {
                server.close();
                System.exit(0);
            }
        }
    }

重点是Endpoint的getInstance方法:
Endpoint是每种Endpoint的基类

public abstract class Endpoint {
    /**
     * 启动服务器
     * @param port
     */
    public abstract void start(int port);

    /**
     * 关闭服务器
     */
    public abstract void close();

    /**
     * 根据传入的bio、nio、aio获取相应的Endpoint实例
     * @param connector
     * @return
     */
    public static Endpoint getInstance(String connector) {
        StringBuilder sb = new StringBuilder();
        sb.append("com.sinjinsong.webserver.core.network.endpoint")
                .append(".")
                .append(connector)
                .append(".")
                .append(StringUtils.capitalize(connector))
                .append("Endpoint");
        try {
            return (Endpoint) Class.forName(sb.toString()).newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        throw new IllegalArgumentException(connector);
    }
}

其实就是使用反射来实现不同的字符串映射到不同的类。
一个具体的IO模型的实现一般是由Endpint(入口,对应ServerSocket),Acceptor(请求接收线程),Dispatcher(请求分发线程,往往对应一个线程池,负责读取Request以及将RequestHandler放入线程池中执行),SocketWrapper(Socket的包装类),RequestHandler(往往是一个Runnable,负责执行servlet等)组成;NIO有些特别,因为它不仅是简单的NIO,而是使用了借鉴了Tomcat的Reactor NIO模型。

BIO

BIO Endpoint
@Slf4j
public class BioEndpoint extends Endpoint {
    private ServerSocket server;
    private BioAcceptor acceptor;
    private BioDispatcher dispatcher;
    private volatile boolean isRunning = true;

    @Override
    public void start(int port) {
        try {
            dispatcher = new BioDispatcher();
            server = new ServerSocket(port);
            initAcceptor();
            log.info("服务器启动");
        } catch (Exception e) {
            e.printStackTrace();
            log.info("初始化服务器失败");
            close();
        }
    }

    private void initAcceptor() {
        acceptor = new BioAcceptor(this, dispatcher);
        Thread t = new Thread(acceptor, "bio-acceptor");
        t.setDaemon(true);
        t.start();
    }

    @Override
    public void close() {
        isRunning = false;
        dispatcher.shutdown();
        try {
            server.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public Socket accept() throws IOException {
        return server.accept();
    }

    public boolean isRunning() {
        return isRunning;
    }
}

主要是启动服务器以及关闭服务器,都比较简单,BIO的ServerSocket初始化是new出来就可以工作了。注意BIO的Acceptor是一个daemon(守护) 线程,也就是在所有非daemon线程都退出后,该线程会自动退出。

BIO Acceptor

接收到客户端的连接后,将socket进行封装,然后交给Dispatcher。

public class BioAcceptor implements Runnable {
    private BioEndpoint server;
    private BioDispatcher dispatcher;

    public BioAcceptor(BioEndpoint server,BioDispatcher dispatcher) {
        this.server = server;
        this.dispatcher = dispatcher;
    }

    @Override
    public void run() {
        log.info("开始监听");
        while (server.isRunning()) {
            Socket client;
            try {
                //TCP的短连接,请求处理完即关闭
                client = server.accept();
                log.info("client:{}", client);
                dispatcher.doDispatch(new BioSocketWrapper(client));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
BIO Dispatcher

负责先读取请求,然后构造RequestHandler然后放到线程池中执行。
这里遇到了一个坑,本来是打算把读取也放到RequestHandler中并发执行的,但是在测试的时候发现线程池中有多个线程都在执行读取,就是一个客户端请求被分到了多个线程中执行,并且Request无法读取完整。所以只能把客户端的读取放到单线程执行的Dispatcher中。
还有一个坑是在读取Request完之后不能把inputStream关掉,否则会把socket(客户端连接)也关掉,导致后面的响应写回时抛出Socket已经关闭的异常。

public class BioDispatcher extends AbstractDispatcher {

    @Override
    public void doDispatch(SocketWrapper socketWrapper) {
        BioSocketWrapper bioSocketWrapper = (BioSocketWrapper) socketWrapper;
        Socket socket = bioSocketWrapper.getSocket();
        Request request = null;
        Response response = null;
        try {
            BufferedInputStream bin = new BufferedInputStream(socket.getInputStream());
            byte[] buf = null;
            try {
                buf = new byte[bin.available()];
                int len = bin.read(buf);
                if (len <= 0) {
                    throw new RequestInvalidException();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            // 这里这里不要把in关掉,把in关掉就等同于把socket关掉
            //解析请求
            response = new Response();
            request = new Request(buf);
            pool.execute(new BioRequestHandler(socketWrapper, servletContext, exceptionHandler, resourceHandler, request, response));
        } catch (ServletException e) {
            exceptionHandler.handle(e, response, socketWrapper);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
BIO RequestHandler

不同IO模型的请求处理器是大同小异的,前面的业务逻辑执行都是一样的,都在父类中,只有响应写回是不同的,所以这里使用了继承来复用代码。
父类:

@Slf4j
@Getter
public abstract class AbstractRequestHandler implements FilterChain, Runnable {

    protected Request request;
    protected Response response;
    protected SocketWrapper socketWrapper;
    protected ServletContext servletContext;
    protected ExceptionHandler exceptionHandler;
    protected ResourceHandler resourceHandler;
    protected boolean isFinished;
    protected Servlet servlet;
    protected List<Filter> filters;
    private int filterIndex = 0;

    public AbstractRequestHandler(SocketWrapper socketWrapper, ServletContext servletContext, ExceptionHandler exceptionHandler, ResourceHandler resourceHandler, Request request, Response response) throws ServletNotFoundException, FilterNotFoundException {
        this.socketWrapper = socketWrapper;
        this.servletContext = servletContext;
        this.exceptionHandler = exceptionHandler;
        this.resourceHandler = resourceHandler;
        this.isFinished = false;
        this.request = request;
        this.response = response;
        request.setServletContext(servletContext);
        request.setRequestHandler(this);
        response.setRequestHandler(this);
        // 根据url查询匹配的servlet,结果是0个或1个
        servlet = servletContext.mapServlet(request.getUrl());
        // 根据url查询匹配的filter,结果是0个或多个
        filters = servletContext.mapFilter(request.getUrl());
    }

    /**
     * 入口
     */
    @Override
    public void run() {
        // 如果没有filter,则直接执行servlet
        if (filters.isEmpty()) {
            service();
        } else {
            // 先执行filter
            doFilter(request, response);
        }
    }

    /**
     * 递归执行,自定义filter中如果同意放行,那么会调用filterChain(也就是requestHandler)的doiFilter方法,
     * 此时会执行下一个filter的doFilter方法;
     * 如果不放行,那么会在sendRedirect之后将响应数据写回客户端,结束;
     * 如果所有Filter都执行完毕,那么会调用service方法,执行servlet逻辑
     * @param request
     * @param response
     */
    @Override
    public void doFilter(Request request, Response response) {
        if (filterIndex < filters.size()) {
            filters.get(filterIndex++).doFilter(request, response, this);
        } else {
            service();
        }
    }

    /**
     * 调用servlet
     */
    private void service() {
        log.info("socket isClosed: {}",this.socketWrapper.isClosed());
        try {
            //处理动态资源,交由某个Servlet执行
            //Servlet是单例多线程
            //Servlet在RequestHandler中执行
            servlet.service(request, response);
        } catch (ServletException e) {
            exceptionHandler.handle(e, response, socketWrapper);
        } catch (Exception e) {
            //其他未知异常
            e.printStackTrace();
            exceptionHandler.handle(new ServerErrorException(), response, socketWrapper);
        } finally {
            if (!isFinished) {
                flushResponse();
            }
        }
        log.info("请求处理完毕");
    }

    /**
     * 响应数据写回到客户端
     */
    public abstract void flushResponse();
}

构造方法中使用url找到了对应的servlet和filter,然后在run方法中先考虑执行filter,最后执行servlet。
注意在两种情况下会将响应数据立即写回客户端,一种是servlet执行完毕的时候,另一种是调用了response的重定向的时候。因为filter存在一种不放行的情况,就无法调用servlet执行完毕后的写回代码,并且此时往往是会使用重定向来结束请求。

BIO子类:

public class BioRequestHandler extends AbstractRequestHandler {

    public BioRequestHandler(SocketWrapper socketWrapper, ServletContext servletContext, ExceptionHandler exceptionHandler, ResourceHandler resourceHandler, Request request, Response response) throws ServletNotFoundException, FilterNotFoundException {
        super(socketWrapper, servletContext, exceptionHandler, resourceHandler, request, response);
    }

    /**
     * 写回后立即关闭socket
     */
    @Override
    public void finishRequest() {
        isFinished = true;
        BioSocketWrapper bioSocketWrapper = (BioSocketWrapper) socketWrapper;
        byte[] bytes = response.getResponseBytes();
        OutputStream os = null;
        try {
            os = bioSocketWrapper.getSocket().getOutputStream();
            os.write(bytes);
            os.flush();
        } catch (IOException e) {
            e.printStackTrace();
            log.error("socket closed");
        } finally {
            try {
                os.close();
                bioSocketWrapper.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        WebApplication.getServletContext().afterRequestDestroyed(request);
    }
}

这里就是把response转为byte数组,然后写回即可。
注意这里在写回之后会将连接立刻关闭,对应着HTTP的connection:close,也就是HTTP短连接。在NIO中实现了HTTP持久连接,也就是connection:keep-alive。在后面的基于JMeter压测中,在测试BIO时注意要设置请求头Connection为close,否则会出现大量的异常(connection refused)。

BIO SocketWrapper
@Slf4j
@Getter
public class BioSocketWrapper implements SocketWrapper {
    private Socket socket;
    public BioSocketWrapper(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void close() throws IOException {
        socket.close();
    }

}

NIO Reactor

Tomcat Reactor

Tomcat中的NIO Reactor模型用最简单的话来介绍就是:
多个(1个或2个)Acceptor阻塞式获取socket连接,然后多个Poller(处理器个数个)非阻塞式轮询socket读事件,检测到读事件时将socket交给线程池处理业务逻辑。
下面是Tomcat中的NIO Reactor实现,我的实现与其大同小异:
这里写图片描述
包含了三个组件:
Acceptor:后台线程,负责监听请求,将接收到的Socket请求放到Poller队列中
Poller:后台线程,当Socket就绪时,将Poller队列中的Socket交给Worker线程池处理
SocketProcessor(Worker):处理socket,本质上委托ConnectionHandler处理

Connector 启动以后会启动一组线程用于不同阶段的请求处理过程。
Acceptor 线程组。用于接受新连接,并将新连接封装一下,选择一个 Poller 将新连接添加到 Poller 的事件队列中。
Poller 线程组。用于监听 Socket 事件,当 Socket 可读或可写等等时,将 Socket 封装一下添加到 worker 线程池的任务队列中。
worker 线程组。用于对请求进行处理,包括分析请求报文并创建 Request 对象,调用容器的 pipeline 进行处理。

Netty Reactor

Netty是一个Java编写的网络编程框架,比如Dubbo,RocketMQ等都是基于它编写的,它的NIO版本也是基于Reactor模式实现的。
Netty中使用的Reactor模式,引入了多Reactor,也即一个主Reactor负责监控所有的连接请求,多个子Reactor负责监控并处理读/写请求,减轻了主Reactor的压力,降低了主Reactor压力太大而造成的延迟。并且每个子Reactor分别属于一个独立的线程,每个成功连接后的Channel的所有操作由同一个线程处理。这样保证了同一请求的所有状态和上下文在同一个线程中,避免了不必要的上下文切换,同时也方便了监控请求响应状态。

多Reactor下,mainReactor是一个,subReactor是多个。mainReqactor对应着一个Selector,也是注册了ServerSocket的Accept事件,当接收到连接事件时,接收到Socket,把它交给subReactor,每个subReactor对应着自己的Selector,把Socket的读事件注册到自己的Selector中。
mainReactor上有一个Selector,注册了ServerSocketChannel的Accept事件;各个subReactor各自对应着自己的Selector,注册了自己对应的SocketChannel的Read事件。
这里写图片描述

NIO Endpoint

我的实现思路是:NIOAcceptor以阻塞方式来接收客户端的连接,接收到后将其注册到某一个Poller的Queue中,每个Poller对应一个独立的Selector以及一个Queue中,每个Poller也是一个线程,会以无限循环的方式去将Queue中的客户端连接注册到自己所持有的Selector中,然后以非阻塞方式去检测Selector读就绪事件,检测到后将客户端连接交给Dispatcher,NIO Dispatcher类似于BIO的Dispatcher,读取Request数据后放入线程池中执行业务逻辑。
专为NIO部分画了一张时序图:
这里写图片描述

@Slf4j
public class NioEndpoint extends Endpoint {

    private int pollerCount = Math.min(2, Runtime.getRuntime().availableProcessors());
    private ServerSocketChannel server;
    private NioDispatcher nioDispatcher;
    private volatile boolean isRunning = true;
    private NioAcceptor nioAcceptor;
    private List<NioPoller> nioPollers;
    /**
     * poller轮询器
     */
    private AtomicInteger pollerRotater = new AtomicInteger(0);
    /**
     * 1min
     */
    private int keepAliveTimeout = 60 * 1000 ;
    /**
     * 针对keep-alive连接,如果长期没有数据交换则将其关闭
     */
    private IdleConnectionCleaner cleaner;

    //********************************初始化*************************************************************
    private void initDispatcherServlet() {
        nioDispatcher = new NioDispatcher();
    }

    private void initServerSocket(int port) throws IOException {
        server = ServerSocketChannel.open();
        server.bind(new InetSocketAddress(port));
        server.configureBlocking(true);
    }

    private void initPoller() throws IOException {
        nioPollers = new ArrayList<>(pollerCount);
        for (int i = 0; i < pollerCount; i++) {
            String pollName = "NioPoller-" + i;
            NioPoller nioPoller = new NioPoller(this, pollName);
            Thread pollerThread = new Thread(nioPoller, pollName);
            pollerThread.setDaemon(true);
            pollerThread.start();
            nioPollers.add(nioPoller);
        }
    }

    /**
     * 初始化Acceptor
     */
    private void initAcceptor() {
        this.nioAcceptor = new NioAcceptor(this);
        Thread t = new Thread(nioAcceptor, "NioAcceptor");
        t.setDaemon(true);
        t.start();
    }

    /**
     * 初始化IdleSocketCleaner
     */
    private void initIdleSocketCleaner() {
        cleaner = new IdleConnectionCleaner(nioPollers);
        cleaner.start();
    }

    //************************初始化结束***************************************************************
    @Override
    public void start(int port) {
        try {
            initDispatcherServlet();
            initServerSocket(port);
            initPoller();
            initAcceptor();
            initIdleSocketCleaner();
            log.info("服务器启动");
        } catch (Exception e) {
            e.printStackTrace();
            log.info("初始化服务器失败");
            close();
        }
    }

    @Override
    public void close() {
        isRunning = false;
        cleaner.shutdown();
        for (NioPoller nioPoller : nioPollers) {
            try {
                nioPoller.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        nioDispatcher.shutdown();
        try {
            server.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 调用dispatcher,处理这个读已就绪的客户端连接
     * @param socketWrapper
     */
    public void execute(NioSocketWrapper socketWrapper) {
        nioDispatcher.doDispatch(socketWrapper);
    }

     /**
     * 轮询Poller,实现负载均衡
     * @return
     */
    private NioPoller getPoller() {
        int idx = Math.abs(pollerRotater.incrementAndGet()) % nioPollers.size();
        return nioPollers.get(idx);
    }

    public boolean isRunning() {
        return isRunning;
    }

    /**
     * 以阻塞方式来接收一个客户端的链接
     * @return
     * @throws IOException
     */
    public SocketChannel accept() throws IOException {
        return server.accept();
    }

    /**
     * 将Acceptor接收到的socket放到轮询到的一个Poller的Queue中
     *  
     * @param socket
     * @return
     */
    public void registerToPoller(SocketChannel socket) throws IOException {
        server.configureBlocking(false);
        getPoller().register(socket, true);
        server.configureBlocking(true);
    }

    public int getKeepAliveTimeout() {
        return this.keepAliveTimeout;
    }
}
NIO Acceptor
@Slf4j
public class NioAcceptor implements Runnable {
    private NioEndpoint nioEndpoint;

    public NioAcceptor(NioEndpoint nioEndpoint) {
        this.nioEndpoint = nioEndpoint;
    }

    @Override
    public void run() {
        log.info("{} 开始监听",Thread.currentThread().getName());
        while (nioEndpoint.isRunning()) {
            SocketChannel client;
            try {
                client = nioEndpoint.accept();
                if(client == null){
                    continue;
                }
                client.configureBlocking(false);
                log.info("Acceptor接收到连接请求 {}",client);
                nioEndpoint.registerToPoller(client); 
                log.info("socketWrapper:{}", client);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
NIO Poller

注意Poller中保存了所有的活跃Socket(成员变量sockets),其中有些socket是初次连接的,有些是keep-alive的,我还另外设置了一个IdleConnectionCleaner,用于清除一段时间内没有任何数据交换的socket,实现就是在SocketWrapper中添加一个waitBegin成员变量,在建立连接/keep-alive时设置waitBegin,并设置一个Scheduler定期扫描sockets,将当前时间距waitBegin超过阈值的连接关闭。

@Slf4j
public class NioPoller implements Runnable {
    private NioEndpoint nioEndpoint;
    private Selector selector;
    private Queue<PollerEvent> events;
    private String pollerName;
    private Map<SocketChannel, NioSocketWrapper> sockets;

    public NioPoller(NioEndpoint nioEndpoint, String pollerName) throws IOException {
        this.sockets = new ConcurrentHashMap<>();
        this.nioEndpoint = nioEndpoint;
        this.selector = Selector.open();
        this.events = new ConcurrentLinkedQueue<>();
        this.pollerName = pollerName;
    }

    public void register(SocketChannel socketChannel, boolean isNewSocket) {
        log.info("Acceptor将连接到的socket放入 {} 的Queue中", pollerName);
        NioSocketWrapper wrapper;
        if (isNewSocket) {
            // 设置waitBegin
            wrapper = new NioSocketWrapper(nioEndpoint, socketChannel, this, isNewSocket);
            // 用于cleaner检测超时的socket和关闭socket
            sockets.put(socketChannel, wrapper);
        } else {
            wrapper = sockets.get(socketChannel);
            wrapper.setWorking(false);
        }
        wrapper.setWaitBegin(System.currentTimeMillis());
        events.offer(new PollerEvent(wrapper));
        // 某个线程调用select()方法后阻塞了,即使没有通道已经就绪,也有办法让其从select()方法返回。只要让其它线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法即可。阻塞在select()方法上的线程会立马返回。
        selector.wakeup();
    }

    public void close() throws IOException {
        for (NioSocketWrapper wrapper : sockets.values()) {
            wrapper.close();
        }
        events.clear();
        selector.close();
    }

    @Override
    public void run() {
        log.info("{} 开始监听", Thread.currentThread().getName());
        while (nioEndpoint.isRunning()) {
            try {
                events();
                if (selector.select() <= 0) {
                    continue;
                }
                log.info("select()返回,开始获取当前选择器中所有注册的监听事件");
                //获取当前选择器中所有注册的监听事件
                for (Iterator<SelectionKey> it = selector.selectedKeys().iterator(); it.hasNext(); ) {
                    SelectionKey key = it.next();
                    //如果"接收"事件已就绪
                    if (key.isReadable()) {
                        //如果"读取"事件已就绪
                        //交由读取事件的处理器处理
                        log.info("serverSocket读已就绪,准备读");
                        NioSocketWrapper attachment = (NioSocketWrapper) key.attachment();
                        if (attachment != null) {
                            processSocket(attachment);
                        }
                    }
                    //处理完毕后,需要取消当前的选择键
                    it.remove();
                }
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClosedSelectorException e) {
                log.info("{} 对应的selector 已关闭", this.pollerName);
            }
        }
    }

    private void processSocket(NioSocketWrapper attachment) {
        attachment.setWorking(true);
        nioEndpoint.execute(attachment);
    }

    private boolean events() {
        log.info("Queue大小为{},清空Queue,将连接到的Socket注册到selector中", events.size());
        boolean result = false;
        PollerEvent pollerEvent;
        for (int i = 0, size = events.size(); i < size && (pollerEvent = events.poll()) != null; i++) {
            result = true;
            pollerEvent.run();
        }
        return result;
    }

    public Selector getSelector() {
        return selector;
    }

    public String getPollerName() {
        return pollerName;
    }

    public void cleanTimeoutSockets() {
        for (Iterator<Map.Entry<SocketChannel, NioSocketWrapper>> it = sockets.entrySet().iterator(); it.hasNext(); ) {
            NioSocketWrapper wrapper = it.next().getValue();
            log.info("缓存中的socket:{}", wrapper);
            if (!wrapper.getSocketChannel().isConnected()) {
                log.info("该socket已被关闭");
                it.remove();
                continue;
            }
            if (wrapper.isWorking()) {
                log.info("该socket正在工作中,不予关闭");
                continue;
            }
            if (System.currentTimeMillis() - wrapper.getWaitBegin() > nioEndpoint.getKeepAliveTimeout()) {
                // 反注册
                log.info("{} keepAlive已过期", wrapper.getSocketChannel());
                try {
                    wrapper.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                it.remove();
            }
        }
    }


    @Data
    @AllArgsConstructor
    private static class PollerEvent implements Runnable {
        private NioSocketWrapper wrapper;

        @Override
        public void run() {
            log.info("将SocketChannel的读事件注册到Poller的selector中");
            try {
                if (wrapper.getSocketChannel().isOpen()) {
                    wrapper.getSocketChannel().register(wrapper.getNioPoller().getSelector(), SelectionKey.OP_READ, wrapper);
                    wrapper.setWaitBegin(System.currentTimeMillis());
                } else {
                    log.error("socket已经被关闭,无法注册到Poller", wrapper.getSocketChannel());
                }
            } catch (ClosedChannelException e) {
                e.printStackTrace();
            }
        }
    }
}
NIO Dispatcher
@Data
@Slf4j
public class NioDispatcher extends AbstractDispatcher {
    /**
     * 分发请求,注意IO读取必须放在IO线程中进行,不能放到线程池中,否则会出现多个线程同时读同一个socket数据的情况
     * 1、读取数据
     * 2、构造request,response
     * 3、将业务放入到线程池中处理
     * @param socketWrapper
     */
    @Override
    public void doDispatch(SocketWrapper socketWrapper) {
        NioSocketWrapper nioSocketWrapper = (NioSocketWrapper) socketWrapper;
        log.info("已经将请求放入worker线程池中");
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        log.info("开始读取Request");
        Request request = null;
        Response response = null;
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            while (nioSocketWrapper.getSocketChannel().read(buffer) > 0) {
                buffer.flip();
                baos.write(buffer.array());
            }
            baos.close();
            request = new Request(baos.toByteArray());
            response = new Response();
            pool.execute(new NioRequestHandler(nioSocketWrapper, servletContext, exceptionHandler, resourceHandler, request, response));
        } catch (IOException e) {
            e.printStackTrace();
            exceptionHandler.handle(new ServerErrorException(), response, nioSocketWrapper);
        } catch (ServletException e) {
            exceptionHandler.handle(e, response, nioSocketWrapper);
        }
    }
}
NIO RequestHandler

这里涉及了keep-alive的实现,如果请求头中有connection:keep-alive,就将其重新注册到Poller的Queue,等待下一次读就绪事件。

@Setter
@Getter
@Slf4j
public class NioRequestHandler extends AbstractRequestHandler {

    public NioRequestHandler(SocketWrapper socketWrapper, ServletContext servletContext, ExceptionHandler exceptionHandler, ResourceHandler resourceHandler, Request request, Response response) throws ServletNotFoundException, FilterNotFoundException {
        super(socketWrapper, servletContext, exceptionHandler, resourceHandler, request, response);
    }

    /**
     * 写入后会根据请求头Connection来判断是关闭连接还是重新将连接放回Poller,实现保活
     */
    @Override
    public void flushResponse() {
        isFinished = true;
        NioSocketWrapper nioSocketWrapper = (NioSocketWrapper) socketWrapper;
        ByteBuffer[] responseData = response.getResponseByteBuffer();
        try {
            nioSocketWrapper.getSocketChannel().write(responseData);
            List<String> connection = request.getHeaders().get("Connection");
            if (connection != null && connection.get(0).equals("close")) {
                log.info("CLOSE: 客户端连接{} 已关闭", nioSocketWrapper.getSocketChannel());
                nioSocketWrapper.close();
            } else {
                // keep-alive 重新注册到Poller中
                log.info("KEEP-ALIVE: 客户端连接{} 重新注册到Poller中", nioSocketWrapper.getSocketChannel());
                nioSocketWrapper.getNioPoller().register(nioSocketWrapper.getSocketChannel(), false);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        WebApplication.getServletContext().afterRequestDestroyed(request);
    }
}
NIO IdleConnectionCleaner

空闲连接的清除,避免keep-alive连接长期不使用,占用服务器资源。

@Slf4j
public class IdleConnectionCleaner implements Runnable {
    private ScheduledExecutorService executor;
    private List<NioPoller> nioPollers;

    public IdleConnectionCleaner(List<NioPoller> nioPollers) {
        this.nioPollers = nioPollers;
    }

    public void start() {
        ThreadFactory threadFactory = new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "IdleConnectionCleaner");
            }
        };
        executor = Executors.newSingleThreadScheduledExecutor(threadFactory);
        executor.scheduleWithFixedDelay(this, 0, 5, TimeUnit.SECONDS);
    }

    public void shutdown() {
        executor.shutdown();
    }

    @Override
    public void run() {
        for (NioPoller nioPoller : nioPollers) {
            log.info("Cleaner 检测{} 所持有的Socket中...", nioPoller.getPollerName());
            nioPoller.cleanTimeoutSockets();
        }
        log.info("检测结束...");
    }
}
NIO SocketWrapper
@Slf4j
@Data
public class NioSocketWrapper implements SocketWrapper {
    private final NioEndpoint server;
    private final SocketChannel socketChannel;
    private final NioPoller nioPoller;
    private final boolean isNewSocket;
    private volatile long waitBegin;
    private volatile boolean isWorking;

    public NioSocketWrapper(NioEndpoint server, SocketChannel socketChannel, NioPoller nioPoller, boolean isNewSocket) {
        this.server = server;
        this.socketChannel = socketChannel;
        this.nioPoller = nioPoller;
        this.isNewSocket = isNewSocket;
        this.isWorking = false;
    }

    public void close() throws IOException {
        socketChannel.keyFor(nioPoller.getSelector()).cancel();
        socketChannel.close();
    }



    @Override
    public String toString() {
        return socketChannel.toString();
    }
}

AIO

Java的AIO的编程风格与BIO、NIO很不相似,accept、read、write都是异步方法,后续代码执行必须放在CompletionHandler中进行回调。

AIO Endpoint
@Slf4j
public class AioEndpoint extends Endpoint {

    private AsynchronousServerSocketChannel server;
    private AioDispatcher aioDispatcher;
    private AioAcceptor aioAcceptor;
    private ExecutorService pool;

    private void initDispatcherServlet() {
        aioDispatcher = new AioDispatcher();
    }

    private void initServerSocket(int port) throws IOException {
        ThreadFactory threadFactory = new ThreadFactory() {
            private int count;

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "Endpoint Pool-" + count++);
            }
        };
        int processors = Runtime.getRuntime().availableProcessors();
        pool = new ThreadPoolExecutor(processors, processors, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(200), threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
        // 以指定线程池来创建一个AsynchronousChannelGroup  
        AsynchronousChannelGroup channelGroup = AsynchronousChannelGroup
                .withThreadPool(pool);
        // 以指定线程池来创建一个AsynchronousServerSocketChannel  
        server = AsynchronousServerSocketChannel.open(channelGroup)
                // 指定监听本机的PORT端口  
                .bind(new InetSocketAddress(port));
        // 使用CompletionHandler接受来自客户端的连接请求  
        aioAcceptor = new AioAcceptor(this);
        // 开始接收客户端连接
        accept();
    }

    /**
     * 接收一个客户端连接
     */
    public void accept() {
        server.accept(null, aioAcceptor);
    }

    @Override
    public void start(int port) {
        try {
            initDispatcherServlet();
            initServerSocket(port);
            log.info("服务器启动");
        } catch (Exception e) {
            e.printStackTrace();
            log.info("初始化服务器失败");
            close();
        }
    }

    @Override
    public void close() {
        aioDispatcher.shutdown();
        try {
            server.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 执行读已就绪的客户端连接的请求
     * @param socketWrapper
     */
    public void execute(AioSocketWrapper socketWrapper) {
        aioDispatcher.doDispatch(socketWrapper);
    }

}
AIO Dispatcher
@Slf4j
public class AioDispatcher extends AbstractDispatcher {

    @Override
    public void doDispatch(SocketWrapper socketWrapper) {
        AioSocketWrapper aioSocketWrapper = (AioSocketWrapper) socketWrapper;
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        aioSocketWrapper.getSocketChannel().read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                Request request = null;
                Response response = null;
                try {
                    //解析请求
                    request = new Request(attachment.array());
                    response = new Response();
                    pool.execute(new AioRequestHandler(aioSocketWrapper, servletContext, exceptionHandler, resourceHandler, this, request, response));
                } catch (ServletException e) {
                    exceptionHandler.handle(e, response, aioSocketWrapper);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void failed(Throwable e, ByteBuffer attachment) {
                log.error("read failed");
                e.printStackTrace();
            }
        });
    }
}
AIO RequestHandler
@Setter
@Getter
@Slf4j
public class AioRequestHandler extends AbstractRequestHandler {
    private CompletionHandler readHandler;

    public AioRequestHandler(SocketWrapper socketWrapper, ServletContext servletContext, ExceptionHandler exceptionHandler, ResourceHandler resourceHandler, CompletionHandler readHandler, Request request, Response response) throws ServletNotFoundException, FilterNotFoundException {
        super(socketWrapper, servletContext, exceptionHandler, resourceHandler,request,response);
        this.readHandler = readHandler;
    }

    /**
     * 写回后重新调用readHandler,进行读取(猜测AIO也是保活的)
     */
    @Override
    public void flushResponse() {
        isFinished = true;
        ByteBuffer[] responseData = response.getResponseByteBuffer();
        AioSocketWrapper aioSocketWrapper = (AioSocketWrapper) socketWrapper;
        AsynchronousSocketChannel socketChannel = aioSocketWrapper.getSocketChannel();
        socketChannel.write(responseData, 0, 2, 0L, TimeUnit.MILLISECONDS, null, new CompletionHandler<Long, Object>() {

            @Override
            public void completed(Long result, Object attachment) {
                log.info("写入完毕...");
                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                socketChannel.read(byteBuffer, byteBuffer, readHandler);
            }

            @Override
            public void failed(Throwable e, Object attachment) {
                log.info("写入失败...");
                e.printStackTrace();
            }
        });
        WebApplication.getServletContext().afterRequestDestroyed(request);
    }
}
AIO SocketWrapper
@Slf4j
@Data
public class AioSocketWrapper implements SocketWrapper {
    private AioEndpoint server;
    private AsynchronousSocketChannel socketChannel;
    private volatile long waitBegin;
    private volatile boolean isWorking;

    public AioSocketWrapper(AioEndpoint server, AsynchronousSocketChannel socketChannel) {
        this.server = server;
        this.socketChannel = socketChannel;
        this.isWorking = false;
    }

    public void close() throws IOException {
        socketChannel.close();
    }


    @Override
    public String toString() {
        return socketChannel.toString();
    }
}

压力测试

使用JMeter对三个版本都进行了压力测试,仅仅是访问一个静态页面,目的还是测试网络部分的性能有什么区别。

BIO

使用JMeter进行压力测试:connection:close
- 2个线程,每个线程循环访问10000次,吞吐量为556个请求/sec,平均响应时间为3ms
- 20个线程,每个线程循环访问1000次,吞吐量为650个请求/sec,平均响应时间为22ms
- 200个线程,每个线程循环访问100次,吞吐量为644个请求/sec,平均响应时间为209ms
- 1000个线程,每个线程循环访问20次,吞吐量为755个请求/sec,平均响应时间为774ms

NIO

使用JMeter进行压力测试:connection:keep-alive

  • 2个线程,每个线程循环访问10000次,吞吐量为559个请求/sec,平均响应时间为2ms
  • 20个线程,每个线程循环访问1000次,吞吐量为651个请求/sec,平均响应时间为21ms
  • 200个线程,每个线程循环访问100次,吞吐量为659个请求/sec,平均响应时间为201ms
  • 1000个线程,每个线程循环访问20次,吞吐量为503个请求/sec,平均响应时间为1396ms

AIO

使用JMeter进行压力测试:connection:keep-alive

  • 2个线程,每个线程循环访问10000次,吞吐量为633个请求/sec,平均响应时间为2ms
  • 20个线程,每个线程循环访问1000次,吞吐量为764个请求/sec,平均响应时间为16ms
  • 200个线程,每个线程循环访问100次,吞吐量为738个请求/sec,平均响应时间为170ms
  • 1000个线程,每个线程循环访问20次,吞吐量为704个请求/sec,平均响应时间为677ms,但有接近20%的错误率,错误信息是connection refused(这一点需要继续debug)

总体上感觉性能差不多,可能是没有测试出瓶颈,我对压力测试还是不够了解,另外其实最好是压测机器和测试机器分开来,现在还没有这个条件,准备过一段时间再重新设计这个压力测试。

Filter

主要是在RequestHandler中的实现,先调用filter,再调用servlet。

接口

public interface Filter {
    /**
     * 过滤器初始化
     */
    void init();

    /**
     * 过滤
     * @param request
     * @param response
     * @param filterChain
     */
    void doFilter(Request request, Response response,FilterChain filterChain) ;

    /**
     * 过滤器销毁
     */
    void destroy();
}
public interface FilterChain {
    /**
     * 当前filter放行,由后续的filter继续进行过滤
     * @param request
     * @param response
     */
    void doFilter(Request request,Response response) ;
}

RequestHandler

这里有一个递归的过程。
run方法开始执行会先调用第一个filter,第一个filter如果放行,那么会调用filterChain(也就是requestHandler,this)的doFilter,此时会将filterIndex++,然后调用下一个filter的doFilter方法,如此反复,直至所有filter都被调用,此时将调用service方法,执行servlet业务逻辑。如果不放行,那么会在某个filter的doFilter执行完毕后结束整个逻辑(一般需要进行重定向)。

@Slf4j
@Getter
public abstract class AbstractRequestHandler implements FilterChain, Runnable {

    protected Request request;
    protected Response response;
    protected SocketWrapper socketWrapper;
    protected ServletContext servletContext;
    protected ExceptionHandler exceptionHandler;
    protected ResourceHandler resourceHandler;
    protected boolean isFinished;
    protected Servlet servlet;
    protected List<Filter> filters;
    private int filterIndex = 0;

    public AbstractRequestHandler(SocketWrapper socketWrapper, ServletContext servletContext, ExceptionHandler exceptionHandler, ResourceHandler resourceHandler, Request request, Response response) throws ServletNotFoundException, FilterNotFoundException {
        this.socketWrapper = socketWrapper;
        this.servletContext = servletContext;
        this.exceptionHandler = exceptionHandler;
        this.resourceHandler = resourceHandler;
        this.isFinished = false;
        this.request = request;
        this.response = response;
        request.setServletContext(servletContext);
        request.setRequestHandler(this);
        response.setRequestHandler(this);
        // 根据url查询匹配的servlet,结果是0个或1个
        servlet = servletContext.mapServlet(request.getUrl());
        // 根据url查询匹配的filter,结果是0个或多个
        filters = servletContext.mapFilter(request.getUrl());
    }

    /**
     * 入口
     */
    @Override
    public void run() {
        // 如果没有filter,则直接执行servlet
        if (filters.isEmpty()) {
            service();
        } else {
            // 先执行filter
            doFilter(request, response);
        }
    }

    /**
     * 递归执行,自定义filter中如果同意放行,那么会调用filterChain(也就是requestHandler)的doiFilter方法,
     * 此时会执行下一个filter的doFilter方法;
     * 如果不放行,那么会在sendRedirect之后将响应数据写回客户端,结束;
     * 如果所有Filter都执行完毕,那么会调用service方法,执行servlet逻辑
     * @param request
     * @param response
     */
    @Override
    public void doFilter(Request request, Response response) {
        if (filterIndex < filters.size()) {
            filters.get(filterIndex++).doFilter(request, response, this);
        } else {
            service();
        }
    }

    /**
     * 调用servlet
     */
    private void service() {
        try {
            //处理动态资源,交由某个Servlet执行
            //Servlet是单例多线程
            //Servlet在RequestHandler中执行
            servlet.service(request, response);
        } catch (ServletException e) {
            exceptionHandler.handle(e, response, socketWrapper);
        } catch (Exception e) {
            //其他未知异常
            e.printStackTrace();
            exceptionHandler.handle(new ServerErrorException(), response, socketWrapper);
        } finally {
            if (!isFinished) {
                flushResponse();
            }
        }
        log.info("请求处理完毕");
    }

    /**
     * 响应数据写回到客户端
     */
    public abstract void flushResponse();
}

Filter映射

Filter映射是实现了如何通过url查找到与之对应的filter,servlet查找同理。
有几种匹配方式,比如精确匹配, 它的优先级是最高的,其次是路径匹配,比如/*,注意这里可能会有多个匹配,就servlet而言,会选择最合适的那个(最具体的,路径最长的),就filter而言,会将所有匹配的都找出来。
其实Tomcat的web.xml中匹配全部路径是/*,它的Matcher是自己实现的,这部分代码比较复杂, 我不打算重写一遍了,于是就用了Spring提供的AntPathMatcher,它实现了Ant风格的路径匹配器,它的匹配全部路径是/**,读者不要感觉奇怪。

注意servlet、filter都是延迟加载的,只有在第一次访问时才会被初始化。

 /**
     * 由URL得到对应的一个Servlet实例
     *
     * @param url
     * @return
     * @throws ServletNotFoundException
     */
    public Servlet mapServlet(String url) throws ServletNotFoundException {
        // 1、精确匹配

        String servletAlias = servletMapping.get(url);
        if (servletAlias != null) {
            return initAndGetServlet(servletAlias);
        }
        // 2、路径匹配
        List<String> matchingPatterns = new ArrayList<>();
        Set<String> patterns = servletMapping.keySet();
        for (String pattern : patterns) {
            if (matcher.match(pattern, url)) {
                matchingPatterns.add(pattern);
            }
        }

        if (!matchingPatterns.isEmpty()) {
            Comparator<String> patternComparator = matcher.getPatternComparator(url);
            Collections.sort(matchingPatterns, patternComparator);
            String bestMatch = matchingPatterns.get(0);
            return initAndGetServlet(bestMatch);
        }
        return initAndGetServlet(DEFAULT_SERVLET_ALIAS);
    }

    /**
     * 初始化并获取Servlet实例,如果已经初始化过则直接返回
     *
     * @param servletAlias
     * @return
     * @throws ServletNotFoundException
     */
    private Servlet initAndGetServlet(String servletAlias) throws ServletNotFoundException {
        ServletHolder servletHolder = servlets.get(servletAlias);
        if (servletHolder == null) {
            throw new ServletNotFoundException();
        }
        if (servletHolder.getServlet() == null) {
            try {
                Servlet servlet = (Servlet) Class.forName(servletHolder.getServletClass()).newInstance();
                servlet.init();
                servletHolder.setServlet(servlet);
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return servletHolder.getServlet();
    }


    /**
     * 由URL得到一系列匹配的Filter实例
     *
     * @param url
     * @return
     */
    public List<Filter> mapFilter(String url) throws FilterNotFoundException {
        List<String> matchingPatterns = new ArrayList<>();
        Set<String> patterns = filterMapping.keySet();
        for (String pattern : patterns) {
            if (matcher.match(pattern, url)) {
                matchingPatterns.add(pattern);
            }
        }

        Set<String> filterAliases = matchingPatterns.stream().flatMap(pattern -> this.filterMapping.get(pattern).stream()).collect(Collectors.toSet());
        List<Filter> result = new ArrayList<>();
        for (String alias : filterAliases) {
            result.add(initAndGetFilter(alias));
        }
        return result;
    }

    /**
     * 初始化并返回Filter实例,如果已经初始化过则直接返回
     *
     * @param filterAlias
     * @return
     * @throws FilterNotFoundException
     */
    private Filter initAndGetFilter(String filterAlias) throws FilterNotFoundException {
        FilterHolder filterHolder = filters.get(filterAlias);
        if (filterHolder == null) {
            throw new FilterNotFoundException();
        }
        if (filterHolder.getFilter() == null) {
            try {
                Filter filter = (Filter) Class.forName(filterHolder.getFilterClass()).newInstance();
                filter.init();
                filterHolder.setFilter(filter);
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return filterHolder.getFilter();
    }

Listener

接口

public interface ServletContextListener extends EventListener {
    /**
     * 应用启动
     * @param sce
     */
    void contextInitialized(ServletContextEvent sce);

    /**
     * 应用关闭
     * @param sce
     */
    void contextDestroyed(ServletContextEvent sce);
}
public interface HttpSessionListener extends EventListener {
    /**
     * session创建
     * @param se
     */
    void sessionCreated(HttpSessionEvent se);

    /**
     * session销毁
     * @param se
     */
    void sessionDestroyed(HttpSessionEvent se);

}

public interface ServletRequestListener extends EventListener {
    /**
     * 请求初始化
     * @param sre
     */
    void requestInitialized(ServletRequestEvent sre);

    /**
     * 请求销毁
     * @param sre
     */
    void requestDestroyed(ServletRequestEvent sre);
}
ServletContext

listener的加载过程是在servletContext,其实servlet和filter也是,只是不必把代码贴出来了,注意listener是单例的,同一个listener实例实现了多种listener接口,不要多次创建。
先看示例项目的web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app>

    <servlet>
        <servlet-name>LoginServlet</servlet-name>
        <servlet-class>com.sinjinsong.webserver.sample.web.servlet.LoginServlet</servlet-class>
    </servlet>

    <servlet>
        <servlet-name>LogoutServlet</servlet-name>
        <servlet-class>com.sinjinsong.webserver.sample.web.servlet.LogoutServlet</servlet-class>
    </servlet>

    <servlet>
        <servlet-name>UserServlet</servlet-name>
        <servlet-class>com.sinjinsong.webserver.sample.web.servlet.UserServlet</servlet-class>
    </servlet>

    <servlet>
        <servlet-name>UserEditServlet</servlet-name>
        <servlet-class>com.sinjinsong.webserver.sample.web.servlet.UserEditServlet</servlet-class>
    </servlet>

    <servlet>
        <servlet-name>DefaultServlet</servlet-name>
        <servlet-class>com.sinjinsong.webserver.core.servlet.impl.DefaultServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>LoginServlet</servlet-name>
        <url-pattern>/login</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>LogoutServlet</servlet-name>
        <url-pattern>/logout</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>UserServlet</servlet-name>
        <url-pattern>/user</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>UserEditServlet</servlet-name>
        <url-pattern>/user/edit</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>DefaultServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!--Filter-->
    <filter>
        <filter-name>LoginFilter</filter-name>
        <filter-class>com.sinjinsong.webserver.sample.web.filter.LoginFilter</filter-class>
    </filter>

    <filter>
        <filter-name>LogFilter</filter-name>
        <filter-class>com.sinjinsong.webserver.sample.web.filter.LogFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>LoginFilter</filter-name>
        <url-pattern>/**</url-pattern>
    </filter-mapping>

    <filter-mapping>
        <filter-name>LogFilter</filter-name>
        <url-pattern>/**</url-pattern>
    </filter-mapping>

    <listener>
        <listener-class>com.sinjinsong.webserver.sample.web.listener.ServletContextAndSessionListener</listener-class>
        <listener-class>com.sinjinsong.webserver.sample.web.listener.MyServletRequestListener</listener-class>
    </listener>
</web-app>

这里有几个listener,下面看一下它们是怎么加载的。
这部分涉及XML的解析,我使用了dom4j来解析。就listener而言,需要动态判断实现了哪些listener接口,然后保存起来,待到合适时机时被调用。

 /**
     * web.xml文件解析,比如servlet,filter,listener等
     *
     * @throws ClassNotFoundException
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    private void parseConfig() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        Document doc = XMLUtil.getDocument(ServletContext.class.getResourceAsStream("/web.xml"));
        Element root = doc.getRootElement();
        // 解析servlet
        List<Element> servlets = root.elements("servlet");
        for (Element servletEle : servlets) {
            String key = servletEle.element("servlet-name").getText();
            String value = servletEle.element("servlet-class").getText();
            this.servlets.put(key, new ServletHolder(value));
        }

        List<Element> servletMapping = root.elements("servlet-mapping");
        for (Element mapping : servletMapping) {
            List<Element> urlPatterns = mapping.elements("url-pattern");
            String value = mapping.element("servlet-name").getText();
            for (Element urlPattern : urlPatterns) {
                this.servletMapping.put(urlPattern.getText(), value);
            }
        }

        // 解析 filter
        List<Element> filters = root.elements("filter");
        for (Element filterEle : filters) {
            String key = filterEle.element("filter-name").getText();
            String value = filterEle.element("filter-class").getText();
            this.filters.put(key, new FilterHolder(value));
        }

        List<Element> filterMapping = root.elements("filter-mapping");
        for (Element mapping : filterMapping) {
            List<Element> urlPatterns = mapping.elements("url-pattern");
            String value = mapping.element("filter-name").getText();
            for (Element urlPattern : urlPatterns) {
                List<String> values = this.filterMapping.get(urlPattern.getText());
                if (values == null) {
                    values = new ArrayList<>();
                    this.filterMapping.put(urlPattern.getText(), values);
                }
                values.add(value);
            }
        }

        // 解析listener
        Element listener = root.element("listener");
        List<Element> listenerEles = listener.elements("listener-class");
        for (Element listenerEle : listenerEles) {
            EventListener eventListener = (EventListener) Class.forName(listenerEle.getText()).newInstance();
            if (eventListener instanceof ServletContextListener) {
                servletContextListeners.add((ServletContextListener) eventListener);
            }
            if (eventListener instanceof HttpSessionListener) {
                httpSessionListeners.add((HttpSessionListener) eventListener);
            }
            if (eventListener instanceof ServletRequestListener) {
                servletRequestListeners.add((ServletRequestListener) eventListener);
            }
        }
    }
Listener触发

以HttpSessionListener为例:
在ServletContext中createSession时会回调listener的create方法。

    /**
     * 创建session
     * @param response
     * @return
     */
    public HttpSession createSession(Response response) {
        HttpSession session = new HttpSession(UUIDUtil.uuid());
        sessions.put(session.getId(), session);
        response.addCookie(new Cookie("JSESSIONID", session.getId()));
        HttpSessionEvent httpSessionEvent = new HttpSessionEvent(session);
        for (HttpSessionListener listener : httpSessionListeners) {
            listener.sessionCreated(httpSessionEvent);
        }
        return session;
    }

销毁session时会回调listener的destroy方法。

/**
     * 销毁session
     * @param session
     */
    public void invalidateSession(HttpSession session) {
        sessions.remove(session.getId());
        afterSessionDestroyed(session);
    }

    /**
     * 清除空闲的session
     * 由于ConcurrentHashMap是线程安全的,所以remove不需要进行加锁
     */
    public void cleanIdleSessions() {
        for (Iterator<Map.Entry<String, HttpSession>> it = sessions.entrySet().iterator(); it.hasNext(); ) {
            Map.Entry<String, HttpSession> entry = it.next();
            if (Duration.between(entry.getValue().getLastAccessed(), Instant.now()).getSeconds() >= DEFAULT_SESSION_EXPIRE_TIME) {
//                log.info("该session {} 已过期", entry.getKey());
                afterSessionDestroyed(entry.getValue());
                it.remove();
            }
        }
    }

    private void afterSessionDestroyed(HttpSession session) {
        HttpSessionEvent httpSessionEvent = new HttpSessionEvent(session);
        for (HttpSessionListener listener : httpSessionListeners) {
            listener.sessionDestroyed(httpSessionEvent);
        }
    }

TemplateEngine 增强

这里的增强是指原来是仅支持如requestScope.username的表达式,现在支持requestScope.a.b.c….这样较为负责的表达式,实现原理就是基于反射获取属性值。

@Slf4j
public class TemplateResolver {
    public static final Pattern regex = Pattern.compile("\\$\\{(.*?)}");

    public static String resolve(String content, Request request) throws TemplateResolveException {
        Matcher matcher = regex.matcher(content);
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            log.info("{}", matcher.group(1));
            // placeHolder 格式为scope.x.y.z
            // scope值为requestScope,sessionScope,applicationScope
            String placeHolder = matcher.group(1);
            if (placeHolder.indexOf('.') == -1) {
                throw new TemplateResolveException();
            }
            ModelScope scope = ModelScope
                    .valueOf(
                            placeHolder.substring(0, placeHolder.indexOf('.'))
                                    .replace("Scope", "")
                                    .toUpperCase());
            // key 格式为x.y.z
            String key = placeHolder.substring(placeHolder.indexOf('.') + 1);
            if (scope == null) {
                throw new TemplateResolveException();
            }
            Object value = null;
            // 按照.分隔为数组,格式为[x,y,z]
            String[] segments = key.split("\\.");
            log.info("key: {} , segments:{}", key,Arrays.toString(segments));
            switch (scope) {
                case REQUEST:
                    value = request.getAttribute(segments[0]);
                    break;
                case SESSION:
                    value = request.getSession().getAttribute(segments[0]);
                    break;
                case APPLICATION:
                    value = request.getServletContext().getAttribute(segments[0]);
                    break;
                default:
                    break;
            }
            // 此时value为x,如果没有y、z,那么会直接返回;如果有,就会递归地进行属性读取(基于反射)
            if (segments.length > 1) {
                try {
                    value = parse(value, segments, 1);
                } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                    e.printStackTrace();
                    throw new TemplateResolveException();
                }
            }
            log.info("value:{}", value);
            // 如果解析得到的值为null,则将占位符去掉;否则将占位符替换为值
            if (value == null) {
                matcher.appendReplacement(sb, "");
            } else {
                //把group(1)得到的数据,替换为value
                matcher.appendReplacement(sb, value.toString());
            }
        }
        // 将源文件后续部分添加至尾部
        matcher.appendTail(sb);
        String result = sb.toString();
        return result.length() == 0 ? content : result;
    }

    /**
     * 基于反射实现多级查询,比如user.dept.name
     *
     * @param segments
     * @return
     */
    private static Object parse(Object value, String[] segments, int index) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        if (index == segments.length) {
            return value;
        }
        Method method = value.getClass().getMethod("get" + StringUtils.capitalize(segments[index]), new Class[0]);
        return parse(method.invoke(value, new Object[0]), segments, index + 1);
    }
}

总结

这个项目在春招实习中经常被问到,而一个普通的网站项目却无人问津,足以看出有一些想法和设计的轮子项目是面试官比较喜欢的,重复造轮子是有价值的;另外在写这种轮子的时候感觉也很好,很多地方都需要仔细设计,没有在开发普通web项目时的那种“全是套路”的感觉。希望大家在寻求技术提升的时候也可以多多尝试造轮子的方法。
WebServer项目目前来看还是有一些提升空间的,比如对WebSocket的支持、异步Servlet的支持,以及尝试一些支持更高并发量的网络模型(比如Tomcat的APR);其压力测试也有很多的优化空间。就目前而言,该项目的维护将暂告一段落,可能下一次的更新要等到2019年的春夏学期,如果有任何疑问和建议,欢迎评论或者在Github上提issue。

  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值