简单的tomcat实现

来源:互联网 发布:微信网页授权回调域名 编辑:程序博客网 时间:2024/04/29 10:58

做了长时间web开发,一直都是用spring,导致自己成了操作工,按照既定的模子,重复的劳动,没有丝毫的进步,所以想深入的了解一番干了这么长时间的web的整个运行流程,绝大多数web开发学习应该都是servlet开始的吧,所以又重拾了servelt狠狠的研究了一番,最后发现servlet其实就是些标准,那啥为标准,说白了,就是定了些接口,导致看源码的过程很不过瘾,感觉没啥提升,就决定了解下更底层的工作原理,也就是tomcat。
开始我并不知道tomcat是java写的,对tomcat那是神秘,惧怕,感觉太过高大上,自己做的那点web开发和tomcat这种红遍全球的软件相比简直小巫见大巫,技术含量压根儿就不是一个层次。不过我还是想一窥究竟,偶然的机会,看到了《深入剖析Tomcat》,作者对tomcat源码讲解的很是详细,通过循序渐进的例子,非常认真的讲出了tomcat的精髓,我是受益匪浅。现在我想自己也写一个,来作为这阶段学习的一个交代,就叫tiny-tomcat吧,同样通过循序渐进,慢慢完善,源码在github上:https://github.com/esiyuan/tiny-tomcat.git

简单的web服务器

git checkout step-001:主要功能如下

  • 接受http请求
private void handlerRequest() throws IOException {           while(true) {                Socket socket = serverSocket.accept();                logger.info("获取连接[ address: " + socket.getInetAddress() + ", port: " + socket.getPort() + " ]");                Request request = new Request(socket.getInputStream());                request.parse();                Response response = new Response(request.getUri(), socket.getOutputStream());                response.sendStaticResource();                socket.close();           }     }

通过ServerSocket监听本地端口,serverSocket.accept()等待请求。

  • 解析请求uri
public void parse() throws IOException {           BufferedReader reader = IOUtil.getBufferedReader(inputStream);           String requestString = reader.readLine();           this.uri = parseUri(requestString);     }

为了分开请求和响应,贴合servlet风格,我们通过Request对象,解析请求字符串,request对象构造的时候,传入socket的输入流,由于我们只对第一行感兴趣,所以在只读取了第一行进行解析,解析出uri作为属性保存。

  • 根据uri定位html文件,并返回响应

最后通过 Response对象进行html响应,而response在构造的时候,传入socket的输出流。

/**      * 输出响应      * <p>文件找到uri对应的文件,则进行输出,否则输出404      * @throws IOException      */     public void sendStaticResource() throws IOException {           File file = new File(Constants.WEB_ROOT, uri);           if (file.exists()) {                sendFile(file);           } else {                sendDefault();           }     }

这里写图片描述

简单的servlet容器

git checkout step-002
主要在前一节代码上进行了部分改造,增加了响应servlet的功能,并分出了简单的servlet处理器,因为HttpServletRequest和HttpServletResponse是接口,为了达到演示的效果,避免request和response类过于复杂,使用了门面RequestFacade和ResponseFacade。

