单元目标 • 概述 • 一个简单的顺序服务器 • 一个简单的线程服务器 • 一个网络井字游戏 • 一个多用户聊天应用程序 • 总结
单位目标
熟练使用 Java 在套接字级别编写客户端-服务器应用程序。
概述
我们将研究四个完全用 Java 从头开始编写的网络应用程序。这些应用程序中的每一个都使用我们之前讨论过的客户端-服务器范例。我们将在这里专门使用 TCP。回想一下,从 49152 到 65535 的端口可以用于您想要的任何东西,所以我们将使用这些。
Java 对套接字 API 的抽象是使用自动侦听的ServerSocket对象,然后在接受时创建不同的套接字。Java 套接字内置了输入流和输出流,这使得编程变得相当愉快。
四种应用程序按复杂性递增的顺序呈现:
- 一个简单的数据服务器和客户端,说明了简单的单向通信。服务器只向客户端发送数据。
- 大写 server 和 client,说明双向通信和服务器端线程,以更有效地同时处理多个连接。
- 一个两人玩的 tic tac toe 游戏,说明需要跟踪游戏状态并通知每个客户端的服务器,以便他们可以更新自己的显示。
- 多用户聊天应用程序,其中服务器必须向其所有客户端广播消息。
这些应用程序的通信不安全。
这些应用程序甚至都没有尝试保护通信。所有数据在主机之间完全明文发送。此时的目标是说明最基本的应用程序以及它们如何使用传输级服务。在现实生活中,使用安全套接字层。
一个简单的顺序服务器
这可能是最简单的服务器。它侦听端口 59090。当客户端连接时,服务器将当前日期时间发送到客户端。连接套接字是在 try-with-resources 块中创建的,因此它会在该块的末尾自动关闭。只有在提供日期时间并关闭连接后,服务器才会返回等待下一个客户端。
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());
}
}
}
}
}
讨论:
- 此代码仅用于说明;你不太可能写出这么简单的东西。
- 这不能很好地处理多个客户端;每个客户端都必须等到前一个客户端被完全服务后才能被接受。
- 与几乎所有套接字程序一样,服务器套接字 只是侦听,而不同的“普通”套接字与客户端通信。
- 该
ServerSocket.accept()
呼叫是BLOCKING CALL。 - 套接字通信始终以字节为单位;因此套接字带有输入流和输出流。但是通过用 a 包装套接字的输出流
PrintWriter
,我们可以指定要写入的字符串,Java 会自动将其转换(解码)为字节。 - 通过套接字的通信总是被缓冲的。这意味着在缓冲区填满或您明确刷新缓冲区之前,不会发送或接收任何内容。的第二个参数
PrintWriter
在这种情况下true
告诉 Java 在每个println
. - 我们在 try-with-resources 块中定义了所有套接字,因此它们将在其块的末尾自动关闭。不需要显式
close
调用。 - 将日期时间发送给客户端后,try-block 结束并关闭通信套接字,因此在这种情况下,关闭连接是由服务器发起的。
运行服务器:
$ javac DateServer.java && java DateServer
The date server is running...
要查看它正在运行(您将需要一个不同的终端窗口):
$ netstat -an | grep 59090
tcp46 0 0 *.59090 *.* LISTEN
使用以下命令测试服务器nc
:
$ nc localhost 59090
Sat Feb 16 18:03:34 PST 2019
哇nc
是惊人的!不过,让我们看看如何用 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());
}
}
讨论:
- 在客户端,
Socket
构造函数获取服务器上的 IP 地址和端口。如果连接请求被接受,我们将获得一个套接字对象进行通信。 - 我们的应用程序非常简单,客户端从不写入服务器,它只读取。因为我们正在与文本通信,所以最简单的做法是将套接字的输入流包装在一个
Scanner
. 这些功能强大且方便。在我们的例子中,我们从服务器读取一行文本Scanner.nextLine
。
测试客户端:
$ javac DateClient.java && java DateClient 127.0.0.1
Server response: Sat Feb 16 18:02:35 PST 2019
一个简单的线程服务器
前面的例子非常简单:它没有从客户端读取任何数据,更糟糕的是,它一次只服务一个客户端。
下一个服务器从客户端接收文本行并将大写的行发回。它一次有效地处理多个客户端:当一个客户端连接时,服务器产生一个线程,专用于该客户端,以读取、大写和回复。服务器可以同时侦听和服务其他客户端,因此我们具有真正的并发性。
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);
}
}
}
}
讨论:
- 服务器套接字在接受连接后,只会触发一个线程。
- 在 Java 中,永远不要直接创建线程;相反,使用线程池并使用执行程序服务来管理线程。
- 限制线程池大小可以保护我们免于被数以百万计的客户端淹没。
- 在线程上运行的东西称为任务;他们实现了
Runnable
接口;他们以他们的run
方法做他们的工作。 - 注意不要在任务的构造函数中做太多的工作!构造函数在主线程上运行。将所有工作(除了捕获构造函数参数)放在
run
方法中。 - 该
run
方法有一个循环,它不断从套接字读取行,将它们大写,然后将它们发送出去。注意套接字流在 aScanner
和 a 中的包装,PrintWriter
以便我们可以使用字符串。 - 该
finally
块关闭套接字。我们不能在这里使用 try-with-resources 块,因为套接字是在主线程上创建的。 - 围绕套接字关闭调用的烦人的 try-catch 必须存在,因为我们无法添加
throws IOException
到run
方法签名中(因为我们是从Runnable
接口实现它的。
在编写客户端之前,让我们用nc
. 我们的服务器读取直到标准输入用完,所以当你完成输入行时,按 Ctrl+D(干净退出)或 Ctrl+C(中止):
$ nc 127.0.0.1 59898
yeet
YEET
Seems t'be workin'
SEEMS T'BE WORKIN'
Привет, мир
ПРИВЕТ, МИР
现在是一个非常简单的命令行客户端:
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());
}
}
}
}
此客户端重复从标准输入读取行,将它们发送到服务器,并写入服务器响应。它可以交互使用:
$ javac CapitalizeClient.java && java CapitalizeClient localhost
Enter lines of text then Ctrl+D or Ctrl+C to quit
hello
HELLO
bye
BYE
或者你可以在一个文件中使用管道!
$ 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
课堂作业
分成两人一组。一个学生将在一个终端窗口中启动一个服务器,在另一个终端窗口中启动一个客户端,然后启动每个窗口。另一个学生将创建两个终端窗口,每个窗口运行一个客户端。在三个客户端中的任何一个向服务器发送任何数据之前,运行netstat
以确保您看到侦听服务器和所有客户端连接。(在 Mac 上,netstat -an | grep tcp | grep 59898
只看到好东西很有用。)将 netstat 输出与服务器和客户端回显的日志消息相关联。发送数据时,继续运行 netstat。观察连接从 ESTABLISHED 到 TIME_WAIT,然后消失。记下发生的一切;当每个人都完成后,我们将作为一个小组讨论。
网络井字游戏
这是多个两人游戏的服务器。它监听两个客户端的连接,并为每个客户端生成一个线程:第一个是玩家 X,第二个是玩家 O。客户端和服务器来回发送简单的字符串消息;消息对应于 Tic Tac Toe 协议,我为此示例编写了该协议。
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());
}
}
}
}
如今,像这样的游戏将在 Web 浏览器中与客户端一起玩,而服务器将是一个 Web 服务器(可能使用 WebSockets 库)。但是今天,我们正在学习直接使用套接字、自定义端口和自定义协议进行编程,因此我们坚持使用 Java 来处理我们的自定义客户端。这个程序的第一个版本是在 2002 年左右编写的,所以它使用...等等...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();
}
}
练习:在nc
. 这有多棒?你觉得老派吗?
多用户聊天应用程序
这是一个聊天服务器。服务器必须向所有参与聊天的客户端广播最近传入的消息。这是通过让服务器在字典中收集所有客户端套接字,然后向每个套接字发送新消息来完成的。
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) {
}
}
}
}
}
这是 2002 年使用 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();
}
}
练习: 增强协议以将系统消息(例如人们加入和离开)与聊天消息区分开来,然后增强客户端使其使用颜色和字体来突出显示不同类型的消息。