基于多线程与 Socket 实现的聊天系统 v1.1(多线程、命令行、可记录用户信息)

image.png

image.png

image.png

image.png

服务端

public class Server
{
    private static final int SERVER_PORT = 30000;
    // 使用CrazyitMap对象来保存每个客户名字和对应输出流之间的对应关系。
    public static CrazyitMap clients
        = new CrazyitMap<>();
    public void init()
    {
        try(
            // 建立监听的ServerSocket
            ServerSocket ss = new ServerSocket(SERVER_PORT))
        {
            // 采用死循环来不断接受来自客户端的请求
            while(true)
            {
                Socket socket = ss.accept();
                new ServerThread(socket).start();
            }
        }
        // 如果抛出异常
        catch (IOException ex)
        {
            System.out.println("服务器启动失败,是否端口"
                + SERVER_PORT + "已被占用?");
        }
    }
    public static void main(String[] args)
    {
        Server server = new Server();
        server.init();
    }
}

服务端实现

public class ServerThread extends Thread
{
    private Socket socket;
    BufferedReader br = null;
    PrintStream ps = null;
    // 定义一个构造器,用于接收一个Socket来创建ServerThread线程
    public ServerThread(Socket socket)
    {
        this.socket = socket;
    }
    @Override
    public void run()
    {
        try
        {
            // 获取该Socket对应的输入流
            br = new BufferedReader(new InputStreamReader(socket
                .getInputStream()));
            // 获取该Socket对应的输出流
            ps = new PrintStream(socket.getOutputStream());
            String line = null;
            while((line = br.readLine())!= null)
            {
                // 如果读到的行以CrazyitProtocol.USER_ROUND开始,并以其结束,
                // 可以确定读到的是用户登录的用户名
                if (line.startsWith(CrazyitProtocol.USER_ROUND)
                    && line.endsWith(CrazyitProtocol.USER_ROUND))
                {
                    // 得到真实消息
                    String userName = getRealMsg(line);
                    // 如果用户名重复
                    if (Server.clients.map.containsKey(userName))
                    {
                        System.out.println("重复");
                        ps.println(CrazyitProtocol.NAME_REP);
                    }
                    else
                    {
                        System.out.println("成功");
                        ps.println(CrazyitProtocol.LOGIN_SUCCESS);
                        Server.clients.put(userName , ps);
                    }
                }
                // 如果读到的行以CrazyitProtocol.PRIVATE_ROUND开始,并以其结束,
                // 可以确定是私聊信息,私聊信息只向特定的输出流发送
                else if (line.startsWith(CrazyitProtocol.PRIVATE_ROUND)
                    && line.endsWith(CrazyitProtocol.PRIVATE_ROUND))
                {
                    // 得到真实消息
                    String userAndMsg = getRealMsg(line);
                    // 以SPLIT_SIGN分割字符串,前半是私聊用户,后半是聊天信息
                    String user = userAndMsg.split(CrazyitProtocol.SPLIT_SIGN)[0];
                    String msg = userAndMsg.split(CrazyitProtocol.SPLIT_SIGN)[1];
                    // 获取私聊用户对应的输出流,并发送私聊信息
                    Server.clients.map.get(user).println(Server.clients
                        .getKeyByValue(ps) + "悄悄地对你说:" + msg);
                }
                // 公聊要向每个Socket发送
                else
                {
                    // 得到真实消息
                    String msg = getRealMsg(line);
                    // 遍历clients中的每个输出流
                    for (PrintStream clientPs : Server.clients.valueSet())
                    {
                        clientPs.println(Server.clients.getKeyByValue(ps)
                            + "说:" + msg);
                    }
                }
            }
        }
        // 捕捉到异常后,表明该Socket对应的客户端已经出现了问题
        // 所以程序将其对应的输出流从Map中删除
        catch (IOException e)
        {
            Server.clients.removeByValue(ps);
            System.out.println("剩余在线人数" + Server.clients.map.size());
            // 关闭网络、IO资源
            try
            {
                if (br != null)
                {
                    br.close();
                }
                if (ps != null)
                {
                    ps.close();
                }
                if (socket != null)
                {
                    socket.close();
                }
            }
            catch (IOException ex)
            {
                ex.printStackTrace();
            }
        }
    }
    // 将读到的内容去掉前后的协议字符,恢复成真实数据
    private String getRealMsg(String line)
    {
        return line.substring(CrazyitProtocol.PROTOCOL_LEN
            , line.length() - CrazyitProtocol.PROTOCOL_LEN);
    }
}

