teaching machines

CS1: Lecture 36 – Chatting with Sockets and Threads

December 6, 2019 by . Filed under cs1, fall 2019, lectures.

Dear students,

At the end of the semester, we have an opportunity to explore some new, crazy ideas. Next week we’ll have a look at graphical user interfaces. Today we’ll look at sockets and threads as we make a chat client.

Sockets

Computers got especially interesting when they gained the ability to talk to each other. They’ve had that ability long enough now that the tools needed for communication are baked into our programming languages. Let’s have a look at what Java provides.

Many programs that communicate are actually two programs: a central server and a local client. The provider of the server is usually a company that acts as an intermediary between you and other users, or you and its data, or you and your own data as the case may be. You connect to this server via a client application that runs on your local machine. Communication often travels in both directions—the client speaks and listens to the server, and the server speaks and listens to zero or more clients.

The computer running the server may have many servers running at one time. For example, it may be serving out web pages, a shared Minecraft world, email, Git repositories, amongst other things. So that a computer may listen to many servers, it has many “ears.” These ears are called ports. Web pages are often delivered across ports 80 or 443 and Minecraft across port 25565.

First Draft of Chat Server

Let’s create a server that listens for one client and sending lines of text. We’ll need to create a ServerSocket and then wait for a client connect by calling the socket’s accept method. Once we have a client’s Socket, we can send text via its output stream or receive text via its input stream.

Our first draft of a server might look something like this:

public class ChatServer {
  private ServerSocket serverSocket;

  public ChatServer() throws IOException {
    serverSocket = new ServerSocket(10001);
  }

  public void listen() throws IOException {
    Socket clientSocket = serverSocket.accept();
    Scanner in = new Scanner(clientSocket.getInputStream());
    while (in.hasNextLine()) {
      String line = in.nextLine();
      System.out.println(line);
    }
    in.close();
    clientSocket.close();
  }

  public static void main(String[] args) throws IOException {
    new ChatServer().listen();
  }
}

First Draft of a Client

The client runs on the user’s computer, asks for a line of input, and then sends it to the server. We create a Socket, for which we must specify both the port and the IP address of the server to which we wish to connect.

Our first draft of a client might look something like this:

public class ChatClient {
  public static void main(String[] args) throws IOException, InterruptedException {
    Socket socket = new Socket("localhost", 10001);
    PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
    Scanner in = new Scanner(System.in);

    while (in.hasNextLine()) {
      String line = in.nextLine();
      out.println(line);
    }
  }
}

For the IP address, I am using the magic text localhost, which is shorthand for the IP address of the local computer. After we are done testing, we’ll run the server on a different machine and use its IP address.

Receive Thread

Currently the client can only send messages to the server. To be a real chat program, we want the server to send any received messages back to all the clients. As a first step, let’s tweak the server’s listen method to write the message it receives from the client back to the client:

