Bootstrap

java 基于socket多线程的swing聊天室(完整版,附源码,带数据库存储)

目    录

设计需求 1

1.1 实现功能: 1

总体设计 1

1.1 实验环境准备: 11.2 实现流程 1

1.3 项目框架 2

详细设计 2

1.1实体类部分 2

1.1.1 Info类 2

1.1.2 User类 4

1.2客户端部分 4

1.1.1 登录部分 4

1.1.2 修改密码部分 5

1.1.3 注册部分 7

1.3工具类部分 7

1.1.1 RedisUtils工具类 7

1.1.2 SwingUtils工具类: 11

1.1.3 ToolUtils工具类 12

1.4服务器部分 14

1.1.1 静态内部类(线程类) 14

1.1.2 群聊部分 17

1.1.3 私聊部分 17

测试与分析 23

1.1 客户端启动界面: 23

1.2 通信面板界面: 24

1.3 服务端启动界面: 24

1.4 客户端群聊界面: 25

1.5 服务器面板: 25

1.6 客户端选择私聊对象: 26

1.7 客户端私聊发送文件: 26

1.8 客户端上传文件: 27

1.9 客户端之间私聊信息: 28

总结 28

参考文献 29

 

注意:

用的本地redis数据库,所以你需要现在本地安装redis数据库

完整工程在gitee中,链接https://gitee.com/zhupengchengzpc/chatroom.git

 

设计需求

实现功能:

客户端和客户端之间的消息传递(私聊)

客户端和客户端之间的文件发送(私聊)

客户端和服务器之间的消息传递(群聊)

总体设计

1.1 实验环境准备:

集成开发环境:IDEA

JDK:JDK>=9

实现流程

 

 

项目框架

Javabean目录:存储java实体类。

Info:通信的消息报文类,包括消息报文的类型、内容、发送者、接收者、附件。

User:客户端(用户)实体,字段为username,password,image,分别是用户名,密码,头像。

Service目录:主要的业务流程、界面,包括登录、注册等业务、广播聊天和私聊业务。

Starter:对外提供的启动类

Client:启动一个客户端

Server:启动服务器

Util:封装好基本操作的工具类

RedisUtils:完成和redis远程服务器的操作,主要存储用户名、密码、通信缓存等

SwingUtils:注册组件时候需要的工具类,封装了比如图标比例自适应、等比例设置面板大小等常用功能

ToolUtils:包括一些通用功能,比如获取系统时间、获得全局唯一日志对象,获得文件后缀名、读写文件、获得用户头像等。

详细设计

1.1实体类部分

1.1.1 Info类

  1. public class Info implements Serializable {  
  2.   
  3.     /** 
  4.      * 消息类型 
  5.      */  
  6.     private int type;  
  7.     /** 
  8.      * 消息内容 
  9.      */  
  10.     private String content;  
  11.     /** 
  12.      * 消息发送者用户名 
  13.      */  
  14.     private String source;  
  15.     /** 
  16.      * 消息接收者用户名 
  17.      */  
  18.     private String dest;  
  19.   
  20.     /** 
  21.      * 文件名 
  22.      */  
  23.     private String fileName;  
  24.   
  25.     /** 
  26.      * 附件 
  27.      */  
  28.     private List attach;  
  29.     ... 
  30.     Get
  31.     Set
  32.     toString 
  33. }  

 

这个类代表整个通信过程传输的报文,其中,type字段最为关键,它决定了发送报文的类型,不同的值代表不同的类型,接受方收到报文后,解析出其中的type字段,根据不同的值执行不同的操作。

 

Type字段对应类型如下:

  1. /** 
  2.  * 服务器和客户端之间的消息协议 
  3.  * 0-CS第一次握手信息 
  4.  * 1-client请求广播信息 
  5.  * 2-保留 
  6.  * 3-client请求上传文件 
  7.  * 4-client查询在线人数 
  8.  * 5-server返回系统在线人数 
  9.  * 6-server返回反馈消息 
  10.  * 7-client查询在线用户列表 
  11.  * 8-server返回在线用户列表 
  12.  * 9-client私聊发送信息 
  13.  * 10-client私聊发送文件 
  14.  * 11-server转发私聊文件 
  15.  * @author zhupengcheng 
  16.  */  

 

 

1.1.2 User类

  1. public class User implements Serializable {  
  2.   
  3.     /** 
  4.      * 用户名 
  5.      */  
  6.     private String username;  
  7.     /** 
  8.      * 密码 
  9.      */  
  10.     private String password;  
  11.     /** 
  12.      * 头像 
  13.      */  
  14.     private String image;  
  15.     ......  
  16.     get  
  17.     set  
  18.     toString  
  19. }  

 

