Minicat要做的事情:作为⼀个服务器软件提供服务的,也即我们可以通过浏览器客户端发送http请求,Minicat可以接收到请求进行处理,处理之后的结果可以返回浏览器客户端。
整体思路:
(1)提供服务,接收请求(Socket通信)
(2)请求信息封装成Request对象(Response对象)
(3)客户端请求资源,资源分为静态资源(html)和动态资源(Servlet)
(4)资源返回给客户端浏览器
我们递进式完成以上需求,提出V1.0、V2.0、V3.0版本的需求:
需求说明:浏览器请求http://localhost:8080,返回⼀个固定的字符串到页面"Hello Minicat!"
1、创建一个Maven项目Minicat
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.cydgroupId>
<artifactId>MinicatartifactId>
<version>1.0-SNAPSHOTversion>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.1version>
<configuration>
<source>1.8source>
<target>1.8target>
<encoding>utf-8encoding>
configuration>
plugin>
plugins>
build>
project>
3、迷你版Tomcat入口(Java类的main函数)
【在分析Tomcat源码时,也能发现Tomcat的入口也是一个main函数】
public class Bootstrap {
private int port = 8080;
/**
* Minicat启动需要初始化展开的一些操作
*
* 完成Minicat 1.0版本
* 需求:浏览器请求http://localhost:8080,返回一个固定的字符串到页面"Hello Minicat!"
*/
private void start() throws IOException {
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("=====>>>Minicat start on port:" + port);
while (true) {
// 有了socket,接收到请求,获取输出流
Socket socket = serverSocket.accept();
OutputStream outputStream = socket.getOutputStream();
outputStream.write("Hello Minicat!".getBytes());
socket.close();
}
}
public static void main(String[] args) {
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行main函数,启动服务器,浏览器输入 http://localhost:8080 进行方法,发现浏览器出现了ERR_INVALID_HTTP_RESPONSE
,这是因为浏览器发送的是HTTP请求,所以请求消息和应答消息都必须遵循HTTP规范(请求头+请求体/应答头+应答体),我们这边直接往socket输出流中输出的字符串是不符合HTTP规范的。
那么我们应该输出什么样的数据呢?可以打开一个浏览器的控制台,访问www.baidu.com
从图中我们可以看出,向浏览器输出内容,需要先由Response Headers(应答头),该应答头首行必须是HTTP/1.1 200 OK,然后再加上一些key:value格式的数据。下面我们对输出信息进行改造,代码如下:
Bootstrap
public class Bootstrap {
private int port = 8080;
/**
* Minicat启动需要初始化展开的一些操作
*
* 完成Minicat 1.0版本
* 需求:浏览器请求http://localhost:8080,返回一个固定的字符串到页面"Hello Minicat!"
*/
private void start() throws IOException {
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("=====>>>Minicat start on port:" + port);
while (true) {
// 有了socket,接收到请求,获取输出流
Socket socket = serverSocket.accept();
OutputStream outputStream = socket.getOutputStream();
String contentText = "Hello Minicat!";
String text = HttpProtocolUtil.getHeader200(contentText.length()) + contentText;
outputStream.write(text.getBytes());
socket.close();
}
}
public static void main(String[] args) {
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
HttpProtocolUtil
/**
* http协议工具类,主要是提供响应头信息,这里我们只提供200和404的情况
*/
public class HttpProtocolUtil {
/**
* 为响应码200提供请求头信息(这里只输出几个必要的信息)
*/
public static String getHeader200(int contentLength) {
return "HTTP/1.1 200 OK" + "\n" +
"Content-Type: text/html" + "\n" +
"Content-Length: " + contentLength + "\n" +
"\r\n";
}
/**
* 为响应码404提供请求头信息(此处也包含了数据内容)
*/
public static String getHeader404() {
String str404 = "404 not found
";
return "HTTP/1.1 404 NOT Found" + "\n" +
"Content-Type: text/html" + "\n" +
"Content-Length: " + str404.length() + "\n" +
"\r\n" + str404;
}
}
V2.0需求:封装Request和Response对象,返回html静态资源文件
Request
public class Request {
/**
* 请求方式
*/
private String method;
/**
* 请求url
*/
private String url;
public String getMethod() {
return method;
}
public void setMethod(String method) {
method = method;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Request() {
}
/**
* 构造器,传入输入流
*
* 获取请求头信息,封装请求方式和请求url
*
* @param inputStream
* @throws IOException
*/
public Request(InputStream inputStream) throws IOException {
// 从输入流中获取请求信息
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(" ");
method = strings[0];
url = strings[1];
System.out.println("=====>>method:" + method);
System.out.println("=====>>url:" + url);
}
}
Response
public class Response {
private OutputStream outputStream;
public OutputStream getOutputStream() {
return outputStream;
}
public void setOutputStream(OutputStream outputStream) {
this.outputStream = outputStream;
}
public Response() {
}
public Response(OutputStream outputStream) {
this.outputStream = outputStream;
}
/**
* @param url url,随后要根据url来获取到静态资源的绝对路径,进一步根据绝对路径读取该静态资源文件,最终通过输出流输出
* /-----> classes
*/
public void outPutHtml(String url) throws IOException {
// 获取静态资源文件的绝对路径
String absolutePath = StaticResourceUtil.getAbsolutePath(url);
// 输入静态资源文件
File file = new File(absolutePath);
if (file.exists() && file.isFile()) {
// 读取静态资源文件,输出静态资源
StaticResourceUtil.outputStaticResource(new FileInputStream(file), outputStream);
} else {
// 输出404
output(HttpProtocolUtil.getHeader404());
}
}
/**
* 使用输出流输出指定字符串
*
* @param content 输出的字符串内容
* @throws IOException
*/
public void output(String content) throws IOException {
outputStream.write(content.getBytes());
}
}
StaticResourceUtil
public class StaticResourceUtil {
/**
* 获取静态资源文件的绝对路径
*
* @param url
* @return
*/
public static String getAbsolutePath(String url) {
String absolutePath = StaticResourceUtil.class.getResource("/").getPath();
return absolutePath.replaceAll("\\\\", "/") + url;
}
/**
* 读取静态资源文件输入流,通过输出流输出
*/
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.getHeader200(resourceSize).getBytes());
// 读取内容输出
long written = 0;// 已经读取的内容长度
int byteSize = 1024; // 计划每次缓冲的长度
byte[] bytes = new byte[byteSize];
while (written < resourceSize) {
if (written + byteSize > resourceSize) {
// 说明剩余未读取大小不足一个1024长度,那就按真实长度处理
byteSize = (int) (resourceSize - written); // 剩余的文件内容长度
bytes = new byte[byteSize];
}
inputStream.read(bytes);
outputStream.write(bytes);
outputStream.flush();
written += byteSize;
}
}
}
Bootstrap启动类
public class Bootstrap {
private int port = 8080;
/**
* 完成Minicat 2.0版本
* 需求:封装Request和Response对象,返回html静态资源文件
*/
private void start() throws IOException {
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("=====>>>Minicat start on port:" + port);
while (true) {
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
int read = inputStream.read();
// 封装Request对象
Request request = new Request(inputStream);
// 封装Response对象
Response response = new Response(outputStream);
// 输出静态资源
response.outPutHtml(request.getUrl());
socket.close();
}
}
public static void main(String[] args) {
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
HTML页面
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Minicat Indextitle>
head>
<body>
<h1>Hello Minicat!h1>
body>
html>
V3.0需求:可以请求动态资源(Servlet)
1、定义Servlet接口、抽象类HttpServlet
Servlet
public interface Servlet {
void init() throws Exception;
void destroy() throws Exception;
void service(Request request, Response response) throws Exception;
}
HttpServlet
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);
}
}
}
2、创建处理请求的Servlet实现类
public class TestServlet extends HttpServlet {
@Override
public void doGet(Request request, Response response) {
String content = "TestServlet get
";
try {
response.output((HttpProtocolUtil.getHeader200(content.getBytes().length) + content));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void doPost(Request request, Response response) {
String content = "TestServlet post
";
try {
response.output((HttpProtocolUtil.getHeader200(content.getBytes().length) + content));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void init() throws Exception {
}
@Override
public void destroy() throws Exception {
}
}
3、在resources新建web.xml文件
<web-app>
<servlet>
<servlet-name>testServletservlet-name>
<servlet-class>server.TestServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>testServletservlet-name>
<url-pattern>/testurl-pattern>
servlet-mapping>
web-app>
4、新建xml解析器XPathParser,解析xml,获取所有servlet
/**
* xml解析器
*/
public class XPathParser {
private Map<String, HttpServlet> servletMap = new HashMap<>();
public Map<String, HttpServlet> getServletMap() {
return servletMap;
}
public void setServletMap(Map<String, HttpServlet> servletMap) {
this.servletMap = servletMap;
}
/**
* 加载解析相关的配置,web.xml
*/
public void loadXml() {
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("web.xml");
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(resourceAsStream);
Element rootElement = document.getRootElement();
List<Element> servletNodes = rootElement.selectNodes("//servlet");
for (int i = 0; i < servletNodes.size(); i++) {
Element element = servletNodes.get(i);
// testServlet
Element servletNameElement = (Element) element.selectSingleNode("servlet-name");
String servletName = servletNameElement.getStringValue();
// server.TestServlet
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 + "']");
String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
5、Bootstrap启动类
public class Bootstrap {
private int port = 8080;
/**
* 完成Minicat 3.0版本
* 需求:可以请求动态资源(Servlet)
*/
private void start() throws Exception {
// 加载解析相关的配置,web.xml
XPathParser xPathParser = new XPathParser();
// 解析xml
xPathParser.loadXml();
// 获取所有的servlet
Map<String, HttpServlet> servletMap = xPathParser.getServletMap();
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("=====>>>Minicat start on port:" + port);
while (true) {
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
int read = inputStream.read();
// 封装Request对象
Request request = new Request(inputStream);
// 封装Response对象
Response response = new Response(outputStream);
if (servletMap.get(request.getUrl()) == null) {
// 输出静态资源
response.outPutHtml(request.getUrl());
} else {
// 动态资源servlet请求
HttpServlet httpServlet = servletMap.get(request.getUrl());
httpServlet.service(request, response);
}
socket.close();
}
}
public static void main(String[] args) {
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
假设访问http://localhost:8080/test这个请求路径,对应的TestServlet中的service()方法处理请求的时候比较长,那么当调用这个方法后,再通过浏览器访问静态资源的话需要等待,直到service()请求处理完成后才轮到静态资源的访问。因为在start()方法中是阻塞处理每一个请求的。
解决方案:使用多线程处理请求。
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();
OutputStream outputStream = socket.getOutputStream();
int read = inputStream.read();
// 封装Request对象和Response对象
Request request = new Request(inputStream);
Response response = new Response(outputStream);
// 静态资源处理
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();
}
}
}
2、创建线程池工具类 ThreadPoolUtil
public class ThreadPoolUtil {
private ThreadPoolUtil() {
}
private static final ThreadPoolUtil INSTANCE = new ThreadPoolUtil();
public static ThreadPoolUtil getInstance() {
return INSTANCE;
}
// 核心线程数
private int corePoolSize = 3;
// 最大线程数
private int maximumPoolSize = 10;
// 心跳时间
private long keepAliveTime = 100L;
// 心跳时间单位
private TimeUnit unit = TimeUnit.SECONDS;
// 线程池队列
private BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(50);
// 线程工厂
private ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 拒绝策略
private RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
// 自定义线程池
private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
/**
* 获取线程池
*
* @return
*/
public ThreadPoolExecutor getThreadPoolExecutor() {
return threadPoolExecutor;
}
}
3、Bootstrap启动类
public class Bootstrap {
private int port = 8080;
/**
* 完成Minicat 3.0版本
* 需求:可以请求动态资源(Servlet)
*/
private void start() throws Exception {
// 加载解析相关的配置,web.xml
XPathParser xPathParser = new XPathParser();
// 解析xml
xPathParser.loadXml();
// 获取所有的servlet
Map<String, HttpServlet> servletMap = xPathParser.getServletMap();
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("=====>>>Minicat start on port:" + port);
// 获取线程池
ThreadPoolExecutor threadPoolExecutor = ThreadPoolUtil.getInstance().getThreadPoolExecutor();
while (true) {
Socket socket = serverSocket.accept();
// 使用多线程
RequestProcessor requestProcessor = new RequestProcessor(socket, servletMap);
// 执行任务
threadPoolExecutor.execute(requestProcessor);
}
}
public static void main(String[] args) {
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}