1 代码结构
现在主要想实现一个简单的Tomcat,支持特定请求的解析和静态资源的实现。主要是通过socket来实现的,代码结构如下:
2 整体设计
Socket连接接口线程接收到请求后,构造相应的Request和Response,同时构造单个请求对应的处理器,针对不同的资源构造不同的处理器。整体流程如下:
3 代码介绍
3.1 请求接收处理器
请求接受处理器主要绑定端口,配置线程池,启动端口监听。线程池这边采用默认的配置。主要的过程:
- 构造ServerSocket,开始监听端口
- 构造Request和Response
- 构造Processor来处理单个请求
- 将构造的请求丢到线程池中进行处理
相应的代码如下:
package com.sunx.simple.tomcat;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;
/**
* @Author sunx
* @Description
* @Date 2019/2/20
**/
public class AcceptHandler {
/**
* 监听的端口
*/
private int port = 8080;
/**
* 是否关闭
*/
private boolean running = true;
/**
* 服务端socket
*/
private ServerSocket serverSocket;
/**
* 线程池
*/
private ExecutorService pool;
/**
* 构造实例对象
* @return
*/
public static AcceptHandler newInstance(){
return new AcceptHandler();
}
/**
* 绑定端口
* @param port
* @return
*/
public AcceptHandler port(int port){
this.port = port;
return this;
}
public AcceptHandler pool(int threads, ThreadFactory threadFactory){
pool = Executors.newFixedThreadPool(threads,threadFactory);
return this;
}
/**
* 启动服务端socket
* @return
*/
public AcceptHandler socket(){
try {
serverSocket = new ServerSocket(port);
} catch (IOException e) {
//抛出异常,显示服务端出现问题,暂不处理
e.printStackTrace();
}
return this;
}
/**
* 启动线程,进行监听接收请求
* @return
*/
public AcceptHandler accept(){
pool.execute(new AcceptTaskThread());
return this;
}
/**
* 连接请求接受线程
*/
public class AcceptTaskThread implements Runnable{
@Override
public void run() {
acceptConnection();
}
}
public void acceptConnection(){
//启动监听
while(running){
//接收相应的数据内容
try {
System.out.println("start to accept.");
Socket socket = serverSocket.accept();
System.out.println("接收到请求:" + socket.toString());
builderRequestAndResponse(socket);
} catch (IOException e) {
//获取连接
e.printStackTrace();
}
}
}
public void builderRequestAndResponse(Socket socket) {
try{
//包装构造request
Request request = builderRequestByInputStream(socket.getInputStream());
//包装构造response
Response response = builderResponseByOutputStreamAndRequest(socket.getOutputStream(),request);
//包装当前socket,丢入到线程池中,等待处理
System.out.println("包装当前socket,丢入到线程池中,等待处理.");
SocketProcessor processor = SocketProcessor.newInstance(socket,request,response);
pool.execute(processor);
}catch (IOException e){
e.printStackTrace();
SocketUtils.sendError(socket);
SocketUtils.close(socket);
}
}
/**
* 构造请求对象
* @param inputStream
* @return
*/
private Request builderRequestByInputStream(InputStream inputStream) throws IOException{
System.out.println("构造请求对象");
Request request = new Request(inputStream);
request.prepareParseRequest();
return request;
}
/**
* 构造相应对象
* @param outputStream
* @param request
* @return
*/
private Response builderResponseByOutputStreamAndRequest(OutputStream outputStream,Request request){
System.out.println("构造相应对象");
return new Response(outputStream,request);
}
}
3.2 构造请求对象
连接过来以后从流中读取出相应的请求头数据,进而解析出相应的数据并填充到Request对象中,Request对象中需要先绑定对应的输入流。
/**
* 用户请求输入流
*/
private InputStream inputStream;
public Request(InputStream inputStream) {
this.inputStream = inputStream;
}
输入流绑定后,从输入流中读取相应的数据。具体的先从流中读取数据转化为字符串:
/**
* 解析请求数据
*/
public String parseRequestLine(){
StringBuilder temp = new StringBuilder();
int cache;
try{
while ((cache = inputStream.read()) != -1) {
//读取到第一个\r\n时则说明读取请求行完毕
if (CARRIAGE_RETURN == cache) {
break;
}
temp.append((char)cache);
}
}catch (Exception e){
e.printStackTrace();
}
return temp.toString();
}
请求头相应数据读取出来以后,开始进行解析,将请求头和请求方式等数据,具体的如下:
/**
* 解析获取到的数据,丰富当前的request数据
*/
public void prepareParseRequest() throws IOException {
this.requestStr = parseRequestLine();
//对数据进行判断
if (null == requestStr || requestStr.length() == 0) {
throw new IOException("error request");
}
//解析数据,将数据切分出来
List<String> list = Arrays.asList(requestStr.split("\r\n"));
if (null == list || list.size() <= 0) {
throw new RuntimeException("error request");
}
//解析请求方式,请求地址,http协议
parseMethodAndPathFromRequestStr(list.get(0));
//解析请求头
parserRequestHeaderFromRequestStr(list);
}
/**
* 解析请求方式
* 解析请求地址
* 解析请求协议
* @param path
*/
private void parseMethodAndPathFromRequestStr(String path) throws IOException{
//针对请求头进行处理
List<String> strs = Arrays.asList(path.split(" "));
//切分好以后,进行参数结果判断
if (null == strs || strs.size() != 3) {
throw new IOException("error request");
}
//开始进行相应数据的赋值
this.requestMethod = strs.get(0);
this.requestPath = strs.get(1);
//开始解析路径参数
//解析参数
}
/**
* 解析请求头
* @param list
*/
private void parserRequestHeaderFromRequestStr(List<String> list) throws IOException{
if(list.size() <= 2){
return;
}
List<String> array = list.subList(1,list.size() - 1);
//循环遍历,将字符串进行切割后
for(String item : array){
String[] strs = item.split(": ");
if (strs == null || strs.length != 2) {
throw new IOException("error request");
}
header.put(strs[0],strs[1]);
}
}
3.3 构造响应对象
响应对象主要需要绑定OutputStream和Request,用于将响应数据返回回去,这里只是做了简单的包装和OutputStream的返回,主要的代码和逻辑如下:
public class Response implements ServletResponse {
private Request request;
private OutputStream outputStream;
public Response(OutputStream outputStream, Request request){
this.outputStream = outputStream;
this.request = request;
}
public OutputStream getResponseOutputStream(){
return this.outputStream;
}
@Override
public PrintWriter getWriter() throws IOException {
return new PrintWriter(this.outputStream,true);
}
......
}
3.4 请求处理器
请求处理器,经过Request中的解析出请求方式,请求地址,请求头等数据后,根据不同的地址和资源进行明确采用不同的资源处理器。Processor继承Runnable,将当前的Processor后丢给线程池进行执行。Processor执行的过程为:
- 从Request中获取请求地址,判断地址中是否含有/api,如果含有/api,则走ApiResouceProcessor,否则走StaticResourceProcessor
- Api接口的处理,需要找到对应的Servlet,然后实例化后进行执行,如果初步实例化标记的Servlet后,那么可以类似于SpringBoot来进行IOC注入后进行请求分发
- 静态资源的获取找到对应的地址后,进行解析展示到前端页面
具体的StaticResourceProcessor代码如下:
/**
* 静态资源的处理
*/
public class StaticResourceProcessor {
/**
* 开始处理请求
* @param request
* @param response
* @throws IOException
*/
public void proccess(Request request, Response response) throws Exception {
//就是上面的那个字符串截取方法
String uri = request.getRequestPath();
//获取uri对应的静态资源
URL ROOT = TomcatServer.class.getClassLoader().getResource(STATIC_RESOURCE_URI);
File staticResource = new File(ROOT.getPath() + uri);
if(staticResource.exists() ||staticResource.isFile()){
response.getResponseOutputStream().write(MessageUtils.responseToByte(200,"OK"));
write(response.getResponseOutputStream(),staticResource);
}else{
staticResource = new File(ROOT.getPath() + "/404.html");
response.getResponseOutputStream().write(MessageUtils.responseToByte(404,"file not found"));
write(response.getResponseOutputStream(),staticResource);
}
}
/**
* 向服务端写入资源数据
* @param outputStream
* @param resouce
*/
private void write(OutputStream outputStream, File resouce){
try{
try (FileInputStream fis = new FileInputStream(resouce)) {
byte[] cache = new byte[1024];
int read;
while ((read = fis.read(cache, 0, 1024)) != -1) {
outputStream.write(cache, 0, read);
}
outputStream.flush();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
具体的ApiResouceProcessor代码如下:
/**
* 接口资源的处理
*/
public class ApiResouceProcessor{
/**
* 开始处理请求
* @param request
* @param response
* @throws IOException
*/
public void proccess(Request request, Response response) throws Exception {
//就是上面的那个字符串截取方法
String uri = request.getRequestPath();
//找到对应的路径地址对应的servlt
String clazz = relation.getOrDefault(uri,null);
if (clazz == null || clazz.length() <= 0 ) {
throw new RuntimeException("no mapping relation of servlet");
}
//实例化该servlet对象
Class<?> servletClass = this.getClass().getClassLoader().loadClass(clazz);
//实例化对象
Servlet servlet = (Servlet)servletClass.newInstance();
response.getResponseOutputStream().write(MessageUtils.responseToByte(200,"OK"));
//调用servlet的service方法
servlet.service(request,response);
}
}
在ApiResouceProcessor中,relation为一个映射集合,下面是一个简单的例子:
relation.put("/api/test","com.sunx.simple.tomcat.TestServlet");
3.5 构造自定义Servlet
public class TestServlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
System.out.println("Start invoke TestServlet ... ");
response.getWriter().println("Hello Servlet!");
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
relation的映射关系可以通过注解的方式来实例化自定义的类,用于执行特定的方法,这里不一定是需要实现Servlet.
测试结果如下: