Skip to content

Commit

Permalink
Move SSL handshake logic from listener thread to connection thread to…
Browse files Browse the repository at this point in the history
… improve connection creation throughput. Running the SSL handshake concurrently frees listener thread to accept new connections. (helidon-io#7764)
  • Loading branch information
spericas authored and dalexandrov committed Oct 17, 2023
1 parent 41968a8 commit 334f752
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,26 @@
package io.helidon.webserver;

import java.io.UncheckedIOException;
import java.net.Socket;
import java.util.HexFormat;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Semaphore;

import javax.net.ssl.SSLSocket;

import io.helidon.common.buffers.BufferData;
import io.helidon.common.buffers.DataReader;
import io.helidon.common.buffers.DataWriter;
import io.helidon.common.socket.HelidonSocket;
import io.helidon.common.socket.PeerInfo;
import io.helidon.common.socket.PlainSocket;
import io.helidon.common.socket.SocketWriter;
import io.helidon.common.socket.TlsSocket;
import io.helidon.common.task.InterruptableTask;
import io.helidon.common.tls.Tls;
import io.helidon.http.HttpException;
import io.helidon.http.RequestException;
import io.helidon.webserver.spi.ServerConnection;
Expand All @@ -53,30 +60,35 @@ class ConnectionHandler implements InterruptableTask<Void>, ConnectionContext {
private final ConnectionProviders connectionProviders;
private final List<ServerConnectionSelector> providerCandidates;
private final Map<String, ServerConnection> activeConnections;
private final HelidonSocket socket;
private final Socket socket;
private final String serverChannelId;
private final Router router;
private final SocketWriter writer;
private final DataReader reader;
private final Tls tls;

private ServerConnection connection;
private HelidonSocket helidonSocket;
private DataReader reader;
private SocketWriter writer;

ConnectionHandler(ListenerContext listenerContext,
Semaphore connectionSemaphore,
Semaphore requestSemaphore,
ConnectionProviders connectionProviders,
Map<String, ServerConnection> activeConnections,
HelidonSocket socket,
Router router) {
Socket socket,
String serverChannelId,
Router router,
Tls tls) {
this.listenerContext = listenerContext;
this.connectionSemaphore = connectionSemaphore;
this.requestSemaphore = requestSemaphore;
this.connectionProviders = connectionProviders;
this.providerCandidates = connectionProviders.providerCandidates();
this.activeConnections = activeConnections;
this.socket = socket;
this.serverChannelId = serverChannelId;
this.router = router;
this.writer = SocketWriter.create(listenerContext.executor(), socket, listenerContext.config().writeQueueLength());
this.reader = new DataReader(socket);
this.tls = tls;
}

@Override
Expand All @@ -86,19 +98,49 @@ public boolean canInterrupt() {

@Override
public final void run() {
String socketsId = socket.socketId() + " " + socket.childSocketId();
String channelId = "0x" + HexFormat.of().toHexDigits(System.identityHashCode(socket));

// handle SSL and init helidonSocket, reader and writer
try {
if (tls.enabled()) {
SSLSocket sslSocket = (SSLSocket) socket;
sslSocket.setHandshakeApplicationProtocolSelector(
(sslEngine, list) -> {
for (String protocolId : list) {
if (connectionProviders.supportedApplicationProtocols()
.contains(protocolId)) {
return protocolId;
}
}
return null;
});
sslSocket.startHandshake();
helidonSocket = TlsSocket.server(sslSocket, channelId, serverChannelId);
} else {
helidonSocket = PlainSocket.server(socket, channelId, serverChannelId);
}

reader = new DataReader(helidonSocket);
writer = SocketWriter.create(listenerContext.executor(), helidonSocket,
listenerContext.config().writeQueueLength());
} catch (Exception e) {
throw e instanceof RuntimeException re ? re : new RuntimeException(e); // see ServerListener
}

// connection handling
String socketsId = helidonSocket.socketId() + " " + helidonSocket.childSocketId();
Thread.currentThread().setName("[" + socketsId + "] WebServer socket");
if (LOGGER.isLoggable(DEBUG)) {
socket.log(LOGGER,
helidonSocket.log(LOGGER,
DEBUG,
"accepted socket from %s:%d",
socket.remotePeer().host(),
socket.remotePeer().port());
helidonSocket.remotePeer().host(),
helidonSocket.remotePeer().port());
}

try {
if (socket.protocolNegotiated()) {
this.connection = connectionProviders.byApplicationProtocol(socket.protocol())
if (helidonSocket.protocolNegotiated()) {
this.connection = connectionProviders.byApplicationProtocol(helidonSocket.protocol())
.connection(this);
}

Expand All @@ -112,17 +154,17 @@ public final void run() {
activeConnections.put(socketsId, connection);
connection.handle(requestSemaphore);
} catch (RequestException e) {
socket.log(LOGGER, WARNING, "escaped Request exception", e);
helidonSocket.log(LOGGER, WARNING, "escaped Request exception", e);
} catch (HttpException e) {
socket.log(LOGGER, WARNING, "escaped HTTP exception", e);
helidonSocket.log(LOGGER, WARNING, "escaped HTTP exception", e);
} catch (CloseConnectionException e) {
// end of request stream - safe to close the connection, as it was requested by our client
socket.log(LOGGER, TRACE, "connection close requested", e);
helidonSocket.log(LOGGER, TRACE, "connection close requested", e);
} catch (UncheckedIOException e) {
// socket exception - the socket failed, probably killed by OS, proxy or client
socket.log(LOGGER, TRACE, "received I/O exception", e);
helidonSocket.log(LOGGER, TRACE, "received I/O exception", e);
} catch (Exception e) {
socket.log(LOGGER, WARNING, "unexpected exception", e);
helidonSocket.log(LOGGER, WARNING, "unexpected exception", e);
} finally {
// connection has finished the loop of handling, release the semaphore
connectionSemaphore.release();
Expand All @@ -131,32 +173,32 @@ public final void run() {
closeChannel();
}

socket.log(LOGGER, DEBUG, "socket closed");
helidonSocket.log(LOGGER, DEBUG, "socket closed");
}

@Override
public PeerInfo remotePeer() {
return socket.remotePeer();
return helidonSocket.remotePeer();
}

@Override
public PeerInfo localPeer() {
return socket.localPeer();
return helidonSocket.localPeer();
}

@Override
public boolean isSecure() {
return socket.isSecure();
return helidonSocket.isSecure();
}

@Override
public String socketId() {
return socket.socketId();
return helidonSocket.socketId();
}

@Override
public String childSocketId() {
return socket.childSocketId();
return helidonSocket.childSocketId();
}

@Override
Expand Down Expand Up @@ -198,7 +240,7 @@ private ServerConnection identifyConnection() {
while (true) {
Iterator<ServerConnectionSelector> iterator = providerCandidates.iterator();
if (!iterator.hasNext()) {
socket.log(LOGGER, DEBUG, "Could not find a suitable connection provider. "
helidonSocket.log(LOGGER, DEBUG, "Could not find a suitable connection provider. "
+ "initial connection buffer (may be empty if no providers exist):\n%s",
currentBuffer.debugDataHex(false));
return null;
Expand Down Expand Up @@ -240,7 +282,7 @@ private ServerConnection identifyConnection() {
// we may have removed all candidates, we must re-check
// we must return before requesting more data (as more data may not be available)
if (providerCandidates.isEmpty()) {
socket.log(LOGGER,
helidonSocket.log(LOGGER,
DEBUG,
"Could not find a suitable connection provider. "
+ "initial connection buffer (may be empty if no providers exist):\n%s",
Expand All @@ -256,9 +298,9 @@ private ServerConnection identifyConnection() {

private void closeChannel() {
try {
socket.close();
helidonSocket.close();
} catch (Throwable e) {
socket.log(LOGGER, TRACE, "Failed to close socket on connection close", e);
helidonSocket.log(LOGGER, TRACE, "Failed to close socket on connection close", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,11 @@

import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLSocket;

import io.helidon.common.HelidonServiceLoader;
import io.helidon.common.LazyValue;
import io.helidon.common.context.Context;
import io.helidon.common.socket.HelidonSocket;
import io.helidon.common.socket.PlainSocket;
import io.helidon.common.socket.SocketOptions;
import io.helidon.common.socket.TlsSocket;
import io.helidon.common.task.HelidonTaskExecutor;
import io.helidon.common.tls.Tls;
import io.helidon.http.encoding.ContentEncodingContext;
Expand All @@ -71,10 +67,8 @@ class ServerListener implements ListenerContext {
private static final System.Logger LOGGER = System.getLogger(ServerListener.class.getName());

@SuppressWarnings("rawtypes")
private static final LazyValue<List<ServerConnectionSelectorProvider>> SELECTOR_PROVIDERS = LazyValue.create(() -> {
return HelidonServiceLoader.create(ServiceLoader.load(ServerConnectionSelectorProvider.class))
.asList();
});
private static final LazyValue<List<ServerConnectionSelectorProvider>> SELECTOR_PROVIDERS = LazyValue.create(() ->
HelidonServiceLoader.create(ServiceLoader.load(ServerConnectionSelectorProvider.class)).asList());

private final ConnectionProviders connectionProviders;
private final String socketName;
Expand Down Expand Up @@ -359,37 +353,16 @@ private void listen() {
Socket socket = serverSocket.accept();

try {
ConnectionHandler handler;
String channelId = "0x" + HexFormat.of().toHexDigits(System.identityHashCode(socket));
connectionOptions.configureSocket(socket);

HelidonSocket helidonSocket;
if (tls.enabled()) {
SSLSocket sslSocket = (SSLSocket) socket;
sslSocket.setHandshakeApplicationProtocolSelector(
(sslEngine, list) -> {
for (String protocolId : list) {
if (connectionProviders.supportedApplicationProtocols()
.contains(protocolId)) {
return protocolId;
}
}
return null;
});
sslSocket.startHandshake();
helidonSocket = TlsSocket.server(sslSocket, channelId, serverChannelId);
} else {
helidonSocket = PlainSocket.server(socket, channelId, serverChannelId);
}

handler = new ConnectionHandler(this,
ConnectionHandler handler = new ConnectionHandler(this,
connectionSemaphore,
requestSemaphore,
connectionProviders,
activeConnections,
helidonSocket,
router);

socket,
serverChannelId,
router,
tls);
readerExecutor.execute(handler);
} catch (RejectedExecutionException e) {
LOGGER.log(ERROR, "Executor rejected handler for new connection");
Expand Down

0 comments on commit 334f752

Please sign in to comment.