View Javadoc

1   /*
2   @See License.txt@
3    */
4   
5   package spellcast.server;
6   
7   import java.io.IOException;
8   import java.io.InterruptedIOException;
9   import java.net.InetAddress;
10  import java.net.ServerSocket;
11  import java.net.Socket;
12  import java.util.ArrayList;
13  import java.util.Collections;
14  import java.util.Iterator;
15  import java.util.List;
16  
17  import org.apache.log4j.Logger;
18  import spellcast.event.DisconnectionRequestEvent;
19  import spellcast.event.GameEvent;
20  import spellcast.game.IPC;
21  import spellcast.game.IPCHandle;
22  import spellcast.game.IPCRequest;
23  import spellcast.model.Id;
24  import spellcast.net.VersionDetails;
25  import spellcast.net.WizardSocket;
26  
27  /***
28   * This class handles all incoming and outgoing network traffic.
29   * Since network io can be time consuming a thread is used.
30   * <p>
31   * On each loop through run():
32   * <p>
33   * Checks for and accepts one new connection.
34   * <p>
35   * Each open connection is checked for a new message and only one message
36   * per connection is received.
37   * <p>
38   * All out going messages are sent on the caller's thread.
39   *
40   * @author Barrie Treloar
41   */
42  public class ServerConnectionHandler implements IPC, Runnable {
43      private static int DEFAULT_LIST_SIZE = 10;
44  
45      private boolean isRunning;
46      private VersionDetails programVersionDetails;
47      private InetAddress bindAddress;
48      private int port;
49      private ServerSocket serverSocket;
50      private List wizards =
51          Collections.synchronizedList(new ArrayList(DEFAULT_LIST_SIZE));
52      private Thread serverConnectionThread;
53  
54      /***
55       * A multi-threaded accessed object.  Ensure lock on <code>incomingEventQueueLock</code>
56       * is obtained before accessing.
57       */
58      private ArrayList incomingEventQueue = new ArrayList(DEFAULT_LIST_SIZE);
59      private Object incomingEventQueueLock = new Object();
60  
61      /***
62       * Only allow 5 back logged connections
63       */
64      private static final int BACKLOG = 5;
65  
66      /***
67       * The timeout for all socket operations is 100 milliseconds.
68       */
69      private static final int SOCKET_TIMEOUT = 100;
70  
71      private static final Logger logger = Logger.getLogger("server.connect");
72      private static final Logger logger_net = Logger.getLogger("server.connect.net");
73  
74      public ServerConnectionHandler(
75          VersionDetails programVersionDetails,
76          InetAddress bindAddress,
77          int port) {
78          this.programVersionDetails = programVersionDetails;
79          this.bindAddress = bindAddress;
80          this.port = port;
81      }
82  
83      /***
84       * Add an event to the incoming event queue.
85       * When an event is added it calls <code>notify</code> to 
86       * allow any waiting threads to attempt to continue.
87       */
88      private void addIncomingEvent(IPCRequest e) {
89          synchronized (incomingEventQueueLock) {
90              incomingEventQueue.add(e);
91              incomingEventQueueLock.notify();
92          }
93      }
94  
95      /***
96       * Removes the first event from the incoming event queue and returns it.
97       * If there are no events in the queue this will <code>wait</code>
98       * until one is added when a client sends the server an event, at which point 
99       * the thread will be notified.
100      */
101     public IPCRequest receive() {
102         synchronized (incomingEventQueueLock) {
103             if (incomingEventQueue.size() == 0) {
104                 try {
105                     incomingEventQueueLock.wait();
106                 }
107                 catch (InterruptedException e) {
108                     logger.error(
109                         "Unexpectedly interrupted while waiting in "
110                             + "ServerConnectionHandler.receive",
111                         e);
112                     throw new RuntimeException(
113                         "Unexpectedly interrupted while waiting in "
114                             + "ServerConnectionHandler.receive");
115                 }
116             }
117             return (IPCRequest) incomingEventQueue.remove(0);
118         }
119     }
120 
121     /***
122      * Send an event to the specified client.
123      * Sending an event occurs on the caller's thread.  This may block while the message is being sent.
124      */
125     public void send(Id client, GameEvent event) {
126         WizardSocket ws = findWizardSocket(client);
127         if (ws != null) {
128             try {
129                 ws.send(event);
130             }
131             catch (IOException e) {
132                 logger.warn(
133                     "IOException from " + ws.getSocket().getInetAddress().getHostAddress(),
134                     e);
135                 addIncomingEvent(
136                     new IPCRequest(ws, ws.getID(), new DisconnectionRequestEvent()));
137                 disconnect(ws);
138             }
139         }
140     }
141 
142     /***
143      * Send the event to all clients.
144      * Equivalent to calling <code>send</code> multiple times for all clients.
145      */
146     public void sendToAll(GameEvent e) {
147         synchronized (wizards) {
148             Iterator wizardSocketIterator = wizards.iterator();
149             while (wizardSocketIterator.hasNext()) {
150                 WizardSocket ws = (WizardSocket) wizardSocketIterator.next();
151                 send(ws.getID(), e);
152             }
153         }
154     }
155 
156     /***
157      * This thread will start running when <code>start</code> is called and 
158      * continue running until the <code>stop</code> method is called.
159      * <p>
160      * On each loop through run():
161      * <p>
162      * Checks for and accepts one new connection.
163      * <p>
164      * Each open connection is checked for a new message and only one message
165      * per connection is received.
166      */
167     public void run() {
168         while (isRunning) {
169             checkForNewConnection();
170             Thread.yield();
171             receiveMessages();
172             Thread.yield();
173         }
174     }
175 
176     /***
177      * Accept one new connection on the server socket. 
178      * When a new connection is established create the WizardSocket to manage
179      * the new socket connection.  Add the WizardSocket to the new connections list.
180      */
181     private void checkForNewConnection() {
182         try {
183             Socket newConnection = serverSocket.accept();
184             newConnection.setSoTimeout(SOCKET_TIMEOUT);
185             WizardSocket ws = new WizardSocket(programVersionDetails);
186             ws.setSocket(newConnection);
187             wizards.add(ws);
188             logger_net.debug(
189                 "New connection from " + newConnection.getInetAddress().getHostAddress());
190         }
191         catch (InterruptedIOException ix) {
192             // Ignore interrupted exceptions
193         }
194         catch (IOException ex) {
195             logger.error("Failed on serverSocket.receive.", ex);
196         }
197     }
198 
199     /***
200      * Each open connection is checked for a new message and only one message
201      * per connection is received.
202      */
203     private void receiveMessages() {
204         synchronized (wizards) {
205             Iterator wizardIterator = wizards.iterator();
206             while (wizardIterator.hasNext()) {
207                 WizardSocket ws = (WizardSocket) wizardIterator.next();
208                 try {
209                     GameEvent event = ws.receive();
210                     if (event != null) {
211                         addIncomingEvent(new IPCRequest(ws, ws.getID(), event));
212                     }
213                 }
214                 catch (IOException e) {
215                     logger.warn(
216                         "IOException from " + ws.getSocket().getInetAddress().getHostAddress(),
217                         e);
218                     addIncomingEvent(
219                         new IPCRequest(ws, ws.getID(), new DisconnectionRequestEvent()));
220                     disconnect(ws);
221                 }
222             }
223         }
224     }
225 
226     /***
227      * Connect the handle with the specified Id.
228      */
229     public void connect(IPCHandle handle, Id client) {
230         WizardSocket ws = (WizardSocket) handle;
231         ws.setID(client);
232     }
233 
234     /***
235      * Disconnect the client.
236      */
237     public void disconnect(IPCHandle handle) {
238         WizardSocket ws = (WizardSocket) handle;
239         wizards.remove(ws);
240         ws.close();
241     }
242 
243     /***
244      * Find the wizard socket that matches the client's Id.
245      * If no client matched, return null.
246      *
247      * @param client the Id of the client whose wizard socket is requested
248      * @return the wizard socket for the client or null if not found.
249      */
250     private WizardSocket findWizardSocket(Id client) {
251         WizardSocket result = null;
252 
253         synchronized (wizards) {
254             Iterator wizardIterator = wizards.iterator();
255             while (wizardIterator.hasNext()) {
256                 WizardSocket ws = (WizardSocket) wizardIterator.next();
257                 if (ws.getID().equals(client)) {
258                     result = ws;
259                     break;
260                 }
261             }
262         }
263 
264         if (result == null) {
265             logger.warn("Could not find wizard socket for requested Id: " + client);
266         }
267 
268         return result;
269     }
270 
271     /***
272      * Allocate resources, create and start the thread.
273      * Resources are deallocated in <code>cleanup</code>.
274      * 
275      * @throws ServerException thrown if the server list socket could not be allocated.
276      */
277     public void start() throws ServerException {
278         try {
279             serverSocket = new ServerSocket(port, BACKLOG, bindAddress);
280             serverSocket.setSoTimeout(SOCKET_TIMEOUT);
281             logger_net.debug(
282                 "Connection Socket = "
283                     + serverSocket.getInetAddress().getHostAddress()
284                     + ":"
285                     + serverSocket.getLocalPort());
286 
287             isRunning = true;
288             serverConnectionThread = new Thread(this, "ServerConnectionThread");
289             serverConnectionThread.start();
290         }
291         catch (Exception e) {
292             throw new ServerException(e);
293         }
294     }
295 
296     /***
297      * Sets the isRunning flag to false which lets <code>run</code> exit the 
298      * infite loop. This method will wait until the thread dies before 
299      * calling <code>cleanup</code> where resources are deallocated.
300      */
301     public void stop() {
302         isRunning = false;
303         try {
304             serverConnectionThread.join();
305         }
306         catch (InterruptedException e) {
307             logger.error("Unexepectedly Interrupted.", e);
308         }
309         cleanup();
310     }
311 
312     /***
313      * Dellocate resources allocated in <code>start</code>
314      */
315     private void cleanup() {
316         if (serverSocket != null) {
317             try {
318                 serverSocket.close();
319             }
320             catch (IOException e) {
321                 logger.error("Unexpected IO Exception.", e);
322             }
323             serverSocket = null;
324         }
325         serverConnectionThread = null;
326     }
327 
328 }