Skip to content

Instantly share code, notes, and snippets.

@colin-haber
Created April 23, 2012 07:54
Show Gist options
  • Select an option

  • Save colin-haber/2469422 to your computer and use it in GitHub Desktop.

Select an option

Save colin-haber/2469422 to your computer and use it in GitHub Desktop.
/**
* CHAT - CHAT Has Acronym Troubles - The stupid messaging protocol.
* Copyright (C) 2012 Jon Stevens and Colin Haber
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.limyc.chat;
import com.n1nja.semver.Version;
/**
* @author Colin Haber
* @version 0.2.0
* @since 0.2.0
*/
public class CHAT {
public static final Version VERSION = new Version(0, 2, 0);
public static final String PROTOCOL = "CHAT";
public static enum Command {
REGISTER("REG"),
MESSAGE("MSG"),
DISCONNECT("DSC");
private String text;
private Command(String text) {
this.text = text;
}
public String getCommand() {
return this.text;
}
@Override
public String toString() {
return this.getCommand();
}
}
public static enum Status {
GENERIC_SUCCESS("000"),
GENERIC_FAILURE("010"),
GENERIC_INVALID_ID("011"),
GENERIC_ERROR("020"),
REGISTER_SUCCESS("100"),
REGISTER_FAILURE("110"),
REGISTER_NAME_TAKEN("111"),
REGISTER_ERROR("120"),
MESSAGE_SUCCESS("200"),
MESSAGE_FAILURE("210"),
MESSAGE_INVALID_ID("211"),
MESSAGE_ERROR("220"),
DISCONNECT_SUCCESS("300"),
DISCONNECT_FAILURE("310"),
DISCONNECT_INVALID_ID("311"),
DISCONNECT_ERROR("320");
private String text;
private Status(String text) {
this.text = text;
}
public String getStatus() {
return this.text;
}
@Override
public String toString() {
return this.getStatus();
}
}
public static String getHeader(Command c) {
return CHAT.PROTOCOL + ":" + c.getCommand() + ":";
}
public static String getHeader(Command c, Status s) {
return CHAT.getHeader(c) + s.getStatus() + ":";
}
}
package com.n1nja.networking;
import java.io.*;
import java.net.*;
/**
* A simple network client framework for single-line communication
* client-server pairs.
* @author Colin Haber
*/
public abstract class Client extends ConsoleApplication {
private final Socket socket;
private BufferedReader netReader;
private BufferedWriter netWriter;
/**
* Creates a new Client.
*/
public Client() {
socket = new Socket();
}
/**
* Creates a new Client, and connects it to the provided host.
* Equivalent to {@code new Client().connect(hostAddress, port)}.
* @param hostAddress
* @param port
*/
public Client(InetAddress hostAddress, int port) {
this();
this.connect(hostAddress, port);
}
/**
* Connects to the provided host.
* @param hostAddress
* @param port
*/
public void connect(InetAddress hostAddress, int port) {
try {
this.socket.connect(new InetSocketAddress(hostAddress, port));
this.netReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
this.netWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
this.printLine("Connected to " + this.socket.getInetAddress().getCanonicalHostName() + ":" + this.socket.getPort());
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Tests whether the Client has ever been connected to a server.
* @return
*/
public boolean isConnected() {
return this.socket.isConnected();
}
@Override
public void start() {
super.start();
final Client client = this;
new Thread(
new Runnable() {
@Override
public void run() {
while (!client.socket.isInputShutdown()) {
try {
if (client.netReader.ready()) {
client.handle(client.readLine());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
).start();
}
/**
* Called when input is read from the Server.
* @param s
*/
public abstract void handle(String s);
/**
* Closes any open sockets and exits the program.
*/
public void stop() {
if (!this.socket.isClosed()) {
try {
this.socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.exit(0);
}
/**
* Writes a single char to the Server.
* @param c
*/
public void write(char c) {
this.write((int) c);
}
/**
* Writes a single int to the Server.
* @param i
*/
public void write(int i) {
try {
this.netWriter.write(i);
this.netWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Writes a line of text to the Server.
* @param s
*/
public void writeLine(String s) {
try {
this.netWriter.write(s + System.getProperty("line.separator"));
this.netWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Reads a single int from the Server.
* @return
*/
public int read() {
int read = -1;
try {
read = this.netReader.read();
} catch (IOException e) {
e.printStackTrace();
}
return read;
}
/**
* Reads a line of text from the Server.
* @return
*/
public String readLine() {
String readLine = null;
try {
readLine = this.netReader.readLine();
} catch (IOException e) {
e.printStackTrace();
}
return readLine;
}
}
package com.limyc.limychat.server;
import java.net.Socket;
import java.util.Arrays;
import com.limyc.chat.CHAT;
import com.limyc.limychat.User;
public class Connection extends com.n1nja.networking.Connection{
private User user;
private Server server;
public Connection(Server server, Socket socket) {
super(server, socket);
this.user = new User();
this.server = server;
}
@Override
public void handle(String s) {
this.printLine(s);
if (s.startsWith(CHAT.PROTOCOL)) {
String[] args = s.split(":");
this.command(args[1], Arrays.copyOfRange(args, 2, args.length));
}
}
@Override
public void stop() {
this.writeLine(CHAT.getHeader(CHAT.Command.DISCONNECT, CHAT.Status.DISCONNECT_SUCCESS));
this.printLine("Disconnected.");
this.server.removeUser(this.getUser());
super.stop();
}
public void command(String command, String[] params) {
if (command.equals(CHAT.Command.REGISTER.getCommand()) && params.length >= 1) {
String name = params[0];
long ID = System.nanoTime();
this.getUser().setName(name);
this.getUser().setID(ID);
if (this.server.addUser(this.getUser(), this)) {
this.writeLine(CHAT.getHeader(CHAT.Command.REGISTER, CHAT.Status.REGISTER_SUCCESS) + this.getUser().getID());
this.printLine("Registered as " + this.getUser().getName());
} else {
this.writeLine(CHAT.getHeader(CHAT.Command.REGISTER, CHAT.Status.REGISTER_NAME_TAKEN));
}
} else if (command.equals(CHAT.Command.MESSAGE.getCommand()) && params.length >= 2) {
String ID = params[0];
if (this.getUser().getID() == Long.valueOf(ID)) {
String message = params[1];
this.server.writeLine(CHAT.getHeader(CHAT.Command.MESSAGE) + this.getUser().getName() + ":" + message);
this.printLine(this.getUser().getName() + ": " + message);
} else {
this.writeLine(CHAT.getHeader(CHAT.Command.MESSAGE, CHAT.Status.MESSAGE_INVALID_ID));
}
} else if (command.endsWith(CHAT.Command.DISCONNECT.getCommand()) && params.length >= 1) {
String ID = params[0];
if (this.getUser().getID() == Long.valueOf(ID)) {
this.stop();
} else {
this.writeLine(CHAT.getHeader(CHAT.Command.DISCONNECT, CHAT.Status.DISCONNECT_INVALID_ID));
}
} else {
this.writeLine(CHAT.getHeader(CHAT.Command.GENERIC, CHAT.Status.GENERIC_FAILURE));
}
}
public User getUser() {
return this.user;
}
}
package com.n1nja.networking;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
public abstract class ConsoleApplication {
private SimpleDateFormat timestampFormat;
public ConsoleApplication() {
this.timestampFormat = new SimpleDateFormat("'['yyyy-MM-dd HH:mm:ss']'");
}
public synchronized void printLine(String s) {
System.out.println(this.timestampFormat.format(new Date()) + " " + s);
}
public void start() {
new Thread(
new Runnable() {
@Override
public void run() {
monitor();
}
}
).start();
}
public void monitor() {
BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
while (true) {
try {
if (consoleReader.ready()) {
String[] args = consoleReader.readLine().split(" ");
command(args[0].toUpperCase(), Arrays.copyOfRange(args, 0, args.length));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public abstract void command(String command, String[] params);
}
package com.limyc.limychat.server;
import java.util.HashMap;
import com.limyc.limychat.LimyChat;
import com.limyc.limychat.User;
public class Server extends com.n1nja.networking.Server<Connection> {
public static void main(String[] s) {
new Server(LimyChat.DEFAULT_PORT).start();
}
private HashMap<User, Connection> registeredUsers;
public Server(int port) {
super(port);
registeredUsers = new HashMap<User, Connection>();
}
@Override
public void command(String command, String[] params) {
if (command.equals("STOP") && params.length >= 0) {
this.stop();
}
}
@Override
public void listen() {
final Connection c = new Connection(this, this.accept());
new Thread(
new Runnable() {
@Override
public void run() {
c.start();
}
}
).start();
this.addConnection(c);
}
@Override
public void writeLine(String s) {
for (Connection c : this.registeredUsers.values()) {
c.writeLine(s);
}
}
public synchronized boolean addUser(User u, Connection c) {
boolean nameIsTaken = false;
checkNames:for (User r : this.registeredUsers.keySet()) {
if (u.getName().equals(r.getName())) {
nameIsTaken = true;
break checkNames;
}
}
if (!nameIsTaken) {
this.registeredUsers.put(u, c);
}
return !nameIsTaken;
}
public synchronized void removeUser(User u) {
this.registeredUsers.remove(u);
}
}
package com.n1nja.networking;
import java.net.*;
import java.io.*;
public abstract class Connection {
private final BufferedWriter netWrite;
private final BufferedReader netRead;
private final Socket socket;
private final Server<? extends Connection> server;
public Connection(Server<? extends Connection> server, Socket socket) {
this.server = server;
this.socket = socket;
BufferedWriter netWrite = null;
BufferedReader netRead = null;
try {
netWrite = new BufferedWriter(new OutputStreamWriter(this.socket.getOutputStream()));
netRead = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
} catch (IOException e) {
e.printStackTrace();
}
this.netWrite = netWrite;
this.netRead = netRead;
this.printLine("Connected.");
}
public void start() {
while (!this.socket.isClosed()) {
try {
if (this.netRead.ready()) {
this.handle(this.netRead.readLine());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public abstract void handle(String s);
public void stop() {
try {
this.socket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
this.server.removeConnection(this);
}
}
public void write(char c) {
this.write((int) c);
}
public void write(int i) {
try {
this.netWrite.write(i);
this.netWrite.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
public int read() {
int read = -1;
try {
read = this.netRead.read();
} catch (IOException e) {
e.printStackTrace();
}
return read;
}
public void writeLine(String s) {
try {
this.netWrite.write(s + System.getProperty("line.separator"));
this.netWrite.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
public String readLine() {
String readLine = null;
try {
readLine = this.netRead.readLine();
} catch (IOException e) {
e.printStackTrace();
}
return readLine;
}
public void printLine(String s) {
this.server.printLine("[" + this.socket.getInetAddress().getCanonicalHostName() + "] " + s);
}
public Server<? extends Connection> getServer() {
return this.server;
}
}
package com.n1nja.networking;
import java.net.*;
import java.io.*;
import java.util.*;
public abstract class Server<C extends Connection> extends ConsoleApplication {
private ServerSocket listener;
private HashSet<Connection> connections;
public Server(int port) {
try {
this.listener = new ServerSocket(port);
} catch (IOException e) {
e.printStackTrace();
}
this.connections = new HashSet<Connection>();
}
@Override
public void start() {
super.start();
final Server<C> server = this;
new Thread(
new Runnable() {
@Override
public void run() {
server.listen();
}
}
).start();
try {
this.printLine("Server started at " + InetAddress.getLocalHost().getCanonicalHostName() + ":" + this.listener.getLocalPort());
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
public abstract void listen();
public synchronized void writeLine(String s) {
for (Connection c: this.connections) {
c.writeLine(s);
}
}
public void stop() {
try {
this.listener.close();
} catch (IOException e) {
e.printStackTrace();
}
for (Connection c : this.connections) {
c.stop();
}
System.exit(0);
}
public synchronized void addConnection(Connection c) {
this.connections.add(c);
}
public synchronized void removeConnection(Connection c) {
this.connections.remove(c);
}
public Socket accept() {
Socket accept = null;
try {
accept = this.listener.accept();
} catch (IOException e) {
e.printStackTrace();
}
return accept;
}
}
package com.limyc.limychat;
public class User {
private String name;
private long ID;
public User() {
this.name = null;
this.ID = 0L;
}
public void setName(String name) {
this.name = name;
}
public void setID(long ID) {
this.ID = ID;
}
public String getName() {
return this.name;
}
public long getID() {
return this.ID;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment