仿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连接的理解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o2bQBP7p-1628067076111)(C:\Users\26905\AppData\Roaming\Typora\typora-user-images\image-20210804152847666.png)]

<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);
        try(OutputStream os=new FileOutputStream(sessionDataFile)){
            ObjectOutputStream objectOutputStream=new ObjectOutputStream(os);
            objectOutputStream.writeObject(sessionData);
            objectOutputStream.flush();
        }
    }
    @Override
    public Object getAttribute(String name) {
        return sessionData.get(name);
    }

    @Override
    public void removeAttribute(String name) {
        sessionData.remove(name);
    }

    @Override
    public void setAttribute(String name, Object value) {
        sessionData.put(name, value);
    }
  1. 构建Response响应对象

将响应的状态码,响应体及Cookie等信息放入Response对象中,并使用IO输出到页面

部分Response对象代码:

public class Response implements HttpServletResponse {
    public int status = 200;
    public final List<Cookie> cookieList;
    public final Map<String, String> headers;
    public final ByteArrayOutputStream bodyOutputStream;
    public final PrintWriter bodyPrintWriter;

    public Response() throws UnsupportedEncodingException {
        cookieList = new ArrayList<>();
        headers = new HashMap<>();
        bodyOutputStream = new ByteArrayOutputStream(1024);
        Writer writer = new OutputStreamWriter(bodyOutputStream, "UTF-8");
        bodyPrintWriter = new PrintWriter(writer);
    }
}
  1. 根据请求的contextPath找到,交给哪个Context处理

如果没找到Context,即交给默认Context处理(404)

Context handleContext = HttpServer.defaultContext;
for (Context context : HttpServer.contextList) {
    if (context.getName().equals(request.getContextPath())) {
        handleContext = context;
        break;
    }
}
  1. 根据servletPath找到,交给哪个Servlet处理

如果没找到ServletPath,交给默认的Servlet处理

Servlet servlet = handleContext.getServlet(request.getServletPath());
if (servlet == null) {
    servlet = HttpServer.defaultServlet;
}

这里默认的Servlet:

首先先寻找这个ServletPath是否是静态资源,如果是静态资源,按照静态资源的Servlet->DefaultServlet处理

如果也不是静态资源,交给NotFoundServlet处理->404处理

public class DefaultServlet extends HttpServlet {
    private final String welcomeFile="index.html";
    private final Map<String,String> mime=new HashMap<>();
    private final String defaultContentType="text/plain";

    //静态资源后缀名所对应的输出格式
    @Override
    public void init() throws ServletException {
        mime.put("htm","text/html");
        mime.put("html","text/html");
        mime.put("jpg","image/jepg");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String contextPath=req.getContextPath();
        String servletPath=req.getServletPath();
        //静态资源处理
        if(servletPath.equals("/")){
            servletPath=welcomeFile;//欢迎页面
        }
        String fileName=String.format("%s\\%s\\%s", HttpServer.WEBAPPS_BASE,contextPath,servletPath);
        System.out.println(fileName);
        File file=new File(fileName);
        if(!file.exists()){
            //按照404的方式进行处理
            HttpServer.notFoundServlet.service(req,resp);
            System.out.println("not");
            return;
        }
        String contentType=getContentType(servletPath);
        System.out.println(contentType);
        resp.setContentType(contentType);

        OutputStream outputStream=resp.getOutputStream();
        try(InputStream is=new FileInputStream(file)){
            byte[] buffer=new byte[1024];
            int len=-1;
            while ((len = is.read(buffer)) != -1){
                outputStream.write(buffer,0,len);
            }
            outputStream.flush();
        }

    }

    private String getContentType(String servletPath) {
        String contentType=defaultContentType;
        int i=servletPath.lastIndexOf(".");
        if(i != -1){
            String extension=servletPath.substring(i+1);
            contentType=mime.getOrDefault(extension,defaultContentType);
        }
        return contentType;
    }
}
  1. 调用servlet.service()方法,交给业务处理

自己实现的Servlet方法继承Servlet类,重写service()方法,实现自己的业务

servlet.service(request, response);
  1. 业务处理完之后,根据Response对象中的数据,发送HTTP响应

在发送HTTP响应时,需要将Cookie的数据放入响应体中,并将Session数据保存在本地文件中

private void sendResponse(OutputStream outputStream, Request request, Response response) throws IOException {
    // 保存 session
    // 1. 种 cookie
    // 2. 保存成文件
    if (request.session != null) {
        Cookie cookie = new Cookie("session-id", request.session.sessionId);
        response.addCookie(cookie);
        request.session.saveSessionData();
    }

    Writer writer = new OutputStreamWriter(outputStream, "UTF-8");
    PrintWriter printWriter = new PrintWriter(writer);
    for (Cookie cookie : response.cookieList) {
        response.setHeader("Set-Cookie", String.format("%s=%s", cookie.getName(), cookie.getValue()));
    }
}
(3)销毁所有的Servlet对象,结束生命周期
private static void destroyAllServletClass () {
    for (Context context : contextList) {
        context.destroy();
    }
    defaultServlet.destroy();
    notFoundServlet.destroy();
}

<3>自定义登录逻辑进行测试

public class LoginActionServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username=req.getParameter("username");
        String password=req.getParameter("password");
        if(username.equals("zyf") && password.equals("123")){
            User user=new User(username,password);
            HttpSession session = req.getSession();
            session.setAttribute("user",user);
            resp.sendRedirect("profile-action");
        }else {
            resp.sendRedirect("login.html");
        }
    }
}
public class ProfileActionServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession();
        User user=(User)session.getAttribute("user");
        if(user == null){
            resp.sendRedirect("login.html");
        }else {
            resp.setContentType("text/plain");
            resp.getWriter().println(user.toString());
        }
    }
}

4.项目总结

HTTP服务器部分的总结:

在这里插入图片描述

仿Tomcat的设计:

管理多个Web项目的Servlet容器,通过TCP建立连接,解析Http的请求和响应。对Servlet的生命周期进行管理(通过不同项目的类加载器,通过反射,每个项目对各自的Servlet进行管理)

5.项目的不足

  1. 只支持了HTTP1.0协议(短链接),一条TCP只能处理一次的请求-响应周期
  2. 只支持了GET方法
  3. 字符集的编码,固定成了"UTF-8"

对于TCP Server

  1. 项目使用的时BIO的形式,真实的Tomcat使用的NIO
  2. 线程池只是使用的最简单的,不高效
  3. 没有涉及日志功能,调试只能使用System.out.printIn()打印观察
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Serendipity sn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值