仿Tmocat的简易版HTTP服务器

Posted Serendipity sn

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了仿Tmocat的简易版HTTP服务器相关的知识,希望对你有一定的参考价值。

HTTP项目(MyTomcat)

1.项目总览

流程图:

流程:

  1. 初始化工作
    1. 扫描所有的Context
    2. 读取并解析各子的web配置文件
    3. 加载需要的ServletClass,表现为Class<?>
    4. 实例化需要的Servlet对象
    5. 执行Servlet对象的初始化工作
  2. 处理Http请求-响应(单次的请求响应处理逻辑)
    1. 读取解析HTTP请求->Request对象,实现标准种定义的HttpServletRequest接口
      1. 解析请求行
        1. 解析方法
        2. 解析路径
          1. contextPath
          2. servletPath
          3. queryString->parameters
        3. 解析版本(这里我们不使用)
      2. 解析请求头(核心是解析cookie,根据cookie-name是session的找出session-id(也可能不存在))
      3. 理论上也需要解析请求体,但是这里我们只支持Get方法
    2. 构建Response对象
    3. 根据请求的contextPath找到,交给哪个Context处理
    4. 根据servletPath找到,交给哪个Servlet处理
    5. 调用servlet.service(请求,响应)
    6. 发送Response对象->响应
  3. 销毁工作

2.Servlet容器

作为Servlet容器,所以满足Servlet标准,定义了满足Servlet标准的抽象类和接口

3.HTTP服务器

<1>TCP连接的理解

<2>正式项目

(1)初始化工作

找到所有Servlet对象,进行初始化

  1. 通过webapps目录下不同的Web项目,找到他们的项目路径Context

    本质上是文件操作
    扫描固定的目录(webapps)下有哪些子目录
    目录名称作为context的name

 public static final String WEBAPPS_BASE = "D:\\\\javaCode\\\\HTTP\\\\http-project\\\\webapps";
    //管理所有的Context对象
    public static final List<Context> contextList = new ArrayList<>();
    private static final ConfigReader configReader = new ConfigReader();
    public static final DefaultContext defaultContext=new DefaultContext(configReader);

    private static void scanContexts() {
        //扫描目录,获取context
        File webappsRoot = new File(WEBAPPS_BASE);
        File[] files = webappsRoot.listFiles();
        if (files == null) {
            throw new RuntimeException();
        }
        for (File file : files) {
            //不是目录,说明不是web应用,直接跳过
            if (!file.isDirectory()) {
                continue;
            }
            //获取各个web应用对应的应用上下文路径(context)
            String contextName = file.getName();
            Context context = new Context(configReader, contextName);
            contextList.add(context);
        }
    }
  1. 读取并解析webapps/WEB-INF目录下的web.conf文件,获取Servlet类的名称

web.conf:例如

servlets:
  # ServletName = ServletClassName
  TranslateServlet = org.example.webapps.dictionary.TranslateServlet
  LoginActionServlet= org.example.webapps.dictionary.LoginActionServlet
  ProfileActionServlet = org.example.webapps.dictionary.ProfileActionServlet

servlet-mappings:
  # URLPattern = ServletName
  /translate = TranslateServlet
  /login-action = LoginActionServlet
  /profile-action = ProfileActionServlet

利用有限状态机已经对字符串的切割等处理,得到两个map,找到了url和需要处理的Servlet类对象之间的映射

//ServletName和ServletClassName对应关系:上文servlets:后的对应关系
Map<String,String > servletNameToServletClassNameMap=new HashMap<>();
//URI和Servlet类名称的对应关系:上文servlet-mappings:后的对应关系
LinkedHashMap<String,String> urlToServletNameMap=new LinkedHashMap<>();