这个类代表用户实体,主要用在登录、注册流程中,及其登录、注册业务跳转到通信业务中,我需要传递一个user对象,记录这个用户(客户端)的一些信息。

 

 

 

 

 

1.2客户端部分

1.1.1 登录部分

  1. /** 
  2.          * 登录点击事件 
  3.          */  
  4.         login_btn.addActionListener(new ActionListener() {  
  5.             @Override  
  6.             public void actionPerformed(ActionEvent e) {  
  7.                 String username = username_edit.getText().trim();  
  8.                 String password = new String(password_edit.getPassword());  
  9.                 if (username == null || password == null) {  
  10.                     JOptionPane.showMessageDialog(null,"用户名、密码不许为空!""用户登录", JOptionPane.INFORMATION_MESSAGE);  
  11.                 }  
  12.                 final int recv = RedisUtils.validUser(username, password);  
  13.                 if (recv == 0) {  
  14.                     JOptionPane.showMessageDialog(null,"用户不存在,请先注册!""用户登录", JOptionPane.INFORMATION_MESSAGE);  
  15.                     return;  
  16.                 }  
  17.                 if (recv == 1) {  
  18.                     JOptionPane.showMessageDialog(null,"密码错误!""用户登录", JOptionPane.INFORMATION_MESSAGE);  
  19.                     return;  
  20.                 }  
  21.                 if (RedisUtils.isAlive(username)) {  
  22.                     JOptionPane.showMessageDialog(null,"你已经登录了,不能重复登录!""用户登录", JOptionPane.INFORMATION_MESSAGE);  
  23.                     return;  
  24.                 }  
  25.                 JOptionPane.showMessageDialog(null,"登录成功!""用户登录", JOptionPane.INFORMATION_MESSAGE);  
  26.                 final User user = new User();  
  27.                 user.setUsername(username);  
  28.                 user.setUsername(password);  
  29.                 user.setImage(ToolUtils.getProfile());  
  30.                 ProfileFrame.launch(user);  
  31.                 dispose();  
  32.             }  
  33.         }); 

首先,判断输入的用户名、密码是否为空,如果为空,给出提示;如果不为空,再判断用户名是否存在于数据库中,没有不存在于数据库中说明没有注册,给出提示;如果存在,再判断密码与数据库中存储的密码是否一致,如果不一致,给出提示,如果一致,再判断用户是否已经登录了,如果用户在线那么数据库缓存列表有记录,如果已经登录成功了,给出提示,防止重复登录,如果没有,那么登录,跳转到聊天室页面。

 

 

 

 

 

 

 

 

1.1.2 修改密码部分

  1. /** 
  2.          * 修改密码点击事件 
  3.          */  
  4.         modify_btn.addActionListener(new ActionListener() {  
  5.             @Override  
  6.             public void actionPerformed(ActionEvent e) {  
  7.                 String username = username_edit.getText().trim();  
  8.                 String password = new String(password_edit.getPassword());  
  9.                 if (username == null || password == null) {  
  10.                     JOptionPane.showMessageDialog(null,"用户名、密码不许为空!""修改密码", JOptionPane.INFORMATION_MESSAGE);  
  11.                 }  
  12.                 final int recv = RedisUtils.validUser(username, password);  
  13.                 if (recv == 0) {  
  14.                     JOptionPane.showMessageDialog(null,"用户不存在,请先注册!""修改密码", JOptionPane.INFORMATION_MESSAGE);  
  15.                     return;  
  16.                 }  
  17.                 if (recv == 1) {  
  18.                     JOptionPane.showMessageDialog(null,"密码错误!""修改密码", JOptionPane.INFORMATION_MESSAGE);  
  19.                     return;  
  20.                 }  
  21.                 final String newPassword = (String) JOptionPane.showInputDialog(null"请输入新密码""修改密码", JOptionPane.INFORMATION_MESSAGE, nullnullnull);  
  22.                 System.out.println(newPassword);  
  23.                 if (StringUtils.isBlank(newPassword)) {  
  24.                     JOptionPane.showMessageDialog(null,"新密码不许为空!""修改密码", JOptionPane.INFORMATION_MESSAGE);  
  25.                     return;  
  26.                 }  
  27.                 RedisUtils.modifyUser(username, newPassword);  
  28.             }  
  29.         });  

 

首先,判断输入的用户名、密码是否为空,如果为空,给出提示;如果不为空,判断用户名是否存在于数据库中,如果不存在,给出注册的都提示;如果存在,继续判断密码,如果与数据库中存储的密码不一致,给出提示;如果一致,给出输入对话框,读取用户输入的新密码,这里继续判断新密码的合理性,如果为空,给出提示;如果和旧密码一样,给出提示;如果全是空格,给出提示;符合要求后存储到数据库中。

 

