做了长时间web开发,一直都是用spring,导致自己成了操作工,按照既定的模子,重复的劳动,没有丝毫的进步,所以想深入的了解一番干了这么长时间的web的整个运行流程,绝大多数web开发学习应该都是servlet开始的吧,所以又重拾了servelt狠狠的研究了一番,最后发现servlet其实就是些标准,那啥为标准,说白了,就是定了些接口,导致看源码的过程很不过瘾,感觉没啥提升,就决定了解下更底层的工作原理,也就是tomcat。
开始我并不知道tomcat是java写的,对tomcat那是神秘,惧怕,感觉太过高大上,自己做的那点web开发和tomcat这种红遍全球的软件相比简直小巫见大巫,技术含量压根儿就不是一个层次。不过我还是想一窥究竟,偶然的机会,看到了《深入剖析Tomcat》,作者对tomcat源码讲解的很是详细,通过循序渐进的例子,非常认真的讲出了tomcat的精髓,我是受益匪浅。现在我想自己也写一个,来作为这阶段学习的一个交代,就叫tiny-tomcat吧,同样通过循序渐进,慢慢完善,源码在github上:https://github.com/esiyuan/tiny-tomcat.git
简单的web服务器
git checkout step-001:主要功能如下
- 接受http请求
private void handlerRequest() throws IOException {
while(true) {
Socket socket = serverSocket.accept();
logger.info("获取连接[ address: " + socket.getInetAddress() + ", port: " + socket.getPort() + " ]");
Request request = new Request(socket.getInputStream());
request.parse();
Response response = new Response(request.getUri(), socket.getOutputStream());
response.sendStaticResource();
socket.close();
}
}
通过ServerSocket监听本地端口,serverSocket.accept()等待请求。
- 解析请求uri
public void parse() throws IOException {
BufferedReader reader = IOUtil.getBufferedReader(inputStream);
String requestString = reader.readLine();
this.uri = parseUri(requestString);
}
为了分开请求和响应,贴合servlet风格,我们通过Request对象,解析请求字符串,request对象构造的时候,传入socket的输入流,由于我们只对第一行感兴趣,所以在只读取了第一行进行解析,解析出uri作为属性保存。
- 根据uri定位html文件,并返回响应
最后通过 Response对象进行html响应,而response在构造的时候,传入socket的输出流。
/**
* 输出响应
* <p>文件找到uri对应的文件,则进行输出,否则输出404
* @throws IOException
*/
public void sendStaticResource() throws IOException {
File file = new File(Constants.WEB_ROOT, uri);
if (file.exists()) {
sendFile(file);
} else {
sendDefault();
}
}
简单的servlet容器
git checkout step-002
主要在前一节代码上进行了部分改造,增加了响应servlet的功能,并分出了简单的servlet处理器,因为HttpServletRequest和HttpServletResponse是接口,为了达到演示的效果,避免request和response类过于复杂,使用了门面RequestFacade和ResponseFacade。
public class ServletProcessor {
private ConcurrentHashMap<String, Servlet> cache = new ConcurrentHashMap<String, Servlet>();
/**
* servlet没有加载,则加载并初始化
* <p>如有加载直接取缓存
* @param request
* @param response
*/
public void process(Request request, Response response){
Servlet servlet = cache.get(request.getServerName());
if(servlet != null) {
try {
servlet.service(request, response);
} catch (ServletException | IOException e) {
e.printStackTrace();
}
return;
}
try(URLClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file://" + Constants.TOMCAT_CLASSLOADER_REPOSITORY)});) {
Class<?> clazz = classLoader.loadClass("web_root." + request.getServerName());
servlet = (Servlet)clazz.newInstance();
servlet.init(null);
servlet.service(request, response);
cache.putIfAbsent(request.getServerName(), servlet);
} catch (Exception e) {
e.printStackTrace();
System.out.println("服务异常!");
}
}
}
为了体现servlet的生命周期,增加了init()方法,并且通过ConcurrentHashMap缓存了sevlet实例,为什么用ConcurrentHashMap,主要是考虑线程安全,其分段锁的设计,能够拥有较高的并发写的能力,同时并不会妨碍并发读取,由于读没有进行加锁,不需要等待写完成,所有必然的会造成数据的弱一致性,不过大多数时候都会有取出非空判断逻辑,也造不成什么问题。
servlet代码如下:
public class HelloServlet extends HttpServlet {
private static final long serialVersionUID = -2585140950753353037L;
private static final Logger logger = Logger.getLogger(HelloServlet.class);
public void service(ServletRequest request, ServletResponse response)
throws ServletException, IOException {
PrintWriter out = response.getWriter();
out.println("hello servlet...");
}
@Override
public void init() throws ServletException {
System.out.println("HelloServlet..初始化开始..");
}
}
运行结果:
非阻塞的servlet容器
git checkout step-003
前面的实现,都是非常简单的单线程,万一线程阻塞,那岂不是不能提供服务了,实际的产品肯定不能是这样的。tomcat连接器监听端口,获取创建socket,这个应该是单点,而容易阻塞的地方,应该是具体业务逻辑处理的代码了,为了保证服务的可用,提高系统的吞吐率,可以使用多线程提供多个处理器,让每个请求有单独的线程进行处理,这样性能会大幅度提高,不会以为单个连接出现问题而造成服务不可用。下面我们分析具体的代码。
Bootstrap应用启动,主要功能,初始化连接器:
public final class Bootstrap {
public static void main(String[] args) {
HttpConnector connector = new HttpConnector("localhost", 8080);
try {
connector.initialize();
connector.start();
System.in.read(); //连接器线程,让主线程挂起,保证其他后台线程运行
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (TomcatException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
连接器进行本地端口的监听,实例化并运行处理器:
/**
* 初始化连接处理器和处理器
* @throws TomcatException
* @throws UnknownHostException
* @throws IOException
*/
public void initialize() throws TomcatException, UnknownHostException, IOException {
createServer();
while (curProcessors < minProcessors) {
if (curProcessors >= maxProcessors)
break;
HttpProcessor processor = newProcessor();
recycle(processor);
}
}
处理器进入wait状态(HttpProcessor中方法),等待被唤醒。
@Override
public void run() {
try {
while(!Thread.interrupted()) {
Socket socket = waitingToNewSocket();
process(socket);
}
} catch (InterruptedException | IOException | ServletException e) {
System.out.println("处理器异常。。。");
}
}
private synchronized Socket waitingToNewSocket() throws InterruptedException {
while (!newSocketCome) {
wait();
}
Socket socket = this.socket;
newSocketCome = false;
return (socket);
}
当有请求到来时,唤醒挂起的处理器,处理 完成,处理器重新入栈,等待下次被使用。
public synchronized void assign(Socket socket) {
this.socket = socket;
newSocketCome = true;
notifyAll();
}
模块化的web容器
git checkout step-004
tomcat作为一个大型的产品,为了开发维护的方便,必然的会把大任务进行分解,进行分层分模块,这也是如今软件设计的思路,模块清晰,层次明了,对于后期的维护,更新都会有极大的便利。
tomcat进行功能的拆分,模拟管道与阀的思想,对于容器的处理组件,都放在阀中,请求会像流水一样流过每个阀,最后得到最终的处理。
而tomcat通过接口来定下标准,Pipeline接口表示管道,Valve表示阀,具体可以看代码。
生命周期监听
git checkout step-005
本节主要增加了生命周期控制组件,接口Lifecycle,定义组件的生命周期,主要增加了事件监听模块,监听tomcat启动状态改变,并作出反应,下图监听的逻辑。
每个实现生命周期接口的组件都能进行监听器的绑定,统一的实现监听器的控制逻辑,使用了LifecycleSupport来代理,实现监听器绑定,解绑,通知。