客户端

public class Client
{
    private static final int SERVER_PORT = 30000;
    private Socket socket;
    private PrintStream ps;
    private BufferedReader brServer;
    private BufferedReader keyIn;
    public void init()
    {
        try
        {
            // 初始化代表键盘的输入流
            keyIn = new BufferedReader(
                new InputStreamReader(System.in));
            // 连接到服务器
            socket = new Socket("127.0.0.1", SERVER_PORT);
            // 获取该Socket对应的输入流和输出流
            ps = new PrintStream(socket.getOutputStream());
            brServer = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
            String tip = "";
            // 采用循环不断地弹出对话框要求输入用户名
            while(true)
            {
                String userName = JOptionPane.showInputDialog(tip
                    + "输入用户名");    //①
                // 将用户输入的用户名的前后增加协议字符串后发送
                ps.println(CrazyitProtocol.USER_ROUND + userName
                    + CrazyitProtocol.USER_ROUND);
                // 读取服务器的响应
                String result = brServer.readLine();
                // 如果用户重复,开始下次循环
                if (result.equals(CrazyitProtocol.NAME_REP))
                {
                    tip = "用户名重复!请重新";
                    continue;
                }
                // 如果服务器返回登录成功,结束循环
                if (result.equals(CrazyitProtocol.LOGIN_SUCCESS))
                {
                    break;
                }
            }
        }
        // 捕捉到异常,关闭网络资源,并退出该程序
        catch (UnknownHostException ex)
        {
            System.out.println("找不到远程服务器,请确定服务器已经启动!");
            closeRs();
            System.exit(1);
        }
        catch (IOException ex)
        {
            System.out.println("网络异常!请重新登录!");
            closeRs();
            System.exit(1);
        }
        // 以该Socket对应的输入流启动ClientThread线程
        new ClientThread(brServer).start();
    }
    // 定义一个读取键盘输出,并向网络发送的方法
    private void readAndSend()
    {
        try
        {
            // 不断读取键盘输入
            String line = null;
            while((line = keyIn.readLine()) != null)
            {
                // 如果发送的信息中有冒号,且以//开头,则认为想发送私聊信息
                if (line.indexOf(":") > 0 && line.startsWith("//"))
                {
                    line = line.substring(2);
                    ps.println(CrazyitProtocol.PRIVATE_ROUND +
                    line.split(":")[0] + CrazyitProtocol.SPLIT_SIGN
                        + line.split(":")[1] + CrazyitProtocol.PRIVATE_ROUND);
                }
                else
                {
                    ps.println(CrazyitProtocol.MSG_ROUND + line
                        + CrazyitProtocol.MSG_ROUND);
                }
            }
        }
        // 捕捉到异常,关闭网络资源,并退出该程序
        catch (IOException ex)
        {
            System.out.println("网络通信异常!请重新登录!");
            closeRs();
            System.exit(1);
        }
    }
    // 关闭Socket、输入流、输出流的方法
    private void closeRs()
    {
        try
        {
            if (keyIn != null)
            {
                ps.close();
            }
            if (brServer != null)
            {
                ps.close();
            }
            if (ps != null)
            {
                ps.close();
            }
            if (socket != null)
            {
                keyIn.close();
            }
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
    public static void main(String[] args)
    {
        Client client = new Client();
        client.init();
        client.readAndSend();
    }
}

客户端实现

public class Client
{
    private static final int SERVER_PORT = 30000;
    private Socket socket;
    private PrintStream ps;
    private BufferedReader brServer;
    private BufferedReader keyIn;
    public void init()
    {
        try
        {
            // 初始化代表键盘的输入流
            keyIn = new BufferedReader(
                new InputStreamReader(System.in));
            // 连接到服务器
            socket = new Socket("127.0.0.1", SERVER_PORT);
            // 获取该Socket对应的输入流和输出流
            ps = new PrintStream(socket.getOutputStream());
            brServer = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
            String tip = "";
            // 采用循环不断地弹出对话框要求输入用户名
            while(true)
            {
                String userName = JOptionPane.showInputDialog(tip
                    + "输入用户名");    //①
                // 将用户输入的用户名的前后增加协议字符串后发送
                ps.println(CrazyitProtocol.USER_ROUND + userName
                    + CrazyitProtocol.USER_ROUND);
                // 读取服务器的响应
                String result = brServer.readLine();
                // 如果用户重复,开始下次循环
                if (result.equals(CrazyitProtocol.NAME_REP))
                {
                    tip = "用户名重复!请重新";
                    continue;
                }
                // 如果服务器返回登录成功,结束循环
                if (result.equals(CrazyitProtocol.LOGIN_SUCCESS))
                {
                    break;
                }
            }
        }
        // 捕捉到异常,关闭网络资源,并退出该程序
        catch (UnknownHostException ex)
        {
            System.out.println("找不到远程服务器,请确定服务器已经启动!");
            closeRs();
            System.exit(1);
        }
        catch (IOException ex)
        {
            System.out.println("网络异常!请重新登录!");
            closeRs();
            System.exit(1);
        }
        // 以该Socket对应的输入流启动ClientThread线程
        new ClientThread(brServer).start();
    }
    // 定义一个读取键盘输出,并向网络发送的方法
    private void readAndSend()
    {
        try
        {
            // 不断读取键盘输入
            String line = null;
            while((line = keyIn.readLine()) != null)
            {
                // 如果发送的信息中有冒号,且以//开头,则认为想发送私聊信息
                if (line.indexOf(":") > 0 && line.startsWith("//"))
                {
                    line = line.substring(2);
                    ps.println(CrazyitProtocol.PRIVATE_ROUND +
                    line.split(":")[0] + CrazyitProtocol.SPLIT_SIGN
                        + line.split(":")[1] + CrazyitProtocol.PRIVATE_ROUND);
                }
                else
                {
                    ps.println(CrazyitProtocol.MSG_ROUND + line
                        + CrazyitProtocol.MSG_ROUND);
                }
            }
        }
        // 捕捉到异常,关闭网络资源,并退出该程序
        catch (IOException ex)
        {
            System.out.println("网络通信异常!请重新登录!");
            closeRs();
            System.exit(1);
        }
    }
    // 关闭Socket、输入流、输出流的方法
    private void closeRs()
    {
        try
        {
            if (keyIn != null)
            {
                ps.close();
            }
            if (brServer != null)
            {
                ps.close();
            }
            if (ps != null)
            {
                ps.close();
            }
            if (socket != null)
            {
                keyIn.close();
            }
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
    public static void main(String[] args)
    {
        Client client = new Client();
        client.init();
        client.readAndSend();
    }
}

服务端封装的 Map,为聊天室提供存储用户的功能

// 通过组合HashMap对象来实现CrazyitMap,CrazyitMap要求value也不可重复
public class CrazyitMap
{
    // 创建一个线程安全的HashMap
    public Map map = Collections.synchronizedMap(new HashMap());
    // 根据value来删除指定项
    public synchronized void removeByValue(Object value)
    {
        for (Object key : map.keySet())
        {
            if (map.get(key) == value)
            {
                map.remove(key);
                break;
            }
        }
    }
    // 获取所有value组成的Set集合
    public synchronized Set valueSet()
    {
        Set result = new HashSet();
        // 将map中所有value添加到result集合中
        map.forEach((key , value) -> result.add(value));
        return result;
    }
    // 根据value查找key。
    public synchronized K getKeyByValue(V val)
    {
        // 遍历所有key组成的集合
        for (K key : map.keySet())
        {
            // 如果指定key对应的value与被搜索的value相同,则返回对应的key
            if (map.get(key) == val || map.get(key).equals(val))
            {
                return key;
            }
        }
        return null;
    }
    // 实现put()方法,该方法不允许value重复
    public synchronized V put(K key,V value)
    {
        // 遍历所有value组成的集合
        for (V val : valueSet() )
        {
            // 如果某个value与试图放入集合的value相同
            // 则抛出一个RuntimeException异常
            if (val.equals(value)
                && val.hashCode()== value.hashCode())
            {
                throw new RuntimeException("MyMap实例中不允许有重复value!");
            }
        }
        return map.put(key , value);
    }
}

协议字符

public interface CrazyitProtocol
{
    // 定义协议字符串的长度
    int PROTOCOL_LEN = 2;
    // 下面是一些协议字符串,服务器和客户端交换的信息
    // 都应该在前、后添加这种特殊字符串。
    String MSG_ROUND = "§γ";
    String USER_ROUND = "∏∑";
    String LOGIN_SUCCESS = "1";
    String NAME_REP = "-1";
    String PRIVATE_ROUND = "★【";
    String SPLIT_SIGN = "※";
}

你可能感兴趣的:(基于多线程与 Socket 实现的聊天系统 v1.1(多线程、命令行、可记录用户信息))