手写简易版tomcat
程序员文章站
2024-03-22 23:53:22
...
功能
- Minicat要做的事情:作为一个服务器软件提供服务的,也即我们可以通过浏览器客户端发送http请求, Minicat可以接收到请求进行处理,处理之后的结果可以返回浏览器客户端。
目标
- 提供服务,接收请求(Socket通信)
- 请求信息封装成Request对象(Response对象)
- 客户端请求资源,资源分为静态资源(html)和动态资源(Servlet)
- 资源返回给客户端浏览器
改进
- V1.0需求:浏览器请求http://localhost:8080,返回一个固定的字符串到⻚面"Hello Minicat!"
- V2.0需求:封装Request和Response对象,返回html静态资源文件
- V3.0需求:可以请求动态资源(Servlet)
V1.0 返回一个固定的字符串到⻚面"Hello Minicat!"
启动类,程序入口
public class Bootstrap {
/**
* 定义socket监听的端口号
*/
private int port = 8080;
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
/**
* 启动方法
*
* @throws IOException
*/
private void start() throws IOException {
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("=====>>>Minicat start on port:" + port);
/**
* 完成Minicat 1.0版本
* 需求:浏览器请求http://localhost:8080,返回一个固定的字符串到页面"Hello Minicat!"
*/
while (true) {
Socket socket = serverSocket.accept();
// 有了socket,接收到请求,获取输出流
OutputStream outputStream = socket.getOutputStream();
String data = "Hello Minicat!";
String responseText = HttpProtocolUtil.getHttpHeader200(data.getBytes().length) + data;
outputStream.write(responseText.getBytes());
socket.close();
}
}
/**
* Minicat 的程序启动入口
*
* @param args
*/
public static void main(String[] args) {
Bootstrap bootstrap = new Bootstrap();
try {
// 启动Minicat
bootstrap.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
工具类
/**
* @Description http协议工具类,主要是提供响应头信息,这里我们只提供200和404的情况
* @Author rpp
* @Date 2020/7/18 2:59 下午
*/
public class HttpProtocolUtil {
/**
* 为响应码200提供请求头信息
*
* @return
*/
public static String getHttpHeader200(long contentLength) {
return "HTTP/1.1 200 OK \n" +
"Content-Type: text/html \n" +
"Content-Length: " + contentLength + " \n" +
"\r\n";
}
/**
* 为响应码404提供请求头信息(此处也包含了数据内容)
*
* @return
*/
public static String getHttpHeader404() {
String str404 = "<h1>404 not found</h1>";
return "HTTP/1.1 404 NOT Found \n" +
"Content-Type: text/html \n" +
"Content-Length: " + str404.getBytes().length + " \n" +
"\r\n" + str404;
}
}
效果验证:启动后浏览器输入http://localhost:8080/
V2.0 封装Request和Response对象,返回html静态资源文件
- 封装request,根据输入字节流解析出请求方式及url,代码如下
/**
* 把请求信息封装为Request对象(根据InputSteam输入流封装)
*/
public class Request {
/**
* 请求方式,比如GET/POST
*/
private String method;
/**
* 例如url /,/index.html
*/
private String url;
/**
* 输入流,其他属性从输入流中解析出来
*/
private InputStream inputStream;
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public InputStream getInputStream() {
return inputStream;
}
public void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
public Request() {
}
/**
* 构造器,输入流传入
*
* @param inputStream
* @throws IOException
*/
public Request(InputStream inputStream) throws IOException {
this.inputStream = inputStream;
// 从输入流中获取请求信息
int count = 0;
while (count == 0) {
count = inputStream.available();
}
byte[] bytes = new byte[count];
inputStream.read(bytes);
String inputStr = new String(bytes);
// 获取第一行请求头信息
// GET / HTTP/1.1
String firstLineStr = inputStr.split("\\n")[0];
String[] strings = firstLineStr.split(" ");
this.method = strings[0];
this.url = strings[1];
System.out.println("=====>>method:" + method);
System.out.println("=====>>url:" + url);
}
}
- 使用输出流输出静态资源文件,可根据url找出静态资源的绝对路径,然后读取并输出
- 根据ur解析读取静态资源文件工具类
public class StaticResourceUtil {
/**
* 获取静态资源文件的绝对路径
*
* @param path
* @return
*/
public static String getAbsolutePath(String path) {
String absolutePath = StaticResourceUtil.class.getResource("/").getPath();
return absolutePath.replaceAll("\\\\", "/") + path;
}
/**
* 读取静态资源文件输入流,通过输出流输出
*/
public static void outputStaticResource(InputStream inputStream, OutputStream outputStream) throws IOException {
int count = 0;
while (count == 0) {
count = inputStream.available();
}
int resourceSize = count;
// 输出http请求头,然后再输出具体内容
outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes());
// 读取内容输出
// 已经读取的内容长度
long written = 0;
// 计划每次缓冲的长度
int byteSize = 1024;
byte[] bytes = new byte[byteSize];
while (written < resourceSize) {
// 说明剩余未读取大小不足一个1024长度,那就按真实长度处理
if (written + byteSize > resourceSize) {
// 剩余的文件内容长度
byteSize = (int) (resourceSize - written);
bytes = new byte[byteSize];
}
inputStream.read(bytes);
outputStream.write(bytes);
outputStream.flush();
written += byteSize;
}
}
}
- response
/**
* 封装Response对象,需要依赖于OutputStream
* <p>
* 该对象需要提供核心方法,输出html
*/
public class Response {
private OutputStream outputStream;
public Response() {
}
public Response(OutputStream outputStream) {
this.outputStream = outputStream;
}
/**
* 使用输出流输出指定字符串
*
* @param content
* @throws IOException
*/
public void output(String content) throws IOException {
outputStream.write(content.getBytes());
}
/**
* @param path url,随后要根据url来获取到静态资源的绝对路径,进一步根据绝对路径读取该静态资源文件,最终通过输出流输出
*/
public void outputHtml(String path) throws IOException {
// 获取静态资源文件的绝对路径
String absoluteResourcePath = StaticResourceUtil.getAbsolutePath(path);
// 输入静态资源文件
File file = new File(absoluteResourcePath);
if (file.exists() && file.isFile()) {
// 读取静态资源文件,输出静态资源
StaticResourceUtil.outputStaticResource(new FileInputStream(file), outputStream);
} else {
// 输出404
output(HttpProtocolUtil.getHttpHeader404());
}
}
}
- 启动方法修改
/**
* 启动方法
*
* @throws IOException
*/
private void start() throws IOException {
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("=====>>>Minicat start on port:" + port);
/**
* 完成Minicat 2.0版本
* 需求:封装Request和Response对象,返回html静态资源文件
*/
while(true) {
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
// 封装Request对象和Response对象
Request request = new Request(inputStream);
Response response = new Response(socket.getOutputStream());
response.outputHtml(request.getUrl());
socket.close();
}
}
- resource目录下创建静态资源文件index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>static resouce</title>
</head>
<body>
Hello Minicat-static resouce!
</body>
</html>
- 验证结果
V3.0 可以请求动态资源(Servlet)
- 自定义Servlet接口及实现类
public interface Servlet {
void init() throws Exception;
void destory() throws Exception;
void service(Request request,Response response) throws Exception;
}
public abstract class HttpServlet implements Servlet {
public abstract void doGet(Request request, Response response);
public abstract void doPost(Request request, Response response);
@Override
public void service(Request request, Response response) throws Exception {
if ("GET".equalsIgnoreCase(request.getMethod())) {
doGet(request, response);
} else {
doPost(request, response);
}
}
}
public class MyServlet extends HttpServlet {
@Override
public void doGet(Request request, Response response) {
String content = "<h1>MyServlet get</h1>";
try {
response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void doPost(Request request, Response response) {
String content = "<h1>MyServlet post</h1>";
try {
response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void init() throws Exception {
}
@Override
public void destory() throws Exception {
}
}
- 在resources目录下创建配置文件web.xml
<?xml version="1.0" encoding="UTF-8" ?>
<web-app>
<servlet>
<servlet-name>rpp</servlet-name>
<servlet-class>com.rpp.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>rpp</servlet-name>
<url-pattern>/rpp</url-pattern>
</servlet-mapping>
</web-app>
- 修改启动类,启动时加载配置文件web.xml,然后根据请求动态调用方法
public class Bootstrap {
/**
* 定义socket监听的端口号
*/
private int port = 8080;
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
/**
* 启动方法
*
* @throws IOException
*/
private void start() throws Exception {
// 加载解析相关的配置,web.xml
loadServlet();
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("=====>>>Minicat start on port:" + port);
/**
* 完成Minicat 3.0版本
* 需求:可以请求动态资源(Servlet)
*/
while (true) {
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
// 封装Request对象和Response对象
Request request = new Request(inputStream);
Response response = new Response(socket.getOutputStream());
// 静态资源处理
if (servletMap.get(request.getUrl()) == null) {
response.outputHtml(request.getUrl());
} else {
// 动态资源servlet请求
HttpServlet httpServlet = servletMap.get(request.getUrl());
httpServlet.service(request, response);
}
socket.close();
}
}
private Map<String, HttpServlet> servletMap = new HashMap<String, HttpServlet>();
/**
* 加载解析web.xml,初始化Servlet
*/
private void loadServlet() {
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("web.xml");
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(resourceAsStream);
Element rootElement = document.getRootElement();
List<Element> selectNodes = rootElement.selectNodes("//servlet");
for (int i = 0; i < selectNodes.size(); i++) {
Element element = selectNodes.get(i);
// <servlet-name>rpp</servlet-name>
Element servletnameElement = (Element) element.selectSingleNode("servlet-name");
String servletName = servletnameElement.getStringValue();
// <servlet-class>com.rpp.MyServlet</servlet-class>
Element servletclassElement = (Element) element.selectSingleNode("servlet-class");
String servletClass = servletclassElement.getStringValue();
// 根据servlet-name的值找到url-pattern
Element servletMapping = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
// /rpp
String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Minicat 的程序启动入口
*
* @param args
*/
public static void main(String[] args) {
Bootstrap bootstrap = new Bootstrap();
try {
// 启动Minicat
bootstrap.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 测试
优化改造1 ,使用多线程
- 上面的方式在多个请求连接时,如果第一个请求阻塞了,后面的请求都不会进来,因为当前处理请求就一个线程
- 优化思路:使用多线程,单独封装一个类处理请求,继承Thread,代码如下
1. RequestProcessor
public class RequestProcessor extends Thread {
private Socket socket;
private Map<String,HttpServlet> servletMap;
public RequestProcessor(Socket socket, Map<String, HttpServlet> servletMap) {
this.socket = socket;
this.servletMap = servletMap;
}
@Override
public void run() {
try{
InputStream inputStream = socket.getInputStream();
// 封装Request对象和Response对象
Request request = new Request(inputStream);
Response response = new Response(socket.getOutputStream());
// 静态资源处理
if(servletMap.get(request.getUrl()) == null) {
response.outputHtml(request.getUrl());
}else{
// 动态资源servlet请求
HttpServlet httpServlet = servletMap.get(request.getUrl());
httpServlet.service(request,response);
}
socket.close();
}catch (Exception e) {
e.printStackTrace();
}
}
}
- 修改启动方法
/**
* 启动方法
*
* @throws IOException
*/
private void start() throws Exception {
// 加载解析相关的配置,web.xml
loadServlet();
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("=====>>>Minicat start on port:" + port);
/**
* 多线程改造(不使用线程池)
*/
while (true) {
Socket socket = serverSocket.accept();
RequestProcessor requestProcessor = new RequestProcessor(socket, servletMap);
requestProcessor.start();
}
}
- 在MyServlet中的get方法中增加线程休眠时间,用于验证
public void doGet(Request request, Response response) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String content = "<h1>MyServlet get</h1>";
try {
response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
} catch (IOException e) {
e.printStackTrace();
}
}
- 验证结果
请求http://localhost:8080/rpp,线程一直阻塞的同时,然后请求http://localhost:8080/index.html,可以正常返回结果
优化改造2 ,使用线程池
public class Bootstrap {
/**
* 定义socket监听的端口号
*/
private int port = 8080;
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
/**
* 定义一个线程池
*/
private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
10,
50,
100L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(50),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
/**
* 启动方法
*
* @throws IOException
*/
private void start() throws Exception {
// 加载解析相关的配置,web.xml
loadServlet();
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("=====>>>Minicat start on port:" + port);
/**
* 多线程改造(使用线程池)
*/
while (true) {
Socket socket = serverSocket.accept();
RequestProcessor requestProcessor = new RequestProcessor(socket, servletMap);
threadPoolExecutor.execute(requestProcessor);
}
}
private Map<String, HttpServlet> servletMap = new HashMap<String, HttpServlet>();
/**
* 加载解析web.xml,初始化Servlet
*/
private void loadServlet() {
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("web.xml");
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(resourceAsStream);
Element rootElement = document.getRootElement();
List<Element> selectNodes = rootElement.selectNodes("//servlet");
for (int i = 0; i < selectNodes.size(); i++) {
Element element = selectNodes.get(i);
// <servlet-name>rpp</servlet-name>
Element servletnameElement = (Element) element.selectSingleNode("servlet-name");
String servletName = servletnameElement.getStringValue();
// <servlet-class>com.rpp.MyServlet</servlet-class>
Element servletclassElement = (Element) element.selectSingleNode("servlet-class");
String servletClass = servletclassElement.getStringValue();
// 根据servlet-name的值找到url-pattern
Element servletMapping = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
// /rpp
String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Minicat 的程序启动入口
*
* @param args
*/
public static void main(String[] args) {
Bootstrap bootstrap = new Bootstrap();
try {
// 启动Minicat
bootstrap.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
推荐阅读
-
手写简易版tomcat
-
自己动手写RecyclerView的下拉刷新
-
《自己动手写云盘》 – 数据库设计 博客分类: 云计算大数据 自己动手写云盘大数据云计算HadoopHBase
-
Cognos安装与配置(使用自带的数据库derby+服务器Tomcat) 博客分类: WEB DerbyTomcatEXTIEJDK
-
我们让tomcat不扫描指定的jar包 博客分类: Tomcat7集群
-
Howto: 如何让tomcat bundle liferay使用不同的数据库 博客分类: 定置 TomcatOracle脚本HSQLDBJDBC
-
【tomcat】常见问题 博客分类: 问题解决 tomcatservletweb
-
【tomcat】常见问题 博客分类: 问题解决 tomcatservletweb
-
同一个Tomcat不同Web应用之间共享Session 博客分类: Tomcat
-
tomcat部署的问题困扰我很长时间 博客分类: java java