View Javadoc

1   /*
2   @See License.txt@
3    */
4   
5   package spellcast.net;
6   
7   import java.io.BufferedInputStream;
8   import java.io.BufferedOutputStream;
9   import java.io.ByteArrayInputStream;
10  import java.io.ByteArrayOutputStream;
11  import java.io.DataInputStream;
12  import java.io.DataOutputStream;
13  import java.io.EOFException;
14  import java.io.IOException;
15  import java.io.InterruptedIOException;
16  import java.io.ObjectInputStream;
17  import java.io.ObjectOutputStream;
18  import java.net.Socket;
19  
20  import org.apache.log4j.Logger;
21  import spellcast.event.GameEvent;
22  import spellcast.event.MessageEvent;
23  import spellcast.game.IPCHandle;
24  import spellcast.model.Id;
25  
26  /***
27   * A Wizard Socket encapsulates the network communication required
28   * to send and receive game events.
29   *
30   * @author Barrie Treloar
31   */
32  public class WizardSocket implements IPCHandle {
33      private static final int DEFAULT_BUFFER_SIZE = 256;
34  
35      private Socket socket;
36      private Id id;
37      private ByteArrayOutputStream baos;
38      private int messageSize;
39      private DataInputStream in;
40      private DataOutputStream out;
41      private VersionDetails programVersionDetails;
42  
43      private static final Logger logger = Logger.getLogger("wizard.net");
44  
45      public WizardSocket(VersionDetails programVersionDetails) {
46          messageSize = -1;
47          id = Id.NO_ONE;
48          this.programVersionDetails = programVersionDetails;
49          baos = new ByteArrayOutputStream();
50      }
51  
52      /***
53       * Receive a message from the socket.
54       * If an entire message is not read in then return null.
55       * Each time this method is called try to build up an entire message.
56       * When one has been completely received then return it.
57       * <p>
58       * The format of the message is:
59       * <ul>
60       *   <li> MessageLength (int)
61       *   <li> MessageBody (see marshalMessage and unmarshalMessage)
62       * </ul>
63       * 
64       * @see marshalMessage
65       * @see unmarshalMessage
66       *
67       * @throws IOException if an I/O error occurs
68       */
69      public GameEvent receive() throws IOException {
70          GameEvent result = null;
71  
72          try {
73              byte[] buf = new byte[DEFAULT_BUFFER_SIZE];
74              int bytesRead = 0;
75  
76              if (messageSize == -1) {
77                  try {
78                      messageSize = in.readInt();
79                      logger.debug("Message size: " + messageSize);
80                  }
81                  catch (EOFException e) {
82                      // If we are trying to read a new message its possible
83                      // that we have yet to process a disconnect message
84                      // because of multiple threads.
85                      // Ignore an EOFException here.  If there truly is
86                      // an exception it should get taken care of in the send phase.
87                      return null;
88                  }
89              }
90  
91              // Determine how many more bytes to read in for a complete message;
92              int bytesLeft = messageSize - baos.size();
93              // Try to read the minimum of the buffer size or bytesLeft
94              while (bytesLeft != 0) {
95                  bytesRead = in.read(buf, 0, Math.min(bytesLeft, buf.length));
96                  // If EOF reached throw an exception
97                  if (bytesRead < 0) {
98                      throw new EOFException(
99                          "Unexpected End Of File reached for "
100                             + socket.getInetAddress().getHostAddress());
101                 }
102                 // if nothing read in then break out of loop
103                 if (bytesRead == 0) {
104                     break;
105                 }
106                 // otherwise add the data read in to our message buffer
107                 baos.write(buf, 0, bytesRead);
108                 bytesLeft = messageSize - baos.size();
109             }
110 
111             // Check for a complete message
112             if (messageSize == baos.size()) {
113                 result = unmarshalMessage();
114                 messageSize = -1;
115                 baos.reset();
116             }
117         }
118         catch (InterruptedIOException ix) {
119             // Ignore interrupted exceptions
120         }
121         return result;
122     }
123 
124     /***
125      * Send the GameEvent down the socket. 
126      * The format of the message is:
127      * <ul>
128      *   <li> MessageLength (int)
129      *   <li> MessageBody (see marshalMessage and unmarshalMessage)
130      * </ul>
131      * 
132      * @see marshalMessage
133      * @see unmarshalMessage
134      */
135     public void send(GameEvent e) throws IOException {
136         byte[] messageBytes = marshalMessage(e);
137 
138         out.writeInt(messageBytes.length);
139         out.write(messageBytes, 0, messageBytes.length);
140         out.flush();
141         logger.debug("Message size: " + messageBytes.length);
142     }
143 
144     /***
145      * Construct the message header and payload
146      * <p>
147      * The format of the message is:
148      * <ul>
149      *   <li> Program Name (String)
150      *   <li> Protocol Major Version Number (int)
151      *   <li> Protocol Minor Version Number (int)
152      *   <li> Protocol Revision Version Number (int)
153      *   <li> Payload (Serialized Object)
154      * </ul>
155      */
156     private byte[] marshalMessage(GameEvent e) throws IOException {
157         ByteArrayOutputStream messageBuffer = new ByteArrayOutputStream();
158         DataOutputStream dos = new DataOutputStream(messageBuffer);
159 
160         dos.writeUTF(programVersionDetails.getProgramName());
161         dos.writeInt(programVersionDetails.getMajorVersionNumber());
162         dos.writeInt(programVersionDetails.getMinorVersionNumber());
163         dos.writeInt(programVersionDetails.getRevisionVersionNumber());
164         int headerSize = messageBuffer.size();
165         logger.debug("Header Size: " + headerSize);
166         ObjectOutputStream oos = new ObjectOutputStream(dos);
167         oos.writeObject(e);
168         logger.debug("Payload Size: " + (messageBuffer.size() - headerSize));
169 
170         return messageBuffer.toByteArray();
171     }
172 
173     /***
174      * Check the message header to ensure its protocol is compliant.
175      * Take out the payload and re-construct the serialized GameEvent and
176      * return it.
177      * <p>
178      * If the protocol is found to be non-compliant, send a return message
179      * indicating the failure and throw an IOException.
180      * Only the major version number is important.
181      * <p>
182      * If the major version number is less than the program's major version number
183      * then the version is too old.<br>
184      * If the major version number is greater than the program's major version number 
185      * then the version is too new.<br>
186      * Only when the major version number is identical to the program's major version
187      * number is the version ok.
188      * <p>
189      * The format of the message is:
190      * <ul>
191      *   <li> Program Name (String)
192      *   <li> Protocol Major Version Number (int)
193      *   <li> Protocol Minor Version Number (int)
194      *   <li> Protocol Revision Version Number (int)
195      *   <li> Payload (Serialized Object)
196      * </ul>
197      */
198     private GameEvent unmarshalMessage() throws IOException {
199         GameEvent result = null;
200         ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
201         DataInputStream dis = new DataInputStream(bais);
202 
203         int messageSize = dis.available();
204         VersionDetails packetVersionDetails =
205             new VersionDetails(dis.readUTF(), dis.readInt(), dis.readInt(), dis.readInt());
206         int payloadSize = dis.available();
207         logger.debug("Header Size: " + (messageSize - payloadSize));
208         logger.debug("Payload Size: " + payloadSize);
209 
210         if (packetVersionDetails.getMajorVersionNumber()
211             < programVersionDetails.getMajorVersionNumber()) {
212             MessageEvent e =
213                 new MessageEvent(
214                     "Spellcast: Your client version is earlier than the server's version ("
215                         + programVersionDetails
216                         + ").  Please upgrade your client.");
217             send(e);
218             throw new IOException(
219                 "Message protocol from "
220                     + socket.getInetAddress().getHostAddress()
221                     + " is too old.  Version Details received: "
222                     + packetVersionDetails);
223         }
224         else
225             if (packetVersionDetails.getMajorVersionNumber()
226                 > programVersionDetails.getMajorVersionNumber()) {
227                 MessageEvent e =
228                     new MessageEvent(
229                         "Spellcast: Your client version is later than the server's version ("
230                             + programVersionDetails
231                             + ").  Please upgrade the server.");
232                 send(e);
233                 throw new IOException(
234                     "Message protocol from "
235                         + socket.getInetAddress().getHostAddress()
236                         + " is too new.  Version Details received: "
237                         + packetVersionDetails);
238             }
239 
240         try {
241             ObjectInputStream ois = new ObjectInputStream(dis);
242             Object o = ois.readObject();
243             if (!(o instanceof GameEvent)) {
244                 throw new IOException(
245                     "Payload of message does not contain a GameEvent. "
246                         + "Instead received: "
247                         + o.getClass().getName());
248             }
249             result = (GameEvent) o;
250         }
251         catch (ClassNotFoundException cnfe) {
252             throw new IOException(cnfe.getMessage());
253         }
254 
255         return result;
256     }
257 
258     /***
259      * Immediately closes all input and output streams and closes the socket.
260      * Other classes should ensure that no threads are currently using this object.
261      */
262     public void close() {
263         if (in != null) {
264             try {
265                 in.close();
266             }
267             catch (IOException e) { /* exception ignored */
268             }
269             finally {
270                 in = null;
271             }
272         }
273         if (out != null) {
274             try {
275                 out.close();
276             }
277             catch (IOException e) { /* exception ignored */
278             }
279             finally {
280                 out = null;
281             }
282         }
283         if (socket != null) {
284             try {
285                 socket.close();
286             }
287             catch (IOException e) { /* exception ignored */
288             }
289             finally {
290                 socket = null;
291             }
292         }
293     }
294 
295     /***
296      * Get the value of socket.
297      *
298      * @return value of socket.
299      */
300     public Socket getSocket() {
301         return socket;
302     }
303 
304     /***
305      * Set the value of socket.
306      *
307      * @param v Value to assign to socket.
308      */
309     public void setSocket(Socket v) throws IOException {
310         socket = v;
311         in = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
312         out = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream()));
313     }
314 
315     /***
316      * Get the value of id.
317      *
318      * @return value of id.
319      */
320     public Id getID() {
321         return id;
322     }
323 
324     /***
325      * Set the value of id.
326      *
327      * @param v Value to assign to id.
328      */
329     public void setID(Id v) {
330         id = v;
331     }
332 
333     /***
334      * Indicates whether the IPC system has connected the client yet.
335      * The WizardSocket is only connected when it has an Id that is not
336      * <code>Id.NO_ONE</code>.
337      */
338     public boolean isConnected() {
339         return !getID().equals(Id.NO_ONE);
340     }
341 
342 }