Java Swing GUI客户端和服务器聊天应用程序TextArea未更新 [英] Java Swing GUI Client and Server Chat App TextArea not updating
问题描述
我正在使用Java和针对GUI的Swing类来做一个聊天应用程序.
I am doing a Chat Application using Java and Swing class for GUI.
ChatServer类将是从客户端接收消息并回显所有客户端的服务器,但我仅打算与2个客户端进行聊天.
The ChatServer class will be the server receiving message from clients and echo back to all clients but I only intend to make the chat for 2 clients.
ChatClient类是两个客户端.它们在文本区域"上显示从服务器发送的内容.并将文本字段"中的文本发送到服务器.
The ChatClient class is both of the client. They display what was sent from the server on the Text Area. And send texts in the Text Field to the server.
ChatClient类
package chatclient;
import java.net.Socket;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
public class ChatClient extends javax.swing.JFrame {
public ChatClient() {
initComponents();
}
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
* regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">
private void initComponents() {
scrollPane = new javax.swing.JScrollPane();
textArea = new javax.swing.JTextArea();
btnConnect = new javax.swing.JButton();
btnDisconnect = new javax.swing.JButton();
lblStatus = new javax.swing.JLabel();
lblShowStatus = new javax.swing.JLabel();
txtInput = new javax.swing.JTextField();
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
setTitle("Chat Client A");
textArea.setEditable(false);
textArea.setColumns(20);
textArea.setRows(5);
textArea.setText("Welcome to the Chat Server. Type '/close' or Click 'Disconnect' to close.");
textArea.setWrapStyleWord(true);
textArea.setCaretPosition(textArea.getDocument().getLength());
scrollPane.setViewportView(textArea);
btnConnect.setText("Connect");
btnConnect.setActionCommand("btnConnect");
btnConnect.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(java.awt.event.MouseEvent evt) {
btnConnectMouseClicked(evt);
}
});
btnDisconnect.setText("Disconnect");
btnDisconnect.setActionCommand("btnDisconnect");
btnDisconnect.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
btnDisconnectActionPerformed(evt);
}
});
lblStatus.setText("Status: ");
lblShowStatus.setFont(new java.awt.Font("Tahoma", 1, 11)); // NOI18N
lblShowStatus.setForeground(new java.awt.Color(255, 51, 51));
lblShowStatus.setText("Disconnected");
txtInput.setToolTipText("");
txtInput.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
txtInputActionPerformed(evt);
}
});
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(scrollPane)
.addGroup(layout.createSequentialGroup()
.addComponent(btnConnect)
.addGap(18, 18, 18)
.addComponent(btnDisconnect)
.addGap(42, 42, 42)
.addComponent(lblStatus)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(lblShowStatus)
.addGap(0, 42, Short.MAX_VALUE))
.addComponent(txtInput))
.addContainerGap())
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(scrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 213, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 11, Short.MAX_VALUE)
.addComponent(txtInput, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(btnConnect)
.addComponent(btnDisconnect)
.addComponent(lblStatus)
.addComponent(lblShowStatus))
.addContainerGap())
);
pack();
}// </editor-fold>
private void btnConnectMouseClicked(java.awt.event.MouseEvent evt) {
// TODO add your handling code here:
lblShowStatus.setFont(new java.awt.Font("Tahoma", 1, 11)); // NOI18N
lblShowStatus.setForeground(new java.awt.Color(0, 204, 51));
lblShowStatus.setText("Connected");
// ADD CODES FOR CONNECTING TO CHAT SERVER
}
private void btnDisconnectActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
lblShowStatus.setFont(new java.awt.Font("Tahoma", 1, 11)); // NOI18N
lblShowStatus.setForeground(new java.awt.Color(255, 51, 51));
lblShowStatus.setText("Disconnected");
// ADD CODES FOR DISCONNECTING FROM CHAT SERVER
}
private void txtInputActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
}
/**
* @param args the command line arguments
*/
public static void main(String args[]) {
/* Set the Nimbus look and feel */
//<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
/* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
* For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
*/
try {
for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
if ("Nimbus".equals(info.getName())) {
javax.swing.UIManager.setLookAndFeel(info.getClassName());
break;
}
}
} catch (ClassNotFoundException ex) {
java.util.logging.Logger.getLogger(ChatClient.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (InstantiationException ex) {
java.util.logging.Logger.getLogger(ChatClient.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
java.util.logging.Logger.getLogger(ChatClient.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (javax.swing.UnsupportedLookAndFeelException ex) {
java.util.logging.Logger.getLogger(ChatClient.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
}
//</editor-fold>
/* Create and display the form */
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new ChatClient().setVisible(true);
}
});
String input = "", serverInput = "";
String host = "localhost";
int port = 1337;
Socket client;
// updateTextArea("TEST UPDATE");
try {
client = new Socket(host, port);
System.out.println("Connected to Server!");
DataInputStream in = new DataInputStream(client.getInputStream());
DataOutputStream out = new DataOutputStream(client.getOutputStream());
System.out.println("Before setting text area");
updateTextArea("trying to update");
do {
// HANDLE INPUT PART HERE
serverInput = in.readUTF();
if(serverInput != null) {
System.out.println("Reached here");
System.out.println(serverInput);
updateTextArea(serverInput);
}
} while(!input.equals("/close"));
System.out.println("Program closed");
}
catch(Exception exc) {
System.err.println(exc.getMessage());
}
}
private static void updateTextArea(String temp) {
textArea.setText(textArea.getText() + "\n" + temp + "\n");
textArea.setCaretPosition(textArea.getDocument().getLength());
}
// Variables declaration - do not modify
private javax.swing.JButton btnConnect;
private javax.swing.JButton btnDisconnect;
private javax.swing.JLabel lblShowStatus;
private javax.swing.JLabel lblStatus;
private javax.swing.JScrollPane scrollPane;
private static javax.swing.JTextArea textArea;
private javax.swing.JTextField txtInput;
// End of variables declaration
}
ChatServer类
package chatserver;
import java.io.BufferedReader;
import java.io.InputStreamReader;
// for testing
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
*
* @author wacats
*/
public class ChatServer {
public static void main(String args[]) {
int port = 1337;
try {
ServerSocket server = new ServerSocket(port);
String inMessage = "";
while(true) {
Socket clientA = server.accept();
DataInputStream inA = new DataInputStream(clientA.getInputStream());
DataOutputStream outA = new DataOutputStream(clientA.getOutputStream());
// outA.writeUTF("Welcome to the Chat Server. Type '/close' or Click 'Disconnect' to close.");
// for testing
// BufferedReader user = new BufferedReader(new InputStreamReader(System.in));
do {
inMessage = inA.readUTF();
outA.writeUTF("testing");
if(inMessage != null) {
outA.writeUTF(inMessage);
}
} while(!inMessage.equals("/close"));
clientA.close();
}
}
catch(Exception ex) {
ex.printStackTrace();
}
}
}
我对程序执行过程的想法是:
My idea on the process of this program is:
- 启动ChatServer
- 启动ChatClient
- 当两个客户端都连接到服务器时,他们可以开始聊天.
- 按"Enter"将把文本字段"中的文本发送到服务器.
- 服务器会将文本广播到两个客户端.
- 客户端将更新文本区域,并附加从服务器接收的文本.
推荐答案
基本问题是,尝试调用updateTextArea
时textArea
是null
,这是因为尚未执行invokeLater
调用并构造好用户界面后,您基本上就有了竞争条件.
The basic problem is, textArea
is null
when you try and call updateTextArea
, this is because the invokeLater
call hasn't executed yet and constructed your UI, you basically have a race condition.
在Swing中处理Socket
的正常方法是使用SwingWorker
The normal way to deal with Socket
s in Swing is, is to use a SwingWorker
看看 Swing中的并发和工作线程和SwingWorker 了解更多详细信息.
Have a look at Concurrency in Swing and Worker Threads and SwingWorker for more details.
有很多方法可以解决您的问题.您可以使用SwingWorker
从套接字读取文本并生成更新通知(通过publish
/process
)方法,这通常称为观察者模式".这样做很好,因为它可以使您的代码脱钩并生成更可重用的解决方案.
There are a number of ways you could solve you problem. You could use the SwingWorker
to read the text from the socket and generate update notifications (through the publish
/process
) methods, this is commonly known as an "observer pattern". This is good as it de-couples your code and generates a more reusable solution.
这个类所做的全部工作就是从Socket
中读取文本,并简单地从文本中生成ActionEvent
.
All this class does is reads text from the Socket
and generates ActionEvent
s from the text, simple.
public class SocketReader extends SwingWorker<Void, String> {
private List<ActionListener> actionListeners;
public SocketReader() {
actionListeners = new ArrayList<>(25);
}
public void addActionListener(ActionListener listener) {
actionListeners.add(listener);
}
public void removeActionListener(ActionListener listener) {
actionListeners.remove(listener);
}
@Override
protected Void doInBackground() throws Exception {
System.out.println("Connected to Server!");
try (DataInputStream in = new DataInputStream(SocketManager.INSTACNE.getInputStream())) {
System.out.println("Before setting text area");
String serverInput = null;
do {
// HANDLE INPUT PART HERE
serverInput = in.readUTF();
if (serverInput != null) {
System.out.println("Read " + serverInput);
publish(serverInput);
}
} while (!serverInput.equals("/close"));
System.out.println("Program closed");
}
return null;
}
@Override
protected void process(List<String> chunks) {
for (String text : chunks) {
ActionEvent evt = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, text);
for (ActionListener listener : actionListeners) {
listener.actionPerformed(evt);
}
}
}
}
SocketWriter
这简单地将文本写入到Socket
中.从技术上讲,您不需要为此使用SwingWorker
,但是我喜欢这样的事实:取消它相对容易
SocketWriter
This simple writes text to the Socket
, simple. Technically, you don't need to use a SwingWorker
for this, but I like the fact that it's relatively easy to cancel
public class SocketWriter extends SwingWorker<Void, Void> {
private List<String> messages;
private ReentrantLock lock;
private Condition waitCon;
public SocketWriter() {
messages = Collections.synchronizedList(new ArrayList<String>(25));
lock = new ReentrantLock();
waitCon = lock.newCondition();
}
public void write(String text) {
System.out.println("Write " + text);
messages.add(text);
try {
lock.lock();
waitCon.signalAll();
} finally {
lock.unlock();
}
}
@Override
protected Void doInBackground() throws Exception {
try (DataOutputStream out = new DataOutputStream(SocketManager.INSTACNE.getOutputStream())) {
while (!isCancelled()) {
while (messages.isEmpty() && !isCancelled()) {
try {
lock.lock();
waitCon.await();
} finally {
lock.unlock();
}
}
List<String> cache = new ArrayList<>(messages);
messages.clear();
for (String text : cache) {
System.out.println("Send " + text);
out.writeUTF(text);
}
}
}
return null;
}
}
SocketManager
好的,这对我来说有点过大了,但是我想要一个Socket
的中央控制器,您不必使用单例,您可以简单地使其成为简单的类并将其引用传递给您的ChatClient
直到SocketReader/Writer
,但是已经晚了,我很懒
SocketManager
Okay, this is slightly overkill on my part, but I want a central controller for the Socket
, you don't have to use a singleton, you could simply make it a simple class and pass a reference of it to your ChatClient
and on down to the SocketReader/Writer
, but it's late and I'm lazy
public enum SocketManager {
INSTACNE;
private String host = "localhost";
private int port = 1337;
private Socket socket;
public Socket open() throws IOException {
if (socket != null) {
close();
}
socket = new Socket(host, port);
return socket;
}
public void close() throws IOException {
if (socket == null) {
return;
}
socket.close();
}
public boolean isOpen() {
return socket != null
&& socket.isConnected()
&& !socket.isClosed()
&& !socket.isInputShutdown()
&& !socket.isOutputShutdown();
}
public InputStream getInputStream() throws IOException {
Objects.requireNonNull(socket, "Socket is not open");
return socket.getInputStream();
}
public OutputStream getOutputStream() throws IOException {
Objects.requireNonNull(socket, "Socket is not open");
return socket.getOutputStream();
}
}
ChatClient
太棒了,所有这些都很好,但是您应该如何使用它呢?
ChatClient
That's all nice a awesome and all, but how are you suppose to use it?
从根本上说,将在ChatClient
中创建SocketReader
和SocketWriter
的实例,将ActionListener
附加到读取器,并在触发时更新JTextArea
并发送所需的文本发送给SocketWriter
,例如...
Will, very basically, you create an instance of SocketReader
and SocketWriter
in your ChatClient
, you attach an ActionListener
to the reader and update the JTextArea
when it's triggered and send the text you want sent to the SocketWriter
, for example...
public class ChatClient extends javax.swing.JFrame {
public ChatClient() {
initComponents();
socketReader = new SocketReader();
socketReader.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String text = e.getActionCommand();
textArea.append(text);
textArea.append("\n");
textArea.setCaretPosition(textArea.getDocument().getLength());
}
});
socketReader.execute();
socketWriter = new SocketWriter();
socketWriter.execute();
}
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
* regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">
private void initComponents() {
scrollPane = new javax.swing.JScrollPane();
textArea = new javax.swing.JTextArea();
btnConnect = new javax.swing.JButton();
btnDisconnect = new javax.swing.JButton();
lblStatus = new javax.swing.JLabel();
lblShowStatus = new javax.swing.JLabel();
txtInput = new javax.swing.JTextField();
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
setTitle("Chat Client A");
textArea.setEditable(false);
textArea.setColumns(20);
textArea.setRows(5);
textArea.setText("Welcome to the Chat Server. Type '/close' or Click 'Disconnect' to close.");
textArea.setWrapStyleWord(true);
textArea.setCaretPosition(textArea.getDocument().getLength());
scrollPane.setViewportView(textArea);
btnConnect.setText("Connect");
btnConnect.setActionCommand("btnConnect");
btnConnect.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(java.awt.event.MouseEvent evt) {
btnConnectMouseClicked(evt);
}
});
btnDisconnect.setText("Disconnect");
btnDisconnect.setActionCommand("btnDisconnect");
btnDisconnect.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
btnDisconnectActionPerformed(evt);
}
});
lblStatus.setText("Status: ");
lblShowStatus.setFont(new java.awt.Font("Tahoma", 1, 11)); // NOI18N
lblShowStatus.setForeground(new java.awt.Color(255, 51, 51));
lblShowStatus.setText("Disconnected");
txtInput.setToolTipText("");
txtInput.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
txtInputActionPerformed(evt);
}
});
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(scrollPane)
.addGroup(layout.createSequentialGroup()
.addComponent(btnConnect)
.addGap(18, 18, 18)
.addComponent(btnDisconnect)
.addGap(42, 42, 42)
.addComponent(lblStatus)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(lblShowStatus)
.addGap(0, 42, Short.MAX_VALUE))
.addComponent(txtInput))
.addContainerGap())
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(scrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 213, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 11, Short.MAX_VALUE)
.addComponent(txtInput, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(btnConnect)
.addComponent(btnDisconnect)
.addComponent(lblStatus)
.addComponent(lblShowStatus))
.addContainerGap())
);
pack();
}// </editor-fold>
private void btnConnectMouseClicked(java.awt.event.MouseEvent evt) {
// TODO add your handling code here:
lblShowStatus.setFont(new java.awt.Font("Tahoma", 1, 11)); // NOI18N
lblShowStatus.setForeground(new java.awt.Color(0, 204, 51));
lblShowStatus.setText("Connected");
// ADD CODES FOR CONNECTING TO CHAT SERVER
}
private void btnDisconnectActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
lblShowStatus.setFont(new java.awt.Font("Tahoma", 1, 11)); // NOI18N
lblShowStatus.setForeground(new java.awt.Color(255, 51, 51));
lblShowStatus.setText("Disconnected");
// ADD CODES FOR DISCONNECTING FROM CHAT SERVER
}
private void txtInputActionPerformed(java.awt.event.ActionEvent evt) {
if (SocketManager.INSTACNE.isOpen()) {
socketWriter.write(txtInput.getText());
} else {
System.out.println("!! Not open");
}
}
/**
* @param args the command line arguments
*/
public static void main(String args[]) {
try {
/* Set the Nimbus look and feel */
//<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
/* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
* For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
*/
try {
for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
if ("Nimbus".equals(info.getName())) {
javax.swing.UIManager.setLookAndFeel(info.getClassName());
break;
}
}
} catch (ClassNotFoundException ex) {
java.util.logging.Logger.getLogger(ChatClient.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (InstantiationException ex) {
java.util.logging.Logger.getLogger(ChatClient.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
java.util.logging.Logger.getLogger(ChatClient.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (javax.swing.UnsupportedLookAndFeelException ex) {
java.util.logging.Logger.getLogger(ChatClient.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
}
//</editor-fold>
SocketManager.INSTACNE.open();
/* Create and display the form */
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new ChatClient().setVisible(true);
}
});
} catch (IOException ex) {
ex.printStackTrace();
}
//</editor-fold>
}
private SocketWriter socketWriter;
private SocketReader socketReader;
// Variables declaration - do not modify
private javax.swing.JButton btnConnect;
private javax.swing.JButton btnDisconnect;
private javax.swing.JLabel lblShowStatus;
private javax.swing.JLabel lblStatus;
private javax.swing.JScrollPane scrollPane;
private javax.swing.JTextArea textArea;
private javax.swing.JTextField txtInput;
// End of variables declaration
}
您会注意到,我在main
中使用了SocketManager#open
,抱歉,错过了您的连接"代码.我建议改为将其移至该方法;)
You'll note, I used SocketManager#open
in the main
, sorry, missed you "connect" code. I would suggest moving that to that method instead ;)
我对此没有做太多改变,但是以防万一...
I didn't make to much of change to this, but just in case...
public class ChatServer {
public static void main(String args[]) {
int port = 1337;
try {
ServerSocket server = new ServerSocket(port);
String inMessage = "";
while (true) {
System.out.println("Waiting");
Socket clientA = server.accept();
System.out.println("Connected");
DataInputStream inA = new DataInputStream(clientA.getInputStream());
DataOutputStream outA = new DataOutputStream(clientA.getOutputStream());
// outA.writeUTF("Welcome to the Chat Server. Type '/close' or Click 'Disconnect' to close.");
// for testing
// BufferedReader user = new BufferedReader(new InputStreamReader(System.in));
do {
inMessage = inA.readUTF();
if (inMessage != null) {
outA.writeUTF(inMessage);
}
} while (!inMessage.equals("/close"));
clientA.close();
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
通常,当客户端连接时,您将启动一个新的Thread
并让它处理客户端Socket
,但这不是我的重点.
Normally, when a client connects, you'd start a new Thread
and have it process the client Socket
, but that wasn't my focus.
因此,基于上述所有内容,您需要大量阅读,包括关于套接字的所有信息
So, based on all that, you have a lot of reading to catch up on, including Concurrency in Java and All About Sockets
这篇关于Java Swing GUI客户端和服务器聊天应用程序TextArea未更新的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!