1
2
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
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 }