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

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


需求功能:

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

 

核心思想:

将服务器作为转接的中间站,用集合存储链接的socket、记录和在线成员。

 

原参考代码会出现的主要问题:

任意关闭一个客户端或关闭服务器时会抛出 java.net.SocketException: Connection reset 的异常,原因是:一端退出,但退出时并未关闭该连接,另一端如果在从连接中读数据则抛出该异常。因此此处的解决方法为在ServerThreadbuf读取数据时加个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();
    }
}

 

遗留待解决问题:

  1. 输入暱称时未阻塞,其他客户端发信息时会直接显示(应该是把设定暱称的操作放到线程外,但有点不美观就pass了..)
  2. 服务器断开后客户端收到提示但要手动关闭(发送信息的线程还在运作,暂时没能成功判断socket已失效)
  3. 聊天记录显示问题,本来想实现时间在上语句在下再接一行空行,会被各种吃换行符还是先pass了,只好弄了个残疾版
  4. ServeThread中若在死循环退出后直接close bufpw,会导致一个客户端的退出会让所有服务器线程一起报错,即一个退出全都退出。没想到原因,希望有大佬解惑。

 

小注意点:不要在一个文件中看到socket用完就close,我就因为在ClientThread中直接finally close导致debug了一下午(ry

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章