做了长时间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
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(); } }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..初始化开始.."); } }运行结果:
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(); }git checkout step-004 tomcat作为一个大型的产品,为了开发维护的方便,必然的会把大任务进行分解,进行分层分模块,这也是如今软件设计的思路,模块清晰,层次明了,对于后期的维护,更新都会有极大的便利。 tomcat进行功能的拆分,模拟管道与阀的思想,对于容器的处理组件,都放在阀中,请求会像流水一样流过每个阀,最后得到最终的处理。
而tomcat通过接口来定下标准,Pipeline接口表示管道,Valve表示阀,具体可以看代码。
git checkout step-005 本节主要增加了生命周期控制组件,接口Lifecycle,定义组件的生命周期,主要增加了事件监听模块,监听tomcat启动状态改变,并作出反应,下图监听的逻辑。 每个实现生命周期接口的组件都能进行监听器的绑定,统一的实现监听器的控制逻辑,使用了LifecycleSupport来代理,实现监听器绑定,解绑,通知。
