Όταν μια εφαρμογή διακομιστή εκτελείται σε ένα υπολογιστή, τότε η εφαρμογή αυτή αναμένει αιτήματα από κάποιον πελάτη ('ακούει') σε μια συγκεκριμένη θύρα (port) αυτού του υπολογιστή. Οι θύρες αυτές καθορίζονται με βάση έναν αριθμό και αποτελούν μια λογική αφαίρεση του λογισμικού συστήματος και δεν αντιστοιχούν απαραίτητα σε συγκεκριμένη θύρα Εισόδου/Εξόδου (I/O port). Είναι δουλειά του λειτουργικού συστήματος να απεικονίσει τη θύρα της εφαρμογής διακομιστή σε κάποια συγκεκριμένη συσκευή ή άλλη εφαρμογή. Οι συνηθέστερες εφαρμογές όταν εκτελούνται δεσμεύουν συγκεκριμένο αριθμό θύρας (port number) με αριθμό στο διάστημα 0 έως 1024 (δες αρχείο /etc/services).
Ο πελάτης, από την άλλη πλευρά, για να αποστείλει το αίτημά του, μέσω του δικτύου, χρησιμοποιεί και αυτός την αντίστοιχη θύρα για την αποστολή του αιτήματός του. Επιπλέον όμως πρέπει να γνωρίζει όχι μόνο τη θύρα όπου αναμένει η εφαρμογή διακομιστή, αλλά και την ΙP διεύθυνση του πελάτη. O συνδυασμός IP διεύθνσης και αριθμού θύρας ονομάζεται υποδοχή (socket).
Μόλις το αρχικό αίτημα του πελάτη παραληφθεί και γίνει αποδεκτό από την εφαρμογή διακομιστή, η αντίστοιχη υποδοχή του πελάτη είναι πλέον γνωστή στον διακομιστή. Έτσι εγκαθίσταται μια σύνδεση (connection), δηλαδή μια μόνιμη αμφίδρομη ροή δεδομένων μεταξύ πελάτη και διακομιστή. Μετά την εγκατάσταση της σύνδεσης, ο πελάτης και διακομιστής επικοινωνούν μέσω ανάγνωσης και εγγραφής δεδομένων στις ροές των υποδοχών τους. Η σύνδεση τερματίζεται είτε με το κλείσιμο της υποδοχή ή το τερματισμό της εφαρμογής. Το πακέτο
java.net
της Java παρέχει τις κλάσειςSocket
καιServerSocket
που υλοποιούν τα δύο άκρα του μηχανισμού επικοινωνίας μέσω υποδοχών που παρουσιάστηκε παραπάνω, αποκρύπτοντας τις τεχνικές λεπτομέρειες της δικτυακής επικοινωνίας.
Ας δούμε ένα απλό παράδειγμα εγκαθίδρυσης επικοινωνίας. Κατ' αρχή θα ασχοληθούμε μόνο με τη πλευρά του πελάτη. Στο παράδειγμά μας θεωρούμε οτι ο διακομιστής είναι έτοιμος και αναμένει αιτήματα. Ο πελάτης χρησιμοποιεί τη κλάσηSocket
για να εγκαθιδρύσει σύνδεση με το διακομιστή και στη συνέχεια αποστέλει και λαμβάνει δεδομένα μέσω αυτής της υποδοχής.Το πρόγραμμα πελάτη,
EchoClient
, επικοινωνεί με ένα διακομιστή 'ηχούς'. Η εφαρμογή διακομιστή 'ηχούς' (echo) παραλαμβάνει δεδομένα και απλά τα αποστέλει πίσω. Η υπηρεσία είναι διαθέσιμη στη θύρα με αριθμό 7. Σημειώνεται οτι σε αρκετές πρόσφατες εκδόσεις Unix/Linux η υπηρεσία αυτή δεν είναι διαθέσιμη, γιατί θεωρείται πιθανό κενό ασφαλείας. Επομένως για την ορθή λειτουργία του παραδείγματος θα πρέπει να βεβαιωθείτε οτι το σύστημά σας διαθέτει υπηρεσία 'ηχούς'.
Το πρόγραμμα
EchoClient
φαίνεται παρακάτω:import java.io.*;
import java.net.*;
public class EchoClient {
public static void main(String[] args) throws IOException {
Socket echoSocket = null;
PrintWriter out = null;
BufferedReader in = null;
try {
echoSocket = new Socket("taranis", 7);
out = new PrintWriter(echoSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(
echoSocket.getInputStream()));
} catch (UnknownHostException e) {
System.err.println("Don't know about host: taranis.");
System.exit(1);
} catch (IOException e) {
System.err.println("Couldn't get I/O for "
+ "the connection to: taranis.");
System.exit(1);
}
BufferedReader stdIn = new BufferedReader(
new InputStreamReader(System.in));
String userInput;
while ((userInput = stdIn.readLine()) != null) {
out.println(userInput);
System.out.println("echo: " + in.readLine());
}
out.close();
in.close();
stdIn.close();
echoSocket.close();
}
}Ας μελετήσουμε το πρόγραμμα. Η δομή
try
στην αρχή της μεθόδουmain
είναι κρίσιμη. Οι τρείς εντολές που ακολουθούν εγκαθιστούν μια σύνδεση υποδοχών μεταξύ πελάτη και διακομιστή, και στη συνέχεια ορίζουν δύο ροές, μια εγγραφής προς και μια ανάγνωσης από την υποδοχή (PrintWriter out
καιBufferedReader in
):Η πρώτη εντολή δημιουργεί ένα αντικείμενοechoSocket = new Socket("taranis", 7);
out = new PrintWriter(echoSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(
echoSocket.getInputStream()));Socket
με όνομαechoSocket
. Ο κατασκευαστήςSocket
που χρησιμοποιείται εδώ δέχεται ως παραμέτρους το όνομα και τη θύρα του διακομιστή. Αντί του ονόματος θα μπορύσαμε να δώσουμε την IP διεύθνση του διακομιστή. Το όνομα που χρησιμοποιείται εδώ, δηλαδήtaranis
, προφανώς θα πρέπει να τροποποιηθεί με βάση το όνομα ή την IP διεύθυνση του υπολογιστή που θα σας προσφέρει την υπηρεσία 'ηχούς'. Η θύρα αριθμός 7 είναι η τυπική θύρα που 'ακούει' η υπηρεσία 'ηχούς'.Η δεύτερη εντολή ανοίγει μια ροή εγγραφής
PrintWriter
προς την υποδοχή, με όνομαout
. Όμοια, η τρίτη εντολή ανοίγει μια ροή ανάγνωσηςBufferedReader
από την υποδοχή, με όνομαin
. Οι συγκεκριμένοι τύποι επιτρέπουν χρήση χαρακτήρων Unicode, αλλά θα μπορούσαν να χρησιμοποιηθούν και άλλοι.Για να στείλει δεδομένα προς το διακομιστή, ο πελάτης
EchoClient
γράφει στη ροήPrintWriter out
. Για να παραλάβει δεδομένα από το διακομιστή, ο πελάτηςEchoClient
διαβάζει από τη ροήBufferedReader in
.Ακολουθούν οι χειρισμοί των πιθανών εξαιρέσεων (όπως ένας άγνωστος διακομιστής ή αδυναμία ανοίγματος ροής). Το επόμενο ενδιαφέρον σημείο είναι η δομή
while
. Το πρόγραμμα διαβάζει μια γραμμή από το πληκτρολόγοstdIn
και την γράφει στη ροήPrintWriter out
, δηλαδή την αποστέλει στο διακομιστή μέσω της υποδοχής. Στη τελευταία εντολή της δομήςwhile
, ο πελάτης διαβάζει από τη ροήBufferedReadr in
, δηλαδή περιμένει δεδομένα από την υποδοχή. Η μέθοδοςreadLine
θα περιμένει μέχρι ο διακομιστής να στείλει πίσω τα δεδομένα που έστειλε οEchoClient
. Όταν ηreadline
επιστρέψει, οEchoClient
εμφανίζει τα αποτελέσματα στην οθόνη.String userInput;
while ((userInput = stdIn.readLine()) != null) {
out.println(userInput);
System.out.println("echo: " + in.readLine());
}Η επανάληψη
while
συνεχίζει μέχρι ο χρήστης να εισάγει το χαρακτήρα end-of-input character (^D για Unix/Linux). Μετά την έξοδο από την επανάληψη το πρόγραμμα κλείνει τις ροές και την υποδοχή:Προφανώς ο απλός τερματισμός του προγράμματος είναι επίσης επαρκής αλλά ο καλός προγραμματισμός υπαγορεύει τη ρητή διαχείριση των πιθανών εκκρεμοτήτων. Η σειρά εδώ είναι σημαντική: πρώτα κλείνουμε τις ροές και μετά την υποδοχή.out.close();
in.close();
stdIn.close();
echoSocket.close();Το πρόγραμμα του πελάτη είναι απλό, επειδή το πρωτόκολλο της 'ηχούς' είναι πολύ απλό. Ο πελάτης στέλνει μια γραμμή κειμένου στο διακομιστή και αυτός το επιστρέφει. Αν το πρωτόκολλο ήταν πιο σύνθετο, τότε προφανώς και το πρόγραμμα του πελάτη (και του διακομιστή φυσικά) θα ήταν πιο σύνθετο. Όμως η βασική δομή παραμένει σταθερή:
Αυτό που ουσιαστικά αλλάζει είναι το βήμα 3.
- Άνοιγμα υποδοχής.
- Άνοιγμα ροών εγγραφής και ανάγνωσης της υποδοχής.
- Επανάληψη μέχρι το τερματισμό της σύνδεσης : Εγγραφή στην υποδοχή, αναμονή και ανάγνωση από την υποδοχή, εκτέλεση λειτουργίας σύμφωνα με το προκαθορισμένο πρωτόκολλο.
- Κλείσιμο ροών.
- Κλείσιμο υποδοχής.
Αυτή η ενότητα παρουσιάζει την ανάπτυξη ενός συστήματος πελάτη / διακομιστή που 'παίζουν' το παιγνίδι Knock Knock. Το παιγνίδι αφορά στην ανταλλαγή 'αστείων' μεταξύ δύο παικτών. Ο διάλογος έχει συγκεκριμένη δομή. Αυτό που συνήθως αλλάζει είναι το όνομα και το 'αστείο':Server: "Knock knock!"
Client: "Who's there?"
Server: "Dexter."
Client: "Dexter who?"
Server: "Dexter halls with boughs of holly."
Client: "Groan."Το παράδειγμα συνίσταται από δύο ανεξάρτητα προγράμματα: το πελάτη και το διακομιστή. Το πρόγραμμα του πελάτη υλοποιείται από μια κλαη, τη
KnockKnockClient
, και είναι αρκετά όμοιο με το πρόγραμμαEchoClient
. Το πρόγραμμα του διακομιστή υλοποιείται με δύο κλάσεις: τιςKnockKnockServer
καιKnockKnockProtocol
. Η κλάσηKnockKnockServer
περιέχει τη μέθοδοmain
που εκτελεί τις βασικές λειτουργίες: αναμονή αιτήματος, εγκαθίδρυση σύνδεσης, εγγραφή και ανάγνωση στην υποδοχή. Η κλάσηKnockKnockProtocol
διαχειρίζεται το στοιχειώδες πρωτόκολλο των 'αστείων' ερωταποκρίσεωνs. Πρόκειται για μια πολύ απλή μηχανή πεπρασμένων καταστάσεων: με βάση το τρέχον μήνυμα του πελάτη και τη τρέχουσα κατάσταση (knock knock, όνομα, αστείο), επιστρέφει την απάντηση του διακομιστή και τη νέα κατάσταση. Ο πελάτης πρέπει να είναι ενήμερος για το πρωτόκολλο που χρησιμοποιεί ο διακομιστής. Στη περίπτωσή μας ο πελάτης παρέχει μια απλή διεπιφάνεια μεταξύ του χρήστη και του διακομιστή. Επομένως η υλοποίηση του πρωτοκόλλου από τη πλευρά του πελάτη επαφίεται στο χρήστη. Σε άλλες περιπτώσεις, το πρόγραμμα του πελάτη θα πρέπει να έχει στη διάθεσή του μια κλάση ανάλογη τουKnockKnockProtocol
.
Παρακάτω φαίνεταο ο κώδικας της κλάσηςKnockKnockServer
.
import java.net.*;
import java.io.*;
public class KnockKnockServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(4444);
} catch (IOException e) {
System.err.println("Could not listen on port: 4444.");
System.exit(1);
}
Socket clientSocket = null;
try {
clientSocket = serverSocket.accept();
} catch (IOException e) {
System.err.println("Accept failed.");
System.exit(1);
}
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(
clientSocket.getInputStream()));
String inputLine, outputLine;
KnockKnockProtocol kkp = new KnockKnockProtocol();
outputLine = kkp.processInput(null);
out.println(outputLine);
while ((inputLine = in.readLine()) != null) {
outputLine = kkp.processInput(inputLine);
out.println(outputLine);
if (outputLine.equals("Bye."))
break;
}
out.close();
in.close();
clientSocket.close();
serverSocket.close();
}
}
Το πρόγραμμα ξεκινά με τη δημιουργία ενός αντικειμένου
ServerSocket
που ακούει σε μια συγκεκριμένη θύρα (δείτε το κώδικα σε bold που ακολουθεί). Όταν γράφουμε κώδικα για διακομιστή, επιλέγουμε θύρα η οποία δεν έχει καταληφθεί από άλλη εφαρμογή διακομιστή. Συνήθως επιλέγουμε αριθμό θύρας μεγαλύτερο του 1024, αφού αυτοί οι αριθμοί είναι δεσμευμένοι από 'επίσημες' υπηρεσίες (δείτε /etc/services). Εδώ ο διακομιστήςKnockKnockServer
ακούει τη θύρα 4444 :try {
serverSocket = new ServerSocket(4444);
} catch (IOException e) {
System.out.println("Could not listen on port: 4444");
System.exit(-1);
}Ο κατασκευαστής
ServerSocket
παράγει εξαίρεση αν δεν μπορεί να 'ακούσει' τη συγκεκριμένη θύρα (αν πχ η θύρα χρησιμοποιείται από άλλο διακομιστή). Σε αυτή τη περίπτωση, οKnockKnockServer
προκαλεί εξαίρεση και τερματίζει.Αν ο το αντικείμενο
ServerSocket
δημιουργηθεί με επιτυχία, τότε αρχίζει να 'ακούει' στη θύρα και αναμένει σύνδεση (εντολή σε bold):
H μέθοδοςSocket clientSocket = null;
try {
clientSocket = serverSocket.accept();
} catch (IOException e) {
System.out.println("Accept failed: 4444");
System.exit(-1);
}accept
αναμένει μέχρι να δεχτεί ένα αίτημα σύνδεσης από κάποιο πελάτη. Μόλις εγκαθιδρυθεί η σύνδεση η μέθοδος επιστρέφει ένα νέο αντικείμενοSocket
που συνδέεται στην ίδια θύρα. Ο διακομιστής επικοινωνεί με το πελάτη μέσω αυτού του αντικειμένουSocket
και συνεχίζει να ακούει στην αρχική υποδοχήServerSocket
. Η συγκεκριμένη έκδοση του προγράμματος δεν μπορεί να υποστηρίξει πολλαπλές αιτήσεις πελατών. Μια τροποποιημένη έκδοση του προγράμματος για την υποστήριξη πολλαπλών αιτήσεων πελατών παρουσιάζεται παρακάτω.Στη συνέχεια ο διακομιστής επικοινωνεί με το πελάτη με το κώδικα που ακολουθεί:
Ο κώδικας αυτός:PrintWriter out = new PrintWriter(
clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(
clientSocket.getInputStream()));
String inputLine, outputLine;
// initiate conversation with client
KnockKnockProtocol kkp = new KnockKnockProtocol();
outputLine = kkp.processInput(null);
out.println(outputLine);
while ((inputLine = in.readLine()) != null) {
outputLine = kkp.processInput(inputLine);
out.println(outputLine);
if (outputLine.equals("Bye."))
break;
}Το βήμα 1 είναι γνωστό. Το βήμα 2 απαιτεί λίγο σχολιασμό. Κατόπιν ο κώδικας δημιουργεί ένα αντικείμενο της κλάσης
- Ανοίγει ροές εγγραφής και ανάγνωσηε στην υποδοχή πελάτη.
- Ξεκινά την επικοινωνία με το πελάτη μέσω της ροής εγγραφής στην υποδοχή πελάτη (τμήμα σε bold).
- Επικοινωνεί με το πελάτη μέσω των ροών ανάγνωσης και εγγραφής στην υποδοχή (βρόχος
while
).KnockKnockProtocol
- το αντικείμενο αυτό υλοποιεί τη μηχανή πεπερασμένων καταστάσεων του πρωτοκόλλου. Μετά τη δημιουργία του αντικειμένουKnockKnockProtocol kkp
, ο κώδικας καλεί τη μέθοδοKnockKnockProtocol
.processInput
η οποία επεξεργάζεται τηνinputLine
με βάση το πρωτόκολλο και επιστρέφει τηνoutputLine
. HoutputLine
αποστέλλεται στη ροή εγγραφής της υποδοχής. Στο βήμα 2, αρχικά ηinputLine
είναιnull
, πράγμα που σημαίνει οτι πρόκειται για την εκκίνηση της επικοινωνίας, άρα το πρωτόκολλο παράγει το πρώτο μήνυμα "Knock! Knock!" για το πελάτη.Στο βήμα 3 ο βρόχος
while
ξεκινά με την παραλαβή της απάντησης του πελάτηinputLine
από τη ροή ανάγνωσης της υποδοχής. Ο κώδικας καλεί τη μέθοδοKnockKnockProtocol
.processInput
η οποία επεξεργάζεται τηνinputLine
με βάση το πρωτόκολλο και επιστρέφει τηνoutputLine
. HoutputLine
αποστέλλεται στη ροή εγγραφής της υποδοχής. Η επικοινωνία τερματίζεται όταν το πρωτόκολλο παράγειoutputLine
ίση με
Bye
.Στις τελευταίες γραμμές ο κώδικα κλείνει τις ανοικτές ροές και υποδοχές:
out.close();
in.close();
clientSocket.close();
serverSocket.close();
Η κλάσηKnockKnockProtocol
υλοποιεί το πρωτόκολλο με βάση το οποίο επικοινωνούν ο πελάτης και ο διακομιστής. Το πρωτόκολλο υλοποιεί μια μηχανή πεπερασμένων καταστάσεων. Η είσοδος σε συνδυασμό με τη τρέχουσα κατάσταση καθορίζουν την έξοδο και τη νέα κατάσταση. Η βασική δομή του κώδικα είναι ένα nested ifelseόπου το πρώτο επίπεδο εξαρτάται από τη τρέχουσα κατάσταση ενώ το δεύτερο από την είσοδο. Κάθε κλάδος του nested ifelse τελειώενει με καθορισμό της εξόδου και την νέας κατάστασης.
import java.net.*;
import java.io.*;
public class KnockKnockProtocol {
private static final int WAITING = 0;
private static final int SENTKNOCKKNOCK = 1;
private static final int SENTCLUE = 2;
private static final int ANOTHER = 3;
private static final int NUMJOKES = 5;
private int state = WAITING;
private int currentJoke = 0;
private String[] clues = { "Turnip", "Little Old Lady", "Atch", "Who", "Who" };
private String[] answers = { "Turnip the heat, it's cold in here!",
"I didn't know you could yodel!",
"Bless you!",
"Is there an owl in here?",
"Is there an echo in here?" };
public String processInput(String theInput) {
String theOutput = null;
if (state == WAITING) {
theOutput = "Knock! Knock!";
state = SENTKNOCKKNOCK;
} else if (state == SENTKNOCKKNOCK) {
if (theInput.equalsIgnoreCase("Who's there?")) {
theOutput = clues[currentJoke];
state = SENTCLUE;
} else {
theOutput = "You're supposed to say \"Who's there?\"! " +
"Try again. Knock! Knock!";
}
} else if (state == SENTCLUE) {
if (theInput.equalsIgnoreCase(clues[currentJoke] + " who?")) {
theOutput = answers[currentJoke] + " Want another? (y/n)";
state = ANOTHER;
} else {
theOutput = "You're supposed to say \"" +
clues[currentJoke] +
" who?\"" +
"! Try again. Knock! Knock!";
state = SENTKNOCKKNOCK;
}
} else if (state == ANOTHER) {
if (theInput.equalsIgnoreCase("y")) {
theOutput = "Knock! Knock!";
if (currentJoke == (NUMJOKES - 1))
currentJoke = 0;
else
currentJoke++;
state = SENTKNOCKKNOCK;
} else {
theOutput = "Bye.";
state = WAITING;
}
}
return theOutput;
}
}
Είναι προφανές οτι ένα ζεύγος πελάτη/διακομιστή πρέπει να μιλά το ίδιο πρωτόκολλο, αλλιώς η επικοινωνία δεν θα έχει νόημα.
Η κλάσηKnockKnockClient
βασίζεται στο πρόγραμμαEchoClient
που είδαμε παραπάνω.
import java.io.*;
import java.net.*;
public class KnockKnockClient {
public static void main(String[] args) throws IOException {
Socket kkSocket = null;
PrintWriter out = null;
BufferedReader in = null;
try {
kkSocket = new Socket("taranis", 4444);
out = new PrintWriter(kkSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(kkSocket.getInputStream()));
} catch (UnknownHostException e) {
System.err.println("Don't know about host: taranis.");
System.exit(1);
} catch (IOException e) {
System.err.println("Couldn't get I/O for the connection to: taranis.");
System.exit(1);
}
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
String fromServer;
String fromUser;
while ((fromServer = in.readLine()) != null) {
System.out.println("Server: " + fromServer);
if (fromServer.equals("Bye."))
break;
fromUser = stdIn.readLine();
if (fromUser != null) {
System.out.println("Client: " + fromUser);
out.println(fromUser);
}
}
out.close();
in.close();
stdIn.close();
kkSocket.close();
}
}
H δομήtry catch
έχει ήδη συζητηθεί σε προηγούμενη ενότητα: εγκαθιδρύει τη σύνδεση υποδοχής και ανοίγει τις ροές ανάγνωσης και εγγραφής στην υποδοχή. Σε περίπτωση σφάλματος παράγει τα κατάλληλα μηνύματα.
Στη συνέχεια, το πρόγραμμα ανοίγει μια ροή ανάγνωσης από το
stdIn
ώστε ο χρήστης να εισάγει τις απαντήσεις που ο πελάτης στέλνει στο διακομιστή. Ο βρόχοςwhile
είναι το κύριο τμήμα του προγράμματος. Ο πελάτης λαμβάνει το μήνυμα του διακομιστή από τη ροή ανάγνωσης της υποδοχής και το εμφανίζει στην οθόνη του χρήστη. Αν το μήνυμα είναιBye
ο βρόχος τερματίζει. Αλλιώς, το πρόγραμμα αναμένει την απόκριση του χρήστη, την οποία εμφανίζει και πάλι στην οθόνη του χρήστη, αλλά τη στέλνει στο διακομιστή μέσω της ροής εγγραφής της υποδοχής.
while ((fromServer = in.readLine()) != null) {
System.out.println("Server: " + fromServer);
if (fromServer.equals("Bye."))
break;
fromUser = stdIn.readLine();
if (fromUser != null) {
System.out.println("Client: " + fromUser);
out.println(fromUser);
}
}Το πρόγραμμα τερματίζει με το κλείσιμο των ροών και της υποδοχής:
out.close();
in.close();
stdIn.close();
kkSocket.close();
Πρώτα ξεκινούμε το πρόγραμμα του διακομιστή, όπως οποιοδήποτε πρόγραμμα Java. Κατόπιν ξεκινούμε το πρόγραμμα του πελάτη με παρόμοιο τρόπο. Αν ο πελάτης ξεκινήσει πριν το διακομιστή θα τερματίσει με σφάλμα.Το πρόγραμμα του πελάτη μπορεί να εκτελεστεί στο ίδιο ή σε άλλο υπολογιστή, αρκεί να τροποποιηθεί κατάλληλα η παράμετρος της δημιουργίας της υποδοχής. Δηλαδή αντί
taranis
θα πρέπει να εισάγουμε μια έγκυρη IP διεύθυνση ή όνομα υπολογιστή, ή ακόμη καιlocalhost.
Μετά την επιτυχμένη σύνδεση ο χρήστης βλέπει στην οθόνη το μήνυμα:
Η απάντηση του χρήστη πρέπει να είναι:Server: Knock! Knock!Μετά τη νέα απόκριση του διακομιστή, η οθόνη του χρήστη είναι ως εξής (με bold αυτά που έχει εισάγει ο χρήστης):Who's there?Η νέα απάντηση του χρήστη:Server: Knock! Knock!
Who's there?
Client: Who's there?
Server: TurnipΚαι μετά την νέα απόκριση του διακομιστή:Turnip who?Για ένα νέο διάλογο ο χρήστης εισάγει y; για τερματισμό n. Αν ο χρήστης εισάγει n, ο διακομιστής αποκρίνεται "Bye." και έτσι τα δύο προγράμματα τερματίζουν.Server: Knock! Knock!
Who's there?
Client: Who's there?
Server: Turnip
Turnip who?
Client: Turnip who?
Server: Turnip the heat, it's cold in here! Want another? (y/n)
Σε περίπτωση σφάλματος πληκτρολόγησης του χρήστη ο
KnockKnockServer
αποκρίνεται κάπως έτσι:και ξεκινά το διάλογο από την αρχή:Server: You're supposed to say "Who's there?"!Server: Try again. Knock! Knock!
Το πρόγραμμαKnockKnockServer
μπορεί να εξυπηρετήσει μόνο μια αίτηση πελάτη κάθε φορά. Συνήθως όμως, ένας διακομιστής μπορεί να δεχτεί πολλαπλά σχεδόν ταυτόχρονα αιτήματα στο ίδιοServerSocket
. Στη λύση που παρουσιάστηκε, τα αιτήματα θα μπούν σε μια ουρά και θα εξυπηρετηθούν ακολουθιακά.
Εναλλακτικά, ένας πολυνηματικός διακομιστής μπορεί να δημιουργήσει ένα νέο νήμα για κάθε αίτημα στοServerSocket
. Με αυτό το τρόπο ο διακομιστής μπορεί να εξυπηρετήσει πολλαπλούς πελάτες ταυτόχρονα.Η βασική δομή ενός πολυνηματικού διακομιστή είναι η εξής:
Το πρόγραμμα του πολυνηματικού διακομιστή αποτελείται από δύο κλάσεις: τηwhile (true) {
accept a connection ;
create a thread to deal with the client ;
end whileKKMultiServer
και ηKKMultiServerThread
.
Ο κώδικαςKKMultiServer
αποτελεί τη κύρια μέθοδο του διακομιστή: περιέχει ένα ατερμονα βρόχο που αναμένει αιτήματα σύνδεσης από πελάτες προς τοServerSocket
. Μόλις παραληφθεί ένα αίτημα, οKKMultiServer
δέχεται τη σύνδεση, δημιουργεί ένα νέο νήμα (αντικείμενο reates a newKKMultiServerThread
), στο οποίο περνά ως παράμετρο την υποδοχή που δημιουργείται με την αποδοχή της σύνδεσης. Κατόπιν οKKMultiServer
είναι έτοιμος να εξυπηρετήσει νέο αίτημα σύνδεσης.
Το νήμαKKMultiServerThread
αναλαμβάνει όλη την επικοινωνία μεταξύ των υποδοχών πελάτη και διακομιστή για τη συγκεκριμένη σύνδεση.
import java.net.*;
import java.io.*;
public class KKMultiServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = null;
boolean listening = true;
try {
serverSocket = new ServerSocket(4444);
} catch (IOException e) {
System.err.println("Could not listen on port: 4444.");
System.exit(-1);
}
while (listening)
new KKMultiServerThread(serverSocket.accept()).start();
serverSocket.close();
}
}
import java.net.*;
import java.io.*;
public class KKMultiServerThread extends Thread {
private Socket socket = null;
public KKMultiServerThread(Socket socket) {
super("KKMultiServerThread");
this.socket = socket;
}
public void run() {
try {
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
String inputLine, outputLine;
KnockKnockProtocol kkp = new KnockKnockProtocol();
outputLine = kkp.processInput(null);
out.println(outputLine);
while ((inputLine = in.readLine()) != null) {
outputLine = kkp.processInput(inputLine);
out.println(outputLine);
if (outputLine.equals("Bye"))
break;
}
out.close();
in.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}