这里分析一个实际的请求是如何在Tomcat中被处理的,以及最后是怎么样找到要处理的Servlet的?当我们在浏览器中输入http://hostname:port/contextPath/servletPath,前面的hostname与port用于建立tcp连接,由于Http也是基于Tcp协议的,所以这里涉及TCP连接的三次握手。后面的contextPath与servletPath则是与服务器进行请求的信息,contextPath指明了与服务器中哪个Context容器进行交互,服务器会根据这个URL与对应的Context容器建立连接,那么这个过程是如何实现的呢?

在Tomcat7(本文也是基于Tomcat7)中主要通过一个映射来完成的,这个映射的工作交给org.apache.tomcat.util.http.mapper.Mapper类来完成的,这个类保存了Container容器所有子容器的信息,在请求从Connector交给Container容器之前,Mapper会根据hostname和port将host容器与context容器设置到Request的mappingData属性中,这样在Connector的请求进入Container容器之前就知道了交给哪个容器了。

  1. // Virtual host mapping
  2. if (mappingData.host == null) {
  3. Host[] hosts = this.hosts;
  4. int pos = findIgnoreCase(hosts, host);
  5. if ((pos != -1) && (host.equalsIgnoreCase(hosts[pos].name))) {
  6. mappingData.host = hosts[pos].object;
  7. contexts = hosts[pos].contextList.contexts;
  8. nesting = hosts[pos].contextList.nesting;
  9. } else {
  10. if (defaultHostName == null) {
  11. return;
  12. }
  13. pos = find(hosts, defaultHostName);
  14. if ((pos != -1) && (defaultHostName.equals(hosts[pos].name))) {
  15. mappingData.host = hosts[pos].object;
  16. contexts = hosts[pos].contextList.contexts;
  17. nesting = hosts[pos].contextList.nesting;
  18. } else {
  19. return;
  20. }
  21. }
  22. }
  23. // Context mapping
  24. if (mappingData.context == null) {
  25. int pos = find(contexts, uri);
  26. if (pos == -1) {
  27. return;
  28. }
  29. int lastSlash = -1;
  30. int uriEnd = uri.getEnd();
  31. int length = -1;
  32. boolean found = false;
  33. while (pos >= 0) {
  34. if (uri.startsWith(contexts[pos].name)) {
  35. length = contexts[pos].name.length();
  36. if (uri.getLength() == length) {
  37. found = true;
  38. break;
  39. } else if (uri.startsWithIgnoreCase("/", length)) {
  40. found = true;
  41. break;
  42. }
  43. }
  44. if (lastSlash == -1) {
  45. lastSlash = nthSlash(uri, nesting + 1);
  46. } else {
  47. lastSlash = lastSlash(uri);
  48. }
  49. uri.setEnd(lastSlash);
  50. pos = find(contexts, uri);
  51. }
  52. uri.setEnd(uriEnd);
  53. if (!found) {
  54. if (contexts[0].name.equals("")) {
  55. context = contexts[0];
  56. }
  57. } else {
  58. context = contexts[pos];
  59. }
  60. if (context != null) {
  61. mappingData.contextPath.setString(context.name);
  62. }
  63. }
  64. if (context != null) {
  65. ContextVersion[] contextVersions = context.versions;
  66. int versionCount = contextVersions.length;
  67. if (versionCount > 1) {
  68. Object[] contextObjects = new Object[contextVersions.length];
  69. for (int i = 0; i < contextObjects.length; i++) {
  70. contextObjects[i] = contextVersions[i].object;
  71. }
  72. mappingData.contexts = contextObjects;
  73. }
  74. if (version == null) {
  75. // Return the latest version
  76. contextVersion = contextVersions[versionCount - 1];
  77. } else {
  78. int pos = find(contextVersions, version);
  79. if (pos < 0 || !contextVersions[pos].name.equals(version)) {
  80. // Return the latest version
  81. contextVersion = contextVersions[versionCount - 1];
  82. } else {
  83. contextVersion = contextVersions[pos];
  84. }
  85. }
  86. mappingData.context = contextVersion.object;
  87. mappingData.contextSlashCount = contextVersion.slashCount;
  88. }
  89. // Wrapper mapping
  90. if ((contextVersion != null) && (mappingData.wrapper == null)) {
  91. internalMapWrapper(contextVersion, uri, mappingData);
  92. }

下面启动服务器,在浏览器中输入http://localhost:8080/examples/jsp/jsp2/el/composite.jsp,断点调试可以mappingData.host属性为localhost,mappingData.contextPath.setString(context.name)中context.name为examples,mappingData.wrapperPath为/jsp/jsp2/el/composite.jsp,这验证了mappingData属性的有效性,那么mappingData属性是如何设置到Request对象的属性中的呢?

