forked from MagicBane/Server
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.
412 lines
15 KiB
412 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; |
|
} |
|
|
|
}
|
|
|