View Javadoc

1   /*
2   @See License.txt@
3    */
4   
5   package spellcast.client;
6   
7   import java.io.IOException;
8   import java.net.InetAddress;
9   import java.net.Socket;
10  import java.util.ArrayList;
11  
12  import org.apache.log4j.Logger;
13  import spellcast.event.ConnectionRequestEvent;
14  import spellcast.event.GameEvent;
15  import spellcast.net.NetConstants;
16  import spellcast.net.VersionDetails;
17  import spellcast.net.WizardSocket;
18  import spellcast.ui.SpellcastView;
19  import spellcast.util.TextUtil;
20  
21  /***
22   * This class handles the socket functions for the client.
23   *
24   * @author Barrie Treloar
25   */
26  public class ClientConnectionHandler implements Runnable {
27      private SpellcastView view;
28      private Thread socketHandlerThread;
29      private boolean isConnected;
30      private InetAddress serverAddress;
31      private int port;
32      private String serverAddressString;
33      private String wizardName;
34      private String gender;
35      private WizardSocket wizardSocket;
36      private VersionDetails programVersionDetails;
37      private boolean cleanupAlreadyDone;
38  
39      /***
40       * A multi-threaded accessed object.  Ensure lock on <code>incomingEventQueueLock</code>
41       * is obtained before accessing.
42       */
43      private ArrayList incomingEventQueue = new ArrayList(10);
44      private Object incomingEventQueueLock = new Object();
45  
46      /***
47       * The timeout for all socket operations is 300 milliseconds.
48       */
49      private static final int SOCKET_TIMEOUT = 300;
50  
51      private static final Logger logger = Logger.getLogger("client.net");
52  
53      public ClientConnectionHandler(
54          SpellcastView view,
55          VersionDetails programVersionDetails) {
56          this.view = view;
57          this.programVersionDetails = programVersionDetails;
58      }
59  
60      public void run() {
61          GameEvent event = null;
62  
63          connectToServer();
64  
65          while (isConnected) {
66              Thread.yield();
67              try {
68                  event = wizardSocket.receive();
69                  if (event != null) {
70                      addIncomingEvent(event);
71                  }
72              }
73              catch (IOException e) {
74                  logger.warn("Exception while reading message from server.", e);
75                  isConnected = false;
76              }
77          }
78          cleanup();
79      }
80  
81      /***
82       * Add an event to the incoming event queue.
83       * When an event is added it calls <code>notify</code> to 
84       * allow any waiting threads to attempt to continue.
85       */
86      private void addIncomingEvent(GameEvent e) {
87          synchronized (incomingEventQueueLock) {
88              incomingEventQueue.add(e);
89              incomingEventQueueLock.notify();
90          }
91      }
92  
93      /***
94       * Removes the first event from the incoming event queue and returns it.
95       * If there are no events in the queue this will <code>wait</code>
96       * until one is added when the server sends the client an event, at which point 
97       * the thread will be notified.
98       * <p> 
99       * If there is an IOException <code>cleanup</code> may call notify in which 
100      * case the event will be null.
101      */
102     public GameEvent receive() {
103         GameEvent result = null;
104 
105         synchronized (incomingEventQueueLock) {
106             if (incomingEventQueue.size() == 0) {
107                 try {
108                     incomingEventQueueLock.wait();
109                 }
110                 catch (InterruptedException e) {
111                     logger.error(
112                         "Unexpectedly interrupted while waiting in "
113                             + "ClientConnectionHandler.receive",
114                         e);
115                     throw new RuntimeException(
116                         "Unexpectedly interrupted while waiting in "
117                             + "ServerConnectionHandler.receive");
118                 }
119             }
120             // This can happen if cleanup calls notify.  Otherwise there
121             // should be a message on the queue
122             if (incomingEventQueue.size() != 0) {
123                 result = (GameEvent) incomingEventQueue.remove(0);
124             }
125         }
126         return result;
127     }
128 
129     /***
130      * Send the event to the server.
131      */
132     public void send(GameEvent event) {
133         try {
134             if (isConnected) {
135                 wizardSocket.send(event);
136             }
137         }
138         catch (IOException e) {
139             logger.warn("Exception whilst sending message.", e);
140             isConnected = false;
141             cleanup();
142         }
143     }
144 
145     /***
146      * Connect to the Spellcast server.
147      */
148     private void connectToServer() {
149         try {
150             String hostString = TextUtil.getIPAddress(serverAddressString);
151             port = TextUtil.getPort(serverAddressString);
152             if (port == -1) {
153                 port = NetConstants.SPELLCAST_NET_DEFAULT_PORT;
154             }
155             view.addMessage("Connecting to " + hostString + ":" + port + " ... ");
156             Socket s = new Socket(hostString, port);
157             s.setSoTimeout(SOCKET_TIMEOUT);
158             wizardSocket = new WizardSocket(programVersionDetails);
159             wizardSocket.setSocket(s);
160             wizardSocket.send(new ConnectionRequestEvent(wizardName, gender));
161             view.addMessage("established.\n");
162             view.connected(true);
163             isConnected = true;
164         }
165         catch (IOException e) {
166             isConnected = false;
167             logger.warn("Failed to connect to server.", e);
168             view.addMessage("failed.\n");
169             view.connected(false);
170         }
171     }
172 
173     /***
174      * Request connection to the specified address and port.
175      *
176      * @param serverAddressString a server string of the form host[:port]
177      * @param wizardName the name of the client's wizard
178      * @param gender the gender of the client's wizard
179      */
180     public void connect(
181         String serverAddressString,
182         String wizardName,
183         String gender) {
184         this.serverAddressString = serverAddressString;
185         this.wizardName = wizardName;
186         this.gender = gender;
187         cleanupAlreadyDone = false;
188         socketHandlerThread = new Thread(this, "ClientToServer");
189         socketHandlerThread.start();
190     }
191 
192     /***
193      * Request the network controller to disconnect from
194      * the existing connection.
195      */
196     public void disconnect() {
197         isConnected = false;
198     }
199 
200     /***
201      * Waits for the socket handler thread to complete.
202      */
203     public void join() {
204         if (socketHandlerThread != null) {
205             try {
206                 socketHandlerThread.join();
207             }
208             catch (InterruptedException e) {
209                 logger.error("Unexepectedly Interrupted.", e);
210             }
211         }
212     }
213 
214     /***
215      * Deallocate resources allocated used during <code>run</code> 
216      * If this is a graceful cleanup the client state machine would have received
217      * a disconnect event for itself and terminated cleanly BEFORE cleanup is called.
218      * If this is not a graceful cleanup (because of IOExceptions) then the state
219      * machine will not have terminated yet and is possibly blocked waiting for 
220      * an external event. 
221      */
222     private synchronized void cleanup() {
223         if (!cleanupAlreadyDone) {
224             synchronized (incomingEventQueue) {
225                 incomingEventQueue.notify();
226             }
227             if (wizardSocket != null) {
228                 wizardSocket.close();
229             }
230             view.disconnected();
231             socketHandlerThread = null;
232             cleanupAlreadyDone = true;
233         }
234     }
235 
236 }