代码基础参考链接,十分感谢。
需求功能:
- 实现客户端与服务器的连接
- 各个客户端能够共享消息界面,即一个客户端发送消息后所有在线客户端都能够收到
- 客户端登录时可以自定义暱称
- 客户端登录后显示已在线成员
- 客户端登录后通知其他在线成员,下线后也通知
- 客户端登录后显示之前的聊天记录
- 服务器断开后能通知各客户端重启
- 启动客户端时若服务器未打开显示提示信息
- 客户端退出后服务器能够提示,登录同
核心思想:
将服务器作为转接的中间站,用集合存储链接的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