The Tomcat server is architecturally divided into two core components: the Connector and the Container. While the Container handles servlet lifecycle and request processing, the Connector serves as the gateway between external clients and the internal servlet engine. This article dissects the Connector’s internal structure and flow, explaining how HTTP requests are received, parsed, and forwarded to the Container.
Core Responsibilities of the Connector
The Connector performs four key functions:
- Network Listening – Binds to a port and accepts incoming TCP connections.
- Protocol Decoding – Interprets raw byte streams using application-layer protocols (e.g., HTTP/1.1, AJP).
- Request/Response Translation – Converts protocol-specific objects into standardized Servlet API objects and vice versa.
- Response Transmission – Serializes the Servlet response back into network bytes for delivery to the client.
These responsibilities are logically separated into three modular components to ensure loose coupling and extensibility.
Internal Components: Endpoint, Processor, and Adapter
Tomcat encapsulates these functions within three primary abstractions:
- Endpoint: Manages low-level I/O operations and socket lifecycle.
- Processor: Parses application-layer protocols and constructs request/response objects.
- Adapter: Bridges the Connector and Container by translating between Coyote and Servlet API objects.
These are grouped under a single abstraction: ProtocolHandler, which coordinates their interaction.
Endpoint: The I/O Gateway
The Endpoint handles TCP socket acceptance and dispatch. It is implemented as an abstract class with two critical subcomponents:
- Acceptor: Listens for incoming connections on a server socket.
- SocketProcessor: Processes each accepted socket in a thread-safe manner.
The AbstractEndpoint class defines the base structure:
public abstract class AbstractEndpoint<S> {
protected Acceptor[] acceptors;
protected SynchronizedStack<SocketProcessorBase<S>> processorCache;
private Executor executor;
protected abstract SocketProcessorBase<S> createSocketProcessor(
SocketWrapperBase<S> socketWrapper, SocketEvent event);
}
An internal thread pool (Executor) manages concurrency. When a connection arrives, the Acceptor accepts the socket and delegates it to the SocketProcessor via the processSocket() method:
public boolean processSocket(SocketWrapperBase<S> socketWrapper, SocketEvent event, boolean dispatch) {
SocketProcessorBase<S> sc = processorCache.pop();
if (sc == null) {
sc = createSocketProcessor(socketWrapper, event);
} else {
sc.reset(socketWrapper, event);
}
getExecutor().execute(sc);
return true;
}
The Acceptor thread runs continuously, accepting connections and queuing them for processing:
protected class Acceptor extends Thread {
@Override
public void run() {
while (running) {
try {
SocketChannel socket = serverSock.accept();
setSocketOptions(socket);
} catch (IOException e) {
// Handle error
}
}
}
}
The setSocketOptions() method internally invokes processSocket(), which retrieves a reusable SocketProcessor instance from a cache or creates a new one.
The abstract SocketProcessorBase implements Runnable and ensures thread-safe processing:
public abstract class SocketProcessorBase<S> implements Runnable {
protected SocketWrapperBase<S> socketWrapper;
protected SocketEvent event;
@Override
public final void run() {
synchronized (socketWrapper) {
if (socketWrapper.isClosed()) return;
doRun();
}
}
protected abstract void doRun();
}
Concrete implementations, such as SocketProcessor, delegate to a handler that manages the request lifecycle:
protected static class ConnectionHandler<S> implements AbstractEndpoint.Handler<S> {
private final Map<S, Processor> connections = new ConcurrentHashMap<>();
@Override
public SocketState process(SocketWrapperBase<S> wrapper, SocketEvent status) {
S socket = wrapper.getSocket();
Processor processor = connections.get(socket);
return processor.process(wrapper, status);
}
}
This hendler retrieves the associated Processor bound to the socket, passing control to it for protocol-level handling.
Processer: Protocol-Specific Request Handling
The Processor is responsible for interpreting the incoming byte stream according to a specific protocol (e.g., HTTP). It parses headers, body, and method, then constructs a Request and Response object in the Coyote API.
Its core method delegates to the Adapter:
@Override
public SocketState service(SocketWrapperBase<?> wrapper) throws IOException {
try {
getAdapter().service(request, response);
} catch (Exception e) {
// Log and handle
}
return SocketState.CLOSED;
}
This decouples protocol parsing from servlet execution, allowing multiple protocols (HTTP, AJP) to share the same Container.
Adapter: Bridging to the Servlet Container
The Adapter performs the critical translation between Tomcat’s internal Coyote objects and the standard Servlet API:
public void service(org.apache.coyote.Request coyoteReq, org.apache.coyote.Response coyoteRes)
throws Exception {
Request servletReq = (Request) coyoteReq.getNote(ADAPTER_NOTES);
Response servletRes = (Response) coyoteRes.getNote(ADAPTER_NOTES);
if (servletReq == null) {
servletReq = connector.createRequest();
servletReq.setCoyoteRequest(coyoteReq);
servletRes = connector.createResponse();
servletRes.setCoyoteResponse(coyoteRes);
}
// Delegate to the Container pipeline
connector.getService().getContainer().getPipeline().getFirst().invoke(servletReq, servletRes);
}
Here, the Adapter creates or reuses Servlet API objects, maps the Coyote request/response data into them, and triggers the Container’s pipeline. The first valve in the pipeline eventually invokes the appropriate servlet, executing application logic.
At this point, the request has fully transitioned from the network layer into the servlet runtime environment, where business logic is processed and a response is generated.