1.1.3 注册部分

  1. /** 
  2.         * 注册点击事件 
  3.         */  
  4.        register_btn.addActionListener(new ActionListener() {  
  5.            @Override  
  6.            public void actionPerformed(ActionEvent e) {  
  7.                String username = username_edit.getText().trim();  
  8.                String password = new String(password_edit.getPassword());  
  9.                if (username == null || password == null) {  
  10.                    JOptionPane.showMessageDialog(null,"用户名、密码不许为空!""注册", JOptionPane.INFORMATION_MESSAGE);  
  11.                }  
  12.                final int recv = RedisUtils.insertUser(username, password);  
  13.                if (recv == 0) {  
  14.                    JOptionPane.showMessageDialog(null,"用户已存在,请直接登录!""注册", JOptionPane.INFORMATION_MESSAGE);  
  15.                }  
  16.                if (recv == 1) {  
  17.                    JOptionPane.showMessageDialog(null,"注册失败!""注册", JOptionPane.INFORMATION_MESSAGE);  
  18.                }  
  19.                if (recv == 2 ) {  
  20.                    JOptionPane.showMessageDialog(null,"注册成功!""注册", JOptionPane.INFORMATION_MESSAGE);  
  21.                }  
  22.            }  
  23.        });  

 

这里首先判断用户输入的用户名、密码是否为空,如果为空,给出提示;然后判断用户名是否已经存在于数据库中,如果已经存在,给出可以直接登录的提示;这里面新用户的注册细节全部由自己封装的redis数据库工具类完成,只需要保存调用函数返回的结果进行判断即可。

 

1.3工具类部分

1.1.1 RedisUtils工具类

  1. public class RedisUtils {  
  2.     /** 
  3.      * redis操作实体 
  4.      */  
  5.     private static Jedis jedis;  
  6.     /** 
  7.      * 远程服务器ip地址 
  8.      */  
  9.     private static final String HOST = "101.37.32.165";  
  10.     /** 
  11.      * 操作正确的返回值 
  12.      */  
  13.     private static final String OK = "OK";  
  14.     /** 
  15.      * 存储在线用户的集合 
  16.      */  
  17.     private static String ALIVEUSERS = "aliveUsers";  
  18.   
  19.     /** 
  20.      * 建立连接 
  21.      */  
  22.     public static Jedis init() {  
  23.         jedis = new Jedis(HOST);  
  24.         jedis.select(1);  
  25.         ToolUtils.getLogger().info(ToolUtils.getRealTime() + "\t" + jedis.ping());  
  26.         return jedis;  
  27.     }  
  28.   
  29.     /** 
  30.      * 验证用户 
  31.      * @param username 用户名 
  32.      * @return 0-用户不存在,1-用户存在,密码错误,2-用户存在,密码正确 
  33.      */  
  34.     public static int validUser(String username, String password) {  
  35.         if (jedis == null) {  
  36.             RedisUtils.init();  
  37.         }  
  38.         if (! isExists(username)) {  
  39.             return 0;  
  40.         }  
  41.         String recv = jedis.get(username);  
  42.         if (! Objects.equals(recv,password)) {  
  43.             return 1;  
  44.         }  
  45.         return 2;  
  46.     }  
  47.   
  48.     /** 
  49.      * 验证用户名是否存在 
  50.      * @param username 用户名 
  51.      * @return 
  52.      */  
  53.     private static boolean isExists(String username) {  
  54.         if (jedis == null) {  
  55.             RedisUtils.init();  
  56.         }  
  57.         if (username == null) {  
  58.             return false;  
  59.         }  
  60.         return jedis.exists(username);  
  61.     }  
  62.   
  63.     /** 
  64.      * 注册用户 
  65.      * @param username 用户名 
  66.      * @param password 密码 
  67.      * @return 0-用户已存在,1-注册失败,2-注册成功 
  68.      */  
  69.     public static int insertUser(String username, String password) {  
  70.         if (jedis == null) {  
  71.             RedisUtils.init();  
  72.         }  
  73.         if (isExists(username)) {  
  74.             return 0;  
  75.         }  
  76.         final String recv = jedis.set(username, password);  
  77.         if (OK.equals(recv)) {  
  78.             return 2;  
  79.         }  
  80.         return 1;  
  81.     }  
  82.   
  83.     /** 
  84.      * 修改密码 
  85.      * @param username 用户名 
  86.      * @param password  老密码 
  87.      * @return 
  88.      */  
  89.     public static void modifyUser(String username, String password) {  
  90.         if (jedis == null) {  
  91.             RedisUtils.init();  
  92.         }  
  93.         jedis.set(username, password);  
  94.     }  
  95.   
  96.     /** 
  97.      * 关闭连接 
  98.      */  
  99.     public static void close() {  
  100.         if (jedis == null) {  
  101.             return;  
  102.         }  
  103.         if (jedis.isConnected()) {  
  104.             jedis.close();  
  105.         }  
  106.     }  
  107.   
  108.   
  109.     /** 
  110.      * 增添在线用户缓存 
  111.      * @param username 用户名 
  112.      */  
  113.     public static long addAliveUser(String username) {  
  114.         if (jedis == null) {  
  115.             RedisUtils.init();  
  116.         }  
  117.         jedis.select(1);  
  118.         return jedis.sadd(ALIVEUSERS, username);  
  119.     }  
  120.   
  121.     /** 
  122.      * 删除在线用户缓存 
  123.      * @param username 用户名 
  124.      */  
  125.     public static long delAliveUser(String username) {  
  126.         if (jedis == null) {  
  127.             RedisUtils.init();  
  128.         }  
  129.         jedis.select(1);  
  130.         return jedis.srem(ALIVEUSERS, username);  
  131.     }  
  132.   
  133.     /** 
  134.      * 获得在线用户缓存 
  135.      * @return 缓存集合 
  136.      */  
  137.     public static Set getAliveUsers() {  
  138.         if (jedis == null) {  
  139.             RedisUtils.init();  
  140.         }  
  141.         jedis.select(1);  
  142.         return jedis.smembers(ALIVEUSERS);  
  143.     }  
  144.   
  145.     /** 
  146.      * 验证用户是否已登录 
  147.      * @param username 用户名 
  148.      * @return true-用户已经登录,false-用户未登录 
  149.      */  
  150.     public static boolean isAlive(String username) {  
  151.         if (jedis == null) {  
  152.             RedisUtils.init();  
  153.         }  
  154.         return jedis.sismember(ALIVEUSERS, username);  
  155.     }  
  156. }  

 

