package robombs.clientserver; import java.net.*; import java.io.*; import java.util.*; /** * This the client's counterpart to the SimpleServer. If connects to a SimpleServer and exchanges DataContainers with it. */ public class SimpleClient { private boolean exit = false; private List<DataTransferListener> listener = new ArrayList<DataTransferListener>(); private List<ClientPreProcessor> pres = new ArrayList<ClientPreProcessor>(); private DataContainer[] nextData = null; private int myID = -1; private String server = null; private int port = 0; private boolean zip = false; private Sender sender = null; private Thread senderThread=null; private boolean connected = false; private DataContainer initialDG = null; private volatile boolean waitingForData=false; private PerformanceCounter pc = new PerformanceCounter("Client"); private final static Object SYNC = new Object(); /** * Creates a new SimpleClient. The client won't connect to the server unless the connect()-method has been called. * @param server the address of the server. This can be IPv4, IPv6 or a name. * @param port the port on which the server is listening for clients to connect * @param zip if true, all transfers (except for the login itself) will be zipped. This consumes more cpu power but reduces bandwidth usage. * @param data an initial DataContainer. This container will be passed to the registered ClientLoginListeners (if any) by the server. The SimpleServer itself won't process it. */ public SimpleClient(String server, int port, boolean zip, DataContainer data) { this.server = server; this.port = port; this.zip = zip; this.initialDG = data; } /** * Creates a new SimpleClient. The client won't connect to the server unless the connect()-method has been called. * @param se the ServerEntry (usually taken from the ServerBrowser) that holds the server's data. * @param zip if true, all transfers (except for the login itself) will be zipped. This consumes more cpu power but reduces bandwidth usage. * @param data an initial DataContainer. This container will be passed to the registered ClientLoginListeners (if any) by the server. The SimpleServer itself won't process it. */ public SimpleClient(ServerEntry se, boolean zip, DataContainer data) { this(se.getAddress().getHostName(), se.getPort(), zip, data); } /** * Tries to connect to the configured server. If that fails, an Exception will be thrown. * @throws Exception failed? */ public void connect() throws Exception { sender = new Sender(); senderThread=new Thread(sender); senderThread.start(); } /** * Disconnects from a server. If there is no connection established, nothing will be done. */ public void disconnect() { if (sender != null) { sender.disconnect(); } sender = null; } public String getServer() { String ip=sender.conn.toString(); int pos=ip.indexOf("/"); if (pos!=-1) { ip=ip.substring(pos+1); } return ip.trim(); } /** * Adds a DataTransferListener to the client. This listener will be called when the client receives data from the server. * @param sl the listener */ public void addListener(DataTransferListener sl) { listener.add(sl); } /** * Adds a ClientPreProcessor to the client. A ClientPreProcessor will be called before sending to the server, before receiving * from the server and after receiving from the server to allow its implementing class to do whats needed in these stages. For example, * "before sending" should prepare the data to be send to the server and inject it into the SimpleClient. * @param cpp ClientPreProcessor */ public void addPreProcessor(ClientPreProcessor cpp) { pres.add(cpp); } /** * Sets the data to be transfered to the server in the next transfer. This is usually called from inside an implementation * of a ClientPreProcessor's beforeSending()-method. * @param c an array of DataContainers */ public void setContainers(DataContainer[] c) { synchronized (SYNC) { nextData = c; } if (waitingForData) { // Already waiting for data? Interrupt that... senderThread.interrupt(); } } /** * Returns this client's id. Once connected, this is a positive integer that is unique on the server. * @return int the id */ public int getClientID() { return myID; } /** * Is this client connected to a server? * @return boolean is it? */ public boolean isConnected() { return connected; } /** * Make the client thread process new data regardless if he's waiting or not... * This increases network load but smoothes movement of remote entities. */ public void triggerTransfer() { senderThread.interrupt(); } /** * The working thread for a client. In this thread, data is being sent and received to/from the server. */ private class Sender implements Runnable { private Socket conn = null; private InputStream is = null; private OutputStream os = null; private boolean disconnect = false; /** * Connects to the server with the configured address/port. * @throws Exception if the connection fails, the client thread fails... */ public Sender() throws Exception { NetLogger.log("Trying to connect to " + server + ":" + port); conn = new Socket(server, port); conn.setSoTimeout(10000); conn.setTcpNoDelay(NetGlobals.lowLatency); conn.setPerformancePreferences(0, 2, 1); os = conn.getOutputStream(); is = conn.getInputStream(); connect(); } /** * Disconnects the client from the server. This is done sending a logout request to the server and then * terminating the thread. If the server gets the request, it can't vote against it, i.e. the client * will be disconnected in every case. */ private void disconnect() { disconnect = true; } /** * Tries to disconnect from the server. If this isn't possible, this is not an error. Maybe it has already been * done before or the server is dead... */ private void disconnectInternal() { try { NetLogger.log("Trying to disconnect from " + server + ":" + port); DataContainer c = new DataContainer(); c.setMessageType(MessageTypes.LOGOUT_REQUEST); os.write(DataContainerFactory.toByteArray(new DataContainer[] {c}, false)); os.flush(); c = new DataContainer(StreamConverter.convert(is)); if (c.getMessageType() == MessageTypes.LOGOUT_SUCCESS) { NetLogger.log("Client: Disconnected!"); } else { NetLogger.log("Client: Unexpected server response!"); } } catch (Exception e) { NetLogger.log("Client: Unable to disconnect (already disconnected?)!"); } exit = true; } /** * Connects to the server * @throws Exception */ private void connect() throws Exception { boolean con = false; myID = -1; while (!con) { DataContainer c = new DataContainer(); c.setMessageType(MessageTypes.LOGIN_REQUEST); if (zip) { c.add((byte) 1); } else { c.add((byte) 0); } DataContainer[] cont = null; if (initialDG != null) { cont = new DataContainer[] {c, initialDG}; } else { cont = new DataContainer[] {c}; } os.write(DataContainerFactory.toByteArray(cont, false)); c = new DataContainer(StreamConverter.convert(is)); boolean ok = false; if (c.getMessageType() == MessageTypes.LOGIN_SUCCESS) { ok = true; con = true; myID = c.getNextInt(); pc = new PerformanceCounter("Client "+myID); NetLogger.log("Client: Logged in - ID is: " + myID + "!"); connected = true; } if (!ok) { NetLogger.log("Client: Error logging in!"); } if (!con) { // retry! Thread.sleep(200); } } } public void run() { Thread.currentThread().setPriority(Thread.MAX_PRIORITY); try { while (!exit) { long start=System.nanoTime(); pc.printStats(); synchronized (SYNC) { // Call the PreProcessors. It's most likely that those will create the Packet to // send or something... for (ClientPreProcessor cpp:pres) { cpp.beforeSending(); } } byte[] bytes = null; synchronized (SYNC) { if (nextData != null) { bytes = DataContainerFactory.toByteArray(nextData, zip); nextData=null; } } DataContainer c = null; DataContainer[] cs = null; if (bytes != null) { if (!disconnect) { os.write(bytes); pc.out(bytes.length); os.flush(); boolean ok = true; byte[] res = null; try { synchronized (SYNC) { for (ClientPreProcessor cpp:pres) { cpp.beforeReceiving(); } } res = StreamConverter.convert(is); if (res == null || res.length == 0) { NetLogger.log("Client: Got a zero-sized response from server. Trying to disconnect now!"); disconnectInternal(); ok = false; } pc.in(res.length); } catch (Exception e) { e.printStackTrace(); NetLogger.log("Client: Server error (server running?)!"); disconnectInternal(); ok = false; } /* if (Globals.emulateRemoteServer) { try { Thread.sleep(30); } catch(Exception e){} }*/ if (ok) { cs = DataContainerFactory.extractContainers(res, zip); for (int i = 0; i < cs.length; i++) { c = cs[i]; int type = c.getMessageType(); for (DataTransferListener sl:listener) { sl.dataReceived(c, type); } } for (DataTransferListener sl:listener) { sl.dataReceivedEnd(); } synchronized (SYNC) { for (ClientPreProcessor cpp:pres) { cpp.afterReceiving(); } } } } else { disconnectInternal(); } long end=System.nanoTime(); end=(end-start)/1000000L; if (end>250) { NetLogger.log("Client: Client lags ("+end+"ms)!"); } long st=Math.min(NetGlobals.clientWaitTime, Math.max(0,NetGlobals.clientWaitTime-end)); if (st>=0) { try { Thread.sleep(st); } catch(Exception e) { pc.interrupted(); // This is intentionally } } else { Thread.yield(); } } else { if (!disconnect) { // This branch is entered only, if the client doesn't return any data to // send. This happens almost never... waitingForData=true; try { Thread.sleep(10); } catch(Exception e) { // This is intentionally } waitingForData=false; } else { disconnectInternal(); } } } is.close(); os.close(); conn.close(); connected = false; NetLogger.log("Client: Network thread terminated!"); } catch (Exception e) { exit=true; throw new RuntimeException(e); } } } }