前言
很早就有看到公众号推送的手写简单tomcat的类似文章。一直想模仿着写写,主要感受下人家的思维和思路,今天终于参考别人写的自己手动也写了一下。
整体思路要求
- 保证长连接,保持长时间监控端口请求
- 封装请求参数信息,其中包括地址映射的servlet,请求地址,请求端口等
- 执行请求封装返回信息等
主要代码实现
项目地址:抄袭miniTomcat
- 使用Socket做网络连接端口监听,这里一定要注意异常catch掉,否则会断掉服务
/**
* 启动类
*/
public void start() {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(PORT);
//while保持长时间连接
while (true) {
//注意这里try catch保证catch到所有错误保证程序不停止
try {
Socket accept = serverSocket.accept();
InputStream inputStream = accept.getInputStream();
OutputStream outputStream = accept.getOutputStream();
Request request = Request.create(inputStream);
Response response = Response.create(outputStream);
requestMapping.dispatch(request, response);
accept.close();
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//保证连接关闭
if (Objects.nonNull(serverSocket)) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- Request封装:analysis方法是主要的解析方法,httpString拿到的字符串就是http请求的request的一整块,通过换行符和空格去拿到对应的需要封装的参数,包括请求参数等(这里只拿了url和请求方式),具体http请求有的参数参考http官方文档
package http;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
/**
* request封装类
*
* @author 50238
*/
public class Request {
static Request create(InputStream inputStream) {
return new Request(inputStream);
}
/**
* http
*/
private String httpString;
/**
* 请求方式
*/
private String method;
/**
* 请求地址
*/
private String url;
private Request(InputStream inputStream) {
if (Objects.isNull(inputStream)) {
throw new NullPointerException("http请求输入错误");
}
try {
byte[] inputByte = new byte[1024];
inputStream.read(inputByte);
httpString = new String(inputByte);
analysis();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 解析http
* 主要根据http协议进行解析:header body参数等
* 这里有一定感觉所谓的协议,不过是按照规矩组成的字符串
* {参考http协议官方文档@link: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Messages}
*/
private void analysis() {
String[] split = httpString.split("\\n");
System.out.println(httpString);
this.method = split[0].split("\\s")[0];
this.url = split[0].split("\\s")[1];
}
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;
}
}
- Response封装:这里主要是返回数据封装,即http请求中的Response那一块数据(我这里只返回了一个html字符,Content-type指定返回类型)
package http;
import java.io.IOException;
import java.io.OutputStream;
/**
* 输出
*
* @author 50238
*/
public class Response {
private OutputStream outputStream;
static Response create(OutputStream outputStream) {
return new Response(outputStream);
}
private Response(OutputStream outputStream) {
this.outputStream = outputStream;
}
/**
* 输出内容
* 这里指定协议,数据类型,返回数据等
*
* @param outString
*/
public void write(String outString) throws IOException {
String httpResponse = "HTTP/1.1 200 OK\n" +
"Content-type: text/html\n" +
"\r\n" +
"<html><body>" +
outString +
"</body></html>";
outputStream.write(httpResponse.getBytes());
outputStream.close();
}
}
- servlet定义:MyServlet抽象类定义了通用的get post调用方法,analysis做请求方式的解析调用
package http;
/**
* 自定义servlet
*
* @author 50238
*/
abstract class MyServlet {
/**
* get method
*
* @param request
* @param response
*/
abstract void doGet(Request request, Response response);
/**
* post method
*
* @param request
* @param response
*/
abstract void doPost(Request request, Response response);
/**
* 解析请求方式
* 这里可以进行解析其他请求方式post get put delete等
* {参考官方文档@link:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/DELETE}
*
* @param request
* @param response
*/
void analysis(Request request, Response response) {
String method = request.getMethod().toUpperCase();
switch (method) {
case "GET":
doGet(request, response);
break;
case "POST":
doPost(request, response);
break;
default:
throw new NullPointerException("请求方法不支持");
}
}
}
package http;
import java.io.IOException;
/**
* 自定义Servlet
*
* @author 50238
*/
public class Servlet extends MyServlet {
@Override
void doGet(Request request, Response response) {
try {
response.write("doGet");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
void doPost(Request request, Response response) {
try {
response.write("doPost");
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 最后地址映射方法(这里只写了一个hello的映射),主要方法init初始化servlet。
package http;
import java.util.HashMap;
/**
* 地址映射
*
* @author 50238
*/
public class RequestMapping {
/**
* 地址解析
* {key:地址,类名}
*/
private final HashMap<String, String> SERVLET_URL = new HashMap<>(1);
/**
* 地址解析
* {key:地址,实例}
*/
private final HashMap<String, MyServlet> SERVLETS = new HashMap<>(1);
{
//这里提前指定了地址和servlet,等于tomcat解析web.xml
//这里可以写成注解扫描处理servlet或者解析xml处理,后面有时间写一个吧
SERVLET_URL.put("/hello", "http.Servlet");
init();
}
/**
* 初始化
* 通过反射实例化servlet
*/
private void init() {
SERVLET_URL.forEach((k, v) -> {
try {
try {
SERVLETS.put(k, (MyServlet) Class.forName(v).newInstance());
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
});
}
/**
* 获取servlet
*
* @return
*/
public void dispatch(Request request, Response response) {
String url = request.getUrl();
MyServlet myServlet = SERVLETS.get(url);
myServlet.analysis(request, response);
}
}
- 启动
import http.MyTomcat;
/**
* 启动类
*
* @author 50238
*/
public class Application {
public static void main(String[] args) {
MyTomcat m = new MyTomcat();
m.start();
}
}
RequestMapping类扩展想法思路:
上面地址解析时写死的,只能请求http//xxxxxxx/hello,如果想丰富的话有两种常用处理方式:
- 注解式:通过扫描包将自定义注解@Servlet(可以自己命名)扫描放到容器中,获取必填参数url进行Map(url,servlet)映射
- 配置xml方式,解析xml按照格式解析和注解方式一样放入容器
如果想配置过滤器也可以使用servlet相同方式进行处理,放入容器然后地址映射在servlet之前调用即可
个人总结
Http协议:从这个项目中个人感觉所谓的协议就是一串规定格式的串罢了,其实也不是感觉很神秘了。不过看官方文档可以知道很多http可以优化的东西,这里不赘述,作为web开发还是应该知道这些。
Socket:感受到了自己对网络编程这块的缺失,对Socket不熟悉。里面还涉及着大量的东西。
IO:,以前也总是感觉没啥用。在何时起感觉到程序无论什么操作感觉都是在处理IO,让我认识到了IO的重要性。在学习Mysql时很多时候也提到IO对优化的影响,这也是一大片空白需要恶补。
反射:很重要的东西,不管是现在主流的框架或者说tonmcat都是用到了这一特性,也是需要注意学习的。
容器:听着很高大上,可以就理解成一个集合或者Map吧,感觉是一种思想,在之前看Spring、Mybatis的一些东西的时候慢慢的给了我越来越深的印象。
缺失的东西:这个简单的tomcat还没涉及到多线程的请求、session问题、多项目包共享问题、监听器实现、很多参数解析等问题,感觉任重而道远还要学习的东西很多。