在手写Tomcat(一)中,我实现了通过服务器能够以二进制流的形式访问WEB-ROOT目录下的所有文件(除图片).在这篇文章中,我要实现通过服务器能够访问到Servlet.
gitHub地址(持续更新中):https://github.com/shenshaoming/tomcat
本项目为纯Java语言,克隆项目之后只需要用src中的文件就可以了.
先截个图解释一下我的项目结构:
其中AbstractServlet实现了GenericServlet接口,并且重写了service方法,在service中通过判断方法类型(GET,POST)选择对应的方法.Main是启动服务器的程序.
接下来是代码,我把注释都写在代码中了,文章里只做一些简单介绍:
首先是Servlet相关的.java文件:
GenericServlet.java:规定了Servlet中的方法.
package com.tomcat.baseservlet;
import com.tomcat.core.Request;
import com.tomcat.core.Response;
/**
* Servlet约束,子类必须拥有这些方法
* @Author: 申劭明
* @Date: 2019/9/18 9:43
*/
public interface GenericServlet {
/**
* @Description : Servlet初始化时执行的方法
*
* @author : 申劭明
* @date : 2019/9/18 10:10
*/
void init() throws Exception;
/**
* @Description : Servlet销毁时执行的方法
*
* @return : void
* @author : 申劭明
* @date : 2019/9/18 10:10
*/
void destroy();
/**
* @Description : Servlet分发请求时用到的方法
*
* @param request 请求
* @param response 响应
* @author : 申劭明
* @date : 2019/9/18 10:11
*/
void service(Request request, Response response) throws Exception;
}
AbstractServlet.java:实现接口,并通过判断http请求的类型决定具体访问的方法
package com.tomcat.baseservlet;
import com.tomcat.core.Request;
import com.tomcat.core.Response;
/**
* GenericServlet的抽象实现
* @Author: 申劭明
* @Date: 2019/9/18 10:03
*/
public abstract class AbstractServlet implements GenericServlet{
private static final String GET_METHOD = "GET";
private static final String POST_METHOD = "POST";
/**
* @Description : 在抽象类中实现请求的分发
*
* @param request 请求
* @param response 响应
* @author : 申劭明
* @date : 2019/9/18 10:13
*/
@Override
public void service(Request request, Response response) throws Exception {
if (GET_METHOD.equalsIgnoreCase(request.getMethod())){
this.doGet(request,response);
}else if (POST_METHOD.equalsIgnoreCase(request.getMethod())){
this.doPost(request,response);
}
}
/**
* @Description : 请求类型为GET时执行的方法
*
* @param request 请求
* @param response 响应
* @author : 申劭明
* @date : 2019/9/18 10:12
*/
protected abstract void doGet(Request request, Response response);
/**
* @Description : 请求类型为POST时执行的方法
*
* @param request 请求
* @param response 响应
* @author : 申劭明
* @date : 2019/9/18 10:12
*/
protected abstract void doPost(Request request, Response response);
}
UserServlet.java:Servlet测试
package com.tomcat.servlet;
import com.tomcat.annotations.Servlet;
import com.tomcat.baseservlet.AbstractServlet;
import com.tomcat.core.Request;
import com.tomcat.core.Response;
/**
* @Author: 申劭明
* @Date: 2019/9/18 10:09
*/
@Servlet("/user")
public class UserServlet extends AbstractServlet {
@Override
protected void doGet(Request request, Response response) {
this.doPost(request,response);
}
@Override
protected void doPost(Request request, Response response) {
//处理
//响应
response.setResponseContent(new StringBuilder("<h1>Your are requesting the UserServlet</h1>"));
}
@Override
public void init() throws Exception {
}
@Override
public void destroy() {
}
}
其次是core包中的.java文件:
HttpServer.java:负责监听端口与启动线程处理收到的请求.
package com.tomcat.core;
import com.tomcat.annotations.Servlet;
import com.tomcat.baseservlet.AbstractServlet;
import java.io.*;
import java.net.*;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* 监听请求,调用request和response对请求作出反应
* @Author: 申劭明
* @Date: 2019/9/16 17:21
* @version: 4.1
*/
public class HttpServer {
/**
* 监听端口
*/
public static int port = 8080;
/**
* 关闭服务器的请求URI
*/
static final String CLOSE_URI = "/shutdown";
/**
* Key值为Servlet的别名(uri),value为该Servlet对象
* default权限
*/
static HashMap<String, AbstractServlet> map;
static {
//包名,可以通过application.properties设置
getServlets("com.tomcat.servlet");
}
/**
* 单例,因为是通过主函数启动,不涉及多进程启动的问题,所以不需要做多线程方面的考虑
*/
static ServerSocket serverSocket = null;
/**
* @Description : 多线程bio监听数据请求
* @author : 申劭明
* @date : 2019/9/17 10:29
*/
public void acceptWait() {
try {
serverSocket = new ServerSocket(port);
} catch (IOException e) {
e.printStackTrace();
}
//扫描包,读取包下的所有Servlet
while (!serverSocket.isClosed()) {
try {
//单线程,阻塞式监听(bio)
Socket socket = serverSocket.accept();
RequestHandler handler = new RequestHandler(socket);
handler.start();
} catch (IOException e) {
e.printStackTrace();
continue;
}
}
}
/**
* @param packageName 包名,如com.tomcat.servlet
* @return : void
* @Description : 扫描packageName包下的所有带有@Servlet注解的类文件
* @author : 申劭明
* @date : 2019/9/18 10:36
*/
private static void getServlets(String packageName) {
//class类的集合
Set<Class<?>> classes = new LinkedHashSet<>();
try {
String packageDirName = packageName.replace(".", "/");
Enumeration<URL> resources = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
String protocol = url.getProtocol();
if ("file".equals(protocol)) {
String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
findAndAddClassesInPackageByFile(packageName, filePath,true,classes);
}else if("jar".equals(protocol)){
//扫描JAR包
}
}
//遍历class集合
if (map == null){
map = new HashMap<>(classes.size());
}
for (Class<?> aClass : classes) {
//如果该class有Servlet注解
if (aClass.isAnnotationPresent(Servlet.class)){
try {
//添加至map集合中
map.put(aClass.getAnnotation(Servlet.class).value(), (AbstractServlet) aClass.newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* @Description : 对于file类型获取该类型的所有class
*
* @param packageName 包名,com.tomcat.servlet
* @param packagePath 包路径,com/tomcat/servlet
* @param recursive 是否循环遍历子包内的文件
* @param classes class集合
* @return : void
* @author : 申劭明
* @date : 2019/9/18 16:55
*/
public static void findAndAddClassesInPackageByFile(String packageName,
String packagePath, final boolean recursive, Set<Class<?>> classes) {
// 获取此包的目录 建立一个File
File dir = new File(packagePath);
// 如果不存在或者 也不是目录就直接返回
if (!dir.exists() || !dir.isDirectory()) {
return;
}
// 如果存在 就获取包下的所有文件 包括目录
File[] dirfiles = dir.listFiles(new FileFilter() {
// 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件)
@Override
public boolean accept(File file) {
return (recursive && file.isDirectory())
|| (file.getName().endsWith(".class"));
}
});
// 循环所有文件
for (File file : dirfiles) {
// 如果是目录 则继续扫描
if (file.isDirectory()) {
findAndAddClassesInPackageByFile(packageName + "."
+ file.getName(), file.getAbsolutePath(), recursive,
classes);
} else {
// 如果是java类文件 去掉后面的.class 只留下类名
String className = file.getName().substring(0,
file.getName().length() - 6);
try {
// 添加到集合中去
// classes.add(Class.forName(packageName + '.' +
// className));
// 经过回复同学的提醒,这里用forName有一些不好,会触发static方法,没有使用classLoader的load干净
classes.add(Thread.currentThread().getContextClassLoader()
.loadClass(packageName + '.' + className));
} catch (ClassNotFoundException e) {
// log.error("添加用户自定义视图类错误 找不到此类的.class文件");
e.printStackTrace();
}
}
}
}
}
Request.java:负责从HTTP请求报文中提取urI(和URL的概念不同)和请求的类型(GET,POST):
package com.tomcat.core;
import java.io.IOException;
import java.io.InputStream;
/**
* @Author: 申劭明
* @Date: 2019/9/16 17:24
*/
public class Request {
private InputStream is;
/**
* 请求路径,如:/test.txt
*/
private String uri;
/**
* 请求类型,GET或POST等
*/
private String method;
public String getMethod() {
return method;
}
public Request(){
}
public Request(InputStream inputStream){
this.is = inputStream;
//读取报文
parse();
}
/**
* @Description : 获取http请求中的相关参数
*
* @author : 申劭明
* @date : 2019/9/17 10:26
*/
public void parse() {
/**
* 一个包没有固定长度,以太网限制在46-1500字节,
* 1500就是以太网的MTU,超过这个量,TCP会为IP数据报设置偏移量进行分片传输,
* 现在一般可允许应用层设置8k(NTFS系统)的缓冲区,8k的数据由底层分片,
* 而应用层看来只是一次发送。
*/
//创建一个容量为2048的StringBuffer对象
StringBuffer request = new StringBuffer(Response.BUFFER_SIZE);
//记录字节数量
int i ;
byte[] buffer = new byte[Response.BUFFER_SIZE];
try {
//从输入流中读取数据到buffer中,i表示读到了多少字节(多少个byte)
i = is.read(buffer);
} catch (IOException e) {
e.printStackTrace();
i = -1;
}
System.out.println(request);
//i表示有读到了多少字节,所以此处要用<而不是<=
for (int j = 0; j < i; j++) {
request.append((char)buffer[j]);
}
System.err.println(request.toString());
uri = parseUri(request.toString());
method = parseMethod(request.toString());
}
/**
* @Description : 获取请求路径,如http://localhost:8080/test.txt,截取/test.txt(URI)
*
* @param request 请求头
* @return : 请求路径
* @author : 申劭明
* @date : 2019/9/17 9:33
*/
private String parseUri(String request) {
int index1,index2;
//查看socket获取的请求头是否有值
index1 = request.indexOf(' ');
if (index1 != -1){
index2 = request.indexOf(' ', index1 + 1);
if (index2 > index1){
return request.substring(index1 + 1,index2);
}
}
return null;
}
/**
* @Description : 获取请求类型
*
* @param request 请求报文
* @return : GET,POST...
* @author : 申劭明
* @date : 2019/9/18 9:51
*/
private String parseMethod(String request){
int index = request.indexOf(' ');
if (index != -1){
return request.substring(0,index);
}
return null;
}
public String getUri() {
return uri;
}
}
Response.java:负责响应界面的类,当用户的请求不在已经注册过的Servlet中时,就会通过response中的sendStaticResource()方法获取WEB-ROOT(当前设置的是读取D盘中的资源)下的资源.
package com.tomcat.core;
import java.io.*;
/**
* @Author: 申劭明
* @Date: 2019/9/16 17:28
*/
public class Response {
/**
* 传输数组的最大字节数
*/
public static final int BUFFER_SIZE = 2048;
/**
* 访问的文件的路径,即tomcat中部署项目的目录
*/
private static final String WEB_ROOT = "D:";
/**
* 响应头信息
*/
private static final String RESPONSE_HEADER = "HTTP/1.1 200 Read File Success\r\n" +
"Content-Type: text/html;charset=UTF-9\r\n" + "\r\n";
/**
* 请求
*/
private Request request;
/**
* 返回页面的数据
*/
private OutputStream output;
public Response(OutputStream outputStream) {
this.output = outputStream;
}
public void setRequest(Request request) {
this.request = request;
}
/**
* @Description : 向页面返回数据
*
* @author : 申劭明
* @date : 2019/9/17 10:27
*/
public void sendStaticResource() throws IOException {
//返回数据时所用的字节流
byte[] bytes = new byte[BUFFER_SIZE];
FileInputStream fis = null;
File file = new File(WEB_ROOT, request.getUri());
String returnMessage = null;
try {
//如果文件存在,且不是个目录
if (file.exists() && !file.isDirectory()) {
fis = new FileInputStream(file);
//读文件
int ch ;
StringBuilder sb = new StringBuilder(BUFFER_SIZE);
//写文件
while ((ch = fis.read(bytes,0,bytes.length)) != -1) {
sb.append(new String(bytes,0,ch,"UTF-8"));
}
returnMessage = RESPONSE_HEADER + sb;
}else {
//文件不存在,返回给浏览器响应提示,这里可以拼接HTML任何元素
String retMessage = "<h1>" + file.getName() + " file or directory not exists</h1>";
returnMessage = "HTTP/1.1 404 File Not Fount\r\n" +
"Content-Type: text/html\r\n" +
"Content-Length: " + retMessage.length() + "\r\n" +
"\r\n" +
retMessage;
}
//用输出流返回数据给页面
output.write(returnMessage.getBytes());
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (fis != null){
fis.close();
}
if (output != null){
//清空缓存区,调用close方法时会有flush操作
// output.flush();
output.close();
}
}
}
/**
* @Description : 设置返回数据
*
* @param message 返回给页面的数据
* @author : 申劭明
* @date : 2019/9/18 10:19
*/
public void setResponseContent(StringBuilder message){
try {
output.write(new StringBuilder(RESPONSE_HEADER).append(message).toString().getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
public void setResponseHeader(String message){
setResponseContent(new StringBuilder(message));
}
}
RequestHandler.java:继承自Thread类,每当HttpServer接收到一个请求,就会开一个线程(RequestHandler)去处理这个请求.
package com.tomcat.core;
import com.tomcat.baseservlet.AbstractServlet;
import java.net.Socket;
/**
* @Author: 申劭明
* @Date: 2019/9/17 17:45
*/
public class RequestHandler extends Thread {
private Socket socket;
public RequestHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
//接收请求参数
Request request = new Request(socket.getInputStream());
AbstractServlet abstractServlet = HttpServer.map.get(request.getUri());
//如果请求的是/shutdown 则关闭服务器
if (HttpServer.CLOSE_URI.equals(request.getUri())){
HttpServer.serverSocket.close();
return;
}
//创建用于返回浏览器的对象
Response response = new Response(socket.getOutputStream());
response.setRequest(request);
if (abstractServlet != null){
abstractServlet.service(request,response);
}else{
//找不到对应的Servlet则直接访问文件
response.sendStaticResource();
}
//如果http短连接则关闭socket
//socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
启动类Main.java:
import com.tomcat.core.HttpServer;
/**
* @author: 申劭明
* @date: 2019-09-16
*/
public class Main {
public static void main(String[] args) {
HttpServer server = new HttpServer();
server.acceptWait();
}
}