设计需求 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
注意:登录账号:
1.账号:zpc 密码:zpc
2.账号:fd 密码:fd
3.用的redis数据库,你也可以在swing登录界面直接注册
完整工程在gitee中,链接https://gitee.com/zhupengchengzpc/chatroom.git
设计需求
实现功能:
客户端和客户端之间的消息传递(私聊)
客户端和客户端之间的文件发送(私聊)
客户端和服务器之间的消息传递(群聊)
总体设计
1.1 实验环境准备:
编译器:IDEA
JDK:jdk14.0(jdk9.0即可运行)
实现流程
项目框架
Javabean目录:存储java实体类。
Info:通信的消息报文类,包括消息报文的类型、内容、发送者、接收者、附件。
User:客户端(用户)实体,字段为username,password,image,分别是用户名,密码,头像。
Service目录:主要的业务流程、界面,包括登录、注册等业务、广播聊天和私聊业务。
Starter:对外提供的启动类
Client:启动一个客户端
Server:启动服务器
Util:封装好基本操作的工具类
RedisUtils:完成和redis远程服务器的操作,主要存储用户名、密码、通信缓存等
SwingUtils:注册组件时候需要的工具类,封装了比如图标比例自适应、等比例设置面板大小等常用功能
ToolUtils:包括一些通用功能,比如获取系统时间、获得全局唯一日志对象,获得文件后缀名、读写文件、获得用户头像等。
详细设计
1.1实体类部分
1.1.1 Info类
- public class Info implements Serializable {
-
- /**
- * 消息类型
- */
- private int type;
- /**
- * 消息内容
- */
- private String content;
- /**
- * 消息发送者用户名
- */
- private String source;
- /**
- * 消息接收者用户名
- */
- private String dest;
-
- /**
- * 文件名
- */
- private String fileName;
-
- /**
- * 附件
- */
- private List attach;
- ...
- Get
- Set
- toString
- }
这个类代表整个通信过程传输的报文,其中,type字段最为关键,它决定了发送报文的类型,不同的值代表不同的类型,接受方收到报文后,解析出其中的type字段,根据不同的值执行不同的操作。
Type字段对应类型如下:
- /**
- * 服务器和客户端之间的消息协议
- * 0-CS第一次握手信息
- * 1-client请求广播信息
- * 2-保留
- * 3-client请求上传文件
- * 4-client查询在线人数
- * 5-server返回系统在线人数
- * 6-server返回反馈消息
- * 7-client查询在线用户列表
- * 8-server返回在线用户列表
- * 9-client私聊发送信息
- * 10-client私聊发送文件
- * 11-server转发私聊文件
- * @author zhupengcheng
- */
1.1.2 User类
- public class User implements Serializable {
-
- /**
- * 用户名
- */
- private String username;
- /**
- * 密码
- */
- private String password;
- /**
- * 头像
- */
- private String image;
- ......
- get
- set
- toString
- }
这个类代表用户实体,主要用在登录、注册流程中,及其登录、注册业务跳转到通信业务中,我需要传递一个user对象,记录这个用户(客户端)的一些信息。
1.2客户端部分
1.1.1 登录部分
- /**
- * 登录点击事件
- */
- login_btn.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- String username = username_edit.getText().trim();
- String password = new String(password_edit.getPassword());
- if (username == null || password == null) {
- JOptionPane.showMessageDialog(null,"用户名、密码不许为空!", "用户登录", JOptionPane.INFORMATION_MESSAGE);
- }
- final int recv = RedisUtils.validUser(username, password);
- if (recv == 0) {
- JOptionPane.showMessageDialog(null,"用户不存在,请先注册!", "用户登录", JOptionPane.INFORMATION_MESSAGE);
- return;
- }
- if (recv == 1) {
- JOptionPane.showMessageDialog(null,"密码错误!", "用户登录", JOptionPane.INFORMATION_MESSAGE);
- return;
- }
- if (RedisUtils.isAlive(username)) {
- JOptionPane.showMessageDialog(null,"你已经登录了,不能重复登录!", "用户登录", JOptionPane.INFORMATION_MESSAGE);
- return;
- }
- JOptionPane.showMessageDialog(null,"登录成功!", "用户登录", JOptionPane.INFORMATION_MESSAGE);
- final User user = new User();
- user.setUsername(username);
- user.setUsername(password);
- user.setImage(ToolUtils.getProfile());
- ProfileFrame.launch(user);
- dispose();
- }
- });
首先,判断输入的用户名、密码是否为空,如果为空,给出提示;如果不为空,再判断用户名是否存在于数据库中,没有不存在于数据库中说明没有注册,给出提示;如果存在,再判断密码与数据库中存储的密码是否一致,如果不一致,给出提示,如果一致,再判断用户是否已经登录了,如果用户在线那么数据库缓存列表有记录,如果已经登录成功了,给出提示,防止重复登录,如果没有,那么登录,跳转到聊天室页面。
1.1.2 修改密码部分
- /**
- * 修改密码点击事件
- */
- modify_btn.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- String username = username_edit.getText().trim();
- String password = new String(password_edit.getPassword());
- if (username == null || password == null) {
- JOptionPane.showMessageDialog(null,"用户名、密码不许为空!", "修改密码", JOptionPane.INFORMATION_MESSAGE);
- }
- final int recv = RedisUtils.validUser(username, password);
- if (recv == 0) {
- JOptionPane.showMessageDialog(null,"用户不存在,请先注册!", "修改密码", JOptionPane.INFORMATION_MESSAGE);
- return;
- }
- if (recv == 1) {
- JOptionPane.showMessageDialog(null,"密码错误!", "修改密码", JOptionPane.INFORMATION_MESSAGE);
- return;
- }
- final String newPassword = (String) JOptionPane.showInputDialog(null, "请输入新密码", "修改密码", JOptionPane.INFORMATION_MESSAGE, null, null, null);
- System.out.println(newPassword);
- if (StringUtils.isBlank(newPassword)) {
- JOptionPane.showMessageDialog(null,"新密码不许为空!", "修改密码", JOptionPane.INFORMATION_MESSAGE);
- return;
- }
- RedisUtils.modifyUser(username, newPassword);
- }
- });
首先,判断输入的用户名、密码是否为空,如果为空,给出提示;如果不为空,判断用户名是否存在于数据库中,如果不存在,给出注册的都提示;如果存在,继续判断密码,如果与数据库中存储的密码不一致,给出提示;如果一致,给出输入对话框,读取用户输入的新密码,这里继续判断新密码的合理性,如果为空,给出提示;如果和旧密码一样,给出提示;如果全是空格,给出提示;符合要求后存储到数据库中。
1.1.3 注册部分
- /**
- * 注册点击事件
- */
- register_btn.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- String username = username_edit.getText().trim();
- String password = new String(password_edit.getPassword());
- if (username == null || password == null) {
- JOptionPane.showMessageDialog(null,"用户名、密码不许为空!", "注册", JOptionPane.INFORMATION_MESSAGE);
- }
- final int recv = RedisUtils.insertUser(username, password);
- if (recv == 0) {
- JOptionPane.showMessageDialog(null,"用户已存在,请直接登录!", "注册", JOptionPane.INFORMATION_MESSAGE);
- }
- if (recv == 1) {
- JOptionPane.showMessageDialog(null,"注册失败!", "注册", JOptionPane.INFORMATION_MESSAGE);
- }
- if (recv == 2 ) {
- JOptionPane.showMessageDialog(null,"注册成功!", "注册", JOptionPane.INFORMATION_MESSAGE);
- }
- }
- });
这里首先判断用户输入的用户名、密码是否为空,如果为空,给出提示;然后判断用户名是否已经存在于数据库中,如果已经存在,给出可以直接登录的提示;这里面新用户的注册细节全部由自己封装的redis数据库工具类完成,只需要保存调用函数返回的结果进行判断即可。
1.3工具类部分
1.1.1 RedisUtils工具类
- public class RedisUtils {
- /**
- * redis操作实体
- */
- private static Jedis jedis;
- /**
- * 远程服务器ip地址
- */
- private static final String HOST = "101.37.32.165";
- /**
- * 操作正确的返回值
- */
- private static final String OK = "OK";
- /**
- * 存储在线用户的集合
- */
- private static String ALIVEUSERS = "aliveUsers";
-
- /**
- * 建立连接
- */
- public static Jedis init() {
- jedis = new Jedis(HOST);
- jedis.select(1);
- ToolUtils.getLogger().info(ToolUtils.getRealTime() + "\t" + jedis.ping());
- return jedis;
- }
-
- /**
- * 验证用户
- * @param username 用户名
- * @return 0-用户不存在,1-用户存在,密码错误,2-用户存在,密码正确
- */
- public static int validUser(String username, String password) {
- if (jedis == null) {
- RedisUtils.init();
- }
- if (! isExists(username)) {
- return 0;
- }
- String recv = jedis.get(username);
- if (! Objects.equals(recv,password)) {
- return 1;
- }
- return 2;
- }
-
- /**
- * 验证用户名是否存在
- * @param username 用户名
- * @return
- */
- private static boolean isExists(String username) {
- if (jedis == null) {
- RedisUtils.init();
- }
- if (username == null) {
- return false;
- }
- return jedis.exists(username);
- }
-
- /**
- * 注册用户
- * @param username 用户名
- * @param password 密码
- * @return 0-用户已存在,1-注册失败,2-注册成功
- */
- public static int insertUser(String username, String password) {
- if (jedis == null) {
- RedisUtils.init();
- }
- if (isExists(username)) {
- return 0;
- }
- final String recv = jedis.set(username, password);
- if (OK.equals(recv)) {
- return 2;
- }
- return 1;
- }
-
- /**
- * 修改密码
- * @param username 用户名
- * @param password 老密码
- * @return
- */
- public static void modifyUser(String username, String password) {
- if (jedis == null) {
- RedisUtils.init();
- }
- jedis.set(username, password);
- }
-
- /**
- * 关闭连接
- */
- public static void close() {
- if (jedis == null) {
- return;
- }
- if (jedis.isConnected()) {
- jedis.close();
- }
- }
-
-
- /**
- * 增添在线用户缓存
- * @param username 用户名
- */
- public static long addAliveUser(String username) {
- if (jedis == null) {
- RedisUtils.init();
- }
- jedis.select(1);
- return jedis.sadd(ALIVEUSERS, username);
- }
-
- /**
- * 删除在线用户缓存
- * @param username 用户名
- */
- public static long delAliveUser(String username) {
- if (jedis == null) {
- RedisUtils.init();
- }
- jedis.select(1);
- return jedis.srem(ALIVEUSERS, username);
- }
-
- /**
- * 获得在线用户缓存
- * @return 缓存集合
- */
- public static Set getAliveUsers() {
- if (jedis == null) {
- RedisUtils.init();
- }
- jedis.select(1);
- return jedis.smembers(ALIVEUSERS);
- }
-
- /**
- * 验证用户是否已登录
- * @param username 用户名
- * @return true-用户已经登录,false-用户未登录
- */
- public static boolean isAlive(String username) {
- if (jedis == null) {
- RedisUtils.init();
- }
- return jedis.sismember(ALIVEUSERS, username);
- }
- }
这个工具类完成了与数据库交互的全部操作,包括用户账号的验证、保存、修改,在线用户信息的缓存,防止重复登录的验证工作等,类似于web业务中的DAO层,封装与数据库的交互。
1.1.2 SwingUtils工具类:
- public class SwingUtils {
-
- /**
- * 设置大小为电脑屏幕的指定比例
- * @param jFrame 窗口
- * @param w 电脑屏幕宽度的w倍,w<=1
- * @param h 电脑屏幕高度的h倍,h<=1
- */
- public static void setSize(JFrame jFrame, double w, double h){
- int width = (int) (getScreenWidth() * w);
- int height = (int) (getScreenHeight() * h);
- jFrame.setSize(width, height);
- }
-
- /**
- * 获得电脑屏幕宽度
- * @return
- */
- public static double getScreenWidth() {
- return Toolkit.getDefaultToolkit().getScreenSize().getWidth();
- }
-
- /**
- * 获得电脑屏幕宽度
- * @return
- */
- public static double getScreenHeight() {
- return Toolkit.getDefaultToolkit().getScreenSize().getHeight();
- }
-
- /**
- * 按照width、height比例调整图标大小
- * @param imageIcon 要调整的图标
- * @param width 调整后的图像宽度
- * @param height 调整后的图像高度
- * @return
- */
- public static ImageIcon scaleImageIcon(ImageIcon imageIcon, double width, double height) {
- imageIcon.setImage(imageIcon.getImage().getScaledInstance((int)width, (int)height, Image.SCALE_SMOOTH));
- return imageIcon;
- }
- }
这个类我在swing控件注册时候用的比较多,因为重复性地使用一些功能,我就索性把重复工作封装成了一个工具类,方便直接调用。主要就是图片大小自适应调整为父控件的大小,将界面大小调整为电脑屏幕的一定比例。总体来说,这部分不太重要。
1.1.3 ToolUtils工具类
这里面封装的方法比较多,但是大同小异,我就只把文件读取、写入这部分拿出来说一下把,其他关于时间的一些方法都是类似的。
- /**
- * 编码文件成二进制流
- * @param file 文件对象
- * @return 文件字节流
- * @throws FileNotFoundException
- */
- public static byte[] codeFile(File file) throws IOException {
- try (final FileInputStream fileInputStream = new FileInputStream(file))
- {
- return fileInputStream.readAllBytes();
- }
- }
-
- /**
- * 解码二进制成文件,并写入内容
- * @param content 文件内容
- * @param absolutePath 绝对路径
- * @return
- */
- public static void decodeFile(String content, String absolutePath) throws IOException {
- final File file = Path.of(absolutePath).toFile();
- if (! file.exists()) {
- file.createNewFile();
- }
- try (final FileOutputStream fileOutputStream = new FileOutputStream(file,true))
- {
- fileOutputStream.write(content.getBytes());
- fileOutputStream.flush();
- }
- }
在通信时候难免会发送文件,我就把里面的细节封装成了方法,第一个是解码文件成二进制流,方便直接socket传输。用的就是FileInputStream字节流,没有用封装后的缓冲字符流。第二个是解码,就是根据路径生成文件,并且将字符串写入文件中。在项目中经过尝试,发现用绝对路径稳妥些,如果用相对路径的话我这边是默认保存在D盘下的指定文件的。两个函数都用了try-with-resource,为没有用try-catch,省去了释放资源这一步。
1.4服务器部分
这一部分算是最复杂的了,我将这个代码分割成几块来单独说明,比较清楚。
1.1.1 静态内部类(线程类)
- /**
- * 内部静态类,负责与单独的客户端线程的通信工作
- */
- private static class SingleUser extends Thread {
- /**
- * 服务端与客户端对应的套接字
- */
- private Socket socket;
- /**
- * 缓冲字符输入流
- */
- private BufferedReader bufferedReader;
- /**
- * 文本输出流
- */
- private PrintWriter printWriter;
- /**
- * 用户名
- */
- private String id;
-
- public SingleUser(Socket socket) {
- this.socket = socket;
- // 在线人数+1
- userCount.incrementAndGet();
- }
-
- @Override
- public void run() {
- try {
- printWriter = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);
- bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
- String recv = null;
- while (socket.isConnected()) {
- while ( (recv = bufferedReader.readLine()) != null ) {
- final JSONObject jsonObject = JSON.parseObject(recv);
- final int type = jsonObject.getIntValue("type");
- // 客户端、服务器第一次握手
- if (type == 0) {
- final String username = jsonObject.getString("source");
- id = username;
- System.out.println("-----------------------");
- System.out.println(id);
- userMap.put(username, printWriter);
- // 添加在线用户进缓存
- RedisUtils.addAliveUser(id);
- textArea.append(ToolUtils.getRealTime() + ",用户" + username + "连接成功\n");
- // 回复
- final Info info = new Info(6);
- info.setContent(ToolUtils.getRealTime() + ",用户" + username + "连接成功");
- printWriter.println(JSON.toJSONString(info));
- }
- // 客户端请求广播消息
- if (type == 1) {
- final String content = jsonObject.getString("content");
- final String username = jsonObject.getString("source");
- broadcast(content, username);
- textArea.append(ToolUtils.getRealTime() + ",用户" + username + "广播了消息\n");
- }
- // 客户端上传文件
- if (type == 3) {
- final String content = jsonObject.getString("content");
- final String username = jsonObject.getString("source");
- final String fileName = (String) jsonObject.get("fileName");
- // 服务器保存文件制定路径
- String path = rootPath + "files\\" + username + "\\" + ToolUtils.getRealTime("YY_MM_dd_HH_mm_ss") + "_" + fileName;
- System.out.println(path);
- ToolUtils.decodeFile(content, path);
- textArea.append(ToolUtils.getRealTime() + ",用户" + username + "上传了文件\n");
- // 回复
- final Info info = new Info(6);
- info.setContent(ToolUtils.getRealTime() + ",用户" + username + "上传了文件" + fileName);
- printWriter.println(JSON.toJSONString(info));
- }
- // 客户端查询在线人数
- if (type == 4) {
- // 回复
- final Info info = new Info(5);
- info.setContent(String.valueOf(userCount.get()));
- printWriter.println(JSON.toJSONString(info));
- }
- // 客户端请求转发私聊信息
- if (type == 9) {
- final String content = jsonObject.getString("content");
- final String dest = jsonObject.getString("dest");
- final String source = jsonObject.getString("source");
- transferData(source, content, dest);
- }
- // 客户端私聊文件
- if (type == 10) {
- final String content = jsonObject.getString("content");
- final String dest = jsonObject.getString("dest");
- final String source = jsonObject.getString("source");
- final String fileName = (String) jsonObject.get("fileName");
- transferDoc(source, content, dest, fileName);
- }
- }
- }
- } catch (IOException e) {
- // 不给出提示框,直接结束方便finally直接执行
- return;
- } finally {
- // 删除用户缓存
- userMap.remove(id);
- RedisUtils.delAliveUser(id);
- // 在线人数-1
- userCount.decrementAndGet();
- }
- }
- }
我选用了多线程技术,这里服务器监听到一个客户端连接请求后,实例化一个线程类负责和这个客户端的通信,这个静态内部类就是这个线程类,首先,定义了socket字段、输入流、输出流字段,这些都是socket通信基本具备的,然后就是id,当客户端和服务器建立连接后,客户端会发起第一次握手,将自己的用户名发送给服务器,然后该线程类对象就将用户名赋值给id字段,表示对应客户端的唯一标识,方便后续管理。可以看到run方法里有很多对于type字段的判断,这里就是根据我之前定义的Info报文类的type字段,来是被这个报文是什么类型的,即具体要我做什么工作,然后直接去执行对应工作即可。
1.1.2 群聊部分
- // 客户端请求广播消息
- if (type == 1) {
- final String content = jsonObject.getString("content");
- final String username = jsonObject.getString("source");
- broadcast(content, username);
- textArea.append(ToolUtils.getRealTime() + ",用户" + username + "广播了消息\n");
- }
这里服务器首先判断type字段,如果是1,表示客户端请求服务器转发群聊的内容,也就是通过服务器来广播消息。
实现的细节都封装成了broadcast方法,具体如下:
- /**
- * 服务器转发客户端广播消息
- * @param sendData 广播内容
- * @param sender 发送者
- */
- private static void broadcast(String sendData, String sender) {
- final Collection values = userMap.values();
- for (Object value : values) {
- PrintWriter printWriter = (PrintWriter) value;
- // 回复
- final Info info = new Info(6);
- info.setContent(ToolUtils.getRealTime() + ",用户" + sender + ",广播了消息:" + sendData);
- info.setSource(sender);
- printWriter.println(JSON.toJSONString(info));
- }
- }
Broadcast方法需要两个参数,一个是群聊的内容,一个是发送者,即消息来源者。这里有一个userMap,这是一个key-value键值对,key是用户名,值是用户名对应的socket输出流,这里的用户是在线用户(已登录),通过遍历集合,依次发送给每个用户。
1.1.3 私聊部分
- // 客户端请求转发私聊信息
- if (type == 9) {
- final String content = jsonObject.getString("content");
- final String dest = jsonObject.getString("dest");
- final String source = jsonObject.getString("source");
- transferData(source, content, dest);
- }
依然是先判断type字段(报文类型),明白了这个请求报文要让我做什么,然后执行相应操作,这里面转发客户端的私聊信息同样是封装在一个方法中,具体方法代码如下:
- /**
- * 用户私聊发送数据
- * @param sendData 私聊内容
- * @param dest 私聊用户名
- * @param username 发送用户名
- */
- private static void transferData(String username, String sendData, String dest) {
- final PrintWriter sourceWriter = (PrintWriter) userMap.get(username);
- final PrintWriter destWriter = (PrintWriter) userMap.get(dest);
- // 私聊对象已下线
- if (destWriter == null) {
- // 私聊发起人在线
- if (sourceWriter != null) {
- final Info info = new Info(6);
- info.setContent(ToolUtils.getRealTime() + ",抱歉,用户" + dest + ",已下线,请稍候重发");
- sourceWriter.println(JSON.toJSONString(info));
- textArea.append(ToolUtils.getRealTime() + ",用户" + username + "和" + dest + "私聊失败\n");
- }
- }
- // 私聊对象在线
- else {
- final Info info = new Info(6);
- info.setContent(ToolUtils.getRealTime() + ",用户" + username + ",私聊你如下内容:" + sendData);
- destWriter.println(JSON.toJSONString(info));
- textArea.append(ToolUtils.getRealTime() + ",用户" + username + "和" + dest + "私聊成功\n");
- // 私聊发起人在线
- if (sourceWriter != null) {
- final Info info1 = new Info(6);
- info1.setContent(ToolUtils.getRealTime() + ",你刚刚私聊给用户" + dest + "已发送成功");
- sourceWriter.println(JSON.toJSONString(info1));
- }
- }
- }
根据私聊对象是否在线,分成两个逻辑。如果私聊的对象恰好刚刚下线,这时候服务器的在线用户缓存中没有了该用户,同时,私聊发起人还在线,这时服务器就通知私聊发起人你发送的消息转发失败,私聊对象已经下线了。相反,如果他们都在线,服务器就转发消息给私聊发起人,并且告知私聊发起者你发送的消息已经成功发送出去了。
如果用户想要私聊发送文件呢?
具体看下面:
私聊发送文件:
- // 客户端私聊发送文件
- if (type == 10) {
- final String content = jsonObject.getString("content");
- final String dest = jsonObject.getString("dest");
- final String source = jsonObject.getString("source");
- final String fileName = (String) jsonObject.get("fileName");
- transferDoc(source, content, dest, fileName);
- }
对应的字段规定是0,同样,操作细节封装在transDoc方法中,该方法代码如下:
- /**
- * 用户私聊发送文件
- * @param source 发送用户名
- * @param fileContent 文件内容
- * @param dest 私聊用户名
- * @param fileName 文件名
- */
- private static void transferDoc(String source, String fileContent, String dest, String fileName) {
- final PrintWriter destWriter = (PrintWriter) userMap.get(dest);
- final PrintWriter sourceWriter = (PrintWriter) userMap.get(source);
- // 私聊对象已下线
- if (destWriter == null) {
- // 私聊发起人在线
- if (sourceWriter != null) {
- final Info info1 = new Info(6);
- info1.setContent(ToolUtils.getRealTime() + ",你私聊发送文件失败,用户" + dest + "已下线");
- sourceWriter.println(JSON.toJSONString(info1));
- textArea.append(ToolUtils.getRealTime() + ",用户" + source + "和" + dest + "私聊文件失败\n");
- }
- }
- // 私聊对象在线
- else {
- final Info info = new Info(11);
- info.setDest(dest);
- info.setContent(fileContent);
- info.setSource(source);
- info.setFileName(fileName);
- destWriter.println(JSON.toJSONString(info));
- // 私聊发起人在线
- if (sourceWriter != null) {
- final Info info1 = new Info(6);
- info1.setContent(ToolUtils.getRealTime() + ",你私聊发送文件给用户" + dest + "成功");
- sourceWriter.println(JSON.toJSONString(info1));
- textArea.append(ToolUtils.getRealTime() + ",用户" + source + "和" + dest + "私聊文件成功\n");
- }
- }
- }
和私聊发送数据一个逻辑,根据私聊对象是否在线分成两个逻辑,首先如果私聊对象不在线,那么文件发送失败,服务器告诉发起者(前提是发起者在线),如果私聊对象在线,同时发起者也在线,那么告知发起者你发送成功了。其实,这里面有一点不合理,因为只有接收者完成接收到文件,并且发出一个反馈报文,发起者如果收到这个报文,表示文件发送成功了,因为时间所限,后面如果可以的话可以继续改进。
如果接收到接收到了文件,那么他怎么处理?具体处理逻辑如下:
- // 服务器转发私聊文件
- if (type == 11) {
- final String source = jsonObject.getString("source");
- final String fileContent = jsonObject.getString("content");
- final String fileName = jsonObject.getString("fileName");
- ToolUtils.decodeFile(fileContent, FileSystemView.getFileSystemView().getHomeDirectory() + "\\" + ToolUtils.getRealTimeByHour("HH_mm_ss") + "_" + source + "_" + fileName);
- System.out.println("接收" + source + "私聊文件" + fileContent);
- }
如果报文type字段是11,表示这是服务器转发的私聊文件,也就是有别人私发文件给你,这时候你需要从json格式中提取出各个字段,比如文件数据,文件名等。根据这些参数,调用之前介绍的工具类方法decodeFile创建对应的文件,并且把数据写入文件中。由于该方法在前面介绍过,这里便省略了。我设置的默认保存路径是该客户端的桌面。
具体介绍了自己定义的主要几个报文,其实在整个通信过程中还有一些请求报文,比如
客户端查询服务器当前聊天室在线人数,客户端查询当前在线的用户列表,客户端获取这些数据后可以实时地显示在聊天室界面上,方便用户的操作,这几部分所占代码较少。
这里,我再介绍我的私聊部分中关于选择私聊对象的处理逻辑,具体如下:
- /**
- * 选择私聊用户点击事件
- */
- list.addListSelectionListener(new ListSelectionListener() {
- @Override
- public void valueChanged(ListSelectionEvent e) {
- final Object value = list.getSelectedValue();
- if (value != null) {
- final int recv = JOptionPane.showConfirmDialog(null, user.getUsername() + ",你要选择" + value + "为私聊对象吗", "选择好友私聊", JOptionPane.YES_NO_OPTION);
- // 选择成功
- if (recv == 0) {
- if (user.getUsername().equals(value)) {
- JOptionPane.showMessageDialog(null, "不能选择你自己", "选择私聊对象", JOptionPane.WARNING_MESSAGE);
- return;
- }
- secretUser = (String) value;
- secret_edit.setText(secretUser);
- list.setEnabled(false);
- }
- }
- }
- });
这里面用到的控件是JList列表,会实时显示当前系统用户,当数量过多时候,通过下拉框下拉即可。上面的代码是定义了列表区域的监听事件,当列表区域中的选择项(在线用户名)改变时,会询问客户端是否选择该用户为当前私聊对象,如果确认,就把私聊的用户名保存在变量secret_user中,后面私聊时候根据该用户名通过服务器转发到指定的用户。
客户端查询当前系统在线人数:
- /**
- * 定时执行,查询在线人数
- */
- final ActionListener actionListener = new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- if (printWriter != null) {
- // 空报头即可
- final Info info = new Info(4);
- printWriter.println(JSON.toJSONString(info));
- }
- count_edit.setText(String.valueOf(counts.get()));
- }
- };
- 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没有接收到他们之间的私聊内容
总结
参考文献