package gcb; import java.net.InetAddress; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.TimerTask; import java.util.concurrent.atomic.AtomicLong; public class GarenaTCPPool extends Thread { Map<Integer, GarenaTCP> tcpConnections; Map<Integer, TCPWorker> workerMap; List<TCPWorker> workers; Queue<TCPPacket> queue; boolean exitingNicely = false; int workerNextId = 0; //next ID to use for a worker thread //configuration int connectionsPerWorker; //statistics public static int STATISTIC_TRANSMIT_PACKETS = 0; //count of outgoing GarenaTCP packets public static int STATISTIC_TRANSMIT_BYTES = 1; //count of outgoing GarenaTCP bytes public static int STATISTIC_RECEIVE_PACKETS = 2; //count of incoming GarenaTCP packets public static int STATISTIC_RECEIVE_BYTES = 3; //count of incoming GarenaTCP bytes public static int STATISTIC_RETRANSMISSION_COUNT = 4; //retransmission packet count AtomicLong[] statistics = null; public GarenaTCPPool() { tcpConnections = new HashMap<Integer, GarenaTCP>(); workerMap = new HashMap<Integer, TCPWorker>(); workers = new ArrayList<TCPWorker>(); queue = new LinkedList<TCPPacket>(); synchronized(Main.TIMER) { Main.TIMER.schedule(new RetransmitTask(), 20000, (int) Math.ceil(GCBConfig.configuration.getDouble("gcb_tcp_srttg", 20))); Main.TIMER.schedule(new CleanTask(), 1000, 300000); //clean TCP connections every five minutes } //configuration connectionsPerWorker = GCBConfig.configuration.getInt("gcb_tcp_connectionsperworker", 5); if(GCBConfig.configuration.getBoolean("gcb_tcp_enablestats", false)) { statistics = new AtomicLong[5]; for(int i = 0; i < statistics.length; i++) { statistics[i] = new AtomicLong(); } } } public void enqueue(GarenaInterface garena, InetAddress address, int port, byte[] bytes) { synchronized(queue) { queue.add(new TCPPacket(garena, address, port, bytes)); queue.notifyAll(); } } public void registerConnection(int conn_id, TCPWorker worker, GarenaTCP tcp) { synchronized(tcpConnections) { if(tcpConnections.containsKey(conn_id)) { Main.println(1, "[GarenaTCPPool] Warning: duplicate TCP connection ID; overwriting previous"); tcpConnections.get(conn_id).end(true); } tcpConnections.put(conn_id, tcp); } synchronized(workerMap) { workerMap.put(conn_id, worker); } } public void registerConnection(int conn_id, GarenaTCP tcp) { //find a worker to allocate this connection initiation to TCPWorker assigned_worker = null; synchronized(workers) { for(TCPWorker worker : workers) { if(worker.count() < connectionsPerWorker) { worker.registerConnection(conn_id, tcp); assigned_worker = worker; break; } } } if(assigned_worker == null) { assigned_worker = allocateWorker(); assigned_worker.registerConnection(conn_id, tcp); } registerConnection(conn_id, assigned_worker, tcp); } public int count() { return tcpConnections.size(); } //this method is called from TCPWorker, it should not be called from anywhere else public void removeTCPConnection(int conn_id) { synchronized(tcpConnections) { tcpConnections.remove(conn_id); //if we're exiting nicely and we've finished exiting, then unbind UDP /*if(hasExited()) { disconnected(GARENA_PEER, false); }*/ //TODO } synchronized(workerMap) { workerMap.remove(conn_id); } } public void cleanTCPConnections() { synchronized(tcpConnections) { Iterator<Integer> connectionIterator = tcpConnections.keySet().iterator(); while(connectionIterator.hasNext()) { int x = connectionIterator.next(); GarenaTCP connection = tcpConnections.get(x); if(connection.isTimeout()) { Main.println(1, "[GarenaTCPPool] Disconnecting connection " + x + " due to timeout."); //using end(true) would remove the connection, but // since we're currently iterating over it that // would cause a concurrent modification exception //so instead we remove it from here connection.end(false); connectionIterator.remove(); } } } } public void exitNicely() { exitingNicely = true; } public TCPWorker allocateWorker() { TCPWorker worker = new TCPWorker(this, workerNextId++); worker.start(); synchronized(workers) { workers.add(worker); Main.println(4, "[GarenaTCPPool] Allocated a new worker thread (count=" + workers.size() + ", id=" + (workerNextId - 1) + ")"); } return worker; } public void incrementStatistics(int type) { if(statistics != null) { statistics[type].incrementAndGet(); } } public void incrementStatistics(int type, int amount) { if(statistics != null) { statistics[type].addAndGet(amount); } } public boolean isStatisticsEnabled() { return statistics != null; } public Long getStatistics(int type) { if(statistics != null) { return statistics[type].get(); } else { return null; } } public void run() { while(true) { TCPPacket packet = null; synchronized(queue) { while(queue.isEmpty()) { try { queue.wait(); } catch(InterruptedException ie) {} } packet = queue.poll(); } if(packet.bytes[0] == 0x0B && !exitingNicely) { int conn_id = GarenaEncrypt.byteArrayToIntLittle(packet.bytes, 8); if(tcpConnections.containsKey(conn_id)) { //TODO: currently we just reject this connection silently, which would cause timeout continue; } //find a worker to allocate this connection initiation to boolean assigned = false; synchronized(workers) { for(TCPWorker worker : workers) { if(worker.count() < connectionsPerWorker) { worker.enqueue(packet); assigned = true; break; } } } if(!assigned) { allocateWorker().enqueue(packet); } } else if(packet.bytes[0] == 0x0D) { int conn_id = GarenaEncrypt.byteArrayToIntLittle(packet.bytes, 4); synchronized(workerMap) { if(workerMap.containsKey(conn_id)) { workerMap.get(conn_id).enqueue(packet); } } } } } class RetransmitTask extends TimerTask { public void run() { synchronized(tcpConnections) { Iterator<GarenaTCP> connection_it = tcpConnections.values().iterator(); while(connection_it.hasNext()) { connection_it.next().standardRetransmission(); } } } } class CleanTask extends TimerTask { public void run() { cleanTCPConnections(); } } } class TCPWorker extends Thread { GarenaTCPPool pool; int id; Map<Integer, GarenaTCP> tcpConnections; Queue<TCPPacket> queue; public TCPWorker(GarenaTCPPool pool, int id) { this.pool = pool; this.id = id; tcpConnections = new HashMap<Integer, GarenaTCP>(); queue = new LinkedList<TCPPacket>(); } public void enqueue(TCPPacket packet) { synchronized(queue) { queue.add(packet); queue.notifyAll(); } } public void registerConnection(int conn_id, GarenaTCP tcp) { synchronized(tcpConnections) { tcpConnections.put(conn_id, tcp); } tcp.setWorker(this); } public int count() { return tcpConnections.size(); } public GarenaTCPPool getPool() { return pool; } public void removeTCPConnection(int conn_id) { synchronized(tcpConnections) { tcpConnections.remove(conn_id); } pool.removeTCPConnection(conn_id); } public void run() { while(true) { TCPPacket packet = null; synchronized(queue) { while(queue.isEmpty()) { try { queue.wait(); } catch(InterruptedException ie) {} } packet = queue.poll(); } if(packet.bytes[0] == 0x0B) { int remote_id = GarenaEncrypt.byteArrayToIntLittle(packet.bytes, 4); int conn_id = GarenaEncrypt.byteArrayToIntLittle(packet.bytes, 8); int destination = GarenaEncrypt.byteArrayToIntLittle(packet.bytes, 16); //little endian short followed by two zeroes MemberInfo member = packet.garena.memberFromID(remote_id); if(member != null) { Main.println(4, "[TCPWorker " + id + "] Starting TCP connection with " + member.username); } else { Main.println(4, "[TCPWorker " + id + "] Starting TCP connection with " + remote_id); } GarenaTCP tcp_connection = new GarenaTCP(packet.garena, this); tcp_connection.init(packet.address, packet.port, remote_id, conn_id, destination, member); synchronized(tcpConnections) { if(tcpConnections.containsKey(conn_id)) { Main.println(1, "[Worker " + id + "] Warning: duplicate TCP connection ID; overwriting previous"); tcpConnections.get(conn_id).end(true); } tcpConnections.put(conn_id, tcp_connection); } pool.registerConnection(conn_id, this, tcp_connection); } else if(packet.bytes[0] == 0x0D) { int conn_id = GarenaEncrypt.byteArrayToIntLittle(packet.bytes, 4); if(conn_id == 0) { continue; //happens sometimes } GarenaTCP tcp_connection; synchronized(tcpConnections) { tcp_connection = tcpConnections.get(conn_id); } int remote_id = GarenaEncrypt.byteArrayToIntLittle(packet.bytes, 8); if(tcp_connection == null || tcp_connection.remote_id != remote_id) { Main.println(11, "[TCPWorker " + id + "] Warning: CONN packet received from user " + remote_id + " at " + packet.address + ", but connection " + conn_id + " not started with user"); continue; } int seq = GarenaEncrypt.byteArrayToIntLittle(packet.bytes, 12); int ack = GarenaEncrypt.byteArrayToIntLittle(packet.bytes, 16); //CONN ACK, CONN DATA, or CONN FIN? if(packet.bytes[1] == 0x14) { //CONN DATA tcp_connection.data(seq, ack, packet.bytes, 20, packet.bytes.length - 20); } else if(packet.bytes[1] == 0x0E) { //CONN ACK tcp_connection.connAck(seq, ack); } else if(packet.bytes[1] == 0x01) { Main.println(4, "[TCPWorker " + id + "] User requested termination on connection " + conn_id); // tcp_connections will be updated by GarenaTCP // so just call end tcp_connection.end(true); } else { Main.println(11, "[TCPWorker " + id + "] PeerLoop: unknown CONN type received: " + packet.bytes[1]); } } } } } class TCPPacket { GarenaInterface garena; InetAddress address; int port; byte[] bytes; public TCPPacket(GarenaInterface garena,InetAddress address, int port, byte[] bytes) { this.garena = garena; this.address = address; this.port = port; this.bytes = bytes; } }