fda95b521925502c17510b223361c5cae896a012
[WeStealzYourDataz.git] / src / uk / ac / ntu / n0521366 / wsyd / libs / net / NetworkServerUDP.java
1 /*
2  * The MIT License
3  *
4  * Copyright 2015 TJ <hacker@iam.tj>.
5  *
6  * Permission is hereby granted, free of charge, to any person obtaining a copy
7  * of this software and associated documentation files (the "Software"), to deal
8  * in the Software without restriction, including without limitation the rights
9  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10  * copies of the Software, and to permit persons to whom the Software is
11  * furnished to do so, subject to the following conditions:
12  *
13  * The above copyright notice and this permission notice shall be included in
14  * all copies or substantial portions of the Software.
15  *
16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22  * THE SOFTWARE.
23  */
24 package uk.ac.ntu.n0521366.wsyd.libs.net;
25
26 import java.text.MessageFormat;
27 import java.io.IOException;
28 import java.io.ObjectStreamException;
29 import java.net.InetSocketAddress;
30 import java.net.DatagramSocket;
31 import java.net.DatagramPacket;
32 import java.net.SocketException;
33 import java.net.SocketTimeoutException;
34 import java.util.concurrent.ConcurrentLinkedQueue;
35 import java.util.logging.Level;
36 import java.util.logging.Logger;
37 import java.util.logging.LogRecord;
38 import uk.ac.ntu.n0521366.wsyd.libs.message.MessageLogRecord;
39
40 /**
41  * Dual-use multithreading network UDP server that can be used stand-alone
42  * or in a Swing GUI application as a background worker thread.
43  *
44  * @see NetworkServerAbstract
45  * @author TJ <hacker@iam.tj>
46  */
47 public class NetworkServerUDP extends NetworkServerAbstract {
48     /**
49      * Server socket.
50      */
51     private DatagramSocket _datagramSocket = null;
52     
53     /**
54      * Maximum size of UDP packet payload
55      */
56     public static final int UDP_PAYLOAD_SIZE_MAX =  65507;
57
58     /**
59      * Thread safe First In, First Out Queue of NetworkMessage objects waiting to be sent.
60      * 
61      * Allows the Owner Thread to submit new messages for sending that the Worker Thread
62      * can safely access.
63      */
64     protected ConcurrentLinkedQueue<NetworkMessage> _sendMessageQueue = new ConcurrentLinkedQueue<>();
65    
66     /**
67      * Construct the server with a Logger.
68      * 
69      * No socket is opened.
70      * 
71      * @param socketAddress The socket to listen on
72      * @param title source identifier for use in log messages and sent NetworkMessage objects
73      * @param serviceToHostMap the map object used for host <> InetSocketAddress lookups
74      * @param logger An instance of Logger to be used by all objects of this class
75      */
76     public NetworkServerUDP(WSYD_SocketAddress socketAddress, String title, ServiceAddressMap serviceToHostMap, Logger logger) {
77         super(socketAddress, title, serviceToHostMap, logger);
78     }
79
80     /**
81      * Construct the server without a Logger.
82      * 
83      * No socket is opened.
84      * 
85      * @param socketAddress The socket to listen on
86      * @param title source identifier for use in log messages and sent NetworkMessage objects
87      * @param serviceToHostMap the map object used for host <> InetSocketAddress lookups
88      */
89     public NetworkServerUDP(WSYD_SocketAddress socketAddress, String title, ServiceAddressMap serviceToHostMap) {
90         super(socketAddress, title, serviceToHostMap);
91     }
92
93     /**
94      * Get the DatagramSocket for this service.
95      * 
96      * @return the socket
97      */
98     public DatagramSocket getSocket() {
99         return this._datagramSocket;
100     }
101
102     /**
103      * Open the socket ready for accepting packets.
104      * 
105      * It should also set a reasonable socket timeout with a call to setSoTimeout()
106      * to prevent unnecessary blocking.
107      * 
108      * @throws SocketException 
109      */
110     @Override
111     public  void serverOpen() throws SocketException {
112         _datagramSocket = new DatagramSocket(_socketAddress.getPort(), _socketAddress.getAddress());
113         _datagramSocket.setSoTimeout(100); // 1/10th second blocking timeout on receive()
114
115         if (_socketAddress.getPort() == Network.PORTS_EPHEMERAL) {
116             // reflect the actual port in use if an ephermal port was requested
117             InetSocketAddress actualSA = (InetSocketAddress)_datagramSocket.getLocalSocketAddress();
118             _socketAddress.setAddress(actualSA.getAddress());
119             _socketAddress.setPort(actualSA.getPort());
120         }
121     }
122     
123     /**
124      * Close the socket.
125      * 
126      * @throws SocketException
127      */
128     @Override
129     public void serverClose() throws SocketException {
130         // use 'this' to ensure sub-classes refer to their own '_datagramSocket' when inheriting this method
131         if (this._datagramSocket != null)
132             this._datagramSocket.close();
133     }
134
135     /**
136      * Accept packet from remote hosts.
137      * 
138      * 
139      * 
140      * @return true if the server should continue listening
141      */
142     @Override
143     public boolean serverListen() {
144         boolean result = false;
145         byte[] dataReceive = new byte[UDP_PAYLOAD_SIZE_MAX];
146         DatagramPacket packetReceive = new DatagramPacket(dataReceive, dataReceive.length);
147         NetworkMessage messageReceived;
148         try {
149             /* blocks waiting for packet until socket timeout expires, when SocketTimeOut
150              * exception is thrown.
151             */
152             if (this.getSocket() != null)
153                 this.getSocket().receive(packetReceive);
154             else
155                 throw new SocketTimeoutException("No socket available!");
156             
157             // packet was received
158             messageReceived = NetworkMessage.deserialize(packetReceive.getData());
159             
160             // prevent loopbacks
161             if (!messageReceived.getSender().equals(_title)) {
162             
163                 // add or update the last-seen time of the Sender host in the known services map
164                 ServiceAddressMap.LastSeenHost host = new ServiceAddressMap.LastSeenHost((InetSocketAddress)packetReceive.getSocketAddress());
165                 this._serviceToHostMap.put(messageReceived.getSender(), host);
166                 log(Level.FINEST, _title, MessageFormat.format("Added \"{0}\" to service map", messageReceived.getSender()));
167
168                 // pass the message to the process() method in the Owner Thread
169                 publish(messageReceived);
170
171                 result = true; // successful
172             }
173
174         } catch (SocketTimeoutException e) {
175             result = false; // no packet received
176             if (this._simulate) {
177                 LogRecord record = new LogRecord(Level.FINEST, "Simulated received message");
178                 record.setSourceClassName("Simulator");
179                 record.setMillis(System.currentTimeMillis());
180                 MessageLogRecord m = new MessageLogRecord(record);
181                 publish(new NetworkMessage("Log","Simulator",m));
182                 result = true;
183             }
184             
185         } catch (ObjectStreamException e) {
186             /* order of these Exception catches is important
187              * Deeper sub-classes must be listed before their super classes
188              */
189             // TODO: serverListen() add ObjectStreamException handler
190             System.err.println("ObjectStreamException");
191
192         } catch (IOException e) {
193             // TODO: serverListen() add IOException handler
194             System.err.println("IOException");
195
196         } catch (ClassNotFoundException e) {
197             // TODO: serverListen() add ClassNotFoundException handler
198             System.err.println("ClassNotFoundException");
199         }
200
201         return result;
202     }
203
204     /**
205      * Send an unsolicited message to a remote service.
206      * 
207      * This method is called by the main worker loop if there is a message to
208      * be sent.
209      * 
210      * @param message must have its _serviceTarget parameter set
211      * @return true if the message was sent
212      */
213     @Override
214     protected boolean serverSend(NetworkMessage message) {
215         boolean result = false;
216
217         if (message != null) {
218             ServiceAddressMap.LastSeenHost host = _serviceToHostMap.get(message.getTarget());
219             if (host != null) {
220                 InetSocketAddress address = host.address;
221                 if (address != null) {
222                     message.setSender(_title);
223                     try {
224                         byte[] dataSend = NetworkMessage.serialize(message);
225                         DatagramPacket packetSend = new DatagramPacket(dataSend, dataSend.length);
226                         // set target's remote host address and port
227                         packetSend.setAddress(address.getAddress());
228                         packetSend.setPort(address.getPort());
229
230                         // acknowledge receipt
231                         this.getSocket().send(packetSend);
232                         log(Level.FINEST, _title,
233                             MessageFormat.format("Sending packet for {0} to {1} ({3}:{4,number,#####}) from {2}",
234                                 message.getIntent(),
235                                 message.getTarget(),
236                                 message.getSender(),
237                                 packetSend.getAddress().getHostAddress(),
238                                 packetSend.getPort()
239                             )
240                         );
241
242                         result = true; // successful
243                     } catch (IOException e) {
244                         // TODO: serverSend() add IOException handler
245                         e.printStackTrace();
246                     }
247                 }
248             } else {
249                 log(Level.WARNING, _title, MessageFormat.format("Unable to send message for \"{0}\" to unknown target \"{1}\"", message.getIntent(), message.getTarget()));
250             }
251         }
252         return result;
253     }
254
255      /**
256      * Removes a message from the queue of pending messages.
257      *
258      * This method is called on the Worker Thread by the doInBackground() main loop.
259      *
260      * @return a message to be sent
261      */
262     @Override
263     protected NetworkMessage sendMessage() {
264         return this._sendMessageQueue.poll();
265     }
266     
267     
268     /* XXX: Methods below here all execute on the GUI Event Dispatch Thread */
269     
270     /**
271      * Clean up after doInBackground() has returned.
272      * 
273      * This method will run on the Owner Thread so must complete quickly.
274      */
275     @Override
276     protected  void done() {
277         // TODO: done() implement any clean-up after doInBackground() has returned
278     }
279
280     /**
281      * Adds a message to the queue of pending messages.
282      * 
283      * This method will usually be called from the Owner Thread.
284      * 
285      * @param message to be sent
286      * @return true if the message was added to the queue
287      * @throws IllegalArgumentException if the target does not exist in the serviceToHost mapping
288      */
289     public boolean queueMessage(NetworkMessage message) throws IllegalArgumentException {
290         boolean result = false;
291         if (message != null) {
292             // ensure the target is set and is a valid service
293             String target = message.getTarget();
294             if (target == null)
295                 throw new IllegalArgumentException("target cannot be null");
296             if(!_serviceToHostMap.isServiceValid(target))
297                 throw new IllegalArgumentException("target service does not exist: " + target);
298             
299             NetworkMessage temp;
300             try { // make a deep clone of the message
301                 temp = NetworkMessage.clone(message);
302                 result = this._sendMessageQueue.add(temp);
303             } catch (CloneNotSupportedException e) {
304                 // TODO: queueMessage() log CloneNotSupportedException
305                 e.printStackTrace();
306             }
307         }
308         return result;
309     }    
310 }