Lecture 22 Sockets

Joseph Haugh

University of New Mexico

Code

Download the code here

What is a Socket?

  • A socket is an endpoint for two-way communication between programs over a network
  • Built on TCP: messages arrive in order and reliably
  • Java wraps TCP in familiar stream objects: read and write like a file
  Client                              Server
    |                                   |
  Socket <===== TCP connection =====> Socket
    |                                   |
InputStream                       InputStream
OutputStream                      OutputStream

Socket

  • Socket(String host, int port)
  • InputStream getInputStream()
  • OutputStream getOutputStream()
  • void close()

ServerSocket

  • ServerSocket(int port)
  • Socket accept() – blocks until connection is made
  • void close()

Knock Knock Example

  • Adapted from sockets tutorial on Oracle’s site.
  • KnockKnockServer is running on particular machine and port.
  • KnockKnockClient programs connect to server and are told a joke.

Knock Knock: Message Sequence

The server drives the conversation – the client just responds:

Server                               Client
  | ----  "Knock! Knock!"  ----------> |
  | <---  "Who's there?"  ------------ |
  | ----  "Turnip"  -----------------> |
  | <---  "Turnip who?"  ------------- |
  | ----  "Turnip the heat, ..."  ---> |
  | <---  "y"  ----------------------- |
  | ----  "Knock! Knock!"  ----------> |
  |              ...                   |
  | ----  "n"  ----------------------- |
  | ----  "Bye."  -------------------> |

KnockKnockClient: main

public static void main(String[] args) throws IOException {

  String hostName = args[0];
  int portNumber = Integer.parseInt(args[1]);

  try (Socket socket = new Socket(hostName, portNumber);
       PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
       BufferedReader in =
         new BufferedReader(new InputStreamReader(socket.getInputStream()));
      ) {
    BufferedReader stdIn =
      new BufferedReader(new InputStreamReader(System.in));
    String fromServer = in.readLine();
    while(fromServer != null) {
      System.out.println("Server: " + fromServer);
      if(fromServer.equals("Bye.")) { break; }
      String fromUser = stdIn.readLine();
      if(fromUser != null) {
        System.out.println("Client: " + fromUser);
        out.println(fromUser);
      }
      fromServer = in.readLine();
    }
  }
}

KnockKnockClient: main

public static void main(String[] args) throws IOException {

  String hostName = args[0];
  int portNumber = Integer.parseInt(args[1]);

  // try-with-resources: socket and streams are closed automatically
  try (Socket socket = new Socket(hostName, portNumber);
       PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
       BufferedReader in =
         new BufferedReader(new InputStreamReader(socket.getInputStream()));
      ) {
    // Not inside try since it would close System.in
    BufferedReader stdIn =
      new BufferedReader(new InputStreamReader(System.in));
    // server speaks first, client reads before doing anything
    String fromServer = in.readLine();
    while(fromServer != null) {
      System.out.println("Server: " + fromServer);
      if(fromServer.equals("Bye.")) { break; }
      String fromUser = stdIn.readLine();
      if(fromUser != null) {
        System.out.println("Client: " + fromUser);
        out.println(fromUser);  // forward user input to server
      }
      fromServer = in.readLine();
    }
  }
}

Thread-per-Client Pattern

The server must handle multiple clients simultaneously

  • A single-threaded server processes one client at a time
    • while one client hears a joke, all others wait
  • Solution: spawn a new thread for each accepted connection
    • each thread handles one client independently
    • this is the concurrency design pattern from the previous lectures

KnockKnockServer: main

public static void main(String[] args) throws IOException {

  int portNumber = Integer.parseInt(args[0]);
  System.out.println("Listening on port " + portNumber + " ...");
  try (ServerSocket serverSocket = new ServerSocket(portNumber)) {
    while (true) {
      Socket clientSocket = serverSocket.accept();
      KnockKnock kk = new KnockKnock(clientSocket);
      Thread t = new Thread(kk);
      t.start();
    }
  }
}

KnockKnockServer: main

