//lots of imports here
import java.util.*; // name-IP address map
import java.net.*; // several Socket classes and InetAddress
import java.io.*; // IOException and a couple *Writers
import java.awt.*; // GUI elements
import java.awt.event.*; // GUI event handlers
import java.util.regex.*; // regular expression library, used to prevent a minor exploit
// changed the class/file name, class names should usually start with a capital
// subclass java.awt.Frame (a window) so we can be displayed on-screen
// implement WindowListener and ActionListener (from java.awt.event) so we can handle our own events
// implement java.lang.Runnable so we can listen for incoming messages in a seperate thread
// (I've reciently realized multi-threading wasn't actually necessary...)
public class uChat extends Frame implements WindowListener, ActionListener, Runnable {
// NOTE: within source code, I tend to use "us", "we", "our", etc. to refer to the class/program and it's actions. Don't ask why, I don't know.
// network-related vars
InetAddress addrs; // broadcast IP address, declared below
int port = 12121; // communication port number
DatagramSocket sock; // multi-purpose UDP socket
// GUI elements
TextField name; // top text field
TextArea disp; // main text area
TextField input; // bottom text field
Button send; // obvious
Button connect; // not really "connecting" but close enough
Map lookupTable; // keeps track of which user(name) is at which IP address
// constructor, throws any Exception because I was too lazy to list each one that could happen
// NOT A BEST PRACTICE
public uChat() throws Exception {
super("uChat"); // call superclass constructor: Frame(String s); sets window title
// retrieve broadcast IP address, method may (but shouldn't with a numerical address) throw UnknownHostException
addrs = InetAddress.getByName("255.255.255.255");
// initialze Socket, and bind to the port
sock = new DatagramSocket(port);
// initialize the lookup table as a HashMap
lookupTable = new HashMap();
// set up GUI elements
disp = new TextArea("",0,0,TextArea.SCROLLBARS_VERTICAL_ONLY); // no starting text, default height and width, scrollbar display policy
input = new TextField();
name = new TextField();
send = new Button("Send"); // buttons need
connect = new Button("Join"); // labels
// prevent editing the message display box
disp.setEditable(false);
//GUI layout design can be pretty hairy
// a java.awt.Panel is a simple container
// constructor accept java.awt.LayoutManager as an arg, BorderLayout is a subtype
Panel pnl1 = new Panel(new BorderLayout());
// add an anonymous label, followed by our name field and connect button
pnl1.add(new Label("Name:"), BorderLayout.WEST);
pnl1.add(name, BorderLayout.CENTER);
pnl1.add(connect, BorderLayout.EAST);
// the lower Panel, with the input field and send button
Panel pnl2 = new Panel(new BorderLayout());
pnl2.add(input,BorderLayout.CENTER);
pnl2.add(send,BorderLayout.EAST);
// set the our layout and add the panels and display area
setLayout(new BorderLayout());
add(pnl1, BorderLayout.NORTH);
add(disp, BorderLayout.CENTER);
add(pnl2, BorderLayout.SOUTH);
// set up a new thread and pass us as the Runnable
Thread t = new Thread(this);
// start the thread
t.start();
// add us as ActionListener for the main GUI elements
send.addActionListener(this);
connect.addActionListener(this);
input.addActionListener(this);
name.addActionListener(this);
// add us as a WindowListener for ourself
addWindowListener(this);
// display the main window
setVisible(true);
} // end of constructor
// override Frame.setVisible(boolean b) to add some functionallity
public void setVisible(boolean b) {
super.setVisible(b); // call Frame.setVisible() to actually show/hide the window
if(b) { // if we're being shown...
pack(); // ... shrink/grow the window so that each element gets it's 'prefferred size"
Dimension scrn = Toolkit.getDefaultToolkit().getScreenSize(); // get the screen's size (in pixels) and...
// ... calculater a point exactly our size up and left from the bottom corner of the screen and move us there
setLocation((scrn.width-this.getSize().width)/1,(scrn.height-this.getSize().height)/1);
// the result of this is causing the window to appear in the lower-right corner of the screen
}
} // end of setVisible
public static void main(String arg[]) { // this is actually a stub,
// call the constructor (from inside a try block), but don't keep a reference to the created object
// (it'll dislay itself, so we don't need to)
try { new uChat(); }
// catch any Exception that pops up and pass it to the handle(Excpetion) method (it's way below)
catch(Exception e) { handle(e); }
} // end of main
// a beast of a method here, this is from the ActionListener interface
// it's called any time an ActionEvent occurs, like clicking a button or pressing enter in a text field
// it's pretty convoluted. I'll devote an entire post (or more) to explaining it later.
public void actionPerformed(ActionEvent evt) {
if(name.getText().trim().equals("")) {return;}
byte[] data= null;
try {
if(evt.getSource() == input || evt.getSource() == send) {
if(input.getText().trim().equals("")) {return;}
if(!connect.getLabel().equals("Leave")) {return;}
{ // a little pre-processing to prevent old-skool hax (exploiting text wrapping)
// replaces every instance of 3+ space chars with 2 space chars
input.setText(
Pattern.compile(" {3,}")
.matcher(input.getText().trim())
.replaceAll(" ") ); //end of setText()
}
data = "SAY:".concat(input.getText()).getBytes("ISO-8859-1");
}
else if(evt.getSource() == connect || evt.getSource() == name) {
if(connect.getLabel() == "Join") {
name.setEditable(false);
data = "JOIN".concat(name.getText()).getBytes("ISO-8859-1");
connect.setLabel("Leave");
input.requestFocus();
}
else if (connect.getLabel() == "Leave") {
data = "LEAV".concat(name.getText()).getBytes("ISO-8859-1");
connect.setLabel("Join");
name.setEditable(true);
}
else { return; /* shouldn't happen*/ }
}
else { return; /* shouldn't happen*/ }
DatagramPacket out = new DatagramPacket(data, data.length,addrs,port);
sock.send(out);
} catch(Exception e) { handle(e); }
input.setText("");
}
// a "flag" to tell the listener thread to stop
boolean die = false;
// the run() method is from the java.lang.Runnable interface
// this code will run in a seperate thread
// it listens for incoming UDP packets, interprets them, and updated the display text area accordingly
// it's fairly complicated to, and will get it's own post later
public void run() {
while(!die) {
byte[] data = new byte[1024];
DatagramPacket in = new DatagramPacket(data, data.length);
try {
sock.receive(in);
String tmpIN = new String(data, "ISO-8859-1");
if(tmpIN.startsWith("SAY:")) {
String who = lookupTable.get(in.getAddress());
disp.append(who+": "+new String(data, 4, in.getLength()-4, "ISO-8859-1")+"\n");
toFront();
Toolkit.getDefaultToolkit().beep();
}
else if(tmpIN.startsWith("JOIN")) {
InetAddress tmp = in.getAddress();
String tmpS = new String(data, 4,in.getLength()-4, "ISO-8859-1");
lookupTable.put(tmp, tmpS);
disp.append(tmpS+'('+tmp.getHostAddress()+')'+" has joined\n");
byte[] send = "HERE".concat(name.getText()).getBytes("ISO-8859-1");
DatagramPacket outbound = new DatagramPacket(send, send.length, tmp, port);
sock.send(outbound);
}
else if(tmpIN.startsWith("HERE")) {
InetAddress tmp = in.getAddress();
String tmpS = new String(data, 4,in.getLength()-4, "ISO-8859-1");
lookupTable.put(tmp, tmpS);
if(!tmpS.equals(name.getText())) { //needed to suppress "you are here"
disp.append(tmpS+'('+tmp.getHostAddress()+')'+" is here\n");
}
}
else if(tmpIN.startsWith("LEAV")) {
InetAddress tmp = in.getAddress();
String tmpS = new String(data, 4,in.getLength()-4, "ISO-8859-1");
lookupTable.remove(tmp);
disp.append(tmpS+'('+tmp.getHostAddress()+')'+" has left\n");
}
else { /* unknown, silently ignore */ }
} catch(Exception e) { handle(e); }
}
try {
sock.close();
} catch(Exception e) { handle(e); }
}
// from the WindowListener interface, this method is called when the user asks the window to close by clicking it's exit button
// it does a little clean-up: announcing to other uChaters that we're leaving
public void windowClosing(WindowEvent evt) {
setVisible(false); // hide the window
// change the flag, telling the listener thread to stop
die = true;
// if we're connected
if(connect.getLabel() == "Join") { //only send LEAV if we're JOINed
try {
//build and send packet announcing that we're leaving
byte[] data = "LEAV".concat(name.getText()).getBytes("ISO-8859-1");
DatagramPacket out = new DatagramPacket(data, data.length,addrs,port);
sock.send(out);
} catch(Exception e) { handle(e); } // if an exception occurs, pass it to handle(Exception)
}
// this frees all resources associated with this window, allowing the event-processing threads to stop
dispose();
// this method explicitly tells the JMV to exit, but isn't needed as all threads should be ready to stop
// System.exit(0);
}
// these are also part of the WindowListner interface, but aren't used by this program
public void windowOpened(WindowEvent evt) { }
public void windowClosed(WindowEvent evt) { }
public void windowIconified(WindowEvent evt) { }
public void windowDeiconified(WindowEvent evt) { }
public void windowActivated(WindowEvent evt) { }
public void windowDeactivated(WindowEvent evt) { }
// all purpose Exception handler.
// NOT REMOTELY A BEST PRACTICE but sufficient for this small program
// static, so it can be accessed from main()
public static void handle(Exception e) {
// set up a pair of writers to convert the Excpetion's stack trace to a string
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
// dump the stack trace to the above writer and System.out (aka STDOUT, aka the terminal window)
e.printStackTrace(out);
e.printStackTrace(System.out);
// make a new text area, default sizes, with the stack trace (via the writers) as its content
TextArea disp = new TextArea(sb.getBuffer().toString());
// simple frame, set its layout
Frame f0 = new Frame("Oops");
f0.setLayout(new BorderLayout());
// anonymous inner class as event listener for the frame, more in another post
f0.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent evt) { ((Window)evt.getSource()).dispose(); }
});
// add a label, then add the text area from above
f0.add(new Label("Oops, something went wrong. uC may or may not still work."), BorderLayout.NORTH);
f0.add(disp, BorderLayout.CENTER);
// pack() and show the frame
f0.pack();
f0.setVisible(true);
// if you crash uChat (hint: start another instance while one's running), you'll see this method
// in action. it's not a pretty error message, and would tell your average user absolutely nothing.
// to be honest, this was 75% to shut the compiler up about "uncaught exceptions", 25% for debugging
}
}
First time here? Please, start at the beginning.
uChat Source
Same as in Part 2.1, except with the complicated parts still in (still no comments there, sorry):
Subscribe to:
Comments (Atom)
No comments:
Post a Comment