代码:

 public Config read(String name) throws IOException {
         Map<String,String > servletNameToServletClassNameMap=new HashMap<>();
         LinkedHashMap<String,String> urlToServletNameMap=new LinkedHashMap<>();
        //进行web.conf文件的读取+解析
        //规范:web.conf放哪里,必须符合规范,否则就会读不到
        String fileName=String.format("%s/%s/WEB-INF/web.conf", HttpServer.WEBAPPS_BASE,name);//两个参数分别表示webapps的绝对路径即Context项目地址
        String stage="start";//"servlets"/"mappings" 下面switch的三种状态,分别表示三个解析的步骤

        //进行文本文件的读取
        try (InputStream is=new FileInputStream(fileName)){
            Scanner scanner=new Scanner(is,"UTF-8");
            while (scanner.hasNextLine()){
                String line=scanner.nextLine().trim();
                if(line.isEmpty() || line.startsWith("#")){
                    //如果是空行或者是以#开头的注释不做处理
                    continue;
                }
                switch (stage){
                    case "start":
                        if(line.equals("servlets:")){
                            stage="servlets";
                        }
                        break;
                    case "servlets":
                        if(line.equals("servlet-mappings:")){
                            stage="mappings";
                        }else {
                            // 进行Servlet解析 ServletName=>ServletClassName的解析
                            String []parts=line.split("=");
                            String servletName=parts[0].trim();
                            String servletClassName=parts[1].trim();
                            servletNameToServletClassNameMap.put(servletName,servletClassName);
                        }
                        break;
                    case "mappings":
                        //进行URL => ServletName的解析
                        String []parts=line.split("=");
                        String url=parts[0].trim();
                        String servletName=parts[1].trim();
                        urlToServletNameMap.put(url,servletName);
                        break;
                }
            }
        }
        return new Config(servletNameToServletClassNameMap,urlToServletNameMap);
    }
  1. 加载需要的ServletClass,表现为Class<?>

通过步骤2,得到了URI对应需要处理的Servlet的全类名,通过反射,可以得到需要的类

进行Servlet类加载

 List<Class<?>> servletClassList=new ArrayList<>();
    public void loadServletClasses() throws ClassNotFoundException {
       Set<String> servletClassNames = new HashSet<>(config.servletNameToServletClassNameMap.values());
       for(String servletClassName : servletClassNames){
           Class<?> clazz=webappsClassLoader.loadClass(servletClassName);
           servletClassList.add(clazz);
       }
    }

这里我们每个Context项目使用各自的类加载器classLoader,将项目之间进行隔离,防止数据库等版本的不同,而进行类加载时发生错误

  1. 实例化需要的Servlet对象

List<Servlet> servletList = new ArrayList<>();
    public void instantiateServletObjects() throws IllegalAccessException, InstantiationException {
        for(Class<?> servletClass : servletClassList){
            //利用反射,默认调用该类的无参构造方法,进行实例化对象
            Servlet servlet = (Servlet) servletClass.newInstance();
            servletList.add(servlet);
        }
    }

利用反射,实例化Servlet对象

  1. 调用Servlet的init(),执行类的初始化工作

    涉及到“Servlet生命周期”的概念
    调用每个Servlet对象的init()方法,这样子类可以通过重写自己的init()方法,进行不同的初始化

private static void initializeServletObjects() throws ServletException {
        for (Context context : contextList) {
            context.initServletObjects();
        }
        defaultServlet.init();
        notFoundServlet.init();
    }

(2)处理HTTP请求-响应(单次的请求响应处理逻辑)

服务器逻辑:使用简单的线程池,使用多线程对每次的响应进行处理,每次响应间不存在共享变量,所以无序考虑线程安全问题,将单次响应任务放入RequestResponseTask任务中处理

private static void startServer() throws IOException {
    ExecutorService threadPool = Executors.newFixedThreadPool(10);
    ServerSocket serverSocket = new ServerSocket(8080);
    //2.每次循环处理一个请求
    while (true) {
        Socket socket = serverSocket.accept();
        Runnable task = new RequestResponseTask(socket);
        threadPool.execute(task);
    }
}

RequestResponseTask任务处理逻辑:

  1. 读取解析HTTP请求->Request对象,实现标准中定义的HttpServletRequest接口

我们定义一个专门解析请求Request的类,对HttpRequest的请求行和请求头进行解析(简易版HTTP服务器,只支持GET方法,所以我们不解析请求体).保存请求中的Cookie信息

public class HttpRequestParser {