通过org.apache.catalina.connector.Request的源码可以知道,其是通过setContextPath方法与setHost方法设置进去的,其源码如下:

  1. public void setHost(Host host) {
  2. mappingData.host = host;
  3. }
  4. public void setContextPath(String path) {
  5. if (path == null) {
  6. mappingData.contextPath.setString("");
  7. } else {
  8. mappingData.contextPath.setString(path);
  9. }
  10. }

由于请求是从Connector传过来的,而CoyoteAdapter是Connector中处理请求的最后一个类,那么设置这两个属性的代码肯定在CoyoteAdapter类中,果不其然:

  1. // This will map the the latest version by default
  2. connector.getMapper().map(serverName, decodedURI, version,
  3. request.getMappingData());
  4. request.setContext((Context) request.getMappingData().context);
  5. request.setWrapper((Wrapper) request.getMappingData().wrapper);
  6. //Mapper的map方法
  7. public void map(MessageBytes host, MessageBytes uri, String version,
  8. MappingData mappingData)
  9. throws Exception {
  10. if (host.isNull()) {
  11. host.getCharChunk().append(defaultHostName);
  12. }
  13. host.toChars();
  14. uri.toChars();
  15. internalMap(host.getCharChunk(), uri.getCharChunk(), version,
  16. mappingData);
  17. }

intenalMap方法执行就是代码清单5-4的内容,这样就把从Connector传入请求,并设置Request对象的mappingData属性的整个流程就打通了。还有一个疑问是为什么Mapper类中可以拥有Container所有子容器的信息呢?答案需要回到Tomcat启动过程图的第21步的startIntenal方法了:

  1. public void startInternal() throws LifecycleException {
  2. setState(LifecycleState.STARTING);
  3. // Find any components that have already been initialized since the
  4. // MBean listener won't be notified as those components will have
  5. // already registered their MBeans
  6. findDefaultHost();
  7. Engine engine = (Engine) connector.getService().getContainer();
  8. addListeners(engine);
  9. Container[] conHosts = engine.findChildren();
  10. for (Container conHost : conHosts) {
  11. Host host = (Host) conHost;
  12. if (!LifecycleState.NEW.equals(host.getState())) {
  13. // Registering the host will register the context and wrappers
  14. registerHost(host);
  15. }
  16. }
  17. }

这段代码就是将MapperListener作为一个监听者加到整个Container容器的每一个子容器中,这样任何一个子容器发生变化,MapperListener都将被通知,响应的mappingData属性也会改变。最后可以总结访问请求地址为http://localhost:8080/examples/composite.jsp的处理过程:

  1. 在端口8080启动Server,并通知Service完成启动,Service通知Connector完成初始化和启动的过程
  2. Connector首先收到这个请求,会调用ProtocolHandler完成http协议的解析,然后交给SocketProcessor处理,解析请求头,再交给CoyoteAdapter解析请求行和请求体,并把解析信息封装到Request和Response对象中
  3. 把请求(此时应该是Request对象,这里的Request对象已经封装了Http请求的信息)交给Container容器
  4. Container容器交给其子容器——Engine容器,并等待Engine容器的处理结果
  5. Engine容器匹配其所有的虚拟主机,这里匹配到Host
  6. 请求被移交给hostname为localhost的Host容器,host匹配其所有子容器Context,这里找到contextPath为/examples的Context容器。如果匹配不到就把该请求交给路径名为”“的Context去处理
  7. 请求再次被移交给Context容器,Context继续匹配其子容器Wrapper,由Wrapper容器加载composite.jsp对应的servlet,这里编译的servlet是basic_002dcomparisons_jsp.class文件
  8. Context容器根据后缀匹配原则*.jsp找到composite.jsp编译的java类的class文件
  9. Connector构建一个org.apache.catalina.connector.Request以及org.apache.catalina.connector.Response对象,使用反射调用Servelt的service方法
  10. Context容器把封装了响应消息的Response对象返回给Host容器
  11. Host容器把Response返回给Engine容器
  12. Engine容器返回给Connector
  13. Connetor容器把Response返回给浏览器
  14. 浏览器解析Response报文
  15. 显示资源内容

其中的映射关系是由MapperListener类完成的。


原文博主地址:rhwayfunn

