记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查

时间:2024-03-28 09:26:35

关于tomcat的apr、bio、nio模式

关于tomcat的这几种模式,以及在springboot内嵌tomcat的方式下如何设置,可以参考以下几篇文章进行设置。
tomcat bio nio apr 模式性能测试与个人看法
Spring Boot切换为APR模式
TOMCAT开启APR模式

首先将springboot应用程序跑起来,用postman随便访问一个接口可以看到打印出来的记录里显示出是基于nio模式的
记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查

接下来我们参考上面的这些文章,将springboot程序设置为Apr模式运行。关键的几个步骤见下面的
注意启用apr模式之前,相关的apr要安装。具体安装方法很简单,见这篇文章 [Tomcat中如何配置使用APR],为了方便懒人,直接截图过来了,(https://blog.csdn.net/elinespace/article/details/53017967)。感谢原作者。
记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查
附图片中的apr下载路径apr下载路径
设置tomcat模式为 apr 模式。这一步都是so easy
记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查

好了,一切都已经搞定了。应用程序跑起来就是这个样子的了
记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查
已经根据Apr模式启动了。一切看起来都很OK。走到这一步,基本上都没啥问题了。

接下来让人郁闷的事情来了

问题现象

用POSTMAN 访问127.0.0.1 没有任何响应!见下图

记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查

因为只改动了这个protocol设置。那么注销掉下图红色这一行的设置(这行代码是设置protocol模式为 spring.server.protocol=org.apache.coyote.http11.Http11AprProtocol 的)。

记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查
注销掉红色这部分的代码,发现应用以nio模式启动,正常接收和响应数据。
说明问题的确出在apr模式上。但是为什么apr模式会有问题,nio模式没有问题呢?

问题描述

1.nio模式没有问题,可以正常响应和输出数据。只要切换到apr模式就会不响应。内心是这样的
记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查

分析思路
1.请求数据发过去了没有,tomcat有没有接收到
2.端口有没有绑定(做过socket编程的小伙伴们就有了解了,socket需要create、bind、accept、listen之类的操作)有没有正常监听?

因为我这个springboot应用程序指定的端口是8102。用cmd netstat命令查看了一下绑定的端口
这个是在apr模式下:
记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查
可以看到8102端口有在正常的listening状态。但是ip地址有些诡异。
再切换回nio模式
记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查

看出区别了没有?
在NIO模式下,会有两个地址绑定,一个是IP4,一个是IP6。而在APR模式下只有一个IP6的地址绑定。那么问题就在于tomcat的地址绑定上。为什么NIO模式会有IP4和IP6,而APR模式只有IP6呢?导致用本机地址127.0.0.1无法访问。

于是穷追不舍的我就跟到了springboot内嵌的tomcat源码里面去了。手动滑稽。

nio模式 bind相关的代码

这个是NIO模式的bind函数,在这个路径
C:\Users\Administrator.gradle\caches\modules-2\files-2.1\org.apache.tomcat.embed\tomcat-embed-core\8.5.31\5ade5e787bcaabb9d088d8e4b22a8edffa43a783\tomcat-embed-core-8.5.31-sources.jar!\org\apache\tomcat\util\net\NioEndpoint.java
绑定的 0.0.0.0 地址
记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查

它的地址是怎么来的呢??

仔细看下它获取ip地址的业务逻辑是一个三目运算

InetSocketAddress addr = 
(getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));

如果没有指定地址,呢么就

new InetSocketAddress(getPort()))

跟进去看
C:\Program Files\Java\jdk1.8.0_151\src.zip!\java\net\InetSocketAddress.java

记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查

主要调用了一个anyLocalAddress 字面意思理解就是任意的物理地址

记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查
anyLocalQAddress地址就是 0.0.0.0
记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查
所以,综上所述,在NIO模式下,不需要指定地址,它会自动默认一个 0.0.0.0的地址

apr模式 bind相关的代码

这个是APR模式的bind函数,在这个路径
C:\Users\Administrator.gradle\caches\modules-2\files-2.1\org.apache.tomcat.embed\tomcat-embed-core\8.5.31\5ade5e787bcaabb9d088d8e4b22a8edffa43a783\tomcat-embed-core-8.5.31-sources.jar!\org\apache\tomcat\util\net\AprEndpoint.java
记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查
我们可以看到,它调用 getAddress() ,因为没有手动设置address地址,所以返回值是空。接下来的addressStr也是空的。然后在后面的绑定中也是基于这个参数去绑定。自然是没有地址了。

记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查

断点走过了listen之后,cmd命令去看,就已经绑定了端口。但是没有监听地址
记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查

结果分析

1.在NIO模式下没有指定地址的情况,会调用
new InetSocketAddress(getPort())) 去获取一个 anyLocalAddress (这个参数启动时候会被初始化为 0.0.0.0),所以NIO模式下不用指定地址即可绑定。
2.在apr模式下没有指定地址的情况,会继续往前执行,看这个
Address.info函数,传入的addressStr参数是空的。
记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查
Address.info是一个native函数,也就是jni调用。对接到系统层了,无法源码调试。但是看函数的注释,也很清楚,如果没有地址没有指定,它会给你返回一个 0.0.0.0 或 :: 也就是我们看到的绑定的地址是 ::

记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查

真相大白了。没有指定地址的情况下,的确是会bind到 :: 这个地址。

解决方案

在apr配置中手工指定地址。
记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查
记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查

再次启动,tomcat绑定地址
记一次springboot2.x 内置tomcat在apr模式下无法访问的问题排查

apr模式启动且接口产生响应。

总结

解决问题后,我没有非常深入去探究为什么没有指定address的情况下,它绑定的地址会返回 :: (是不是哪个参数没有设置?跟我本机安装了vmware产生多个虚拟网卡有没有关系?跟打开ipv6有没有关系?跟内置tomcat默认设置有没有关系等)
大概的追踪了一下tomcat源码,学到了很多东西,也找到了关键问题,记录一下,希望对遇到同样问题的伙伴们有所帮助