Προγραμματισμός Υποδοχών σε Java

Όταν μια εφαρμογή διακομιστή εκτελείται σε ένα υπολογιστή, τότε η εφαρμογή αυτή αναμένει αιτήματα από κάποιον πελάτη ('ακούει') σε μια συγκεκριμένη θύρα (port) αυτού του υπολογιστή. Οι θύρες αυτές καθορίζονται με βάση έναν αριθμό και αποτελούν μια λογική αφαίρεση του λογισμικού συστήματος και δεν αντιστοιχούν απαραίτητα σε συγκεκριμένη θύρα Εισόδου/Εξόδου (I/O port). Είναι δουλειά του λειτουργικού συστήματος να απεικονίσει τη θύρα της εφαρμογής διακομιστή σε κάποια συγκεκριμένη συσκευή ή άλλη εφαρμογή. Οι συνηθέστερες εφαρμογές όταν εκτελούνται δεσμεύουν συγκεκριμένο αριθμό θύρας (port number) με αριθμό στο διάστημα 0 έως 1024 (δες αρχείο /etc/services). 

Ο πελάτης, από την άλλη πλευρά, για να αποστείλει το αίτημά του, μέσω του δικτύου, χρησιμοποιεί και αυτός την αντίστοιχη θύρα για την αποστολή του αιτήματός του. Επιπλέον όμως πρέπει να γνωρίζει όχι μόνο τη θύρα όπου αναμένει η εφαρμογή διακομιστή, αλλά και την ΙP διεύθυνση του πελάτη. O συνδυασμός IP διεύθνσης και αριθμού θύρας ονομάζεται υποδοχή (socket).

A client's connection request
Μόλις το αρχικό αίτημα του πελάτη παραληφθεί και γίνει αποδεκτό από την εφαρμογή διακομιστή, η αντίστοιχη υποδοχή του πελάτη είναι πλέον γνωστή στον διακομιστή. Έτσι εγκαθίσταται μια σύνδεση (connection), δηλαδή μια μόνιμη αμφίδρομη ροή δεδομένων μεταξύ πελάτη και διακομιστή.
The connection is made
Μετά την εγκατάσταση της σύνδεσης, ο πελάτης και διακομιστής επικοινωνούν μέσω ανάγνωσης και εγγραφής δεδομένων στις ροές των υποδοχών τους. Η σύνδεση τερματίζεται είτε με το κλείσιμο της υποδοχή ή το τερματισμό της εφαρμογής.

Το πακέτο 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();
Προφανώς ο απλός τερματισμός του προγράμματος είναι επίσης επαρκής αλλά ο καλός προγραμματισμός υπαγορεύει τη ρητή διαχείριση των πιθανών εκκρεμοτήτων. Η σειρά εδώ είναι σημαντική: πρώτα κλείνουμε τις ροές και μετά την υποδοχή.

Το πρόγραμμα του πελάτη είναι απλό, επειδή το πρωτόκολλο της 'ηχούς' είναι πολύ απλό. Ο πελάτης στέλνει μια γραμμή κειμένου στο διακομιστή και αυτός το επιστρέφει. Αν το πρωτόκολλο ήταν πιο σύνθετο, τότε προφανώς και το πρόγραμμα του πελάτη (και του διακομιστή φυσικά) θα ήταν πιο σύνθετο. Όμως η βασική δομή παραμένει σταθερή:

  1. Άνοιγμα υποδοχής.
  2. Άνοιγμα ροών εγγραφής και ανάγνωσης της υποδοχής.
  3. Επανάληψη μέχρι το τερματισμό της σύνδεσης : Εγγραφή στην υποδοχή, αναμονή και ανάγνωση από την υποδοχή, εκτέλεση λειτουργίας σύμφωνα με το προκαθορισμένο πρωτόκολλο.
  4. Κλείσιμο ροών.
  5. Κλείσιμο υποδοχής.
Αυτό που ουσιαστικά αλλάζει είναι το βήμα 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.

O Διακομιστής Knock Knock

Παρακάτω φαίνεταο ο κώδικας της κλάσης 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):

