4 * Copyright 2015 TJ <hacker@iam.tj>.
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:
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
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
24 package uk.ac.ntu.n0521366.wsyd.libs.net;
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;
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.
44 * @see NetworkServerAbstract
45 * @author TJ <hacker@iam.tj>
47 public class NetworkServerUDP extends NetworkServerAbstract {
51 private DatagramSocket _datagramSocket = null;
54 * Maximum size of UDP packet payload
56 public static final int UDP_PAYLOAD_SIZE_MAX = 65507;
59 * Thread safe First In, First Out Queue of NetworkMessage objects waiting to be sent.
61 * Allows the Owner Thread to submit new messages for sending that the Worker Thread
64 protected ConcurrentLinkedQueue<NetworkMessage> _sendMessageQueue = new ConcurrentLinkedQueue<>();
67 * Construct the server with a Logger.
69 * No socket is opened.
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
76 public NetworkServerUDP(WSYD_SocketAddress socketAddress, String title, ServiceAddressMap serviceToHostMap, Logger logger) {
77 super(socketAddress, title, serviceToHostMap, logger);
81 * Construct the server without a Logger.
83 * No socket is opened.
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
89 public NetworkServerUDP(WSYD_SocketAddress socketAddress, String title, ServiceAddressMap serviceToHostMap) {
90 super(socketAddress, title, serviceToHostMap);
94 * Get the DatagramSocket for this service.
98 public DatagramSocket getSocket() {
99 return this._datagramSocket;
103 * Open the socket ready for accepting packets.
105 * It should also set a reasonable socket timeout with a call to setSoTimeout()
106 * to prevent unnecessary blocking.
108 * @throws SocketException
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()
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());
126 * @throws SocketException
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();
136 * Accept packet from remote hosts.
140 * @return true if the server should continue listening
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;
149 /* blocks waiting for packet until socket timeout expires, when SocketTimeOut
150 * exception is thrown.
152 if (this.getSocket() != null)
153 this.getSocket().receive(packetReceive);
155 throw new SocketTimeoutException("No socket available!");
157 // packet was received
158 messageReceived = NetworkMessage.deserialize(packetReceive.getData());
161 if (!messageReceived.getSender().equals(_title)) {
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()));
168 // pass the message to the process() method in the Owner Thread
169 publish(messageReceived);
171 result = true; // successful
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));
185 } catch (ObjectStreamException e) {
186 /* order of these Exception catches is important
187 * Deeper sub-classes must be listed before their super classes
189 // TODO: serverListen() add ObjectStreamException handler
190 System.err.println("ObjectStreamException");
192 } catch (IOException e) {
193 // TODO: serverListen() add IOException handler
194 System.err.println("IOException");
196 } catch (ClassNotFoundException e) {
197 // TODO: serverListen() add ClassNotFoundException handler
198 System.err.println("ClassNotFoundException");
205 * Send an unsolicited message to a remote service.
207 * This method is called by the main worker loop if there is a message to
210 * @param message must have its _serviceTarget parameter set
211 * @return true if the message was sent
214 protected boolean serverSend(NetworkMessage message) {
215 boolean result = false;
217 if (message != null) {
218 ServiceAddressMap.LastSeenHost host = _serviceToHostMap.get(message.getTarget());
220 InetSocketAddress address = host.address;
221 if (address != null) {
222 message.setSender(_title);
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());
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}",
237 packetSend.getAddress().getHostAddress(),
242 result = true; // successful
243 } catch (IOException e) {
244 // TODO: serverSend() add IOException handler
249 log(Level.WARNING, _title, MessageFormat.format("Unable to send message for \"{0}\" to unknown target \"{1}\"", message.getIntent(), message.getTarget()));
256 * Removes a message from the queue of pending messages.
258 * This method is called on the Worker Thread by the doInBackground() main loop.
260 * @return a message to be sent
263 protected NetworkMessage sendMessage() {
264 return this._sendMessageQueue.poll();
268 /* XXX: Methods below here all execute on the GUI Event Dispatch Thread */
271 * Clean up after doInBackground() has returned.
273 * This method will run on the Owner Thread so must complete quickly.
276 protected void done() {
277 // TODO: done() implement any clean-up after doInBackground() has returned
281 * Adds a message to the queue of pending messages.
283 * This method will usually be called from the Owner Thread.
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
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();
295 throw new IllegalArgumentException("target cannot be null");
296 if(!_serviceToHostMap.isServiceValid(target))
297 throw new IllegalArgumentException("target service does not exist: " + target);
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