Lesson 19 - Java Chat - Client - Server Connection Part 3
In the previous lesson, Java Chat - Client - Server Connection Part 2, we focused on implementing a client communicator.
In today's Java tutorial, we're finally going to establish a connection with the server.
Modifying the Connect Controller
We'll start by modifying the ConnectController
class. In the
class, we'll create a new instance variable of the
IClientCommunicationService
type. We'll also create a
setter for it to set the communicator:
public void setCommunicator(IClientCommunicationService communicator) { this.communicator = communicator; final BooleanBinding connected = Bindings.createBooleanBinding(() -> this.communicator.getConnectionState() == ConnectionState.CONNECTED, this.communicator.connectionStateProperty()); btnConnect.disableProperty().bind(connected.or(txtServer.textProperty().isEmpty())); btnDisconnect.disableProperty().bind(connected.not()); lblConnectedTo.textProperty().bind(this.communicator.connectedServerNameProperty()); }
We have to set the communicator like this because we don't have any
dependency management implemented in this project, as we did with the server. In
the setter we create a connected
BooleanProperty
to which we bind disableProperty
of
the Connect and Disconnect buttons. The disableProperty
of the
"connect" button has a bit more complicated logic, because we have to check that
there's anything to connect to (the TextField
with the server is
filled in).
Next, we'll edit the initialize()
method to which we'll add a
response to the listView
item selection:
lvServers.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { if (newValue == null) { return; } txtServer.textProperty().set(String.format("%s:%d", newValue.getServerAddress().getHostAddress(), newValue.getPort())); });
By calling the getSelectionModel().selectedItemProperty()
methods we get an observable read-only property to which we add a listener. It's
called whenever we change the selected item in the listView
. If the
new value is null
, we don't do anything. Otherwise we fill the
txtServer
TextField
with a value in the format:
"server:port".
Now we'll create the most important method in the controller,
connect()
, to connect to the server:
private void connect() { final String hostPort = txtServer.textProperty().get(); final String host = hostPort.substring(0, hostPort.indexOf(":")); final String portRaw = hostPort.substring(hostPort.indexOf(":") + 1); int port; try { port = Integer.parseInt(portRaw); } catch (Exception ex) { Alert alert = new Alert(AlertType.ERROR); alert.setHeaderText("Error"); alert.setContentText("Unable to parse the server port."); alert.showAndWait(); return; } this.communicator.connect(host, port) .exceptionally(throwable -> { Alert alert = new Alert(AlertType.ERROR); alert.setHeaderText("Error"); alert.setContentText("Unable to connect to the server."); alert.showAndWait(); throw new RuntimeException(throwable); }) .thenAccept(ignored -> { Alert alert = new Alert(AlertType.INFORMATION); alert.setHeaderText("Information"); alert.setContentText("Connection established successfully."); alert.showAndWait(); }); }
In the first part of the method, we parse the server address and port which
the server listens on from the TextField
. If the server port cannot
be parsed, we notify the user that the port is invalid. Then the
connect()
method is called on the communicator instance. The
exceptionaly()
method handles the case when the connection failed.
Once we get to the exceptionaly()
branch, we can stay there by
throwing an exception again, or return a correct value to get to the default
branch. The thenAccept()
branch is evaluated if the connection was
successfully established.
Finally, all we have to do is set the appropriate action for the buttons:
@FXML private void handleConnect(ActionEvent actionEvent) { connect(); } @FXML private void handleDisconnect(ActionEvent actionEvent) { communicator.disconnect(); }
Customizing the Main Controller
Now we'll move to the main controller which will hold a single communicator
instance. So, in the MainController
class, we'll create an instance
constant with the communicator:
private final IClientCommunicationService communicator = new ClientCommunicationService();
Next, we'll modify the handleConnect()
method in which we'll
show the window:
@FXML private void handleConnect(ActionEvent actionEvent) { try { final ConnectController controller = showNewWindow("connect/connect", "Connect to server..."); controller.setCommunicator(communicator); } catch (IOException e) { e.printStackTrace(); } }
The showNewWindow()
method returns a new window controller. We
store this controller to a local variable and set the communicator using
setCommunicator()
.
We'll also use the onClose()
method, which is called when the
window is closed. In this method we'll disconnect from the server just to be
sure. This terminates the connection safely and all threads associated with
it:
@Override public void onClose() { communicator.disconnect() }
Testing the Functionality
Finally, we can connect to the server. From the previous lesson we have
searching for local servers available, so we'll use it now. Start the server and
the client. Next, we'll display the connection window. When our running server
appears in the list, we'll click this item. At this point,
TextField
should be filled in with the server address and port and
the connection button should be active. After clicking the button, we should see
a dialog that the connection was successful and the connection button shouldn't
be disabled. On the contrary, the connect button should be activated. The best
way to know that we're connected is that the number of connected users on the
server changed:
That would be all for today's lesson.
Next time, in the lesson Java Chat - Server - User Management, we'll create a simple server-side user management and log in to the server using a nickname.