/** * MSTviewer.java * * Kenneth J. Goldman * Washington University * September, 2002 * * This is a server to coordinate testing/execution of nodes in a * minimum spanning tree computation. It allows nodes to connect, * constructs the graph, and provides a visualization as the * computation unfolds. In detail... * * What this viewer does: * 1. Opens ServerSocket at PORT_NUM; * 2. Accepts "Registration" messages, adding nodes to the visualization. * Each node is assigned a unique integer identifier (UID). * 3. Expects user to click "start" button when the nodes have conencted. * Nodes that connect afterward "start" is pressed may not be * added to the graph. * 4. Creates edges at random to form a connected graph. (Euclidian * distance is used as the edge cost for natural visualization.) * 5. Sends back a "Registration" message to each node with an edge list. * 6. Sends "Wakeup" messages to nodes the user clicks on. * 7. Forwards messages sent along edges between nodes, monitoring those * messsages to update the visualization. * 8. Resets when user clicks "terminate" button, so a new session can start * without restarting the server. * * What the visualization shows: * 1. Each node with its UID, followed by its current (core,level) pair * as can be determined by the messages sent and received so far. * The node name is shown as mouse-over text. * (Greyed-out nodes have disconnected from the server.) * 2. A "start" button (see item (3) above). * 3. Each graph edge: connecting (white), basic (blue), * rejected (gray), and branch (green). * 4. Messages in transit. * 5. A "pause" button for freezing the global execution. * 6. A slider to control the message latency. * 6. In addition, each message delivered is printed to the standard output. * * What this server expects from its clients: * 1. All messages are instances of the accompanying Message class. * 2. Upon connecting, the client sends a REGISTRATION Message with * a String as the serverData (to be used as the node name). * 3. The server (eventually) replies with a REGISTRATION Message with * a collection of edges as the serverData. The collection is * a TreeMap from Integer objects (edge costs) to Integer objects (UIDs). * NOTE: The mapping is from COSTS TO UIDs (not the other way around). * 4. The server then expects a series of Message objects from the client * to be forwarded to a destination (the destination UID must be one * of the nodes in the clients edge collection). */ import java.io.*; import java.net.*; import java.awt.*; import java.awt.geom.*; import java.awt.event.*; import javax.swing.*; import javax.swing.event.*; import java.util.*; public class MSTviewer extends JFrame implements Runnable { //communication: public static final int PORT_NUM = 12345; ServerSocket ss; //data: static final int MAX_NODES = 50; // limit on the number of nodes in graph boolean started; // true when the algorithm has been started up by user ArrayList nodes = new ArrayList(); // Node objects indexed by unique IDs static int edgeCount = 0; int degree = 2; // incident edges per node (must be EVEN and less than number of nodes) //graphics: static final Color[] msgColors = {Color.red, Color.white, Color.green.darker(), Color.black, Color.green.darker(), Color.black, Color.black, Color.magenta.darker(), Color.black, Color.green.darker(), Color.black}; static final Color TRANSLUCENT_GRAY = new Color(50,50,50,128); static final int MAX_DELAY = 50; static final int WIDTH = 800; static final int HEIGHT = 600; static final int COLUMNS = 35; static final int ROWS = 50; boolean[][] positionsInUse; JButton startButton, endButton; JToggleButton pauseButton; JPanel graphPane; JSlider rateSlider; volatile boolean painting = true; public MSTviewer() { super("distributed MST visualization"); Container mainPane = getContentPane(); mainPane.setLayout(new BorderLayout()); graphPane = new JPanel(); graphPane.setLayout(null); graphPane.setPreferredSize(new Dimension(WIDTH,HEIGHT)); mainPane.add(new JScrollPane(graphPane)); JPanel buttonPanel = new JPanel(); startButton = new JButton("start"); startButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { setStarted(true); startButton.setEnabled(false); } }); buttonPanel.add(startButton); pauseButton = new JToggleButton("pause"); pauseButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { synchronized(pauseButton) { pauseButton.notifyAll(); } } }); buttonPanel.add(pauseButton); endButton = new JButton("terminate"); endButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { closeConnections(); setStarted(false); synchronized(endButton) { endButton.notify(); } } }); buttonPanel.add(endButton); JPanel sliderPanel = new JPanel(new BorderLayout()); rateSlider = new JSlider(JSlider.VERTICAL,0,MAX_DELAY,MAX_DELAY/2); sliderPanel.add(rateSlider); sliderPanel.add(new JLabel("slow"),BorderLayout.SOUTH); sliderPanel.add(new JLabel("fast"),BorderLayout.NORTH); mainPane.add(buttonPanel, BorderLayout.SOUTH); mainPane.add(sliderPanel, BorderLayout.EAST); pack(); show(); addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent we) { closeConnections(); System.exit(0); } }); // keep graph pane refreshed: (new Thread() { public void run() { while (true) { try {Thread.sleep(50);} catch (InterruptedException ie) {} if (painting) graphPane.repaint(); } } }).start(); } synchronized void setStarted(boolean b) {started = b;} synchronized boolean isStarted() {return started;} final int getDelay() { return MAX_DELAY - rateSlider.getValue(); } public void run() { while (true) { try { ss = new ServerSocket(12345); reset(); acceptRegistration(); createEdges(); sendEdges(); waitForTermination(); } catch (Exception e) { System.out.println("Server reset due to: "); e.printStackTrace(); } finally { try {ss.close();} catch (Exception e) {} } } } void reset() { graphPane.removeAll(); positionsInUse = new boolean[ROWS][COLUMNS]; closeConnections(); edgeCount = 0; nodes = new ArrayList(); nodes.add(null); // entry 0 is unused since UID=0 is this server setStarted(false); startButton.setEnabled(true); } void closeConnections() { Iterator it = nodes.iterator(); while (it.hasNext()) { Node n = (Node) it.next(); if (n != null) { n.closeConnection("normal termination"); } } } void acceptRegistration() { try { ss.setSoTimeout(1000); // wait at most 1 second for each accept call while ((nodes.size() < MAX_NODES+1 || nodes.size() <= 3) && !isStarted()) { try { nodes.add(new Node(ss.accept(), nodes.size())); } catch (Exception e) {} } } catch (SocketException se) { System.out.println("Registration terminated due to: " + se); } } int getDegreeFromUser() { int maxDegree = nodes.size()-2; if (maxDegree == 2) return 2; boolean requireEvenDegree = (nodes.size() % 2 == 0); // incl. dummy node painting = false; final JSlider s = new JSlider(2,maxDegree,Math.min(maxDegree,degree)); s.setMajorTickSpacing(maxDegree-2); if (requireEvenDegree) s.setMinorTickSpacing(2); else s.setMinorTickSpacing(1); s.setSnapToTicks(true); s.setPaintTicks(true); s.setPaintLabels(true); final String suffix = (requireEvenDegree) ? " (must be even)" : ""; final JLabel label = new JLabel("Your choice: "+ (int) s.getValue() + suffix); label.setForeground(Color.blue); s.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent ce) { label.setText("Your choice: "+ (int) s.getValue() + suffix); } }); JPanel panel = new JPanel(new BorderLayout()); panel.add(new JLabel("Select the minimum number of edges per node."), BorderLayout.NORTH); panel.add(s); panel.add(label,BorderLayout.SOUTH); JOptionPane.showMessageDialog(this, panel,"Choose degree", JOptionPane.QUESTION_MESSAGE); painting = true; return s.getValue(); } void createEdges() { degree = getDegreeFromUser(); // first, create a cycle through the graph to ensure connectedness for (int i = 1; i < nodes.size()-1; i++) { new Edge((Node) nodes.get(i), (Node) nodes.get(i+1)); } new Edge((Node) nodes.get(1), (Node) nodes.get(nodes.size()-1)); // At this point, all nodes have degree 2. // Now add random edges to bring each node up to the proper degree. int edgeLimit = Math.min(degree, nodes.size()-1); for (int currentLimit = 4; currentLimit <= edgeLimit; currentLimit+=2) { Iterator it = nodes.iterator(); while (it.hasNext()) { Node node = (Node) it.next(); if (node != null) { while (node.getNeighbors().size() < currentLimit) { int candidate = (int) (Math.random() * (nodes.size()-1)) + 1; Node dest = (Node) nodes.get(candidate); if (dest != node && dest.getNeighbors().size() < currentLimit+1 && !dest.getNeighbors().containsKey(node)) { new Edge(node,dest); } } } } } } void sendEdges() { Iterator it = nodes.iterator(); while (it.hasNext()) { Node node = (Node) it.next(); if (node != null) // skip dummy entry 0 node.sendEdges(); } } void waitForTermination() { while (isStarted()) synchronized(endButton) { try {endButton.wait();} catch (InterruptedException ie) {} } System.out.println("\n SESSION TERMINATED BY USER \n"); } synchronized Point computeNodePosition() { /* PURELY RANDOM ALTERNATIVE return new Point((int) (Math.random()*WIDTH), (int) (Math.random()*HEIGHT)); */ // ALTERNATIVE FOR POSITIONING ON GRID: boolean overlap = true; int row = 0; int col = 0; while (overlap) { row = (int) (Math.random()*ROWS); col = (int) (Math.random()*COLUMNS); // to avoid crowding, check the neighborhood around the node overlap = false; for (int c = Math.max(0,col-1); c <= Math.min(COLUMNS-1,col+1); c++) for (int r = Math.max(0,row-1); r <= Math.min(ROWS-1,row+1); r++) if (positionsInUse[r][c]) overlap = true; } positionsInUse[row][col] = true; return new Point(WIDTH/COLUMNS*col,HEIGHT/ROWS*row); } class Node extends JButton implements Runnable { Socket socket; int UID; String name; int level; int core; ObjectOutputStream oos; ObjectInputStream ois; HashMap neighbors; // maps neighboring nodes to edges boolean terminated; Node(Socket socket, int UID) { this.socket = socket; this.UID = UID; this.core = UID; this.name = "?"; neighbors = new HashMap(); addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { sendMessage(new Message(Message.WAKEUP,0,Node.this.UID)); } }); (new Thread(this)).start(); setOpaque(false); setBorder(null); } HashMap getNeighbors() { return neighbors; } void updateLabel() { setText(UID+"("+core + "," + level + ")"); this.setSize(this.getPreferredSize()); } public void run() { updateLabel(); setToolTipText("node " + UID); this.setLocation(computeNodePosition()); graphPane.add(this); try { oos = new ObjectOutputStream(socket.getOutputStream()); ois = new ObjectInputStream(socket.getInputStream()); receiveMessages(); } catch (Exception e) { closeConnection(e); } } synchronized void sendMessage(Message m) { try { oos.writeObject(m); System.out.println("delivered " + m); } catch (Exception e) { closeConnection(e); } } void sendEdges() { final TreeMap h = new TreeMap(); Iterator it = neighbors.keySet().iterator(); while (it.hasNext()) { Node n = (Node) it.next(); h.put(new Integer(((Edge) neighbors.get(n)).getCost()), new Integer(n.UID)); } (new Thread() { public void run() { sendMessage(new Message(UID,h)); } }).start(); } void receiveMessages() { try { while (true) { Message m = (Message) ois.readObject(); switch (m.messageType) { case Message.REGISTRATION: name = (String) m.serverData; updateLabel(); setToolTipText("node " + UID + ": " + name); break; case Message.WAKEUP: closeConnection("node sent Wakeup message"); break; case Message.INITIATE: case Message.INFORM: level = m.level; core = m.core; updateLabel(); // don't break default: forwardMessage(m); } } } catch (Exception e) { closeConnection(e); } } void forwardMessage(Message m) { if (m.destination == UID) sendMessage(m); else { Node n = (Node) nodes.get(m.destination); Edge e = (Edge) neighbors.get(n); e.forwardMessage(this,n,m); } } void closeConnection(Object reason) { this.setEnabled(false); if (!terminated) { // print only if not terminated yet System.out.println("Closing socket to " + name + " due to: " + reason); if (reason instanceof Exception) ((Exception) reason).printStackTrace(); } try {socket.close();} catch (Exception se) {} terminated = true; } Point2D getCenter() { return new Point(this.getX() + this.getWidth()/2, this.getY() + this.getHeight()/2); } }//Node class Edge extends ShapeComponent { Node a, b; int edgeID; int inTransit; boolean isBranch; LinkedList aQueue, bQueue; Edge(Node a, Node b) { super(new Line2D.Double(a.getCenter(),b.getCenter()), Color.blue); edgeID = edgeCount++; graphPane.add(this); // graphPane.repaint(); this.a = a; this.b = b; a.getNeighbors().put(b,this); b.getNeighbors().put(a,this); aQueue = new LinkedList(); // ensures FIFO delivery from a to b bQueue = new LinkedList(); // ensures FIFO delivery from b to a } int getCost() { // use Euclidian distance, with unique low order bits return ((int) a.getCenter().distance(b.getCenter()))*1000 + edgeID; } void forwardMessage(final Node src, final Node dest, final Message m) { if (!((src == a && dest == b) || (src == b && dest == a))) throw new IllegalArgumentException("message " + m + " can't travel along " + this); final LinkedList messageQueue; if (src == a) { messageQueue = aQueue; } else { messageQueue = bQueue; } final int priorCount = messageQueue.size(); synchronized (messageQueue) { messageQueue.addLast(m); // enqueue } switch (m.messageType) { case Message.REJECT: if (!isBranch) this.setForeground(TRANSLUCENT_GRAY); break; case Message.CONNECT: this.setForeground(Color.white); this.repaint(); break; case Message.INITIATE: case Message.INFORM: isBranch = true; this.setForeground(Color.red); this.repaint(); dest.level = m.level; dest.core = m.core; dest.updateLabel(); } (new Thread() { public void run() { // animation from src to dest, then delivery if (getDelay() > 0) { try { Thread.sleep(getDelay()*20*priorCount); } catch (InterruptedException ie) {} JLabel msgView = new JLabel(m.shortString()); msgView.setForeground(msgColors[m.messageType]); Point2D srcPos = src.getCenter(); Point2D destPos = dest.getCenter(); msgView.setSize(msgView.getPreferredSize()); msgView.setLocation((int) srcPos.getX(), (int) srcPos.getY()); graphPane.add(msgView); int steps = 50; int delay = 20; double deltaX = ((double) destPos.getX() - srcPos.getX()) / steps; double deltaY = ((double) destPos.getY() - srcPos.getY()) / steps; for (int i = 0; i < steps; i++) { msgView.setLocation((int) (srcPos.getX() + deltaX*i), (int) (srcPos.getY() + deltaY*i)); // graphPane.repaint(); waitIfPaused(); try { Thread.sleep(getDelay()); } catch (InterruptedException ie) {} } graphPane.remove(msgView); // graphPane.repaint(); } Message toDeliver; synchronized (messageQueue) { toDeliver = (Message) messageQueue.removeFirst(); //dequeue } dest.sendMessage(toDeliver); // message is delivered here inTransit--; } }).start(); } void waitIfPaused() { if (pauseButton.isSelected()) synchronized (pauseButton) { while (pauseButton.isSelected()) try { pauseButton.wait(); } catch (InterruptedException ie) {} } } public String toString() { return "edge (" + a.UID + "," + b.UID + ")"; } } public static void main(String[] args) { MSTviewer viewer = new MSTviewer(); (new Thread(viewer)).start(); } }