这个工具类完成了与数据库交互的全部操作,包括用户账号的验证、保存、修改,在线用户信息的缓存,防止重复登录的验证工作等,类似于web业务中的DAO层,封装与数据库的交互。

 

 

 

1.1.2 SwingUtils工具类:

  1. public class SwingUtils {  
  2.   
  3.     /** 
  4.      * 设置大小为电脑屏幕的指定比例 
  5.      * @param jFrame 窗口 
  6.      * @param w 电脑屏幕宽度的w倍,w<=1 
  7.      * @param h 电脑屏幕高度的h倍,h<=1 
  8.      */  
  9.     public static void setSize(JFrame jFrame, double w, double h){  
  10.         int width = (int) (getScreenWidth() * w);  
  11.         int height = (int) (getScreenHeight() * h);  
  12.         jFrame.setSize(width, height);  
  13.     }  
  14.   
  15.     /** 
  16.      * 获得电脑屏幕宽度 
  17.      * @return 
  18.      */  
  19.     public static double getScreenWidth() {  
  20.         return Toolkit.getDefaultToolkit().getScreenSize().getWidth();  
  21.     }  
  22.   
  23.     /** 
  24.      * 获得电脑屏幕宽度 
  25.      * @return 
  26.      */  
  27.     public static double getScreenHeight() {  
  28.         return Toolkit.getDefaultToolkit().getScreenSize().getHeight();  
  29.     }  
  30.   
  31.     /** 
  32.      * 按照width、height比例调整图标大小 
  33.      * @param imageIcon 要调整的图标 
  34.      * @param width 调整后的图像宽度 
  35.      * @param height 调整后的图像高度 
  36.      * @return 
  37.      */  
  38.     public static ImageIcon scaleImageIcon(ImageIcon imageIcon, double width, double height) {  
  39.         imageIcon.setImage(imageIcon.getImage().getScaledInstance((int)width, (int)height, Image.SCALE_SMOOTH));  
  40.         return imageIcon;  
  41.     }  
  42. }  

 

这个类我在swing控件注册时候用的比较多,因为重复性地使用一些功能,我就索性把重复工作封装成了一个工具类,方便直接调用。主要就是图片大小自适应调整为父控件的大小,将界面大小调整为电脑屏幕的一定比例。总体来说,这部分不太重要。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

1.1.3 ToolUtils工具类

 

 

这里面封装的方法比较多,但是大同小异,我就只把文件读取、写入这部分拿出来说一下把,其他关于时间的一些方法都是类似的。

 

  1. /** 
  2.     * 编码文件成二进制流 
  3.     * @param file 文件对象 
  4.     * @return 文件字节流 
  5.     * @throws FileNotFoundException 
  6.     */  
  7.    public static byte[] codeFile(File file) throws IOException {  
  8.        try (final FileInputStream fileInputStream = new FileInputStream(file))  
  9.        {  
  10.            return fileInputStream.readAllBytes();  
  11.        }  
  12.    }  
  13.   
  14.    /** 
  15.     * 解码二进制成文件,并写入内容 
  16.     * @param content 文件内容 
  17.     * @param absolutePath 绝对路径 
  18.     * @return 
  19.     */  
  20.    public static void decodeFile(String content, String absolutePath) throws IOException {  
  21.        final File file = Path.of(absolutePath).toFile();  
  22.        if (! file.exists()) {  
  23.            file.createNewFile();  
  24.        }  
  25.        try (final FileOutputStream fileOutputStream = new FileOutputStream(file,true))  
  26.        {  
  27.            fileOutputStream.write(content.getBytes());  
  28.            fileOutputStream.flush();  
  29.        }  
  30.    }  

 

