This repository has been archived on 2023-08-20. You can view files and clone it, but cannot push or open issues or pull requests.
simple_java_nio_chat/src/ChatServer.java

635 lines
23 KiB
Java

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This is the main class for the Chat Server
*/
public class
ChatServer
{
// A pre-allocated buffer for the received data
private static final ByteBuffer buffer = ByteBuffer.allocate(16384);
// Decoder and encoder for transmitting text
private static final Charset charset = StandardCharsets.UTF_8;
private static final CharsetDecoder decoder = charset.newDecoder();
private static final CharsetEncoder encoder = charset.newEncoder();
// Regex for message process
private static final String REGEX_MSG_NEW_NICKNAME = "nick " + Message.REGEX_NICKNAME;
private static final String REGEX_MSG_JOIN = "join " + Message.REGEX_ROOM_NAME;
private static final String REGEX_MSG_LEAVE = "leave";
private static final String REGEX_MSG_BYE = "bye";
private static final String REGEX_MSG_PRIVATE = "priv " + Message.REGEX_NICKNAME + " " + Message.REGEX_TEXT;
// Users info
private static final HashMap<String, User> nickname_user = new HashMap<>();
private static final HashMap<String, Room> nickname_room = new HashMap<>();
/**
* Run the server
*
* @param args
*/
public static void
main (String[] args)
{
// Port to listen
int server_port = Integer.parseInt(args[0]);
try
{
// Instead of creating a ServerSocket, create a ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
// Set it to non-blocking, so we can use select
ssc.configureBlocking(false);
// Get the Socket connected to this channel, and bind
// it to the listening port
ServerSocket ss = ssc.socket();
InetSocketAddress isa = new InetSocketAddress(server_port);
ss.bind(isa);
// Create a new Selector for selecting
Selector selector = Selector.open();
// Register the ServerSocketChannel, so we can listen
// for incoming connections
ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Listening on port " + server_port);
while (true)
{
// See if we've had any activity -- either an incoming connection,
// or incoming data on an existing connection
int num = selector.select();
// If we don't have any activity, loop around and wait again
if (num == 0)
{
continue;
}
// Get the keys corresponding to the activity that has been
// detected, and process them one by one
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys)
{
// Get a key representing one of bits of I/O activity
// What kind of activity is it?
if (key.isAcceptable())
{
// It's an incoming connection. Register this socket with
// the Selector so we can listen for input on it
Socket s = ss.accept();
System.out.println("Got connection from " + s + ".");
// Make sure to make it non-blocking, so we can use a selector
// on it.
SocketChannel sc = s.getChannel();
sc.configureBlocking(false);
// Register it with the selector, for reading
sc.register(selector, SelectionKey.OP_READ, new User(sc));
}
else if (key.isReadable())
{
SocketChannel sc = null;
try
{
// It's incoming data on a connection -- process it
sc = (SocketChannel) key.channel();
boolean ok = processInput(key, sc);
// If the connection is dead, remove it from the selector
// and close it
if (!ok)
{
key.cancel();
close_client(key, sc);
}
}
catch (IOException ie)
{
// On exception, remove this channel from the selector
key.cancel();
close_client(key, sc);
}
}
}
// We remove the selected keys, because we've dealt with them.
keys.clear();
}
}
catch (IOException ie)
{
System.err.println(ie.getMessage());
}
}
/**
* Close a connection with a client
*
* @param key
* @param sc
*/
private static void
close_client (SelectionKey key, SocketChannel sc)
{
User sender = (User)key.attachment();
if (sender.get_state() == State.INSIDE)
{
Room room = sender.get_room();
room.left_user(sender);
TreeSet<User> user_list = room.get_users();
for (User user : user_list)
{
try
{
send_left_message(user, sender.get_nickname());
}
catch (IOException ie)
{
System.err.println("Error sending left message: " + ie);
}
}
if (user_list.size() == 0)
{
nickname_room.remove(room.get_name());
}
}
nickname_user.remove(sender.get_nickname());
key.cancel();
Socket s = sc.socket();
try
{
System.out.println("Closing connection to " + s);
sc.close();
}
catch (IOException ie)
{
System.err.println("Error closing socket " + s + ": " + ie);
}
}
/**
* Read a message from the socket and process it
*
* @param key
* @param sc
* @return boolean false if connection closed, true otherwise
* @throws IOException
*/
private static boolean
processInput (SelectionKey key, SocketChannel sc)
throws IOException
{
// Read the message to the buffer
buffer.clear();
sc.read(buffer);
buffer.flip();
// If no data, close the connection
if (buffer.limit() == 0)
{
return false;
}
// Decode and add to buffer
User sender = (User)key.attachment();
sender.add_to_buffer(decoder.decode(buffer).toString());
String instructions = sender.get_buffer().toString();
// // Print byte representation of incoming package
// for (final char c : instructions.toCharArray()) {
// System.out.print((int) c + " ");
// }
// System.out.println();
// We only want to process full instructions
if (!instructions.endsWith("\n"))
{
return true;
}
for (String instruction : instructions.split("\n"))
{
Matcher tokens;
if (instruction.startsWith("/"))
{
String cmd = instruction.substring(1).trim();
if ((tokens = Pattern.compile(REGEX_MSG_NEW_NICKNAME).matcher(cmd)).find())
{
send_nickname_command(sender, tokens.group(1));
}
else if ((tokens = Pattern.compile(REGEX_MSG_JOIN).matcher(cmd)).find())
{
send_join_command(sender, tokens.group(1));
}
else if (Pattern.matches(REGEX_MSG_LEAVE, cmd))
{
send_leave_command(sender);
}
else if (Pattern.matches(REGEX_MSG_BYE, cmd))
{
send_bye_command(key, sender);
}
else if ((tokens = Pattern.compile(REGEX_MSG_PRIVATE).matcher(cmd)).find())
{
send_private_command(
sender,
// Emitter
tokens.group(1),
// Message
tokens.group(2)
);
}
else if (cmd.startsWith("/"))
{
send_public_message(sender, cmd);
}
else
{
send_error_message(sender, "Unknown command.");
}
}
else if (instruction.length() != 0)
{
send_public_message(sender, instruction.trim());
}
sender.advance_buffer(instruction.length() + 1);
}
return true;
}
/**
* Helper function to send a message
*
* @param sc
* @param message
* @throws IOException
*/
private static void
send_message (SocketChannel sc, Message message)
throws IOException
{
sc.write(encoder.encode(CharBuffer.wrap(message.toString())));
}
/**
* Helper function to send a message
*
* @param receiver
* @param sender
* @param message_value
* @throws IOException
*/
private static void
send_message (User receiver, String sender, String message_value)
throws IOException
{
Message message = new Message(MessageType.MESSAGE, sender, message_value);
send_message(receiver.get_socket(), message);
}
/**
* Send error message
*
* @param receiver
* @param error_message
* @throws IOException
*/
private static void
send_error_message (User receiver, String error_message)
throws IOException
{
Message message = new Message(MessageType.ERROR, error_message);
send_message(receiver.get_socket(), message);
}
/**
* Send ok message
*
* @param receiver
* @throws IOException
*/
private static void
send_ok_message (User receiver)
throws IOException
{
Message message = new Message(MessageType.OK);
send_message(receiver.get_socket(), message);
}
/**
* Send newnick message
*
* @param receiver
* @param old_nickname
* @param new_nickname
* @throws IOException
*/
private static void
send_nickname_message (User receiver, String old_nickname, String new_nickname)
throws IOException
{
Message message = new Message(MessageType.NEW_NICKNAME, old_nickname, new_nickname);
send_message(receiver.get_socket(), message);
}
/**
* Send joined message
*
* @param receiver
* @param join_nickname
* @throws IOException
*/
private static void
send_joined_message (User receiver, String join_nickname)
throws IOException
{
Message message = new Message(MessageType.JOINED, join_nickname);
send_message(receiver.get_socket(), message);
}
/**
* Send left message
*
* @param receiver
* @param left_nickname
* @throws IOException
*/
private static void
send_left_message (User receiver, String left_nickname)
throws IOException
{
Message message = new Message(MessageType.LEFT, left_nickname);
send_message(receiver.get_socket(), message);
}
/**
* Send bye message
*
* @param receiver
* @throws IOException
*/
private static void
send_bye_message (User receiver)
throws IOException
{
Message message = new Message(MessageType.BYE);
send_message(receiver.get_socket(), message);
}
/**
* Send private message
*
* @param receiver
* @param sender
* @param message_value
* @throws IOException
*/
private static void
send_private_message (User receiver, String sender, String message_value)
throws IOException
{
Message message = new Message(MessageType.PRIVATE, sender, message_value);
send_message(receiver.get_socket(), message);
}
/**
* Send simple message
*
* @param sender
* @param message_value
* @throws IOException
*/
private static void
send_public_message (User sender, String message_value)
throws IOException
{
if (sender.get_state() == State.INSIDE)
{
Room sender_room = sender.get_room();
TreeSet<User> user_list = sender_room.get_users();
for (User user : user_list)
{
send_message(user, sender.get_nickname(), message_value);
}
}
else
{
send_error_message(sender, "You are not in a room.");
}
}
/**
* Send nick command
*
* @param sender
* @param nick
* @throws IOException
*/
private static void
send_nickname_command (User sender, String nick)
throws IOException
{
if (
// Allow to change to the current nick
// Only compare if the user already has a nickname
((sender.get_state() != State.INIT)
&& sender.get_nickname().equals(nick))
|| // Don't allow to set a nickname already in use
!nickname_user.containsKey(nick)
)
{
if (sender.get_state() == State.INIT)
{
sender.set_state(State.OUTSIDE);
}
if (sender.get_state() == State.INSIDE)
{
Room sender_room = sender.get_room();
TreeSet<User> user_list = sender_room.get_users();
for (User user : user_list)
{
if (user != sender)
{
send_nickname_message(user, sender.get_nickname(), nick);
}
}
}
nickname_user.remove(sender.get_nickname());
nickname_user.put(nick, sender);
send_ok_message(sender);
sender.set_nickname(nick);
}
else
{
send_error_message(sender, "There already is a user with nick " + nick);
}
}
/**
* Send join command
*
* @param sender
* @param room_name
* @throws IOException
*/
private static void
send_join_command (User sender, String room_name)
throws IOException
{
if (sender.get_state() == State.INIT)
{
send_error_message(sender, "You don't have a nickname.");
}
/*else if (sender.get_room().get_name().equals(room_name))
{
send_error_message(sender, "You already are in that room.");
}*/
else
{
// If already in a room, leave it first
if (sender.get_state() == State.INSIDE)
{
send_leave_command(sender);
}
// If room doesn't exist
if (!nickname_room.containsKey(room_name))
{
nickname_room.put(room_name, new Room(room_name));
}
// Notify
Room new_room = nickname_room.get(room_name);
TreeSet<User> current_room_user_list = new_room.get_users();
new_room.join_user(sender);
for (User user : current_room_user_list)
{
send_joined_message(user, sender.get_nickname());
}
send_ok_message(sender);
sender.set_room(new_room);
sender.set_state(State.INSIDE);
}
}
/**
* Send leave command
*
* @param sender
* @throws IOException
*/
private static void
send_leave_command (User sender)
throws IOException
{
if (sender.get_state() != State.INSIDE)
{
send_error_message(sender, "You are not in a room.");
}
else
{
Room room = sender.get_room();
room.left_user(sender);
TreeSet<User> user_list = room.get_users();
for (User user : user_list)
{
send_left_message(user, sender.get_nickname());
}
if (user_list.size() == 0)
{
nickname_room.remove(room.get_name());
}
send_ok_message(sender);
sender.set_state(State.OUTSIDE);
}
}
/**
* Send bye command
*
* @param key
* @param sender
* @throws IOException
*/
private static void
send_bye_command (SelectionKey key, User sender)
throws IOException
{
send_bye_message(sender);
close_client(key, sender.get_socket());
}
/**
* Send private message
*
* @param sender
* @param receiver
* @param message_value
* @throws IOException
*/
private static void
send_private_command (User sender, String receiver, String message_value)
throws IOException
{
if (sender.get_state() == State.INIT)
{
send_error_message(sender, "You don't have a nickname.");
}
else
{
if (nickname_user.containsKey(receiver))
{
send_ok_message(sender);
send_private_message(nickname_user.get(receiver), sender.get_nickname(), message_value);
}
else
{
send_error_message(sender, receiver + ": No such nickname online.");
}
}
}
}