A Trivial Sequential Server
This is perhaps the simplest possible server. It listens on port 59090. When a client connects, the server sends the current datetime to the client. The connection socket is created in a try-with-resources block so it is automatically closed at the end of the block. Only after serving the datetime and closing the connection will the server go back to waiting for the next client.
DateServer.java
import java.io.IOException; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Date; /** * A simple TCP server. When a client connects, it sends the client the current * datetime, then closes the connection. This is arguably the simplest server * you can write. Beware though that a client has to be completely served its * date before the server will be able to handle another client. */ public class DateServer { public static void main(String[] args) throws IOException { try (var listener = new ServerSocket(59090)) { System.out.println("The date server is running..."); while (true) { try (var socket = listener.accept()) { var out = new PrintWriter(socket.getOutputStream(), true); out.println(new Date().toString()); } } } } }
Discussion:
- This code is just for illustration; you are unlikely to ever write anything so simple.
- This does not handle multiple clients well; each client must wait until the previous client is completely served before it even gets accepted.
- As in virtually all socket programs, a server socket just listens, and a different, “plain” socket communicates with the client.
- The
ServerSocket.accept()
call is a BLOCKING CALL. - Socket communication is always with bytes; therefore sockets come with input streams and output streams. But by wrapping the socket’s output stream with a
PrintWriter
, we can specify strings to write, which Java will automatically convert (decode) to bytes. - Communication through sockets is always buffered. This means nothing is sent or received until the buffers fill up, or you explicitly flush the buffer. The second argument to the
PrintWriter
, in this casetrue
tells Java to flush automatically after everyprintln
. - We defined all sockets in a try-with-resources block so they will automatically close at the end of their block. No explicit
close
call is required. - After sending the datetime to the client, the try-block ends and the communication socket is closed, so in this case, closing the connection is initiated by the server.
Run the server:
$ javac DateServer.java && java DateServer The date server is running...
To see that is running (you will need a different terminal window):
$ netstat -an | grep 59090 tcp46 0 0 *.59090 *.* LISTEN
Test the server with
nc
:$ nc localhost 59090 Sat Feb 16 18:03:34 PST 2019
Woah
nc
is amazing! Still, let’s see how to write our own client in Java:
DateClient.java
import java.util.Scanner; import java.net.Socket; import java.io.IOException; /** * A command line client for the date server. Requires the IP address of * the server as the sole argument. Exits after printing the response. */ public class DateClient { public static void main(String[] args) throws IOException { if (args.length != 1) { System.err.println("Pass the server IP as the sole command line argument"); return; } var socket = new Socket(args[0], 59090); var in = new Scanner(socket.getInputStream()); System.out.println("Server response: " + in.nextLine()); } }
Discussion:
- On the client side, the
Socket
constructor takes the IP address and port on the server. If the connect request is accepted, we get a socket object to communicate. - Our application is so simple that the client never writes to the server, it only reads. Because we are communicating with text, the simplest thing to do is to wrap the socket’s input stream in a
Scanner
. These are powerful and convenient. In our case we read a line of text from the server withScanner.nextLine
.
Test the client:
$ javac DateClient.java && java DateClient 127.0.0.1 Server response: Sat Feb 16 18:02:35 PST 2019
A Simple Threaded Server
The previous example was pretty trivial: it did not read any data from the client, and worse, it served only one client at a time.
This next server receives lines of text from a client and sends back the lines uppercased. It efficiently handles multiple clients at once: When a client connects, the server spawns a thread, dedicated to just that client, to read, uppercase, and reply. The server can listen for and serve other clients at the same time, so we have true concurrency.
CapitalizeServer.java
import java.io.IOException; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; import java.util.concurrent.Executors; /** * A server program which accepts requests from clients to capitalize strings. When * a client connects, a new thread is started to handle it. Receiving client data, * capitalizing it, and sending the response back is all done on the thread, allowing * much greater throughput because more clients can be handled concurrently. */ public class CapitalizeServer { /** * Runs the server. When a client connects, the server spawns a new thread to do * the servicing and immediately returns to listening. The application limits the * number of threads via a thread pool (otherwise millions of clients could cause * the server to run out of resources by allocating too many threads). */ public static void main(String[] args) throws Exception { try (var listener = new ServerSocket(59898)) { System.out.println("The capitalization server is running..."); var pool = Executors.newFixedThreadPool(20); while (true) { pool.execute(new Capitalizer(listener.accept())); } } } private static class Capitalizer implements Runnable { private Socket socket; Capitalizer(Socket socket) { this.socket = socket; } @Override public void run() { System.out.println("Connected: " + socket); try { var in = new Scanner(socket.getInputStream()); var out = new PrintWriter(socket.getOutputStream(), true); while (in.hasNextLine()) { out.println(in.nextLine().toUpperCase()); } } catch (Exception e) { System.out.println("Error:" + socket); } finally { try { socket.close(); } catch (IOException e) {} System.out.println("Closed: " + socket); } } } }
Discussion:
- The server socket, upon accepting a connection, does nothing more than fire off a thread.
- In Java, you should never create threads directly; instead, employ a thread pool and use an executor service to manage the threads.
- Limiting the thread pool size protects us against being swamped with millions of clients.
- The things that are run on threads are called tasks; they implement the
Runnable
interface; they do their work in theirrun
method. - Take care to never do too much work in the task’s constructor! The constructor is run on the main thread. Put all the work (other than capturing constructor arguments) in the
run
method. - The
run
method has a loop which keeps reading lines from the socket, uppercasing them, then sending them out. Note the wrapping of the socket streams in aScanner
and aPrintWriter
so that we can work with strings. - The
finally
block closes the socket. We could not use a try-with-resources block here because the socket was created on the main thread. - The annoying try-catch around the socket close call has to be there because we cannot add
throws IOException
to therun
method signature (because we are implementing it from theRunnable
interface.
Before writing a client, let’s test with
nc
. Our server reads until standard input is exhausted, so when you are done typing in lines, hit Ctrl+D (clean exit) or Ctrl+C (abort):$ nc 127.0.0.1 59898 yeet YEET Seems t'be workin' SEEMS T'BE WORKIN' Привет, мир ПРИВЕТ, МИÐ
Now for a pretty simple command line client:
CapitalizeClient.java
import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; public class CapitalizeClient { public static void main(String[] args) throws Exception { if (args.length != 1) { System.err.println("Pass the server IP as the sole command line argument"); return; } try (var socket = new Socket(args[0], 59898)) { System.out.println("Enter lines of text then Ctrl+D or Ctrl+C to quit"); var scanner = new Scanner(System.in); var in = new Scanner(socket.getInputStream()); var out = new PrintWriter(socket.getOutputStream(), true); while (scanner.hasNextLine()) { out.println(scanner.nextLine()); System.out.println(in.nextLine()); } } } }
This client repeatedly reads lines from standard input, sends them to the server, and writes server responses. It can be used interactively:
$ javac CapitalizeClient.java && java CapitalizeClient localhost Enter lines of text then Ctrl+D or Ctrl+C to quit hello HELLO bye BYE
Or you can pipe in a file!
$ python3 -c 'for a in "dog rat cat".split(): print(a)' > animals $ javac CapitalizeClient.java && java CapitalizeClient localhost < animals Enter lines of text then Ctrl+D or Ctrl+C to quit DOG RAT CAT
Classwork
Get into groups of two. One student will start a server in one terminal window and a client in another, and start each. The other student will create two terminal windows each running a client. Before any of the three clients send any data to the server, runnetstat
to make sure you see the listening server and all of the client connections. (On a Mac,netstat -an | grep tcp | grep 59898
is useful to see just the good stuff.) Correlate the netstat output with the log messages echoed by the server and client. As data is sent, keep running netstat. Watch the connections go from ESTABLISHED to TIME_WAIT, and then disappear. Make notes of everything that happens; we’ll discuss as a group when everyone’s done.
A Network Tic-Tac-Toe Game
Here is the server for multiple two-player games. It listens for two clients to connect, and spawns a thread for each: the first is Player X and the second is Player O. The client and server send simple string messages back and forth to each other; messages correspond to the Tic Tac Toe protocol, which I made up for this example.
TicTacToeServer.java
import java.io.IOException; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Arrays; import java.util.Scanner; import java.util.concurrent.Executors; /** * A server for a multi-player tic tac toe game. Loosely based on an example in * Deitel and Deitel’s “Java How to Program” book. For this project I created a * new application-level protocol called TTTP (for Tic Tac Toe Protocol), which * is entirely plain text. The messages of TTTP are: * * Client -> Server * MOVE <n> * QUIT * * Server -> Client * WELCOME <char> * VALID_MOVE * OTHER_PLAYER_MOVED <n> * OTHER_PLAYER_LEFT * VICTORY * DEFEAT * TIE * MESSAGE <text> */ public class TicTacToeServer { public static void main(String[] args) throws Exception { try (var listener = new ServerSocket(58901)) { System.out.println("Tic Tac Toe Server is Running..."); var pool = Executors.newFixedThreadPool(200); while (true) { Game game = new Game(); pool.execute(game.new Player(listener.accept(), 'X')); pool.execute(game.new Player(listener.accept(), 'O')); } } } } class Game { // Board cells numbered 0-8, top to bottom, left to right; null if empty private Player[] board = new Player[9]; Player currentPlayer; public boolean hasWinner() { return (board[0] != null && board[0] == board[1] && board[0] == board[2]) || (board[3] != null && board[3] == board[4] && board[3] == board[5]) || (board[6] != null && board[6] == board[7] && board[6] == board[8]) || (board[0] != null && board[0] == board[3] && board[0] == board[6]) || (board[1] != null && board[1] == board[4] && board[1] == board[7]) || (board[2] != null && board[2] == board[5] && board[2] == board[8]) || (board[0] != null && board[0] == board[4] && board[0] == board[8]) || (board[2] != null && board[2] == board[4] && board[2] == board[6] ); } public boolean boardFilledUp() { return Arrays.stream(board).allMatch(p -> p != null); } public synchronized void move(int location, Player player) { if (player != currentPlayer) { throw new IllegalStateException("Not your turn"); } else if (player.opponent == null) { throw new IllegalStateException("You don't have an opponent yet"); } else if (board[location] != null) { throw new IllegalStateException("Cell already occupied"); } board[location] = currentPlayer; currentPlayer = currentPlayer.opponent; } /** * A Player is identified by a character mark which is either 'X' or 'O'. * For communication with the client the player has a socket and associated * Scanner and PrintWriter. */ class Player implements Runnable { char mark; Player opponent; Socket socket; Scanner input; PrintWriter output; public Player(Socket socket, char mark) { this.socket = socket; this.mark = mark; } @Override public void run() { try { setup(); processCommands(); } catch (Exception e) { e.printStackTrace(); } finally { if (opponent != null && opponent.output != null) { opponent.output.println("OTHER_PLAYER_LEFT"); } try {socket.close();} catch (IOException e) {} } } private void setup() throws IOException { input = new Scanner(socket.getInputStream()); output = new PrintWriter(socket.getOutputStream(), true); output.println("WELCOME " + mark); if (mark == 'X') { currentPlayer = this; output.println("MESSAGE Waiting for opponent to connect"); } else { opponent = currentPlayer; opponent.opponent = this; opponent.output.println("MESSAGE Your move"); } } private void processCommands() { while (input.hasNextLine()) { var command = input.nextLine(); if (command.startsWith("QUIT")) { return; } else if (command.startsWith("MOVE")) { processMoveCommand(Integer.parseInt(command.substring(5))); } } } private void processMoveCommand(int location) { try { move(location, this); output.println("VALID_MOVE"); opponent.output.println("OPPONENT_MOVED " + location); if (hasWinner()) { output.println("VICTORY"); opponent.output.println("DEFEAT"); } else if (boardFilledUp()) { output.println("TIE"); opponent.output.println("TIE"); } } catch (IllegalStateException e) { output.println("MESSAGE " + e.getMessage()); } } } }
These days, games like this would be played with clients in a web browser, and the server would be a web server (likely using a WebSockets library). But today, we’re learning about programming directly with sockets, on custom ports, with custom protocols, so we’re sticking with Java for our custom clients. The first version of this program was written in about 2002, so it uses...wait for it...Java Swing!
TicTacToeClient.java
import java.awt.Font; import java.awt.Color; import java.awt.GridLayout; import java.awt.GridBagLayout; import java.awt.BorderLayout; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Scanner; import java.io.PrintWriter; import java.net.Socket; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; /** * A client for a multi-player tic tac toe game. Loosely based on an example in * Deitel and Deitel’s “Java How to Program” book. For this project I created a * new application-level protocol called TTTP (for Tic Tac Toe Protocol), which * is entirely plain text. The messages of TTTP are: * * Client -> Server * MOVE <n> * QUIT * * Server -> Client * WELCOME <char> * VALID_MOVE * OTHER_PLAYER_MOVED <n> * OTHER_PLAYER_LEFT * VICTORY * DEFEAT * TIE * MESSAGE <text> */ public class TicTacToeClient { private JFrame frame = new JFrame("Tic Tac Toe"); private JLabel messageLabel = new JLabel("..."); private Square[] board = new Square[9]; private Square currentSquare; private Socket socket; private Scanner in; private PrintWriter out; public TicTacToeClient(String serverAddress) throws Exception { socket = new Socket(serverAddress, 58901); in = new Scanner(socket.getInputStream()); out = new PrintWriter(socket.getOutputStream(), true); messageLabel.setBackground(Color.lightGray); frame.getContentPane().add(messageLabel, BorderLayout.SOUTH); var boardPanel = new JPanel(); boardPanel.setBackground(Color.black); boardPanel.setLayout(new GridLayout(3, 3, 2, 2)); for (var i = 0; i < board.length; i++) { final int j = i; board[i] = new Square(); board[i].addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { currentSquare = board[j]; out.println("MOVE " + j); } }); boardPanel.add(board[i]); } frame.getContentPane().add(boardPanel, BorderLayout.CENTER); } /** * The main thread of the client will listen for messages from the server. * The first message will be a "WELCOME" message in which we receive our * mark. Then we go into a loop listening for any of the other messages, * and handling each message appropriately. The "VICTORY", "DEFEAT", "TIE", * and "OTHER_PLAYER_LEFT" messages will ask the user whether or not to * play another game. If the answer is no, the loop is exited and the server * is sent a "QUIT" message. */ public void play() throws Exception { try { var response = in.nextLine(); var mark = response.charAt(8); var opponentMark = mark == 'X' ? 'O' : 'X'; frame.setTitle("Tic Tac Toe: Player " + mark); while (in.hasNextLine()) { response = in.nextLine(); if (response.startsWith("VALID_MOVE")) { messageLabel.setText("Valid move, please wait"); currentSquare.setText(mark); currentSquare.repaint(); } else if (response.startsWith("OPPONENT_MOVED")) { var loc = Integer.parseInt(response.substring(15)); board[loc].setText(opponentMark); board[loc].repaint(); messageLabel.setText("Opponent moved, your turn"); } else if (response.startsWith("MESSAGE")) { messageLabel.setText(response.substring(8)); } else if (response.startsWith("VICTORY")) { JOptionPane.showMessageDialog(frame, "Winner Winner"); break; } else if (response.startsWith("DEFEAT")) { JOptionPane.showMessageDialog(frame, "Sorry you lost"); break; } else if (response.startsWith("TIE")) { JOptionPane.showMessageDialog(frame, "Tie"); break; } else if (response.startsWith("OTHER_PLAYER_LEFT")) { JOptionPane.showMessageDialog(frame, "Other player left"); break; } } out.println("QUIT"); } catch (Exception e) { e.printStackTrace(); } finally { socket.close(); frame.dispose(); } } static class Square extends JPanel { JLabel label = new JLabel(); public Square() { setBackground(Color.white); setLayout(new GridBagLayout()); label.setFont(new Font("Arial", Font.BOLD, 40)); add(label); } public void setText(char text) { label.setForeground(text == 'X' ? Color.BLUE : Color.RED); label.setText(text + ""); } } public static void main(String[] args) throws Exception { if (args.length != 1) { System.err.println("Pass the server IP as the sole command line argument"); return; } TicTacToeClient client = new TicTacToeClient(args[0]); client.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); client.frame.setSize(320, 320); client.frame.setVisible(true); client.frame.setResizable(false); client.play(); } }
Exercise: Play this game in
nc
. How awesome is this? Do you feel old school?A Multi-User Chat Application
Here is a chat server. The server must broadcast recently incoming messages to all the clients participating in a chat. This is done by having the server collect all of the client sockets in a dictionary, then sending new messages to each of them.
ChatServer.java
import java.io.IOException; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Set; import java.util.HashSet; import java.util.Scanner; import java.util.concurrent.Executors; /** * A multithreaded chat room server. When a client connects the server requests a screen * name by sending the client the text "SUBMITNAME", and keeps requesting a name until * a unique one is received. After a client submits a unique name, the server acknowledges * with "NAMEACCEPTED". Then all messages from that client will be broadcast to all other * clients that have submitted a unique screen name. The broadcast messages are prefixed * with "MESSAGE". * * This is just a teaching example so it can be enhanced in many ways, e.g., better * logging. Another is to accept a lot of fun commands, like Slack. */ public class ChatServer { // All client names, so we can check for duplicates upon registration. private static Set<String> names = new HashSet<>(); // The set of all the print writers for all the clients, used for broadcast. private static Set<PrintWriter> writers = new HashSet<>(); public static void main(String[] args) throws Exception { System.out.println("The chat server is running..."); var pool = Executors.newFixedThreadPool(500); try (var listener = new ServerSocket(59001)) { while (true) { pool.execute(new Handler(listener.accept())); } } } /** * The client handler task. */ private static class Handler implements Runnable { private String name; private Socket socket; private Scanner in; private PrintWriter out; /** * Constructs a handler thread, squirreling away the socket. All the interesting * work is done in the run method. Remember the constructor is called from the * server's main method, so this has to be as short as possible. */ public Handler(Socket socket) { this.socket = socket; } /** * Services this thread's client by repeatedly requesting a screen name until a * unique one has been submitted, then acknowledges the name and registers the * output stream for the client in a global set, then repeatedly gets inputs and * broadcasts them. */ public void run() { try { in = new Scanner(socket.getInputStream()); out = new PrintWriter(socket.getOutputStream(), true); // Keep requesting a name until we get a unique one. while (true) { out.println("SUBMITNAME"); name = in.nextLine(); if (name == null) { return; } synchronized (names) { if (!name.isBlank() && !names.contains(name)) { names.add(name); break; } } } // Now that a successful name has been chosen, add the socket's print writer // to the set of all writers so this client can receive broadcast messages. // But BEFORE THAT, let everyone else know that the new person has joined! out.println("NAMEACCEPTED " + name); for (PrintWriter writer : writers) { writer.println("MESSAGE " + name + " has joined"); } writers.add(out); // Accept messages from this client and broadcast them. while (true) { String input = in.nextLine(); if (input.toLowerCase().startsWith("/quit")) { return; } for (PrintWriter writer : writers) { writer.println("MESSAGE " + name + ": " + input); } } } catch (Exception e) { System.out.println(e); } finally { if (out != null) { writers.remove(out); } if (name != null) { System.out.println(name + " is leaving"); names.remove(name); for (PrintWriter writer : writers) { writer.println("MESSAGE " + name + " has left"); } } try { socket.close(); } catch (IOException e) {} } } } }
Here’s an old client cobbled together in 2002, using Swing.
ChatClient.java
import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.IOException; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; import java.awt.BorderLayout; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JTextField; /** * A simple Swing-based client for the chat server. Graphically it is a frame with a text * field for entering messages and a textarea to see the whole dialog. * * The client follows the following Chat Protocol. When the server sends "SUBMITNAME" the * client replies with the desired screen name. The server will keep sending "SUBMITNAME" * requests as long as the client submits screen names that are already in use. When the * server sends a line beginning with "NAMEACCEPTED" the client is now allowed to start * sending the server arbitrary strings to be broadcast to all chatters connected to the * server. When the server sends a line beginning with "MESSAGE" then all characters * following this string should be displayed in its message area. */ public class ChatClient { String serverAddress; Scanner in; PrintWriter out; JFrame frame = new JFrame("Chatter"); JTextField textField = new JTextField(50); JTextArea messageArea = new JTextArea(16, 50); /** * Constructs the client by laying out the GUI and registering a listener with the * textfield so that pressing Return in the listener sends the textfield contents * to the server. Note however that the textfield is initially NOT editable, and * only becomes editable AFTER the client receives the NAMEACCEPTED message from * the server. */ public ChatClient(String serverAddress) { this.serverAddress = serverAddress; textField.setEditable(false); messageArea.setEditable(false); frame.getContentPane().add(textField, BorderLayout.SOUTH); frame.getContentPane().add(new JScrollPane(messageArea), BorderLayout.CENTER); frame.pack(); // Send on enter then clear to prepare for next message textField.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { out.println(textField.getText()); textField.setText(""); } }); } private String getName() { return JOptionPane.showInputDialog( frame, "Choose a screen name:", "Screen name selection", JOptionPane.PLAIN_MESSAGE ); } private void run() throws IOException { try { var socket = new Socket(serverAddress, 59001); in = new Scanner(socket.getInputStream()); out = new PrintWriter(socket.getOutputStream(), true); while (in.hasNextLine()) { var line = in.nextLine(); if (line.startsWith("SUBMITNAME")) { out.println(getName()); } else if (line.startsWith("NAMEACCEPTED")) { this.frame.setTitle("Chatter - " + line.substring(13)); textField.setEditable(true); } else if (line.startsWith("MESSAGE")) { messageArea.append(line.substring(8) + "\n"); } } } finally { frame.setVisible(false); frame.dispose(); } } public static void main(String[] args) throws Exception { if (args.length != 1) { System.err.println("Pass the server IP as the sole command line argument"); return; } var client = new ChatClient(args[0]); client.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); client.frame.setVisible(true); client.run(); } }
Exercise: Enhance the protocol so that it distinguishes system messages (such as people joining and leaving), from chat messages, then enhance the client so that it uses color and fonts to highlight the different kinds of messages.
No comments:
Post a Comment