Tomcat 详解URL请求的更多相关文章

  1. TOMCAT原理详解及请求过程(转载)

    转自https://www.cnblogs.com/hggen/p/6264475.html TOMCAT原理详解及请求过程 Tomcat: Tomcat是一个JSP/Servlet容器.其作为Ser ...

  2. 详解SpringMVC请求的时候是如何找到正确的Controller

    详解SpringMVC请求的时候是如何找到正确的Controller[附带源码分析] 目录 前言 源码分析 重要接口介绍 SpringMVC初始化的时候做了什么 HandlerExecutionCha ...

  3. Tomcat详解及SNS系统的部署实现

    Tomcat详解及SNS系统的部署实现   http://jungege.blog.51cto.com/4102814/1409290

  4. Tomcat详解系列(1) - 如何设计一个简单的web容器

    Tomcat - 如何设计一个简单的web容器 在学习Tomcat前,很多人先入为主的对它的认知是巨复杂的:所以第一步,在学习它之前,要打破这种观念,我们通过学习如何设计一个最基本的web容器来看它需 ...

  5. Tomcat详解系列(2) - 理解Tomcat架构设计

    Tomcat - 理解Tomcat架构设计 前文我们已经介绍了一个简单的Servlet容器是如何设计出来,我们就可以开始正式学习Tomcat了,在学习开始,我们有必要站在高点去看看Tomcat的架构设 ...

  6. ASP.NET 运行时详解 揭开请求过程神秘面纱

    对于ASP.NET开发,排在前五的话题离不开请求生命周期.像什么Cache.身份认证.Role管理.Routing映射,微软到底在请求过程中干了哪些隐秘的事,现在是时候揭晓了.抛开乌云见晴天,接下来就 ...

  7. day08:软件系统的体系结构&Tomcat详解&Web应用&http协议

        day08 软件系统体系结构     常见软件系统体系结构B/S.C/S 1.1 C/S C/S结构即客户端/服务器(Client/Server),例如QQ: 需要编写服务器端程序,以及客户端 ...

  8. Tomcat详解系列(3) - 源码分析准备和分析入口

    Tomcat - 源码分析准备和分析入口 上文我们介绍了Tomcat的架构设计,接下来我们便可以下载源码以及寻找源码入口了.@pdai 源代码下载和编译 首先是去官网下载Tomcat的源代码和二进制安 ...

  9. Linux 安装 Tomcat 详解

    说明:安装的 tomcat 为解压版(即免安装版):apache-tomcat-8.5.15.tar.gz (1)使用 root 用户登录虚拟机,在根目录下的 opt 文件夹新建一个 software ...

随机推荐

  1. redis基础-Remote Dictionary Server

    Redis支持多个数据库,并且每个数据库的数据是隔离的不能共享,并且基于单机才有,如果是集群就没有数据库的概念. Redis默认支持16个数据库(可以通过配置文件支持更多,无上限),可以通过配置dat ...

  2. Sqoop(一)安装及基本使用

    Sqoop:     1.sqoop从数据库中导入数据到HDFS     2.SQOOP从数据库导入数据到hive     3.sqoop从hive中将数据导出到数据库   sqoop底层还是执行的m ...

  3. 2021年了,C 语言会被淘汰吗?

    一年365天,总有那么几百天听到有人说"C语言过时了""C语言要被时代淘汰了",那么真的会被淘汰吗? C 语言发布于 1972 年,到2021年已经有49年的历 ...

  4. linux IP 注释

    DEVICE=name,这里name是物理设备的名字(动态分配的PPP设备应当除外,它的名字是"逻辑名". IPADDR=addr, 这里addr是IP地址. NETMASK=ma ...

  5. Blazor VS 传统Web应用程序

    原文作者: Christian Findlay 原文链接: https://christianfindlay.com/2020/07/09/blazor-vs-traditional-web-apps ...

  6. ICPC Central Russia Regional Contest (CRRC 19)题解

    题目连接:https://codeforces.com/gym/102780 寒假第二次训练赛,(某菜依旧是4个小时后咕咕咕),战况还行,个人表现极差(高级演员) A:Green tea 暴力枚举即可 ...

  7. 关于.NET中的控制反转(三)- 依赖注入之 Autofac

    一.Autofac简介 Autofac和其他容器的不同之处是它和C#语言的结合非常紧密,在使用过程中对你的应用的侵入性几乎为零,更容易与第三方的组件集成.Autofac的主要特性如下: 组件侵入性为零 ...

  8. JavaScript 获取当天0点以及当前时间方法

    js 取得今天0点: const start = new Date(new Date(new Date().toLocaleDateString()).getTime()); console.log( ...

  9. 【C++】《Effective C++》第九章

    杂项讨论 条款53:不要轻忽编译器的警告 请记住 严肃对待编译器发出的警告信息.努力在你的编译器的最高(最严苛)警告级别下争取"无任何警告"的容易. 不要过度依赖编译器的报警能力, ...

  10. 为linux添加一块新硬盘并分区

    一---如何增加一块硬盘1:虚拟机添加硬盘2:分区3:格式化4:挂载5:设置可以自动挂载 1---设置里面 2---分区命令 fdisk /dev/sdb开始分区m显示命令列表p显示磁盘分区 同fdi ...