在通信时候难免会发送文件,我就把里面的细节封装成了方法,第一个是解码文件成二进制流,方便直接socket传输。用的就是FileInputStream字节流,没有用封装后的缓冲字符流。第二个是解码,就是根据路径生成文件,并且将字符串写入文件中。在项目中经过尝试,发现用绝对路径稳妥些,如果用相对路径的话我这边是默认保存在D盘下的指定文件的。两个函数都用了try-with-resource,为没有用try-catch,省去了释放资源这一步。

 

 

 

1.4服务器部分

这一部分算是最复杂的了,我将这个代码分割成几块来单独说明,比较清楚。

 

1.1.1 静态内部类(线程类)

  1. /** 
  2.     * 内部静态类,负责与单独的客户端线程的通信工作 
  3.     */  
  4.    private static class SingleUser extends Thread {  
  5.        /** 
  6.         * 服务端与客户端对应的套接字 
  7.         */  
  8.        private Socket socket;  
  9.        /** 
  10.         * 缓冲字符输入流 
  11.         */  
  12.        private BufferedReader bufferedReader;  
  13.        /** 
  14.         * 文本输出流 
  15.         */  
  16.        private PrintWriter printWriter;  
  17.        /** 
  18.         * 用户名 
  19.         */  
  20.        private String id;  
  21.   
  22.        public SingleUser(Socket socket) {  
  23.            this.socket = socket;  
  24.            // 在线人数+1  
  25.            userCount.incrementAndGet();  
  26.        }  
  27.   
  28.        @Override  
  29.        public void run() {  
  30.            try {  
  31.                printWriter = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);  
  32.                bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));  
  33.                String recv = null;  
  34.                while (socket.isConnected()) {  
  35.                    while ( (recv = bufferedReader.readLine()) != null ) {  
  36.                        final JSONObject jsonObject = JSON.parseObject(recv);  
  37.                        final int type = jsonObject.getIntValue("type");  
  38.                        // 客户端、服务器第一次握手  
  39.                        if (type == 0) {  
  40.                            final String username = jsonObject.getString("source");  
  41.                            id = username;  
  42.                            System.out.println("-----------------------");  
  43.                            System.out.println(id);  
  44.                            userMap.put(username, printWriter);  
  45.                            // 添加在线用户进缓存  
  46.                            RedisUtils.addAliveUser(id);  
  47.                            textArea.append(ToolUtils.getRealTime() + ",用户" + username + "连接成功\n");  
  48.                            // 回复  
  49.                            final Info info = new Info(6);  
  50.                            info.setContent(ToolUtils.getRealTime() + ",用户" + username + "连接成功");  
  51.                            printWriter.println(JSON.toJSONString(info));  
  52.                        }  
  53.                        // 客户端请求广播消息  
  54.                        if (type == 1) {  
  55.                            final String content = jsonObject.getString("content");  
  56.                            final String username = jsonObject.getString("source");  
  57.                            broadcast(content, username);  
  58.                            textArea.append(ToolUtils.getRealTime() + ",用户" + username + "广播了消息\n");  
  59.                        }  
  60.                        // 客户端上传文件  
  61.                        if (type == 3) {  
  62.                            final String content = jsonObject.getString("content");  
  63.                            final String username = jsonObject.getString("source");  
  64.                            final String fileName = (String) jsonObject.get("fileName");  
  65.                            // 服务器保存文件制定路径  
  66.                            String path = rootPath + "files\\" + username + "\\" + ToolUtils.getRealTime("YY_MM_dd_HH_mm_ss") + "_" + fileName;  
  67.                            System.out.println(path);  
  68.                            ToolUtils.decodeFile(content, path);  
  69.                            textArea.append(ToolUtils.getRealTime() + ",用户" + username + "上传了文件\n");  
  70.                            // 回复  
  71.                            final Info info = new Info(6);  
  72.                            info.setContent(ToolUtils.getRealTime() + ",用户" + username + "上传了文件" + fileName);  
  73.                            printWriter.println(JSON.toJSONString(info));  
  74.                        }  
  75.                        // 客户端查询在线人数  
  76.                        if (type == 4) {  
  77.                            // 回复  
  78.                            final Info info = new Info(5);  
  79.                            info.setContent(String.valueOf(userCount.get()));  
  80.                            printWriter.println(JSON.toJSONString(info));  
  81.                        }  
  82.                        // 客户端请求转发私聊信息  
  83.                        if (type == 9) {  
  84.                            final String content = jsonObject.getString("content");  
  85.                            final String dest = jsonObject.getString("dest");  
  86.                            final String source = jsonObject.getString("source");  
  87.                            transferData(source, content, dest);  
  88.                        }  
  89.                        // 客户端私聊文件  
  90.                        if (type == 10) {  
  91.                            final String content = jsonObject.getString("content");  
  92.                            final String dest = jsonObject.getString("dest");  
  93.                            final String source = jsonObject.getString("source");  
  94.                            final String fileName = (String) jsonObject.get("fileName");  
  95.                            transferDoc(source, content, dest, fileName);  
  96.                        }  
  97.                    }  
  98.                }  
  99.            } catch (IOException e) {  
  100.                // 不给出提示框,直接结束方便finally直接执行  
  101.                return;  
  102.            } finally {  
  103.                // 删除用户缓存  
  104.                userMap.remove(id);  
  105.                RedisUtils.delAliveUser(id);  
  106.                // 在线人数-1  
  107.                userCount.decrementAndGet();  
  108.            }  
  109.        }  
  110.    }  

 

 

