手写模拟一个tomcat

前言:

通过自己手写模拟tomcat的核心功能,能够让我们更好的理解tomcat的运行机制,底层也是通过jdk提供的api进一步步封装实现而来,我们作为使用者而言,可能更多关注使用上的方便、敏捷性,通过手写源码,相信我们会对tomcat有更加深刻的理解。

【版本一】:测试服务端和客户端建立连接

image.png
创建一个客户端(模拟浏览器)

package com.server;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 创建一个服务端
 */
public class Server {
    public static void main(String[] args) {
        try {
            //创建一个Socket接收请求
            ServerSocket serverSocket=new ServerSocket(9999);
            //等待客户端的连接(浏览器、其他发起请求的终端)
            Socket socket = serverSocket.accept();//阻塞效果,一直等待
            System.out.println("有一个客户端连接过来了");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

创建一个服务器端(接收请求)

package com.browser;

import java.io.IOException;
import java.net.Socket;

/**
 * 模拟一个浏览器(客户端)
 */
public class Browser {
    public static void main(String[] args) {
        try {
            //1、主动发起一个请求
            Socket socket = new Socket("localhost", 9999);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        //2、输入一个url
        //3、解析url
        //4、创建一个连接
        //5、发送请求给服务器(输出流)
        //6、读取服务器写回来的相应响应信息(String)
        //7、解析响应信息, 浏览器中展示出来
    }
}

启动验证结果:
image.png

【版本二】:完整版本

image.png

模拟一个客户端(浏览器):Client

package com.browser;

import java.io.*;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

/**
 * 模拟一个浏览器(客户端)
 */
public class Client {
    public static void main(String[] args) {
        //启动客户端
        new Client().open();
    }

    private Socket socket;

    //2、打开一个浏览器,输入一个url ip:port/context?key=value
    //设计一个方法
    private void open() {
        System.out.println("===打开浏览器==");
        System.out.println("请输入一个请求的url:");
        Scanner input = new Scanner(System.in);
        //读取输入的一行
        String url = input.nextLine();
        //解析urL
        parseurl(url);
    }


    //3、解析url  ip port context?key=value
    private void parseurl(String url) {
        //ip:port/context?key=value
        int index1 = url.indexOf(":");//冒号的位置
        int index2 = url.indexOf("/");//返回 /的位置
        //获取ip
        String ip = url.substring(0, index1);
        //获取端口
        int port = Integer.parseInt(url.substring(index1 + 1, index2));
        //获取context的内容 ===>context?key=value
        String context = url.substring(index2 + 1);
        createSocketAndSendRequest(ip, port, context);
    }

    //4、创建一个连接
    //5、发送请求给服务器(输出流)
    private void createSocketAndSendRequest(String ip, int port, String context) {
        //创建socket
        try {
            socket = new Socket(ip, port);
            OutputStream outputStream = socket.getOutputStream();
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.println(context);
            printWriter.flush();
            //接受响应的信息
            this.receiveResponse();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    //6、读取服务器写回来的相应响应信息(String)
    private void receiveResponse() {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String responseContent = reader.readLine();
            while (responseContent != null && "" != responseContent) {
                System.out.println(responseContent);
                responseContent = reader.readLine();
            }
            this.parseResponseContent(responseContent);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 7、解析响应信息, 浏览器中展示出来
     *
     * @param responseContent
     */
    private void parseResponseContent(String responseContent) {
        System.out.println(responseContent);
    }
}

服务端:Server

服务端入口:Server

image.png

package com.server;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * 创建一个服务端
 */
public class Server {


    //维护一个socket对象
    private Socket socket;

    public static void main(String[] args) throws IOException {
        System.out.println("== myTomcat start===");
        ServerSocket serverSocket = new ServerSocket(9999);
        //因为这里是一直对请求进行接收的,所以应该是死循环
        while (true) {
            Socket socket = serverSocket.accept();
            Handler handler = new Handler(socket);
            //启动多线程
            handler.start();
        }

        //测试解析浏览器的请求:
        // 简易版的服务器 ,读取真正浏览器发来的请求
//        ServerSocket serverSocket=new ServerSocket(9999);
//        //接受请求
//        Socket socket = serverSocket.accept();
//        BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//        String value = reader.readLine();
//        //循环读取内容太
//        while (value!=null && ""!=value){
//            System.out.println(value);
//            value=reader.readLine();
//        }
        //浏览器真正的消息  GET /index?username=hsc&pass=17 HTTP/1.1 这样的参数,需要进行解析
//        解析一个请求得到的完整信息:
//        GET /index?username=hsc&pass=17 HTTP/1.1
//        Host: localhost:9999
//        Connection: keep-alive
//        sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"
//        sec-ch-ua-mobile: ?0
//        sec-ch-ua-platform: "Windows"
//        Upgrade-Insecure-Requests: 1
//        User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
//        Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
//        Sec-Fetch-Site: none
//        Sec-Fetch-Mode: navigate
//        Sec-Fetch-User: ?1
//        Sec-Fetch-Dest: document
//        Accept-Encoding: gzip, deflate, br
//        Accept-Language: zh-CN,zh;q=0.9
    }


}
请求数据的载体:HttpServletRequest
package com.server;

import java.util.Map;

public class HttpServletRequest {
    private String requestName;
    private Map<String,String> paramenterMap;

    public HttpServletRequest(String requestName, Map<String, String> paramenterMap) {
        this.requestName = requestName;
        this.paramenterMap = paramenterMap;
    }
    //提供get方法获取值
    public String getRequestName(){
        return requestName;
    }

    public Map<String,String> getParamenterMap(){
        return paramenterMap;
    }
    public String getParameter(String key){
        return paramenterMap.get(key);
    }

}

响应数据的载体:HttpServletResponse
package com.server;

//定义这个类的目的,是为了当做一个容器,装在controller执行后的结果
// 类中可以描述一个方法,帮助处理String的处理,用户使用起来更加的方便
public class HttpServletResponse {

    private StringBuilder responseContent=new StringBuilder();

    /**
     * 添加方法,用来向responseContent属性中追加响应内容
     * @param message
     */
    public void write(String message){
        responseContent.append(message);
    }

    /**
     * 提供一个方法。获取responseContent属性中内容
     * @return
     */
    public String getResponseContent(){
        return responseContent.toString();
    }
}

定义规范抽象类:HttpServlet
package com.server;
//服务器定义规则
//目的是为了后续的使用者遵循这个规则
//遵序该规则,服务器将帮助进行管理
public abstract  class HttpServlet {
    //定义抽象方法
    public abstract void service(HttpServletRequest request,HttpServletResponse response);
}

服务器多线程处理请求:Handler
package com.server;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

/**
 * 服务器未来只有一个
 * 启动服务器之后就不能停掉了
 * 如果来了一个浏览器访问,需要启动一个服务器线程去处理当前浏览器的请求和响应
 * 服务器等待下一个浏览器
 */
public class Handler extends Thread {


    //因为这里启用多线程进行接收请求,run方法是无法传递参数的
    private Socket socket;

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

    @Override
    public void run() {
        this.receiveRequest(socket);
    }

    //为了管理每个controller对象是单例的
    //服务器内写一个管理对象的容器,
    private Map<String, HttpServlet> contrllerMap = new HashMap<>();


//这个方法就不需要了
//    private void start() {
//        System.out.println("== myTomcat start===");
//        //1、启动服务器:创建一个Socket接收请求
//        ServerSocket serverSocket = null;
//        try {
//            serverSocket = new ServerSocket(9999);
//            //2、等待客户端的连接(浏览器、其他发起请求的终端)
//            socket = serverSocket.accept();//阻塞效果,一直等待
//            System.out.println("有一个客户端连接过来了");
//            //接收请求进行相应的处理
//        } catch (IOException e) {
//            throw new RuntimeException(e);
//        }
//    }

    //3、流读取:读取浏览器发送过来的请求信息 ===》content?key=value
    private void receiveRequest(Socket socket) {
        try {
            //创建一个用来读取信息的输入流
            InputStream inputStream = socket.getInputStream();
            //将字节流转为成字符流(可以用来读取中文字符)这里io中的适配器模式,用InputStreamReader作为适配器,将字节流转为字符流
            //io中两个重要的适配器(Adapter):InputStreamReader和 OutputStreamWriter
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
            //将字符输入流转为字符缓冲输入流(提供一个读取一行的方法)
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            //读取浏览器发送过来的一行请求信息
            String content = bufferedReader.readLine();
            System.out.println("浏览器发送过来的一行请求信息:" + content);
            System.out.println("开始解析请求");
            parseContent(content);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    //4、解析请求
    private void parseContent(String content) {
        //如果是浏览器传进来的,参数将会是这样:GET /index?username=hsc&pass=17 HTTP/1.1
        if (content.contains("HTTP/1.1")) {
            //如果包含协议说明是浏览器发送过来的请求
            String[] browerContent = content.split(" ");
            if (browerContent.length > 0) {
                // /index?username=hsc&pass=17这一部分内容
                // 因为真正的浏览器中带着  / 需要将截取掉 ====> index?username=hsc&pass=17
                content = browerContent[1].substring(1);
            }
        }
        //否则则是我们自己写的客户端的请求:
        // 传入进来的参数: content?key=value&key=value
        //获取content值
        String requestName;
        //获取key=value 键值对 Map存储
        Map<String, String> paramenterMap = null; //懒加载 如果后面带有在参数再进行创建
        //查找?开始的位置 ---content?key=value
        int index = content.indexOf("?");
        if (index != -1) { //说明是有参数的
            //截取content的内容,也就是请求访问路径
            requestName = content.substring(0, index);
            paramenterMap = new HashMap<>();
            //获取问号后面的所有的键值对key=value&key=value
            String allKeyAndValue = content.substring(index + 1);
            //key=value 根据&符号进行分隔 key=value&key=value
            String[] keyAndValues = allKeyAndValue.split("&");
            for (String keyAndValue : keyAndValues) {
                String[] KV = keyAndValue.split("=");
                //添加key 和value
                paramenterMap.put(KV[0], KV[1]);
            }
        } else {
            requestName = content;
        }
        System.out.println("解析到的请求名称:" + requestName);
        System.out.println("解析参数的信息:" + paramenterMap);
        //这里我们传递的这个两个参数可以使用HttpServletRequest对象进行存储
        HttpServletRequest request = new HttpServletRequest(requestName, paramenterMap);
        HttpServletResponse response = new HttpServletResponse();
        this.findServlet(request, response);
    }

    //5、用请求名字,找寻资源(文件/操作)
    private void findServlet(HttpServletRequest request, HttpServletResponse response) {
        //第一种获取文件流的方式
//        InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("web.properties");
        try {
            //1、获取request对象中请求名
            String requestName = request.getRequestName();
            //2、根据请求名称找到对应的Controller对象,Controller都必须实现HttpServlet 中规定的方法
            HttpServlet servlet = contrllerMap.get(requestName);
            //如果为null
            if (servlet == null) {
                //找到对应的类
                //根据请求名称获取对应的类名称
                String className = MyServerReader.getValue(requestName);
                //4、通过类的名字反射加载类
                Class clazz = Class.forName(className);
                //5、通过clazz创建对象
                Constructor constructor = clazz.getConstructor();
                //向上转型
                servlet = (HttpServlet) constructor.newInstance();
                contrllerMap.put(requestName, servlet);
            }
            //执行servlet
            Class clazz = servlet.getClass();
            //6、通过clazz找寻类中的那个需要执行的方法,如果方法是携带参数的,需要将参数进行传递
            Method method = clazz.getMethod("service", HttpServletRequest.class, HttpServletResponse.class);
            //7、让方法执行
            method.invoke(servlet, request, response);
            //当调用万方法之后,response被填充满信息了
            //填充完信息之后,响应回浏览器
            this.responseToBrowser(response);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 6、资源执行完结果,将结果响应回去给浏览器
     *
     * @param response
     */
    private void responseToBrowser(HttpServletResponse response) {
        String responseContent = response.getResponseContent();
        try {
            //获取同个socket 输出流
            OutputStream outputStream = socket.getOutputStream();
            //转为字符输出流
            PrintWriter writer = new PrintWriter(outputStream);
            //真正响应回浏览器的位置
            writer.println(responseContent);
            writer.flush();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }
}

服务器读取文件对象:MyServerReader
package com.server;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

//负责服务器启动的时候,一次性读取配置文件的信息,这个对象不需要重复的创建
public class MyServerReader {
    //用来存储文件的信息
    private static Map<String, String> map = new HashMap();

    //类启动的时候优先加载,并且只加载一次
    static {
        try {
            //这里进行了优化:配置文件只需要加载一次就够了,不用每次启动的时候都创建一个读取文件的对象,让其变成一个单例的对象
            //读取配置文件,通过请求名得到真实的类全名 (读取流文件)
            //通过prop集合里面读取出来的文件记录,找寻类全名
            Properties properties = new Properties();
            InputStream inputStream = new FileInputStream("src/web.properties");
            properties.load(inputStream);
            Enumeration<?> enumeration = properties.propertyNames(); //获取迭代器信息
            if (enumeration.hasMoreElements()) {
                String key = (String) enumeration.nextElement();
                String value = properties.getProperty(key);
                map.put(key, value);
            }
            System.out.println("配置文件web.properties文件夹读取完毕");
            inputStream.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    //提供一个获取map集合中的方法
    public static String getValue(String key) {
        return map.get(key);
    }
}

控制层:Controller

package com.controller;

import com.server.HttpServlet;
import com.server.HttpServletRequest;
import com.server.HttpServletResponse;
import com.service.UserService;

/**
 * 用于处理所有的请求
 */
public class IndexController extends HttpServlet {

    public IndexController() {
        System.out.println("IndexController对象创建了。。。");
    }

    //我们service层进行真正的执行业务
    private UserService userService = new UserService();

    /**
     * 如果想要服务器进行管理,需要继承接口,并且实现其方法
     *
     * @param request
     */
    @Override
    public void service(HttpServletRequest request, HttpServletResponse response) {
        System.out.println("test方法执行了。。。。");
        //1、接收请求发送过来的参数(携带的参数)
        String username = request.getParameter("username");
        String pass = request.getParameter("pass");
        //2、负责找真正的业务层干活
        String result = userService.login(username, pass);
        //执行完业务信息进行响应回
//      response.write(result);
        //如果想要浏览器看到信息,遵序浏览器识别的规则,下面是模拟一些前段的标准响应回去
        response.write("HTTP1.1 200 OK\r\n");
        response.write("Content-Type: text/html;charset=UTF-8\r\n");
        response.write("\r\n");
        response.write("<html>");
        response.write("<body>");
        response.write("<input type='button' value='按钮'>");
        response.write("</body>");
        response.write("</html>");
        System.out.println(result);
    }
}

数据库映射实体:UserDomain

package com.domain;

public class UserDomain {
    private String username;
    private String age;


    public void setUsername(String username) {
        this.username = username;
    }

    public String getUsername() {
        return username;
    }

    public void setAge(String age) {
        this.age = age;
    }

    public String getAge() {
        return age;
    }
}

业务处理层:UserService

package com.service;

import com.dao.UserDao;
import com.domain.UserDomain;

public class UserService {

    private UserDao userDao=new UserDao();
    public UserService(){}

    //做登录的处理
    public String login(String username, String pass) {

        //模拟数据库查询数据
        UserDomain userDomain = userDao.selectOne(username, pass);
        if (userDomain!=null){
            return "登录成功";
        }
        return "登录失败";
    }
}

数据持久层:UserDao

package com.dao;

import com.domain.UserDomain;

public class UserDao {

    public UserDomain selectOne(String username, String pass) {
        UserDomain userDomain=new UserDomain();
        return userDomain;
    }
}

测试:

启动服务端:
image.png
手写客户端的测试:访问localhost:9999/index?username=hsc&pass=17 访问
服务端:
image.png
客户端:
image.png
浏览器端测试: 访问localhost:9999/index?username=hsc&pass=17
服务端:
image.png
客户端响应成功:
image.png

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值