您将要构建的 Java Chat 应用程序是一个从命令行启动的控制台应用程序。服务器和客户端可以在同一网络(例如局域网 (LAN))中的不同计算机上运行。
可以有多个客户端连接到一个服务器,他们可以互相聊天,就像在聊天室里,每个人都可以看到其他用户的消息。为简单起见,两个用户之间没有私人聊天。
连接到服务器后,用户必须提供他或她的姓名才能进入聊天。服务器向新用户发送当前在线用户列表。
当新用户到达和用户离开时,每个用户都会收到通知。每条消息都以用户名作为前缀,以跟踪谁发送了消息。
最后,用户说“再见”以退出聊天。
该应用程序由两部分组成:服务器和客户端。每个部分都可以在不同的计算机上独立运行。
现在,让我们看看如何详细地编写这个 Java 聊天应用程序。
服务器由两个类实现:ChatServer 和UserThread。
该的ChatServer类启动服务器,侦听特定端口上。当新客户端连接时,会创建一个UserThread实例来为该客户端提供服务。由于每个连接都在单独的线程中处理,因此服务器能够同时处理多个客户端。
以下是ChatServer类的源代码:
package net.codejava.networking.chat.server;
import java.io.*;
import java.net.*;
import java.util.*;
/**
* This is the chat server program.
* Press Ctrl + C to terminate the program.
*
* @author www.codejava.net
*/
public class ChatServer {
private int port;
private Set<String> userNames = new HashSet<>();
private Set<UserThread> userThreads = new HashSet<>();
public ChatServer(int port) {
this.port = port;
}
public void execute() {
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("Chat Server is listening on port " + port);
while (true) {
Socket socket = serverSocket.accept();
System.out.println("New user connected");
UserThread newUser = new UserThread(socket, this);
userThreads.add(newUser);
newUser.start();
}
} catch (IOException ex) {
System.out.println("Error in the server: " + ex.getMessage());
ex.printStackTrace();
}
}
public static void main(String[] args) {
if (args.length < 1) {
System.out.println("Syntax: java ChatServer <port-number>");
System.exit(0);
}
int port = Integer.parseInt(args[0]);
ChatServer server = new ChatServer(port);
server.execute();
}
/**
* Delivers a message from one user to others (broadcasting)
*/
void broadcast(String message, UserThread excludeUser) {
for (UserThread aUser : userThreads) {
if (aUser != excludeUser) {
aUser.sendMessage(message);
}
}
}
/**
* Stores username of the newly connected client.
*/
void addUserName(String userName) {
userNames.add(userName);
}
/**
* When a client is disconneted, removes the associated username and UserThread
*/
void removeUser(String userName, UserThread aUser) {
boolean removed = userNames.remove(userName);
if (removed) {
userThreads.remove(aUser);
System.out.println("The user " + userName + " quitted");
}
}
Set<String> getUserNames() {
return this.userNames;
}
/**
* Returns true if there are other users connected (not count the currently connected user)
*/
boolean hasUsers() {
return !this.userNames.isEmpty();
}
}
如您所见,ChatServer类有两个Set集合来跟踪连接的客户端的名称和线程。使用 Set是因为它不允许重复并且元素的顺序无关紧要:
private Set<String> userNames = new HashSet<>();
private Set<UserThread> userThreads = new HashSet<>();
ChatServer类中的一个重要方法是broadcast(),它将消息从一个客户端传送到所有其他客户端:
void broadcast(String message, UserThread excludeUser) {
for (UserThread aUser : userThreads) {
if (aUser != excludeUser) {
aUser.sendMessage(message);
}
}
}
该UserThread类是负责读取从客户端发送的消息和广播消息发送给所有其他客户端。首先,它将在线用户列表发送给新用户。然后它读取用户名并通知其他用户有关新用户的信息。
以下代码属于UserThread类:
package net.codejava.networking.chat.server;
import java.io.*;
import java.net.*;
import java.util.*;
/**
* This thread handles connection for each connected client, so the server
* can handle multiple clients at the same time.
*
* @author www.codejava.net
*/
public class UserThread extends Thread {
private Socket socket;
private ChatServer server;
private PrintWriter writer;
public UserThread(Socket socket, ChatServer server) {
this.socket = socket;
this.server = server;
}
public void run() {
try {
InputStream input = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
OutputStream output = socket.getOutputStream();
writer = new PrintWriter(output, true);
printUsers();
String userName = reader.readLine();
server.addUserName(userName);
String serverMessage = "New user connected: " + userName;
server.broadcast(serverMessage, this);
String clientMessage;
do {
clientMessage = reader.readLine();
serverMessage = "[" + userName + "]: " + clientMessage;
server.broadcast(serverMessage, this);
} while (!clientMessage.equals("bye"));
server.removeUser(userName, this);
socket.close();
serverMessage = userName + " has quitted.";
server.broadcast(serverMessage, this);
} catch (IOException ex) {
System.out.println("Error in UserThread: " + ex.getMessage());
ex.printStackTrace();
}
}
/**
* Sends a list of online users to the newly connected user.
*/
void printUsers() {
if (server.hasUsers()) {
writer.println("Connected users: " + server.getUserNames());
} else {
writer.println("No other users connected");
}
}
/**
* Sends a message to the client.
*/
void sendMessage(String message) {
writer.println(message);
}
}
然后它进入一个循环,从用户处读取消息并将其发送给所有其他用户,直到用户发送“再见”,表明他或她将要退出。最后它会通知其他用户该用户已断开连接并关闭连接。
客户端由三个类实现:ChatClient、ReadThread和WriteThread。
该ChatClient启动客户端程序,连接到由指定服务器的主机名/ IP地址和端口号。建立连接后,它会创建并启动两个线程ReadThread和WriteThread。
这是ChatClient类的源代码:
package net.codejava.networking.chat.client;
import java.net.*;
import java.io.*;
/**
* This is the chat client program.
* Type 'bye' to terminte the program.
*
* @author www.codejava.net
*/
public class ChatClient {
private String hostname;
private int port;
private String userName;
public ChatClient(String hostname, int port) {
this.hostname = hostname;
this.port = port;
}
public void execute() {
try {
Socket socket = new Socket(hostname, port);
System.out.println("Connected to the chat server");
new ReadThread(socket, this).start();
new WriteThread(socket, this).start();
} catch (UnknownHostException ex) {
System.out.println("Server not found: " + ex.getMessage());
} catch (IOException ex) {
System.out.println("I/O Error: " + ex.getMessage());
}
}
void setUserName(String userName) {
this.userName = userName;
}
String getUserName() {
return this.userName;
}
public static void main(String[] args) {
if (args.length < 2) return;
String hostname = args[0];
int port = Integer.parseInt(args[1]);
ChatClient client = new ChatClient(hostname, port);
client.execute();
}
}
该ReadThread负责从服务器读取输入并将其连续打印到控制台,直到客户端断开连接。这个类的实现如下:
package net.codejava.networking.chat.client;
import java.io.*;
import java.net.*;
/**
* This thread is responsible for reading server's input and printing it
* to the console.
* It runs in an infinite loop until the client disconnects from the server.
*
* @author www.codejava.net
*/
public class ReadThread extends Thread {
private BufferedReader reader;
private Socket socket;
private ChatClient client;
public ReadThread(Socket socket, ChatClient client) {
this.socket = socket;
this.client = client;
try {
InputStream input = socket.getInputStream();
reader = new BufferedReader(new InputStreamReader(input));
} catch (IOException ex) {
System.out.println("Error getting input stream: " + ex.getMessage());
ex.printStackTrace();
}
}
public void run() {
while (true) {
try {
String response = reader.readLine();
System.out.println("\n" + response);
// prints the username after displaying the server's message
if (client.getUserName() != null) {
System.out.print("[" + client.getUserName() + "]: ");
}
} catch (IOException ex) {
System.out.println("Error reading from server: " + ex.getMessage());
ex.printStackTrace();
break;
}
}
}
}
而WriteThread负责从用户读取输入,并将其发送到服务器,不断直到用户输入“再见”结束聊天。这个类的实现如下:
package net.codejava.networking.chat.client;
import java.io.*;
import java.net.*;
/**
* This thread is responsible for reading user's input and send it
* to the server.
* It runs in an infinite loop until the user types 'bye' to quit.
*
* @author www.codejava.net
*/
public class WriteThread extends Thread {
private PrintWriter writer;
private Socket socket;
private ChatClient client;
public WriteThread(Socket socket, ChatClient client) {
this.socket = socket;
this.client = client;
try {
OutputStream output = socket.getOutputStream();
writer = new PrintWriter(output, true);
} catch (IOException ex) {
System.out.println("Error getting output stream: " + ex.getMessage());
ex.printStackTrace();
}
}
public void run() {
Console console = System.console();
String userName = console.readLine("\nEnter your name: ");
client.setUserName(userName);
writer.println(userName);
String text;
do {
text = console.readLine("[" + userName + "]: ");
writer.println(text);
} while (!text.equals("bye"));
try {
socket.close();
} catch (IOException ex) {
System.out.println("Error writing to server: " + ex.getMessage());
}
}
}
同时运行这两个线程的原因是读取操作总是阻塞当前线程(从命令行读取用户输入和通过网络读取服务器输入)。这意味着如果当前线程正在等待用户的输入,则它无法从服务器读取输入。
因此,使用两个独立的线程来使客户端响应:它可以在读取来自当前用户的消息的同时显示来自其他用户的消息。
这就是聊天应用程序的设计方式。有关更多详细信息,您可以阅读提供的源代码中的注释。但是没有太多注释,因为代码是不言自明的。
从命令行运行服务器程序时需要指定端口号。例如:
java ChatServer 8989
这将启动服务器侦听端口号 8989,一旦启动,您将在服务器中看到以下输出:
Chat Server is listening on port 8989
服务器永远在等待新客户端,因此您必须按 Ctrl + C 来终止它。
要运行客户端,您需要在命令行中指定服务器的主机名/IP 地址和端口号。例如:
java ChatClient localhost 8989
这告诉客户端通过端口 8989 连接到位于 localhost 的服务器。然后您会在服务器的控制台中看到以下消息:
New user connected
在客户端的控制台中:
Connected to chat server
No other users connected
你看,服务器告诉客户端连接了多少用户,但此时没有用户。然后程序要求输入用户名:
Enter your name:_
输入一个用户名,比如 John,然后你就可以开始聊天了:
Enter your name: John
[John]:_
现在,让我们以用户名 Peter 启动第二个客户端。这时,你看到服务器告诉有一个在线用户是约翰:
Connected users: [John]
用户 John 收到有关新用户 Peter 的通知:
New user connected: Peter
输入来自 John 和 Peter 的一些消息,您会看到每个用户都会看到其他用户的消息,就像在聊天室中交谈一样。
现在,John 想退出,所以他键入“bye”——客户端程序终止,您会在服务器的控制台中看到以下输出:
The user John quitted
Peter 还从服务器收到一条消息:
John has quitted.
这基本上就是聊天应用程序的运行方式。您可以使用更多客户端对其进行测试,并且该应用程序仍能顺利运行。以下屏幕截图说明了使用 4 个客户端进行的测试:
你适合学Java吗?4大专业测评方法
代码逻辑 吸收能力 技术学习能力 综合素质
先测评确定适合在学习