public class ServletProcessor {    private ConcurrentHashMap<String, Servlet> cache = new ConcurrentHashMap<String, Servlet>();    /**     * servlet没有加载,则加载并初始化     * <p>如有加载直接取缓存     * @param request     * @param response     */    public void process(Request request, Response response){            Servlet servlet = cache.get(request.getServerName());            if(servlet != null) {                try {                    servlet.service(request, response);                } catch (ServletException | IOException e) {                    e.printStackTrace();                }                return;            }            try(URLClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file://" + Constants.TOMCAT_CLASSLOADER_REPOSITORY)});) {                Class<?> clazz = classLoader.loadClass("web_root." + request.getServerName());                servlet = (Servlet)clazz.newInstance();                servlet.init(null);                servlet.service(request, response);                cache.putIfAbsent(request.getServerName(), servlet);            } catch (Exception e) {                e.printStackTrace();                System.out.println("服务异常!");            }    }}

为了体现servlet的生命周期,增加了init()方法,并且通过ConcurrentHashMap缓存了sevlet实例,为什么用ConcurrentHashMap,主要是考虑线程安全,其分段锁的设计,能够拥有较高的并发写的能力,同时并不会妨碍并发读取,由于读没有进行加锁,不需要等待写完成,所有必然的会造成数据的弱一致性,不过大多数时候都会有取出非空判断逻辑,也造不成什么问题。
servlet代码如下:

public class HelloServlet extends HttpServlet {    private static final long serialVersionUID = -2585140950753353037L;    private static final Logger logger = Logger.getLogger(HelloServlet.class);    public void service(ServletRequest request, ServletResponse response)            throws ServletException, IOException {        PrintWriter out = response.getWriter();        out.println("hello servlet...");    }    @Override    public void init() throws ServletException {        System.out.println("HelloServlet..初始化开始..");    }}

运行结果:
这里写图片描述

非阻塞的servlet容器

git checkout step-003
前面的实现,都是非常简单的单线程,万一线程阻塞,那岂不是不能提供服务了,实际的产品肯定不能是这样的。tomcat连接器监听端口,获取创建socket,这个应该是单点,而容易阻塞的地方,应该是具体业务逻辑处理的代码了,为了保证服务的可用,提高系统的吞吐率,可以使用多线程提供多个处理器,让每个请求有单独的线程进行处理,这样性能会大幅度提高,不会以为单个连接出现问题而造成服务不可用。下面我们分析具体的代码。
Bootstrap应用启动,主要功能,初始化连接器:

    public final class Bootstrap {    public static void main(String[] args) {        HttpConnector connector = new HttpConnector("localhost", 8080);        try {            connector.initialize();            connector.start();            System.in.read(); //连接器线程,让主线程挂起,保证其他后台线程运行        } catch (UnknownHostException e) {            e.printStackTrace();        } catch (TomcatException e) {            e.printStackTrace();        } catch (IOException e) {            e.printStackTrace();        }    }}

连接器进行本地端口的监听,实例化并运行处理器:

/**     * 初始化连接处理器和处理器     * @throws TomcatException     * @throws UnknownHostException     * @throws IOException     */    public void initialize() throws TomcatException, UnknownHostException, IOException {        createServer();        while (curProcessors < minProcessors) {            if (curProcessors >= maxProcessors)                break;            HttpProcessor processor = newProcessor();            recycle(processor);        }    }

处理器进入wait状态(HttpProcessor中方法),等待被唤醒。

    @Override    public void run() {        try {            while(!Thread.interrupted()) {                Socket socket = waitingToNewSocket();                process(socket);            }        } catch (InterruptedException | IOException | ServletException e) {            System.out.println("处理器异常。。。");        }    }    private synchronized Socket waitingToNewSocket() throws InterruptedException {        while (!newSocketCome) {            wait();        }        Socket socket = this.socket;        newSocketCome = false;        return (socket);   }

当有请求到来时,唤醒挂起的处理器,处理 完成,处理器重新入栈,等待下次被使用。

public synchronized void assign(Socket socket) {        this.socket = socket;        newSocketCome = true;        notifyAll();    }

模块化的web容器

git checkout step-004
tomcat作为一个大型的产品,为了开发维护的方便,必然的会把大任务进行分解,进行分层分模块,这也是如今软件设计的思路,模块清晰,层次明了,对于后期的维护,更新都会有极大的便利。
tomcat进行功能的拆分,模拟管道与阀的思想,对于容器的处理组件,都放在阀中,请求会像流水一样流过每个阀,最后得到最终的处理。

这里写图片描述
而tomcat通过接口来定下标准,Pipeline接口表示管道,Valve表示阀,具体可以看代码。

生命周期监听

git checkout step-005
本节主要增加了生命周期控制组件,接口Lifecycle,定义组件的生命周期,主要增加了事件监听模块,监听tomcat启动状态改变,并作出反应,下图监听的逻辑。
这里写图片描述
每个实现生命周期接口的组件都能进行监听器的绑定,统一的实现监听器的控制逻辑,使用了LifecycleSupport来代理,实现监听器绑定,解绑,通知。

0 0
原创粉丝点击