org.zodiac.sdk.nio.http.client.Client 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.client;
import io.vavr.control.Either;
import io.vavr.control.Try;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.List;
import java.util.Objects;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zodiac.sdk.nio.common.InetLocation;
import org.zodiac.sdk.nio.core.Packet;
import org.zodiac.sdk.nio.core.PacketUtil;
import org.zodiac.sdk.nio.http.NioHttpConstants;
import org.zodiac.sdk.nio.http.TransportProtocol;
import org.zodiac.sdk.nio.http.common.*;
import org.zodiac.sdk.nio.http.common.HTTPRequest.RequestError;
import static java.nio.channels.SelectionKey.OP_READ;
import static org.zodiac.sdk.nio.core.Packet.State.*;
public class Client {
private static final Logger log = LoggerFactory.getLogger(Client.class);
TransportProtocol transportProtocol;
public Client(final TransportProtocol transportProtocol) {
this.transportProtocol = transportProtocol;
}
public HTTPResponse request(final HTTPRequest request) throws RequestError {
final Callable task = dispatch(request);
final ExecutorService executorService = Executors.newSingleThreadExecutor();
final Future future = executorService.submit(task);
executorService.shutdown();
try {
return future.get(NioHttpConstants.TIMEOUT_LIMIT_SECONDS, TimeUnit.SECONDS);
} catch (final InterruptedException ie) {
throw new RequestError("InterruptedException: The task was interrupted. Please ensure the request is valid.\n" + (ie.getMessage() != null ? ie
.getMessage() : ""));
} catch (final ExecutionException ee) {
throw new RequestError(
"ExecutionException: An error occurred during the execution of the task. Please ensure the request is valid.\n" + (ee.getMessage() != null ? ee
.getMessage() : ""));
} catch (final TimeoutException te) {
throw new RequestError("TimeoutException: The request was not received after " + NioHttpConstants.TIMEOUT_LIMIT_SECONDS + " seconds. Please ensure the request is valid or retrieve a smaller payload.\n" + (te
.getMessage() != null ? te.getMessage() : ""));
} catch (final Exception e) {
throw new RequestError(e.getClass()
.getSimpleName() + ": An unknown error occurred. Please ensure the request is valid.\n" + (e.getMessage() != null ? e
.getMessage() : ""));
}
}
private Callable dispatch(final HTTPRequest request) throws RequestError {
try {
if (transportProtocol instanceof UDPSRProtocol) {
return new UDPHandler(request, transportProtocol);
} else {
return new TCPHandler(request, transportProtocol);
}
} catch (final Exception e) {
throw new RequestError(e.getClass().getSimpleName() + ": " + e.getMessage() + "\n\nRequest: \n" + request.toString() + "\n");
}
}
private static class TCPHandler implements Callable {
private final HTTPRequest request;
private final TransportProtocol transportProtocol;
public TCPHandler(final HTTPRequest request, final TransportProtocol transportProtocol) throws IOException {
this.request = request;
this.transportProtocol = transportProtocol;
}
@Override
public HTTPResponse call() throws IOException {
log.info("client started in TCP mode");
return callHelper(request);
}
private HTTPResponse callHelper(final HTTPRequest request) throws IOException {
final StringBuilder message = new StringBuilder();
try (final Socket socket = new Socket(request.host(), request.url().getPort());
final PrintWriter writer = new PrintWriter(socket.getOutputStream());
final Scanner reader = new Scanner(new InputStreamReader(socket.getInputStream()))) {
log.debug(request.toString());
writer.print(request.toString());
writer.flush();
while (reader.hasNextLine()) {
final String line = reader.nextLine();
message.append(String.format("%s%s", line, NioHttpConstants.CRLF));
}
final Either responseAttempt = HTTPResponse.of(request, message.toString());
HTTPResponse response = responseAttempt.isLeft() ? responseAttempt.getLeft() : null;
if (response != null && response.getStatusCode() != null && response.getStatusCode().matches("3\\d+")) {
final String location = response.getHeaders().getLocation().toString();
final InetLocation redirectInetLocation = Try.of(() -> InetLocation.fromSpec(location))
.getOrElse(() ->
Try.of(() -> request.url().toBuilder().path(location).build())
.getOrElse(() -> null));
try {
response = callHelper(request.toBuilder().inetLocation(redirectInetLocation).build());
} catch (IOException e) {
throw e;
} catch (RequestError e) {
throw new IOException(e);
}
}
if (responseAttempt.isRight()) {
throw new IOException(responseAttempt.get());
} else {
return response;
}
}
}
}
public static class UDPHandler implements Callable, UDPSRProtocol.Agent {
private final HTTPRequest request;
private final TransportProtocol transportProtocol;
private final InetSocketAddress router;
private final InetSocketAddress server;
private final DatagramChannel channel;
private final Selector selector;
public UDPHandler(final HTTPRequest request, final TransportProtocol transportProtocol) throws IOException {
this.request = request;
router = request.routerAddress();
server = request.socketAddress();
this.transportProtocol = transportProtocol;
channel = DatagramChannel.open();
selector = Selector.open();
channel.configureBlocking(false);
channel.register(selector, OP_READ);
log.debug("client started in UDP mode");
}
@Override
public HTTPResponse call() {
try {
if (handshake()) {
final ByteBuffer[] buffers = PacketUtil.split(request.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)
.payload(buffers[i].array())
.build();
}
if (!transportProtocol.send(this, packets)) {
log.error(
"unable to successfully send request to server after {} attempts",
transportProtocol.maxConsecutiveRetries());
log.info("request={}", request.toString().length() > 0 ? "\n" + request.toString() : "");
} else {
log.info("request was successfully sent to server!");
log.info("request={}", request.toString().length() > 0 ? "\n" + request.toString() : "");
final HTTPResponse response = transportProtocol.receive(this);
if (response != null) {
log.info("response successfully received, terminating client");
log.info("response={}", response.toString().length() > 0 ? "\n" + response.toString() : "");
return response;
}
}
} else {
log.error("unable to handshake");
}
return null;
} catch (final Exception e) {
log.error("{}", e.getMessage());
//throw e;
return null;
} finally {
log.info("closing connection");
try {
channel.close();
selector.close();
} catch (IOException e) {
log.error("{}: {}", e.getClass().getSimpleName(), e.getMessage());
}
}
}
@Override
public Packet read() throws IOException {
final ByteBuffer buffer = PacketUtil.emptyBuffer();
final int nReady = wait0();
channel.receive(buffer);
buffer.flip();
if (nReady > 0) {
final Packet packet = Packet.of(buffer);
log.info("received {}", packet);
return packet;
} else {
return null;
}
}
@Override
public void write(final Packet packet) throws IOException {
final Packet writablePacket = packet.toBuilder()
.peerAddress(server)
.build();
channel.send(writablePacket.buffer(), router);
log.info("sent {}", writablePacket);
}
private int wait(final int timeout) throws IOException {
log.info("blocking up to {}ms for response", timeout);
final int nReady = selector.select(timeout);
final Set keys = selector.selectedKeys();
if (keys.isEmpty()) {
log.warn("no response after timeout");
}
keys.clear();
return nReady;
}
private int wait0() throws IOException {
return wait(transportProtocol.packetTimeoutMs());
}
private Packet readRetry(int attempts) throws IOException {
final Packet packet = read();
return packet == null && attempts-- > 0 ? readRetry(attempts) : packet;
}
private void writeRetry(int attempts, final Packet packet) throws IOException {
write(packet);
if (attempts-- > 0) {
writeRetry(attempts, packet);
}
}
private Packet readWriteRetry(int attempts, final Packet out) throws IOException {
writeRetry(0, out);
final Packet in = readRetry(0);
return in == null && attempts-- > 0 ? readWriteRetry(attempts, out) : in;
}
private boolean handshake() throws IOException {
log.info("initiating handshake");
final Packet synPacket = Packet.builder()
.state(SYN)
.sequenceNumber(0)
.peerAddress(server)
.build();
write(synPacket);
log.info("packet.is(SYN)={}", synPacket.is(SYN));
Packet synAckPacket = readRetry(3);
synAckPacket = synAckPacket != null ? synAckPacket : readWriteRetry(transportProtocol.maxConsecutiveRetries(), synPacket);
if (synAckPacket != null) {
log.info("packet.is(SYNACK)={}", synAckPacket.is(SYNACK));
final Packet ackPacket = synAckPacket.toBuilder()
.state(ACK)
.sequenceNumber(synAckPacket.getSequenceNumber() + 1)
.peerAddress(server)
.build();
writeRetry(0, ackPacket);
log.info("packet.is(ACK)={}", ackPacket.is(ACK));
return true;
} else {
log.error("unable to handshake, synack never received after {} tries", transportProtocol.maxConsecutiveRetries());
return false;
}
}
@Override
public T make(final List packets) {
final String combinedPayload = packets.stream()
.filter(Objects::nonNull)
.map(Packet::payload)
.collect(Collectors.joining(""));
try {
final Either response = HTTPResponse.of(request, combinedPayload);
if (response.isLeft()) {
log.info("response successfully created");
return (T) response.getLeft();
} else {
log.info("response={}", combinedPayload.length() > 0 ? "\n" + combinedPayload : "");
log.error("response invalid: {}", response.get());
return null;
}
} catch (final Exception e) {
log.error("{}: {}", e.getClass().getSimpleName(), e.getMessage());
log.debug("response={}", combinedPayload.length() > 0 ? "\n" + combinedPayload : "");
return null;
}
}
}
}