Socket clientSocket = null;
try {
clientSocket = serverSocket.accept();
} catch (IOException e) {
System.out.println("Accept failed: 4444");
System.exit(-1);
}
H μέθοδος 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).
  3. Επικοινωνεί με το πελάτη μέσω των ροών ανάγνωσης και εγγραφής στην υποδοχή (βρόχος while).
Το βήμα 1 είναι γνωστό. Το βήμα 2 απαιτεί λίγο σχολιασμό.  Κατόπιν ο κώδικας δημιουργεί ένα αντικείμενο της κλάσης KnockKnockProtocol- το αντικείμενο αυτό υλοποιεί τη μηχανή πεπερασμένων καταστάσεων του πρωτοκόλλου. Μετά τη δημιουργία του αντικειμένου KnockKnockProtocol kkp, ο κώδικας καλεί τη μέθοδο KnockKnockProtocol.processInput η οποία επεξεργάζεται την inputLine με βάση το πρωτόκολλο και επιστρέφει την outputLine. H outputLine αποστέλλεται στη ροή εγγραφής της υποδοχής. Στο βήμα 2, αρχικά η inputLine είναι null, πράγμα που σημαίνει οτι πρόκειται για την εκκίνηση της επικοινωνίας, άρα το πρωτόκολλο παράγει το πρώτο μήνυμα "Knock! Knock!" για το πελάτη.

Στο βήμα 3 ο βρόχος while ξεκινά με την παραλαβή της απάντησης του πελάτη inputLine από τη ροή ανάγνωσης της υποδοχής. Ο κώδικας καλεί τη μέθοδο KnockKnockProtocol.processInput η οποία επεξεργάζεται την inputLine με βάση το πρωτόκολλο και επιστρέφει την outputLine.  H outputLine αποστέλλεται στη ροή εγγραφής της υποδοχής. Η επικοινωνία τερματίζεται όταν το πρωτόκολλο παράγει outputLine ίση με Bye .

Στις τελευταίες γραμμές ο κώδικα κλείνει τις ανοικτές ροές και υποδοχές:

out.close();
in.close();
clientSocket.close();
serverSocket.close();

Το Πρωτόκολλο Knock Knock

Η κλάση 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;
}
}

Είναι προφανές οτι ένα ζεύγος πελάτη/διακομιστή πρέπει να μιλά το ίδιο πρωτόκολλο, αλλιώς η επικοινωνία δεν θα έχει νόημα.

Ο Πελάτης Knock Knock Client

Η κλάση 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!
Η απάντηση του χρήστη πρέπει να είναι:
Who's there?
Μετά τη νέα απόκριση του διακομιστή, η οθόνη του χρήστη είναι ως εξής (με bold αυτά που έχει εισάγει ο χρήστης):
Server: Knock! Knock!
Who's there?
Client: Who's there?
Server: Turnip
Η νέα απάντηση του χρήστη:
Turnip who?
Και μετά την νέα απόκριση του διακομιστή:
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)
Για ένα νέο διάλογο ο χρήστης εισάγει y; για τερματισμό n. Αν ο χρήστης εισάγει n, ο διακομιστής αποκρίνεται "Bye." και έτσι τα δύο προγράμματα τερματίζουν.

Σε περίπτωση σφάλματος πληκτρολόγησης του χρήστη ο 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 while
Το πρόγραμμα του πολυνηματικού διακομιστή αποτελείται από δύο κλάσεις: τη KKMultiServer και η KKMultiServerThread.

Ο κώδικας KKMultiServer αποτελεί τη κύρια μέθοδο του διακομιστή: περιέχει ένα ατερμονα βρόχο που αναμένει αιτήματα σύνδεσης από πελάτες προς το ServerSocket. Μόλις παραληφθεί ένα αίτημα, ο KKMultiServer δέχεται τη σύνδεση, δημιουργεί ένα νέο νήμα (αντικείμενο reates a new KKMultiServerThread), στο οποίο περνά ως παράμετρο την υποδοχή που δημιουργείται με την αποδοχή της σύνδεσης. Κατόπιν ο 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();
}
}
}