基于多线程与 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<String , PrintStream> 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<K,V>
{
    // 创建一个线程安全的HashMap
    public Map<K ,V> map = Collections.synchronizedMap(new HashMap<K,V>());
    // 根据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<V> valueSet()
    {
        Set<V> result = new HashSet<V>();
        // 将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 = "※";
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,659评论 0 15
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,084评论 1 32
  • 一、简历准备 1、个人技能 (1)自定义控件、UI设计、常用动画特效 自定义控件 ①为什么要自定义控件? Andr...
    lucas777阅读 5,186评论 2 54
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 今年的三八节恰逢二月二,龙凤节,大家似乎都很兴奋!我早早起来修饰一下,还破天慌的带了一副耳环,因为我们也有活动,与...
    81c9dcfe55b5阅读 339评论 0 0