/**
* Copyright (C) 2014 zml (netevents@zachsthings.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.zachsthings.netevents;
import com.zachsthings.netevents.packet.*;
import java.io.Closeable;
import java.io.IOException;
import java.net.SocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SocketChannel;
import java.util.List;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
/**
* Represents a single connection. {@link Forwarder} wraps connection for reconnecting, but once this is closed it's closed permanently.
*/
class Connection implements Closeable {
// State tracking
private final AtomicBoolean disconnectHandled = new AtomicBoolean();
private final List<Runnable> closeListeners = new CopyOnWriteArrayList<>();
// Connection objects
private final SocketChannel chan;
private OutputThread out;
private InputThread in;
private final SocketAddress remoteAddress;
private final Forwarder attachment;
private Connection(Forwarder attachment, SocketChannel chan) throws IOException {
this.attachment = attachment;
this.chan = chan;
this.remoteAddress = chan.getRemoteAddress();
if (remoteAddress == null) {
throw new IOException("Null remote address for " + chan);
}
}
private void startThreads() throws IOException {
this.out = new OutputThread();
this.in = new InputThread();
out.start();
in.start();
}
public static Connection open(Forwarder attachment, SocketChannel chan) throws IOException {
final Connection ret = new Connection(attachment, chan);
ret.startThreads();
return ret;
}
public void close() throws IOException {
chan.close();
handleClosed();
}
void handleClosed() {
if (chan.isConnected()) {
return;
}
out.interrupt();
in.interrupt();
if (disconnectHandled.compareAndSet(false, true)) {
for (Runnable r : closeListeners) {
r.run();
}
}
}
public void write(Packet p) {
if (!chan.isConnected()) {
throw new IllegalStateException("Channel not connected");
}
out.sendQueue.addLast(new PacketEntry(p, false));
}
public void writeAndClose(Packet p) {
if (!chan.isConnected()) {
// We're assuming that the channel has been disconnected from the other side,
// so this termination packet is no longer necessary
//throw new IllegalStateException("Channel not connected");
return;
}
out.sendQueue.addLast(new PacketEntry(p, true));
}
SocketChannel getChannel() {
return chan;
}
public SocketAddress getRemoteAddress() {
return remoteAddress;
}
public void addCloseListener(Runnable listener) {
closeListeners.add(listener);
}
public NetEventsPlugin getPlugin() {
return attachment.getPlugin();
}
public Forwarder getAttachment() {
return attachment;
}
@Override
public String toString() {
return "Connection{" +
"closeListeners=" + closeListeners +
", chan=" + chan +
", disconnectHandled=" + disconnectHandled +
'}';
}
private static class PacketEntry {
private final Packet packet;
private final boolean toClose;
private PacketEntry(Packet packet, boolean toClose) {
this.packet = packet;
this.toClose = toClose;
}
}
public class OutputThread extends IOThread {
private final BlockingDeque<PacketEntry> sendQueue = new LinkedBlockingDeque<>();
public OutputThread() throws IOException {
super("output", Connection.this);
}
@Override
public void act() throws IOException {
PacketEntry packet;
try {
while ((packet = sendQueue.takeFirst()) != null) {
write(packet.packet);
if (packet.toClose) {
conn.close();
}
}
} catch (InterruptedException e) {
conn.close();
}
}
private void write(Packet packet) throws IOException {
ByteBuffer payload = packet.write();
headerBuf.clear();
headerBuf.put(packet.getOpcode());
headerBuf.putInt(payload.limit());
headerBuf.flip();
payload.flip();
chan.write(headerBuf);
int written = 0;
while (written < payload.limit()) {
written += chan.write(payload);
}
}
}
public class InputThread extends IOThread {
public InputThread() throws IOException {
super("input", Connection.this);
}
@Override
public void act() throws IOException {
headerBuf.clear();
int read = chan.read(headerBuf);
if (read == -1) {
throw new ClosedChannelException();
}
headerBuf.flip();
final int opcode = headerBuf.get();
final int len = headerBuf.getInt();
ByteBuffer payload = ByteBuffer.allocate(len);
read = 0;
while (read < len) {
int tempRead = chan.read(payload);
if (tempRead == -1) {
throw new ClosedChannelException();
}
read += tempRead;
}
payload.flip();
try {
Packet packet;
switch (opcode) {
case Opcodes.SERVER_ID:
packet = ServerIDPacket.read(payload);
break;
case Opcodes.PASS_EVENT:
packet = EventPacket.read(payload);
if (packet == null) {
getPlugin().debug("Unknown event received from " + conn.getRemoteAddress());
}
break;
case Opcodes.DISCONNECT:
packet = DisconnectPacket.read(payload);
break;
default:
throw new IOException("Unknown opcode " + opcode + " received");
}
if (packet != null) {
getPlugin().debug("Received packet " + packet + " from " + conn.getRemoteAddress());
getPlugin().getHandlerQueue().queuePacket(packet, attachment);
}
} catch (Exception e) {
getPlugin().getLogger().log(Level.SEVERE, "Unable to read packet (id " + opcode + ") from " + conn.getRemoteAddress() + ", skipping", e);
}
}
}
static void configureSocketChannel(SocketChannel chan) throws IOException {
chan.configureBlocking(true);
chan.setOption(StandardSocketOptions.TCP_NODELAY, true);
}
}