org.zodiac.sdk.nio.http.server.Server Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of zodiac-sdk-nio Show documentation
Show all versions of zodiac-sdk-nio Show documentation
Zodiac SDK NIO2(New Non-Blocking IO)
package org.zodiac.sdk.nio.http.server;
import io.vavr.control.Either;
import static org.zodiac.sdk.nio.core.Packet.State.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zodiac.sdk.nio.common.ErrorFactory;
import org.zodiac.sdk.nio.core.Packet;
import org.zodiac.sdk.nio.core.PacketUtil;
import org.zodiac.sdk.nio.http.ApplicationProtocol;
import org.zodiac.sdk.nio.http.NioHttpConstants;
import org.zodiac.sdk.nio.http.TransportProtocol;
import org.zodiac.sdk.nio.http.TransportProtocol.Type;
import org.zodiac.sdk.nio.http.common.*;
public class Server {
private final Logger log = LoggerFactory.getLogger(getClass());
private final ExecutorService executorService;
private volatile boolean isRunning = false;
private final Server.Configuration configuration;
public Server(final Server.Configuration configuration) {
this.configuration = configuration;
executorService = Executors.newFixedThreadPool(configuration.threadPoolSize());
}
public synchronized void run() {
if (isRunning) {
return;
}
log.info("starting server");
log.info("using port {}", configuration.port());
try {
isRunning = true;
new ServerThread().start();
log.debug("server started");
} catch (final IOException e) {
log.error(e.getMessage());
e.printStackTrace();
}
}
public void stop() {
isRunning = false;
}
class ServerThread extends Thread {
private final ConcurrentHashMap> clientPacketQueueTable =
new ConcurrentHashMap<>();
private DatagramChannel channel;
private ServerSocketChannel tcpChannel;
private ByteBuffer buffer;
ServerThread() throws IOException {
super("listener-thread");
log.debug("starting server with listener thread");
if (configuration.transportProtocolType == TransportProtocol.Type.TCP) {
tcpChannel = ServerSocketChannel.open();
tcpChannel.bind(new InetSocketAddress(configuration.port()));
} else {
final Selector selector = Selector.open();
channel = DatagramChannel.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
channel.bind(new InetSocketAddress(configuration.port()));
buffer = transportProtocol().emptyBuffer();
}
}
@Override
public void run() {
while (isRunning) {
try {
if (configuration.transportProtocolType == TransportProtocol.Type.TCP) { // TCP
executorService.execute(handler(tcpChannel.socket().accept(), null, null, null));
} else { // UDP
buffer.clear();
final SocketAddress router = channel.receive(buffer);
buffer.flip();
if (router != null) {
final Packet packet = Packet.of(buffer);
final InetSocketAddress client = packet.getPeerAddress();
log.debug("incoming packet={}", packet);
log.debug("client={}", client);
final BlockingQueue existingQueue =
clientPacketQueueTable.putIfAbsent(client, new ArrayBlockingQueue<>(100));
final BlockingQueue clientPacketQueue =
Objects.requireNonNull(clientPacketQueueTable.get(client),
"client incoming packet queue should have been present in client table");
clientPacketQueue.put(packet);
if (packet.is(SYN) && existingQueue == null) {
log.debug("{} wishes to establish connection", client);
executorService.execute(handler(null, channel, client, clientPacketQueue));
} else {
log.debug("received {} added to queue {}", packet, clientPacketQueue);
}
}
}
} catch (final IOException | InterruptedException e) {
log.error(e.getMessage());
e.printStackTrace();
}
}
}
}
private Runnable handler(final Socket socket, final DatagramChannel channel, final SocketAddress client,
final BlockingQueue queue) throws IOException {
switch (configuration.getTransportProtocolType()) {
case UDP:
return new UDPHandler(channel, client, queue, configuration, transportProtocol(),
applicationProtocol());
case TCP:
return new TCPHandler(socket, configuration, transportProtocol(), applicationProtocol());
default:
throw ErrorFactory.invalidTransportProtocol(configuration.getTransportProtocolType().name());
}
}
private TransportProtocol transportProtocol() {
switch (configuration.getTransportProtocolType()) {
case UDP:
return new UDPSRProtocol();
case TCP:
return null;
default:
throw ErrorFactory.invalidTransportProtocol(configuration.getTransportProtocolType().name());
}
}
private ApplicationProtocol.Response applicationProtocol() throws IOException {
switch (configuration.getApplicationProtocolType()) {
case FILESERVER:
return new FileServerProtocol(configuration.getDirectory());
default:
throw ErrorFactory.invalidApplicationProtocol(configuration.getApplicationProtocolType().name());
}
}
private static class UDPHandler implements Runnable, UDPSRProtocol.Agent {
private final Logger log;
DatagramChannel channel;
Selector selector;
InetSocketAddress client;
BlockingQueue queue;
TransportProtocol transportProtocol;
private final ApplicationProtocol.Response applicationProtocol;
private final Configuration configuration;
private final InetSocketAddress router;
public UDPHandler(final DatagramChannel channel, final SocketAddress client, final BlockingQueue queue,
final Configuration configuration, final TransportProtocol transportProtocol,
final ApplicationProtocol.Response applicationProtocol) throws UnknownHostException {
this(null, channel, client, queue, configuration, transportProtocol, applicationProtocol);
}
public UDPHandler(final Logger log, final DatagramChannel channel, final SocketAddress client, final BlockingQueue queue,
final Configuration configuration, final TransportProtocol transportProtocol,
final ApplicationProtocol.Response applicationProtocol) throws UnknownHostException {
this.log = null != log ? log : LoggerFactory.getLogger(getClass());
this.channel = channel;
this.client = (InetSocketAddress)client;
this.queue = queue;
this.transportProtocol = transportProtocol;
this.applicationProtocol = applicationProtocol;
this.configuration = configuration;
router = configuration.router();
}
@Override
public void run() {
try {
configure();
if (handshake()) {
final HTTPRequest request = transportProtocol.receive(this);
if (request != null) {
log.info("request received, preparing response");
final HTTPResponse response = applicationProtocol.response(request);
final ByteBuffer[] buffers = PacketUtil.split(response.toString().getBytes());
final Packet[] packets = new Packet[buffers.length];
for (int i = 0; i < buffers.length; i++) {
packets[i] = Packet.builder().state(BFRD).sequenceNumber(i).peerAddress(client)
.payload(buffers[i].array()).build();
}
if (transportProtocol.send(this, packets)) {
log.info("response sent!");
} else {
log.error("unable to confirm response delivery after {} attempts",
transportProtocol.maxConsecutiveRetries());
}
log.info("response=\n{}", response.toString());
}
}
} catch (final Exception e) {
log.error("{}: {}", e.getClass().getSimpleName(), e.getMessage());
} finally {
log.debug("disconnecting");
try {
selector.close();
} catch (IOException e) {
log.error("{}: {}", e.getClass().getSimpleName(), e.getMessage());
}
log.debug("exiting thread");
}
}
private void configure() throws IOException {
selector = Selector.open();
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
private boolean handshake() throws InterruptedException, IOException {
final Packet synPacket =
Objects.requireNonNull(queue.poll(), "synPacket should have been in client queue but none were found");
log.debug("received syn {}", synPacket);
final Packet synAckPacket =
synPacket.toBuilder().state(SYNACK).sequenceNumber(1).peerAddress(client).build();
write(synAckPacket);
log.debug("sent synack {}", synAckPacket);
final Packet ackPacket = queue.poll(transportProtocol.packetTimeoutMs(), TimeUnit.MILLISECONDS);
if (ackPacket == null) {
log.error("did not receive ack after timeout, assuming was sent");
} else if (!ackPacket.is(ACK)) {
Packet packet = ackPacket;
if (packet.is(SYN)) {
while (packet != null && packet.is(SYN)) {
log.warn("received again syn {}", packet);
write(packet.toBuilder().state(SYNACK).build());
log.debug("sent synack {}", synAckPacket);
packet = queue.poll(transportProtocol.packetTimeoutMs(), TimeUnit.MILLISECONDS);
}
if (packet == null) {
log.error("did not receive ack after timeout, assuming was sent");
} else {
log.error("received a packet but was not ack, will offer other packet back to queue");
final boolean offeredToQueue =
queue.offer(ackPacket, transportProtocol.packetTimeoutMs(), TimeUnit.MILLISECONDS);
log.info("{} was successfully to queue", offeredToQueue);
}
} else {
log.error("received a packet but was not ack, will offer other packet back to queue");
final boolean offeredToQueue =
queue.offer(ackPacket, transportProtocol.packetTimeoutMs(), TimeUnit.MILLISECONDS);
log.info("{} was successfully to queue", offeredToQueue);
}
} else {
log.debug("received ackPacket {}", ackPacket);
log.debug("connection established");
}
return true;
}
private Packet read(final int timeout) throws InterruptedException {
log.info("blocking up to {}ms for response", timeout);
final Packet packet = queue.poll(timeout, TimeUnit.MILLISECONDS);
log.info("polled {}", packet);
return packet;
}
@Override
public Packet read() throws InterruptedException {
return read(transportProtocol.packetTimeoutMs());
}
@Override
public void write(final Packet packet) throws IOException {
log.info("sent {}", packet);
channel.send(packet.buffer(), router);
}
@Override
public T make(final List packets) {
final String combinedPayload =
packets.stream().filter(Objects::nonNull).map(Packet::payload).collect(Collectors.joining(""));
try {
final Either request = HTTPRequest.of(combinedPayload);
if (request.isLeft()) {
log.info("request successfully created");
return (T)request.getLeft();
} else {
log.error("request invalid: {}", request.get());
log.info("\nrequest: \n{}", combinedPayload);
return null;
}
} catch (final Exception e) {
log.error("{}: {}", e.getClass().getSimpleName(), e.getMessage());
log.debug("request: \n{}", combinedPayload);
return null;
}
}
}
private static class TCPHandler implements Runnable {
private final Logger log;
private final Socket socket;
private final TransportProtocol transportProtocol;
private final ApplicationProtocol.Response applicationProtocol;
private final Server.Configuration configuration;
public TCPHandler(final Socket socket, final Configuration configuration,
final TransportProtocol transportProtocol, final ApplicationProtocol.Response applicationProtocol) {
this(null, socket, configuration, transportProtocol, applicationProtocol);
}
public TCPHandler(final Logger log, final Socket socket, final Configuration configuration,
final TransportProtocol transportProtocol, final ApplicationProtocol.Response applicationProtocol) {
this.log = null != log ? log : LoggerFactory.getLogger(getClass());
this.socket = socket;
this.transportProtocol = transportProtocol;
this.applicationProtocol = applicationProtocol;
this.configuration = configuration;
}
@Override
public void run() {
try (final PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
final BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
log.debug("connection accepted");
boolean keepAlive = true;
int contentLength = 0;
while (keepAlive) {
HTTPRequest request = null;
final StringBuilder requestBuilder = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
requestBuilder.append(line);
requestBuilder.append(NioHttpConstants.CRLF);
if (line.toLowerCase().contains(NioHttpConstants.Headers.CONTENT_LENGTH.toLowerCase())) {
final String[] keyValue = line.split(":");
contentLength = Integer.parseInt(keyValue[1].trim());
}
log.debug("line: {}", line);
if (line.equalsIgnoreCase("")) {
for (int i = 0; i < contentLength; i++) {
requestBuilder.append((char)in.read());
}
break;
}
}
log.debug("builder: {}", requestBuilder.toString());
final Either requestAttempt = HTTPRequest.of(requestBuilder.toString());
if (requestAttempt.isLeft()) {
request = requestAttempt.getLeft();
} else {
throw new HTTPRequest.RequestError(requestAttempt.get());
}
if (request != null) {
log.debug("request:");
log.debug(request.toString());
final String responseText = applicationProtocol.response(request).toString();
log.debug("response:");
log.debug(responseText);
out.print(responseText);
out.flush();
if (request.headers().containsKey(NioHttpConstants.Headers.CONNECTION)) {
//final String connection = request.headers().getFirst(NioHttpConstants.Headers.CONNECTION);
final String connection = request.headers().getFirstConnection();
keepAlive = connection.equalsIgnoreCase(NioHttpConstants.Headers.KEEP_ALIVE);
} else {
keepAlive = false;
}
} else {
log.debug("connection terminated");
keepAlive = false;
}
}
} catch (final IOException | HTTPRequest.RequestError e) {
log.error("{}: {}", e.getClass().getSimpleName(), e.getMessage());
}
}
}
// @Accessors(fluent = true)
static final class Configuration {
private final TransportProtocol.Type transportProtocolType;
private final ApplicationProtocol.Type applicationProtocolType;
private final int port;
private final boolean verbose;
private final String directory;
public Configuration(Type transportProtocolType,
org.zodiac.sdk.nio.http.ApplicationProtocol.Type applicationProtocolType, int port, boolean verbose,
String directory) {
super();
this.transportProtocolType = transportProtocolType;
this.applicationProtocolType = applicationProtocolType;
this.port = port;
this.verbose = verbose;
this.directory = directory;
}
public TransportProtocol.Type getTransportProtocolType() {
return transportProtocolType;
}
public ApplicationProtocol.Type getApplicationProtocolType() {
return applicationProtocolType;
}
public int getPort() {
return port;
}
public boolean isVerbose() {
return verbose;
}
public String getDirectory() {
return directory;
}
public final int port() {
return port == 0 || port == -1 ? NioHttpConstants.DEFAULT_SERVER_PORT : port;
}
public final InetSocketAddress router() throws UnknownHostException {
return new InetSocketAddress(InetAddress.getByName(NioHttpConstants.DEFAULT_ROUTER_HOST), NioHttpConstants.DEFAULT_ROUTER_PORT);
}
public final int threadPoolSize() {
return NioHttpConstants.DEFAULT_THREAD_POOL_SIZE;
}
}
}