我选用了多线程技术,这里服务器监听到一个客户端连接请求后,实例化一个线程类负责和这个客户端的通信,这个静态内部类就是这个线程类,首先,定义了socket字段、输入流、输出流字段,这些都是socket通信基本具备的,然后就是id,当客户端和服务器建立连接后,客户端会发起第一次握手,将自己的用户名发送给服务器,然后该线程类对象就将用户名赋值给id字段,表示对应客户端的唯一标识,方便后续管理。可以看到run方法里有很多对于type字段的判断,这里就是根据我之前定义的Info报文类的type字段,来是被这个报文是什么类型的,即具体要我做什么工作,然后直接去执行对应工作即可。

 

 

 

 

1.1.2 群聊部分

  1.                          // 客户端请求广播消息  
  2.                         if (type == 1) {  
  3.                             final String content = jsonObject.getString("content");  
  4.                             final String username = jsonObject.getString("source");  
  5.                             broadcast(content, username);  
  6.                             textArea.append(ToolUtils.getRealTime() + ",用户" + username + "广播了消息\n");  
  7.                         }  

 

这里服务器首先判断type字段,如果是1,表示客户端请求服务器转发群聊的内容,也就是通过服务器来广播消息。

实现的细节都封装成了broadcast方法,具体如下:

 

  1. /** 
  2.      * 服务器转发客户端广播消息 
  3.      * @param sendData 广播内容 
  4.      * @param sender 发送者 
  5.     */  
  6.     private static void broadcast(String sendData, String sender) {  
  7.         final Collection values = userMap.values();  
  8.         for (Object value : values) {  
  9.             PrintWriter printWriter = (PrintWriter) value;  
  10.             // 回复  
  11.             final Info info = new Info(6);  
  12.             info.setContent(ToolUtils.getRealTime() + ",用户" + sender + ",广播了消息:" + sendData);  
  13.             info.setSource(sender);  
  14.             printWriter.println(JSON.toJSONString(info));  
  15.         }  
  16.     }  

 

Broadcast方法需要两个参数,一个是群聊的内容,一个是发送者,即消息来源者。这里有一个userMap,这是一个key-value键值对,key是用户名,值是用户名对应的socket输出流,这里的用户是在线用户(已登录),通过遍历集合,依次发送给每个用户。

 

 

 

 

1.1.3 私聊部分

  1. // 客户端请求转发私聊信息  
  2.                         if (type == 9) {  
  3.                             final String content = jsonObject.getString("content");  
  4.                             final String dest = jsonObject.getString("dest");  
  5.                             final String source = jsonObject.getString("source");  
  6.                             transferData(source, content, dest);  
  7.                         }  

 

