Short Introduction:
I have a mini-chat feature where users can join a server of a specific project posted in my app. Each time someone clicks the button: "Join chat", the program checks whether someone is currently hosting a server for that specific project or the user that clicked the button can be the admin, hosting the server.
Every single user should be able to see how many users are there online in the server, this being displayed. Every single Admin should be able to close the whole chat and get everyone out when exiting. These two features of the chat are handled by two lists kept in the server, the clientChannels
(the size of it is the number of online users) and the clientInterfaces
(allowing for closing all the chats of the users and their interfaces displaying the chat when Admin exits the chat).
Everything works just fine if I test it from one single projectInterface, but if I try to join the same project on two different interfaces displaying it, the user that is not the admin can not see the online count and won't get forced to disconnect from the chat when the Admin does. The main feature, of actual communicating, is working just fine.
Why I think I get this problem:
Each interface will have its own server instance with the data parsed for it to work: host, port etc. Even if the host and port are the same for the two interfaces, the lists get a new instance for each Server Object. Therefore, I can still communicate, the non-admin user joining the first created server, but the lists that I need for the other features aren't the same.
What have I tried:
Create a custom ServerSocketChannel so I can obtain the instance already created rather than creating a new server and parsing the same data. The custom serverChannel will have the two lists with the data unchanged and public for all clients that join. However, this doesn't work either for some reason and this is what I am trying to find out, why?
I know that the easiest way to solve this is to keep track of online count and of the users that are in the chat in the database, and extract those when I need, but this will be too slow, as I have to update the online count each 1.5 sec. Also, I don't know if there is possible to store the whole Server Instance in MySQL, this would have solved the porblem.
CODE (Only the relevant parts):
The project interface, where you can access the chat:
joinChatButton.addActionListener(e -> {
joinChatButton.setEnabled(false);
String selectQuery = "SELECT id, name, ip_address, port FROM servers.locations WHERE name = ?";
String addQuery = "INSERT INTO servers.locations (name, ip_address, port) VALUES (?, ?, ?)";
frame.setState(Frame.ICONIFIED);
try (Connection connection = ds.getConnection();
PreparedStatement statement = connection.prepareStatement(selectQuery);
PreparedStatement ps = connection.prepareStatement(addQuery)) {
statement.setString(1, projectName);
ResultSet resultSet = statement.executeQuery();
if (!resultSet.next()) {
String IP_ADDRESS = InetAddress.getLocalHost().getHostAddress();
new Chat_Interface(serverChannel, true, IP_ADDRESS, projectName, frame, joinChatButton);
ps.setString(1, projectName);
ps.setString(2, IP_ADDRESS);
ps.setInt(3, 5000);
ps.addBatch();
ps.executeBatch();
ps.clearBatch();
} else {
new Chat_Interface(serverChannel, false, resultSet.getString(3), projectName, frame, joinChatButton);
}
} catch (SQLException | UnknownHostException ex) {
throw new RuntimeException(ex);
}
});
frame.add(joinChatButton);
// Make the frame visible
frame.setVisible(true);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
area.setEnabled(true);
}
});
}
public static void main(String[] args) throws IOException {
// Example usage
new ProjectInterface("Amazing Project", "Andrei",
"This is a detailed and captivating project description that will now be displayed over multiple lines. "
+ "The description can go on and on to test how it dynamically adjusts in the label.",
";, new JTextArea());
}
}
The Server:
CustomServerSocket serverChannel = new CustomServerSocket(SelectorProvider.provider(), clientChannels, clientInterfaces)) {
serverChannel.socket().bind(new InetSocketAddress(5000));
serverChannel.configureBlocking(false);
System.out.println("Server is listening on port " + serverChannel.socket().getLocalPort());
ByteBuffer buffer = ByteBuffer.allocate(1024);
clientChannels = serverChannel.getClientChannels();
clientInterfaces = serverChannel.getClientInterfaces();
while (true) {
SocketChannel clientChannel = serverChannel.accept();
Iterator<SocketChannel> iterator = clientChannels.iterator();
if (clientChannel != null) {
clientChannel.configureBlocking(false);
clientChannels.add(clientChannel);
System.out.printf("Client %s connected%n", clientChannel.socket().getRemoteSocketAddress());
}
while (iterator.hasNext()) {
SocketChannel client = iterator.next();
try {
buffer.clear();
int bytesRead = client.read(buffer);
if (bytesRead == -1) {
System.out.printf("Client %s disconnected%n", client.socket().getRemoteSocketAddress());
clientChannels.remove(client);
client.close();
} else if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String message = String.format("%s", new String((data), StandardCharsets.UTF_8));
for (SocketChannel otherClient : clientChannels) {
buffer.clear();
buffer.put((message).getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
otherClient.write(buffer);
}
}
}
} catch (IOException e) {
System.out.printf("Client %s disconnected due to error%n", client.socket().getRemoteSocketAddress());
clientChannels.remove(client);
client.close();
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public List<SocketChannel> getClientChannels() {
return clientChannels;
}
public List<Chat_Interface> getClientInterfaces() {
return clientInterfaces;
}
}
The Chat Interface:
public Chat_Interface(SimpleServerChannel serverChannel, boolean ownerOfServer, String IPAddress, String projectName, Frame projectInterface, JButton joinButton) {
if (ownerOfServer) {
new Thread(serverChannel::start).start();
}
serverChannel.getClientInterfaces().add(this);
//code...
iconLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getButton() != 1) return;
if (nameField.getText().trim().isEmpty()) return;
try {
if (iconLabel.isEnabled()) {
client.sendMessageToServer(nameField.getText(), ownerOfServer);
statusButton.setText(serverChannel.getClientChannels().size()+"");
nameField.setText("");
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
if (iconLabel.isEnabled()) {
iconLabel.setEnabled(false);
Timer timer = new Timer(1500, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
iconLabel.setEnabled(true);
}
});
timer.setRepeats(false);
timer.start();
}
}
});
Properties props = new Properties();
try {
props.load(Files.newInputStream(Path.of("storefront.properties"), StandardOpenOption.READ));
} catch (IOException e) {
JOptionPane.showMessageDialog(frame, "An error occurred", "Error", JOptionPane.ERROR_MESSAGE);
}
MysqlDataSource ds = new MysqlDataSource();
ds.setServerName("localhost");
ds.setPortNumber(3306);
ds.setUser(props.getProperty("user"));
ds.setPassword(props.getProperty("pass"));
String removeQuery = "DELETE FROM servers.locations WHERE name = ?";
Consumer<Client> closeClient = c -> {
frame.dispose();
client.close();
projectInterface.setState(Frame.NORMAL);
if (ownerOfServer) {
serverChannel.getClientChannels().forEach(client1 -> {
try {
client1.close();
} catch (IOException ex) {
//do nothing
}
});
serverChannel.getClientInterfaces().forEach(client1 -> client1.frame.dispose());
}
try (Connection connection = ds.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(removeQuery)) {
preparedStatement.setString(1, projectName);
preparedStatement.execute();
} catch (SQLException e) {
throw new RuntimeException(e);
}
};
The CustomServerSocketChannel:
public class CustomServerSocket extends ServerSocketChannel {
private final ServerSocketChannel delegate; // Real instance
private List<SocketChannel> clientChannels;
private List<Chat_Interface> clientInterfaces;
public CustomServerSocket(SelectorProvider provider, List<SocketChannel> clientChannels, List<Chat_Interface> clientInterfaces) throws IOException {
super(provider);
this.delegate = ServerSocketChannel.open(); // Create a real instance
this.clientChannels = clientChannels;
this.clientInterfaces = clientInterfaces;
}
@Override
public ServerSocketChannel bind(SocketAddress local, int backlog) throws IOException {
return delegate.bind(local, backlog); // Delegate the call
}
@Override
public <T> ServerSocketChannel setOption(SocketOption<T> name, T value) throws IOException {
return delegate.setOption(name, value); // Delegate the call
}
@Override
public <T> T getOption(SocketOption<T> name) throws IOException {
return delegate.getOption(name); // Delegate the call
}
@Override
public Set<SocketOption<?>> supportedOptions() {
return delegate.supportedOptions(); // Delegate the call
}
@Override
public ServerSocket socket() {
return delegate.socket(); // Delegate the call
}
@Override
public SocketChannel accept() throws IOException {
return delegate.accept(); // Delegate the call
}
@Override
public SocketAddress getLocalAddress() throws IOException {
return delegate.getLocalAddress(); // Delegate the call
}
@Override
protected void implCloseSelectableChannel() throws IOException {
delegate.close(); // Close the delegate
}
@Override
protected void implConfigureBlocking(boolean block) throws IOException {
delegate.configureBlocking(block); // Delegate the call
}
public List<SocketChannel> getClientChannels() {
return clientChannels;
}
public List<Chat_Interface> getClientInterfaces() {
return clientInterfaces;
}
}