1
2
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
83
84
85
86
87 return null;
88 }
89 }
90
91
92 int bytesLeft = messageSize - baos.size();
93
94 while (bytesLeft != 0) {
95 bytesRead = in.read(buf, 0, Math.min(bytesLeft, buf.length));
96
97 if (bytesRead < 0) {
98 throw new EOFException(
99 "Unexpected End Of File reached for "
100 + socket.getInetAddress().getHostAddress());
101 }
102
103 if (bytesRead == 0) {
104 break;
105 }
106
107 baos.write(buf, 0, bytesRead);
108 bytesLeft = messageSize - baos.size();
109 }
110
111
112 if (messageSize == baos.size()) {
113 result = unmarshalMessage();
114 messageSize = -1;
115 baos.reset();
116 }
117 }
118 catch (InterruptedIOException ix) {
119
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) {
268 }
269 finally {
270 in = null;
271 }
272 }
273 if (out != null) {
274 try {
275 out.close();
276 }
277 catch (IOException e) {
278 }
279 finally {
280 out = null;
281 }
282 }
283 if (socket != null) {
284 try {
285 socket.close();
286 }
287 catch (IOException e) {
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 }