public static void main(String[] args) throws IOException {

  int portNumber = Integer.parseInt(args[0]);
  System.out.println("Listening on port " + portNumber + " ...");
  // bind to the port, no connection yet, just listening
  try (ServerSocket serverSocket = new ServerSocket(portNumber)) {
    // Listen for new clients forever
    while (true) {
      // accept() blocks here until a client connects, then returns a Socket
      Socket clientSocket = serverSocket.accept();
      // hand the socket to a new thread, main loop is free immediately
      KnockKnock kk = new KnockKnock(clientSocket);
      Thread t = new Thread(kk);
      t.start();
    }
  }
}

KnockKnockServer: constants

private static String BYE = "Bye.";

private enum State {
  WAITING,
  SENT_KNOCK_KNOCK,
  SENT_CLUE,
  ANOTHER
}

private static String[] clues =
  { "Turnip",
    "Little Old Lady",
    "Atch", "Who", "Who" };
private static 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?" };

KnockKnock: init

public static class KnockKnock implements Runnable {
  private final Socket clientSocket;
  private PrintWriter out;
  private BufferedReader in;

  private State state = State.WAITING;
  private int currentJoke = 0;

  public KnockKnock(Socket clientSocket)
    throws IOException {
    this.clientSocket = clientSocket;
    out = new PrintWriter(clientSocket.getOutputStream(), true);
    in = new BufferedReader(
          new InputStreamReader(
            clientSocket.getInputStream()));
  }
}

KnockKnock: run

public void run() {
  String inputLine = null;
  String outputLine;

  do {
    outputLine = processInput(inputLine);
    out.println(outputLine);
    if(outputLine.equals(BYE)) {
      break;
    }
    try {
      inputLine = in.readLine();
    } catch (IOException ex) {
      inputLine = null;
    }
  } while(inputLine != null);
}

KnockKnock: run

public void run() {
  String inputLine = null;                // server speaks first
  String outputLine;

  do {
    outputLine = processInput(inputLine); // state machine produces response
    out.println(outputLine);              // send response to client
    if(outputLine.equals(BYE)) {
      break;                              // conversation over, exit loop
    }
    try {
      inputLine = in.readLine();          // wait for client's reply
    } catch (IOException ex) {
      inputLine = null;
    }
  } while(inputLine != null);             // null means client disconnected
}

State-Driven Logic

processInput implements a finite state machine

  • Each case corresponds to a state in the State enum
  • On valid input: send the next message and advance to the next state
  • On invalid input: send an error and stay in the same state (or retry)

The state machine ensures the conversation follows the knock knock protocol

KnockKnock: processInput

private String processInput(String input) {
  String output = null;
  switch (state) {
    case WAITING:
      output = "Knock! Knock!";
      state = State.SENT_KNOCK_KNOCK;
      break;
    case SENT_KNOCK_KNOCK:
      if(input.equalsIgnoreCase("Who's there?")) {
        output = clues[currentJoke];
        state = State.SENT_CLUE;
      } else {
        output =
        "You're supposed to say \"Who's there?\"! " +
        "Try again. Knock! Knock!";
      }
      break;
    // ...
  }
  return output;
}

KnockKnock: processInput

private String processInput(String input) {
  String output = null;
  switch (state) {
    case WAITING: // initial state: start the joke
      output = "Knock! Knock!";
      state = State.SENT_KNOCK_KNOCK;
      break;
    case SENT_KNOCK_KNOCK: // waiting for "Who's there?"
      if(input.equalsIgnoreCase("Who's there?")) {
        output = clues[currentJoke];
        state = State.SENT_CLUE;
      } else {
        output =
        "You're supposed to say \"Who's there?\"! " +
        "Try again. Knock! Knock!";
      }
      break;
    case SENT_CLUE: // waiting for "clue who?"
      if(input.equalsIgnoreCase(clues[currentJoke] + " who?")) {
        output = answers[currentJoke] +
          " Want another? (y/n)";
        state = State.ANOTHER;
      } else {
        output = "You're supposed to say \"" +
          clues[currentJoke] + " who?\"! " +
          "Try again. Knock! Knock!";
        state = State.SENT_KNOCK_KNOCK;
      }
      break;
    case ANOTHER: // waiting for "y" or "n"
      if(input.equalsIgnoreCase("y")) {
        output = "Knock! Knock!";
        currentJoke = (currentJoke + 1) % clues.length;
        state = State.SENT_KNOCK_KNOCK;
      } else {
        output = BYE;
        state = State.WAITING;
      }
      break;
  }
  return output;
}

Knock Knock Protocol