前言:
通过自己手写模拟tomcat的核心功能,能够让我们更好的理解tomcat的运行机制,底层也是通过jdk提供的api进一步步封装实现而来,我们作为使用者而言,可能更多关注使用上的方便、敏捷性,通过手写源码,相信我们会对tomcat有更加深刻的理解。
【版本一】:测试服务端和客户端建立连接
创建一个客户端(模拟浏览器)
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、解析响应信息, 浏览器中展示出来
}
}
启动验证结果:
【版本二】:完整版本
模拟一个客户端(浏览器):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
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;
}
}
测试:
启动服务端:
手写客户端的测试:访问localhost:9999/index?username=hsc&pass=17 访问
服务端:
客户端:
浏览器端测试: 访问localhost:9999/index?username=hsc&pass=17
服务端:
客户端响应成功: