🎄概述
我将模拟实现一个Tomcat
,它的名字叫YellowBabyDuck
(小黄鸭服务器),与Tomcat显眼的区别在于,它俩的logo很不一样
🎄开始
🍭创建一个Maven项目
引入Servlet的依赖
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
🍭异步BIO方式获取并处理Socket连接
🌴版本1.1设计 只能处理一个请求
分析 下面第一个版本的设计❌只能接收一个请求 ❌接收之后服务器程序便终止了
package com.bigbigmeng;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
@author Liu Xianmeng
@createTime 2023/10/4 18:46
@instruction 小黄鸭服务器类
*/
@SuppressWarnings({"all"})
public class YellowBabyDuctServer {
private void start() {
try {
ServerSocket serverSocket = new ServerSocket(9999);
// 只能接收一个请求
Socket accept = serverSocket.accept();
processSocket(accept);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void processSocket(Socket accept) {
// ...
}
public static void main(String[] args) {
YellowBabyDuctServer yellowBabyDuctServer = new YellowBabyDuctServer();
yellowBabyDuctServer.start();
}
}
🌴版本1.2设计 只能同步处理请求
修改
start()
使得服务器可以不间断地接收请求 但还是存在一个重要的问题 那就是整个服务器❌只能处理同步请求 ❌只有上一个程序请求处理完 才能接收处理下一个请求
private void start() {
try {
// 开启一个服务器端口
ServerSocket serverSocket = new ServerSocket(9999);
// 只能接收一个请求
Socket accept = serverSocket.accept();
processSocket(accept);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
🌴版本1.3设计 处理异步请求
新建一个类
SocketProcessor
专门用来处理Socket连接
package com.bigbigmeng;
import java.net.Socket;
/**
@author Liu Xianmeng
@createTime 2023/10/4 19:05
@instruction SocketProcessor类 专门用来处理一个Socket连接
[1] SocketProcessor类本身要是一个可执行的Task 所以这里让它去🟪实现Runnable接口重写run()方法
[2] 既然它用来处理一个Socket连接 那么它就应该持有一个⚡Socket连接
*/
@SuppressWarnings({"all"})
public class SocketProcessor implements Runnable {
// ⚡持有一个Socket连接
private Socket socket;
public SocketProcessor(Socket socket) {
this.socket = socket;
}
@Override
public void run() { // 🟪
processSocket(socket);
}
private void processSocket(Socket socket) {
}
}
修改
YellowBabyDuctServer
类
package com.bigbigmeng;
/**
@author Liu Xianmeng
@createTime 2023/10/4 18:46
@instruction 小黄鸭服务器类
*/
public class YellowBabyDuctServer {
private void start() {
try {
// 开启一个服务器端口
ServerSocket serverSocket = new ServerSocket(9999);
// 创建一个线程池执行器来执行请求 线程池大小为20
ExecutorService executorService = Executors.newFixedThreadPool(20);
while(true) {
// 接收一个socket请求
Socket socket = serverSocket.accept();
// 扔给线程池进行处理
executorService.execute(new SocketProcessor(socket));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
YellowBabyDuctServer yellowBabyDuctServer = new YellowBabyDuctServer();
yellowBabyDuctServer.start();
}
}
❓服务器具体如何处理请求?
看下面
SocketProcessor
类的processSocket()
方法
/**
* 处理传入的Socket
* @param socket
*/
private void processSocket(Socket socket) {
try {
InputStream is = socket.getInputStream();
// 每次读1024B
byte[] bytes = new byte[1024];
is.read(bytes);
// 读完以后打印出来
for(byte b : bytes) {
System.out.print((char)b);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
测试一下 查看后端日志
这样的打印结果还不够清晰 因此我们可以使用BufferedReader来按行进行读取 并打印
private void processSocket(Socket socket) {
try {
InputStream is = socket.getInputStream();
// 修改读取方式 按行读取并打印
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String strLine = null;
while((strLine = br.readLine()) != null) {
System.out.println(strLine);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
执行测试 看到http的请求头被打印出来了 可以看到请求头中是有很多字段可以被服务器进行解析的 例如第一行的 请求方式GET 请求路径/ 请求协议HTTP/1.1
GET / HTTP/1.1
User-Agent: PostmanRuntime/7.33.0
Accept: */*
Cache-Control: no-cache
Postman-Token: 4c57ed99-34b9-46c1-aeb7-966225a8ffee
Host: localhost:9999
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: ticket=ee85af76cc8a40bca2f86c8173a3e787
再用POST的方式携带数据请求 看看会打印怎样的结果
当对以上的所有字段进行解析之后 就可以得到一个Request对象 在传统的JavaWeb的开发里 这个对象就是常用的
HttpServletRequest
紧接着 我们来创建这个Request对象
🌴创建Request对象 封装请求
这个Request对象就是原本Tomcat应该封装的HttpServletRequest对象
🎯可以看到下面的JavaWeb的开发实例场景 这个代码就用到了HttpServletRequest对象 Tomcat会自动将请求中的一系列字段封装都这个对象 并共后端进行使用和操作
/**
@author Liu Xianmeng
@createTime 2022/8/4 10:36
@instruction
*/
@SuppressWarnings({"all"})
@WebServlet("/cart") // 专门用来处理关于购物车的请求的
public class CartCtrller extends BasicCtrller {
private CartServ cs = new CartServImpl();
private CartDao cd = new CartDao(); // 专门用来处理数据库表中的数据的
/**
* 这个函数就实现刚才向所登录的用户的购物车中添加一个产品(所点击添加的那个产品)
*/
public void addOne(🎯HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("C CartCtrller M addOne()...");
// 要获取从请求传递过来的参数 参数有哪些?
String furnName = 🎯req.getParameter("name");
String memberId = 🎯req.getParameter("memberId");
String furnId = 🎯req.getParameter("furnId");
...
}
}
接下来我们来创建小黄鸭服务器的Request对象 这个对象用来封装上面测试控制台打印出来的HTTP报文的所有内容
package com.bigbigmeng;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.concurrent.ConcurrentHashMap;
/**
@author Liu Xianmeng
@createTime 2023/10/4 20:12
@instruction
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Request {
private String method; // 请求方法
private String path; // 请求路径
private String protocol; // 协议
/**
* 每一个socket都应该持有一个Request对象 所以不需要考虑线程安全的问题
* 直接使用Properties来存储其他的属性
*/
private Properties properties = new Properties();
// 使用这个方法来返回属性
public String getParamater(String key) {
return properties.getProperty(key);
}
}
上面代码使用了lombok注解 因为我引入了lombok
<!-- 20231004 引入lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
创建好Request对象后 就可以将请求的参数封装起来了 下面是封装过程
/**
* 处理传入的Socket
*
* @param socket
*/
private void processSocket(Socket socket) {
try {
InputStream is = socket.getInputStream();
// 使用BufferedReader包装流 它能提供更强的读取功能
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 创建一个Request对象 用来封装所有请求参数
Request request = new Request();
// 封装请求参数
// 定义一个变量来接收br读取到的行字符串
String curLine = null;
try {
// 先读取第一行来获取 请求方式|路径|协议
curLine = br.readLine();
if(curLine == null) return;
// 先封装 请求方式|路径|协议
String[] firstLineThreeStr = curLine.split(" ");
request.setMethod(firstLineThreeStr[0]);
request.setPath(firstLineThreeStr[1]);
request.setProtocol(firstLineThreeStr[2]);
// 继续封装剩下的参数
while((curLine = br.readLine()) != null) {
String[] twoPartStr = curLine.split("\\: ");
if(twoPartStr[0].equals("Cookie")) {
// 关于Cookie的封装可以放到后面再进行
} else {
// 封装其他的请求头
if(twoPartStr.length > 1) {
request.getProperties().setProperty(twoPartStr[0], twoPartStr[1]);
}
}
}
// 判断请求是否携带数据
if(request.getMethod().equals("POST")) {
// 封装数据 后面再加处理逻辑
}
System.out.println("C SocketProcessor M processSocket() -> request = " + request);
BigBigMengServlet bigBigMengServlet = new BigBigMengServlet();
// 调用service方法 -> 根据请求参数再决定调用GET还是POST方法
// 传入刚刚写的Request和Response对象
bigBigMengServlet.doGet(request, response);
// 执行完Servlet之后就可以发送响应了
response.complete();
socket.close(); // 关闭socket连接
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
/*
try {
br.close();
socket.close();
System.out.println("C SocketProcessor M processSocket() -> 连接关闭");
} catch (IOException e) {
throw new RuntimeException(e);
}*/
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
请求测试结果如下 Request对象将请求的参数封装起来了
🍭按照Servlet规范实现Request和Response
实现Request
先写一个抽象类AbstractHttpServletRequest实现HttpServletRequest接口
package com.bigbigmeng;
/**
@author Liu Xianmeng
@createTime 2023/10/4 21:46
@instruction
*/
public class AbstractHttpServletRequest implements HttpServletRequest {
@Override
public String getAuthType() {
return null;
}
@Override
public Cookie[] getCookies() {
return new Cookie[0];
}
...
}
然后让刚刚写的Request类继承AbstractHttpServletRequest
package com.bigbigmeng;
/**
@author Liu Xianmeng
@createTime 2023/10/4 20:12
@instruction
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Request extends AbstractHttpServletRequest {
private String method; // 请求方法
private String requestUrl; // 请求路径
private String protocol; // 协议
/**
* 每一个socket都应该持有一个Request对象 所以不需要考虑线程安全的问题
* 直接使用Properties来存储其他的属性
*/
private Properties properties = new Properties();
// 使用这个方法来返回属性
@Override
public String getParamater(String key) {
return properties.getProperty(key);
}
@Override
public String getMethod() {
return method;
}
@Override
public String getProtocol() {
return method;
}
@Override
public String getRequestURI() {
return requestUrl;
}
...
}
实现Response
先写一个抽象类AbstractHttpServletResponse实现HttpServletResponse接口
package com.bigbigmeng;
/**
@author Liu Xianmeng
@createTime 2023/10/5 9:18
@instruction
*/
public class AbstractHttpServletResponse implements HttpServletResponse {
@Override
public void addCookie(Cookie cookie) {
}
@Override
public boolean containsHeader(String name) {
return false;
}
...
}
然后再写一个Response类继承AbstractHttpServletResponse
package com.bigbigmeng;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
/**
@author Liu Xianmeng
@createTime 2023/10/5 9:18
@instruction
*/
@SuppressWarnings({"all"})
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Response extends AbstractHttpServletResponse {
private int status = 200; // 状态码
private String message = "OK"; // 返回消息
private Map<String, String> headers = new HashMap<>();
private Request request; // 一个Response对应一个Request对象
private OutputStream socketOutputStream; // Response要用outputStream返回内容
// 响应体的outputStream 暂存响应数据
private ResponseServletOutputStream responseServletOutputStream = new ResponseServletOutputStream();
private byte SP = ' ';
private byte CR = '\r'; // 换行
private byte LF = '\b';
/**
* 完成最后的响应
*/
public void complete() throws IOException {
sendResponseLine();
sendResponseHeader();
sendResponseBody();
}
// 写响应行 响应的第一行
private void sendResponseLine() throws IOException {
String nextLine = "HTTP/1.1 " + status + " " + message;
socketOutputStream.write(nextLine.getBytes());
socketOutputStream.write('\r');
socketOutputStream.write('\n');
}
// 写响应头
private void sendResponseHeader() throws IOException {
for(String key : headers.keySet()) {
socketOutputStream.write((key + ": " + headers.get(key)).getBytes());
// “\r\n”是一个换行
socketOutputStream.write('\r');
socketOutputStream.write('\n');
}
socketOutputStream.write('\r');
socketOutputStream.write('\n');
}
// 写返回的数据
private void sendResponseBody() throws IOException {
// 获取数据
int count = responseServletOutputStream.getPos();
byte[] tempData = responseServletOutputStream.getBytes();
byte[] data = new byte[count];
for(int i = 0; i < count; ++i) {
data[i] = tempData[i];
}
// 写出数据
socketOutputStream.write(data);
}
@Override
public void addHeader(String name, String value) {
this.headers.put(name, value);
}
@Override
public ResponseServletOutputStream getOutputStream() throws IOException {
return responseServletOutputStream;
}
}
编写一个测试Servlet ->
BigBigMengServlet
package com.bigbigmeng;
/**
@author Liu Xianmeng
@createTime 2023/10/5 9:36
@instruction
*/
public class BigBigMengServlet extends HttpServlet {
/********** 重写三个处理请求的方法 ********/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("C BigBigMengServlet M doGet() -> method = " + req.getMethod());
// 🎯🎯🎯写回返回的数据
resp.getOutputStream().write("C BigBigMengServlet M doGet()".getBytes());
resp.addHeader("Content-Length", "C BigBigMengServlet M doGet()".length() + "");
resp.addHeader("Content-Type", "text/plain;charset=utf-8");
System.out.println("断定调试...");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
super.service(req, res);
}
}
修改SocketProcessor类的
processSocket
方法
...
// 判断请求是否携带数据
if(request.getMethod().equals("POST")) {
// 封装数据 后面再加处理逻辑
}
logger.info("request = " + request);
BigBigMengServlet bigBigMengServlet = new BigBigMengServlet();
// 调用service方法 -> 根据请求参数再决定调用GET还是POST方法
// 传入刚刚写的Request和Response对象
bigBigMengServlet.service(request, response);
...
执行测试 后面的报错空指针是因为自己写的Response类没有重写
getOutputStream()
方法
🍭暂存响应体
新建一个ResponseServletOutputStream类
package com.bigbigmeng;
/**
@author Liu Xianmeng
@createTime 2023/10/5 10:25
@instruction
*/
@Data
public class ResponseServletOutputStream extends ServletOutputStream {
private byte[] bytes = new byte[2048]; // 暂存响应数据
private int pos = 0;
@Override
public void write(int b) throws IOException {
bytes[pos++] = (byte) b;
}
}
Response
类添加@Override方法
@Override
public ResponseServletOutputStream getOutputStream() throws IOException {
return responseServletOutputStream;
}
🍭按照HTTP协议发送数据
修改Request类 添加socket属性
public class Request extends AbstractHttpServletRequest {
...
private Socket socket; // 一个请求持有一个socket
}
修改
SocketProcessor
类的processSocket
方法 创建Request对象的时候setSocket
// 创建一个Request对象 用来封装所有请求参数
Request request = new Request();
request.setSocket(socket);
修改
SocketProcessor
类的processSocket
方法 创建Response对象的时候setRequest
...
Response response = new Response();
response.setRequest(request);
response.setOutputStream(request.getSocket().getOutputStream());
...
⚡阶段测试
服务端持续监听
Postman的请求结果
🍭Tomcat部署应用实现
🌴补全小黄鸭服务器的目录
🌴部署Servlet 阶段1 获取Servlet的Class对象
添加@WebServlet(urlPatterns = {"/test"})注解 并重新编译
package com.bigbigmeng;
/**
@author Liu Xianmeng
@createTime 2023/10/5 9:36
@instruction
*/
@WebServlet(urlPatterns = {"/test"})
public class BigBigMengServlet extends HttpServlet {
...
}
编译后将其放到
webapps/app/classes/com/bigbigmeng
目录下
修改项目访问Servlet的方式 注销
SocketProcessor
类中对BigBigMengServlet
的调用
主类YellowBabyDuckServer编写🎯deployApps()方法 用于部署项目
package com.bigbigmeng;
/**
@author Liu Xianmeng
@createTime 2023/10/4 18:46
@instruction 小黄鸭服务器类
*/
public class YellowBabyDuctServer {
...
public static void main(String[] args) {
YellowBabyDuctServer yellowBabyDuctServer = new YellowBabyDuctServer();
deployApps();
yellowBabyDuctServer.start();
}
// 🎯部署项目
private static void deployApps() {
File file = new File(System.getProperty("user.dir"), "webapps");
for (String app : file.list()) {
System.out.println(app);
}
}
}
打印查看
新建方法
deployApp()
/**
* 在这个方法中 服务器需要获取到所有的Servlet并登记其路径
* 以便在调用Servlet的时候直接根据请求路径进行get
* @param webapps
* @param appName
*/
private static void deployApp(File webapps, String appName) {
// 指定父文件夹和子文件名可以获取子文件夹
File appDirectory = new File(webapps, appName);
File classDirectory = new File(appDirectory, "classes");
List<File> allFiles = new ArrayList<>();
getAllFiles(allFiles, classDirectory);
// 判断哪些文件是.class文件
for (File file : allFiles) {
String path = file.getPath();
// 把父目录的路径删除
path = path.replace(classDirectory.getPath() + "\\", "");
// 删除文件后缀名
path = path.replace(".class", "");
// 把路径的/替换为.
path = path.replace("\\", ".");
// 把全类名打印出来
System.out.println("C YellowBabyDuctServer M deployApp() -> 当前全类名 = " + path);
// 使用反射判断当前的class对象是不是Servlet
try {
// 创建classLoader对象
ServerClassLoader classLoader = new ServerClassLoader(new URL[]{classDirectory.toURL()});
// 加载当前全类名对应的java对象
Class<?> aClass = classLoader.loadClass(path);
// 判断是不是一个Servlet
if(HttpServlet.class.isAssignableFrom(aClass)) {
// 如果是HttpServlet的子类 则它是一个Servlet
System.out.println("C YellowBabyDuctServer M deployApp() -> class对象 = " + aClass);
System.out.println("...");
}
} catch (MalformedURLException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
// 获取某个文件夹下的所有文件
private static void getAllFiles(List<File> list, File classDirectory) {
for (File file : classDirectory.listFiles()) {
if(!file.isFile()) {
getAllFiles(list, file);
}
else list.add(file);
}
}
⚡阶段测试
🌴部署Servlet 阶段2 完成url和Servlet的映射
新建Context类
package com.bigbigmeng;
/**
@author Liu Xianmeng
@createTime 2023/10/5 17:24
@instruction 上下文类 服务器部署App 一个App对应一个Context
*/
@Data
public class Context {
// app的名字
private String appName;
// app的url和Servlet之间的额映射关系
private Map<String, Servlet> urlMapping = new HashMap<>();
public void addServletUrlMapping(String url, Servlet servlet) {
urlMapping.put(url, servlet);
}
}
给小黄鸭服务器添加一个🎯新的属性
/**
@author Liu Xianmeng
@createTime 2023/10/4 18:46
@instruction 小黄鸭服务器类
*/
public class YellowBabyDuctServer {
// app和上下文的映射map
private static HashMap<String, Context> 🎯contextMap = new HashMap<>();
public static HashMap<String, Context> getContextMap() {
return contextMap;
}
...
}
完成url和Servlet的映射
/**
* 在这个方法中 服务器需要获取到所有的Servlet并登记其路径
* 以便在调用Servlet的时候直接根据请求路径进行get
* @param webapps
* @param appName
*/
private static void deployApp(File webapps, String appName) {
// 创建一个上下文对象 一个应用对应一个上下文对象
Context context = new Context();
context.setAppName(appName);
...
// 判断哪些文件是.class文件
for (File file : allFiles) {
...
try {
...
if(HttpServlet.class.isAssignableFrom(servletClazz)) {
...
// 如果这个class对象被@WebService注解修饰 则获取映射路径
if(servletClazz.isAnnotationPresent(WebServlet.class)) {
WebServlet annotation = servletClazz.getAnnotation(WebServlet.class);
String[] urlPatterns = annotation.urlPatterns();
// 创建一个Servlet对象
Servlet servlet = (Servlet) servletClazz.newInstance();
// 添加映射关系
for (String urlPattern : urlPatterns) {
context.addServletUrlMapping(urlPattern, servlet);
}
}
}
} catch (Exception e) {
...
}
}
// 处理完之后给服务器添加context的映射
contextMap.put(appName, context);
}
修改
SocketProcessor
类 🎯完成根据请求路径映射Servlet这个地方我将之前的
processSocket()
函数弃用 直接把Runnable的任务写在run()方法里面
package com.bigbigmeng;
/**
@author Liu Xianmeng
@createTime 2023/10/4 19:05
@instruction SocketProcessor类 专门用来处理一个Socket连接
*/
public class SocketProcessor implements Runnable {
...
@Override
public void run() { // 🟪
try {
...
// 判断请求是否携带数据
if(request.getMethod().equals("POST")) {
// 封装数据 后面再加处理逻辑
}
System.out.println("C SocketProcessor M processSocket() -> request = " + request);
/**
* 🎯调用service方法 -> 根据请求参数再决定调用GET还是POST方法
* 传入刚刚写的Request和Response对象
*/
//❌BigBigMengServlet bigBigMengServlet = new BigBigMengServlet();
//❌bigBigMengServlet.doGet(request, response);
/✅*********** 20231005 重新处理url和Servlet调用的映射 开始 **********/
// 获取请求路径 /app/test /app映射到应用 后面的/text映射到应用下的servelet
String originUrl = request.getRequestUrl();
// 使用StringBuilder对象进行处理
StringBuilder sb = new StringBuilder(originUrl);
// 删除第一个/
sb.delete(0,1);
// 获取app的名字
String appName = sb.substring(0, sb.indexOf("/"));
// 删除app的名字 -> /app
sb.delete(0, sb.indexOf("/"));
// 获取请求路径 -> 对应Servlet
Context context = YellowBabyDuctServer.getContextMap().get(appName);
// 根据originUrl请求路径获取 -> 对应Servlet
Servlet servlet = context.getUrlMapping().get(sb.toString());
// 执行目标Servlet
servlet.service(request, response);
/✅*********** 20231005 重新处理url和Servlet调用的映射 结束 **********/
// 执行完Servlet之后就可以发送响应了
response.complete();
socket.close(); // 关闭socket连接
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
...
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 处理传入的Socket
* @param socket
*/
private void processSocket(Socket socket) {
// 清空弃用这个函数
}
}
⚡阶段测试
成功响应
到此 一个简易功能的YellowBabyDuck服务器(模拟实现Tomcat)✨就实现了~ 如果想让其支持更加复杂的功能 后续可以进行补充