tomcat实现(3)——tomcat实现的具体细节

这是tomcat实现的第三篇,tomcat实现的具体细节
完整源码地址:https://github.com/zhangjingao/tomcat.git

在开始之前,先来了解一下客户端和服务器端建立连接的技术——socket套接字。
老习惯,上图再说话
这里写图片描述

socket是什么
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket
Socket为TCP协议提供了两个类,分别为Socket和ServerSocket,一个代表客户端,一个代表 服务器端。通过操作这两个类即可实现TCP协议通信

由上篇博客得知tomcat在运行中需要做下面几个事情:

  1. Main方法:
    servlet程序是没有Main的,但是一个程序没有Main是无法启动的,所以tomcat需要有Main方法。
  2. 得到所有的servlet,得到servlet类和servlet地址的映射关系
    tomcat要判断哪些类是servlet,并可以根据地址实例化servlet对象。
  3. 同时接受多个请求
    tomcat要满足多个客户端的同时访问,所以需要多线程接收多个请求。
  4. 处理请求
    如果是请求的静态资源就直接定位静态资源写回客户端,如果请求的servlet,那么定位具体的请求的servlet并调用。
  5. 得到响应并返回客户端

先看一下我的目录结构有个总体印象
这里写图片描述
webapp:所有的jsp以及html等静态资源都存在这
com.zjg.tomdog包下:
1. annotations:
WebService类是一个自定义注解,凡是实现了HttpServlet接口并且被WebService注解标志的才被认为是servlet。
ServletMapping类是将所有的servlet装进一个Map集合中
2. controller: 是一个模拟web应用的包,我们整个项目是一个tomcat,如果再新建一个项目作为web项目,然后加载这个项目去调用web项目中的servlet就比较麻烦,于是这个包就是一个模拟的web应用,里面有普通的类还有servlet
3. handle:这个包起到的作用就是接收请求,监听80端口,接收客户端请求。
4. handlecore和handlecoreimpl包:
分别是处理请求接口包和处理请求实现类包,主要功能有读取请求,判断请求的资源类型,转发调用servlet,关闭连接。
5. servletdefine:定义servlet的接口,定义其必须实现的get和post方法。
6. thread:多线程包,分出一个线程进行处理客户端请求。
7. util:工具包,判断请求资源类型,request请求定义,response定义

这里主要介绍核心伪代码,细节可以去github看我的源码
第一步:Main方法开启tomcat就不用多说了吧
第二步:得到所有的servlet,得到servlet类和servlet地址的映射关系
首先定义一个静态的存放servlet的map集合,当然也可以是其他的。

public static Map<Class<?> , String> servlets = new HashMap<Class<?>, String>();
    /**
     * 映射注解
     */
    public void getAnnotation () {
        Map<Class<?> , String> allClass = getClasses("com.zjg.tomdog");//得到com.zjg.tomdog包下所有的java类
        for (Map.Entry<Class<?>, String> clas : allClass.entrySet()) {
            boolean useAnnota = clas.getKey().isAnnotationPresent(WebService.class);//判断是否被WebService这个注解标志,是的话存放进map
            if (useAnnota) {
                MainApp.servlets.put(clas.getKey(),clas.getValue());
            }
        }
    }