public void listen() throws IOException {
  Socket clientSocket = serverSocket.accept();
  Scanner in = new Scanner(clientSocket.getInputStream());
  PrintWriter out = new PrintWriter(clientSocket.getOutputStream());

  while (in.hasNextLine()) {
    String line = in.nextLine();
    out.println(line);
  }

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

When we run this, we find that the client never receives the echoed message. That’s because the client is stuck in a loop waiting on user input. It’s not also listening for input on the socket. It looks like we need the client to do two things at once: listen for user input and listen for socket input. To support simultaneous execution, we need two threads of execution.

So far, all our programs have had exactly one thread of execution, which starts in main and lasts until the end of main or until our program crashes. We can create another thread by creating an object that derives or extends the builtin class Thread and giving it a special method named run, like so:

class ReceiveThread extends Thread {
  private Scanner in;

  public ReceiveThread(InputStream in) {
    this.in = new Scanner(in);
  }

  public void run() {
    while (in.hasNextLine()) {
      System.out.println(in.nextLine());
    }
    System.exit(0);
  }
}

Then we tweak the client’s main to spawn this thread before it gets locked up in user input:

ReceiveThread listenThread = new ReceiveThread(socket.getInputStream());
listenThread.start();

// user input loop

listenThread.join();

Now we can both read from System.in and the socket at the same time.

Server’s View of Client

Currently the server lets one client connect, listens to it for messages, and then echoes that message back to the sender. Really we want to let many clients to connect, listen to all of them for messages, and then broadcast their messages to all the other clients. To support this, we are going to need to rework the server.

As a first step, let’s create an abstraction that manages listening to a single client. We move our code from listen out to a separate class:

class Client extends Thread {
  private Socket socket;
  private ChatServer server;
  private PrintWriter out;

  public Client(Socket socket, ChatServer server) throws IOException {
    this.socket = socket;
    this.server = server;
    this.out = new PrintWriter(socket.getOutputStream());
  }

  public void run() {
    try {
      Scanner in = new Scanner(socket.getInputStream());
      while (in.hasNextLine()) {
        String line = in.nextLine();
        out.println(line);
      }
      in.close();
      out.close();
      socket.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

In main, we replace the code that handles a single client with code that spawns a new thread for listening to each client that connects:

while (true) {
  Socket socket = server.accept();
  Client listener = new Client(socket, this);
  listener.start();
}

Multiple Clients

With a thread per client, we allow multiple clients to connect. The server should keep a record of all these so that when a message comes in, it can broadcast that message to them all. Let’s keep a list of clients around:

// ...
private ArrayList<Client> clients;

public ChatServer() throws IOException {
  // ...
  clients = new ArrayList<>();
}

public void listen() throws IOException {
  while (true) {
    Socket socket = server.accept();
    Client listener = new Client(socket, this);
    client.start();
    clients.add(new Client(socket, this));
  }
}

We also add a method for broadcasting a message to all clients but the sender:

public void broadcast(Client sender, String line) {
  for (Client client : clients) {
    if (client != sender) {
      client.send(line);
    }
  }
}

Then in Client we add a send method and tweak run to broadcast a received message:

public void send(String line) {
  out.println(line);
}

public void run() {
  try {
    Scanner in = new Scanner(socket.getInputStream());
    while (in.hasNextLine()) {
      String line = in.nextLine();
      server.broadcast(this, line);
    }
    out.close();
    in.close();
    socket.close();
  } catch (IOException e) {
    e.printStackTrace();
  }
}

Final Draft of Client

This should do it. I think… But we should test it live. Here’s the final draft of the client, which you can copy, paste, and run. It connects to my server on port 10001.

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.InputMismatchException;
import java.util.Scanner;

public class ChatClient {
  public static void main(String[] args) throws IOException, InterruptedException {
    Socket socket = new Socket("138.68.15.70", 10001);
    PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
    Scanner in = new Scanner(System.in);

    ReceiveThread listenThread = new ReceiveThread(socket.getInputStream());
    listenThread.start();

    while (in.hasNextLine()) {
      String line = in.nextLine();
      out.println(line);
    }

    listenThread.join();
  }
}

class ReceiveThread extends Thread {
  private Scanner in;

  public ReceiveThread(InputStream in) {
    this.in = new Scanner(in);
  }

  public void run() {
    while (in.hasNextLine()) {
      System.out.println(in.nextLine());
    }
    System.exit(0);
  }
}

Issues

There are a couple of issues we haven’t dealt with. The ones I know of include:

We may try to address these, but you will spend your whole life dealing with concurrency issues, so we’re not in any rush.

TODO

See you next class!

Sincerely,

P.S. It’s time for a haiku!

Mom has Dad’s letters
Just like we had each other’s
Until Hotmail died

P.P.S. Here’s the code we wrote together…

ChatClient.java

package lecture1206.cs145;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class ChatClient {
  public static void main(String[] args) throws IOException, InterruptedException {
    Socket socket = new Socket("localhost", 10001);
    PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
    Scanner in = new Scanner(System.in);

    ReceiveThread serverListener = new ReceiveThread(socket.getInputStream());
    serverListener.start();

    while (in.hasNextLine()) {
      String line = in.nextLine();
      out.println(line);
    }

    serverListener.join();
  }
}

class ReceiveThread extends Thread {
  private Scanner in;

  public ReceiveThread(InputStream in) {
    this.in = new Scanner(in);
  }

  public void run() {
    while (in.hasNextLine()) {
      System.out.println(in.nextLine());
    }
    System.exit(0);
  }
}

ChatServer.java

package lecture1206.cs145;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class ChatServer {
  private ServerSocket serverSocket;

  public ChatServer() throws IOException {
    this.serverSocket = new ServerSocket(10001);
  }

  public void listen() throws IOException {
    Socket clientSocket = serverSocket.accept();
    PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);

    Scanner in = new Scanner(clientSocket.getInputStream());
    while (in.hasNextLine()) {
      String line = in.nextLine();
      out.println(line);
    }

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

  public static void main(String[] args) throws IOException {
    new ChatServer().listen();
  }
}