package robombs.clientserver;
import java.net.*;
import java.util.*;
import java.io.*;
/**
* As the name indicates, this is a simple server class. Once started, it opens a TCP-socket for clients to connect
* as well as an (optional) UDP datagram socket (if possible) that broadcasts this server's ip and port for allowing
* to implement a simple server browser.<br>
* The communication between this server and its clients is done via DataContainers, which are simple Wrappers for
* primitive values (and Strings) into byte arrays.
*/
public class SimpleServer {
/**
* The default tcp port
*/
public final static int DEFAULT_PORT = 7000;
/**
* The default UDP port
*/
public final static int UDP_DEFAULT_PORT = 7001;
private List<DataTransferListener> listener = new ArrayList<DataTransferListener>();
private List<ClientLogoutListener> logoutListener = new ArrayList<ClientLogoutListener>();
private List<ClientLoginListener> loginListener = new ArrayList<ClientLoginListener>();
private Map<ClientInfo, ClientProcessor> clientThreads = new HashMap<ClientInfo, ClientProcessor>();
private PerformanceCounter pc = new PerformanceCounter("Server");
private boolean terminate = false;
private String name = "Default";
private boolean registerRunning = false;
private boolean broadcastRunning = false;
private Thread serverBroadcast=null;
private int tcpPort=0;
/**
* Creates a new Server. When creating a two server, up to two threads are being spawned. One for
* listening on the tcp socket for clients to connect and one for broadcasting the server's data
* via UDP.
* @param tcpPort The tcp port to which a client can connect with this server. This port has to be open and unused or otherwise, the server won't start.
* @param udpPort the udp port for broadcasting the server's ip and port (and player count). If this port is not available, the broadcasting thread will
* terminate, but the server will run anyway.
* @param doBroadcast If true, the broadcasting thread will be spawned. Otherwise, it won't.
* @param serverName The name of the server. Should be unique but doesn't have to.
*/
public SimpleServer(int tcpPort, int udpPort, boolean doBroadcast, String serverName) {
if (serverName != null) {
name = serverName;
}
this.tcpPort=tcpPort;
new Thread(new ClientRegisterService(tcpPort)).start();
if (doBroadcast) {
serverBroadcast=new Thread(new ServerBroadcast(tcpPort, udpPort));
serverBroadcast.start();
}
}
/**
* Adds a new DataTransferListener to the server. A DataTransferListener will be notified in case of data
* being transfered from the client to the server.
* @param sl the listener
*/
public void addListener(DataTransferListener sl) {
listener.add(sl);
}
/**
* Adds a new ClientLogoutListener to the server. This listener will be notified if a client logs out
* from this server.
* @param ctl the listener
*/
public void addLogoutListener(ClientLogoutListener ctl) {
logoutListener.add(ctl);
}
/**
* Adds a new ClientLoginListener to the server. This listener will be notified if a client logs in
* on this server.
* @param ctl the listener
*/
public void addLoginListener(ClientLoginListener ctl) {
loginListener.add(ctl);
}
public int getPort() {
return tcpPort;
}
/**
* Shuts down the server. This shut down is semi-hard, i.e. all server threads and such will be terminated correctly, but
* there's no waiting for the clients to be informed about this. The clients have to take care of a "lost" server themselves.
*/
public void shutDown() {
terminate = true;
if (serverBroadcast!=null) {
serverBroadcast.interrupt();
}
try {
while (registerRunning || broadcastRunning) {
Thread.sleep(20);
}
} catch (Exception e) {
// Who cares...?
e.printStackTrace();
}
}
public int getClientCount() {
return clientThreads.size();
}
/**
* The broadcast thread. The broadcast goes to all clients in the local subnet via UDP.
* If the port is not available, this thread will terminate with a RuntimeException but the
* server will continue to run without it.
* Currently, the server will send its data into the network every three seconds.
*/
private class ServerBroadcast implements Runnable {
private int port = 0;
private int tcpPort = 0;
public ServerBroadcast(int tcpPort, int port) {
this.port = port;
this.tcpPort = tcpPort;
}
public void run() {
try {
DatagramSocket bsock = new DatagramSocket(port);
InetAddress bc = InetAddress.getByName("255.255.255.255");
broadcastRunning = true;
while (!terminate) {
try {
NetLogger.log("Server: Broadcasting server data!");
send(bc, bsock, false);
Thread.sleep(3000);
} catch (Exception e) {
// Don't stop...just take notice!
NetLogger.log("Server: Broadcast interrupted!");
}
}
// Send exit-information to the browser multiple times to increase the chance
// that he gets this information (it's UDP, so...)
for (int i=0; i<2; i++) {
send(bc, bsock, true);
if (i==0) {
try {
Thread.sleep(100);
} catch (Exception e) {
}
}
}
bsock.close();
broadcastRunning = false;
NetLogger.log("Server: Broadcast thread terminated!");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void send(InetAddress bc, DatagramSocket bsock, boolean exit) throws Exception {
DataContainer dc = new DataContainer();
dc.add(name);
dc.add(tcpPort);
if (!exit) {
dc.add(clientThreads.size());
} else {
// This flags the server browser, that this server goes down.
dc.add(-9999);
}
byte[] bytes = DataContainerFactory.toByteArray(new DataContainer[] {dc}, false);
DatagramPacket dps = new DatagramPacket(bytes, bytes.length, bc, port + 1);
bsock.send(dps);
}
}
/**
* This is the server's listening thread. It waits for client connections on the specified tcp port and spawns a new
* thread for each client that handles this client's requests and responses. If this thread dies (or doesn't start up
* due to the port not being available), the server in not running.
*/
private class ClientRegisterService implements Runnable {
private int port = 0;
public ClientRegisterService(int port) {
this.port = port;
}
public void run() {
try {
ServerSocket socket = new ServerSocket(port);
socket.setSoTimeout(1000);
registerRunning = true;
NetLogger.log("Server: Listening for client requests at port: " + port);
while (!terminate) {
try {
Socket sock = socket.accept();
NetLogger.log("Server: Request from client " + sock.getInetAddress() + "!");
InputStream is = sock.getInputStream();
OutputStream os = sock.getOutputStream();
byte[] bytes = StreamConverter.convert(is);
DataContainer[] cs = DataContainerFactory.extractContainers(bytes, false);
DataContainer c = cs[0];
DataContainer add = null;
if (cs.length > 1) {
add = cs[1];
}
DataContainer res = new DataContainer();
boolean ok = false;
if (c.hasData()) {
NetLogger.log("Server: Checking request type!");
int command = c.getMessageType();
// Login
if (command == MessageTypes.LOGIN_REQUEST) {
ClientInfo ci = new ClientInfo(sock.getInetAddress(), sock.getPort());
if (!clientThreads.containsKey(ci)) {
byte zip = c.getNextByte();
NetLogger.log("Server: Request type is 'login', clientID will be " + ci.getID() + ", zip=" + (zip == 1) + "!");
ci.setZipMode(zip == 1);
res.setMessageType(MessageTypes.LOGIN_SUCCESS);
res.add(ci.getID());
ClientProcessor cp = new ClientProcessor(sock, ci);
clientThreads.put(ci, cp);
ok = true;
for (Iterator<ClientLoginListener> itty = loginListener.iterator(); itty.hasNext(); ) {
ClientLoginListener cll = itty.next();
DataContainer resp = cll.loggedIn(ci, add);
if (resp != null) {
broadcast(resp);
} else {
ok = false;
break;
}
}
if (ok) {
new Thread(cp).start();
}
} else {
// @todo: May this happen?
}
}
}
if (!ok) {
res.setMessageType(MessageTypes.LOGIN_FAILURE);
NetLogger.log("Server: Request type is 'unknown' or no request type found (" + c.hasData() + ")!");
}
// Send response
os.write(DataContainerFactory.toByteArray(new DataContainer[] {res}, false));
os.flush();
} catch (SocketTimeoutException se) {
// Nobody cares...
} catch (Exception e) {
// This thread must not die. But it may print out its opinions on the current situation...
e.printStackTrace();
}
}
socket.close();
registerRunning = false;
NetLogger.log("Server: Register thread terminated!");
}
catch (Exception e) {
registerRunning=false;
throw new RuntimeException(e);
}
}
}
/**
* Adds a DataContainer to the broadcast-queue. Each container added to this queue is ensured to be send
* once to all clients known to the server at the time that this method is being called (unless the client
* hasn't died or logout, of course).<br>
* This has nothing to do with the data broadcast via UDP that the ServerBroadcast is doing. This is a tcp
* "broadcast" to all known clients.
* @param dc the DataContainer that should be enqueued
*/
public void broadcast(DataContainer dc) {
if (dc != null) {
for (ClientProcessor cpt: clientThreads.values()) {
cpt.hasToSend(dc);
}
}
}
public void broadcastToOthers(DataContainer dc, int clientID) {
if (dc != null) {
for (ClientProcessor cpt: clientThreads.values()) {
if (cpt.ci.getID()!=clientID) {
cpt.hasToSend(dc);
}
}
}
}
/**
* Adds a DataContainer to the broadcast-queue of a single client. Each container added to this queue is ensured to be send
* once to the client.
* @param dc the DataContainer that should be enqueued
*/
public void sendToSingleClient(DataContainer dc, int clientID) {
if (dc != null) {
for (ClientProcessor cpt: clientThreads.values()) {
if (cpt.ci.getID()==clientID) {
cpt.hasToSend(dc);
}
}
}
}
public void setTimeOut(int timeOut) {
for (ClientProcessor cpt: clientThreads.values()) {
cpt.setTimeOut(timeOut);
}
}
/**
* This is the worker thread. Each client connecting will cause an instance of this to be spawned. Each client's
* thread terminates if the client logs or times out. Time out time is three seconds.
*/
private class ClientProcessor implements Runnable {
private InputStream is = null;
private OutputStream os = null;
private ClientInfo ci = null;
private Socket sock = null;
private volatile int timeOut=3000;
private boolean exit = false;
private List<DataContainer> hasToSend = new ArrayList<DataContainer>();
/**
* Creates a instance with the given socket and the ClientInfo that identifies this client.
* @param sock the socket to which is client is connected
* @param ci the ClientInfo
* @throws Exception if anything goes wrong...
*/
public ClientProcessor(Socket sock, ClientInfo ci) throws Exception {
sock.setSoTimeout(NetGlobals.serverTimeOut);
sock.setTcpNoDelay(NetGlobals.lowLatency);
sock.setPerformancePreferences(0, 2, 1);
this.is = sock.getInputStream();
this.os = sock.getOutputStream();
this.ci = ci;
this.sock = sock;
}
/**
* Stops a client's thread and removes the client from the server's clients list.
*/
public void kill() {
exit = true;
}
/**
* Used to enqueue a DataContainer into this client's broadcast queue.
* @param dc the container
*/
public void hasToSend(DataContainer dc) {
if (dc != null) {
synchronized (hasToSend) {
hasToSend.add(dc);
}
}
}
public void setTimeOut(int t) {
try {
sock.setSoTimeout(t);
timeOut=t;
} catch(Exception e) {
NetLogger.log("Unable to set timeout for socket!");
}
}
public void run() {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
try {
DataContainer[] crs = new DataContainer[1];
while (!exit && !terminate) {
long start=System.nanoTime();
boolean recd = false;
DataContainer c = null;
DataContainer[] cs = null;
int lastTimeOut=timeOut;
try {
byte[] bytes = StreamConverter.convert(is);
cs = DataContainerFactory.extractContainers(bytes, ci.getZipMode());
recd = true;
} catch (Exception e) {
if (timeOut==lastTimeOut) {
// If the timeout has changed in between, this is no problem...
e.printStackTrace();
NetLogger.log("Server: Client lost (timeout?)!");
logout(ci);
exit = true;
}
}
pc.printStats();
if (recd) {
List<DataContainer> res = new ArrayList<DataContainer>();
for (int i = 0; i < cs.length; i++) {
c = cs[i];
pc.in(c.getLength());
c.setClientInfo(ci);
int msgType = c.getMessageType();
if (msgType <= MessageTypes.INTERNAL_BORDER) {
// "Normal" transmission, i.e. no internal server message
for (DataTransferListener sl:listener) {
DataContainer[] crss = sl.dataReceived(c, msgType);
if (crss != null) {
res.addAll(Arrays.asList(crss));
}
}
} else {
// an internal server message
processMessage(msgType, res);
}
}
// Notify all listeners that data has been received...
for (DataTransferListener sl:listener) {
sl.dataReceivedEnd();
}
// Send response to the client...
int pos = 0;
synchronized (hasToSend) {
int len=res.size() + hasToSend.size();
if (len!=crs.length) {
// Create a new one only if needed!
crs = new DataContainer[len];
}
// Append all responses from the transfer listeners...
for (DataContainer cr:res) {
crs[pos] = cr;
pos++;
}
// Append all data from the broadcast queue...
for (DataContainer dhts:hasToSend) {
if (dhts == null) {
throw new RuntimeException("Null value in broascast queue at position " + (pos - 1));
}
crs[pos] = dhts;
pos++;
}
hasToSend.clear();
}
byte[] bytes = DataContainerFactory.toByteArray(crs, ci.getZipMode());
os.write(bytes);
pc.out(bytes.length);
os.flush();
ci.touch();
}
// Each thread runs once every xx milliseconds.
long end=System.nanoTime();
end=(end-start)/1000000L;
if (end>250) {
NetLogger.log("Server: Server lags ("+end+"ms)!");
}
long st=Math.min(NetGlobals.serverWaitTime, Math.max(0,NetGlobals.serverWaitTime-end));
if (st>=0) {
Thread.sleep(st);
} else {
Thread.yield();
}
}
// This client has logged out, died, failed...whatever...we are done with it.
logout(ci);
is.close();
os.close();
sock.close();
NetLogger.log("Server: Thread for client " + ci + " terminated!");
} catch (Exception e) {
throw new RuntimeException(e);
}
finally {
clientThreads.remove(ci);
}
}
/**
* Processes an internal server message. Currently, this can be either log out...or log out...:-)
* @param msgType int
* @param res List
*/
private void processMessage(int msgType, List<DataContainer> res) {
DataContainer cr = new DataContainer();
cr.setZip(ci.getZipMode());
if (msgType == MessageTypes.LOGOUT_REQUEST) {
NetLogger.log("Server: Request type is 'logout' from client " + ci + "!");
cr.setMessageType(MessageTypes.LOGOUT_SUCCESS);
kill();
logout(ci);
}
res.add(cr);
}
}
public void logout(int cid) {
for (ClientInfo ci:clientThreads.keySet()) {
if (ci.getID()==cid) {
logout(ci);
break;
}
}
}
public boolean isRunning() {
return registerRunning;
}
/**
* Logout a client by notifying all listeners and adding their responses to the broadcast
* queue.
* @param ci the client to log out
*/
private void logout(ClientInfo ci) {
if (clientThreads.containsKey(ci) && !ci.isLoggedOut()) {
for (Iterator<ClientLogoutListener> itty2 = logoutListener.iterator(); itty2.hasNext(); ) {
ClientLogoutListener ctl = itty2.next();
DataContainer dc = ctl.loggedOut(ci);
broadcast(dc);
NetLogger.log("Server: Client " + ci.getAddress() + " logged out!");
}
ci.logout();
}
}
}