第三步:同时接受多个请求
这里使用循环监听连接请求,每当监听到一个连接请求就从线程池中分出一个线程去处理。

    /**
     * 监听并处理请求
     */
    public void handle() {
        ThreadPoolFactory poolFactory = ThreadPoolFactory.getInstance();
        try {
            ServerSocket serverSocket = new ServerSocket(80);//监听80端口
            while (true) { //等待请求
                Socket socket = serverSocket.accept();//当服务器接受一个请求时就创建一个socket
                poolFactory.addTask(new ThreadTask(socket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
package com.zjg.tomdog.thread;

import com.zjg.tomdog.handlecore.ExceptionHandle;
import com.zjg.tomdog.handlecore.HandleCore;
import com.zjg.tomdog.handlecoreimpl.ExceptionHandleImpl;
import com.zjg.tomdog.handlecoreimpl.HandleCoreImpl;
import com.zjg.tomdog.util.Request;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author zjg
 * @date 2018/4/4 20:30
 * @Description
 */
public class ThreadTask extends Thread {

    private Socket socket = null;

    private final HandleCore handleCore = new HandleCoreImpl();//核心方法类

    private final ExceptionHandle exceptionHandle = new ExceptionHandleImpl();

    public ThreadTask (Socket socket) {
        this.socket = socket;
    }

   @Override
   public void run () {
       int timeOut = 3; //3秒超时
       Date start = new Date();//第一次进入时间
       Date end;
       Request requestMessage = null;
       while (true) {
           end = new Date();//再次请求时间
           long requestTime = (end.getTime() - start.getTime())/1000;
           try {
               if (requestTime > timeOut) {
                   handleCore.closeSocket(socket);
                   break;
               } else {
                   start = end;//不超时则重新赋值计算超时的时间
               }
               socket.setSoTimeout(3*1000);//设置超时时间为3秒,超出抛出异常
               requestMessage = handleCore.read(socket); //读取request请求
               if (null != requestMessage) {
                   handleCore.forword(socket, requestMessage);//加载请求的类,方法
               } else {
                   handleCore.closeSocket(socket);
                   break;
               }
           } catch (Exception e) {
               e.printStackTrace();
               exceptionHandle.cantHandlePath("接收请求或关闭socket异常");
               handleCore.closeSocket(socket);
               break;
           }
       }
   }

}

这里有个超时时间,前面说过了,对于长连接一次请求结束后并不是直接http请求断开了,而是继续使用,继续使用那么服务器端怎么直到这个http请求什么时候不再使用了呢,其实一般不用服务器端管理,一个http请求结束后,客户端如果不需要了,它会直接断开,如果需要,那么每隔一段时间,客户端会自动发一个心跳包维持连接,就是说http自己有一个超时时间,如果客户端继续使用的话,客户端会在这个时间之前发一个心跳包维持http的连接。这个超时管理对于服务器端来讲是一个保险机制。
那么我们限制超时时间3s。请求刚来的时候我们记录时间,当它处理完这个请求后再判断下时间是否超时,socket.setSoTimeout这个方法就是另一道保险。这个方法用于限制socket连接时间是3s,设置他的原因是我们可以得到处理完请求前的时间,但无法判断它再次读请求的时间,如果它卡在读请求的地方,那么这个线程永远无法释放,所以这个方法是另一道保险,如果时间到了,直接抛出异常,停止连接。
看一下是如何读请求的,也就是这行代码的实现
requestMessage = handleCore.read(socket); //读取request请求

    @Override
    public Request read (Socket socket) {
        String url = null;
        try {
            InputStream inputStream = new BufferedInputStream(socket.getInputStream());
            byte reads = (byte) inputStream.read();
            byte[] bc = new byte[inputStream.available()+1];
            bc[0] = reads;
            inputStream.read(bc, 1, inputStream.available());
            url = new String(bc);

        } catch (SocketTimeoutException ex) {
            System.out.println("Read timed out");
        } catch (IOException e) {
            new ExceptionHandleImpl().cantHandlePath("连接超时");
            return null;
        }
        Request httpMessage = null;
        if (url != null) {
            httpMessage = new Request(url,socket);
        }
        return httpMessage;
    }

再看一下,读到的请求,
读到的请求是这样

GET /BackServlet HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.79 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

我们如何提取到我们需要的信息,并封装为request呢?看下面

package com.zjg.tomdog.util;

import com.zjg.tomdog.handlecore.ExceptionHandle;

import javax.sound.midi.Soundbank;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

/**
 * @author zjg
 * @date 2018/3/26 17:02
 * @Description 接收到的http信息
 */
public class Request {

    private String protocol;//协议类型

    private String data;//请求数据

    private String method;//请求方法(post/get)

    private Map<String , String> attrs; //参数

    private Socket socket;


    /**
     * 请求方法
     * @param protocol 协议
     * @param data 请求数据
     * @param method 请求方法
     */
    public Request(String protocol, String data, String method) {
        this.protocol = protocol;
        this.data = data;
        this.method = method;
    }

    /**
     * 请求方法
     * @param request 完整的url
     */
    public Request(String request,Socket socket) {
        this.getRequestMess(request);
        this.socket = socket;
    }

    /**
     * 得到request信息
     * @param request request url
     */
    private void getRequestMess (String request) {
        String [] methodUrlPro = request.split("\n")[0].split(" ");
        if (methodUrlPro.length == 3) {
            this.method = methodUrlPro[0];
            this.protocol = methodUrlPro[2];
            String [] urlAttr = methodUrlPro[1].split("\\?");//分隔参数和url
            this.data = urlAttr[0].substring(1,urlAttr[0].length());
            String [] attrArray , keyValue;//所有参数,每个参数的参数名和值
            if (urlAttr.length > 1) {
                attrArray = urlAttr[1].split("&");
                for (String s : attrArray) {
                    keyValue = s.split("=");
                    this.addAttrs(keyValue[0],keyValue[1]);
                }
            }
        }
        System.out.println("request message:"+request);
    }

    /**
     * 添加参数
     * @param key 参数名
     * @param value 值
     */
    private void addAttrs(String key,String value) {
        if (attrs == null) {
            attrs = new HashMap<String , String>();
        }
        this.attrs.put(key,value);
    }
    public void setProtocol (String protocol) {
        this.protocol = protocol;
    }

    public String getProtocol() {
        return protocol;
    }

    public void setData(String data) {
        this.data = data;
    }

    public String getData() {
        return data;
    }

    public void setMethod(String method) {
        this.method = method;
    }

    public String getMethod() {
        return method;
    }

    public Map<String, String> getAttrs() {
        return attrs;
    }

    public void setAttrs(Map<String, String> attrs) {
        this.attrs = attrs;
    }

    public Socket getSocket() {
        return socket;
    }

    public void setSocket(Socket socket) {
        this.socket = socket;
    }

    @Override
    public String toString() {
        return "\n{ \nprotocol: "+protocol+"\ndata: "+data+"\nmethod: "+method+"\nattrs:"+attrs+"\n}";
    }
}


根据请求信息之前的分割符分割开,存放进request,
读请求就结束了,看一下处理请求

    @Override
    public void forword(Socket socket, Request requestMessage) {
        boolean isStatic = new JudgeSource().judgeSourceType(requestMessage.getData());
        String path = null;
        if (!isStatic) {
            Map<Class<?> , String> allServlet = MainApp.servlets;//所有的servlet映射
            String requestData = requestMessage.getData();//请求信息中的具体请求serlvet
            String requestMethod = requestMessage.getMethod();//请求方法
            boolean isExists = false;//是否存在这个servlet
            try {
                Class<?> clazz = null;
                Method method = null;
                Response response = new Response();//响应对象
                for (Map.Entry<Class<?> , String> servlets: allServlet.entrySet()) {//循环判断,因为存放的是类的包名,所以不能由key直接找到映射信息
                    String servletName = servlets.getKey().getSimpleName();
                    if (requestData.equals(servletName)) {
                        isExists = true;
                        clazz = Class.forName(servlets.getKey().getName());//反射实例化servlet对象
                        if ("GET".equals(requestMethod)) { //判断请求方式
                            method = clazz.getMethod("doGet",Request.class,Response.class);
                        } else if ("POST".equals(requestMethod)) {
                            method = clazz.getMethod("doPost",Request.class,Response.class);
                        }
                        assert method != null;
                        method.invoke(clazz.newInstance(),requestMessage,response);//调用方法
                    }
                }
                if (!isExists) {
                    path = exceptionHandle.noFoundPath(requestData);//请求的类名
                }
            } catch (NoSuchMethodException e) {
                path = exceptionHandle.cantHandlePath("NoSuchMethodException: 不存在这样的方法");
            } catch (IllegalAccessException |InstantiationException | InvocationTargetException e) {
                path = exceptionHandle.cantHandlePath("反射机制使用异常");
            } catch (ClassNotFoundException e) {
                path = exceptionHandle.noFoundPath("ClassNotFoundException: 反射机制加载类失败,该类不存在");
            }
        } else {
//            System.out.println("静态资源");
            path = requestMessage.getData();
        }
        if (path != null) {
            new Response().write(socket,path);
        }
    }

先判断请求资源类型,如果是动态资源,那么就寻找是否存在这个servlet,存在就反射调用,不存在就报404,未找到。如果是静态资源,直接写回客户端。

看一下其中一个servlet,然后看下运行效果图

package com.zjg.tomdog.controller;

import com.zjg.tomdog.annotations.WebService;
import com.zjg.tomdog.servletdefine.HttpServlet;
import com.zjg.tomdog.util.Request;
import com.zjg.tomdog.util.Response;

/**
 * @author zjg
 * @date 2018/4/9 21:26
 * @Description
 */
@WebService
public class BackServlet implements HttpServlet{

    @Override
    public void doGet(Request request , Response response) {
        System.out.println("do get ... do something ...... tologin");
        response.write(request.getSocket(),"index.html");
    }

    @Override
    public void doPost(Request request , Response response) {
        System.out.println("do post ...... do something");
        response.write(request.getSocket(),"homepage.html");
    }

}

当访问http://127.0.0.1/BackServlet 时,看一下运行效果,后台效果如下,前台能正常显示
这里写图片描述

这里写图片描述

至此所有的地方都展示和讲解完了,如果有什么问题可以留言讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值