Public Repository for the Magicbane Shadowbane Emulator
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

413 lines
15 KiB

// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2022
// www.magicbane.com
package engine.net;
import engine.job.JobManager;
import engine.net.client.Protocol;
import engine.net.client.msg.ClientNetMsg;
import org.pmw.tinylog.Logger;
import java.io.IOException;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.NotYetConnectedException;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
public abstract class AbstractConnection implements
NetMsgHandler {
protected static final long SOCKET_FULL_WRITE_DELAY = 50L; //wait one second to write on full socket
private static final String error01 = "A byte buffer is being free()ed that is not of the size stored in ByteBufferPool. Size: %1%";
private static final String error02 = "(IP=%1%): Socket has reached the maximum outbound buffer length of %2%. Moving on while we wait for data to be sent.";
private static final int OPCODEHASH = 1016; //Opcodes are unique modulo this number as of 11/21/10
protected final SocketChannel sockChan;
protected final AbstractConnectionManager connMan;
protected final NetMsgFactory factory;
protected final AtomicBoolean execTask = new AtomicBoolean(false);
protected final ReentrantLock writeLock = new ReentrantLock();
protected final ReentrantLock readLock = new ReentrantLock();
protected long lastMsgTime = System.currentTimeMillis();
protected long lastKeepAliveTime = System.currentTimeMillis();
protected long lastOpcode = -1;
protected ConcurrentLinkedQueue<ByteBuffer> outbox = new ConcurrentLinkedQueue<>();
protected ByteBuffer outBuf = null;
protected ConcurrentHashMap<Long, Byte> cacheList;
protected long nextWriteTime = 0L;
//Opcode tracking
protected Protocol lastProtocol = Protocol.NONE;
public AbstractConnection(AbstractConnectionManager connMan,
SocketChannel sockChan, boolean clientMode) {
this.connMan = connMan;
this.sockChan = sockChan;
this.factory = new NetMsgFactory(this);
}
public void disconnect() {
try {
this.sockChan.close();
} catch (IOException e) {
Logger.error(e.toString());
}
}
/**
* Serializes AbstractNetMsg <i>msg</i> into a ByteBuffer, then queues that
* ByteBuffer for sending on this AbstractConnection's SocketChannel.
*
* @param msg - AbstractNetMsg to be sent.
* @return boolean status if queue is successful or not. On false return,
* the field <i>lastError</i> will be set.
*/
public final boolean sendMsg(AbstractNetMsg msg) {
// Logger.info("Send: " + msg.getSimpleClassName());
try {
return this._sendMsg(msg);
} catch (Exception e) { // Catch-all
Logger.error(e);
return false;
}
}
/**
* Serializes AbstractNetMsg <i>msg</i> into a ByteBuffer, then queues that
* ByteBuffer for sending on this AbstractConnection's SocketChannel. This
* internal function is <b>required</b> to be overridden by subclasses.
*
* @param msg - AbstractNetMsg to be sent.
* @return boolean status if queue is successful or not. On false return,
* the field <i>lastError</i> will be set.
*/
protected abstract boolean _sendMsg(AbstractNetMsg msg);
/**
* Queues ByteBuffer <i>bb</i> for sending on this AbstractConnection's
* SocketChannel.
*
* @param bb - ByteBuffer to be sent.
* @return boolean status if queue is successful or not. On false return,
* the field <i>lastError</i> will be set.
*/
public final boolean sendBB(ByteBuffer bb) {
if (bb == null)
return false;
try {
return this._sendBB(bb);
} catch (Exception e) { // Catch-all
Logger.error(e);
e.printStackTrace();
return false;
}
}
/**
* Queues ByteBuffer <i>bb</i> for sending on this AbstractConnection's
* SocketChannel. This internal function is designed to be overrideable by
* subclasses if needed.
*
* @param bb - ByteBuffer to be sent.
* @return boolean status if queue is successful or not. On false return,
* the field <i>lastError</i> will be set.
*/
protected boolean _sendBB(ByteBuffer bb) {
this.outbox.add(bb);
this.connMan.sendStart(this.sockChan);
return true;
}
/**
* Move data off the socketChannel's buffer and into the Connection's
* internal NetMsgFactory.
*/
// FIXME the int return value on this means nothing! Clean it up!
protected int read() {
if (readLock.tryLock())
try {
if (this.sockChan.isOpen() == false) {
Logger.info("Sock channel closed. Disconnecting " + this.getRemoteAddressAndPortAsString());
this.disconnect();
return 0;
}
// get socket buffer sized buffer from multipool
ByteBuffer bb = Network.byteBufferPool.getBuffer(16);
int totalBytesRead = 0;
int lastRead;
do {
try {
bb.clear();
lastRead = this.sockChan.read(bb);
// On EOF on the SocketChannel, disconnect.
if (lastRead <= -1) {
Logger.info(" EOF on Socket Channel " + this.getRemoteAddressAndPortAsString());
this.disconnect();
break;
}
if (lastRead == 0)
continue;
synchronized (this.factory) {
this.factory.addData(bb);
}
this.checkInternalFactory();
totalBytesRead += lastRead;
} catch (NotYetConnectedException e) {
Logger.error(e.toString());
break;
} catch (ClosedChannelException e) {
Logger.error(e.toString());
this.disconnect();
break;
} catch (IOException e) {
if (!e.getMessage().startsWith(
"An existing connection was forcibly closed"))
Logger.error(e.toString());
this.disconnect();
break;
} catch (Exception e) {
Logger.error(e.toString());
break;
}
} while (lastRead > 0);
// put buffer back into multipool
Network.byteBufferPool.putBuffer(bb);
this.checkInternalFactory();
return totalBytesRead;
} finally {
readLock.unlock();
}
else {
Logger.debug("Another thread already has a read lock! Skipping.");
return 0;
}
}
/**
* Move data off the Connection's buffer and into the SocketChannel's
* Buffer.
*/
protected boolean writeAll() {
//intentional delay if socket is full. Give the socket a chance to clear out.
if (System.currentTimeMillis() < this.nextWriteTime)
return false;
if (writeLock.tryLock())
try {
String s = "";
int written = 0;
boolean allSentOK = true, bufferDoubled = false;
while (this.outbox.peek() != null) {
ByteBuffer bb = this.outbox.peek();
int toSend = bb.position();
try {
bb.flip();
written = this.sockChan.write(bb);
// Logger.debug("Using a BB with cap of: " + bb.capacity()
// + ". toSend: " + toSend + ", written: " + written);
if (written != toSend)
bb.compact(); // Clean up in case not all gets sent
if (toSend == written) {
// Actually remove it from the queue
this.outbox.poll();
// Pool it
Network.byteBufferPool.putBuffer(bb);
continue;
} else if (written == 0) {
//Socket full, let's delay the next write on socket.
this.nextWriteTime = System.currentTimeMillis() + AbstractConnection.SOCKET_FULL_WRITE_DELAY;
int currentBufferSize = this.sockChan.socket()
.getSendBufferSize();
if (!bufferDoubled && currentBufferSize <= Network.INITIAL_SOCKET_BUFFER_SIZE) {
this.doubleSocketSendBufferSize();
bufferDoubled = true;
} else {
// s = error02;
// s = s.replaceAll("%1%", this
// .getRemoteAddressAndPortAsString() + ":" +
// this.printLastOpcodes());
// s = s.replaceAll("%2%", currentBufferSize + "");
// this.warn(s);
allSentOK = false;
break;
}
}
} catch (ClosedChannelException e) {
// Catches AsynchronousCloseException,
// and ClosedByInterruptException
Logger.error(e);
break;
} catch (Exception e) {
// Catches NotYetConnectedException
// and IOException
Logger.error(e);
this.disconnect();
break;
}
}
return allSentOK;
} finally {
writeLock.unlock();
}
else {
Logger.debug("Another thread already has a write lock! Skipping.");
return false;
}
}
private boolean doubleSocketSendBufferSize() {
String s;
try {
int currentSize = this.sockChan.socket().getSendBufferSize();
int newSize = currentSize << 1;
this.sockChan.socket().setSendBufferSize(newSize);
// s = "(IP=" + this.getRemoteAddressAndPortAsString() + "): ";
// s += this.printLastOpcodes();
// s += "Socket has reached the maximum outbound buffer length of ";
// s += currentSize + ". Attempting to double the outbound buffer size.";
//
// this.warn(s);
return true;
} catch (SocketException e) {
Logger.error(e.toString());
return false;
}
}
public boolean isConnected() {
return this.sockChan.isConnected();
}
public boolean isOpen() {
return this.sockChan.isOpen();
}
/*
* Getters n Setters
*/
public SocketChannel getSocketChannel() {
return this.sockChan;
}
public final void checkInternalFactory() {
CheckNetMsgFactoryJob j = new CheckNetMsgFactoryJob(this);
JobManager.getInstance().submitJob(j);
}
public NetMsgFactory getFactory() {
return factory;
}
public String getRemoteAddressAndPortAsString() {
String out = "";
if (this.sockChan == null)
out += "NotConnected";
else if (this.sockChan.socket() == null)
out += "NotConnected";
else if (this.sockChan.socket().getRemoteSocketAddress() == null)
out += "NotConnected";
else
out += this.sockChan.socket().getRemoteSocketAddress().toString();
return out;
}
public String getLocalAddressAndPortAsString() {
String out = "";
if (this.sockChan == null)
out += "NotConnected";
else if (this.sockChan.socket() == null)
out += "NotConnected";
else if (this.sockChan.socket().getRemoteSocketAddress() == null)
out += "NotConnected";
else {
out += this.sockChan.socket().getLocalSocketAddress().toString();
out += ":";
out += this.sockChan.socket().getLocalPort();
}
return out;
}
/**
* Gives the Connection a chance to act on a msg prior to sending it to the
* provided NetMsgHandler
*/
@Override
public abstract boolean handleClientMsg(ClientNetMsg msg);
protected long getLastMsgTime() {
return this.lastMsgTime;
}
protected void setLastMsgTime() {
// TODO Consider making this a static to latest system time
this.lastMsgTime = System.currentTimeMillis();
}
protected long getLastKeepAliveTime() {
return this.lastKeepAliveTime;
}
protected void setLastKeepAliveTime() {
this.lastKeepAliveTime = System.currentTimeMillis();
}
public long getLastOpcode() {
return lastOpcode;
}
public void setLastOpcode(long lastOpcode) {
this.lastOpcode = lastOpcode;
}
}