SpringBoot Tomcat 基础配置

2023年 9月 28日 141.5k 0

ServerProperties 配置类

SpringBoot Tomcat 配置封装在 ServerProperties 类中,yml 配置文件中的前缀为 server。

@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {

    /**
     * Server HTTP port.
     */
    private Integer port;

    /**
     * Network address to which the server should bind.
     */
    private InetAddress address;

    @NestedConfigurationProperty
    private final ErrorProperties error = new ErrorProperties();

    /**
     * Strategy for handling X-Forwarded-* headers.
     */
    private ForwardHeadersStrategy forwardHeadersStrategy;

    /**
     * Value to use for the Server response header (if empty, no header is sent).
     */
    private String serverHeader;

    /**
     * Maximum size of the HTTP message header.
     */
    private DataSize maxHttpHeaderSize = DataSize.ofKilobytes(8);

    /**
     * Type of shutdown that the server will support.
     */
    private Shutdown shutdown = Shutdown.IMMEDIATE;

    @NestedConfigurationProperty
    private Ssl ssl;

    @NestedConfigurationProperty
    private final Compression compression = new Compression();

    @NestedConfigurationProperty
    private final Http2 http2 = new Http2();

    private final Servlet servlet = new Servlet();

    private final Reactive reactive = new Reactive();

    private final Tomcat tomcat = new Tomcat();

    private final Jetty jetty = new Jetty();

    private final Netty netty = new Netty();

    private final Undertow undertow = new Undertow();
    

Integer port:端口号

SpringBoot Web 进程启动监听的端口,在 TomcatServletWebServerFactory#customizeConnector(Connector connector) 方法中,调用 connector.setPort(port) 设置 Connector#port 属性。

另外可以看到,SpringBoot Tomcat 默认使用的 Protocol implementation 为 org.apache.coyote.http11.Http11NioProtocol。

public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory
		implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware {
                
	// The class name of default protocol used.
        // 默认使用的 http 协议为 http1.1,Tomcat 内部实现方式为 Nio
	public static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol";
	private String protocol = DEFAULT_PROTOCOL;
        
    @Override
    // 该方法的代码经过一定程度的精简
    public WebServer getWebServer(ServletContextInitializer... initializers) {
        Tomcat tomcat = new Tomcat();
        Connector connector = new Connector(this.protocol);
        connector.setThrowOnFailure(true);
        tomcat.getService().addConnector(connector);
        // 设置 connector 的属性
        customizeConnector(connector);
        tomcat.setConnector(connector);
        return getTomcatWebServer(tomcat);
    }
    
    // Needs to be protected so it can be used by subclasses
    protected void customizeConnector(Connector connector) {
        // 获取端口,设置 connector 的端口
        int port = Math.max(getPort(), 0);
        connector.setPort(port);
        // ...
    }

在 Apache Tomcat 中,Connector 是用于处理客户端与服务器之间的网络连接的组件。Connector 负责监听网络端口、接受客户端请求,并将请求传递给 Tomcat 服务器进行处理。Tomcat 支持多种类型的 Connector,比如:HTTP Connector、AJP Connector、WebSocket Connector、JMX(Java Management Extensions)Connector 等。

Tomcat#setConnector() 在 Service 中寻找已注册的 Connectors,如果已经注册过,则直接返回,如果未注册,则将 Connector 对象添加到 Service 中。

public class Tomcat {

    // Set the specified connector in the service, if it is not already present.
    public void setConnector(Connector connector) {
        Service service = getService();
        boolean found = false;
        for (Connector serviceConnector : service.findConnectors()) {
            if (connector == serviceConnector) {
                found = true;
                break;
            }
        }
        if (!found) {
            service.addConnector(connector);
        }
    }

Service 是个啥?Service 表示 Tomcat 的服务实例,它可以包含多个 Connector。每个 Service 实例都独立运行,监听一个或多个网络端口,并负责处理客户端的请求。

Connector 则是 Service 中用于处理特定协议的组件。它负责监听指定的端口,接受客户端请求,并将请求传递给 Tomcat 服务器进行处理。一个 Service 可以配置多个 Connector,以便支持不同的协议和连接方式。

// A Service is a group of one or more Connectors that share a single Container to process their incoming requests. 
// This arrangement allows, for example, a non-SSL and SSL connector to share the same population of web apps.
public interface Service extends Lifecycle {
    // the Engine that handles requests for all Connectors associated with this Service.
    public Engine getContainer();
    
    // the Server with which we are associated (if any).
    public Server getServer();

    // Add a new Connector to the set of defined Connectors, and associate it with this Service's Container.
    public void addConnector(Connector connector);

    // Find and return the set of Connectors associated with this Service.
    public Connector[] findConnectors();

顺着 Connector#setPort(int port) 方法追下去,发现 port 属性最终设置在 Connector#protocolHandler 成员变量上:IntrospectionUtils.setProperty(protocolHandler, name, value)。

ProtocolHandler 又是个啥?在 Apache Tomcat 中,Connector 和 ProtocolHandler 是两个相关但不同的组件,它们在处理客户端请求时发挥不同的角色。

ProtocolHandler 是用于处理底层协议的组件,例如 HTTP、AJP 等。它负责监听端口、接受客户端连接,并处理与客户端之间的通信。ProtocolHandler 实现了特定协议的处理逻辑,处理请求和响应的编解码、连接管理、流控制等。

Connector 则是 ProtocolHandler 的配置和管理器。它负责将特定的 ProtocolHandler 与 Tomcat 的服务实例关联起来,并提供配置选项来定义监听的端口、协议版本、连接池大小等。Connector 充当了 ProtocolHandler 和 Tomcat 之间的桥梁,提供了高级的配置选项和管理功能。

在 Tomcat 的架构中,一个 Connector 实例通常对应一个 ProtocolHandler 实例。通过配置 Connector,您可以指定要使用的 ProtocolHandler 类型,并对其进行额外的配置。

public class Connector extends LifecycleMBeanBase  {

    // Set the port number on which we listen for requests.
    public void setPort(int port) {
        setProperty("port", String.valueOf(port));
    }

    // Set a property on the protocol handler.
    public boolean setProperty(String name, String value) {
        if (protocolHandler == null) {
            return false;
        }
        return IntrospectionUtils.setProperty(protocolHandler, name, value);
    }

继续跟进到 ProtocolHandler 衍生出的一个子类 AbstractProtocol 中,AbstractProtocol 是一个抽象类,有一个 AbstractEndpoint endpoint 成员变量,endpoint 负责底层 low level 的网络 IO 协议:Endpoint that provides low-level network I/O。

AbstractProtocol 和 AbstractEndpoint 是一对一的关系,AbstractProtocol 中关于网络底层 IO 的方法基本上都交给 AbstractEndpoint 来做。

AbstractEndpoint endpoint 需要与 ProtocolHandler implementation 相匹配,比如 NIO ProtocolHandler 需要使用 NIO Endpoint,Http11NioProtocol 需要使用 NioEndpoint。

看了源码,如果把 Http11NioProtocol、NioEndpoint 和 TCP/IP 分层模型类似,Http11NioProtocol 可以类比为应用层,从 NioEndpoint 中取出数据数据,负责 SSL 和 HTTP 1.1 协议的解析,NioEndpoint 可以类比为传输层,以 NIO 的方式传输数据。

public abstract class AbstractProtocol implements ProtocolHandler,
        MBeanRegistration {
        
    // Endpoint that provides low-level network I/O - must be matched to the ProtocolHandler implementation (ProtocolHandler using NIO, requires NIO Endpoint etc.).
    private final AbstractEndpoint endpoint;
    
    public void setPort(int port) {
        endpoint.setPort(port);
    }
    
    public void setMaxConnections(int maxConnections) {
        endpoint.setMaxConnections(maxConnections);
    }
    
    public void setAcceptCount(int acceptCount) { endpoint.setAcceptCount(acceptCount); }
    
    public void setTcpNoDelay(boolean tcpNoDelay) {
        endpoint.setTcpNoDelay(tcpNoDelay);
    }
    
    public void setConnectionLinger(int connectionLinger) {
        endpoint.setConnectionLinger(connectionLinger);
    }
    
    public void setKeepAliveTimeout(int keepAliveTimeout) {
        endpoint.setKeepAliveTimeout(keepAliveTimeout);
    }
    
    public void setAddress(InetAddress ia) {
        endpoint.setAddress(ia);
    }
    
    public void setConnectionTimeout(int timeout) {
        endpoint.setConnectionTimeout(timeout);
    }

    public long getConnectionCount() {
        return endpoint.getConnectionCount();
    }
// NIO tailored thread pool, providing the following services:
// 1.Socket acceptor thread
// 2.Socket poller thread
// 3.Worker threads pool
public class NioEndpoint extends AbstractJsseEndpoint {
    // Server socket "pointer".
    private volatile ServerSocketChannel serverSock = null;

    // Bytebuffer cache, each channel holds a set of buffers (two, except for SSL holds four)
    private SynchronizedStack nioChannels;

public class Http11NioProtocol extends AbstractHttp11JsseProtocol {

    public Http11NioProtocol() {
        super(new NioEndpoint());
    }

终于,在 NioEndpoint#bind() --> NioEndpoint#initServerSocket() 方法中,调用 serverSock.bind(addr, getAcceptCount()) 方法绑定在指定的 InetSocketAddress addr 上,创建 InetSocketAddress addr 对象时:InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset()),getPortWithOffset() 就是我们在 yml 文件中配置的端口号。

public class NioEndpoint extends AbstractJsseEndpoint {

    @Override
    public void bind() throws Exception {
        initServerSocket();

        setStopLatch(new CountDownLatch(1));

        // Initialize SSL if needed
        initialiseSsl();
    }

    // Separated out to make it easier for folks that extend NioEndpoint to
    // implement custom [server]sockets
    protected void initServerSocket() throws Exception {
        if (getUseInheritedChannel()) {
            // ...
        } else if (getUnixDomainSocketPath() != null) {
            // ...
        } else {
            serverSock = ServerSocketChannel.open();
            socketProperties.setProperties(serverSock.socket());
            InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
            serverSock.bind(addr, getAcceptCount());
        }
        serverSock.configureBlocking(true); //mimic APR behavior
    }

image.png

InetAddress address:绑定 IP 地址

如果将 server.address 设置为 127.0.0.1,http://localhost:8080 和 http://127.0.0.1:8080 可以访问,但是用无线网卡的地址访问 http://192.168.1.6:8080 , 会显示拒绝访问。

相反将 server.address 设置为 192.168.1.6,用无线网卡的地址 http://192.168.1.6:8080 , 可以访问,但是用 http://localhost:8080 和 http://127.0.0.1:8080 ,会显示拒绝访问。

server:
  port: 8080
  address: 127.0.0.1

具体来说,server.address 属性的作用是确定服务器绑定的网络接口地址。当应用程序作为服务器运行时,它需要监听某个网络地址上的传入连接。通过配置 server.address,您可以指定服务器监听的具体网络地址。

  • 0.0.0.0:表示服务器将监听在所有可用的网络接口上,包括本地回环地址和所有网络接口。这是默认值,适用于大多数情况,允许从任何网络接口访问服务器。
  • 127.0.0.1:表示服务器仅监听在本地回环地址上,即只能通过本地访问服务器。这是在开发环境中常用的设置,用于限制对服务器的访问。
  • 具体的 IP 地址:您可以指定具体的 IP 地址,让服务器仅监听在该地址上。这对于服务器部署在多个网络接口的环境中,或者需要限制对服务器的访问的情况很有用。

源码跟踪:与 port 属性的调用链路一样,在 TomcatServletWebServerFactory#customizeProtocol() 方法中,调用 protocol.setAddress(getAddress()) 设置服务器监听的网络接口地址,最终设置到 AbstractEndpoint#address 属性上。

public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory
		implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware {

    // Needs to be protected so it can be used by subclasses
    protected void customizeConnector(Connector connector) {
        int port = Math.max(getPort(), 0);
        connector.setPort(port);
        if (StringUtils.hasText(getServerHeader())) {
            connector.setProperty("server", getServerHeader());
        }
        if (connector.getProtocolHandler() instanceof AbstractProtocol) {
           customizeProtocol((AbstractProtocol) connector.getProtocolHandler());
        }
        // ...
    }
    
    private void customizeProtocol(AbstractProtocol protocol) {
        if (getAddress() != null) {
           protocol.setAddress(getAddress());
        }
    }
    
public abstract class AbstractProtocol implements ProtocolHandler,
        MBeanRegistration {
    public void setAddress(InetAddress ia) {
        endpoint.setAddress(ia);
    }

配置 server.address=127.0.0.1 时,Springboot 仅监听本地回环地址。

image.png

配置 server.address=0.0.0.0 或者省略该配置时,Springboot 将监听在所有可用的网络接口上。

String serverHeader:Server response header

默认情况下,HTTP 响应中不会携带任何 Server 头,如果设置 server.server-header=oneby server,则访问 Tomcat,则每次响应都会携带 Server header,值为 oneby server。

server:
  server-header: oneby server

image.png

源码跟踪:在 TomcatServletWebServerFactory#customizeProtocol() 方法中,调用 connector.setProperty("server", getServerHeader()) 设置 Server header,最终设置在 AbstractHttp11Protocol#server 的属性上。

public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory
		implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware {

    // Needs to be protected so it can be used by subclasses
    protected void customizeConnector(Connector connector) {
        int port = Math.max(getPort(), 0);
        connector.setPort(port);
        if (StringUtils.hasText(getServerHeader())) {
            connector.setProperty("server", getServerHeader());
        }
        if (connector.getProtocolHandler() instanceof AbstractProtocol) {
           customizeProtocol((AbstractProtocol) connector.getProtocolHandler());
        }
        // ...
    }
    
public class Connector extends LifecycleMBeanBase  {

    public boolean setProperty(String name, String value) {
        if (protocolHandler == null) {
            return false;
        }
        return IntrospectionUtils.setProperty(protocolHandler, name, value);
    }

public abstract class AbstractHttp11Protocol extends AbstractProtocol {

    // Set the server header name.
    public void setServer(String server) {
        this.server = server;
    }

image.png

DataSize maxHttpHeaderSize:最大请求头大小

maxHttpHeaderSize 默认值为 DataSize.ofKilobytes(8),在 yml 配置文件中将 maxHttpHeaderSize 设置为 64 bytes。

server:
  max-http-header-size: 64B

访问 http 端点,程序抛出异常。

[2023-09-28 06:30:41.388] ERROR [http-nio-8080-exec-7] org.apache.coyote.http11.Http11Processor 175 [] [TID: N/A] - Error processing request
org.apache.coyote.http11.HeadersTooLargeException: An attempt was made to write more data to the response headers than there was room available in the buffer. Increase maxHttpHeaderSize on the connector or write less data into the response headers.
	at org.apache.coyote.http11.Http11OutputBuffer.checkLengthBeforeWrite(Http11OutputBuffer.java:473)
	at org.apache.coyote.http11.Http11OutputBuffer.write(Http11OutputBuffer.java:426)
	at org.apache.coyote.http11.Http11OutputBuffer.write(Http11OutputBuffer.java:412)
	at org.apache.coyote.http11.Http11OutputBuffer.sendHeader(Http11OutputBuffer.java:370)
	at org.apache.coyote.http11.Http11Processor.prepareResponse(Http11Processor.java:1065)
	at org.apache.coyote.AbstractProcessor.action(AbstractProcessor.java:379)
	at org.apache.coyote.Response.action(Response.java:211)
	at org.apache.coyote.Response.sendHeaders(Response.java:440)
	at org.apache.catalina.connector.OutputBuffer.doFlush(OutputBuffer.java:292)
	at org.apache.catalina.connector.OutputBuffer.close(OutputBuffer.java:252)
	at org.apache.catalina.connector.Response.finishResponse(Response.java:445)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:391)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:891)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1784)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)

DataSize 的单位(DataUnit)在配置文件中的写法:

image.png

源码跟踪:通过 TomcatWebServerFactoryCustomizer#customizeMaxHttpHeaderSize() 方法,最终设置到 AbstractHttp11Protocol#maxHttpHeaderSize 属性上。

public class TomcatWebServerFactoryCustomizer
		implements WebServerFactoryCustomizer, Ordered {
                
    private void customizeMaxHttpHeaderSize(ConfigurableTomcatWebServerFactory factory, int maxHttpHeaderSize) {
        factory.addConnectorCustomizers((connector) -> {
           ProtocolHandler handler = connector.getProtocolHandler();
           if (handler instanceof AbstractHttp11Protocol) {
              AbstractHttp11Protocol protocol = (AbstractHttp11Protocol) handler;
              protocol.setMaxHttpHeaderSize(maxHttpHeaderSize);
              for (UpgradeProtocol upgradeProtocol : protocol.findUpgradeProtocols()) {
                 if (upgradeProtocol instanceof Http2Protocol) {
                    ((Http2Protocol) upgradeProtocol).setMaxHeaderSize(maxHttpHeaderSize);
                 }
              }
           }
        });
    }
    
public abstract class AbstractHttp11Protocol extends AbstractProtocol {

    private int maxHttpHeaderSize = 8 * 1024;
    
    public int getMaxHttpHeaderSize() { return maxHttpHeaderSize; }
    
    public void setMaxHttpHeaderSize(int valueI) { maxHttpHeaderSize = valueI; }

在 Http11Processor 构造函数中,创建 Http11InputBuffer inputBuffer 对象时,设置 inputBuffer 的 headerSizeLimit 为 protocol.getMaxHttpRequestHeaderSize()。在执行 parseHeaders() 解析 http header 时就会抛出异常。

public class Http11Processor extends AbstractProcessor {

    public Http11Processor(AbstractHttp11Protocol protocol, Adapter adapter) {
        // ...
        // protocol.getMaxHttpRequestHeaderSize() 就是我们在配置文件中配的 64 bytes
        inputBuffer = new Http11InputBuffer(request, protocol.getMaxHttpRequestHeaderSize(),
                protocol.getRejectIllegalHeader(), httpParser);
        request.setInputBuffer(inputBuffer);
        // ...
    }
        
// InputBuffer for HTTP that provides request header parsing as well as transfer encoding.
public class Http11InputBuffer implements InputBuffer, ApplicationBufferHandler {

    // The read buffer.
    private ByteBuffer byteBuffer;
    
    public Http11InputBuffer(Request request, int headerBufferSize,
            boolean rejectIllegalHeader, HttpParser httpParser) {
        // ...
        // 设置 maxHeaderSize
        this.headerBufferSize = headerBufferSize;
        // ...
    }

    // Parse the HTTP headers.
    boolean parseHeaders() throws IOException {
        // ...
        do {
            status = parseHeader();
            // Checking that
            // (1) Headers plus request line size does not exceed its limit
            // (2) There are enough bytes to avoid expanding the buffer when
            // reading body
            // Technically, (2) is technical limitation, (1) is logical
            // limitation to enforce the meaning of headerBufferSize
            // From the way how buf is allocated and how blank lines are being
            // read, it should be enough to check (1) only.
            // byteBuffer.position() > headerBufferSize 表示 socket read buffer 大于 maxHeaderSize
            if (byteBuffer.position() > headerBufferSize || byteBuffer.capacity() - byteBuffer.position() < socketReadBufferSize) {
                throw new IllegalArgumentException(sm.getString("iib.requestheadertoolarge.error"));
            }
        } while (status == HeaderParseStatus.HAVE_MORE_HEADERS);
        // ...
    }

image.png

Shutdown shutdown:Tomcat 停机方式

参考:SpringBoot Tomcat 优雅停机

shutdown == Shutdown.GRACEFUL 时,会创建 GracefulShutdown 对象,负责优雅停机相关操作。

public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory
		implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware {
                
    protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
        return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown());
    }
    
public class TomcatWebServer implements WebServer {

    public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
        this.tomcat = tomcat;
        this.autoStart = autoStart;
        this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
        initialize();
    }

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论