依然是先判断type字段(报文类型),明白了这个请求报文要让我做什么,然后执行相应操作,这里面转发客户端的私聊信息同样是封装在一个方法中,具体方法代码如下:

 

  1. /** 
  2.      * 用户私聊发送数据 
  3.      * @param sendData 私聊内容 
  4.      * @param dest 私聊用户名 
  5.      * @param username 发送用户名 
  6.      */  
  7.     private static void transferData(String username, String sendData, String dest) {  
  8.         final PrintWriter sourceWriter = (PrintWriter) userMap.get(username);  
  9.         final PrintWriter destWriter = (PrintWriter) userMap.get(dest);  
  10.         // 私聊对象已下线  
  11.         if (destWriter == null) {  
  12.             // 私聊发起人在线  
  13.             if (sourceWriter != null) {  
  14.                 final Info info = new Info(6);  
  15.                 info.setContent(ToolUtils.getRealTime() + ",抱歉,用户" + dest + ",已下线,请稍候重发");  
  16.                 sourceWriter.println(JSON.toJSONString(info));  
  17.                 textArea.append(ToolUtils.getRealTime() + ",用户" + username + "和" + dest + "私聊失败\n");  
  18.             }  
  19.         }  
  20.         // 私聊对象在线  
  21.         else {  
  22.             final Info info = new Info(6);  
  23.             info.setContent(ToolUtils.getRealTime() + ",用户" + username + ",私聊你如下内容:" + sendData);  
  24.             destWriter.println(JSON.toJSONString(info));  
  25.             textArea.append(ToolUtils.getRealTime() + ",用户" + username + "和" + dest + "私聊成功\n");  
  26.             // 私聊发起人在线  
  27.             if (sourceWriter != null) {  
  28.                 final Info info1 = new Info(6);  
  29.                 info1.setContent(ToolUtils.getRealTime() + ",你刚刚私聊给用户" + dest + "已发送成功");  
  30.                 sourceWriter.println(JSON.toJSONString(info1));  
  31.             }  
  32.         }  
  33.     }  

 

根据私聊对象是否在线,分成两个逻辑。如果私聊的对象恰好刚刚下线,这时候服务器的在线用户缓存中没有了该用户,同时,私聊发起人还在线,这时服务器就通知私聊发起人你发送的消息转发失败,私聊对象已经下线了。相反,如果他们都在线,服务器就转发消息给私聊发起人,并且告知私聊发起者你发送的消息已经成功发送出去了。

 

 

 

如果用户想要私聊发送文件呢?

具体看下面:

私聊发送文件:

  1.                         // 客户端私聊发送文件  
  2.                        if (type == 10) {  
  3.                            final String content = jsonObject.getString("content");  
  4.                            final String dest = jsonObject.getString("dest");  
  5.                            final String source = jsonObject.getString("source");  
  6.                            final String fileName = (String) jsonObject.get("fileName");  
  7.                            transferDoc(source, content, dest, fileName);  
  8.                        }  

对应的字段规定是0,同样,操作细节封装在transDoc方法中,该方法代码如下:

 

  1. /** 
  2.      * 用户私聊发送文件 
  3.      * @param source 发送用户名 
  4.      * @param fileContent 文件内容 
  5.      * @param dest 私聊用户名 
  6.      * @param fileName 文件名 
  7.      */  
  8.     private static void transferDoc(String source, String fileContent, String dest, String fileName) {  
  9.         final PrintWriter destWriter = (PrintWriter) userMap.get(dest);  
  10.         final PrintWriter sourceWriter = (PrintWriter) userMap.get(source); 
  11.         // 私聊对象已下线  
  12.         if (destWriter == null) {  
  13.             // 私聊发起人在线 
  14.             if (sourceWriter != null) {  
  15.                 final Info info1 = new Info(6);  
  16.                 info1.setContent(ToolUtils.getRealTime() + ",你私聊发送文件失败,用户" + dest + "已下线");  
  17.                 sourceWriter.println(JSON.toJSONString(info1));  
  18.                 textArea.append(ToolUtils.getRealTime() + ",用户" + source + "和" + dest + "私聊文件失败\n");  
  19.             }  
  20.         }  
  21.         // 私聊对象在线
  22.         else {  
  23.             final Info info = new Info(11);  
  24.             info.setDest(dest);  
  25.             info.setContent(fileContent);  
  26.             info.setSource(source);  
  27.             info.setFileName(fileName);  
  28.             destWriter.println(JSON.toJSONString(info));  
  29.              // 私聊发起人在线 
  30.             if (sourceWriter != null) {  
  31.                 final Info info1 = new Info(6);  
  32.                 info1.setContent(ToolUtils.getRealTime() + ",你私聊发送文件给用户" + dest + "成功");  
  33.                 sourceWriter.println(JSON.toJSONString(info1));  
  34.                 textArea.append(ToolUtils.getRealTime() + ",用户" + source + "和" + dest + "私聊文件成功\n");  
  35.             }  
  36.         }  
  37.     }  

 

和私聊发送数据一个逻辑,根据私聊对象是否在线分成两个逻辑,首先如果私聊对象不在线,那么文件发送失败,服务器告诉发起者(前提是发起者在线),如果私聊对象在线,同时发起者也在线,那么告知发起者你发送成功了。其实,这里面有一点不合理,因为只有接收者完成接收到文件,并且发出一个反馈报文,发起者如果收到这个报文,表示文件发送成功了,因为时间所限,后面如果可以的话可以继续改进。

