Java RMI - Kurzanleitung
RMI steht für Remote Method Invocation. Es ist ein Mechanismus, mit dem ein Objekt in einem System (JVM) auf ein Objekt zugreifen / es aufrufen kann, das auf einer anderen JVM ausgeführt wird.
RMI wird zum Erstellen verteilter Anwendungen verwendet. Es bietet Remote-Kommunikation zwischen Java-Programmen. Es ist im Paket enthaltenjava.rmi.
Architektur einer RMI-Anwendung
In einer RMI-Anwendung schreiben wir zwei Programme, a server program (befindet sich auf dem Server) und a client program (befindet sich auf dem Client).
Innerhalb des Serverprogramms wird ein Remote-Objekt erstellt und die Referenz dieses Objekts wird dem Client (über die Registrierung) zur Verfügung gestellt.
Das Client-Programm fordert die Remote-Objekte auf dem Server an und versucht, seine Methoden aufzurufen.
Das folgende Diagramm zeigt die Architektur einer RMI-Anwendung.
Lassen Sie uns nun die Komponenten dieser Architektur diskutieren.
Transport Layer- Diese Schicht verbindet den Client und den Server. Es verwaltet die bestehende Verbindung und richtet auch neue Verbindungen ein.
Stub- Ein Stub ist eine Darstellung (Proxy) des Remote-Objekts auf dem Client. Es befindet sich im Client-System. Es fungiert als Gateway für das Client-Programm.
Skeleton - Dies ist das Objekt, das sich auf der Serverseite befindet. stub kommuniziert mit diesem Skelett, um die Anforderung an das entfernte Objekt weiterzuleiten.
RRL(Remote Reference Layer) - Es ist die Ebene, die die vom Client auf das Remote-Objekt vorgenommenen Verweise verwaltet.
Arbeiten einer RMI-Anwendung
Die folgenden Punkte fassen die Funktionsweise einer RMI-Anwendung zusammen:
Wenn der Client das entfernte Objekt anruft, wird es vom Stub empfangen, der diese Anforderung schließlich an die RRL weiterleitet.
Wenn die clientseitige RRL die Anforderung empfängt, ruft sie eine aufgerufene Methode auf invoke() des Objekts remoteRef. Es leitet die Anforderung an die RRL auf der Serverseite weiter.
Die RRL auf der Serverseite leitet die Anforderung an das Skeleton (Proxy auf dem Server) weiter, das schließlich das erforderliche Objekt auf dem Server aufruft.
Das Ergebnis wird vollständig an den Client zurückgegeben.
Marshalling und Unmarshalling
Immer wenn ein Client eine Methode aufruft, die Parameter für ein entferntes Objekt akzeptiert, werden die Parameter in einer Nachricht gebündelt, bevor sie über das Netzwerk gesendet werden. Diese Parameter können vom primitiven Typ oder von Objekten sein. Beim primitiven Typ werden die Parameter zusammengestellt und ein Header angehängt. Wenn es sich bei den Parametern um Objekte handelt, werden sie serialisiert. Dieser Vorgang ist bekannt alsmarshalling.
Auf der Serverseite werden die gepackten Parameter entbündelt und anschließend die erforderliche Methode aufgerufen. Dieser Vorgang ist bekannt alsunmarshalling.
RMI-Registrierung
Die RMI-Registrierung ist ein Namespace, in dem alle Serverobjekte abgelegt werden. Jedes Mal, wenn der Server ein Objekt erstellt, registriert er dieses Objekt bei der RMIregistry (mitbind() oder reBind()Methoden). Diese werden unter einem eindeutigen Namen registriert, der als bekannt istbind name.
Um ein entferntes Objekt aufzurufen, benötigt der Client eine Referenz dieses Objekts. Zu diesem Zeitpunkt ruft der Client das Objekt mit seinem Bindungsnamen (mit) aus der Registrierung ablookup() Methode).
Die folgende Abbildung erklärt den gesamten Prozess -
Ziele von RMI
Es folgen die Ziele von RMI -
- Minimierung der Komplexität der Anwendung.
- Zur Wahrung der Typensicherheit.
- Verteilte Speicherbereinigung.
- Minimieren Sie den Unterschied zwischen der Arbeit mit lokalen und Remote-Objekten.
Um eine RMI-Java-Anwendung zu schreiben, müssen Sie die folgenden Schritte ausführen:
- Definieren Sie die Remote-Schnittstelle
- Entwickeln Sie die Implementierungsklasse (Remote-Objekt).
- Entwickeln Sie das Serverprogramm
- Entwickeln Sie das Client-Programm
- Kompilieren Sie die Anwendung
- Führen Sie die Anwendung aus
Remote-Schnittstelle definieren
Eine Remote-Schnittstelle enthält die Beschreibung aller Methoden eines bestimmten Remote-Objekts. Der Client kommuniziert mit dieser Remote-Schnittstelle.
So erstellen Sie eine Remote-Schnittstelle:
Erstellen Sie eine Schnittstelle, die die vordefinierte Schnittstelle erweitert Remote welches zum Paket gehört.
Deklarieren Sie alle Geschäftsmethoden, die vom Client in dieser Schnittstelle aufgerufen werden können.
Da bei Remote-Anrufen möglicherweise Netzwerkprobleme auftreten, wird eine Ausnahme genannt RemoteExceptionkann auftreten; werfen Sie es.
Es folgt ein Beispiel für eine Remote-Schnittstelle. Hier haben wir eine Schnittstelle mit dem Namen definiertHello und es hat eine Methode namens printMsg().
import java.rmi.Remote;
import java.rmi.RemoteException;
// Creating Remote interface for our application
public interface Hello extends Remote {
void printMsg() throws RemoteException;
}
Entwicklung der Implementierungsklasse (Remote Object)
Wir müssen die im vorherigen Schritt erstellte Remote-Schnittstelle implementieren. (Wir können eine Implementierungsklasse separat schreiben oder das Serverprogramm diese Schnittstelle direkt implementieren lassen.)
Eine Implementierungsklasse entwickeln -
- Implementieren Sie die im vorherigen Schritt erstellte Schnittstelle.
- Stellen Sie die Implementierung aller abstrakten Methoden der Remote-Schnittstelle bereit.
Es folgt eine Implementierungsklasse. Hier haben wir eine Klasse mit dem Namen erstelltImplExample und implementierte die Schnittstelle Hello im vorherigen Schritt erstellt und bereitgestellt body für diese Methode, die eine Nachricht druckt.
// Implementing the remote interface
public class ImplExample implements Hello {
// Implementing the interface method
public void printMsg() {
System.out.println("This is an example RMI program");
}
}
Entwickeln des Serverprogramms
Ein RMI-Serverprogramm sollte die Remote-Schnittstelle implementieren oder die Implementierungsklasse erweitern. Hier sollten wir ein entferntes Objekt erstellen und es an das bindenRMIregistry.
So entwickeln Sie ein Serverprogramm -
Erstellen Sie eine Clientklasse, von der aus Sie das Remote-Objekt aufrufen möchten.
Create a remote object durch Instanziieren der Implementierungsklasse wie unten gezeigt.
Exportieren Sie das Remote-Objekt mit der Methode exportObject() der genannten Klasse UnicastRemoteObject welches zum Paket gehört java.rmi.server.
Holen Sie sich die RMI-Registrierung mit dem getRegistry() Methode der LocateRegistry Klasse, die zum Paket gehört java.rmi.registry.
Binden Sie das erstellte Remote-Objekt mit der an die Registrierung bind() Methode der genannten Klasse Registry. Übergeben Sie dieser Methode eine Zeichenfolge, die den Bindungsnamen und das exportierte Objekt als Parameter darstellt.
Es folgt ein Beispiel für ein RMI-Serverprogramm.
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class Server extends ImplExample {
public Server() {}
public static void main(String args[]) {
try {
// Instantiating the implementation class
ImplExample obj = new ImplExample();
// Exporting the object of implementation class
// (here we are exporting the remote object to the stub)
Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 0);
// Binding the remote object (stub) in the registry
Registry registry = LocateRegistry.getRegistry();
registry.bind("Hello", stub);
System.err.println("Server ready");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
Entwicklung des Client-Programms
Schreiben Sie ein Client-Programm hinein, rufen Sie das Remote-Objekt ab und rufen Sie mit diesem Objekt die erforderliche Methode auf.
Ein Kundenprogramm entwickeln -
Erstellen Sie eine Clientklasse, von der aus Sie das Remote-Objekt aufrufen möchten.
Holen Sie sich die RMI-Registrierung mit dem getRegistry() Methode der LocateRegistry Klasse, die zum Paket gehört java.rmi.registry.
Rufen Sie das Objekt mit der Methode aus der Registrierung ab lookup() der Klasse Registry welches zum Paket gehört java.rmi.registry.
An diese Methode müssen Sie einen Zeichenfolgenwert übergeben, der den Bindungsnamen als Parameter darstellt. Dadurch erhalten Sie das entfernte Objekt zurück.
Lookup () gibt ein Objekt vom Typ remote zurück und wandelt es in den Typ Hello um.
Rufen Sie abschließend die erforderliche Methode mit dem erhaltenen Remote-Objekt auf.
Es folgt ein Beispiel für ein RMI-Client-Programm.
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
private Client() {}
public static void main(String[] args) {
try {
// Getting the registry
Registry registry = LocateRegistry.getRegistry(null);
// Looking up the registry for the remote object
Hello stub = (Hello) registry.lookup("Hello");
// Calling the remote method using the obtained object
stub.printMsg();
// System.out.println("Remote method invoked");
} catch (Exception e) {
System.err.println("Client exception: " + e.toString());
e.printStackTrace();
}
}
}
Kompilieren der Anwendung
So kompilieren Sie die Anwendung:
- Kompilieren Sie die Remote-Schnittstelle.
- Kompilieren Sie die Implementierungsklasse.
- Kompilieren Sie das Serverprogramm.
- Kompilieren Sie das Client-Programm.
Oder,
Öffnen Sie den Ordner, in dem Sie alle Programme gespeichert haben, und kompilieren Sie alle Java-Dateien wie unten gezeigt.
Javac *.java
Ausführen der Anwendung
Step 1 - Starten Sie die rmi Registrierung mit dem folgenden Befehl.
start rmiregistry
Dies startet eine rmi Registrierung in einem separaten Fenster wie unten gezeigt.
Step 2 - Führen Sie die Serverklassendatei wie unten gezeigt aus.
Java Server
Step 3 - Führen Sie die Clientklassendatei wie unten gezeigt aus.
java Client
Verification - Sobald Sie den Client starten, wird die folgende Ausgabe auf dem Server angezeigt.
Im vorherigen Kapitel haben wir eine RMI-Beispielanwendung erstellt. In diesem Kapitel wird erläutert, wie Sie eine RMI-Anwendung erstellen, bei der ein Client eine Methode aufruft, die ein GUI-Fenster (JavaFX) anzeigt.
Remote-Schnittstelle definieren
Hier definieren wir eine Remote-Schnittstelle mit dem Namen Hello mit einer Methode namens animation() drin.
import java.rmi.Remote;
import java.rmi.RemoteException;
// Creating Remote interface for our application
public interface Hello extends Remote {
void animation() throws RemoteException;
}
Entwicklung der Implementierungsklasse
In der Implementierungsklasse (Remote Object) dieser Anwendung versuchen wir, mit JavaFX ein Fenster zu erstellen, in dem GUI-Inhalte angezeigt werden.
import javafx.animation.RotateTransition;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;
// Implementing the remote interface
public class FxSample extends Application implements Hello {
@Override
public void start(Stage stage) {
// Drawing a Box
Box box = new Box();
// Setting the properties of the Box
box.setWidth(150.0);
box.setHeight(150.0);
box.setDepth(100.0);
// Setting the position of the box
box.setTranslateX(350);
box.setTranslateY(150);
box.setTranslateZ(50);
// Setting the text
Text text = new Text(
"Type any letter to rotate the box, and click on the box to stop the rotation");
// Setting the font of the text
text.setFont(Font.font(null, FontWeight.BOLD, 15));
// Setting the color of the text
text.setFill(Color.CRIMSON);
// Setting the position of the text
text.setX(20);
text.setY(50);
// Setting the material of the box
PhongMaterial material = new PhongMaterial();
material.setDiffuseColor(Color.DARKSLATEBLUE);
// Setting the diffuse color material to box
box.setMaterial(material);
// Setting the rotation animation to the box
RotateTransition rotateTransition = new RotateTransition();
// Setting the duration for the transition
rotateTransition.setDuration(Duration.millis(1000));
// Setting the node for the transition
rotateTransition.setNode(box);
// Setting the axis of the rotation
rotateTransition.setAxis(Rotate.Y_AXIS);
// Setting the angle of the rotation
rotateTransition.setByAngle(360);
// Setting the cycle count for the transition
rotateTransition.setCycleCount(50);
// Setting auto reverse value to false
rotateTransition.setAutoReverse(false);
// Creating a text filed
TextField textField = new TextField();
// Setting the position of the text field
textField.setLayoutX(50);
textField.setLayoutY(100);
// Handling the key typed event
EventHandler<KeyEvent> eventHandlerTextField = new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent event) {
// Playing the animation
rotateTransition.play();
}
};
// Adding an event handler to the text feld
textField.addEventHandler(KeyEvent.KEY_TYPED, eventHandlerTextField);
// Handling the mouse clicked event(on box)
EventHandler<javafx.scene.input.MouseEvent> eventHandlerBox =
new EventHandler<javafx.scene.input.MouseEvent>() {
@Override
public void handle(javafx.scene.input.MouseEvent e) {
rotateTransition.stop();
}
};
// Adding the event handler to the box
box.addEventHandler(javafx.scene.input.MouseEvent.MOUSE_CLICKED, eventHandlerBox);
// Creating a Group object
Group root = new Group(box, textField, text);
// Creating a scene object
Scene scene = new Scene(root, 600, 300);
// Setting camera
PerspectiveCamera camera = new PerspectiveCamera(false);
camera.setTranslateX(0);
camera.setTranslateY(0);
camera.setTranslateZ(0);
scene.setCamera(camera);
// Setting title to the Stage
stage.setTitle("Event Handlers Example");
// Adding scene to the stage
stage.setScene(scene);
// Displaying the contents of the stage
stage.show();
}
// Implementing the interface method
public void animation() {
launch();
}
}
Serverprogramm
Ein RMI-Serverprogramm sollte die Remote-Schnittstelle implementieren oder die Implementierungsklasse erweitern. Hier sollten wir ein entferntes Objekt erstellen und es an das bindenRMIregistry.
Es folgt das Serverprogramm dieser Anwendung. Hier erweitern wir die oben erstellte Klasse, erstellen ein Remote-Objekt und registrieren es in der RMI-Registrierung mit dem Bindungsnamenhello.
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class Server extends FxSample {
public Server() {}
public static void main(String args[]) {
try {
// Instantiating the implementation class
FxSample obj = new FxSample();
// Exporting the object of implementation class
// (here we are exporting the remote object to the stub)
Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 0);
// Binding the remote object (stub) in the registry
Registry registry = LocateRegistry.getRegistry();
registry.bind("Hello", stub);
System.err.println("Server ready");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
Client-Programm
Es folgt das Client-Programm dieser Anwendung. Hier rufen wir das entfernte Objekt ab und rufen dessen Methode namens aufanimation().
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
private Client() {}
public static void main(String[] args) {
try {
// Getting the registry
Registry registry = LocateRegistry.getRegistry(null);
// Looking up the registry for the remote object
Hello stub = (Hello) registry.lookup("Hello");
// Calling the remote method using the obtained object
stub.animation();
System.out.println("Remote method invoked");
} catch (Exception e) {
System.err.println("Client exception: " + e.toString());
e.printStackTrace();
}
}
}
Schritte zum Ausführen des Beispiels
Im Folgenden finden Sie die Schritte zum Ausführen unseres RMI-Beispiels.
Step 1 - Öffnen Sie den Ordner, in dem Sie alle Programme gespeichert haben, und kompilieren Sie alle Java-Dateien wie unten gezeigt.
Javac *.java
Step 2 - Starten Sie die rmi Registrierung mit dem folgenden Befehl.
start rmiregistry
Dies startet eine rmi Registrierung in einem separaten Fenster wie unten gezeigt.
Step 3 - Führen Sie die Serverklassendatei wie unten gezeigt aus.
Java Server
Step 4 - Führen Sie die Clientklassendatei wie unten gezeigt aus.
java Client
Verification - Sobald Sie den Client starten, wird die folgende Ausgabe auf dem Server angezeigt.
Im vorherigen Kapitel haben wir eine RMI-Beispielanwendung erstellt, in der ein Client eine Methode aufruft, die ein GUI-Fenster (JavaFX) anzeigt.
In diesem Kapitel sehen wir uns anhand eines Beispiels an, wie ein Clientprogramm die Datensätze einer Tabelle in der MySQL-Datenbank auf dem Server abrufen kann.
Angenommen, wir haben eine Tabelle mit dem Namen student_data in der Datenbank details Wie nachfolgend dargestellt.
+----+--------+--------+------------+---------------------+
| ID | NAME | BRANCH | PERCENTAGE | EMAIL |
+----+--------+--------+------------+---------------------+
| 1 | Ram | IT | 85 | [email protected] |
| 2 | Rahim | EEE | 95 | [email protected] |
| 3 | Robert | ECE | 90 | [email protected] |
+----+--------+--------+------------+---------------------+
Angenommen, der Name des Benutzers lautet myuser und sein Passwort ist password.
Erstellen einer Schülerklasse
Ein ... kreieren Student Klasse mit setter und getter Methoden wie unten gezeigt.
public class Student implements java.io.Serializable {
private int id, percent;
private String name, branch, email;
public int getId() {
return id;
}
public String getName() {
return name;
}
public String getBranch() {
return branch;
}
public int getPercent() {
return percent;
}
public String getEmail() {
return email;
}
public void setID(int id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setBranch(String branch) {
this.branch = branch;
}
public void setPercent(int percent) {
this.percent = percent;
}
public void setEmail(String email) {
this.email = email;
}
}
Remote-Schnittstelle definieren
Definieren Sie die Remote-Schnittstelle. Hier definieren wir eine Remote-Schnittstelle mit dem NamenHello mit einer Methode namens getStudents ()drin. Diese Methode gibt eine Liste zurück, die das Objekt der Klasse enthältStudent.
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.*;
// Creating Remote interface for our application
public interface Hello extends Remote {
public List<Student> getStudents() throws Exception;
}
Entwicklung der Implementierungsklasse
Erstellen Sie eine Klasse und implementieren Sie die oben erstellte interface.
Hier implementieren wir die getStudents() Methode der Remote interface. Wenn Sie diese Methode aufrufen, werden die Datensätze einer Tabelle mit dem Namen abgerufenstudent_data. Setzt diese Werte mithilfe ihrer Setter-Methoden auf die Student-Klasse, fügt sie einem Listenobjekt hinzu und gibt diese Liste zurück.
import java.sql.*;
import java.util.*;
// Implementing the remote interface
public class ImplExample implements Hello {
// Implementing the interface method
public List<Student> getStudents() throws Exception {
List<Student> list = new ArrayList<Student>();
// JDBC driver name and database URL
String JDBC_DRIVER = "com.mysql.jdbc.Driver";
String DB_URL = "jdbc:mysql://localhost:3306/details";
// Database credentials
String USER = "myuser";
String PASS = "password";
Connection conn = null;
Statement stmt = null;
//Register JDBC driver
Class.forName("com.mysql.jdbc.Driver");
//Open a connection
System.out.println("Connecting to a selected database...");
conn = DriverManager.getConnection(DB_URL, USER, PASS);
System.out.println("Connected database successfully...");
//Execute a query
System.out.println("Creating statement...");
stmt = conn.createStatement();
String sql = "SELECT * FROM student_data";
ResultSet rs = stmt.executeQuery(sql);
//Extract data from result set
while(rs.next()) {
// Retrieve by column name
int id = rs.getInt("id");
String name = rs.getString("name");
String branch = rs.getString("branch");
int percent = rs.getInt("percentage");
String email = rs.getString("email");
// Setting the values
Student student = new Student();
student.setID(id);
student.setName(name);
student.setBranch(branch);
student.setPercent(percent);
student.setEmail(email);
list.add(student);
}
rs.close();
return list;
}
}
Serverprogramm
Ein RMI-Serverprogramm sollte die Remote-Schnittstelle implementieren oder die Implementierungsklasse erweitern. Hier sollten wir ein entferntes Objekt erstellen und es an das bindenRMI registry.
Es folgt das Serverprogramm dieser Anwendung. Hier erweitern wir die oben erstellte Klasse, erstellen ein Remote-Objekt und registrieren es in der RMI-Registrierung mit dem Bindungsnamenhello.
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class Server extends ImplExample {
public Server() {}
public static void main(String args[]) {
try {
// Instantiating the implementation class
ImplExample obj = new ImplExample();
// Exporting the object of implementation class (
here we are exporting the remote object to the stub)
Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 0);
// Binding the remote object (stub) in the registry
Registry registry = LocateRegistry.getRegistry();
registry.bind("Hello", stub);
System.err.println("Server ready");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
Client-Programm
Es folgt das Client-Programm dieser Anwendung. Hier rufen wir das entfernte Objekt ab und rufen die benannte Methode aufgetStudents(). Es ruft die Datensätze der Tabelle aus dem Listenobjekt ab und zeigt sie an.
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.*;
public class Client {
private Client() {}
public static void main(String[] args)throws Exception {
try {
// Getting the registry
Registry registry = LocateRegistry.getRegistry(null);
// Looking up the registry for the remote object
Hello stub = (Hello) registry.lookup("Hello");
// Calling the remote method using the obtained object
List<Student> list = (List)stub.getStudents();
for (Student s:list)v {
// System.out.println("bc "+s.getBranch());
System.out.println("ID: " + s.getId());
System.out.println("name: " + s.getName());
System.out.println("branch: " + s.getBranch());
System.out.println("percent: " + s.getPercent());
System.out.println("email: " + s.getEmail());
}
// System.out.println(list);
} catch (Exception e) {
System.err.println("Client exception: " + e.toString());
e.printStackTrace();
}
}
}
Schritte zum Ausführen des Beispiels
Im Folgenden finden Sie die Schritte zum Ausführen unseres RMI-Beispiels.
Step 1 - Öffnen Sie den Ordner, in dem Sie alle Programme gespeichert haben, und kompilieren Sie alle Java-Dateien wie unten gezeigt.
Javac *.java
Step 2 - Starten Sie die rmi Registrierung mit dem folgenden Befehl.
start rmiregistry
Dies startet eine rmi Registrierung in einem separaten Fenster wie unten gezeigt.
Step 3 - Führen Sie die Serverklassendatei wie unten gezeigt aus.
Java Server
Step 4 - Führen Sie die Clientklassendatei wie unten gezeigt aus.
java Client