    public Request parse(InputStream socketInputStream) throws IOException, ClassNotFoundException {
        //1.读取请求行
        Scanner scanner=new Scanner(socketInputStream);
        String method=scanner.next().toUpperCase();//读取请求方法
        String path=scanner.next();//读取请求的全路径
        
        //解析parameters,请求行传来的参数
        Map<String,String> parameters=new HashMap<>();
        String requestURI=path;
        int i=requestURI.indexOf("?");
        if(i != -1){
            requestURI=path.substring(0,i);
            String queryString = path.substring(i+1);
            for(String kv : queryString.split("&")){
                String[] partsKV = kv.split("=");
                String name= URLDecoder.decode(partsKV[0],"UTF-8");
                String value= URLDecoder.decode(partsKV[1],"UTF-8");
                parameters.put(name,value);
            }
        }

        //解析contextPath和servletPath
        int j=requestURI.indexOf('/',1);//找到第二个"/"
        String contextPath="/";
        String servletPath=requestURI;
        if(j != -1){
            //例如:requestURI=/blog/add
             contextPath=requestURI.substring(1,j);//   blog(好比较)
             servletPath=requestURI.substring(j);  //   /add
        }



        String version=scanner.nextLine();//读取版本信息,没用

        //2.读取请求头,将请求头种的Cookie信息保存
        String headerLine;
        Map<String,String> headers=new HashMap<>();
        List<Cookie> cookieList=new ArrayList<>();
        while (scanner.hasNextLine() && !(headerLine=scanner.nextLine().trim()).isEmpty()){
            String[] parts=headerLine.split(":");
            String name=parts[0].toLowerCase();
            String value=parts[1];
            headers.put(name,value);
            //判断是否是cookie
            if(name.equals("cookie")){
                String [] kvcookies=value.split(";");
                for(String kvcookie : kvcookies){
                    if(kvcookie.trim().isEmpty()){
                        continue;
                    }
                    String[] split = kvcookie.split("=");
                    String cookieName=split[0].trim();
                    String cookieValue=split[1].trim();
                    Cookie cookie=new Cookie(cookieName,cookieValue);
                    cookieList.add(cookie);
                }

            }
        }
        return new Request(method,requestURI,contextPath,servletPath,parameters,headers,cookieList);
    }
}

将请求方法,请求的全路径,项目路径ContextPath,Servlet路径servletPath,请求的参数,请求头以及Cookie信息放入到Request对象中

对于Request对象,我们需要遍历Cookie信息,当Cookie存在Session-id时,需要构建Session对象,这里我们专门建一个session文件夹,将Session数据按文件形式保存本地,持久化

 for(Cookie cookie : cookieList){
            if(cookie.getName().equals("session-id")){
                String sessionId=cookie.getValue();
                session = new HttpSessionImpl(sessionId);
                break;
            }
        }

Session对象:

通过loadSessionData()方法,将Session数据保存在本地文件

public class HttpSessionImpl implements HttpSession {
    public final Map<String,Object> sessionData;

    public final String sessionId;
    //没有从cookie中拿到sessionId时使用
    public HttpSessionImpl(){
        sessionId= UUID.randomUUID().toString();//没有传入,随机生成一个
        sessionData=new HashMap<>();
    }
    //从cookie中拿到了sessionId时使用
    public HttpSessionImpl(String sessionId) throws IOException, ClassNotFoundException {
        this.sessionId=sessionId;
        sessionData=loadSessionData(sessionId);//加载Session数据
    }


    private static final String SESSION_BASE="D:\\\\javaCode\\\\HTTP\\\\http-project\\\\sessions";

    //加载Session里面的数据
    //文件名 : <session-id>.session
    private Map<String, Object> loadSessionData(String sessionId) throws IOException, ClassNotFoundException {
        String sessionFileName=String.format("%s\\\\%s.session",SESSION_BASE,sessionId);
        File sessionFile=new File(sessionFileName);
        if(!(sessionFile.exists())){
            return new HashMap<>();//session不存在,返回一个空的map
        }
        try(InputStream is=new FileInputStream(sessionFile) {
        }){
            //使用ObjectInputStream进行对象读取
            ObjectInputStream objectInputStream=new ObjectInputStream(is);
            return (Map<String, Object>) objectInputStream.readObject();
        }
    }

    //保存Session里面的数据
    public void saveSessionData() throws IOException {
        if(sessionData.isEmpty()){
            return;
        }
        String sessionDataFile=String.format("%s\\\\%s.session",SESSION_BASE,sessionId

以上是关于仿Tmocat的简易版HTTP服务器的主要内容,如果未能解决你的问题,请参考以下文章

仿Tmocat的简易版HTTP服务器

FineReport如何部署Tomcat服务器集群

FineReport如何部署Tomcat服务器集群

十三.Netty入门到超神系列-手撸简单版RPC框架(仿Dubbo)

运动的border,仿当当简易效果

python3用http.server模块搭建简易版服务器