如果接收到接收到了文件,那么他怎么处理?具体处理逻辑如下:

  1.                                            // 服务器转发私聊文件  
  2.                                         if (type == 11) {  
  3.                                             final String source = jsonObject.getString("source");  
  4.                                             final String fileContent = jsonObject.getString("content");  
  5.                                             final String fileName = jsonObject.getString("fileName");  
  6.                                             ToolUtils.decodeFile(fileContent, FileSystemView.getFileSystemView().getHomeDirectory() + "\\" + ToolUtils.getRealTimeByHour("HH_mm_ss") + "_" + source + "_" + fileName);  
  7.                                             System.out.println("接收" + source + "私聊文件" + fileContent);  
  8.                                         }  

 

如果报文type字段是11,表示这是服务器转发的私聊文件,也就是有别人私发文件给你,这时候你需要从json格式中提取出各个字段,比如文件数据,文件名等。根据这些参数,调用之前介绍的工具类方法decodeFile创建对应的文件,并且把数据写入文件中。由于该方法在前面介绍过,这里便省略了。我设置的默认保存路径是该客户端的桌面。

 

 

 

 

 

具体介绍了自己定义的主要几个报文,其实在整个通信过程中还有一些请求报文,比如

客户端查询服务器当前聊天室在线人数,客户端查询当前在线的用户列表,客户端获取这些数据后可以实时地显示在聊天室界面上,方便用户的操作,这几部分所占代码较少。

 

这里,我再介绍我的私聊部分中关于选择私聊对象的处理逻辑,具体如下:

 

  1.          /** 
  2.          * 选择私聊用户点击事件 
  3.          */  
  4.         list.addListSelectionListener(new ListSelectionListener() {  
  5.             @Override  
  6.             public void valueChanged(ListSelectionEvent e) {  
  7.                 final Object value = list.getSelectedValue();  
  8.                 if (value != null) {  
  9.                     final int recv = JOptionPane.showConfirmDialog(null, user.getUsername() + ",你要选择" + value + "为私聊对象吗""选择好友私聊", JOptionPane.YES_NO_OPTION);  
  10.                     // 选择成功  
  11.                     if (recv == 0) {  
  12.                         if (user.getUsername().equals(value)) {  
  13.                             JOptionPane.showMessageDialog(null"不能选择你自己""选择私聊对象", JOptionPane.WARNING_MESSAGE);  
  14.                             return;  
  15.                         }  
  16.                         secretUser = (String) value;  
  17.                         secret_edit.setText(secretUser);  
  18.                         list.setEnabled(false);  
  19.                     }  
  20.                 }  
  21.             }  
  22.         });  

 

这里面用到的控件是JList列表,会实时显示当前系统用户,当数量过多时候,通过下拉框下拉即可。上面的代码是定义了列表区域的监听事件,当列表区域中的选择项(在线用户名)改变时,会询问客户端是否选择该用户为当前私聊对象,如果确认,就把私聊的用户名保存在变量secret_user中,后面私聊时候根据该用户名通过服务器转发到指定的用户。

 

 

 

 

 

客户端查询当前系统在线人数:

 

  1. /** 
  2.          * 定时执行,查询在线人数 
  3.          */  
  4.         final ActionListener actionListener = new ActionListener() {  
  5.             @Override  
  6.             public void actionPerformed(ActionEvent e) {  
  7.                 if (printWriter != null) {  
  8.                     // 空报头即可  
  9.                     final Info info = new Info(4);  
  10.                     printWriter.println(JSON.toJSONString(info));  
  11.                 }  
  12.                 count_edit.setText(String.valueOf(counts.get()));  
  13.             }  
  14.         };  
  15.         new Timer(3000, actionListener).start();  

 

这里定义了一个定时器,每3秒发送一个请求报文到服务器查询人数,然后服务器将保存人数的变量赋值给报文类Info对象的content字段,返回给发送者即可。为了保证多线程访问时保持变量的一致性,我定义了AtomicInteger类型变量存储数值,保证用户上线、下线时候变量增减的原子性。

 

 

 

测试与分析

1.1 客户端启动界面:

 

 

 

1.2 通信面板界面:

 

1.3 服务端启动界面:

 

 

1.4 客户端群聊界面:

4个客户端:

 

 

 

 

 

 

 

1.5 服务器面板:

 

 

 

 

 

1.6 客户端选择私聊对象:

 

 

 

 

 

 

1.7 客户端私聊发送文件:

 

 

 

 

1.8 客户端上传文件:

文件选择器:

 

 

 

服务器接收客户端文件,保存在项目路径下files/用户名目录下

 

1.9 客户端之间私聊信息:

客户端zyk私聊客户端zpc,客户端zpc接收到私聊内容,而客户端fd没有接收到他们之间的私聊内容

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

;