[英]Multithreaded server- how to properly handle both client and server disconnect
[英]Clients disconnect on multithreaded Client/Server application
這是我正在使用swing組件執行的多線程Server / Client應用程序,有點像聊天應用程序。 除非我在同一台計算機上打開2個或更多客戶端,否則一切正常。 然后,聊天和消息仍然可以正常工作,但是如果我關閉其中一個客戶端,則其他兩個也都關閉。 我知道它一定與多線程有關,但是我不能完全弄清楚問題出在哪里。
服務器.java
import javax.swing.*;
import java.awt.*;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.A
rrayList;
public class Server extends JFrame {
private int port;
String host;
private ArrayList<ClientThread> clientThreads;
private JTextArea textArea;
private ServerSocket serverSocket = null;
Socket connection;
private PrintWriter out;
private BufferedReader in;
Server(int port) {
this.port = port;
clientThreads = new ArrayList<>();
// Tar hand om JFrame.
setLayout(new BorderLayout());
setVisible(true);
setLocation(750, 0);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
textArea = new JTextArea(15, 60);
JPanel centerPanel = new JPanel();
centerPanel.add(new JScrollPane(textArea));
add(centerPanel, BorderLayout.CENTER);
pack();
try {
serverSocket = new ServerSocket(port);
host = serverSocket.getInetAddress().getLocalHost().getHostAddress();
} catch (IOException e) {
System.err.println(e.getMessage());
}
// Titeln skrivs ut.
updateTitle();
while (true) {
try {
connection = serverSocket.accept();
// Byter titel
updateTitle();
// Startar en Thread för den nya socket:en.
ClientThread task = new ClientThread(connection);
clientThreads.add(task);
task.start();
} catch (IOException ex) {
// Ignorera.
}
}
}
public static void main(String[] args) {
if (args.length == 0) {
// Skickar default värde.
new Server(2000);
} else if (args.length == 1) {
// Skickar argumentet
new Server(Integer.parseInt(args[0]));
}
}
public class ClientThread extends Thread {
// Threadklassen till klientsocket:en.
private Socket clientSocket;
private String clientHost;
public ClientThread(Socket connection) {
this.clientSocket = connection;
clientHost = clientSocket.getInetAddress().getHostName();
}
@Override
public void run() {
// Skriver ut att någon har "loggat in" i JTextArea.
textArea.append("CLIENT: " + clientHost + "CONNECTED" + "\n");
// Metoden som kollar om klienten skriver en chatmeddelande.
try {
synchronized (this) {
out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
while (true) {
synchronized (this) {
if (in == null) System.out.println("NULL");
String line = in.readLine();
textArea.append("CLIENT: " + clientHost + " BROADCAST: " + line + "\n");
broadcast(line);
}
}
}
// Byter titel och stänger allt.
// if (out != null) out.close();
// if (in != null) in.close();
// if (clientSocket != null) clientSocket.close();
} catch (IOException e){
synchronized (this) {
e.printStackTrace();
// Vad händer när en klient blir disconnected.
textArea.append("CLIENT: " + clientHost + " DISCONNECTED" + "\n");
for (Thread t : clientThreads) {
if (t == this) {
t = null;
}
clientThreads.remove(this);
}
System.out.println(clientThreads.size());
updateTitle();
}
}
}
}
public void broadcast(String message) {
// Synchronized för att slippa krocka.
try {
synchronized (this) {
// Skriver ut meddelandet i JTextArea och sänder det till alla klienter.
for (int i = clientThreads.size() - 1; i >= 0; i--) {
out = new PrintWriter(clientThreads.get(i).clientSocket.getOutputStream(), true);
out.write(message + "\n");
out.flush();
}
}
} catch (IOException e) {
System.err.println(e.getMessage());
}
}
public void updateTitle() {
setTitle("HOST: " + host + " | PORT: " + port + " | NUMBER OF CLIENTS: " +
clientThreads.size());
}
}
客戶端.java
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.*;
import java.net.Socket;
import java.net.SocketTimeoutException;
public class Client extends JFrame {
private Socket socket;
private BufferedReader in;
private PrintWriter out;
private JTextField inputTextField = new JTextField(60);
private JTextArea textArea = new JTextArea(15, 60);
// Tråd som kallas för att läsa rader från servern.
private Thread backgroundThread = new Thread(new Runnable() {
@Override
public void run() {
String line;
// Eviga loopen
try {
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
line = in.readLine();
while (line != null) {
textArea.append(line + "\n");
line = in.readLine();
}
} catch (SocketTimeoutException e2) {
// Ignorera.
} catch (IOException e) {
// Ignorera.
}
// try {
// if (out != null) out.close();
// if (in != null) in.close();
// if (socket != null) socket.close();
// } catch (IOException e) {
// Ignorera.
// }
}
});
// Vad som händer när man trycker på enter i JTextField
private AbstractAction onEnterPressAction = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
String textToSend = inputTextField.getText();
inputTextField.setText("");
try {
// Skickar ut strängen till servern.
out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "ISO-8859-1"), true);
out.write(textToSend + "\n");
out.flush();
} catch (SocketTimeoutException e2) {
// Ignorera
} catch (IOException e1) {
e1.printStackTrace();
}
}
};
public Client(String host, int port) {
// Konstruktorn.
// Skapar en Socket.
try {
socket = new Socket(host, port);
socket.setSoTimeout(15000);
} catch (IOException e) {
// Kunde inte kopplas till servern.
JOptionPane.showMessageDialog(null, "Could not connect to server", "Connection Error", JOptionPane.ERROR_MESSAGE);
System.exit(0);
}
// Sätter titelrad
setTitle("CONNECTED TO SERVER: " + host + " IN PORT: " + port);
// Startar eviga läsloopen
backgroundThread.start();
// Tar hand om JFrame.
setLayout(new BorderLayout());
setVisible(true);
// Sätter en annan operation när JFrame stängs för att stoppa loopen.
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
// Söder JPanel (JTextField som man skriver meddelande i)
JPanel southPanel = new JPanel();
southPanel.add(inputTextField);
inputTextField.addActionListener(onEnterPressAction);
// Centrala JPanel (JTextArea som visar alla meddelanden).
JPanel centerPanel = new JPanel();
JScrollPane scrollPane = new JScrollPane(textArea);
centerPanel.add(scrollPane);
// Adderar och packar.
add(centerPanel, BorderLayout.CENTER);
add(southPanel, BorderLayout.SOUTH);
pack();
setLocationRelativeTo(null);
}
public static void main(String[] args) {
// if (args.length == 0) {
// // Skickar default värden.
// new Client("127.0.0.1", 2000);
// } else if (args.length == 1) {
// // Skickar argument och default port värde.
// new Client(args[0], 2000);
// } else if (args.length == 2) {
// // Skickar argumenter.
// new Client(args[0], Integer.parseInt(args[1]));
// }
new Client("192.168.1.66", 2000);
}
}
編輯:這是我得到的錯誤,但這可能並不表示問題出在哪里。
2
java.net.SocketException: Connection reset
at java.net.SocketInputStream.read(SocketInputStream.java:189)
at java.net.SocketInputStream.read(SocketInputStream.java:121)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.company.Server$ClientThread.run(Server.java:98)
1
java.net.SocketException: Connection reset
at java.net.SocketInputStream.read(SocketInputStream.java:134)
at java.net.SocketInputStream.read(SocketInputStream.java:121)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.company.Server$ClientThread.run(Server.java:98)
這只是客戶端斷開連接的正常行為,問題是,當我關閉第三個客戶端(假設我們打開了3個客戶端)后,我去寫了第二個,然后按Enter,它將消息廣播到所有休息,然后該客戶端立即斷開連接,也出現相同的錯誤。 這不是我的迭代。
從本質上講,我認為這與多線程無關。
for (Thread t : clientThreads) {
if (t == this) {
t = null;
}
clientThreads.remove(this);
}
這不是遍歷列表並刪除項目的正確方法。 您將在增強的for循環中隱式創建一個迭代器; 像這樣調用remove是對該迭代器的並發修改,因為迭代器無法知道您已經在列表中調用了remove
。
目前尚不清楚您在這里真正要做什么。 條件只是將局部變量設置為null。 您也可以只調用clientThreads.remove(this)
,而無需循環。
遍歷列表和刪除項目的正確方法是使用顯式迭代器,您可以在其上調用remove
方法。
Iterator<Thread> it = clientThreads.iterator();
while (it.hasNext()) {
Thread t = it.next();
// Do something with t.
it.remove();
}
而且,當然,在應用這兩種方法中的任何一種時,您都需要確保對clientThreads
獨占訪問。
更新:原來問題出在Server
類的這一行:
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
private BufferedReader in
是Server類的私有屬性,因此,它已在所有ClientThreads中共享。 每次調用此行時,它將所有流切換到當前的socketInputStream。 而不是為所有人使用私有成員,您應該為每個線程創建BufferedReader:
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
您在遍歷Server.java類中的列表時使用下面的行。 這是相同的原因。
clientThreads.remove(this);
與其直接刪除線程,不如使用線程安全迭代器。 或使用CopyOnWriteArrayList可以解決此問題。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.