这是tomcat实现的第三篇,tomcat实现的具体细节
完整源码地址:https://github.com/zhangjingao/tomcat.git
在开始之前,先来了解一下客户端和服务器端建立连接的技术——socket套接字。
老习惯,上图再说话
socket是什么
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket
Socket为TCP协议提供了两个类,分别为Socket和ServerSocket,一个代表客户端,一个代表 服务器端。通过操作这两个类即可实现TCP协议通信
由上篇博客得知tomcat在运行中需要做下面几个事情:
- Main方法:
servlet程序是没有Main的,但是一个程序没有Main是无法启动的,所以tomcat需要有Main方法。 - 得到所有的servlet,得到servlet类和servlet地址的映射关系
tomcat要判断哪些类是servlet,并可以根据地址实例化servlet对象。 - 同时接受多个请求
tomcat要满足多个客户端的同时访问,所以需要多线程接收多个请求。 - 处理请求
如果是请求的静态资源就直接定位静态资源写回客户端,如果请求的servlet,那么定位具体的请求的servlet并调用。 - 得到响应并返回客户端
先看一下我的目录结构有个总体印象
webapp:所有的jsp以及html等静态资源都存在这
com.zjg.tomdog包下:
1. annotations:
WebService类是一个自定义注解,凡是实现了HttpServlet接口并且被WebService注解标志的才被认为是servlet。
ServletMapping类是将所有的servlet装进一个Map集合中
2. controller: 是一个模拟web应用的包,我们整个项目是一个tomcat,如果再新建一个项目作为web项目,然后加载这个项目去调用web项目中的servlet就比较麻烦,于是这个包就是一个模拟的web应用,里面有普通的类还有servlet
3. handle:这个包起到的作用就是接收请求,监听80端口,接收客户端请求。
4. handlecore和handlecoreimpl包:
分别是处理请求接口包和处理请求实现类包,主要功能有读取请求,判断请求的资源类型,转发调用servlet,关闭连接。
5. servletdefine:定义servlet的接口,定义其必须实现的get和post方法。
6. thread:多线程包,分出一个线程进行处理客户端请求。
7. util:工具包,判断请求资源类型,request请求定义,response定义
这里主要介绍核心伪代码,细节可以去github看我的源码
第一步:Main方法开启tomcat就不用多说了吧
第二步:得到所有的servlet,得到servlet类和servlet地址的映射关系
首先定义一个静态的存放servlet的map集合,当然也可以是其他的。
public static Map<Class<?> , String> servlets = new HashMap<Class<?>, String>();
/**
* 映射注解
*/
public void getAnnotation () {
Map<Class<?> , String> allClass = getClasses("com.zjg.tomdog");//得到com.zjg.tomdog包下所有的java类
for (Map.Entry<Class<?>, String> clas : allClass.entrySet()) {
boolean useAnnota = clas.getKey().isAnnotationPresent(WebService.class);//判断是否被WebService这个注解标志,是的话存放进map
if (useAnnota) {
MainApp.servlets.put(clas.getKey(),clas.getValue());
}
}
}
第三步:同时接受多个请求
这里使用循环监听连接请求,每当监听到一个连接请求就从线程池中分出一个线程去处理。
/**
* 监听并处理请求
*/
public void handle() {
ThreadPoolFactory poolFactory = ThreadPoolFactory.getInstance();
try {
ServerSocket serverSocket = new ServerSocket(80);//监听80端口
while (true) { //等待请求
Socket socket = serverSocket.accept();//当服务器接受一个请求时就创建一个socket
poolFactory.addTask(new ThreadTask(socket));
}
} catch (IOException e) {
e.printStackTrace();
}
}
package com.zjg.tomdog.thread;
import com.zjg.tomdog.handlecore.ExceptionHandle;
import com.zjg.tomdog.handlecore.HandleCore;
import com.zjg.tomdog.handlecoreimpl.ExceptionHandleImpl;
import com.zjg.tomdog.handlecoreimpl.HandleCoreImpl;
import com.zjg.tomdog.util.Request;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author zjg
* @date 2018/4/4 20:30
* @Description
*/
public class ThreadTask extends Thread {
private Socket socket = null;
private final HandleCore handleCore = new HandleCoreImpl();//核心方法类
private final ExceptionHandle exceptionHandle = new ExceptionHandleImpl();
public ThreadTask (Socket socket) {
this.socket = socket;
}
@Override
public void run () {
int timeOut = 3; //3秒超时
Date start = new Date();//第一次进入时间
Date end;
Request requestMessage = null;
while (true) {
end = new Date();//再次请求时间
long requestTime = (end.getTime() - start.getTime())/1000;
try {
if (requestTime > timeOut) {
handleCore.closeSocket(socket);
break;
} else {
start = end;//不超时则重新赋值计算超时的时间
}
socket.setSoTimeout(3*1000);//设置超时时间为3秒,超出抛出异常
requestMessage = handleCore.read(socket); //读取request请求
if (null != requestMessage) {
handleCore.forword(socket, requestMessage);//加载请求的类,方法
} else {
handleCore.closeSocket(socket);
break;
}
} catch (Exception e) {
e.printStackTrace();
exceptionHandle.cantHandlePath("接收请求或关闭socket异常");
handleCore.closeSocket(socket);
break;
}
}
}
}
这里有个超时时间,前面说过了,对于长连接一次请求结束后并不是直接http请求断开了,而是继续使用,继续使用那么服务器端怎么直到这个http请求什么时候不再使用了呢,其实一般不用服务器端管理,一个http请求结束后,客户端如果不需要了,它会直接断开,如果需要,那么每隔一段时间,客户端会自动发一个心跳包维持连接,就是说http自己有一个超时时间,如果客户端继续使用的话,客户端会在这个时间之前发一个心跳包维持http的连接。这个超时管理对于服务器端来讲是一个保险机制。
那么我们限制超时时间3s。请求刚来的时候我们记录时间,当它处理完这个请求后再判断下时间是否超时,socket.setSoTimeout这个方法就是另一道保险。这个方法用于限制socket连接时间是3s,设置他的原因是我们可以得到处理完请求前的时间,但无法判断它再次读请求的时间,如果它卡在读请求的地方,那么这个线程永远无法释放,所以这个方法是另一道保险,如果时间到了,直接抛出异常,停止连接。
看一下是如何读请求的,也就是这行代码的实现
requestMessage = handleCore.read(socket); //读取request请求
@Override
public Request read (Socket socket) {
String url = null;
try {
InputStream inputStream = new BufferedInputStream(socket.getInputStream());
byte reads = (byte) inputStream.read();
byte[] bc = new byte[inputStream.available()+1];
bc[0] = reads;
inputStream.read(bc, 1, inputStream.available());
url = new String(bc);
} catch (SocketTimeoutException ex) {
System.out.println("Read timed out");
} catch (IOException e) {
new ExceptionHandleImpl().cantHandlePath("连接超时");
return null;
}
Request httpMessage = null;
if (url != null) {
httpMessage = new Request(url,socket);
}
return httpMessage;
}
再看一下,读到的请求,
读到的请求是这样
GET /BackServlet HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.79 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
我们如何提取到我们需要的信息,并封装为request呢?看下面
package com.zjg.tomdog.util;
import com.zjg.tomdog.handlecore.ExceptionHandle;
import javax.sound.midi.Soundbank;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
/**
* @author zjg
* @date 2018/3/26 17:02
* @Description 接收到的http信息
*/
public class Request {
private String protocol;//协议类型
private String data;//请求数据
private String method;//请求方法(post/get)
private Map<String , String> attrs; //参数
private Socket socket;
/**
* 请求方法
* @param protocol 协议
* @param data 请求数据
* @param method 请求方法
*/
public Request(String protocol, String data, String method) {
this.protocol = protocol;
this.data = data;
this.method = method;
}
/**
* 请求方法
* @param request 完整的url
*/
public Request(String request,Socket socket) {
this.getRequestMess(request);
this.socket = socket;
}
/**
* 得到request信息
* @param request request url
*/
private void getRequestMess (String request) {
String [] methodUrlPro = request.split("\n")[0].split(" ");
if (methodUrlPro.length == 3) {
this.method = methodUrlPro[0];
this.protocol = methodUrlPro[2];
String [] urlAttr = methodUrlPro[1].split("\\?");//分隔参数和url
this.data = urlAttr[0].substring(1,urlAttr[0].length());
String [] attrArray , keyValue;//所有参数,每个参数的参数名和值
if (urlAttr.length > 1) {
attrArray = urlAttr[1].split("&");
for (String s : attrArray) {
keyValue = s.split("=");
this.addAttrs(keyValue[0],keyValue[1]);
}
}
}
System.out.println("request message:"+request);
}
/**
* 添加参数
* @param key 参数名
* @param value 值
*/
private void addAttrs(String key,String value) {
if (attrs == null) {
attrs = new HashMap<String , String>();
}
this.attrs.put(key,value);
}
public void setProtocol (String protocol) {
this.protocol = protocol;
}
public String getProtocol() {
return protocol;
}
public void setData(String data) {
this.data = data;
}
public String getData() {
return data;
}
public void setMethod(String method) {
this.method = method;
}
public String getMethod() {
return method;
}
public Map<String, String> getAttrs() {
return attrs;
}
public void setAttrs(Map<String, String> attrs) {
this.attrs = attrs;
}
public Socket getSocket() {
return socket;
}
public void setSocket(Socket socket) {
this.socket = socket;
}
@Override
public String toString() {
return "\n{ \nprotocol: "+protocol+"\ndata: "+data+"\nmethod: "+method+"\nattrs:"+attrs+"\n}";
}
}
根据请求信息之前的分割符分割开,存放进request,
读请求就结束了,看一下处理请求
@Override
public void forword(Socket socket, Request requestMessage) {
boolean isStatic = new JudgeSource().judgeSourceType(requestMessage.getData());
String path = null;
if (!isStatic) {
Map<Class<?> , String> allServlet = MainApp.servlets;//所有的servlet映射
String requestData = requestMessage.getData();//请求信息中的具体请求serlvet
String requestMethod = requestMessage.getMethod();//请求方法
boolean isExists = false;//是否存在这个servlet
try {
Class<?> clazz = null;
Method method = null;
Response response = new Response();//响应对象
for (Map.Entry<Class<?> , String> servlets: allServlet.entrySet()) {//循环判断,因为存放的是类的包名,所以不能由key直接找到映射信息
String servletName = servlets.getKey().getSimpleName();
if (requestData.equals(servletName)) {
isExists = true;
clazz = Class.forName(servlets.getKey().getName());//反射实例化servlet对象
if ("GET".equals(requestMethod)) { //判断请求方式
method = clazz.getMethod("doGet",Request.class,Response.class);
} else if ("POST".equals(requestMethod)) {
method = clazz.getMethod("doPost",Request.class,Response.class);
}
assert method != null;
method.invoke(clazz.newInstance(),requestMessage,response);//调用方法
}
}
if (!isExists) {
path = exceptionHandle.noFoundPath(requestData);//请求的类名
}
} catch (NoSuchMethodException e) {
path = exceptionHandle.cantHandlePath("NoSuchMethodException: 不存在这样的方法");
} catch (IllegalAccessException |InstantiationException | InvocationTargetException e) {
path = exceptionHandle.cantHandlePath("反射机制使用异常");
} catch (ClassNotFoundException e) {
path = exceptionHandle.noFoundPath("ClassNotFoundException: 反射机制加载类失败,该类不存在");
}
} else {
// System.out.println("静态资源");
path = requestMessage.getData();
}
if (path != null) {
new Response().write(socket,path);
}
}
先判断请求资源类型,如果是动态资源,那么就寻找是否存在这个servlet,存在就反射调用,不存在就报404,未找到。如果是静态资源,直接写回客户端。
看一下其中一个servlet,然后看下运行效果图
package com.zjg.tomdog.controller;
import com.zjg.tomdog.annotations.WebService;
import com.zjg.tomdog.servletdefine.HttpServlet;
import com.zjg.tomdog.util.Request;
import com.zjg.tomdog.util.Response;
/**
* @author zjg
* @date 2018/4/9 21:26
* @Description
*/
@WebService
public class BackServlet implements HttpServlet{
@Override
public void doGet(Request request , Response response) {
System.out.println("do get ... do something ...... tologin");
response.write(request.getSocket(),"index.html");
}
@Override
public void doPost(Request request , Response response) {
System.out.println("do post ...... do something");
response.write(request.getSocket(),"homepage.html");
}
}
当访问http://127.0.0.1/BackServlet
时,看一下运行效果,后台效果如下,前台能正常显示
至此所有的地方都展示和讲解完了,如果有什么问题可以留言讨论。