代碼基礎參考鏈接,十分感謝。
需求功能:
- 實現客戶端與服務器的連接
- 各個客戶端能夠共享消息界面,即一個客戶端發送消息後所有在線客戶端都能夠收到
- 客戶端登錄時可以自定義暱稱
- 客戶端登錄後顯示已在線成員
- 客戶端登錄後通知其他在線成員,下線後也通知
- 客戶端登錄後顯示之前的聊天記錄
- 服務器斷開後能通知各客戶端重啓
- 啓動客戶端時若服務器未打開顯示提示信息
- 客戶端退出後服務器能夠提示,登錄同
核心思想:
將服務器作爲轉接的中間站,用集合存儲鏈接的socket、記錄和在線成員。
原參考代碼會出現的主要問題:
任意關閉一個客戶端或關閉服務器時會拋出 java.net.SocketException: Connection reset 的異常,原因是:一端退出,但退出時並未關閉該連接,另一端如果在從連接中讀數據則拋出該異常。因此此處的解決方法爲在ServerThread中buf讀取數據時加個try-catch塊,有異常後進行相應的處理。
運行截圖:
代碼部分:
Server.java 服務器
package ChatRoomDemo;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
public class Server {
public static List<Socket> list = new ArrayList<>(); // 客戶端連接
public static List<String> record = new ArrayList<>(); // 聊天記錄
public static List<String> online_member = new ArrayList<>(); // 在線成員
private static ServerSocket server;
public static void main(String[] args) {
try {
server = new ServerSocket(4233);
System.out.println("Chatroom is opening!");
while(true) {
Socket socket = server.accept();
list.add(socket);
new Thread(new ServerThread(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
ServerThread.java 服務器線程
package ChatRoomDemo;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class ServerThread implements Runnable {
private final Socket socket;
public ServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
BufferedReader buf = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter pw = new PrintWriter(socket.getOutputStream());
// 向客戶端提示輸入暱稱
pw.println("Please enter your nickname:");
pw.flush();
// 讀取客戶端發送的暱稱,並在服務器提示上線
String nickname = buf.readLine();
System.out.println(nickname + " is online");
// 給每個在線的客戶端發送該客戶端上線記錄
for(Socket r : Server.list) {
if(!r.equals(this.socket)) {
pw = new PrintWriter(r.getOutputStream());
pw.println(nickname + " is online");
pw.flush();
}
else {
pw = new PrintWriter(r.getOutputStream());
pw.println("Welcome " + nickname);
pw.flush();
}
}
// 在該客戶端顯示其他已上線的成員,並將自己添加進去
for(String s : Server.online_member) {
pw.println(s + " is online");
pw.flush();
}
Server.online_member.add(nickname);
// 在該客戶端顯示聊天記錄
pw = new PrintWriter(socket.getOutputStream());
for(String s : Server.record) {
pw.println(s);
pw.flush();
}
// 自己聊天的部分
while(true) {
String str;
try {
// 讀取客戶端發送的聊天信息,並記錄
str = buf.readLine();
Server.record.add(nickname + ":" + str);
// 若正確讀取聊天信息,給所有在線成員刷新該信息
for(Socket r : Server.list) {
pw = new PrintWriter(r.getOutputStream());
pw.println(nickname + ":" + str);
pw.flush();
}
} catch (Exception e) {
// 客戶端關閉後
System.out.println(nickname + " is offline");
Server.list.remove(socket);
Server.online_member.remove(nickname);
// 通知其他客戶端該成員已下線
for(Socket r : Server.list) {
pw = new PrintWriter(r.getOutputStream());
pw.println(nickname + " is offline");
pw.flush();
}
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Link.java 客戶端連接並運行
package ChatRoomDemo;
import java.io.IOException;
import java.net.Socket;
public class Link {
public static void linkstart() {
try {
Socket socket = new Socket("localhost", 4233);
System.out.println("Connect successfully!");
new Thread(new ClientThread1(socket)).start(); // 將信息發送給服務器的線程
new Thread(new ClientThread2(socket)).start(); // 從服務器讀取信息的線程
} catch (IOException e) {
// 若服務器未開啓
System.out.println("Server is closed, please try again later");
}
}
}
ClientThread1.java 客戶端發送消息進程
package ChatRoomDemo;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;
/*
Send message to server
*/
public class ClientThread1 implements Runnable{
private Socket socket;
public ClientThread1(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
BufferedReader buf = new BufferedReader(new InputStreamReader(System.in));
PrintWriter pw = new PrintWriter(socket.getOutputStream());
// 輸入暱稱,併發送給服務器
String nickname = buf.readLine();
pw.println(nickname);
pw.flush();
// 發送聊天信息
while(true) {
if(socket.isClosed()) break;
String str = buf.readLine();
String date = new SimpleDateFormat("HH:mm:ss").format(new Date()); // 時間
pw.println(str + " " + date);
pw.flush();
}
buf.close();
pw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
ClientThread2.java 客戶端接收消息進程
package ChatRoomDemo;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.Socket;
/*
Get message from server
*/
public class ClientThread2 implements Runnable{
private Socket socket;
private BufferedReader buf = null;
public ClientThread2(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
buf = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while(true) {
try {
String str = buf.readLine();
if(str!=null) System.out.println(str);
} catch (Exception e) {
// 服務器關閉
System.out.println("Server is closed, please try to restart");
break;
}
}
buf.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Client1.java 同所有Client
package ChatRoomDemo;
public class Client1 {
public static void main(String[] args) {
Link.linkstart();
}
}
遺留待解決問題:
- 輸入暱稱時未阻塞,其他客戶端發信息時會直接顯示(應該是把設定暱稱的操作放到線程外,但有點不美觀就pass了..)
- 服務器斷開後客戶端收到提示但要手動關閉(發送信息的線程還在運作,暫時沒能成功判斷socket已失效)
- 聊天記錄顯示問題,本來想實現時間在上語句在下再接一行空行,會被各種喫換行符還是先pass了,只好弄了個殘疾版
- 在ServeThread中若在死循環退出後直接close buf和pw,會導致一個客戶端的退出會讓所有服務器線程一起報錯,即一個退出全都退出。沒想到原因,希望有大佬解惑。
小注意點:不要在一個文件中看到socket用完就close,我就因爲在ClientThread中直接finally close導致debug了一下午(ry