Java Socket+多线程 实现简易聊天室(注释版)

    技术2022-07-14  74

    代码基础参考链接,十分感谢。


    需求功能:

    实现客户端与服务器的连接各个客户端能够共享消息界面,即一个客户端发送消息后所有在线客户端都能够收到客户端登录时可以自定义昵称客户端登录后显示已在线成员客户端登录后通知其他在线成员,下线后也通知客户端登录后显示之前的聊天记录服务器断开后能通知各客户端重启启动客户端时若服务器未打开显示提示信息客户端退出后服务器能够提示,登录同

     

    核心思想:

    将服务器作为转接的中间站,用集合存储链接的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

    Processed: 0.016, SQL: 9